diff --git a/app.js b/app.js index dd5c370..3c4d93d 100644 --- a/app.js +++ b/app.js @@ -1557,6 +1557,117 @@ async function importCSVRows(rows) { return results; } +// ── References CSV upload ────────────────────────────────── +const refsCsvFileInput = $('refs-csv-file-input'); + +$('btn-upload-refs-csv').addEventListener('click', () => { + bulkEditPopover.hidden = true; + openUploadRefsCsvModal(); +}); + +refsCsvFileInput.addEventListener('change', () => { + $('btn-refs-csv-import').disabled = !refsCsvFileInput.files.length; + $('refs-csv-results').hidden = true; + $('refs-csv-results').innerHTML = ''; +}); + +$('btn-refs-csv-cancel').addEventListener('click', () => $('upload-refs-csv-modal').close()); + +$('btn-refs-csv-import').addEventListener('click', async () => { + const file = refsCsvFileInput.files[0]; + if (!file) return; + $('btn-refs-csv-import').disabled = true; + $('btn-refs-csv-cancel').textContent = 'Close'; + const resultsEl = $('refs-csv-results'); + resultsEl.hidden = false; + resultsEl.innerHTML = '
Importing…
'; + try { + const text = await file.text(); + const rows = parseCSV(text); + if (!rows.length) { resultsEl.innerHTML = 'No data rows found.
'; return; } + const results = await importRefsCsvRows(rows); + await loadAll(); + let html = `✓ ${results.created} reference${results.created !== 1 ? 's' : ''} imported.
`; + if (results.warnings.length) { + html += `${results.warnings.length} skipped — tune not found:
${results.errors.length} error${results.errors.length !== 1 ? 's' : ''}:
Failed: ${esc(e.message)}
`; + $('btn-refs-csv-import').disabled = false; + } +}); + +function openUploadRefsCsvModal() { + refsCsvFileInput.value = ''; + $('btn-refs-csv-import').disabled = true; + $('btn-refs-csv-cancel').textContent = 'Cancel'; + $('refs-csv-results').hidden = true; + $('refs-csv-results').innerHTML = ''; + $('refs-csv-format-details').open = true; + $('upload-refs-csv-modal').showModal(); +} + +async function importRefsCsvRows(rows) { + const siteCache = new Map(); + const musCache = new Map(); + const results = { created: 0, warnings: [], errors: [] }; + + async function resolve(endpoint, list, cache, name) { + if (!name) return null; + const lower = name.toLowerCase(); + if (cache.has(lower)) return cache.get(lower); + const existing = list.find(x => x.name.toLowerCase() === lower); + if (existing) { cache.set(lower, existing.id); return existing.id; } + const item = await apiFetch(endpoint, { method: 'POST', body: JSON.stringify({ name }) }); + list.push(item); + cache.set(lower, item.id); + return item.id; + } + + for (const row of rows) { + const tuneName = row.tune_name?.trim(); + if (!tuneName) continue; + + const tune = allTunes.find(t => (t.name ?? '').toLowerCase() === tuneName.toLowerCase()); + if (!tune) { + results.warnings.push(tuneName); + continue; + } + + try { + const link = row.link?.trim() || null; + const siteId = await resolve('/reference_sites/', allReferenceSites, siteCache, row.site?.trim() || null); + + const musicians = []; + for (const key of ['musician1', 'musician2', 'musician3', 'musician4']) { + const name = row[key]?.trim(); + if (!name) continue; + const id = await resolve('/musicians/', allMusicians, musCache, name); + if (id) musicians.push({ id }); + } + + if (!link && !siteId && musicians.length === 0) continue; + + await apiFetch(`/tunes/${tune.id}/references`, { + method: 'POST', + body: JSON.stringify({ link, site_id: siteId, musicians }), + }); + results.created++; + } catch (e) { + results.errors.push({ name: tuneName, error: e.message }); + } + } + return results; +} + // ── Mobile truncation tooltip ─────────────────────────────────── { const tip = $('mob-tooltip'); diff --git a/index.html b/index.html index 503ed50..8a0bbc3 100644 --- a/index.html +++ b/index.html @@ -51,7 +51,8 @@