Explorar el Código

update all tests

Benjamin Harris hace 2 meses
padre
commit
d78243d7e0

+ 88 - 74
dashboard/crop-analysis/animal-dietary-balance/animal-dietary-balance.php

@@ -1,99 +1,129 @@
 <?php
 /**
- * Animal Dietary Balance results display page.
- * Loads a single animal_records row by rid + rand params.
+ * dashboard/crop-analysis/animal-dietary-balance/animal-dietary-balance.php
+ *
+ * Animal dietary balance results display page.
+ * Supports normal browser access (session login) and headless Chrome (ptoken).
  */
 
 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']) || isset($_GET['ptoken']);
 
-$pdo    = getDBConnection();
-$userId = getCurrentUserId();
+if (!$recordId || $randId === '') {
+    http_response_code(400);
+    die('Invalid request parameters');
+}
 
-$recordId = (int)   ($_GET['rid']  ?? 0);
-$randId   = (float) ($_GET['rand'] ?? 0);
+$chromeAccess = authenticatePrintPage($recordId, $randId);
+
+$pdo    = getDBConnection();
+$userId = $chromeAccess ? null : getCurrentUserId();
 
 $row = null;
-if ($recordId > 0) {
-    $stmt = $pdo->prepare('SELECT * FROM animal_records WHERE id = ? AND rand = ? LIMIT 1');
-    $stmt->execute([$recordId, $randId]);
-    $row = $stmt->fetch();
-}
+$stmt = $pdo->prepare('SELECT * FROM animal_records WHERE id = ? AND rand = ? LIMIT 1');
+$stmt->execute([$recordId, $randId]);
+$row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+$h = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
+
+$today     = date('jS F Y');
+$pageTitle = 'Animal Dietary Balance' . (!empty($row['client_name']) ? ' — ' . $row['client_name'] : '');
+$siteName  = 'Crop Monitor';
 
-$h = fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
+if (!$printMode) {
+    include __DIR__ . '/../../../layouts/header.php';
+}
 ?>
-<!doctype html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <title>Animal Dietary Balance | 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; }
-    </style>
-</head>
-<body>
+
+<link rel="stylesheet" href="/client-assets/home/css/graphPrint.css" media="print">
+<style>
+    @media print {
+        @page { size: A4 portrait; }
+        body { min-width: 992px !important; }
+        .container { min-width: 992px !important; }
+    }
+    .progress { border-radius: 0 !important; }
+</style>
+
 <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">
         </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>
-    </div>
-
-    <div class="row">
-        <div class="col-md-12 text-center fw-bold h4">ANIMAL DIETARY MINERAL BALANCE</div>
-    </div>
-
-    <hr class="p-1 m-1">
-
-    <table class="table table-bordered table-sm">
+    <table class="title w-100 mb-3 small">
         <tbody>
             <tr>
-                <td class="fw-bold text-end">Client:</td>
-                <td><?= $h($row['client_name'] ?? '') ?></td>
-                <td class="fw-bold text-end">Sample ID:</td>
-                <td><?= $h($row['sample_id'] ?? '') ?></td>
+                <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['sample_id']) ?></td>
             </tr>
             <tr>
-                <td class="fw-bold text-end">Lab No:</td>
-                <td><?= $h($row['lab_no'] ?? '') ?></td>
-                <td class="fw-bold text-end">Date Sampled:</td>
-                <td><?= $h($row['date_sampled'] ?? '') ?></td>
+                <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="fw-bold text-end">Analysis Type:</td>
+                <td class="text-end fw-bold text-nowrap">LAB NO:</td>
+                <td><?= $h($row['lab_no']) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">SITE ID:</td>
+                <td><?= $h($row['site_id']) ?></td>
+            </tr>
+            <tr>
+                <td class="text-end fw-bold text-nowrap">ANALYSIS TYPE:</td>
                 <td><?= $h($row['analysis_type'] ?? '') ?></td>
-                <td class="fw-bold text-end">Site ID:</td>
-                <td><?= $h($row['site_id'] ?? '') ?></td>
+                <td></td>
+                <td></td>
+                <td></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/animal-dietary-balance/animal-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=animal-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=animal&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">ANIMAL DIETARY MINERAL BALANCE</div>
+    </div>
+    <hr class="p-1 m-1">
+
     <!-- Analysis calculation rows — pending PHP migration of animalAnalysisCalcs -->
     <div class="alert alert-info mt-3">
         <i class="fas fa-info-circle me-1"></i>
@@ -111,20 +141,4 @@ $h = fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
 
     <?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: 'animal-dietary-balance.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 -->

+ 116 - 0
dashboard/crop-analysis/animal-dietary-balance/animal-print-combined.php

@@ -0,0 +1,116 @@
+<?php
+/**
+ * animal-print-combined.php
+ *
+ * Combined print/PDF page: Animal Analysis + Animal Report in one document.
+ * Accessed by headlessChrome_pdf.php (type=animal) — never directly by users.
+ * Auth is via the shared ptoken written by the generator.
+ */
+
+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();
+}
+
+$recordId = (int)  ($_GET['rid']   ?? 0);
+$randId   = trim(  $_GET['rand']   ?? '');
+$clientId = (int)  ($_GET['cid']   ?? 0);
+$ptoken   = trim(  $_GET['ptoken'] ?? '');
+
+authenticatePrintPage($recordId, $randId);
+
+$coverpageUrl = '/dashboard/crop-analysis/coverpage.php?'
+    . http_build_query([
+        'type'   => 'animal',
+        'rid'    => $recordId,
+        'rand'   => $randId,
+        'cid'    => $clientId,
+        'ptoken' => $ptoken,
+        'print'  => '1',
+    ]);
+
+$analysisUrl = '/dashboard/crop-analysis/animal-dietary-balance/animal-dietary-balance.php?'
+    . http_build_query([
+        'rid'    => $recordId,
+        'rand'   => $randId,
+        'cid'    => $clientId,
+        'ptoken' => $ptoken,
+        'print'  => '1',
+    ]);
+
+$reportUrl = '/dashboard/crop-analysis/animal-dietary-balance/animal-report-pdf.php?'
+    . http_build_query([
+        'rid'    => $recordId,
+        'rand'   => $randId,
+        'ptoken' => $ptoken,
+    ]);
+
+function h(string $v): string {
+    return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
+}
+?>
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Animal Dietary Balance &amp; Report</title>
+    <style>
+        * { margin: 0; padding: 0; box-sizing: border-box; }
+        body { background: #fff; }
+        iframe {
+            width: 100%;
+            border: none;
+            display: block;
+            min-height: 200px;
+        }
+        .page-section { page-break-after: always; }
+        .page-section:last-child { page-break-after: avoid; }
+        @media print {
+            @page { size: A4 portrait; margin: 0; }
+            html, body { margin: 0; padding: 0; background: #fff; }
+            body { min-width: 1030px !important; font-size: 0.75em; }
+            .container { min-width: 1030px !important; }
+            .report-textarea { border: none; }
+        }
+    </style>
+</head>
+<body>
+
+<div class="page-section page">
+    <iframe id="frame-coverpage" src="<?= h($coverpageUrl) ?>" scrolling="no"></iframe>
+</div>
+
+<div class="page-section page">
+    <iframe id="frame-analysis" src="<?= h($analysisUrl) ?>" scrolling="no"></iframe>
+</div>
+
+<div class="page-section page">
+    <iframe id="frame-report" src="<?= h($reportUrl) ?>" scrolling="no"></iframe>
+</div>
+
+<script>
+(function () {
+    function resizeFrame(iframe) {
+        try {
+            var h = iframe.contentDocument.documentElement.scrollHeight;
+            if (h > 0) iframe.style.height = h + 'px';
+        } catch (e) {
+            iframe.style.height = '2970px';
+        }
+    }
+
+    var cover = document.getElementById('frame-coverpage');
+    if (cover) cover.style.height = '1457px';
+
+    ['frame-analysis', 'frame-report'].forEach(function (id) {
+        var el = document.getElementById(id);
+        el.addEventListener('load', function () { resizeFrame(el); });
+    });
+})();
+</script>
+
+</body>
+</html>

+ 209 - 0
dashboard/crop-analysis/animal-dietary-balance/animal-report-pdf.php

@@ -0,0 +1,209 @@
+<?php
+/**
+ * animal-report-pdf.php
+ *
+ * Print / PDF-export version of a completed animal dietary balance report.
+ * Normal access: ?rid=<id>&rand=<token>
+ * Headless Chrome: ?rid=<id>&rand=<token>&ptoken=<one-time-token>
+ */
+
+require_once __DIR__ . '/../../../config/database.php';
+require_once __DIR__ . '/../../../lib/auth.php';
+require_once __DIR__ . '/../../../lib/print_auth.php';
+require_once __DIR__ . '/../../../vendor/autoload.php';
+
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+$recordId  = (int)  ($_GET['rid']  ?? 0);
+$randId    = trim(  $_GET['rand']  ?? '');
+$clientId  = (int)  ($_GET['cid']  ?? 0);
+$printMode = isset($_GET['ptoken']) || isset($_GET['print']);
+
+if (!$recordId || $randId === '') {
+    http_response_code(400);
+    die('Invalid request parameters');
+}
+
+$chromeAccess = authenticatePrintPage($recordId, $randId);
+
+try {
+    $pdo    = getDBConnection();
+    $userId = $chromeAccess ? null : getCurrentUserId();
+
+    $stmt = $pdo->prepare('SELECT * FROM animal_records WHERE id = ? AND rand = ?');
+    $stmt->execute([$recordId, $randId]);
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    if (!$row) {
+        http_response_code(404);
+        die('Animal record not found');
+    }
+
+    // Load saved report comments
+    $savedComments = [
+        'general_details'     => '',
+        'ai_interpretation'   => '',
+        'recommended_details' => '',
+        'foliar_details'      => '',
+    ];
+    $reportUserId = $userId ?? (int)($row['modx_user_id'] ?? 0);
+    $stmtRpt = $pdo->prepare(
+        'SELECT comment FROM reports WHERE record_id = ? AND modx_user_id = ? ORDER BY id DESC LIMIT 1'
+    );
+    $stmtRpt->execute([$recordId, $reportUserId]);
+    $savedRow = $stmtRpt->fetchColumn();
+    if ($savedRow) {
+        $decoded = json_decode($savedRow, true);
+        if (is_array($decoded)) {
+            $savedComments = array_merge($savedComments, $decoded);
+        }
+    }
+
+} catch (PDOException $e) {
+    error_log('DB error in animal-report-pdf.php: ' . $e->getMessage());
+    die('Database error occurred');
+}
+
+$h     = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
+$today = date('jS F Y');
+
+function formatReportText(string $text): string
+{
+    if (trim($text) === '') {
+        return '<p class="text-muted fst-italic">No content saved.</p>';
+    }
+    $parsedown = new Parsedown();
+    $parsedown->setSafeMode(true);
+    return $parsedown->text($text);
+}
+?>
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Animal Dietary Balance Report | Crop Monitor</title>
+    <link rel="icon" href="/favicon.ico?v=2" type="image/x-icon">
+    <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" rel="stylesheet" crossorigin="anonymous">
+    <link href="/client-assets/css/dashboard.css" rel="stylesheet">
+    <style>
+        @media print {
+            .d-print-none { display: none !important; }
+            body { font-size: 11px; }
+        }
+        .report-section p   { margin-bottom: 0.6rem; line-height: 1.6; }
+        .report-section h1, .report-section h2,
+        .report-section h3, .report-section h4 { font-size: 1rem; font-weight: 600; margin: 1rem 0 0.4rem; }
+        .report-section ul, .report-section ol  { padding-left: 1.4rem; margin-bottom: 0.6rem; }
+        .report-section li  { margin-bottom: 0.25rem; line-height: 1.5; }
+        .section-header {
+            background: #212529; color: #fff;
+            padding: 6px 12px; font-weight: 600; margin-bottom: 0;
+        }
+        .section-body {
+            border: 1px solid #dee2e6; border-top: 0;
+            padding: 14px 16px; margin-bottom: 1.2rem;
+        }
+        .title-table td, .title-table th { padding: 2px 8px; }
+    </style>
+</head>
+<body>
+
+<div class="container page" id="pdf-content">
+
+    <?php if (!$row): ?>
+        <div class="alert alert-danger mt-4">Record not found or access denied.</div>
+    <?php else: ?>
+
+    <!-- ── Header ──────────────────────────────────────────────────────────── -->
+    <div class="row align-items-center mb-3 mt-3">
+        <div class="col-3">
+            <img class="img-fluid" src="/client-assets/images/crop-monitor.png"
+                 alt="Crop Monitor" style="max-height:55px;">
+        </div>
+        <div class="col-9 text-end">
+            <div class="fw-bold h5 mb-0">Animal Dietary Balance Report</div>
+            <div class="text-muted small"><?= $h($today) ?></div>
+        </div>
+    </div>
+
+    <table class="title-table w-100 mb-3 small">
+        <tbody>
+            <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">SAMPLE ID:</td>
+                <td><?= $h($row['sample_id']) ?></td>
+            </tr>
+            <tr>
+                <td class="text-end fw-bold text-nowrap">SITE ID:</td>
+                <td><?= $h($row['site_id']) ?></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">ANALYSIS TYPE:</td>
+                <td><?= $h($row['analysis_type'] ?? '') ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">LAB NUMBER:</td>
+                <td><?= $h($row['lab_no']) ?></td>
+            </tr>
+        </tbody>
+    </table>
+
+    <!-- ── Back / Download buttons ─────────────────────────────────────────── -->
+    <div class="d-print-none mb-3 d-flex gap-2">
+        <a href="/dashboard/crop-analysis/animal-dietary-balance/animal-report.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>"
+           class="btn btn-outline-secondary btn-sm">
+            &larr; Back to Report
+        </a>
+        <a href="/pdf-files/headlessChrome_pdf.php?type=animal-report&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>"
+           class="btn btn-success btn-sm">
+            <i class="fas fa-download me-1"></i>Download PDF
+        </a>
+        <button class="btn btn-outline-dark btn-sm" onclick="window.print()">
+            <i class="fas fa-print me-1"></i>Print
+        </button>
+    </div>
+
+    <hr>
+
+    <div class="section-header">General Comment</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['general_details'] ?? '') ?>
+    </div>
+
+    <div class="section-header">AI Dietary Interpretation</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['ai_interpretation'] ?? '') ?>
+    </div>
+
+    <div class="section-header">Recommended Supplementation Program</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['recommended_details'] ?? '') ?>
+    </div>
+
+    <div class="section-header">Ongoing Management Program</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['foliar_details'] ?? '') ?>
+    </div>
+
+    <div class="mt-3 pt-3 border-top">
+        <p class="text-muted fst-italic" style="font-size:0.7rem;">
+            Any recommendations provided by Crop Monitor are advice only. We are not paid consultants
+            and accept no responsibility for any of our suggestions.
+        </p>
+    </div>
+
+    <?php endif; ?>
+
+</div><!-- /container -->
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
+</body>
+</html>

+ 65 - 0
dashboard/crop-analysis/animal-dietary-balance/animal-report-save.php

@@ -0,0 +1,65 @@
+<?php
+/**
+ * animal-report-save.php
+ *
+ * AJAX endpoint: auto-saves animal dietary report consultant notes to the reports table.
+ * Called by the auto-save JS in animal-report.php via POST.
+ *
+ * GET params: rid (animal_records.id), rand (animal_records.rand)
+ * POST params: general_details, ai_interpretation, recommended_details, foliar_details
+ */
+
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+require_once __DIR__ . '/../../../config/database.php';
+require_once __DIR__ . '/../../../lib/auth.php';
+
+if (!isLoggedIn()) {
+    http_response_code(403);
+    echo json_encode(['success' => false, 'message' => 'Unauthorised']);
+    exit;
+}
+
+header('Content-Type: application/json');
+
+$pdo    = getDBConnection();
+$userId = getCurrentUserId();
+
+$recordId = (int)  ($_GET['rid']  ?? 0);
+$randId   = trim(  $_GET['rand']  ?? '');
+
+if ($recordId <= 0) {
+    http_response_code(400);
+    echo json_encode(['success' => false, 'message' => 'Missing record ID']);
+    exit;
+}
+
+$check = $pdo->prepare(
+    'SELECT id FROM animal_records WHERE id = ? AND rand = ? AND modx_user_id = ? LIMIT 1'
+);
+$check->execute([$recordId, $randId, $userId]);
+if (!$check->fetch()) {
+    http_response_code(403);
+    echo json_encode(['success' => false, 'message' => 'Record not found or access denied']);
+    exit;
+}
+
+$data = [
+    'general_details'     => trim($_POST['general_details']     ?? ''),
+    'ai_interpretation'   => trim($_POST['ai_interpretation']   ?? ''),
+    'recommended_details' => trim($_POST['recommended_details'] ?? ''),
+    'foliar_details'      => trim($_POST['foliar_details']      ?? ''),
+];
+
+$comment = json_encode($data, JSON_UNESCAPED_UNICODE);
+
+$stmt = $pdo->prepare('
+    INSERT INTO reports (modx_user_id, record_id, rand, comment, dateTime)
+    VALUES (?, ?, ?, ?, CURDATE())
+    ON DUPLICATE KEY UPDATE comment = VALUES(comment), dateTime = CURDATE()
+');
+$stmt->execute([$userId, $recordId, $randId, $comment]);
+
+echo json_encode(['success' => true, 'saved' => date('H:i:s')]);

+ 347 - 0
dashboard/crop-analysis/animal-dietary-balance/animal-report.php

@@ -0,0 +1,347 @@
+<?php
+/**
+ * dashboard/crop-analysis/animal-dietary-balance/animal-report.php
+ *
+ * Animal dietary balance report — editable sections with auto-save and Ollama AI interpretation.
+ */
+
+require_once __DIR__ . '/../../../config/database.php';
+require_once __DIR__ . '/../../../lib/auth.php';
+require_once __DIR__ . '/../../../lib/csrf.php';
+require_once __DIR__ . '/../../../vendor/autoload.php';
+
+requireLogin();
+
+$recordId = (int)  ($_GET['rid']  ?? 0);
+$randId   = trim(  $_GET['rand']  ?? '');
+$clientId = (int)  ($_GET['cid']  ?? 0);
+
+if (!$recordId || $randId === '') {
+    http_response_code(400);
+    die('Invalid request parameters');
+}
+
+try {
+    $pdo    = getDBConnection();
+    $userId = getCurrentUserId();
+
+    $stmt = $pdo->prepare('SELECT * FROM animal_records WHERE id = ? AND rand = ?');
+    $stmt->execute([$recordId, $randId]);
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    if (!$row) {
+        http_response_code(404);
+        die('Animal record not found');
+    }
+
+    // Load saved report comments
+    $savedComments = [
+        'general_details'     => '',
+        'ai_interpretation'   => '',
+        'recommended_details' => '',
+        'foliar_details'      => '',
+    ];
+    $stmtRpt = $pdo->prepare(
+        'SELECT comment FROM reports WHERE record_id = ? AND modx_user_id = ? ORDER BY id DESC LIMIT 1'
+    );
+    $stmtRpt->execute([$recordId, $userId]);
+    $savedRow = $stmtRpt->fetchColumn();
+    if ($savedRow) {
+        $decoded = json_decode($savedRow, true);
+        if (is_array($decoded)) {
+            $savedComments = array_merge($savedComments, $decoded);
+        }
+    }
+
+} catch (PDOException $e) {
+    error_log('DB error in animal-report.php: ' . $e->getMessage());
+    die('Database error occurred');
+}
+
+$h         = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
+$today     = date('jS F Y');
+$pageTitle = 'Animal Dietary Report' . (!empty($row['client_name']) ? ' — ' . $row['client_name'] : '');
+$siteName  = 'Crop Monitor';
+
+function formatReportText(string $text): string
+{
+    if (trim($text) === '') {
+        return '<p class="text-muted fst-italic">No content saved.</p>';
+    }
+    $parsedown = new Parsedown();
+    $parsedown->setSafeMode(true);
+    return $parsedown->text($text);
+}
+
+include __DIR__ . '/../../../layouts/header.php';
+?>
+<link rel="stylesheet" href="/client-assets/home/css/graphPrint.css" media="print">
+<style>
+    @media print {
+        .report-textarea { display: none; }
+        .report-print-preview { display: block !important; }
+    }
+    .report-print-preview { display: none; }
+    .report-textarea { overflow: hidden; resize: none; overflow-y: auto; }
+</style>
+
+<div class="container-fluid px-4" id="content">
+
+    <!-- ── Page heading ─────────────────────────────────────────── -->
+    <div class="d-flex align-items-center justify-content-between mt-4 mb-3">
+        <h1 class="h3 mb-0">Animal Dietary Balance Report</h1>
+        <div class="d-flex gap-2 d-print-none">
+            <a href="/dashboard/crop-analysis/animal-dietary-balance/animal-dietary-balance.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
+               class="btn btn-outline-secondary btn-sm">
+                &larr; Analysis
+            </a>
+            <a href="/dashboard/crop-analysis/animal-dietary-balance/animal-report-pdf.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
+               class="btn btn-outline-success btn-sm" target="_blank">
+                <i class="fas fa-file-pdf me-1"></i>PDF — Report
+            </a>
+            <a href="/pdf-files/headlessChrome_pdf.php?type=animal&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>
+            <button type="button" class="btn btn-primary btn-sm" id="btn-generate-all">
+                <i class="fas fa-robot me-1"></i>Interpret All with AI
+            </button>
+        </div>
+    </div>
+
+    <!-- ── Client info card ──────────────────────────────────────── -->
+    <div class="card mb-4">
+        <div class="card-body py-2">
+            <div class="row row-cols-2 row-cols-md-3 g-1 small">
+                <div><strong>Client:</strong> <?= $h($row['client_name']) ?></div>
+                <div><strong>Sample ID:</strong> <?= $h($row['sample_id']) ?></div>
+                <div><strong>Date Sampled:</strong> <?= $h($row['date_sampled']) ?></div>
+                <div><strong>Analysis Type:</strong> <?= $h($row['analysis_type'] ?? '') ?></div>
+                <div><strong>Lab No:</strong> <?= $h($row['lab_no']) ?></div>
+                <div><strong>Site ID:</strong> <?= $h($row['site_id']) ?></div>
+                <div><strong>Report Date:</strong> <?= $h($today) ?></div>
+            </div>
+        </div>
+    </div>
+
+    <div id="save-status" class="text-muted small mb-2" style="min-height:1.2rem;"></div>
+
+    <form class="report-form" method="post">
+        <input type="hidden" name="csrf_token"
+               value="<?= htmlspecialchars(generateCsrfToken(), ENT_QUOTES, 'UTF-8') ?>">
+        <input type="hidden" name="rid"  value="<?= $recordId ?>">
+        <input type="hidden" name="rand" value="<?= $h($randId) ?>">
+
+        <!-- ── 1. General Comment ─────────────────────────────────── -->
+        <div class="card mb-4">
+            <div class="card-header d-flex justify-content-between align-items-center fw-bold">
+                <span>General Comment</span>
+                <div class="d-flex d-print-none">
+                    <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                            data-section="general" data-target="#general_details">
+                        <i class="fas fa-robot me-1"></i>Generate with AI
+                    </button>
+                </div>
+            </div>
+            <div class="card-body">
+                <textarea id="general_details" name="general_details"
+                          class="form-control report-textarea" rows="6"
+                          placeholder="Enter a general comment on this dietary balance analysis..."
+                ><?= $h($savedComments['general_details']) ?></textarea>
+                <div class="report-print-preview"><?= formatReportText($savedComments['general_details']) ?></div>
+            </div>
+        </div>
+
+        <!-- ── 2. AI Interpretation ───────────────────────────────── -->
+        <div class="card mb-4">
+            <div class="card-header d-flex justify-content-between align-items-center fw-bold">
+                <span>AI Dietary Interpretation</span>
+                <div class="d-flex d-print-none">
+                    <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                            data-section="ai_interpretation" data-target="#ai_interpretation">
+                        <i class="fas fa-robot me-1"></i>Interpret with AI
+                    </button>
+                </div>
+            </div>
+            <div class="card-body">
+                <p class="text-muted small mb-2">
+                    AI-generated agronomic interpretation. Review and edit before including in the final report.
+                </p>
+                <textarea id="ai_interpretation" name="ai_interpretation"
+                          class="form-control report-textarea" rows="10"
+                          placeholder="Click 'Interpret with AI' to generate an interpretation, or type manually..."
+                ><?= $h($savedComments['ai_interpretation']) ?></textarea>
+                <div class="report-print-preview"><?= formatReportText($savedComments['ai_interpretation']) ?></div>
+            </div>
+        </div>
+
+        <!-- ── 3. Recommended Supplementation Program ───────────────────── -->
+        <div class="card mb-4">
+            <div class="card-header d-flex justify-content-between align-items-center fw-bold">
+                <span>Recommended Supplementation Program</span>
+                <div class="d-flex d-print-none">
+                    <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                            data-section="recommended" data-target="#recommended_details">
+                        <i class="fas fa-robot me-1"></i>Generate with AI
+                    </button>
+                </div>
+            </div>
+            <div class="card-body">
+                <textarea id="recommended_details" name="recommended_details"
+                          class="form-control report-textarea" rows="6"
+                          placeholder="Enter recommended supplementation program details..."
+                ><?= $h($savedComments['recommended_details']) ?></textarea>
+                <div class="report-print-preview"><?= formatReportText($savedComments['recommended_details']) ?></div>
+            </div>
+        </div>
+
+        <!-- ── 4. Ongoing Management Program ──────────────────────────────────────── -->
+        <div class="card mb-4">
+            <div class="card-header d-flex justify-content-between align-items-center fw-bold">
+                <span>Ongoing Management Program</span>
+                <div class="d-flex d-print-none">
+                    <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                            data-section="foliar" data-target="#foliar_details">
+                        <i class="fas fa-robot me-1"></i>Generate with AI
+                    </button>
+                </div>
+            </div>
+            <div class="card-body">
+                <textarea id="foliar_details" name="foliar_details"
+                          class="form-control report-textarea" rows="6"
+                          placeholder="Enter ongoing management and monitoring program details..."
+                ><?= $h($savedComments['foliar_details']) ?></textarea>
+                <div class="report-print-preview"><?= formatReportText($savedComments['foliar_details']) ?></div>
+            </div>
+        </div>
+
+        <!-- ── Disclaimer ─────────────────────────────────────────── -->
+        <div class="card mb-4 border-0 bg-light">
+            <div class="card-body">
+                <p class="text-muted mb-0 fst-italic" style="font-size:0.75rem;">
+                    Any recommendations provided by Crop Monitor are advice only. We are not paid consultants
+                    and accept no responsibility for any of our suggestions.
+                </p>
+            </div>
+        </div>
+
+    </form>
+
+</div><!-- /.container-fluid -->
+
+<script>
+(function () {
+    'use strict';
+
+    var saveTimer  = null;
+    var statusEl   = document.getElementById('save-status');
+    var SAVE_URL   = '/dashboard/crop-analysis/animal-dietary-balance/animal-report-save.php'
+                   + '?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>';
+    var AI_URL     = '/controllers/ollamaGenerate.php';
+    var CSRF_TOKEN = <?= json_encode(generateCsrfToken()) ?>;
+
+    function setStatus(msg, cls) {
+        statusEl.textContent = msg;
+        statusEl.className   = 'small mb-2 text-' + (cls || 'secondary');
+    }
+
+    function autoResize(el) {
+        el.style.height = 'auto';
+        el.style.height = el.scrollHeight + 'px';
+    }
+
+    document.querySelectorAll('.report-textarea').forEach(function (el) {
+        setTimeout(function () { autoResize(el); }, 0);
+        el.addEventListener('input', function () {
+            autoResize(el);
+            clearTimeout(saveTimer);
+            saveTimer = setTimeout(saveReport, 1200);
+        });
+    });
+
+    function saveReport() {
+        var form = document.querySelector('.report-form');
+        var data = new URLSearchParams(new FormData(form));
+        setStatus('Saving…', 'secondary');
+        fetch(SAVE_URL, { method: 'POST', body: data })
+            .then(function (r) { return r.json(); })
+            .then(function (d) {
+                if (d.success) {
+                    setStatus('Saved — ' + new Date().toLocaleTimeString(), 'success');
+                } else {
+                    setStatus('Save failed: ' + (d.message || 'unknown error'), 'danger');
+                }
+            })
+            .catch(function () { setStatus('Network error — not saved', 'danger'); });
+    }
+
+    document.querySelector('.report-form').addEventListener('submit', function (e) {
+        e.preventDefault();
+        saveReport();
+    });
+
+    function generateSection(btn, section, targetSelector) {
+        var textarea = document.querySelector(targetSelector);
+        if (!textarea) return;
+        var origHTML = btn.innerHTML;
+        btn.disabled = true;
+        btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Generating…';
+        setStatus('Requesting AI interpretation…', 'secondary');
+        fetch(AI_URL, {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+            body: new URLSearchParams({
+                csrf_token:  CSRF_TOKEN,
+                rid:         <?= $recordId ?>,
+                rand:        <?= json_encode($randId) ?>,
+                section:     section,
+                record_type: 'animal',
+            }),
+        })
+        .then(function (r) { return r.json(); })
+        .then(function (d) {
+            if (d.success && d.text) {
+                textarea.value = d.text;
+                textarea.dispatchEvent(new Event('input'));
+                setStatus('AI text generated — review before publishing', 'success');
+            } else {
+                setStatus('AI error: ' + (d.error || 'no response returned'), 'danger');
+            }
+        })
+        .catch(function () {
+            setStatus('Could not reach AI service. Is Ollama running on port 11434?', 'danger');
+        })
+        .finally(function () {
+            btn.disabled = false;
+            btn.innerHTML = origHTML;
+        });
+    }
+
+    document.querySelectorAll('.ai-generate-btn').forEach(function (btn) {
+        btn.addEventListener('click', function () {
+            generateSection(btn, btn.dataset.section, btn.dataset.target);
+        });
+    });
+
+    document.getElementById('btn-generate-all').addEventListener('click', function () {
+        var sections = [
+            { section: 'general',           target: '#general_details' },
+            { section: 'ai_interpretation', target: '#ai_interpretation' },
+            { section: 'recommended',       target: '#recommended_details' },
+            { section: 'foliar',            target: '#foliar_details' },
+        ];
+        sections.forEach(function (s, i) {
+            setTimeout(function () {
+                var sectionBtn = document.querySelector('.ai-generate-btn[data-section="' + s.section + '"]');
+                generateSection(
+                    sectionBtn || document.getElementById('btn-generate-all'),
+                    s.section, s.target
+                );
+            }, i * 4000);
+        });
+    });
+
+})();
+</script>
+
+<?php include __DIR__ . '/../../../layouts/footer.php'; ?>

+ 1 - 1
dashboard/crop-analysis/plant-test-data/plant-analysis.php

@@ -16,7 +16,7 @@ if (session_status() === PHP_SESSION_NONE) {
 $recordId  = (int)  ($_GET['rid']  ?? 0);
 $randId    = trim(  $_GET['rand']  ?? '');
 $clientId  = (int)  ($_GET['cid']  ?? 0);
-$printMode = isset($_GET['print']);
+$printMode = isset($_GET['print']) || isset($_GET['ptoken']);
 
 if ($printMode) {
     authenticatePrintPage($recordId, $randId);

+ 116 - 0
dashboard/crop-analysis/plant-test-data/plant-print-combined.php

@@ -0,0 +1,116 @@
+<?php
+/**
+ * plant-print-combined.php
+ *
+ * Combined print/PDF page: Plant Analysis + Plant Report in one document.
+ * Accessed by headlessChrome_pdf.php (type=plant) — never directly by users.
+ * Auth is via the shared ptoken written by the generator.
+ */
+
+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();
+}
+
+$recordId = (int)  ($_GET['rid']   ?? 0);
+$randId   = trim(  $_GET['rand']   ?? '');
+$clientId = (int)  ($_GET['cid']   ?? 0);
+$ptoken   = trim(  $_GET['ptoken'] ?? '');
+
+authenticatePrintPage($recordId, $randId);
+
+$coverpageUrl = '/dashboard/crop-analysis/coverpage.php?'
+    . http_build_query([
+        'type'   => 'plant',
+        'rid'    => $recordId,
+        'rand'   => $randId,
+        'cid'    => $clientId,
+        'ptoken' => $ptoken,
+        'print'  => '1',
+    ]);
+
+$analysisUrl = '/dashboard/crop-analysis/plant-test-data/plant-analysis.php?'
+    . http_build_query([
+        'rid'    => $recordId,
+        'rand'   => $randId,
+        'cid'    => $clientId,
+        'ptoken' => $ptoken,
+        'print'  => '1',
+    ]);
+
+$reportUrl = '/dashboard/crop-analysis/plant-test-data/plant-report-pdf.php?'
+    . http_build_query([
+        'rid'    => $recordId,
+        'rand'   => $randId,
+        'ptoken' => $ptoken,
+    ]);
+
+function h(string $v): string {
+    return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
+}
+?>
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Plant Analysis &amp; Report</title>
+    <style>
+        * { margin: 0; padding: 0; box-sizing: border-box; }
+        body { background: #fff; }
+        iframe {
+            width: 100%;
+            border: none;
+            display: block;
+            min-height: 200px;
+        }
+        .page-section { page-break-after: always; }
+        .page-section:last-child { page-break-after: avoid; }
+        @media print {
+            @page { size: A4 portrait; margin: 0; }
+            html, body { margin: 0; padding: 0; background: #fff; }
+            body { min-width: 1030px !important; font-size: 0.75em; }
+            .container { min-width: 1030px !important; }
+            .report-textarea { border: none; }
+        }
+    </style>
+</head>
+<body>
+
+<div class="page-section page">
+    <iframe id="frame-coverpage" src="<?= h($coverpageUrl) ?>" scrolling="no"></iframe>
+</div>
+
+<div class="page-section page">
+    <iframe id="frame-analysis" src="<?= h($analysisUrl) ?>" scrolling="no"></iframe>
+</div>
+
+<div class="page-section page">
+    <iframe id="frame-report" src="<?= h($reportUrl) ?>" scrolling="no"></iframe>
+</div>
+
+<script>
+(function () {
+    function resizeFrame(iframe) {
+        try {
+            var h = iframe.contentDocument.documentElement.scrollHeight;
+            if (h > 0) iframe.style.height = h + 'px';
+        } catch (e) {
+            iframe.style.height = '2970px';
+        }
+    }
+
+    var cover = document.getElementById('frame-coverpage');
+    if (cover) cover.style.height = '1457px';
+
+    ['frame-analysis', 'frame-report'].forEach(function (id) {
+        var el = document.getElementById(id);
+        el.addEventListener('load', function () { resizeFrame(el); });
+    });
+})();
+</script>
+
+</body>
+</html>

+ 165 - 155
dashboard/crop-analysis/water-test-data/water-analysis-pdf.php

@@ -1,138 +1,167 @@
 <?php
 /**
- * water-analysis-pdf.php
+ * dashboard/crop-analysis/water-test-data/water-analysis-pdf.php
  *
- * Displays water quality analysis results for a single water_records row.
- * Standalone PDF-printable page. Accessed via rid + rand URL params.
+ * Water quality analysis results display page.
+ * Supports normal browser access (session login) and headless Chrome (ptoken).
  */
 
 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']) || isset($_GET['ptoken']);
 
-$pdo    = getDBConnection();
-$userId = getCurrentUserId();
+if (!$recordId || $randId === '') {
+    http_response_code(400);
+    die('Invalid request parameters');
+}
 
-$recordId = (int)   ($_GET['rid']  ?? 0);
-$randId   = (float) ($_GET['rand'] ?? 0);
+$chromeAccess = authenticatePrintPage($recordId, $randId);
+
+$pdo    = getDBConnection();
+$userId = $chromeAccess ? null : getCurrentUserId();
 
 $row   = null;
 $specs = [];
 
-if ($recordId > 0) {
-    $stmt = $pdo->prepare('SELECT * FROM water_records WHERE id = ? AND rand = ? LIMIT 1');
-    $stmt->execute([$recordId, $randId]);
-    $row = $stmt->fetch();
-}
+$stmt = $pdo->prepare('SELECT * FROM water_records WHERE id = ? AND rand = ? LIMIT 1');
+$stmt->execute([$recordId, $randId]);
+$row = $stmt->fetch(PDO::FETCH_ASSOC);
 
 if ($row) {
     $type = $row['analysis_type'] ?? '';
-    $stmt = $pdo->prepare('SELECT * FROM water_specifications WHERE type = ? LIMIT 1');
-    $stmt->execute([$type]);
-    $specs = $stmt->fetch() ?: [];
+    if ($type !== '') {
+        $stmtSpec = $pdo->prepare('SELECT * FROM water_specifications WHERE type = ? LIMIT 1');
+        $stmtSpec->execute([$type]);
+        $specs = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
+    }
 }
 
-$h = fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
+$h = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
 
 function waterBar(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) {
         return '<td class="text-center"><div class="progress"><div class="progress-bar bg-danger" style="width:100%"></div></div></td>'
              . '<td></td><td></td>';
-    } elseif ($found > $max) {
+    }
+    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>';
-    } else {
-        return '<td></td>'
-             . '<td class="text-center"><div class="progress"><div class="progress-bar bg-success" style="width:' . round($pct) . '%"></div></div></td>'
-             . '<td></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 = 'Water Analysis' . (!empty($row['client_name']) ? ' — ' . $row['client_name'] : '');
+$siteName  = 'Crop Monitor';
+
+if (!$printMode) {
+    include __DIR__ . '/../../../layouts/header.php';
 }
 ?>
-<!doctype html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <title>Water 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; }
-        .lightblue   { background: #cce5ff !important; color: #004085 !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>
+
+<link rel="stylesheet" href="/client-assets/home/css/graphPrint.css" media="print">
+<style>
+    @media print {
+        @page { size: A4 portrait; }
+        body { min-width: 992px !important; }
+        .container { min-width: 992px !important; }
+    }
+    .progress { border-radius: 0 !important; }
+    .lightgreen  { background: #d4edda !important; color: #155724 !important; }
+    .lightred    { background: #f8d7da !important; color: #721c24 !important; }
+    .lightblue   { background: #cce5ff !important; color: #004085 !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; }
+    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; }
+</style>
+
 <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">
         </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>
-    </div>
-
-    <!-- Client info -->
-    <table class="table table-sm table-bordered mb-3" style="font-size:0.85rem;">
+    <table class="title w-100 mb-3 small">
         <tbody>
             <tr>
-                <th class="w-25">Client</th>
-                <td><?= $h($row['client_name'] ?? '') ?></td>
-                <th class="w-25">Lab No</th>
-                <td><?= $h($row['lab_no'] ?? '') ?></td>
+                <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['sample_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>
-                <th>Sample ID</th>
-                <td><?= $h($row['sample_id'] ?? '') ?></td>
-                <th>Site ID</th>
-                <td><?= $h($row['site_id'] ?? '') ?></td>
+                <td class="text-end fw-bold text-nowrap">SITE ID:</td>
+                <td><?= $h($row['site_id']) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">LAB NUMBER:</td>
+                <td><?= $h($row['lab_no']) ?></td>
             </tr>
             <tr>
-                <th>Analysis Type</th>
-                <td><?= $h($row['analysis_type'] ?? '') ?></td>
-                <th>Date Sampled</th>
-                <td><?= $h($row['date_sampled'] ?? '') ?></td>
+                <td class="text-end fw-bold text-nowrap">ANALYSIS TYPE:</td>
+                <td><?= $h($row['analysis_type']) ?></td>
+                <td></td>
+                <td></td>
+                <td></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/water-test-data/water-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=water-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=water&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">Water Quality Analysis Results</div>
     </div>
-    <hr class="p-1 m-1">
 
     <table class="chart">
         <tbody>
@@ -141,104 +170,100 @@ function waterBar(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">Parameter</th>
-                <th class="text-center border-bottom">Desired</th>
-                <th class="text-center border-bottom">Found</th>
+                <th class="text-center border-left">Parameter</th>
+                <th class="text-center">Desired</th>
+                <th class="text-center">Found</th>
                 <th class="text-center stripe-1">Deficit</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"></td><td></td><td></td>
+                <td></td><td></td><td class="border-right"></td>
+            </tr>
 
-            <tr><td class="border-left" colspan="6"></td></tr>
+            <!-- General Parameters -->
             <tr class="chart-header-sub">
-                <th colspan="3" class="border-left text-center lightblue">GENERAL PARAMETERS</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 lightblue">GENERAL PARAMETERS</th>
             </tr>
-
             <?php
             $generalParams = [
-                ['ph',       'pH',                          ''],
-                ['cond_dsm', 'Conductivity',                'dS/m'],
-                ['hco3-',    'Bicarbonate (HCO₃)',          'ppm'],
+                ['ph',       'pH',                 '',     3],
+                ['cond_dsm', 'Conductivity',        'dS/m', 3],
+                ['hco3-',    'Bicarbonate (HCO₃)',  'ppm',  2],
             ];
-            foreach ($generalParams as [$col, $label, $unit]):
-                $found = (float)($row[$col] ?? 0);
-                $min   = (float)($specs[$col . '_min'] ?? 0);
-                $max   = (float)($specs[$col . '_max'] ?? 0);
-                $desired = ($min > 0 || $max > 0) ? number_format($min, 2) . '–' . number_format($max, 2) : '—';
+            foreach ($generalParams as [$col, $label, $unit, $dp]):
+                $found   = (float)($row[$col] ?? 0);
+                $min     = (float)($specs[$col . '_min'] ?? 0);
+                $max     = (float)($specs[$col . '_max'] ?? 0);
+                $desired = ($min > 0 || $max > 0) ? number_format($min, $dp) . '–' . number_format($max, $dp) : '—';
                 $display = $unit ? $label . ' (' . $unit . ')' : $label;
             ?>
             <tr>
                 <td class="border-left"><?= $h($display) ?></td>
-                <td><?= $h($desired) ?></td>
-                <td><?= $found > 0 ? number_format($found, 3) : '—' ?></td>
+                <td class="text-center"><?= $h($desired) ?></td>
+                <td class="text-center"><?= $found > 0 ? number_format($found, $dp) : '—' ?></td>
                 <?= waterBar($found, $min, $max) ?>
             </tr>
             <?php endforeach; ?>
 
-            <tr><td class="border-left" colspan="6"></td></tr>
+            <tr><td colspan="6" class="border-left"></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 (ppm)</th>
             </tr>
-
             <?php
             $majorElements = [
-                ['nh4', 'Ammonium Nitrogen (NH₄)', 'ppm'],
-                ['no3', 'Nitrate Nitrogen (NO₃)',  'ppm'],
-                ['p',   'Phosphorus',               'ppm'],
-                ['k',   'Potassium',                'ppm'],
-                ['s',   'Sulphur',                  'ppm'],
-                ['ca',  'Calcium',                  'ppm'],
-                ['mg',  'Magnesium',                'ppm'],
-                ['na',  'Sodium',                   'ppm'],
+                ['nh4', 'Ammonium Nitrogen (NH₄)', 'ppm', 2],
+                ['no3', 'Nitrate Nitrogen (NO₃)',  'ppm', 2],
+                ['p',   'Phosphorus',               'ppm', 2],
+                ['k',   'Potassium',                'ppm', 1],
+                ['s',   'Sulphur',                  'ppm', 2],
+                ['ca',  'Calcium',                  'ppm', 1],
+                ['mg',  'Magnesium',                'ppm', 1],
+                ['na',  'Sodium',                   'ppm', 1],
             ];
-            foreach ($majorElements as [$col, $label, $unit]):
-                $found = (float)($row[$col] ?? 0);
-                $min   = (float)($specs[$col . '_min'] ?? 0);
-                $max   = (float)($specs[$col . '_max'] ?? 0);
-                $desired = ($min > 0 || $max > 0) ? number_format($min, 1) . '–' . number_format($max, 1) : '—';
+            foreach ($majorElements as [$col, $label, $unit, $dp]):
+                $found   = (float)($row[$col] ?? 0);
+                $min     = (float)($specs[$col . '_min'] ?? 0);
+                $max     = (float)($specs[$col . '_max'] ?? 0);
+                $desired = ($min > 0 || $max > 0) ? number_format($min, $dp) . '–' . number_format($max, $dp) : '—';
             ?>
             <tr>
                 <td class="border-left"><?= $h($label) ?> (<?= $h($unit) ?>)</td>
-                <td><?= $h($desired) ?></td>
-                <td><?= $found > 0 ? number_format($found, 2) : '—' ?></td>
+                <td class="text-center"><?= $h($desired) ?></td>
+                <td class="text-center"><?= $found > 0 ? number_format($found, $dp) : '—' ?></td>
                 <?= waterBar($found, $min, $max) ?>
             </tr>
             <?php endforeach; ?>
 
-            <tr><td class="border-left" colspan="6"></td></tr>
+            <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>
-
             <?php
             $traceElements = [
-                ['fe', 'Iron',      'ppm'],
-                ['mn', 'Manganese', 'ppm'],
-                ['zn', 'Zinc',      'ppm'],
-                ['cu', 'Copper',    'ppm'],
-                ['b',  'Boron',     'ppm'],
-                ['m',  'Molybdenum','ppm'],
-                ['co', 'Cobalt',    'ppm'],
+                ['fe', 'Iron',       'ppm', 3],
+                ['mn', 'Manganese',  'ppm', 3],
+                ['zn', 'Zinc',       'ppm', 3],
+                ['cu', 'Copper',     'ppm', 3],
+                ['b',  'Boron',      'ppm', 3],
+                ['m',  'Molybdenum', 'ppm', 3],
+                ['co', 'Cobalt',     'ppm', 3],
             ];
-            foreach ($traceElements as [$col, $label, $unit]):
-                $found = (float)($row[$col] ?? 0);
-                $min   = (float)($specs[$col . '_min'] ?? 0);
-                $max   = (float)($specs[$col . '_max'] ?? 0);
-                $desired = ($min > 0 || $max > 0) ? number_format($min, 2) . '–' . number_format($max, 2) : '—';
+            foreach ($traceElements as [$col, $label, $unit, $dp]):
+                $found   = (float)($row[$col] ?? 0);
+                $min     = (float)($specs[$col . '_min'] ?? 0);
+                $max     = (float)($specs[$col . '_max'] ?? 0);
+                $desired = ($min > 0 || $max > 0) ? number_format($min, $dp) . '–' . number_format($max, $dp) : '—';
             ?>
             <tr>
                 <td class="border-left"><?= $h($label) ?> (<?= $h($unit) ?>)</td>
-                <td><?= $h($desired) ?></td>
-                <td><?= $found > 0 ? number_format($found, 3) : '—' ?></td>
+                <td class="text-center"><?= $h($desired) ?></td>
+                <td class="text-center"><?= $found > 0 ? number_format($found, $dp) : '—' ?></td>
                 <?= waterBar($found, $min, $max) ?>
             </tr>
             <?php endforeach; ?>
@@ -257,24 +282,9 @@ function waterBar(float $found, float $min, float $max): string {
     <div class="mt-4 small text-muted">
         <p><i class="fa fa-tint" style="color:#007bff"></i> Water analysis results should be interpreted in the context of intended use (irrigation, drinking, stock water).</p>
         <p><i class="fa fa-tint" style="color:#007bff"></i> Talk to your qualified consultant to make a plan for correction or maintenance of the found parameter levels.</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;">Any recommendations provided by Crop Monitor 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: 'water-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 -->

+ 116 - 0
dashboard/crop-analysis/water-test-data/water-print-combined.php

@@ -0,0 +1,116 @@
+<?php
+/**
+ * water-print-combined.php
+ *
+ * Combined print/PDF page: Water Analysis + Water Report in one document.
+ * Accessed by headlessChrome_pdf.php (type=water) — never directly by users.
+ * Auth is via the shared ptoken written by the generator.
+ */
+
+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();
+}
+
+$recordId = (int)  ($_GET['rid']   ?? 0);
+$randId   = trim(  $_GET['rand']   ?? '');
+$clientId = (int)  ($_GET['cid']   ?? 0);
+$ptoken   = trim(  $_GET['ptoken'] ?? '');
+
+authenticatePrintPage($recordId, $randId);
+
+$coverpageUrl = '/dashboard/crop-analysis/coverpage.php?'
+    . http_build_query([
+        'type'   => 'water',
+        'rid'    => $recordId,
+        'rand'   => $randId,
+        'cid'    => $clientId,
+        'ptoken' => $ptoken,
+        'print'  => '1',
+    ]);
+
+$analysisUrl = '/dashboard/crop-analysis/water-test-data/water-analysis-pdf.php?'
+    . http_build_query([
+        'rid'    => $recordId,
+        'rand'   => $randId,
+        'cid'    => $clientId,
+        'ptoken' => $ptoken,
+        'print'  => '1',
+    ]);
+
+$reportUrl = '/dashboard/crop-analysis/water-test-data/water-report-pdf.php?'
+    . http_build_query([
+        'rid'    => $recordId,
+        'rand'   => $randId,
+        'ptoken' => $ptoken,
+    ]);
+
+function h(string $v): string {
+    return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
+}
+?>
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Water Analysis &amp; Report</title>
+    <style>
+        * { margin: 0; padding: 0; box-sizing: border-box; }
+        body { background: #fff; }
+        iframe {
+            width: 100%;
+            border: none;
+            display: block;
+            min-height: 200px;
+        }
+        .page-section { page-break-after: always; }
+        .page-section:last-child { page-break-after: avoid; }
+        @media print {
+            @page { size: A4 portrait; margin: 0; }
+            html, body { margin: 0; padding: 0; background: #fff; }
+            body { min-width: 1030px !important; font-size: 0.75em; }
+            .container { min-width: 1030px !important; }
+            .report-textarea { border: none; }
+        }
+    </style>
+</head>
+<body>
+
+<div class="page-section page">
+    <iframe id="frame-coverpage" src="<?= h($coverpageUrl) ?>" scrolling="no"></iframe>
+</div>
+
+<div class="page-section page">
+    <iframe id="frame-analysis" src="<?= h($analysisUrl) ?>" scrolling="no"></iframe>
+</div>
+
+<div class="page-section page">
+    <iframe id="frame-report" src="<?= h($reportUrl) ?>" scrolling="no"></iframe>
+</div>
+
+<script>
+(function () {
+    function resizeFrame(iframe) {
+        try {
+            var h = iframe.contentDocument.documentElement.scrollHeight;
+            if (h > 0) iframe.style.height = h + 'px';
+        } catch (e) {
+            iframe.style.height = '2970px';
+        }
+    }
+
+    var cover = document.getElementById('frame-coverpage');
+    if (cover) cover.style.height = '1457px';
+
+    ['frame-analysis', 'frame-report'].forEach(function (id) {
+        var el = document.getElementById(id);
+        el.addEventListener('load', function () { resizeFrame(el); });
+    });
+})();
+</script>
+
+</body>
+</html>

+ 209 - 0
dashboard/crop-analysis/water-test-data/water-report-pdf.php

@@ -0,0 +1,209 @@
+<?php
+/**
+ * water-report-pdf.php
+ *
+ * Print / PDF-export version of a completed water analysis report.
+ * Normal access: ?rid=<id>&rand=<token>
+ * Headless Chrome: ?rid=<id>&rand=<token>&ptoken=<one-time-token>
+ */
+
+require_once __DIR__ . '/../../../config/database.php';
+require_once __DIR__ . '/../../../lib/auth.php';
+require_once __DIR__ . '/../../../lib/print_auth.php';
+require_once __DIR__ . '/../../../vendor/autoload.php';
+
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+$recordId  = (int)  ($_GET['rid']  ?? 0);
+$randId    = trim(  $_GET['rand']  ?? '');
+$clientId  = (int)  ($_GET['cid']  ?? 0);
+$printMode = isset($_GET['ptoken']) || isset($_GET['print']);
+
+if (!$recordId || $randId === '') {
+    http_response_code(400);
+    die('Invalid request parameters');
+}
+
+$chromeAccess = authenticatePrintPage($recordId, $randId);
+
+try {
+    $pdo    = getDBConnection();
+    $userId = $chromeAccess ? null : getCurrentUserId();
+
+    $stmt = $pdo->prepare('SELECT * FROM water_records WHERE id = ? AND rand = ?');
+    $stmt->execute([$recordId, $randId]);
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    if (!$row) {
+        http_response_code(404);
+        die('Water record not found');
+    }
+
+    // Load saved report comments
+    $savedComments = [
+        'general_details'     => '',
+        'ai_interpretation'   => '',
+        'recommended_details' => '',
+        'foliar_details'      => '',
+    ];
+    $reportUserId = $userId ?? (int)($row['modx_user_id'] ?? 0);
+    $stmtRpt = $pdo->prepare(
+        'SELECT comment FROM reports WHERE record_id = ? AND modx_user_id = ? ORDER BY id DESC LIMIT 1'
+    );
+    $stmtRpt->execute([$recordId, $reportUserId]);
+    $savedRow = $stmtRpt->fetchColumn();
+    if ($savedRow) {
+        $decoded = json_decode($savedRow, true);
+        if (is_array($decoded)) {
+            $savedComments = array_merge($savedComments, $decoded);
+        }
+    }
+
+} catch (PDOException $e) {
+    error_log('DB error in water-report-pdf.php: ' . $e->getMessage());
+    die('Database error occurred');
+}
+
+$h     = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
+$today = date('jS F Y');
+
+function formatReportText(string $text): string
+{
+    if (trim($text) === '') {
+        return '<p class="text-muted fst-italic">No content saved.</p>';
+    }
+    $parsedown = new Parsedown();
+    $parsedown->setSafeMode(true);
+    return $parsedown->text($text);
+}
+?>
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Water Analysis Report | Crop Monitor</title>
+    <link rel="icon" href="/favicon.ico?v=2" type="image/x-icon">
+    <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" rel="stylesheet" crossorigin="anonymous">
+    <link href="/client-assets/css/dashboard.css" rel="stylesheet">
+    <style>
+        @media print {
+            .d-print-none { display: none !important; }
+            body { font-size: 11px; }
+        }
+        .report-section p   { margin-bottom: 0.6rem; line-height: 1.6; }
+        .report-section h1, .report-section h2,
+        .report-section h3, .report-section h4 { font-size: 1rem; font-weight: 600; margin: 1rem 0 0.4rem; }
+        .report-section ul, .report-section ol  { padding-left: 1.4rem; margin-bottom: 0.6rem; }
+        .report-section li  { margin-bottom: 0.25rem; line-height: 1.5; }
+        .section-header {
+            background: #212529; color: #fff;
+            padding: 6px 12px; font-weight: 600; margin-bottom: 0;
+        }
+        .section-body {
+            border: 1px solid #dee2e6; border-top: 0;
+            padding: 14px 16px; margin-bottom: 1.2rem;
+        }
+        .title-table td, .title-table th { padding: 2px 8px; }
+    </style>
+</head>
+<body>
+
+<div class="container page" id="pdf-content">
+
+    <?php if (!$row): ?>
+        <div class="alert alert-danger mt-4">Record not found or access denied.</div>
+    <?php else: ?>
+
+    <!-- ── Header ──────────────────────────────────────────────────────────── -->
+    <div class="row align-items-center mb-3 mt-3">
+        <div class="col-3">
+            <img class="img-fluid" src="/client-assets/images/crop-monitor.png"
+                 alt="Crop Monitor" style="max-height:55px;">
+        </div>
+        <div class="col-9 text-end">
+            <div class="fw-bold h5 mb-0">Water Analysis Report</div>
+            <div class="text-muted small"><?= $h($today) ?></div>
+        </div>
+    </div>
+
+    <table class="title-table w-100 mb-3 small">
+        <tbody>
+            <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">SAMPLE ID:</td>
+                <td><?= $h($row['sample_id']) ?></td>
+            </tr>
+            <tr>
+                <td class="text-end fw-bold text-nowrap">SITE ID:</td>
+                <td><?= $h($row['site_id']) ?></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">ANALYSIS TYPE:</td>
+                <td><?= $h($row['analysis_type']) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">LAB NUMBER:</td>
+                <td><?= $h($row['lab_no']) ?></td>
+            </tr>
+        </tbody>
+    </table>
+
+    <!-- ── Back / Download buttons ─────────────────────────────────────────── -->
+    <div class="d-print-none mb-3 d-flex gap-2">
+        <a href="/dashboard/crop-analysis/water-test-data/water-report.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>"
+           class="btn btn-outline-secondary btn-sm">
+            &larr; Back to Report
+        </a>
+        <a href="/pdf-files/headlessChrome_pdf.php?type=water-report&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>"
+           class="btn btn-success btn-sm">
+            <i class="fas fa-download me-1"></i>Download PDF
+        </a>
+        <button class="btn btn-outline-dark btn-sm" onclick="window.print()">
+            <i class="fas fa-print me-1"></i>Print
+        </button>
+    </div>
+
+    <hr>
+
+    <div class="section-header">General Comment</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['general_details'] ?? '') ?>
+    </div>
+
+    <div class="section-header">AI Water Interpretation</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['ai_interpretation'] ?? '') ?>
+    </div>
+
+    <div class="section-header">Recommended Remedial Program</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['recommended_details'] ?? '') ?>
+    </div>
+
+    <div class="section-header">Application Program</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['foliar_details'] ?? '') ?>
+    </div>
+
+    <div class="mt-3 pt-3 border-top">
+        <p class="text-muted fst-italic" style="font-size:0.7rem;">
+            Any recommendations provided by Crop Monitor are advice only. We are not paid consultants
+            and accept no responsibility for any of our suggestions.
+        </p>
+    </div>
+
+    <?php endif; ?>
+
+</div><!-- /container -->
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
+</body>
+</html>

+ 65 - 0
dashboard/crop-analysis/water-test-data/water-report-save.php

@@ -0,0 +1,65 @@
+<?php
+/**
+ * water-report-save.php
+ *
+ * AJAX endpoint: auto-saves water report consultant notes to the reports table.
+ * Called by the auto-save JS in water-report.php via POST.
+ *
+ * GET params: rid (water_records.id), rand (water_records.rand)
+ * POST params: general_details, ai_interpretation, recommended_details, foliar_details
+ */
+
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+require_once __DIR__ . '/../../../config/database.php';
+require_once __DIR__ . '/../../../lib/auth.php';
+
+if (!isLoggedIn()) {
+    http_response_code(403);
+    echo json_encode(['success' => false, 'message' => 'Unauthorised']);
+    exit;
+}
+
+header('Content-Type: application/json');
+
+$pdo    = getDBConnection();
+$userId = getCurrentUserId();
+
+$recordId = (int)  ($_GET['rid']  ?? 0);
+$randId   = trim(  $_GET['rand']  ?? '');
+
+if ($recordId <= 0) {
+    http_response_code(400);
+    echo json_encode(['success' => false, 'message' => 'Missing record ID']);
+    exit;
+}
+
+$check = $pdo->prepare(
+    'SELECT id FROM water_records WHERE id = ? AND rand = ? AND modx_user_id = ? LIMIT 1'
+);
+$check->execute([$recordId, $randId, $userId]);
+if (!$check->fetch()) {
+    http_response_code(403);
+    echo json_encode(['success' => false, 'message' => 'Record not found or access denied']);
+    exit;
+}
+
+$data = [
+    'general_details'     => trim($_POST['general_details']     ?? ''),
+    'ai_interpretation'   => trim($_POST['ai_interpretation']   ?? ''),
+    'recommended_details' => trim($_POST['recommended_details'] ?? ''),
+    'foliar_details'      => trim($_POST['foliar_details']      ?? ''),
+];
+
+$comment = json_encode($data, JSON_UNESCAPED_UNICODE);
+
+$stmt = $pdo->prepare('
+    INSERT INTO reports (modx_user_id, record_id, rand, comment, dateTime)
+    VALUES (?, ?, ?, ?, CURDATE())
+    ON DUPLICATE KEY UPDATE comment = VALUES(comment), dateTime = CURDATE()
+');
+$stmt->execute([$userId, $recordId, $randId, $comment]);
+
+echo json_encode(['success' => true, 'saved' => date('H:i:s')]);

+ 282 - 21
dashboard/crop-analysis/water-test-data/water-report.php

@@ -1,13 +1,14 @@
 <?php
 /**
- * dashboard/crop-analysis/plant-test-data/plant-report.php
+ * dashboard/crop-analysis/water-test-data/water-report.php
  *
- * Plant analysis report — editable sections with auto-save and Ollama AI interpretation.
+ * Water analysis report — editable sections with auto-save and Ollama AI interpretation.
  */
 
 require_once __DIR__ . '/../../../config/database.php';
 require_once __DIR__ . '/../../../lib/auth.php';
 require_once __DIR__ . '/../../../lib/csrf.php';
+require_once __DIR__ . '/../../../vendor/autoload.php';
 
 requireLogin();
 
@@ -24,21 +25,13 @@ try {
     $pdo    = getDBConnection();
     $userId = getCurrentUserId();
 
-    $stmt = $pdo->prepare('SELECT * FROM plant_records WHERE id = ? AND rand = ?');
+    $stmt = $pdo->prepare('SELECT * FROM water_records WHERE id = ? AND rand = ?');
     $stmt->execute([$recordId, $randId]);
     $row = $stmt->fetch(PDO::FETCH_ASSOC);
 
     if (!$row) {
         http_response_code(404);
-        die('Plant record not found');
-    }
-
-    // Load spec ranges
-    $specs = [];
-    if (!empty($row['crop_type'])) {
-        $stmtSpec = $pdo->prepare('SELECT * FROM plant_specifications WHERE plant_type = ? LIMIT 1');
-        $stmtSpec->execute([$row['crop_type']]);
-        $specs = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
+        die('Water record not found');
     }
 
     // Load saved report comments
@@ -61,7 +54,7 @@ try {
     }
 
 } catch (PDOException $e) {
-    error_log('DB error in plant-report.php: ' . $e->getMessage());
+    error_log('DB error in water-report.php: ' . $e->getMessage());
     die('Database error occurred');
 }
 
@@ -70,18 +63,286 @@ $today     = date('jS F Y');
 $pageTitle = 'Water Report' . (!empty($row['client_name']) ? ' — ' . $row['client_name'] : '');
 $siteName  = 'Crop Monitor';
 
+function formatReportText(string $text): string
+{
+    if (trim($text) === '') {
+        return '<p class="text-muted fst-italic">No content saved.</p>';
+    }
+    $parsedown = new Parsedown();
+    $parsedown->setSafeMode(true);
+    return $parsedown->text($text);
+}
+
 include __DIR__ . '/../../../layouts/header.php';
 ?>
 <link rel="stylesheet" href="/client-assets/home/css/graphPrint.css" media="print">
 <style>
     @media print {
-        .report-textarea {
-            border:none;
-        }
-    }
-    .report-textarea {
-        overflow: hidden;
-        resize: none;
-        overflow-y: auto;
+        .report-textarea { display: none; }
+        .report-print-preview { display: block !important; }
     }
+    .report-print-preview { display: none; }
+    .report-textarea { overflow: hidden; resize: none; overflow-y: auto; }
 </style>
+
+<div class="container-fluid px-4" id="content">
+
+    <!-- ── Page heading ─────────────────────────────────────────── -->
+    <div class="d-flex align-items-center justify-content-between mt-4 mb-3">
+        <h1 class="h3 mb-0">Water Analysis Report</h1>
+        <div class="d-flex gap-2 d-print-none">
+            <a href="/dashboard/crop-analysis/water-test-data/water-analysis-pdf.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
+               class="btn btn-outline-secondary btn-sm">
+                &larr; Analysis
+            </a>
+            <a href="/dashboard/crop-analysis/water-test-data/water-report-pdf.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
+               class="btn btn-outline-success btn-sm" target="_blank">
+                <i class="fas fa-file-pdf me-1"></i>PDF — Report
+            </a>
+            <a href="/pdf-files/headlessChrome_pdf.php?type=water&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>
+            <button type="button" class="btn btn-primary btn-sm" id="btn-generate-all">
+                <i class="fas fa-robot me-1"></i>Interpret All with AI
+            </button>
+        </div>
+    </div>
+
+    <!-- ── Client info card ──────────────────────────────────────── -->
+    <div class="card mb-4">
+        <div class="card-body py-2">
+            <div class="row row-cols-2 row-cols-md-3 g-1 small">
+                <div><strong>Client:</strong> <?= $h($row['client_name']) ?></div>
+                <div><strong>Sample ID:</strong> <?= $h($row['sample_id']) ?></div>
+                <div><strong>Date Sampled:</strong> <?= $h($row['date_sampled']) ?></div>
+                <div><strong>Address:</strong> <?= $h($row['site_address'] ?? '') ?>, <?= $h($row['state_postcode'] ?? '') ?></div>
+                <div><strong>Analysis Type:</strong> <?= $h($row['analysis_type']) ?></div>
+                <div><strong>Lab No:</strong> <?= $h($row['lab_no']) ?></div>
+                <div><strong>Site ID:</strong> <?= $h($row['site_id']) ?></div>
+                <div><strong>Report Date:</strong> <?= $h($today) ?></div>
+            </div>
+        </div>
+    </div>
+
+    <div id="save-status" class="text-muted small mb-2" style="min-height:1.2rem;"></div>
+
+    <form class="report-form" method="post">
+        <input type="hidden" name="csrf_token"
+               value="<?= htmlspecialchars(generateCsrfToken(), ENT_QUOTES, 'UTF-8') ?>">
+        <input type="hidden" name="rid"  value="<?= $recordId ?>">
+        <input type="hidden" name="rand" value="<?= $h($randId) ?>">
+
+        <!-- ── 1. General Comment ─────────────────────────────────── -->
+        <div class="card mb-4">
+            <div class="card-header d-flex justify-content-between align-items-center fw-bold">
+                <span>General Comment</span>
+                <div class="d-flex d-print-none">
+                    <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                            data-section="general" data-target="#general_details">
+                        <i class="fas fa-robot me-1"></i>Generate with AI
+                    </button>
+                </div>
+            </div>
+            <div class="card-body">
+                <textarea id="general_details" name="general_details"
+                          class="form-control report-textarea" rows="6"
+                          placeholder="Enter a general comment on this water analysis..."
+                ><?= $h($savedComments['general_details']) ?></textarea>
+                <div class="report-print-preview"><?= formatReportText($savedComments['general_details']) ?></div>
+            </div>
+        </div>
+
+        <!-- ── 2. AI Interpretation ───────────────────────────────── -->
+        <div class="card mb-4">
+            <div class="card-header d-flex justify-content-between align-items-center fw-bold">
+                <span>AI Water Interpretation</span>
+                <div class="d-flex d-print-none">
+                    <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                            data-section="ai_interpretation" data-target="#ai_interpretation">
+                        <i class="fas fa-robot me-1"></i>Interpret with AI
+                    </button>
+                </div>
+            </div>
+            <div class="card-body">
+                <p class="text-muted small mb-2">
+                    AI-generated agronomic interpretation. Review and edit before including in the final report.
+                </p>
+                <textarea id="ai_interpretation" name="ai_interpretation"
+                          class="form-control report-textarea" rows="10"
+                          placeholder="Click 'Interpret with AI' to generate an interpretation, or type manually..."
+                ><?= $h($savedComments['ai_interpretation']) ?></textarea>
+                <div class="report-print-preview"><?= formatReportText($savedComments['ai_interpretation']) ?></div>
+            </div>
+        </div>
+
+        <!-- ── 3. Recommended Remedial Program ───────────────────── -->
+        <div class="card mb-4">
+            <div class="card-header d-flex justify-content-between align-items-center fw-bold">
+                <span>Recommended Remedial Program</span>
+                <div class="d-flex d-print-none">
+                    <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                            data-section="recommended" data-target="#recommended_details">
+                        <i class="fas fa-robot me-1"></i>Generate with AI
+                    </button>
+                </div>
+            </div>
+            <div class="card-body">
+                <textarea id="recommended_details" name="recommended_details"
+                          class="form-control report-textarea" rows="6"
+                          placeholder="Enter recommended remedial program details..."
+                ><?= $h($savedComments['recommended_details']) ?></textarea>
+                <div class="report-print-preview"><?= formatReportText($savedComments['recommended_details']) ?></div>
+            </div>
+        </div>
+
+        <!-- ── 4. Foliar / Application Program ──────────────────────── -->
+        <div class="card mb-4">
+            <div class="card-header d-flex justify-content-between align-items-center fw-bold">
+                <span>Application Program</span>
+                <div class="d-flex d-print-none">
+                    <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                            data-section="foliar" data-target="#foliar_details">
+                        <i class="fas fa-robot me-1"></i>Generate with AI
+                    </button>
+                </div>
+            </div>
+            <div class="card-body">
+                <textarea id="foliar_details" name="foliar_details"
+                          class="form-control report-textarea" rows="6"
+                          placeholder="Enter application / treatment program details..."
+                ><?= $h($savedComments['foliar_details']) ?></textarea>
+                <div class="report-print-preview"><?= formatReportText($savedComments['foliar_details']) ?></div>
+            </div>
+        </div>
+
+        <!-- ── Disclaimer ─────────────────────────────────────────── -->
+        <div class="card mb-4 border-0 bg-light">
+            <div class="card-body">
+                <p class="text-muted mb-0 fst-italic" style="font-size:0.75rem;">
+                    Any recommendations provided by Crop Monitor are advice only. We are not paid consultants
+                    and accept no responsibility for any of our suggestions.
+                </p>
+            </div>
+        </div>
+
+    </form>
+
+</div><!-- /.container-fluid -->
+
+<script>
+(function () {
+    'use strict';
+
+    var saveTimer  = null;
+    var statusEl   = document.getElementById('save-status');
+    var SAVE_URL   = '/dashboard/crop-analysis/water-test-data/water-report-save.php'
+                   + '?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>';
+    var AI_URL     = '/controllers/ollamaGenerate.php';
+    var CSRF_TOKEN = <?= json_encode(generateCsrfToken()) ?>;
+
+    function setStatus(msg, cls) {
+        statusEl.textContent = msg;
+        statusEl.className   = 'small mb-2 text-' + (cls || 'secondary');
+    }
+
+    function autoResize(el) {
+        el.style.height = 'auto';
+        el.style.height = el.scrollHeight + 'px';
+    }
+
+    document.querySelectorAll('.report-textarea').forEach(function (el) {
+        setTimeout(function () { autoResize(el); }, 0);
+        el.addEventListener('input', function () {
+            autoResize(el);
+            clearTimeout(saveTimer);
+            saveTimer = setTimeout(saveReport, 1200);
+        });
+    });
+
+    function saveReport() {
+        var form = document.querySelector('.report-form');
+        var data = new URLSearchParams(new FormData(form));
+        setStatus('Saving…', 'secondary');
+        fetch(SAVE_URL, { method: 'POST', body: data })
+            .then(function (r) { return r.json(); })
+            .then(function (d) {
+                if (d.success) {
+                    setStatus('Saved — ' + new Date().toLocaleTimeString(), 'success');
+                } else {
+                    setStatus('Save failed: ' + (d.message || 'unknown error'), 'danger');
+                }
+            })
+            .catch(function () { setStatus('Network error — not saved', 'danger'); });
+    }
+
+    document.querySelector('.report-form').addEventListener('submit', function (e) {
+        e.preventDefault();
+        saveReport();
+    });
+
+    function generateSection(btn, section, targetSelector) {
+        var textarea = document.querySelector(targetSelector);
+        if (!textarea) return;
+        var origHTML = btn.innerHTML;
+        btn.disabled = true;
+        btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Generating…';
+        setStatus('Requesting AI interpretation…', 'secondary');
+        fetch(AI_URL, {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+            body: new URLSearchParams({
+                csrf_token:  CSRF_TOKEN,
+                rid:         <?= $recordId ?>,
+                rand:        <?= json_encode($randId) ?>,
+                section:     section,
+                record_type: 'water',
+            }),
+        })
+        .then(function (r) { return r.json(); })
+        .then(function (d) {
+            if (d.success && d.text) {
+                textarea.value = d.text;
+                textarea.dispatchEvent(new Event('input'));
+                setStatus('AI text generated — review before publishing', 'success');
+            } else {
+                setStatus('AI error: ' + (d.error || 'no response returned'), 'danger');
+            }
+        })
+        .catch(function () {
+            setStatus('Could not reach AI service. Is Ollama running on port 11434?', 'danger');
+        })
+        .finally(function () {
+            btn.disabled = false;
+            btn.innerHTML = origHTML;
+        });
+    }
+
+    document.querySelectorAll('.ai-generate-btn').forEach(function (btn) {
+        btn.addEventListener('click', function () {
+            generateSection(btn, btn.dataset.section, btn.dataset.target);
+        });
+    });
+
+    document.getElementById('btn-generate-all').addEventListener('click', function () {
+        var sections = [
+            { section: 'general',           target: '#general_details' },
+            { section: 'ai_interpretation', target: '#ai_interpretation' },
+            { section: 'recommended',       target: '#recommended_details' },
+            { section: 'foliar',            target: '#foliar_details' },
+        ];
+        sections.forEach(function (s, i) {
+            setTimeout(function () {
+                var sectionBtn = document.querySelector('.ai-generate-btn[data-section="' + s.section + '"]');
+                generateSection(
+                    sectionBtn || document.getElementById('btn-generate-all'),
+                    s.section, s.target
+                );
+            }, i * 4000);
+        });
+    });
+
+})();
+</script>
+
+<?php include __DIR__ . '/../../../layouts/footer.php'; ?>

+ 25 - 1
pdf-files/headlessChrome_pdf.php

@@ -91,6 +91,18 @@ $reportTypes = [
         'label'        => 'Plant Analysis & Report',
     ],
     // Water
+    'water-analysis' => [
+        'print_page'   => '/dashboard/crop-analysis/water-test-data/water-analysis-pdf.php?rid={rid}&rand={rand}&cid={cid}',
+        'verify_table' => 'water_records',
+        'filename'     => 'water-analysis',
+        'label'        => 'Water Analysis',
+    ],
+    'water-report' => [
+        'print_page'   => '/dashboard/crop-analysis/water-test-data/water-report-pdf.php?rid={rid}&rand={rand}',
+        'verify_table' => 'water_records',
+        'filename'     => 'water-report',
+        'label'        => 'Water Report',
+    ],
     'water' => [
         'print_page'   => '/dashboard/crop-analysis/water-test-data/water-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
         'verify_table' => 'water_records',
@@ -98,11 +110,23 @@ $reportTypes = [
         'label'        => 'Water Analysis & Report',
     ],
     // Animal
+    'animal-analysis' => [
+        'print_page'   => '/dashboard/crop-analysis/animal-dietary-balance/animal-dietary-balance.php?rid={rid}&rand={rand}&cid={cid}',
+        'verify_table' => 'animal_records',
+        'filename'     => 'animal-analysis',
+        'label'        => 'Animal Dietary Analysis',
+    ],
+    'animal-report' => [
+        'print_page'   => '/dashboard/crop-analysis/animal-dietary-balance/animal-report-pdf.php?rid={rid}&rand={rand}',
+        'verify_table' => 'animal_records',
+        'filename'     => 'animal-report',
+        'label'        => 'Animal Dietary Report',
+    ],
     'animal' => [
         'print_page'   => '/dashboard/crop-analysis/animal-dietary-balance/animal-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
         'verify_table' => 'animal_records',
         'filename'     => 'animal-dietary-report',
-        'label'        => 'Animal Dietary Report',
+        'label'        => 'Animal Dietary Analysis & Report',
     ],
 ];