soil-import.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /**
  2. * client-assets/js/soil-import.js
  3. *
  4. * Handles the lab spreadsheet import flow on the soil test entry page.
  5. *
  6. * Flow:
  7. * 1. User picks a file → "Analyse File" button enabled
  8. * 2. File is sent to soilImportController.php?action=parse
  9. * 3. If one sample: auto-import it
  10. * If many samples: show a sample picker table
  11. * 4. User picks a sample → sent to soilImportController.php?action=import
  12. * 5. Mapped values are filled into the soil analysis form
  13. */
  14. (function () {
  15. 'use strict';
  16. // ── DOM refs ──────────────────────────────────────────────────────────────
  17. const fileInput = document.getElementById('lab-file-input');
  18. const analyseBtn = document.getElementById('lab-analyse-btn');
  19. const importStatus = document.getElementById('import-status');
  20. const samplePanel = document.getElementById('sample-picker-panel');
  21. const sampleTable = document.getElementById('sample-picker-table');
  22. const sampleTbody = sampleTable ? sampleTable.querySelector('tbody') : null;
  23. const importProgress = document.getElementById('import-progress');
  24. if (!fileInput || !analyseBtn) return; // not on the right page
  25. // ── helpers ───────────────────────────────────────────────────────────────
  26. function showStatus(msg, type = 'info') {
  27. if (!importStatus) return;
  28. importStatus.className = `alert alert-${type} mt-2`;
  29. importStatus.textContent = msg;
  30. importStatus.hidden = false;
  31. }
  32. function hideStatus() {
  33. if (importStatus) importStatus.hidden = true;
  34. }
  35. function setProgress(visible, text = '') {
  36. if (!importProgress) return;
  37. importProgress.hidden = !visible;
  38. if (text) importProgress.querySelector('.progress-label').textContent = text;
  39. }
  40. function buildFormData(file, action, extraFields = {}) {
  41. const fd = new FormData();
  42. fd.append('file', file);
  43. fd.append('action', action);
  44. for (const [k, v] of Object.entries(extraFields)) {
  45. fd.append(k, v);
  46. }
  47. return fd;
  48. }
  49. async function postToController(formData) {
  50. const resp = await fetch('/controllers/soilImportController.php', {
  51. method: 'POST',
  52. body: formData,
  53. });
  54. if (!resp.ok) {
  55. throw new Error(`Server error ${resp.status}`);
  56. }
  57. return resp.json();
  58. }
  59. // ── form population ───────────────────────────────────────────────────────
  60. function populateForm(fields) {
  61. let filled = 0;
  62. let skipped = 0;
  63. for (const [fieldName, value] of Object.entries(fields)) {
  64. if (value === null || value === undefined || value === '') continue;
  65. const el = document.getElementById(fieldName)
  66. || document.querySelector(`[name="${fieldName}"]`);
  67. if (!el) { skipped++; continue; }
  68. if (el.tagName === 'SELECT') {
  69. // Try to find a matching option (case-insensitive)
  70. const val = String(value).toLowerCase();
  71. for (const opt of el.options) {
  72. if (opt.value.toLowerCase() === val || opt.text.toLowerCase() === val) {
  73. el.value = opt.value;
  74. filled++;
  75. break;
  76. }
  77. }
  78. } else {
  79. el.value = value;
  80. el.dispatchEvent(new Event('input', { bubbles: true }));
  81. filled++;
  82. }
  83. }
  84. return { filled, skipped };
  85. }
  86. // Highlight fields that were auto-filled so the user can review them
  87. function highlightFilledFields(fields) {
  88. for (const fieldName of Object.keys(fields)) {
  89. if (fields[fieldName] === null || fields[fieldName] === '') continue;
  90. const el = document.getElementById(fieldName)
  91. || document.querySelector(`[name="${fieldName}"]`);
  92. if (el) {
  93. el.classList.add('border-success', 'bg-success-subtle');
  94. }
  95. }
  96. }
  97. // ── sample picker ─────────────────────────────────────────────────────────
  98. function renderSamplePicker(samples, file) {
  99. if (!samplePanel || !sampleTbody) return;
  100. sampleTbody.innerHTML = '';
  101. samples.forEach((s) => {
  102. const tr = document.createElement('tr');
  103. tr.innerHTML = `
  104. <td>${escHtml(s.lab_id)}</td>
  105. <td>${escHtml(s.client)}</td>
  106. <td>${escHtml(s.site)}</td>
  107. <td>${escHtml(s.crop)}</td>
  108. <td>
  109. <button class="btn btn-sm btn-success"
  110. data-idx="${s.idx}"
  111. type="button">
  112. Import
  113. </button>
  114. </td>`;
  115. sampleTbody.appendChild(tr);
  116. });
  117. // Wire import buttons
  118. sampleTbody.querySelectorAll('button[data-idx]').forEach((btn) => {
  119. btn.addEventListener('click', async () => {
  120. const idx = btn.dataset.idx;
  121. await importSample(file, idx);
  122. });
  123. });
  124. samplePanel.hidden = false;
  125. samplePanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  126. }
  127. function escHtml(str) {
  128. const d = document.createElement('div');
  129. d.textContent = str || '';
  130. return d.innerHTML;
  131. }
  132. // ── import a specific sample ──────────────────────────────────────────────
  133. async function importSample(file, sampleIdx) {
  134. setProgress(true, 'Mapping fields with AI…');
  135. hideStatus();
  136. try {
  137. const fd = buildFormData(file, 'import', { sample_idx: sampleIdx });
  138. const data = await postToController(fd);
  139. if (!data.success) {
  140. showStatus('Import failed: ' + data.error, 'danger');
  141. return;
  142. }
  143. const { filled, skipped } = populateForm(data.fields);
  144. highlightFilledFields(data.fields);
  145. if (samplePanel) samplePanel.hidden = true;
  146. const method = data.method === 'ai' ? 'AI-assisted' : 'pattern-matched';
  147. const warning = data.warning ? ` (${data.warning})` : '';
  148. showStatus(
  149. `${filled} field${filled !== 1 ? 's' : ''} populated via ${method}${warning}. ` +
  150. `Please review and adjust before submitting.`,
  151. data.warning ? 'warning' : 'success'
  152. );
  153. // Scroll to the form
  154. document.getElementById('SoilcsvForm')?.scrollIntoView({ behavior: 'smooth' });
  155. } catch (err) {
  156. showStatus('Error: ' + err.message, 'danger');
  157. } finally {
  158. setProgress(false);
  159. }
  160. }
  161. // ── main: analyse file ────────────────────────────────────────────────────
  162. fileInput.addEventListener('change', () => {
  163. analyseBtn.disabled = !fileInput.files.length;
  164. hideStatus();
  165. if (samplePanel) samplePanel.hidden = true;
  166. });
  167. analyseBtn.addEventListener('click', async () => {
  168. const file = fileInput.files[0];
  169. if (!file) return;
  170. setProgress(true, 'Parsing file…');
  171. hideStatus();
  172. if (samplePanel) samplePanel.hidden = true;
  173. analyseBtn.disabled = true;
  174. try {
  175. const fd = buildFormData(file, 'parse');
  176. const data = await postToController(fd);
  177. if (!data.success) {
  178. showStatus('Parse failed: ' + data.error, 'danger');
  179. return;
  180. }
  181. if (data.count === 1) {
  182. // Single sample — go straight to import
  183. await importSample(file, 0);
  184. } else {
  185. showStatus(
  186. `Found ${data.count} samples. Select the one you want to import.`,
  187. 'info'
  188. );
  189. renderSamplePicker(data.samples, file);
  190. }
  191. } catch (err) {
  192. showStatus('Error reading file: ' + err.message, 'danger');
  193. } finally {
  194. setProgress(false);
  195. analyseBtn.disabled = false;
  196. }
  197. });
  198. })();