water-report.php 15 KB

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