soil-report-pdf.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <?php
  2. /**
  3. * soil-report-pdf.php
  4. *
  5. * Print / PDF-export version of a completed soil analysis report.
  6. * Displays the saved AI-generated sections from the reports table alongside
  7. * the element requirement cards and five-year balancing plan.
  8. *
  9. * Normal access (logged-in user):
  10. * ?rid=<id>&rand=<token>
  11. *
  12. * Headless Chrome access (no session, called from headlessChrome_pdf.php):
  13. * ?rid=<id>&rand=<token>&ptoken=<one-time-file-token>
  14. * The ptoken is a short-lived file written by the generator. This lets Chrome
  15. * render the page without a PHP session cookie.
  16. */
  17. require_once __DIR__ . '/../../../config/database.php';
  18. require_once __DIR__ . '/../../../lib/auth.php';
  19. require_once __DIR__ . '/../../../lib/print_auth.php';
  20. require_once __DIR__ . '/../../../lib/soil_calculations.php';
  21. require_once __DIR__ . '/../../../vendor/autoload.php';
  22. if (session_status() === PHP_SESSION_NONE) {
  23. session_start();
  24. }
  25. $recordId = (int) ($_GET['rid'] ?? 0);
  26. $randId = trim( $_GET['rand'] ?? '');
  27. $chromeAccess = authenticatePrintPage($recordId, $randId);
  28. $pdo = getDBConnection();
  29. $userId = $chromeAccess ? null : getCurrentUserId();
  30. $row = null;
  31. $spec = [];
  32. $savedComments = [];
  33. $today = date('jS F Y');
  34. if ($recordId > 0 && $randId !== '') {
  35. $stmt = $pdo->prepare('SELECT * FROM soil_records WHERE id = ? AND rand = ? LIMIT 1');
  36. $stmt->execute([$recordId, $randId]);
  37. $row = $stmt->fetch(PDO::FETCH_ASSOC);
  38. if ($row) {
  39. // Load spec ranges for this soil type
  40. if (!empty($row['soil_type'])) {
  41. $stmtSpec = $pdo->prepare('SELECT * FROM soil_specifications WHERE soil_type = ? LIMIT 1');
  42. $stmtSpec->execute([$row['soil_type']]);
  43. $spec = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
  44. }
  45. // Load saved report comments — use modx_user_id when available, else any comment for this record
  46. if ($userId !== null) {
  47. $stmtRpt = $pdo->prepare(
  48. 'SELECT comment FROM reports WHERE record_id = ? AND modx_user_id = ? ORDER BY id DESC LIMIT 1'
  49. );
  50. $stmtRpt->execute([$recordId, $userId]);
  51. } else {
  52. $stmtRpt = $pdo->prepare(
  53. 'SELECT comment FROM reports WHERE record_id = ? ORDER BY id DESC LIMIT 1'
  54. );
  55. $stmtRpt->execute([$recordId]);
  56. }
  57. $savedRow = $stmtRpt->fetchColumn();
  58. if ($savedRow) {
  59. $decoded = json_decode($savedRow, true);
  60. if (is_array($decoded)) {
  61. $savedComments = $decoded;
  62. }
  63. }
  64. }
  65. }
  66. // ── Five-year plan ────────────────────────────────────────────────────────────
  67. $planElements = [
  68. ['Calcium', 'BS_ca_ppm', 'ca_ppm_min', 'ca_ppm_max', 'kg/ha'],
  69. ['Magnesium', 'BS_mg_ppm', 'mg_ppm_min', 'mg_ppm_max', 'kg/ha'],
  70. ['Potassium', 'BS_k_ppm', 'k_ppm_min', 'k_ppm_max', 'kg/ha'],
  71. ['Sodium', 'BS_na_ppm', 'na_ppm_min', 'na_ppm_max', 'kg/ha'],
  72. ['Phosphate', 'p_colwell', '', '', 'kg/ha'],
  73. ['Sulfur', 's_morgan', '', '', 'kg/ha'],
  74. ['Boron', 'b_cacl2', '', '', 'kg/ha'],
  75. ['Manganese', 'mn_dtpa', '', '', 'kg/ha'],
  76. ['Zinc', 'zn_dtpa', '', '', 'kg/ha'],
  77. ['Copper', 'cu_dtpa', '', '', 'kg/ha'],
  78. ];
  79. $acHa = 2.4710559990832394739;
  80. function calcDeficitPdf(array $row, array $spec, string $col, string $minCol, string $maxCol): float
  81. {
  82. global $acHa;
  83. $value = (float)($row[$col] ?? 0);
  84. $maxVal = $maxCol !== '' ? (float)($row[$maxCol] ?? 0) : (float)($spec[$col] ?? 0);
  85. $deficit = ($maxVal - $value) * $acHa;
  86. return $deficit > 0 ? round($deficit, 2) : 0.0;
  87. }
  88. $h = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
  89. // Parse and render markdown text from AI-generated report sections
  90. function formatReportText(string $text): string
  91. {
  92. if (trim($text) === '') {
  93. return '<p class="text-muted fst-italic">No content saved.</p>';
  94. }
  95. $parsedown = new Parsedown();
  96. $parsedown->setSafeMode(true); // strip any raw HTML in the text
  97. return $parsedown->text($text);
  98. }
  99. ?>
  100. <!doctype html>
  101. <html lang="en">
  102. <head>
  103. <meta charset="UTF-8">
  104. <meta name="viewport" content="width=device-width, initial-scale=1">
  105. <title>Soil Analysis Report | Crop Monitor</title>
  106. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
  107. <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.css" rel="stylesheet" crossorigin="anonymous">
  108. <link href="/client-assets/css/dashboard.css" rel="stylesheet">
  109. <link rel="stylesheet" href="/client-assets/css/graphPrint.css" media="print">
  110. <style>
  111. @media print {
  112. .d-print-none { display: none !important; }
  113. .page-break { page-break-before: always; }
  114. body { font-size: 11px; }
  115. .report-section p { font-size: 11px; }
  116. }
  117. .report-section p { margin-bottom: 0.6rem; line-height: 1.6; }
  118. .report-section h1,
  119. .report-section h2,
  120. .report-section h3,
  121. .report-section h4 { font-size: 1rem; font-weight: 600; margin: 1rem 0 0.4rem; }
  122. .report-section ul,
  123. .report-section ol { padding-left: 1.4rem; margin-bottom: 0.6rem; }
  124. .report-section li { margin-bottom: 0.25rem; line-height: 1.5; }
  125. .report-section table {
  126. width: 100%;
  127. border-collapse: collapse;
  128. margin-bottom: 0.8rem;
  129. font-size: 0.85rem;
  130. }
  131. .report-section table th,
  132. .report-section table td {
  133. border: 1px solid #dee2e6;
  134. padding: 4px 8px;
  135. text-align: left;
  136. }
  137. .report-section table thead th {
  138. background: #f8f9fa;
  139. font-weight: 600;
  140. }
  141. .section-header {
  142. background: #212529;
  143. color: #fff;
  144. padding: 6px 12px;
  145. font-weight: 600;
  146. margin-bottom: 0;
  147. }
  148. .section-body {
  149. border: 1px solid #dee2e6;
  150. border-top: 0;
  151. padding: 14px 16px;
  152. margin-bottom: 1.2rem;
  153. }
  154. .title-table td, .title-table th { padding: 2px 8px; }
  155. .element-required-module .col { padding: 0 6px; }
  156. </style>
  157. </head>
  158. <body>
  159. <div class="container" id="pdf-content">
  160. <?php if (!$row): ?>
  161. <div class="alert alert-danger mt-4">Record not found or access denied.</div>
  162. <?php else: ?>
  163. <!-- ── Header ──────────────────────────────────────────────────────────── -->
  164. <div class="row align-items-center mb-3 mt-3">
  165. <div class="col-3">
  166. <img class="img-fluid" src="/client-assets/images/crop-monitor.png"
  167. alt="Crop Monitor" style="max-height:55px;">
  168. </div>
  169. <div class="col-9 text-end">
  170. <div class="fw-bold h5 mb-0">Soil Analysis Report</div>
  171. <div class="text-muted small"><?= $h($today) ?></div>
  172. </div>
  173. </div>
  174. <table class="title-table w-100 mb-3 small">
  175. <tbody>
  176. <tr>
  177. <td class="text-end fw-bold text-nowrap">CLIENT:</td>
  178. <td><?= $h($row['client_name']) ?></td>
  179. <td></td>
  180. <td class="text-end fw-bold text-nowrap">SAMPLE ID:</td>
  181. <td><?= $h($row['site_id']) ?></td>
  182. </tr>
  183. <tr>
  184. <td class="text-end fw-bold text-nowrap">ADDRESS:</td>
  185. <td><?= $h($row['site_address']) ?></td>
  186. <td></td>
  187. <td class="text-end fw-bold text-nowrap">DATE SAMPLED:</td>
  188. <td><?= $h($row['date_sampled']) ?></td>
  189. </tr>
  190. <tr>
  191. <td></td>
  192. <td><?= $h($row['state_postcode']) ?></td>
  193. <td></td>
  194. <td class="text-end fw-bold text-nowrap">LAB NUMBER:</td>
  195. <td><?= $h($row['lab_no']) ?></td>
  196. </tr>
  197. <tr>
  198. <td></td>
  199. <td><?= $h($row['email']) ?></td>
  200. <td></td>
  201. <td class="text-end fw-bold text-nowrap">CROP:</td>
  202. <td><?= $h($row['sample_id']) ?></td>
  203. </tr>
  204. <tr>
  205. <td></td>
  206. <td></td>
  207. <td></td>
  208. <td class="text-end fw-bold text-nowrap">SOIL TYPE:</td>
  209. <td><?= $h($row['soil_type']) ?></td>
  210. </tr>
  211. </tbody>
  212. </table>
  213. <!-- ── Download / Back buttons ─────────────────────────────────────────── -->
  214. <div class="d-print-none mb-3 d-flex gap-2">
  215. <a href="/dashboard/crop-analysis/soil-test-data/soil-report.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>"
  216. class="btn btn-outline-secondary btn-sm">
  217. &larr; Back to Report
  218. </a>
  219. <a href="/pdf-files/headlessChrome_pdf.php?type=soil-report&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>"
  220. class="btn btn-success btn-sm">
  221. <i class="fas fa-download me-1"></i>Download PDF
  222. </a>
  223. <button class="btn btn-outline-dark btn-sm" onclick="window.print()">
  224. <i class="fas fa-print me-1"></i>Print
  225. </button>
  226. </div>
  227. <hr>
  228. <!-- ── 1. Element requirements ─────────────────────────────────────────── -->
  229. <div class="section-header">
  230. Total kilograms per hectare of each element needed to balance soil
  231. </div>
  232. <div class="section-body">
  233. <div class="element-required-module">
  234. <div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-6 g-2">
  235. <?php
  236. echo soilAnalysisReportCalcs('Ca', 'BS_ca_ppm', 'ca_ppm_min', 'ca_ppm_max', 'Calcium', 'kg', 'col', $recordId, $randId);
  237. echo soilAnalysisReportCalcs('Mg', 'BS_mg_ppm', 'mg_ppm_min', 'mg_ppm_max', 'Magnesium', 'kg', 'col', $recordId, $randId);
  238. echo soilAnalysisReportCalcs('K', 'BS_k_ppm', 'k_ppm_min', 'k_ppm_max', 'Potassium', 'kg', 'col', $recordId, $randId);
  239. echo soilAnalysisReportCalcs('Na', 'BS_na_ppm', 'na_ppm_min', 'na_ppm_max', 'Sodium', 'kg', 'col', $recordId, $randId);
  240. echo soilAnalysisReportCalcs('P', 'p_colwell', '', '', 'Phosphate', 'kg', 'col', $recordId, $randId);
  241. echo soilAnalysisReportCalcs('S', 's_morgan', '', '', 'Sulfur', 'kg', 'col', $recordId, $randId);
  242. echo soilAnalysisReportCalcs('Mn', 'mn_dtpa', '', '', 'Manganese', 'kg', 'col', $recordId, $randId);
  243. echo soilAnalysisReportCalcs('Fe', 'fe_dtpa', '', '', 'Iron', 'kg', 'col', $recordId, $randId);
  244. echo soilAnalysisReportCalcs('Zn', 'zn_dtpa', '', '', 'Zinc', 'kg', 'col', $recordId, $randId);
  245. echo soilAnalysisReportCalcs('Cu', 'cu_dtpa', '', '', 'Copper', 'kg', 'col', $recordId, $randId);
  246. echo soilAnalysisReportCalcs('AmN', 'NH3_N', '', '', 'Amm. Nitrogen', 'kg', 'col', $recordId, $randId);
  247. echo soilAnalysisReportCalcs('B', 'b_cacl2', '', '', 'Boron', 'kg', 'col', $recordId, $randId);
  248. echo soilAnalysisReportCalcs('NN', 'NO3_N', '', '', 'Nit. Nitrogen', 'kg', 'col', $recordId, $randId);
  249. ?>
  250. </div>
  251. </div>
  252. </div>
  253. <!-- ── 2. Five-year balancing plan ─────────────────────────────────────── -->
  254. <div class="section-header">
  255. Ideal Soil Balancing Program — 5-Year Plan (kg/ha per year)
  256. </div>
  257. <div class="section-body p-0">
  258. <table class="table table-sm table-bordered mb-0">
  259. <thead class="table-light">
  260. <tr>
  261. <th>Element</th>
  262. <th class="text-center">Total Deficit</th>
  263. <?php for ($y = 1; $y <= 5; $y++): ?>
  264. <th class="text-center">Year <?= $y ?></th>
  265. <?php endfor; ?>
  266. </tr>
  267. </thead>
  268. <tbody>
  269. <?php foreach ($planElements as [$label, $col, $minCol, $maxCol, $unit]):
  270. $total = calcDeficitPdf($row, $spec, $col, $minCol, $maxCol);
  271. $perYear = $total > 0 ? round($total / 5, 2) : 0;
  272. ?>
  273. <tr>
  274. <td><?= $h($label) ?></td>
  275. <td class="text-center <?= $total > 0 ? 'text-danger fw-semibold' : 'text-success' ?>">
  276. <?= $total > 0 ? $total . ' ' . $unit : '&#10003; Adequate' ?>
  277. </td>
  278. <?php for ($y = 1; $y <= 5; $y++): ?>
  279. <td class="text-center">
  280. <?= $perYear > 0 ? $perYear . ' ' . $unit : '—' ?>
  281. </td>
  282. <?php endfor; ?>
  283. </tr>
  284. <?php endforeach; ?>
  285. </tbody>
  286. </table>
  287. </div>
  288. <!-- ── 3. Overview ─────────────────────────────────────────────────────── -->
  289. <div class="section-header">Overview</div>
  290. <div class="section-body report-section">
  291. <?= formatReportText($savedComments['overview'] ?? '') ?>
  292. </div>
  293. <!-- ── 4. AI Soil Interpretation ───────────────────────────────────────── -->
  294. <div class="section-header">AI Soil Interpretation</div>
  295. <div class="section-body report-section">
  296. <?= formatReportText($savedComments['ai_interpretation'] ?? '') ?>
  297. </div>
  298. <!-- ── 5. Foliar Program ────────────────────────────────────────────────── -->
  299. <div class="section-header">
  300. <?= $h($savedComments['header1'] ?? 'Foliar Program') ?>
  301. </div>
  302. <div class="section-body report-section">
  303. <?= formatReportText($savedComments['foliar_details'] ?? '') ?>
  304. </div>
  305. <!-- ── 6. Microbial Program ─────────────────────────────────────────────── -->
  306. <div class="section-header">Microbial Program</div>
  307. <div class="section-body report-section">
  308. <?= formatReportText($savedComments['microbe_program'] ?? '') ?>
  309. </div>
  310. <!-- ── Disclaimer ──────────────────────────────────────────────────────── -->
  311. <div class="mt-3 pt-3 border-top">
  312. <p class="text-muted" style="font-size:0.7rem;">
  313. Any recommendations provided by Crop Monitor are advice only. We are not paid consultants
  314. and accept no responsibility for any of our suggestions. No control can be exercised over
  315. storage, handling, mixing, application, or use, or over weather, plant or soil conditions
  316. before, during or after application — all of which may affect the performance of our
  317. program. No responsibility for, or liability for, any failure in performance, losses,
  318. damage or injuries consequential or otherwise arising from such storage, mixing,
  319. application or use will be accepted under any circumstances whatsoever. The buyer assumes
  320. all responsibility for the use of any of our products.
  321. </p>
  322. </div>
  323. <?php endif; ?>
  324. </div><!-- /container -->
  325. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
  326. </body>
  327. </html>