Benjamin Harris 2 hónapja
szülő
commit
eba80e10f4

+ 68 - 12
controllers/ollamaGenerate.php

@@ -97,6 +97,28 @@ try {
             $stmtSpec->execute([$row['crop_type']]);
             $spec = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
         }
+
+        // Load up to 3 prior records for the same crop_type + sample_id
+        $historicalRows = [];
+        if (!empty($row['crop_type']) && !empty($row['sample_id'])) {
+            $stmtHist = $pdo->prepare(
+                'SELECT id, date_sampled, n, p, k, s, mg, ca, na, fe, mn, zn, cu, b, m, co, se, cl, c
+                   FROM plant_records
+                  WHERE client_records_id = ?
+                    AND crop_type = ?
+                    AND sample_id = ?
+                    AND id != ?
+                  ORDER BY date_sampled DESC
+                  LIMIT 3'
+            );
+            $stmtHist->execute([
+                $row['client_records_id'],
+                $row['crop_type'],
+                $row['sample_id'],
+                $recordId,
+            ]);
+            $historicalRows = $stmtHist->fetchAll(PDO::FETCH_ASSOC);
+        }
     } else {
         $stmt = $pdo->prepare('SELECT * FROM soil_records WHERE id = ? AND rand = ?');
         $stmt->execute([$recordId, $randId]);
@@ -365,7 +387,7 @@ $systemPrompts = [
         . "relationship between tissue nutrient levels and crop yield, quality, and disease resistance. "
         . "You are familiar with the CSIRO Plant Analysis Handbook, Hill Laboratories guidelines, "
         . "and PIRSA soil and plant analysis standards used in Australia. "
-        . "You understand how soil mineral imbalances, pH, and antagonisms (e.g. Ca/Mg, Zn/P, "
+        . "You understand how plant mineral imbalances, pH, and antagonisms (e.g. Ca/Mg, Zn/P, "
         . "K/Mg) translate into plant deficiency or excess symptoms. "
         . "Always base your interpretation on the measured tissue values and specified ranges. "
         . "Write in a professional but accessible tone suitable for a farmer-facing report.",
@@ -494,6 +516,29 @@ if ($recordType === 'plant') {
         . "In Excess: " . (empty($plantExcesses)     ? 'None detected' : implode(', ', $plantExcesses))     . "\n"
         . "=====================================\n";
 
+    // ── Historical comparison block ───────────────────────────────────────────
+    $historicalContext = '';
+    if (!empty($historicalRows)) {
+        $histElements = ['n','p','k','s','mg','ca','na','fe','mn','zn','cu','b','m','co','se','cl','c'];
+        $histNames    = ['N','P','K','S','Mg','Ca','Na','Fe','Mn','Zn','Cu','B','Mo','Co','Se','Cl','C'];
+
+        $historicalContext = "\n\n=====================================\n"
+                           . "HISTORICAL RECORDS — {$p['crop_type']} / Sample ID: {$p['sample_id']}\n"
+                           . "(most recent first, same site)\n"
+                           . "=====================================\n";
+
+        foreach ($historicalRows as $hr) {
+            $historicalContext .= "\nDate Sampled: " . ($hr['date_sampled'] ?? 'Unknown') . "\n";
+            $line = '';
+            foreach ($histElements as $i => $col) {
+                $val = ($hr[$col] ?? '') !== '' ? number_format((float)$hr[$col], 3) : 'N/A';
+                $line .= sprintf("  %-4s %s", $histNames[$i] . ':', $val);
+            }
+            $historicalContext .= trim($line) . "\n";
+        }
+        $historicalContext .= "=====================================\n";
+    }
+
     $ragChunks        = retrieveRelevantChunks($pdo, $plantData, $section, RAG_TOP_K);
     $knowledgeContext = '';
     if (!empty($ragChunks)) {
@@ -506,38 +551,49 @@ if ($recordType === 'plant') {
         }
     }
 
-    $ctx = $plantData . $knowledgeContext;
+    $ctx = $plantData . $historicalContext . $knowledgeContext;
+
+    $cropType = $p['crop_type'] ?? 'the crop';
 
     $prompts = [
         'general' =>
             "{$system}\n\n{$ctx}\n\n"
-            . "TASK: Write a concise general comment on these plant tissue analysis results (2–3 paragraphs). "
-            . "Cover: (1) the overall nutritional status of the crop, "
-            . "(2) the most significant deficiencies or excesses and their likely effect on crop yield and quality, "
+            . "TASK: Write a concise general comment on the tissue analysis results for THIS SPECIFIC CROP: {$cropType}. "
+            . "Do NOT mention or compare to any other crop type — your entire response must be about {$cropType} only. "
+            . "Cover: (1) the overall nutritional status of this {$cropType} sample, "
+            . "(2) the most significant deficiencies or excesses and their likely effect on {$cropType} yield and quality, "
             . "(3) any elements in good balance. Do not recommend specific product names.",
 
         'ai_interpretation' =>
             "{$system}\n\n{$ctx}\n\n"
-            . "TASK: Write a detailed technical interpretation with these headings:\n"
+            . "TASK: Write a detailed technical interpretation of the tissue analysis for THIS SPECIFIC CROP: {$cropType}. "
+            . "Do NOT mention or compare to any other crop type — your entire response must be about {$cropType} only. "
+            . "Use these headings:\n"
             . "1. MAJOR ELEMENTS (N, P, K, S, Mg, Ca, Na)\n"
             . "2. TRACE ELEMENTS (Fe, Mn, Zn, Cu, B)\n"
             . "3. OTHER ELEMENTS (Mo, Co, Se, Cl, C)\n"
             . "4. NUTRIENT INTERACTIONS & ANTAGONISMS\n"
-            . "5. OVERALL NUTRITIONAL ASSESSMENT\n"
-            . "For each element marked [DEFICIENT] or [EXCESS], explain the agronomic significance, "
-            . "likely causes, and interactions with other nutrients.",
+            . "5. OVERALL NUTRITIONAL ASSESSMENT FOR {$cropType}\n"
+            . (!empty($historicalRows)
+                ? "6. TREND ANALYSIS (compare current results to the HISTORICAL RECORDS provided above — "
+                  . "note which elements have improved, declined, or remained stable, and what this trend means for {$cropType} health)\n"
+                : '')
+            . "For each element marked [DEFICIENT] or [EXCESS], explain the agronomic significance "
+            . "specific to {$cropType}, likely causes, and interactions with other nutrients.",
 
         'recommended' =>
             "{$system}\n\n{$ctx}\n\n"
-            . "TASK: Design a recommended remedial soil and fertiliser program to correct the deficiencies shown. "
+            . "TASK: Design a recommended remedial soil and fertiliser program for {$cropType} to correct the deficiencies shown. "
+            . "Do NOT mention or compare to any other crop type — your entire response must be specific to {$cropType}. "
             . "Format as a numbered list or table: "
             . "Element | Current Status | Recommended Action | Product Type (generic) | Rate | Timing. "
             . "Prioritise elements marked [DEFICIENT]. "
-            . "Note any nutrient antagonisms that may be limiting uptake.",
+            . "Note any nutrient antagonisms that may be limiting uptake in {$cropType}.",
 
         'foliar' =>
             "{$system}\n\n{$ctx}\n\n"
-            . "TASK: Design a foliar spray program to rapidly correct deficiencies shown in the tissue test. "
+            . "TASK: Design a foliar spray program for {$cropType} to rapidly correct the deficiencies shown. "
+            . "Do NOT mention or compare to any other crop type — your entire response must be specific to {$cropType}. "
             . "Format as a numbered list or table: "
             . "Growth Stage | Product Type (generic) | Active Element | Rate (L or kg/ha) | Timing. "
             . "Prioritise elements marked [DEFICIENT]. "

+ 280 - 0
dashboard/crop-analysis/plant-test-data/plant-report-pdf.php

@@ -0,0 +1,280 @@
+<?php
+/**
+ * plant-report-pdf.php
+ *
+ * Print / PDF-export version of a completed plant analysis report.
+ * Displays the saved AI-generated sections from the reports table alongside
+ * the element requirement cards and five-year balancing plan.
+ *
+ * Normal access (logged-in user):
+ *   ?rid=<id>&rand=<token>
+ *
+ * Headless Chrome access (no session, called from headlessChrome_pdf.php):
+ *   ?rid=<id>&rand=<token>&ptoken=<one-time-file-token>
+ *   The ptoken is a short-lived file written by the generator. This lets Chrome
+ *   render the page without a PHP session cookie.
+ */
+
+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);
+
+if (!$recordId || $randId === '') {
+    http_response_code(400);
+    die('Invalid request parameters');
+}
+
+try {
+    $pdo    = getDBConnection();
+    $userId = getCurrentUserId();
+
+    $stmt = $pdo->prepare('SELECT * FROM plant_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) ?: [];
+    }
+
+    // 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 plant-report.php: ' . $e->getMessage());
+    die('Database error occurred');
+}
+
+$h         = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
+$today     = date('jS F Y');
+$pageTitle = 'Plant Report' . (!empty($row['client_name']) ? ' — ' . $row['client_name'] : '');
+$siteName  = 'Crop Monitor';
+
+// Parse and render markdown text from AI-generated report sections
+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>Soil Analysis Report | 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" rel="stylesheet" crossorigin="anonymous">
+    <link href="/client-assets/css/dashboard.css" rel="stylesheet">
+    <link rel="stylesheet" href="/client-assets/css/graphPrint.css" media="print">
+    <style>
+        @media print {
+            .d-print-none  { display: none !important; }
+            .page-break    { page-break-before: always; }
+            body           { font-size: 11px; }
+            .report-section p { 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; }
+        .report-section table {
+            width: 100%;
+            border-collapse: collapse;
+            margin-bottom: 0.8rem;
+            font-size: 0.85rem;
+        }
+        .report-section table th,
+        .report-section table td {
+            border: 1px solid #dee2e6;
+            padding: 4px 8px;
+            text-align: left;
+        }
+        .report-section table thead th {
+            background: #f8f9fa;
+            font-weight: 600;
+        }
+        .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; }
+        .element-required-module .col { padding: 0 6px; }
+    </style>
+</head>
+<body>
+
+<div class="container" 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">Plant 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['site_id']) ?></td>
+            </tr>
+            <tr>
+                <td class="text-end fw-bold text-nowrap">ADDRESS:</td>
+                <td><?= $h($row['site_address']) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">DATE SAMPLED:</td>
+                <td><?= $h($row['date_sampled']) ?></td>
+            </tr>
+            <tr>
+                <td></td>
+                <td><?= $h($row['state_postcode']) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">LAB NUMBER:</td>
+                <td><?= $h($row['lab_no']) ?></td>
+            </tr>
+            <tr>
+                <td></td>
+                <td><?= $h($row['email']) ?></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">CROP:</td>
+                <td><?= $h($row['sample_id']) ?></td>
+            </tr>
+            <tr>
+                <td></td>
+                <td></td>
+                <td></td>
+                <td class="text-end fw-bold text-nowrap">PLANT TYPE:</td>
+                <td><?= $h($row['crop_type']) ?></td>
+            </tr>
+        </tbody>
+    </table>
+
+    <!-- ── Download / Back buttons ─────────────────────────────────────────── -->
+    <div class="d-print-none mb-3 d-flex gap-2">
+        <a href="/dashboard/crop-analysis/plant-test-data/plant-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=plant-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>
+
+    <!-- ── 1. General Comment ─────────────────────────────────────────────────────── -->
+    <div class="section-header">General Comment</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['general_details'] ?? '') ?>
+    </div>
+
+    <!-- ── 2. AI Interpretation ───────────────────────────────────────── -->
+    <div class="section-header">AI Soil Interpretation</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['ai_interpretation'] ?? '') ?>
+    </div>
+
+    <!-- ── 3. Recommended Remedial Program ────────────────────────────────────────────────── -->
+    <div class="section-header">
+        <?= $h($savedComments['header1'] ?? 'Recommended Remedial Program') ?>
+    </div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['recommended_details'] ?? '') ?>
+    </div>
+
+    <!-- ── 4. Foliar Program ─────────────────────────────────────────────── -->
+    <div class="section-header">Foliar Program</div>
+    <div class="section-body report-section">
+        <?= formatReportText($savedComments['foliar_details'] ?? '') ?>
+    </div>
+
+    <!-- ── Disclaimer ──────────────────────────────────────────────────────── -->
+    <div class="mt-3 pt-3 border-top">
+        <p class="text-muted" 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. No control can be exercised over
+            storage, handling, mixing, application, or use, or over weather, plant or soil conditions
+            before, during or after application — all of which may affect the performance of our
+            program. No responsibility for, or liability for, any failure in performance, losses,
+            damage or injuries consequential or otherwise arising from such storage, mixing,
+            application or use will be accepted under any circumstances whatsoever. The buyer assumes
+            all responsibility for the use of any of our products.
+        </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>

+ 2 - 2
dashboard/crop-analysis/plant-test-data/plant-report.php

@@ -115,8 +115,8 @@ include __DIR__ . '/../../../layouts/header.php';
                            class="btn btn-outline-secondary btn-sm">
                             &larr; Analysis
                         </a>
-                        <a href="/pdf-files/headlessChrome_pdf.php?type=plant-report&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
-                           class="btn btn-outline-success btn-sm">
+                        <a href="/dashboard/crop-analysis/plant-test-data/plant-report-pdf.php?rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>"
+                        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=plant&rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"