soil-report-pdf.php 15 KB

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