[feat] bulk import allows date, and quantity is split into multiple entries
This commit is contained in:
@@ -144,15 +144,19 @@ const catalogs = catalogRows
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="small text-secondary mb-3">
|
||||
<p class="small text-secondary mb-2">
|
||||
Upload a CSV exported from Collectr, TCGPlayer, or any marketplace. Columns: <code>name, set, condition, qty, price, market</code>.
|
||||
</p>
|
||||
<p class="small text-secondary mb-3">
|
||||
Optionally add a <code>date</code> column (e.g. <code>2026-07-16</code>) to record when each card was acquired — it’s used as the entry’s added/purchase date. Rows without a date default to today.
|
||||
</p>
|
||||
<label class="form-label small text-secondary text-uppercase fw-semibold" for="csvFileInput">Choose File</label>
|
||||
<input id="csvFileInput" type="file" accept=".csv" class="form-control bg-dark-subtle text-light border-secondary" />
|
||||
<div id="csvPreview" class="mt-3 d-none">
|
||||
<p class="small text-secondary fw-semibold mb-1">Preview</p>
|
||||
<div class="border border-secondary rounded p-2 small text-secondary" id="csvPreviewContent">—</div>
|
||||
<div class="border border-secondary rounded p-2 small text-secondary overflow-auto" id="csvPreviewContent" style="max-height: 12rem;">—</div>
|
||||
</div>
|
||||
<div id="csvImportResult" class="mt-3 d-none"></div>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary">
|
||||
<button type="button" class="btn btn-outline-danger" data-bs-dismiss="modal">Cancel</button>
|
||||
@@ -182,6 +186,85 @@ const catalogs = catalogRows
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Bulk CSV import ──────────────────────────────────────────────────────
|
||||
(function () {
|
||||
const importModal = document.getElementById('bulkImportModal');
|
||||
const fileInput = document.getElementById('csvFileInput');
|
||||
const uploadBtn = document.getElementById('csvUploadBtn');
|
||||
const preview = document.getElementById('csvPreview');
|
||||
const previewContent = document.getElementById('csvPreviewContent');
|
||||
const result = document.getElementById('csvImportResult');
|
||||
if (!importModal || !fileInput || !uploadBtn) return;
|
||||
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
|
||||
let selectedFile = null;
|
||||
let didImport = false;
|
||||
|
||||
fileInput.addEventListener('change', () => {
|
||||
selectedFile = fileInput.files && fileInput.files[0] ? fileInput.files[0] : null;
|
||||
result.classList.add('d-none');
|
||||
if (!selectedFile) { preview.classList.add('d-none'); return; }
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const lines = String(reader.result || '').split(/\r?\n/).filter((l) => l.trim()).slice(0, 6);
|
||||
previewContent.innerHTML = lines
|
||||
.map((l, i) => `<div class="${i === 0 ? 'text-light fw-semibold' : ''}">${esc(l)}</div>`)
|
||||
.join('');
|
||||
preview.classList.remove('d-none');
|
||||
};
|
||||
reader.readAsText(selectedFile);
|
||||
});
|
||||
|
||||
uploadBtn.addEventListener('click', async () => {
|
||||
if (!selectedFile) { alert('Choose a CSV file first.'); return; }
|
||||
const original = uploadBtn.textContent;
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = 'Importing…';
|
||||
result.classList.add('d-none');
|
||||
try {
|
||||
const body = new FormData();
|
||||
body.append('action', 'import');
|
||||
body.append('file', selectedFile);
|
||||
const res = await fetch('/api/inventory', { method: 'POST', body });
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
result.className = 'mt-3 alert alert-danger py-2 small';
|
||||
result.textContent = data.error || 'Import failed.';
|
||||
result.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
didImport = data.imported > 0;
|
||||
const skipped = data.skipped || [];
|
||||
let html = `<div class="fw-semibold ${data.imported > 0 ? 'text-success' : ''}">Imported ${data.imported} card${data.imported === 1 ? '' : 's'}.</div>`;
|
||||
if (skipped.length) {
|
||||
html += `<div class="mt-1 text-warning fw-semibold">Skipped ${skipped.length}:</div>`;
|
||||
html += '<ul class="mb-0 ps-3">' + skipped
|
||||
.map((s) => `<li>Row ${s.row}${s.name ? ' (' + esc(s.name) + ')' : ''}: ${esc(s.reason)}</li>`)
|
||||
.join('') + '</ul>';
|
||||
}
|
||||
result.className = 'mt-3 alert alert-dark py-2 small overflow-auto';
|
||||
result.style.maxHeight = '14rem';
|
||||
result.innerHTML = html;
|
||||
result.classList.remove('d-none');
|
||||
uploadBtn.textContent = didImport ? 'Done' : 'Upload';
|
||||
} catch (e) {
|
||||
result.className = 'mt-3 alert alert-danger py-2 small';
|
||||
result.textContent = 'Import failed.';
|
||||
result.classList.remove('d-none');
|
||||
} finally {
|
||||
uploadBtn.disabled = false;
|
||||
if (uploadBtn.textContent === 'Importing…') uploadBtn.textContent = original;
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh the dashboard (grid, catalogs, totals) after a successful import.
|
||||
importModal.addEventListener('hidden.bs.modal', () => {
|
||||
if (didImport) window.location.reload();
|
||||
});
|
||||
})();
|
||||
|
||||
(function () {
|
||||
const modal = document.getElementById('inventoryModal');
|
||||
if (!modal) return;
|
||||
|
||||
Reference in New Issue
Block a user