Browse Source

Plant Analysis

Benjamin Harris 2 tháng trước cách đây
mục cha
commit
f68b0ae431
4 tập tin đã thay đổi với 238 bổ sung157 xóa
  1. 8 3
      .htaccess
  2. 207 139
      dashboard/crop-analysis/plant-test-data/plant-analysis.php
  3. 22 12
      dashboard/inbox.php
  4. 1 3
      layouts/footer.php

+ 8 - 3
.htaccess

@@ -33,6 +33,11 @@ RewriteRule (?:^|/)\. - [F,L]
 RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
 RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
 
-# ── Pass existing .php files directly (prevent vhost modX routing) ─────────
-RewriteCond %{REQUEST_FILENAME} -f
-RewriteRule \.php$ - [L]
+# ── Pass existing files and directories directly ─────────────────────────────
+RewriteCond %{REQUEST_FILENAME} -f [OR]
+RewriteCond %{REQUEST_FILENAME} -d
+RewriteRule ^ - [L]
+
+# ── Pass dashboard, login, controllers and other migrated paths directly ──────
+# (prevents the modX vhost catch-all from intercepting these requests)
+RewriteRule ^(dashboard|login|controllers|api|components|layouts|config|lib|pdf-files|client-assets|vendor)(/|$) - [L]

+ 207 - 139
dashboard/crop-analysis/plant-test-data/plant-analysis.php

@@ -1,110 +1,192 @@
 <?php
 /**
- * Plant analysis results display page.
- * Loads a single plant_records row by rid + rand params.
+ * dashboard/crop-analysis/plant-test-data/plant-analysis.php
+ *
+ * Plant tissue analysis results display page.
  */
 
 require_once __DIR__ . '/../../../config/database.php';
 require_once __DIR__ . '/../../../lib/auth.php';
+require_once __DIR__ . '/../../../lib/print_auth.php';
 
 if (session_status() === PHP_SESSION_NONE) {
     session_start();
 }
 
-requireLogin();
+$recordId  = (int)  ($_GET['rid']  ?? 0);
+$randId    = trim(  $_GET['rand']  ?? '');
+$clientId  = (int)  ($_GET['cid']  ?? 0);
+$printMode = isset($_GET['print']);
 
-$pdo    = getDBConnection();
-$userId = getCurrentUserId();
+if ($printMode) {
+    authenticatePrintPage($recordId, $randId);
+} else {
+    requireLogin();
+}
 
-$recordId = (int)   ($_GET['rid']  ?? 0);
-$randId   = (float) ($_GET['rand'] ?? 0);
+$pdo    = getDBConnection();
+$userId = $printMode ? null : getCurrentUserId();
 
-$row  = null;
+$row   = null;
 $specs = [];
 
-if ($recordId > 0) {
+if ($recordId > 0 && $randId !== '') {
     $stmt = $pdo->prepare('SELECT * FROM plant_records WHERE id = ? AND rand = ? LIMIT 1');
     $stmt->execute([$recordId, $randId]);
-    $row = $stmt->fetch();
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
 }
 
 if ($row) {
-    $crop = $row['crop'] ?? '';
-    $stmt = $pdo->prepare('SELECT * FROM plant_specifications WHERE crop = ? LIMIT 1');
-    $stmt->execute([$crop]);
-    $specs = $stmt->fetch() ?: [];
+    // Column is plant_type, not crop
+    $plantType = $row['crop_type'] ?? '';
+    if ($plantType !== '') {
+        $stmtSpec = $pdo->prepare('SELECT * FROM plant_specifications WHERE plant_type = ? LIMIT 1');
+        $stmtSpec->execute([$plantType]);
+        $specs = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
+    }
 }
 
-$h = fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
+// ── Spec column name map ──────────────────────────────────────────────────────
+// The plant_specifications table has inconsistent column casing — map each
+// element key to its actual min/max column names in the DB.
+$specCols = [
+    'n'  => ['n_min',  'n_max'],
+    'p'  => ['P_Min',  'P_Max'],
+    'k'  => ['K_Min',  'K_Max'],
+    's'  => ['S_Min',  'S_Max'],
+    'mg' => ['Mg_Min', 'Mg_Max'],
+    'ca' => ['Ca_Min', 'Ca_Max'],
+    'na' => ['Na_Min', 'Na_Max'],
+    'fe' => ['Fe_Min', 'Fe_Max'],
+    'mn' => ['Mn_Min', 'Mn_Max'],
+    'zn' => ['Zn_Min', 'Zn_Max'],
+    'cu' => ['Cu_Min', 'Cu_Max'],
+    'b'  => ['B_Min',  'B_Max'],
+    'm'  => ['M_Min',  'M_Max'],
+    'co' => ['Co_min', 'Co_max'],
+    'se' => ['se_min', 'se_max'],
+    'cl' => ['cl_min', 'cl_max'],
+    'c'  => ['c_min',  'c_max'],
+];
+
+$h = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
 
 function statusBar(float $found, float $min, float $max): string {
     if ($max <= 0) return '<td colspan="3" class="text-muted text-center small">N/A</td>';
-    $pct = ($max > $min) ? min(100, max(0, ($found - $min) / ($max - $min) * 100)) : 0;
     if ($found < $min) {
-        $bar = '<td class="text-center"><div class="progress"><div class="progress-bar bg-danger" style="width:100%"></div></div></td>';
-        $bar .= '<td></td><td></td>';
-    } elseif ($found > $max) {
-        $bar = '<td></td><td></td>';
-        $bar .= '<td class="text-center"><div class="progress"><div class="progress-bar bg-warning" style="width:100%"></div></div></td>';
-    } else {
-        $bar = '<td></td>';
-        $bar .= '<td class="text-center"><div class="progress"><div class="progress-bar bg-success" style="width:' . round($pct) . '%"></div></div></td>';
-        $bar .= '<td></td>';
+        return '<td class="text-center"><div class="progress"><div class="progress-bar bg-danger" style="width:100%"></div></div></td><td></td><td></td>';
     }
-    return $bar;
+    if ($found > $max) {
+        return '<td></td><td></td><td class="text-center"><div class="progress"><div class="progress-bar bg-warning" style="width:100%"></div></div></td>';
+    }
+    $pct = ($max > $min) ? round(min(100, ($found - $min) / ($max - $min) * 100)) : 50;
+    return '<td></td><td class="text-center"><div class="progress"><div class="progress-bar bg-success" style="width:' . $pct . '%"></div></div></td><td></td>';
+}
+
+$today     = date('jS F Y');
+$pageTitle = 'Plant Analysis' . (!empty($row['client_name']) ? ' — ' . $row['client_name'] : '');
+$siteName  = 'Crop Monitor';
+
+if (!$printMode) {
+    include __DIR__ . '/../../../layouts/header.php';
+    include __DIR__ . '/../../../layouts/navbar.php';
 }
 ?>
-<!doctype html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <title>Plant Analysis | Crop Monitor</title>
-    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
-    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.css" crossorigin="anonymous" rel="stylesheet">
-    <link href="/client-assets/css/dashboard-2021.css" rel="stylesheet">
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js" crossorigin="anonymous"></script>
-    <style>
-        @media print {
-            @page { size: A4 portrait; margin: 0.5cm; }
-            .d-print-none { display: none !important; }
-        }
-        .progress { border-radius: 0 !important; }
-        table.chart { width: 100%; border-collapse: collapse; }
-        table.chart th, table.chart td { padding: 3px 6px; font-size: 0.85rem; }
-        .chart-header th { background: #343a40; color: white; }
-        .chart-header-sub th { background: #6c757d; color: white; }
-        .lightgreen { background: #d4edda !important; color: #155724 !important; }
-        .lightred   { background: #f8d7da !important; color: #721c24 !important; }
-        .lightpurple { background: #e2d9f3 !important; color: #432874 !important; }
-        .stripe-1   { background: #f8f9fa; }
-        .border-left  { border-left: 1px solid #dee2e6; }
-        .border-right { border-right: 1px solid #dee2e6; }
-        .border-bottom { border-bottom: 1px solid #dee2e6; }
-        .border-top { border-top: 1px solid #dee2e6; }
-    </style>
-</head>
-<body>
+
+<?php if (!$printMode): ?>
+<link rel="stylesheet" href="/client-assets/home/css/graphPrint.css" media="print">
+<style>
+    .progress { border-radius: 0 !important; }
+    table.chart { width: 100%; border-collapse: collapse; }
+    table.chart th, table.chart td { padding: 3px 6px; font-size: 0.85rem; }
+    .chart-header th { background: #343a40; color: white; }
+    .chart-header-sub th { background: #6c757d; color: white; }
+    .lightgreen  { background: #d4edda !important; color: #155724 !important; }
+    .lightred    { background: #f8d7da !important; color: #721c24 !important; }
+    .lightpurple { background: #e2d9f3 !important; color: #432874 !important; }
+    .stripe-1    { background: #f8f9fa; }
+    .border-left   { border-left: 1px solid #dee2e6; }
+    .border-right  { border-right: 1px solid #dee2e6; }
+    .border-bottom { border-bottom: 1px solid #dee2e6; }
+    .border-top    { border-top: 1px solid #dee2e6; }
+</style>
+
+<div id="layoutSidenav">
+    <div id="layoutSidenav_nav">
+        <?php include __DIR__ . '/../../../layouts/sidebar.php'; ?>
+    </div>
+    <div id="layoutSidenav_content">
+        <main>
+            <div class="container-fluid px-4">
+
+<?php endif; ?>
+
 <div class="container" id="content">
 
     <?php if (!$row): ?>
         <div class="alert alert-danger mt-4">Record not found or access denied.</div>
     <?php else: ?>
 
-    <div class="row mb-3">
+    <!-- ── Header ──────────────────────────────────────────────────────────── -->
+    <div class="row mb-2 mt-3">
         <div class="col-md-3">
-            <img class="img-fluid" src="/client-assets/images/crop-monitor.png" alt="Crop Monitor">
+            <img class="img-fluid" src="/client-assets/images/crop-monitor.png" alt="Crop Monitor" style="max-height:55px;">
         </div>
     </div>
 
-    <div class="d-print-none mb-3">
-        <button class="btn btn-success btn-sm downloadPDF">
-            <i class="fas fa-download me-1"></i>Download PDF
-        </button>
+    <table class="title w-100 mb-3 small">
+        <tbody>
+            <tr>
+                <td class="text-end fw-bold text-nowrap">DATE:</td>
+                <td><?= $h($today) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">SAMPLE ID:</td>
+                <td><?= $h($row['site_id']) ?></td>
+            </tr>
+            <tr>
+                <td class="text-end fw-bold text-nowrap">CLIENT:</td>
+                <td><?= $h($row['client_name']) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">DATE SAMPLED:</td>
+                <td><?= $h($row['date_sampled']) ?></td>
+            </tr>
+            <tr>
+                <td class="text-end fw-bold text-nowrap">ADDRESS:</td>
+                <td><?= $h($row['site_address']) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">LAB NUMBER:</td>
+                <td><?= $h($row['lab_no']) ?></td>
+            </tr>
+            <tr>
+                <td></td>
+                <td><?= $h($row['state_postcode']) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">CROP TYPE:</td>
+                <td><?= $h($row['crop_type']) ?></td>
+            </tr>
+        </tbody>
+    </table>
+
+    <!-- ── Action buttons ──────────────────────────────────────────────────── -->
+    <?php if (!$printMode): ?>
+    <div class="d-print-none mb-3 d-flex gap-2 flex-wrap">
+        <a href="/dashboard/crop-analysis/plant-test-data/plant-report.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
+           class="btn btn-outline-primary btn-sm">
+            <i class="fas fa-file-alt me-1"></i>View Report
+        </a>
+        <a href="/pdf-files/headlessChrome_pdf.php?type=plant-analysis&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
+           class="btn btn-outline-secondary btn-sm">
+            <i class="fas fa-file-pdf me-1"></i>PDF — Analysis
+        </a>
+        <a href="/pdf-files/headlessChrome_pdf.php?type=plant&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
+           class="btn btn-success btn-sm">
+            <i class="fas fa-file-pdf me-1"></i>PDF — Analysis &amp; Report
+        </a>
     </div>
+    <?php endif; ?>
 
     <div class="row">
-        <div class="col-md-12 text-center fw-bold h4">ANALYSIS RESULTS</div>
+        <div class="col-md-12 text-center fw-bold h4">Plant Tissue Analysis Results</div>
     </div>
 
     <hr class="p-1 m-1">
@@ -116,103 +198,98 @@ function statusBar(float $found, float $min, float $max): string {
                 <th colspan="3" class="text-center border-right border-top">STATUS</th>
             </tr>
             <tr class="chart-header-sub">
-                <th class="text-center border-bottom border-left">Elements</th>
+                <th class="text-center border-bottom border-left">Element</th>
                 <th class="text-center border-bottom">Desired</th>
                 <th class="text-center border-bottom">Found</th>
                 <th class="text-center stripe-1">Deficient</th>
                 <th class="text-center stripe-1">Ideal</th>
                 <th class="text-center border-right stripe-1">High</th>
             </tr>
-            <tr>
-                <td class="border-left" colspan="6"></td>
-            </tr>
+            <tr><td class="border-left" colspan="6"></td></tr>
+
+            <!-- Major Elements -->
             <tr class="chart-header-sub">
-                <th colspan="3" class="border-left text-center lightgreen">MAJOR ELEMENTS</th>
-                <th class="text-center border-left stripe-1"></th>
-                <th class="text-center border-left stripe-1"></th>
-                <th class="text-center border-left border-right stripe-1"></th>
+                <th colspan="6" class="border-left text-center lightgreen">MAJOR ELEMENTS (%)</th>
             </tr>
-
             <?php
             $majorElements = [
-                ['n',  'Nitrogen',   '%'],
-                ['p',  'Phosphorus', '%'],
-                ['k',  'Potassium',  '%'],
-                ['mg', 'Magnesium',  '%'],
-                ['ca', 'Calcium',    '%'],
-                ['na', 'Sodium',     '%'],
+                ['n',  'Nitrogen',   '%', 3],
+                ['p',  'Phosphorus', '%', 3],
+                ['k',  'Potassium',  '%', 3],
+                ['s',  'Sulfur',     '%', 3],
+                ['mg', 'Magnesium',  '%', 3],
+                ['ca', 'Calcium',    '%', 3],
+                ['na', 'Sodium',     '%', 3],
             ];
-            foreach ($majorElements as [$el, $nutrient, $unit]):
-                $found = (float) ($row[$el] ?? 0);
-                $min   = (float) ($specs['min_' . $el] ?? 0);
-                $max   = (float) ($specs['max_' . $el] ?? 0);
-                $desired = ($min > 0 || $max > 0) ? number_format($min, 2) . '–' . number_format($max, 2) : '—';
+            foreach ($majorElements as [$el, $nutrient, $unit, $dp]):
+                $found   = (float)($row[$el] ?? 0);
+                [$minCol, $maxCol] = $specCols[$el];
+                $min     = (float)($specs[$minCol] ?? 0);
+                $max     = (float)($specs[$maxCol] ?? 0);
+                $desired = ($min > 0 || $max > 0) ? number_format($min, $dp) . '–' . number_format($max, $dp) : '—';
             ?>
             <tr>
                 <td class="border-left"><?= $h($nutrient) ?> (<?= $h($unit) ?>)</td>
                 <td><?= $h($desired) ?></td>
-                <td><?= $found > 0 ? number_format($found, 3) : '—' ?></td>
+                <td><?= $found > 0 ? number_format($found, $dp) : '—' ?></td>
                 <?= statusBar($found, $min, $max) ?>
             </tr>
             <?php endforeach; ?>
 
             <tr><td colspan="6" class="border-left"></td></tr>
+
+            <!-- Trace Elements -->
             <tr class="chart-header-sub">
-                <th colspan="3" class="border-left text-center lightred">TRACE ELEMENTS</th>
-                <th class="text-center border-left stripe-1"></th>
-                <th class="text-center border-left stripe-1"></th>
-                <th class="text-center border-left border-right stripe-1"></th>
+                <th colspan="6" class="border-left text-center lightred">TRACE ELEMENTS (ppm)</th>
             </tr>
-            <tr><td colspan="6" class="border-left"></td></tr>
-
             <?php
             $traceElements = [
-                ['fe', 'Iron',      'ppm'],
-                ['mn', 'Manganese', 'ppm'],
-                ['zn', 'Zinc',      'ppm'],
-                ['cu', 'Copper',    'ppm'],
-                ['b',  'Boron',     'ppm'],
+                ['fe', 'Iron',      'ppm', 1],
+                ['mn', 'Manganese', 'ppm', 1],
+                ['zn', 'Zinc',      'ppm', 1],
+                ['cu', 'Copper',    'ppm', 1],
+                ['b',  'Boron',     'ppm', 1],
             ];
-            foreach ($traceElements as [$el, $nutrient, $unit]):
-                $found = (float) ($row[$el] ?? 0);
-                $min   = (float) ($specs['min_' . $el] ?? 0);
-                $max   = (float) ($specs['max_' . $el] ?? 0);
-                $desired = ($min > 0 || $max > 0) ? number_format($min, 1) . '–' . number_format($max, 1) : '—';
+            foreach ($traceElements as [$el, $nutrient, $unit, $dp]):
+                $found   = (float)($row[$el] ?? 0);
+                [$minCol, $maxCol] = $specCols[$el];
+                $min     = (float)($specs[$minCol] ?? 0);
+                $max     = (float)($specs[$maxCol] ?? 0);
+                $desired = ($min > 0 || $max > 0) ? number_format($min, $dp) . '–' . number_format($max, $dp) : '—';
             ?>
             <tr>
                 <td class="border-left"><?= $h($nutrient) ?> (<?= $h($unit) ?>)</td>
                 <td><?= $h($desired) ?></td>
-                <td><?= $found > 0 ? number_format($found, 2) : '—' ?></td>
+                <td><?= $found > 0 ? number_format($found, $dp) : '—' ?></td>
                 <?= statusBar($found, $min, $max) ?>
             </tr>
             <?php endforeach; ?>
 
             <tr><td colspan="6" class="border-left"></td></tr>
+
+            <!-- Other Elements -->
             <tr class="chart-header-sub">
-                <th colspan="3" class="border-left text-center lightpurple">MACRO ELEMENTS</th>
-                <th class="text-center border-left stripe-1"></th>
-                <th class="text-center border-left stripe-1"></th>
-                <th class="text-center border-left border-right stripe-1"></th>
+                <th colspan="6" class="border-left text-center lightpurple">OTHER ELEMENTS (ppm)</th>
             </tr>
-
             <?php
-            $macroElements = [
-                ['m',  'Molybdenum', 'ppm'],
-                ['co', 'Cobalt',     'ppm'],
-                ['se', 'Selenium',   'ppm'],
-                ['cl', 'Chloride',   'ppm'],
-                ['c',  'Carbon',     'ppm'],
+            $otherElements = [
+                ['m',  'Molybdenum', 'ppm', 2],
+                ['co', 'Cobalt',     'ppm', 2],
+                ['se', 'Selenium',   'ppm', 2],
+                ['cl', 'Chloride',   'ppm', 2],
+                ['c',  'Carbon',     '%',   2],
             ];
-            foreach ($macroElements as [$el, $nutrient, $unit]):
-                $found = (float) ($row[$el] ?? 0);
-                $min   = (float) ($specs['min_' . $el] ?? 0);
-                $max   = (float) ($specs['max_' . $el] ?? 0);
-                $desired = ($min > 0 || $max > 0) ? number_format($min, 2) . '–' . number_format($max, 2) : '—';
+            foreach ($otherElements as [$el, $nutrient, $unit, $dp]):
+                $found   = (float)($row[$el] ?? 0);
+                [$minCol, $maxCol] = $specCols[$el];
+                $min     = (float)($specs[$minCol] ?? 0);
+                $max     = (float)($specs[$maxCol] ?? 0);
+                $desired = ($min > 0 || $max > 0) ? number_format($min, $dp) . '–' . number_format($max, $dp) : '—';
             ?>
             <tr>
                 <td class="border-left"><?= $h($nutrient) ?> (<?= $h($unit) ?>)</td>
                 <td><?= $h($desired) ?></td>
-                <td><?= $found > 0 ? number_format($found, 3) : '—' ?></td>
+                <td><?= $found > 0 ? number_format($found, $dp) : '—' ?></td>
                 <?= statusBar($found, $min, $max) ?>
             </tr>
             <?php endforeach; ?>
@@ -225,7 +302,6 @@ function statusBar(float $found, float $min, float $max): string {
                 <td class="border-bottom"></td>
                 <td class="border-bottom border-right"></td>
             </tr>
-
         </tbody>
     </table>
 
@@ -233,26 +309,18 @@ function statusBar(float $found, float $min, float $max): string {
         <p><i class="fa fa-leaf" style="color:green"></i> It is always an advantage to assess tissue analysis results along with a corresponding soil analysis for more accurate diagnosis of plant nutrient status.</p>
         <p><i class="fa fa-leaf" style="color:green"></i> Trace element levels — manganese, copper and zinc — can all be affected by fungicide spray residues, giving misleading results.</p>
         <p><i class="fa fa-leaf" style="color:green"></i> Talk to your qualified consultant to make a plan for correction or maintenance of the found nutrient levels.</p>
-        <p style="font-style:italic;font-size:9px;">Desired ranges indexed from: CSIRO Plant Analysis Handbook 2nd Ed. Hill Laboratories consultants guide. PIRSA Soil and Plant Analysis.</p>
-        <p style="font-style:italic;font-size:9px;">Any recommendations provided by Cropmonitor are advice only. We are not paid consultants and are not covered to accept responsibility for any of our suggestions.</p>
+        <p class="fst-italic" style="font-size:9px;">Desired ranges indexed from: CSIRO Plant Analysis Handbook 2nd Ed. Hill Laboratories consultants guide. PIRSA Soil and Plant Analysis.</p>
+        <p class="fst-italic" style="font-size:9px;">Any recommendations provided by Cropmonitor are advice only. We are not paid consultants and accept no responsibility for any of our suggestions.</p>
     </div>
 
     <?php endif; ?>
 
-</div>
-
-<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
-<script>
-document.querySelector('.downloadPDF') && document.querySelector('.downloadPDF').addEventListener('click', function () {
-    var opt = {
-        margin: 3,
-        filename: 'plant-analysis.pdf',
-        image: { type: 'jpeg', quality: 1.0 },
-        html2canvas: { scale: 2, letterRendering: true, windowWidth: 1024 },
-        jsPDF: { orientation: 'portrait', unit: 'mm', format: 'a4' }
-    };
-    html2pdf().from(document.getElementById('content')).set(opt).save();
-});
-</script>
-</body>
-</html>
+</div><!-- /.container -->
+
+<?php if (!$printMode): ?>
+            </div><!-- /.container-fluid -->
+        </main>
+        <?php include __DIR__ . '/../../../layouts/footer.php'; ?>
+    </div><!-- /#layoutSidenav_content -->
+</div><!-- /#layoutSidenav -->
+<?php endif; ?>

+ 22 - 12
dashboard/inbox.php

@@ -18,9 +18,14 @@ $pdo    = getDBConnection();
 $userId = getCurrentUserId();
 
 function countRecords(\PDO $pdo, string $table, int $userId, int $status = 0): int {
-    $stmt = $pdo->prepare("SELECT COUNT(*) FROM `$table` WHERE modx_user_id = ? AND status = ?");
-    $stmt->execute([$userId, $status]);
-    return (int) $stmt->fetchColumn();
+    try {
+        $stmt = $pdo->prepare("SELECT COUNT(*) FROM `$table` WHERE modx_user_id = ? AND status = ?");
+        $stmt->execute([$userId, $status]);
+        return (int) $stmt->fetchColumn();
+    } catch (\PDOException $e) {
+        error_log("countRecords error on $table: " . $e->getMessage());
+        return 0;
+    }
 }
 
 $counts = [
@@ -33,20 +38,25 @@ $counts = [
 ];
 
 function fetchHistory(\PDO $pdo, string $table, int $userId, string $select = 'id, rand, lab_no, sample_id, site_id, crop_type, date_sampled'): array {
-    $stmt = $pdo->prepare(
-        "SELECT {$select}
-         FROM `{$table}`
-         WHERE modx_user_id = ? AND status = 0
-         ORDER BY id DESC LIMIT 100"
-    );
-    $stmt->execute([$userId]);
-    return $stmt->fetchAll();
+    try {
+        $stmt = $pdo->prepare(
+            "SELECT {$select}
+             FROM `{$table}`
+             WHERE modx_user_id = ? AND status = 0
+             ORDER BY id DESC LIMIT 100"
+        );
+        $stmt->execute([$userId]);
+        return $stmt->fetchAll();
+    } catch (\PDOException $e) {
+        error_log("fetchHistory error on $table: " . $e->getMessage());
+        return [];
+    }
 }
 
 $soilRows   = fetchHistory($pdo, 'soil_records',   $userId);
 $plantRows  = fetchHistory($pdo, 'plant_records',  $userId);
 $waterRows  = fetchHistory($pdo, 'water_records',  $userId, 'id, rand, lab_no, sample_id, site_id, date_sampled');
-$animalRows = fetchHistory($pdo, 'animal_records', $userId, 'id, rand, lab_no, sample_id, NULL AS site_id, date_sampled');
+$animalRows = fetchHistory($pdo, 'animal_records', $userId, "id, rand, lab_no, sample_id, '' AS site_id, date_sampled");
 
 $h = fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
 

+ 1 - 3
layouts/footer.php

@@ -18,9 +18,7 @@
 </div><!-- /layoutSidenav_content -->
 </div><!-- /layoutSidenav -->
 
-<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"
-        integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
-        crossorigin="anonymous"></script>
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.min.js" integrity="sha384-G/EV+4j2dNv+tEPo3++6LCgdCROaejBqfUeNjuKAiuXbjrxilcCdDz6ZAVfHWe1Y" crossorigin="anonymous"></script>
 
 <script>
     window.addEventListener('DOMContentLoaded', () => {