| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- /**
- * 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 = `
- <td>${escHtml(s.lab_id)}</td>
- <td>${escHtml(s.client)}</td>
- <td>${escHtml(s.site)}</td>
- <td>${escHtml(s.crop)}</td>
- <td>
- <button class="btn btn-sm btn-success"
- data-idx="${s.idx}"
- type="button">
- Import
- </button>
- </td>`;
- 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;
- }
- });
- })();
|