plant-report.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <?php
  2. /**
  3. * dashboard/crop-analysis/plant-test-data/plant-report.php
  4. *
  5. * Plant analysis report — editable sections with auto-save and Ollama AI interpretation.
  6. */
  7. require_once __DIR__ . '/../../../config/database.php';
  8. require_once __DIR__ . '/../../../lib/auth.php';
  9. require_once __DIR__ . '/../../../lib/csrf.php';
  10. requireLogin();
  11. $recordId = (int) ($_GET['rid'] ?? 0);
  12. $randId = trim( $_GET['rand'] ?? '');
  13. $clientId = (int) ($_GET['cid'] ?? 0);
  14. if (!$recordId || $randId === '') {
  15. http_response_code(400);
  16. die('Invalid request parameters');
  17. }
  18. try {
  19. $pdo = getDBConnection();
  20. $userId = getCurrentUserId();
  21. $stmt = $pdo->prepare('SELECT * FROM plant_records WHERE id = ? AND rand = ?');
  22. $stmt->execute([$recordId, $randId]);
  23. $row = $stmt->fetch(PDO::FETCH_ASSOC);
  24. if (!$row) {
  25. http_response_code(404);
  26. die('Plant record not found');
  27. }
  28. // Load spec ranges
  29. $specs = [];
  30. if (!empty($row['crop_type'])) {
  31. $stmtSpec = $pdo->prepare('SELECT * FROM plant_specifications WHERE plant_type = ? LIMIT 1');
  32. $stmtSpec->execute([$row['crop_type']]);
  33. $specs = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
  34. }
  35. // Load saved report comments
  36. $savedComments = [
  37. 'general_details' => '',
  38. 'ai_interpretation' => '',
  39. 'recommended_details' => '',
  40. 'foliar_details' => '',
  41. ];
  42. $stmtRpt = $pdo->prepare(
  43. 'SELECT comment FROM reports WHERE record_id = ? AND modx_user_id = ? ORDER BY id DESC LIMIT 1'
  44. );
  45. $stmtRpt->execute([$recordId, $userId]);
  46. $savedRow = $stmtRpt->fetchColumn();
  47. if ($savedRow) {
  48. $decoded = json_decode($savedRow, true);
  49. if (is_array($decoded)) {
  50. $savedComments = array_merge($savedComments, $decoded);
  51. }
  52. }
  53. } catch (PDOException $e) {
  54. error_log('DB error in plant-report.php: ' . $e->getMessage());
  55. die('Database error occurred');
  56. }
  57. $h = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
  58. $today = date('jS F Y');
  59. $pageTitle = 'Plant Report' . (!empty($row['client_name']) ? ' — ' . $row['client_name'] : '');
  60. $siteName = 'Crop Monitor';
  61. include __DIR__ . '/../../../layouts/header.php';
  62. ?>
  63. <link rel="stylesheet" href="/client-assets/home/css/graphPrint.css" media="print">
  64. <style>
  65. @media print {
  66. @page {
  67. size: A4 portrait;
  68. }
  69. @page :left {
  70. margin-left: 0.5cm;
  71. }
  72. @page :right {
  73. margin-right: 0.5cm;
  74. }
  75. @page :top {
  76. margin-top: 0cm;
  77. }
  78. @page :bottom {
  79. margin-bottom: 0cm;
  80. }
  81. body {
  82. min-width: 992px !important;
  83. }
  84. .container {
  85. min-width: 992px !important;
  86. }
  87. .report-textarea {
  88. border:none;
  89. }
  90. }
  91. .report-textarea {
  92. overflow: hidden;
  93. resize: none;
  94. overflow-y: auto;
  95. }
  96. </style>
  97. <div class="container-fluid px-4">
  98. <!-- ── Page heading ─────────────────────────────────────────── -->
  99. <div class="d-flex align-items-center justify-content-between mt-4 mb-3">
  100. <h1 class="h3 mb-0">Plant Analysis Report</h1>
  101. <div class="d-flex gap-2 d-print-none">
  102. <a href="/dashboard/crop-analysis/plant-test-data/plant-analysis.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
  103. class="btn btn-outline-secondary btn-sm">
  104. &larr; Analysis
  105. </a>
  106. <a href="/pdf-files/headlessChrome_pdf.php?type=plant-report&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
  107. class="btn btn-outline-success btn-sm">
  108. <i class="fas fa-file-pdf me-1"></i>PDF — Report
  109. </a>
  110. <a href="/pdf-files/headlessChrome_pdf.php?type=plant&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
  111. class="btn btn-success btn-sm">
  112. <i class="fas fa-file-pdf me-1"></i>PDF — Analysis &amp; Report
  113. </a>
  114. <button type="button" class="btn btn-primary btn-sm" id="btn-generate-all">
  115. <i class="fas fa-robot me-1"></i>Interpret All with AI
  116. </button>
  117. </div>
  118. </div>
  119. <!-- ── Client info card ──────────────────────────────────────── -->
  120. <div class="card mb-4">
  121. <div class="card-body py-2">
  122. <div class="row row-cols-2 row-cols-md-3 g-1 small">
  123. <div><strong>Client:</strong> <?= $h($row['client_name']) ?></div>
  124. <div><strong>Sample ID:</strong> <?= $h($row['sample_id']) ?></div>
  125. <div><strong>Date Sampled:</strong> <?= $h($row['date_sampled']) ?></div>
  126. <div><strong>Address:</strong> <?= $h($row['site_address']) ?>, <?= $h($row['state_postcode']) ?></div>
  127. <div><strong>Crop Type:</strong> <?= $h($row['crop_type']) ?></div>
  128. <div><strong>Lab No:</strong> <?= $h($row['lab_no']) ?></div>
  129. <div><strong>Site ID:</strong> <?= $h($row['site_id']) ?></div>
  130. <div><strong>Report Date:</strong> <?= $h($today) ?></div>
  131. </div>
  132. </div>
  133. </div>
  134. <div id="save-status" class="text-muted small mb-2" style="min-height:1.2rem;"></div>
  135. <form class="report-form" method="post">
  136. <input type="hidden" name="csrf_token"
  137. value="<?= htmlspecialchars(generateCsrfToken(), ENT_QUOTES, 'UTF-8') ?>">
  138. <input type="hidden" name="rid" value="<?= $recordId ?>">
  139. <input type="hidden" name="rand" value="<?= $h($randId) ?>">
  140. <!-- ── 1. General Comment ─────────────────────────────────── -->
  141. <div class="card mb-4">
  142. <div class="card-header d-flex justify-content-between align-items-center fw-bold">
  143. <span>General Comment</span>
  144. <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
  145. data-section="general" data-target="#general_details">
  146. <i class="fas fa-robot me-1"></i>Generate with AI
  147. </button>
  148. </div>
  149. <div class="card-body">
  150. <textarea id="general_details" name="general_details"
  151. class="form-control report-textarea" rows="6"
  152. placeholder="Enter a general comment on this plant analysis..."
  153. ><?= $h($savedComments['general_details']) ?></textarea>
  154. </div>
  155. </div>
  156. <!-- ── 2. AI Interpretation ───────────────────────────────── -->
  157. <div class="card mb-4">
  158. <div class="card-header d-flex justify-content-between align-items-center fw-bold">
  159. <span>AI Plant Interpretation</span>
  160. <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
  161. data-section="ai_interpretation" data-target="#ai_interpretation">
  162. <i class="fas fa-robot me-1"></i>Interpret with AI
  163. </button>
  164. </div>
  165. <div class="card-body">
  166. <p class="text-muted small mb-2">
  167. AI-generated agronomic interpretation. Review and edit before including in the final report.
  168. </p>
  169. <textarea id="ai_interpretation" name="ai_interpretation"
  170. class="form-control report-textarea" rows="10"
  171. placeholder="Click 'Interpret with AI' to generate an interpretation, or type manually..."
  172. ><?= $h($savedComments['ai_interpretation']) ?></textarea>
  173. </div>
  174. </div>
  175. <!-- ── 3. Recommended Remedial Program ───────────────────── -->
  176. <div class="card mb-4">
  177. <div class="card-header d-flex justify-content-between align-items-center fw-bold">
  178. <span>Recommended Remedial Program</span>
  179. <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
  180. data-section="recommended" data-target="#recommended_details">
  181. <i class="fas fa-robot me-1"></i>Generate with AI
  182. </button>
  183. </div>
  184. <div class="card-body">
  185. <textarea id="recommended_details" name="recommended_details"
  186. class="form-control report-textarea" rows="6"
  187. placeholder="Enter recommended remedial program details..."
  188. ><?= $h($savedComments['recommended_details']) ?></textarea>
  189. </div>
  190. </div>
  191. <!-- ── 4. Foliar Program ──────────────────────────────────── -->
  192. <div class="card mb-4">
  193. <div class="card-header d-flex justify-content-between align-items-center fw-bold">
  194. <span>Foliar Program</span>
  195. <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
  196. data-section="foliar" data-target="#foliar_details">
  197. <i class="fas fa-robot me-1"></i>Generate with AI
  198. </button>
  199. </div>
  200. <div class="card-body">
  201. <textarea id="foliar_details" name="foliar_details"
  202. class="form-control report-textarea" rows="6"
  203. placeholder="Enter foliar spray program details..."
  204. ><?= $h($savedComments['foliar_details']) ?></textarea>
  205. </div>
  206. </div>
  207. <!-- ── Disclaimer ─────────────────────────────────────────── -->
  208. <div class="card mb-4 border-0 bg-light">
  209. <div class="card-body">
  210. <p class="text-muted mb-1" style="font-size:0.75rem;">
  211. <i class="fa fa-leaf" style="color:green"></i>
  212. It is always an advantage to assess tissue analysis results along with a corresponding soil analysis for more accurate diagnosis of plant nutrient status.
  213. </p>
  214. <p class="text-muted mb-1" style="font-size:0.75rem;">
  215. <i class="fa fa-leaf" style="color:green"></i>
  216. Trace element levels — manganese, copper and zinc — can all be affected by fungicide spray residues, giving misleading results.
  217. </p>
  218. <p class="text-muted mb-0 fst-italic" style="font-size:0.7rem;">
  219. Any recommendations provided by Crop Monitor are advice only. We are not paid consultants and accept no responsibility for any of our suggestions.
  220. </p>
  221. </div>
  222. </div>
  223. </form>
  224. </div><!-- /.container-fluid -->
  225. <script>
  226. (function () {
  227. 'use strict';
  228. var saveTimer = null;
  229. var statusEl = document.getElementById('save-status');
  230. var SAVE_URL = '/dashboard/crop-analysis/plant-test-data/plant-report-save.php'
  231. + '?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>';
  232. var AI_URL = '/controllers/ollamaGenerate.php';
  233. var CSRF_TOKEN = <?= json_encode(generateCsrfToken()) ?>;
  234. function setStatus(msg, cls) {
  235. statusEl.textContent = msg;
  236. statusEl.className = 'small mb-2 text-' + (cls || 'secondary');
  237. }
  238. // ── Auto-save ────────────────────────────────────────────────────────── //
  239. document.querySelectorAll('.report-form .report-textarea')
  240. .forEach(function (el) {
  241. // initial resize
  242. autoResize(el);
  243. el.addEventListener('input', function () {
  244. autoResize(el);
  245. clearTimeout(saveTimer);
  246. saveTimer = setTimeout(saveReport, 1200);
  247. });
  248. });
  249. function saveReport() {
  250. var form = document.querySelector('.report-form');
  251. var data = new URLSearchParams(new FormData(form));
  252. setStatus('Saving…', 'secondary');
  253. fetch(SAVE_URL, { method: 'POST', body: data })
  254. .then(function (r) { return r.json(); })
  255. .then(function (d) {
  256. if (d.success) {
  257. setStatus('Saved — ' + new Date().toLocaleTimeString(), 'success');
  258. } else {
  259. setStatus('Save failed: ' + (d.message || 'unknown error'), 'danger');
  260. }
  261. })
  262. .catch(function () { setStatus('Network error — not saved', 'danger'); });
  263. }
  264. document.querySelector('.report-form').addEventListener('submit', function (e) {
  265. e.preventDefault();
  266. saveReport();
  267. });
  268. function autoResize(el) {
  269. el.style.height = 'auto';
  270. el.style.height = el.scrollHeight + 'px';
  271. }
  272. document.querySelectorAll('.report-textarea').forEach(function (el) {
  273. // force correct initial height AFTER render
  274. setTimeout(function () {
  275. autoResize(el);
  276. }, 0);
  277. el.addEventListener('input', function () {
  278. autoResize(el);
  279. clearTimeout(saveTimer);
  280. saveTimer = setTimeout(saveReport, 1200);
  281. });
  282. });
  283. // ── AI generation ────────────────────────────────────────────────────── //
  284. function generateSection(btn, section, targetSelector) {
  285. var textarea = document.querySelector(targetSelector);
  286. if (!textarea) return;
  287. var origHTML = btn.innerHTML;
  288. btn.disabled = true;
  289. btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Generating…';
  290. setStatus('Requesting AI interpretation…', 'secondary');
  291. fetch(AI_URL, {
  292. method: 'POST',
  293. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  294. body: new URLSearchParams({
  295. csrf_token: CSRF_TOKEN,
  296. rid: <?= $recordId ?>,
  297. rand: <?= json_encode($randId) ?>,
  298. section: section,
  299. record_type: 'plant',
  300. }),
  301. })
  302. .then(function (r) { return r.json(); })
  303. .then(function (d) {
  304. if (d.success && d.text) {
  305. textarea.value = d.text;
  306. textarea.dispatchEvent(new Event('input'));
  307. setStatus('AI text generated — review before publishing', 'success');
  308. } else {
  309. setStatus('AI error: ' + (d.error || 'no response returned'), 'danger');
  310. }
  311. })
  312. .catch(function () {
  313. setStatus('Could not reach AI service. Is Ollama running on port 11434?', 'danger');
  314. })
  315. .finally(function () {
  316. btn.disabled = false;
  317. btn.innerHTML = origHTML;
  318. });
  319. }
  320. document.querySelectorAll('.ai-generate-btn').forEach(function (btn) {
  321. btn.addEventListener('click', function () {
  322. generateSection(btn, btn.dataset.section, btn.dataset.target);
  323. });
  324. });
  325. // "Interpret All" — stagger requests
  326. document.getElementById('btn-generate-all').addEventListener('click', function () {
  327. var sections = [
  328. { section: 'general', target: '#general_details' },
  329. { section: 'ai_interpretation',target: '#ai_interpretation' },
  330. { section: 'recommended', target: '#recommended_details' },
  331. { section: 'foliar', target: '#foliar_details' },
  332. ];
  333. sections.forEach(function (s, i) {
  334. setTimeout(function () {
  335. var sectionBtn = document.querySelector('.ai-generate-btn[data-section="' + s.section + '"]');
  336. generateSection(
  337. sectionBtn || document.getElementById('btn-generate-all'),
  338. s.section, s.target
  339. );
  340. }, i * 4000);
  341. });
  342. });
  343. })();
  344. </script>