| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- /**
- * 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 (<strong>${escHtml(parsedSamples[0]?.lab_no || 'unknown')}</strong>). ` +
- `Review below then import.`,
- 'info'
- );
- } else {
- showStatus(
- `<strong>${data.count}</strong> 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 = `
- <td class="text-center">
- <input type="checkbox" class="form-check-input row-check" checked>
- </td>
- <td class="text-muted small">${escHtml(s.lab_no)}</td>
- <td>
- <input type="text"
- class="form-control form-control-sm paddock-input"
- value="${escHtml(s.sample_id)}"
- placeholder="Paddock / sample ID">
- </td>
- <td class="small">${escHtml(s.client)}</td>
- <td class="small">${escHtml(s.crop)}</td>
- <td class="text-center">
- <button class="btn btn-xs btn-sm btn-outline-secondary fill-form-btn"
- data-idx="${idx}" type="button" title="Pre-fill the form below with this sample">
- <i class="fas fa-pen fa-xs"></i>
- </button>
- </td>`;
- 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
- ? `<br><small class="text-warning">Skipped: ${data.skipped.map(escHtml).join(', ')}</small>`
- : '';
- showStatus(`<i class="fas fa-check-circle me-1"></i>${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(
- `<i class="fas fa-check-circle me-1"></i>${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;
- }
- })();
|