/** * client-assets/js/soil-import.js * * Lab file import flow for the soil test entry page. * * Flow: * 1. User selects lab + file → "Analyse" button enabled * 2. File sent to soilImportController.php (action=parse) * 3. Samples returned → shown in confirmation table * - User can edit sample_id (paddock) inline * - User can uncheck individual rows to skip them * 4a. "Import All" / "Import Selected" → action=import_bulk → records saved to DB * 4b. For single sample: "Fill Form" → action=import_one → pre-fills the form below */ (function () { 'use strict'; // ── DOM refs ────────────────────────────────────────────────────────────── const labSelect = document.getElementById('lab-select'); const fileInput = document.getElementById('lab-file-input'); const analyseBtn = document.getElementById('lab-analyse-btn'); const statusEl = document.getElementById('import-status'); const progressEl = document.getElementById('import-progress'); const bulkPanel = document.getElementById('bulk-import-panel'); const bulkTbody = document.getElementById('bulk-import-tbody'); const bulkImportBtn= document.getElementById('bulk-import-btn'); const bulkAllChk = document.getElementById('bulk-select-all'); if (!fileInput || !analyseBtn) return; let parsedSamples = []; // raw from server let detectedLab = ''; // ── Helpers ─────────────────────────────────────────────────────────────── function showStatus(msg, type = 'info') { if (!statusEl) return; statusEl.className = `alert alert-${type} mt-2`; statusEl.innerHTML = msg; statusEl.hidden = false; } function hideStatus() { if (statusEl) statusEl.hidden = true; } function setProgress(on, text = 'Processing…') { if (!progressEl) return; progressEl.hidden = !on; const lbl = progressEl.querySelector('.progress-label'); if (lbl) lbl.textContent = text; } function buildFormData(extra = {}) { const fd = new FormData(); fd.append('file', fileInput.files[0]); fd.append('lab', labSelect ? labSelect.value : 'auto'); for (const [k, v] of Object.entries(extra)) fd.append(k, v); return fd; } async function post(fd) { const r = await fetch('/controllers/soilImportController.php', { method: 'POST', body: fd }); if (!r.ok) throw new Error(`Server error ${r.status}`); return r.json(); } // ── Enable Analyse button when file chosen ──────────────────────────────── fileInput.addEventListener('change', () => { analyseBtn.disabled = !fileInput.files.length; hideStatus(); if (bulkPanel) bulkPanel.hidden = true; }); // ── Step 1: Analyse ─────────────────────────────────────────────────────── analyseBtn.addEventListener('click', async () => { if (!fileInput.files.length) return; analyseBtn.disabled = true; setProgress(true, 'Reading file…'); hideStatus(); if (bulkPanel) bulkPanel.hidden = true; try { const data = await post(buildFormData({ action: 'parse' })); if (!data.success) { showStatus('Parse error: ' + escHtml(data.error), 'danger'); return; } parsedSamples = data.samples; detectedLab = data.lab; if (labSelect && labSelect.value === 'auto' && detectedLab) { // Update the dropdown to reflect auto-detected lab for (const opt of labSelect.options) { if (opt.value === detectedLab) { opt.selected = true; break; } } } if (data.count === 1) { // Single sample — offer both "fill form" and "bulk import" showStatus( `1 sample found (${escHtml(parsedSamples[0]?.lab_no || 'unknown')}). ` + `Review below then import.`, 'info' ); } else { showStatus( `${data.count} samples found in this file. ` + `Review paddock IDs below, then import all or select individual rows.`, 'info' ); } renderBulkTable(data.samples); } catch (err) { showStatus('Error: ' + escHtml(err.message), 'danger'); } finally { setProgress(false); analyseBtn.disabled = false; } }); // ── Step 2: Render confirmation table ───────────────────────────────────── function renderBulkTable(samples) { if (!bulkPanel || !bulkTbody) return; bulkTbody.innerHTML = ''; samples.forEach((s, idx) => { const tr = document.createElement('tr'); tr.dataset.idx = idx; tr.innerHTML = ` ${escHtml(s.lab_no)} ${escHtml(s.client)} ${escHtml(s.crop)} `; bulkTbody.appendChild(tr); }); // Wire "fill form" buttons bulkTbody.querySelectorAll('.fill-form-btn').forEach(btn => { btn.addEventListener('click', () => fillForm(parseInt(btn.dataset.idx))); }); bulkPanel.hidden = false; bulkPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } // ── Select-all checkbox ─────────────────────────────────────────────────── if (bulkAllChk) { bulkAllChk.addEventListener('change', () => { document.querySelectorAll('.row-check').forEach(cb => { cb.checked = bulkAllChk.checked; }); }); } // ── Step 3a: Bulk import ────────────────────────────────────────────────── if (bulkImportBtn) { bulkImportBtn.addEventListener('click', async () => { const clientId = document.querySelector('[name="client_id"]')?.value || ''; if (!clientId || clientId === 'new') { showStatus('Please select a client before importing.', 'warning'); document.querySelector('[name="client_id"]')?.scrollIntoView({ behavior: 'smooth' }); return; } // Collect checked rows with updated paddock IDs const toImport = []; bulkTbody.querySelectorAll('tr').forEach(tr => { const chk = tr.querySelector('.row-check'); if (!chk?.checked) return; const idx = parseInt(tr.dataset.idx); const paddock = tr.querySelector('.paddock-input')?.value.trim() || ''; const sample = { ...parsedSamples[idx] }; sample.sample_id = paddock || sample.sample_id; // Convert parsed display fields to db field names toImport.push(rawSampleToDbFields(idx, sample)); }); if (!toImport.length) { showStatus('No rows selected.', 'warning'); return; } setProgress(true, `Importing ${toImport.length} sample${toImport.length !== 1 ? 's' : ''}…`); bulkImportBtn.disabled = true; try { const fd = buildFormData({ action: 'import_bulk', client_id: clientId, samples: JSON.stringify(toImport), }); const data = await post(fd); if (!data.success) { showStatus('Import failed: ' + escHtml(data.error), 'danger'); return; } bulkPanel.hidden = true; const skipHtml = data.skipped?.length ? `
Skipped: ${data.skipped.map(escHtml).join(', ')}` : ''; showStatus(`${escHtml(data.message)}${skipHtml}`, 'success'); } catch (err) { showStatus('Error: ' + escHtml(err.message), 'danger'); } finally { setProgress(false); bulkImportBtn.disabled = false; } }); } // ── Step 3b: Fill form with one sample ──────────────────────────────────── async function fillForm(idx) { setProgress(true, 'Mapping fields…'); try { const fd = buildFormData({ action: 'import_one', sample_idx: idx }); const data = await post(fd); if (!data.success) { showStatus('Could not map fields: ' + escHtml(data.error), 'danger'); return; } const { filled } = populateForm(data.fields); highlightFilled(data.fields); const method = data.method === 'csbp' ? 'CSBP direct map' : 'AI-assisted'; showStatus( `${filled} fields filled via ${method}. ` + `Review highlighted fields before submitting.`, 'success' ); document.getElementById('SoilcsvForm')?.scrollIntoView({ behavior: 'smooth' }); } catch (err) { showStatus('Error: ' + escHtml(err.message), 'danger'); } finally { setProgress(false); } } // ── Form population helpers ─────────────────────────────────────────────── function populateForm(fields) { let filled = 0; for (const [name, value] of Object.entries(fields)) { if (value === null || value === undefined || value === '') continue; const el = document.getElementById(name) || document.querySelector(`[name="${name}"]`); if (!el) continue; if (el.tagName === 'SELECT') { const val = String(value).toLowerCase(); for (const opt of el.options) { if (opt.value.toLowerCase() === val || opt.text.toLowerCase() === val) { el.value = opt.value; filled++; break; } } } else { el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); filled++; } } return { filled }; } function highlightFilled(fields) { // Clear old highlights first document.querySelectorAll('.import-filled').forEach(el => { el.classList.remove('import-filled', 'border-success', 'bg-success-subtle'); }); for (const name of Object.keys(fields)) { if (!fields[name]) continue; const el = document.getElementById(name) || document.querySelector(`[name="${name}"]`); if (el) el.classList.add('import-filled', 'border-success', 'bg-success-subtle'); } } // ── Convert display sample → db field names for bulk save ───────────────── // The parsed CSBP sample already uses db field names, so this is mostly pass-through. // For generic lab samples (raw headers), the server handles mapping via Ollama. function rawSampleToDbFields(idx, sample) { // sample already has db keys from csbpParse / genericParse // just ensure sample_id is updated from the editable paddock field return sample; } // ── Utility ─────────────────────────────────────────────────────────────── function escHtml(str) { const d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; } })();