/**
* 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;
}
})();