/** * client-assets/js/soil-import.js * * Handles the lab spreadsheet import flow on the soil test entry page. * * Flow: * 1. User picks a file → "Analyse File" button enabled * 2. File is sent to soilImportController.php?action=parse * 3. If one sample: auto-import it * If many samples: show a sample picker table * 4. User picks a sample → sent to soilImportController.php?action=import * 5. Mapped values are filled into the soil analysis form */ (function () { 'use strict'; // ── DOM refs ────────────────────────────────────────────────────────────── const fileInput = document.getElementById('lab-file-input'); const analyseBtn = document.getElementById('lab-analyse-btn'); const importStatus = document.getElementById('import-status'); const samplePanel = document.getElementById('sample-picker-panel'); const sampleTable = document.getElementById('sample-picker-table'); const sampleTbody = sampleTable ? sampleTable.querySelector('tbody') : null; const importProgress = document.getElementById('import-progress'); if (!fileInput || !analyseBtn) return; // not on the right page // ── helpers ─────────────────────────────────────────────────────────────── function showStatus(msg, type = 'info') { if (!importStatus) return; importStatus.className = `alert alert-${type} mt-2`; importStatus.textContent = msg; importStatus.hidden = false; } function hideStatus() { if (importStatus) importStatus.hidden = true; } function setProgress(visible, text = '') { if (!importProgress) return; importProgress.hidden = !visible; if (text) importProgress.querySelector('.progress-label').textContent = text; } function buildFormData(file, action, extraFields = {}) { const fd = new FormData(); fd.append('file', file); fd.append('action', action); for (const [k, v] of Object.entries(extraFields)) { fd.append(k, v); } return fd; } async function postToController(formData) { const resp = await fetch('/controllers/soilImportController.php', { method: 'POST', body: formData, }); if (!resp.ok) { throw new Error(`Server error ${resp.status}`); } return resp.json(); } // ── form population ─────────────────────────────────────────────────────── function populateForm(fields) { let filled = 0; let skipped = 0; for (const [fieldName, value] of Object.entries(fields)) { if (value === null || value === undefined || value === '') continue; const el = document.getElementById(fieldName) || document.querySelector(`[name="${fieldName}"]`); if (!el) { skipped++; continue; } if (el.tagName === 'SELECT') { // Try to find a matching option (case-insensitive) 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, skipped }; } // Highlight fields that were auto-filled so the user can review them function highlightFilledFields(fields) { for (const fieldName of Object.keys(fields)) { if (fields[fieldName] === null || fields[fieldName] === '') continue; const el = document.getElementById(fieldName) || document.querySelector(`[name="${fieldName}"]`); if (el) { el.classList.add('border-success', 'bg-success-subtle'); } } } // ── sample picker ───────────────────────────────────────────────────────── function renderSamplePicker(samples, file) { if (!samplePanel || !sampleTbody) return; sampleTbody.innerHTML = ''; samples.forEach((s) => { const tr = document.createElement('tr'); tr.innerHTML = ` ${escHtml(s.lab_id)} ${escHtml(s.client)} ${escHtml(s.site)} ${escHtml(s.crop)} `; sampleTbody.appendChild(tr); }); // Wire import buttons sampleTbody.querySelectorAll('button[data-idx]').forEach((btn) => { btn.addEventListener('click', async () => { const idx = btn.dataset.idx; await importSample(file, idx); }); }); samplePanel.hidden = false; samplePanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function escHtml(str) { const d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; } // ── import a specific sample ────────────────────────────────────────────── async function importSample(file, sampleIdx) { setProgress(true, 'Mapping fields with AI…'); hideStatus(); try { const fd = buildFormData(file, 'import', { sample_idx: sampleIdx }); const data = await postToController(fd); if (!data.success) { showStatus('Import failed: ' + data.error, 'danger'); return; } const { filled, skipped } = populateForm(data.fields); highlightFilledFields(data.fields); if (samplePanel) samplePanel.hidden = true; const method = data.method === 'ai' ? 'AI-assisted' : 'pattern-matched'; const warning = data.warning ? ` (${data.warning})` : ''; showStatus( `${filled} field${filled !== 1 ? 's' : ''} populated via ${method}${warning}. ` + `Please review and adjust before submitting.`, data.warning ? 'warning' : 'success' ); // Scroll to the form document.getElementById('SoilcsvForm')?.scrollIntoView({ behavior: 'smooth' }); } catch (err) { showStatus('Error: ' + err.message, 'danger'); } finally { setProgress(false); } } // ── main: analyse file ──────────────────────────────────────────────────── fileInput.addEventListener('change', () => { analyseBtn.disabled = !fileInput.files.length; hideStatus(); if (samplePanel) samplePanel.hidden = true; }); analyseBtn.addEventListener('click', async () => { const file = fileInput.files[0]; if (!file) return; setProgress(true, 'Parsing file…'); hideStatus(); if (samplePanel) samplePanel.hidden = true; analyseBtn.disabled = true; try { const fd = buildFormData(file, 'parse'); const data = await postToController(fd); if (!data.success) { showStatus('Parse failed: ' + data.error, 'danger'); return; } if (data.count === 1) { // Single sample — go straight to import await importSample(file, 0); } else { showStatus( `Found ${data.count} samples. Select the one you want to import.`, 'info' ); renderSamplePicker(data.samples, file); } } catch (err) { showStatus('Error reading file: ' + err.message, 'danger'); } finally { setProgress(false); analyseBtn.disabled = false; } }); })();