plant-report-pdf.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. <?php
  2. /**
  3. * plant-report-pdf.php
  4. *
  5. * Print / PDF-export version of a completed plant 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__ . '/../../../vendor/autoload.php';
  21. if (session_status() === PHP_SESSION_NONE) {
  22. session_start();
  23. }
  24. $recordId = (int) ($_GET['rid'] ?? 0);
  25. $randId = trim( $_GET['rand'] ?? '');
  26. $clientId = (int) ($_GET['cid'] ?? 0);
  27. $printMode = isset($_GET['ptoken']) || isset($_GET['print']);
  28. if (!$recordId || $randId === '') {
  29. http_response_code(400);
  30. die('Invalid request parameters');
  31. }
  32. if ($printMode) {
  33. authenticatePrintPage($recordId, $randId);
  34. } else {
  35. requireLogin();
  36. }
  37. try {
  38. $pdo = getDBConnection();
  39. $userId = $printMode ? null : getCurrentUserId();
  40. $stmt = $pdo->prepare('SELECT * FROM plant_records WHERE id = ? AND rand = ?');
  41. $stmt->execute([$recordId, $randId]);
  42. $row = $stmt->fetch(PDO::FETCH_ASSOC);
  43. if (!$row) {
  44. http_response_code(404);
  45. die('Plant record not found');
  46. }
  47. // Load spec ranges
  48. $specs = [];
  49. if (!empty($row['crop_type'])) {
  50. $stmtSpec = $pdo->prepare('SELECT * FROM plant_specifications WHERE plant_type = ? LIMIT 1');
  51. $stmtSpec->execute([$row['crop_type']]);
  52. $specs = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
  53. }
  54. // Load saved report comments
  55. $savedComments = [
  56. 'general_details' => '',
  57. 'ai_interpretation' => '',
  58. 'recommended_details' => '',
  59. 'foliar_details' => '',
  60. ];
  61. // In print/headless mode there is no session user; use the record owner's id instead.
  62. $reportUserId = $userId ?? (int)($row['modx_user_id'] ?? 0);
  63. $stmtRpt = $pdo->prepare(
  64. 'SELECT comment FROM reports WHERE record_id = ? AND modx_user_id = ? ORDER BY id DESC LIMIT 1'
  65. );
  66. $stmtRpt->execute([$recordId, $reportUserId]);
  67. $savedRow = $stmtRpt->fetchColumn();
  68. if ($savedRow) {
  69. $decoded = json_decode($savedRow, true);
  70. if (is_array($decoded)) {
  71. $savedComments = array_merge($savedComments, $decoded);
  72. }
  73. }
  74. } catch (PDOException $e) {
  75. error_log('DB error in plant-report.php: ' . $e->getMessage());
  76. die('Database error occurred');
  77. }
  78. $h = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
  79. $today = date('jS F Y');
  80. $pageTitle = 'Plant Report' . (!empty($row['client_name']) ? ' — ' . $row['client_name'] : '');
  81. $siteName = 'Crop Monitor';
  82. // Parse and render markdown text from AI-generated report sections
  83. function formatReportText(string $text): string
  84. {
  85. if (trim($text) === '') {
  86. return '<p class="text-muted fst-italic">No content saved.</p>';
  87. }
  88. $parsedown = new Parsedown();
  89. $parsedown->setSafeMode(true);
  90. return $parsedown->text($text);
  91. }
  92. ?>
  93. <!doctype html>
  94. <html lang="en">
  95. <head>
  96. <meta charset="UTF-8">
  97. <meta name="viewport" content="width=device-width, initial-scale=1">
  98. <title>Plant Analysis Report | Crop Monitor</title>
  99. <link rel="icon" href="/favicon.ico?v=2" type="image/x-icon">
  100. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
  101. <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.css" rel="stylesheet" crossorigin="anonymous">
  102. <link href="/client-assets/css/dashboard.css" rel="stylesheet">
  103. <link rel="stylesheet" href="/client-assets/css/graphPrint.css" media="print">
  104. <style>
  105. @media print {
  106. .d-print-none { display: none !important; }
  107. .page-break { page-break-before: always; }
  108. body { font-size: 11px; }
  109. .report-section p { font-size: 11px; }
  110. }
  111. .report-section p { margin-bottom: 0.6rem; line-height: 1.6; }
  112. .report-section h1,
  113. .report-section h2,
  114. .report-section h3,
  115. .report-section h4 { font-size: 1rem; font-weight: 600; margin: 1rem 0 0.4rem; }
  116. .report-section ul,
  117. .report-section ol { padding-left: 1.4rem; margin-bottom: 0.6rem; }
  118. .report-section li { margin-bottom: 0.25rem; line-height: 1.5; }
  119. .report-section table {
  120. width: 100%;
  121. border-collapse: collapse;
  122. margin-bottom: 0.8rem;
  123. font-size: 0.85rem;
  124. }
  125. .report-section table th,
  126. .report-section table td {
  127. border: 1px solid #dee2e6;
  128. padding: 4px 8px;
  129. text-align: left;
  130. }
  131. .report-section table thead th {
  132. background: #f8f9fa;
  133. font-weight: 600;
  134. }
  135. .section-header {
  136. background: #212529;
  137. color: #fff;
  138. padding: 6px 12px;
  139. font-weight: 600;
  140. margin-bottom: 0;
  141. }
  142. .section-body {
  143. border: 1px solid #dee2e6;
  144. border-top: 0;
  145. padding: 14px 16px;
  146. margin-bottom: 1.2rem;
  147. }
  148. .title-table td, .title-table th { padding: 2px 8px; }
  149. .element-required-module .col { padding: 0 6px; }
  150. </style>
  151. </head>
  152. <body>
  153. <div class="container page" id="pdf-content">
  154. <?php if (!$row): ?>
  155. <div class="alert alert-danger mt-4">Record not found or access denied.</div>
  156. <?php else: ?>
  157. <!-- ── Header ──────────────────────────────────────────────────────────── -->
  158. <div class="row align-items-center mb-3 mt-3">
  159. <div class="col-3">
  160. <img class="img-fluid" src="/client-assets/images/crop-monitor.png"
  161. alt="Crop Monitor" style="max-height:55px;">
  162. </div>
  163. <div class="col-9 text-end">
  164. <div class="fw-bold h5 mb-0">Plant Analysis Report</div>
  165. <div class="text-muted small"><?= $h($today) ?></div>
  166. </div>
  167. </div>
  168. <table class="title-table w-100 mb-3 small">
  169. <tbody>
  170. <tr>
  171. <td class="text-end fw-bold text-nowrap">CLIENT:</td>
  172. <td><?= $h($row['client_name']) ?></td>
  173. <td></td>
  174. <td class="text-end fw-bold text-nowrap">SAMPLE ID:</td>
  175. <td><?= $h($row['site_id']) ?></td>
  176. </tr>
  177. <tr>
  178. <td class="text-end fw-bold text-nowrap">ADDRESS:</td>
  179. <td><?= $h($row['site_address']) ?></td>
  180. <td></td>
  181. <td class="text-end fw-bold text-nowrap">DATE SAMPLED:</td>
  182. <td><?= $h($row['date_sampled']) ?></td>
  183. </tr>
  184. <tr>
  185. <td></td>
  186. <td><?= $h($row['state_postcode']) ?></td>
  187. <td></td>
  188. <td class="text-end fw-bold text-nowrap">LAB NUMBER:</td>
  189. <td><?= $h($row['lab_no']) ?></td>
  190. </tr>
  191. <tr>
  192. <td></td>
  193. <td><?= $h($row['email']) ?></td>
  194. <td></td>
  195. <td class="text-end fw-bold text-nowrap">CROP:</td>
  196. <td><?= $h($row['sample_id']) ?></td>
  197. </tr>
  198. <tr>
  199. <td></td>
  200. <td></td>
  201. <td></td>
  202. <td class="text-end fw-bold text-nowrap">PLANT TYPE:</td>
  203. <td><?= $h($row['crop_type']) ?></td>
  204. </tr>
  205. </tbody>
  206. </table>
  207. <!-- ── Download / Back buttons ─────────────────────────────────────────── -->
  208. <div class="d-print-none mb-3 d-flex gap-2">
  209. <a href="/dashboard/crop-analysis/plant-test-data/plant-report.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>"
  210. class="btn btn-outline-secondary btn-sm">
  211. &larr; Back to Report
  212. </a>
  213. <a href="/pdf-files/headlessChrome_pdf.php?type=plant-report&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>"
  214. class="btn btn-success btn-sm">
  215. <i class="fas fa-download me-1"></i>Download PDF
  216. </a>
  217. <button class="btn btn-outline-dark btn-sm" onclick="window.print()">
  218. <i class="fas fa-print me-1"></i>Print
  219. </button>
  220. </div>
  221. <hr>
  222. <!-- ── 1. General Comment ─────────────────────────────────────────────────────── -->
  223. <div class="section-header">General Comment</div>
  224. <div class="section-body report-section">
  225. <?= formatReportText($savedComments['general_details'] ?? '') ?>
  226. </div>
  227. <!-- ── 2. AI Interpretation ───────────────────────────────────────── -->
  228. <div class="section-header">AI Plant Interpretation</div>
  229. <div class="section-body report-section">
  230. <?= formatReportText($savedComments['ai_interpretation'] ?? '') ?>
  231. </div>
  232. <!-- ── 3. Recommended Remedial Program ────────────────────────────────────────────────── -->
  233. <div class="section-header">
  234. <?= $h($savedComments['header1'] ?? 'Recommended Remedial Program') ?>
  235. </div>
  236. <div class="section-body report-section">
  237. <?= formatReportText($savedComments['recommended_details'] ?? '') ?>
  238. </div>
  239. <!-- ── 4. Foliar Program ─────────────────────────────────────────────── -->
  240. <div class="section-header">Foliar Program</div>
  241. <div class="section-body report-section">
  242. <?= formatReportText($savedComments['foliar_details'] ?? '') ?>
  243. </div>
  244. <!-- ── Disclaimer ──────────────────────────────────────────────────────── -->
  245. <div class="mt-3 pt-3 border-top">
  246. <p class="text-muted" style="font-size:0.7rem;">
  247. Any recommendations provided by Crop Monitor are advice only. We are not paid consultants
  248. and accept no responsibility for any of our suggestions. No control can be exercised over
  249. storage, handling, mixing, application, or use, or over weather, plant or soil conditions
  250. before, during or after application — all of which may affect the performance of our
  251. program. No responsibility for, or liability for, any failure in performance, losses,
  252. damage or injuries consequential or otherwise arising from such storage, mixing,
  253. application or use will be accepted under any circumstances whatsoever. The buyer assumes
  254. all responsibility for the use of any of our products.
  255. </p>
  256. </div>
  257. <?php endif; ?>
  258. </div><!-- /container -->
  259. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
  260. </body>
  261. </html>