Explorar el Código

AI Update for Plant, Soil, Water, Animals

Benjamin Harris hace 2 meses
padre
commit
464ed0c40a

+ 302 - 93
controllers/ollamaGenerate.php

@@ -63,36 +63,60 @@ if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
     exit;
 }
 
-$recordId = (int)trim($_POST['rid']  ?? '');
-$randId   = trim($_POST['rand']      ?? '');
-$section  = trim($_POST['section']   ?? '');
+$recordId   = (int)trim($_POST['rid']         ?? '');
+$randId     = trim($_POST['rand']             ?? '');
+$section    = trim($_POST['section']          ?? '');
+$recordType = trim($_POST['record_type']      ?? 'soil');  // soil | plant
 
-$validSections = ['overview', 'ai_interpretation', 'foliar', 'microbial'];
-if (!$recordId || $randId === '' || !in_array($section, $validSections, true)) {
+$validSoilSections  = ['overview', 'ai_interpretation', 'foliar', 'microbial'];
+$validPlantSections = ['general', 'ai_interpretation', 'recommended', 'foliar'];
+$validSections      = $recordType === 'plant' ? $validPlantSections : $validSoilSections;
+
+if (!$recordId || $randId === '' || !in_array($section, $validSections, true)
+    || !in_array($recordType, ['soil', 'plant'], true)) {
     http_response_code(400);
     echo json_encode(['success' => false, 'error' => 'Invalid parameters']);
     exit;
 }
 
-// ── Load soil record + spec ───────────────────────────────────────────────────
+// ── Load record + spec ────────────────────────────────────────────────────────
 try {
     $pdo = getDBConnection();
 
-    $stmt = $pdo->prepare('SELECT * FROM soil_records WHERE id = ? AND rand = ?');
-    $stmt->execute([$recordId, $randId]);
-    $row = $stmt->fetch(PDO::FETCH_ASSOC);
-
-    if (!$row) {
-        http_response_code(404);
-        echo json_encode(['success' => false, 'error' => 'Record not found']);
-        exit;
-    }
-
-    $spec = [];
-    if (!empty($row['soil_type'])) {
-        $stmtSpec = $pdo->prepare('SELECT * FROM soil_specifications WHERE soil_type = ? LIMIT 1');
-        $stmtSpec->execute([$row['soil_type']]);
-        $spec = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
+    if ($recordType === 'plant') {
+        $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);
+            echo json_encode(['success' => false, 'error' => 'Record not found']);
+            exit;
+        }
+
+        $spec = [];
+        if (!empty($row['crop_type'])) {
+            $stmtSpec = $pdo->prepare('SELECT * FROM plant_specifications WHERE plant_type = ? LIMIT 1');
+            $stmtSpec->execute([$row['crop_type']]);
+            $spec = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
+        }
+    } else {
+        $stmt = $pdo->prepare('SELECT * FROM soil_records WHERE id = ? AND rand = ?');
+        $stmt->execute([$recordId, $randId]);
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        if (!$row) {
+            http_response_code(404);
+            echo json_encode(['success' => false, 'error' => 'Record not found']);
+            exit;
+        }
+
+        $spec = [];
+        if (!empty($row['soil_type'])) {
+            $stmtSpec = $pdo->prepare('SELECT * FROM soil_specifications WHERE soil_type = ? LIMIT 1');
+            $stmtSpec->execute([$row['soil_type']]);
+            $spec = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
+        }
     }
 
 } catch (PDOException $e) {
@@ -324,80 +348,265 @@ $soilData .= "Deficient: " . (empty($deficiencies) ? 'None detected' : implode('
 $soilData .= "In Excess: " . (empty($excesses)     ? 'None detected' : implode(', ', $excesses))     . "\n";
 $soilData .= "=====================================\n";
 
-// ── RAG: retrieve relevant passages from knowledge_chunks ─────────────────────
-$ragChunks       = retrieveRelevantChunks($pdo, $soilData, $section, RAG_TOP_K);
-$knowledgeContext = '';
-
-if (!empty($ragChunks)) {
-    $knowledgeContext = "\n\n===================================================\n"
-                      . "RELEVANT PASSAGES FROM SOIL SCIENCE LITERATURE\n"
-                      . "(William A. Albrecht and other authorities)\n"
-                      . "===================================================\n";
-    foreach ($ragChunks as $i => $chunk) {
-        $knowledgeContext .= sprintf(
-            "\n[%d] \"%s\" — %s (p.%d)\n%s\n",
-            $i + 1,
-            $chunk['source'],
-            $chunk['author'],
-            $chunk['page'],
-            $chunk['chunk_text']
-        );
+// ── System prompt per record type ────────────────────────────────────────────
+$systemPrompts = [
+    'soil' =>
+        "You are a certified agronomist and soil scientist specialising in mineral soil balancing, "
+        . "trained in the Albrecht method of base saturation and cation exchange chemistry. "
+        . "You have deep knowledge of soil chemistry, soil biology, and the relationship between "
+        . "soil mineral balance and crop productivity, livestock health, and human nutrition. "
+        . "You understand Australian soil types — Vertosols, Chromosols, Sodosols, Tenosols, "
+        . "Dermosols and others — and how climate and rainfall influence nutrient behaviour. "
+        . "Always ground your recommendations in the measured data. "
+        . "Write in a professional but accessible tone suitable for a farmer-facing report. "
+        . "When knowledge passages from the Albrecht literature are provided, "
+        . "prefer them over your general training — they are the authoritative reference.",
+
+    'plant' =>
+        "You are a certified agronomist specialising in plant tissue analysis and crop nutrition. "
+        . "You have deep knowledge of plant physiology, nutrient uptake mechanisms, and the "
+        . "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, "
+        . "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.",
+
+    'water' =>
+        "You are a certified irrigation agronomist and water quality specialist. "
+        . "You have deep knowledge of water chemistry, salinity, sodicity, and the effects of "
+        . "irrigation water quality on soil structure, nutrient availability, and crop health. "
+        . "You are familiar with Australian irrigation guidelines (ANZECC/ARMCANZ), SAR and "
+        . "EC thresholds for different soil types and crops, and bicarbonate/carbonate effects "
+        . "on calcium and magnesium availability. "
+        . "Always base your interpretation on the measured water analysis values. "
+        . "Write in a professional but accessible tone suitable for a farmer-facing report.",
+
+    'animal' =>
+        "You are a certified animal nutritionist and ruminant dietitian with expertise in "
+        . "livestock dietary mineral balance for Australian conditions. "
+        . "You have deep knowledge of macro and trace mineral requirements for cattle, sheep, "
+        . "and other livestock — including the relationship between pasture/feed mineral levels "
+        . "and animal health, reproduction, and production outcomes. "
+        . "You understand antagonisms such as Cu/Mo/S, Se/S, Zn/Fe, and the role of "
+        . "Albrecht-balanced soils in producing nutritionally complete feed. "
+        . "Always base your interpretation on the measured dietary analysis values. "
+        . "Write in a professional but accessible tone suitable for a farmer or veterinarian.",
+
+    'compost' =>
+        "You are a certified agronomist and composting specialist with expertise in organic "
+        . "matter management, compost maturity assessment, and the use of compost to restore "
+        . "soil biology and mineral balance. "
+        . "You have deep knowledge of C:N ratios, microbial succession, humus formation, "
+        . "and how compost inputs interact with existing soil chemistry. "
+         "You are familiar with the Elaine Ingham and Arden Anderson's compost guidelines, "
+        . "You understand Australian composting standards and the role of biologically active "
+        . "compost in supporting the Albrecht soil balancing philosophy. "
+        . "Always base your interpretation on the measured compost analysis values. "
+        . "Write in a professional but accessible tone suitable for a farmer-facing report.",
+];
+
+$system = $systemPrompts[$recordType] ?? $systemPrompts['soil'];
+
+// ── Build analysis data block + prompts depending on record type ──────────────
+
+if ($recordType === 'plant') {
+
+    // ── Plant data summary ────────────────────────────────────────────────────
+    $p = $row;
+    $ps = $spec;
+
+    // Spec column map (matches plant_specifications DB schema)
+    $pSpecCols = [
+        'n'  => ['n_min',  'n_max'],
+        'p'  => ['P_Min',  'P_Max'],
+        'k'  => ['K_Min',  'K_Max'],
+        's'  => ['S_Min',  'S_Max'],
+        'mg' => ['Mg_Min', 'Mg_Max'],
+        'ca' => ['Ca_Min', 'Ca_Max'],
+        'na' => ['Na_Min', 'Na_Max'],
+        'fe' => ['Fe_Min', 'Fe_Max'],
+        'mn' => ['Mn_Min', 'Mn_Max'],
+        'zn' => ['Zn_Min', 'Zn_Max'],
+        'cu' => ['Cu_Min', 'Cu_Max'],
+        'b'  => ['B_Min',  'B_Max'],
+        'm'  => ['M_Min',  'M_Max'],
+        'co' => ['Co_min', 'Co_max'],
+        'se' => ['se_min', 'se_max'],
+        'cl' => ['cl_min', 'cl_max'],
+        'c'  => ['c_min',  'c_max'],
+    ];
+
+    $plantElements = [
+        ['n',  'Nitrogen',   '%'],
+        ['p',  'Phosphorus', '%'],
+        ['k',  'Potassium',  '%'],
+        ['s',  'Sulfur',     '%'],
+        ['mg', 'Magnesium',  '%'],
+        ['ca', 'Calcium',    '%'],
+        ['na', 'Sodium',     '%'],
+        ['fe', 'Iron',      'ppm'],
+        ['mn', 'Manganese', 'ppm'],
+        ['zn', 'Zinc',      'ppm'],
+        ['cu', 'Copper',    'ppm'],
+        ['b',  'Boron',     'ppm'],
+        ['m',  'Molybdenum','ppm'],
+        ['co', 'Cobalt',    'ppm'],
+        ['se', 'Selenium',  'ppm'],
+        ['cl', 'Chloride',  'ppm'],
+        ['c',  'Carbon',    '%'],
+    ];
+
+    $plantDeficiencies = [];
+    $plantExcesses     = [];
+    $elementLines      = '';
+
+    foreach ($plantElements as [$el, $name, $unit]) {
+        $val = (float)($p[$el] ?? 0);
+        [$minCol, $maxCol] = $pSpecCols[$el];
+        $min = (float)($ps[$minCol] ?? 0);
+        $max = (float)($ps[$maxCol] ?? 0);
+        $range = ($min > 0 || $max > 0)
+            ? number_format($min, 3) . '–' . number_format($max, 3)
+            : 'N/A';
+        $found = $val > 0 ? number_format($val, 3) : 'N/A';
+        $status = '';
+        if ($val > 0 && ($min > 0 || $max > 0)) {
+            if ($min > 0 && $val < $min) { $status = '[DEFICIENT]'; $plantDeficiencies[] = $name; }
+            elseif ($max > 0 && $val > $max) { $status = '[EXCESS]'; $plantExcesses[] = $name; }
+            else { $status = '[IDEAL]'; }
+        }
+        $elementLines .= sprintf("%-20s %s  found: %-8s  range: %-15s %s\n",
+            "$name ($unit):", '', $found, $range, $status);
     }
-}
 
-// ── Section-specific prompts ──────────────────────────────────────────────────
-$system = "You are a certified agronomist specialising in soil fertility, trained in the "
-    . "Albrecht method of mineral soil balancing. You have deep knowledge of soil chemistry, "
-    . "plant nutrition, and the relationship between soil mineral balance and crop and livestock health. "
-    . "Always ground your recommendations in the measured data provided. "
-    . "For Australian conditions, reference typical soil types and climate where relevant. "
-    . "Write in a professional but accessible tone suitable for a farmer-facing report. "
-    . "When the knowledge passages conflict with your training, prefer the passages — "
-    . "they are from authoritative soil science texts.";
-
-$ctx = $soilData . $knowledgeContext;
-
-$prompts = [
-    'overview' =>
-        "{$system}\n\n{$ctx}\n\n"
-        . "TASK: Write an executive overview of these soil test results (3–4 paragraphs). "
-        . "Cover: (1) overall soil health and fertility level, "
-        . "(2) the most significant deficiencies or imbalances and their likely effect on crop performance, "
-        . "(3) any positive attributes. "
-        . "Use the Albrecht philosophy as a framework. Do not recommend specific product names.",
-
-    'ai_interpretation' =>
-        "{$system}\n\n{$ctx}\n\n"
-        . "TASK: Write a detailed technical interpretation structured with these headings:\n"
-        . "1. SOIL REACTION (pH, EC, Paramagnetic)\n"
-        . "2. ORGANIC MATTER & BIOLOGY (C, N, C:N ratio)\n"
-        . "3. CATION EXCHANGE CAPACITY & BASE SATURATIONS\n"
-        . "4. MAJOR ELEMENTS (Ca, Mg, K, Na, P — ppm and saturation %)\n"
-        . "5. TRACE ELEMENTS (S, B, Mn, Cu, Zn, Fe, Al, Si, Co, Mo, Se)\n"
-        . "6. ELEMENTAL RATIOS & INTERACTIONS (Ca:Mg, C:N, K:Mg antagonisms)\n"
-        . "7. OVERALL SOIL BALANCE ASSESSMENT\n"
-        . "For each element marked [DEFICIENT] or [EXCESS], explain agronomic significance "
-        . "and interactions with other elements. Reference the Albrecht literature where relevant.",
-
-    'foliar' =>
-        "{$system}\n\n{$ctx}\n\n"
-        . "TASK: Design a foliar nutrition program to address the deficiencies shown. "
-        . "Format as a numbered list or table: "
-        . "Growth Stage | Product Type (generic) | Active Element | Rate (L or kg/ha) | Timing. "
-        . "Prioritise elements marked [DEFICIENT]. "
-        . "Note antagonisms (e.g. Ca/Mg competition, Zn/P, K/Mg lockout). "
-        . "Add a note on carrier water pH and adjuvant recommendations.",
-
-    'microbial' =>
-        "{$system}\n\n{$ctx}\n\n"
-        . "TASK: Design a biological/microbial soil improvement program. "
-        . "Structure your response:\n"
-        . "1. CURRENT BIOLOGY ASSESSMENT (based on OM%, C:N ratio, pH)\n"
-        . "2. RECOMMENDED INOCULANTS (mycorrhizae, rhizobia, EM, compost tea etc.)\n"
-        . "3. CARBON FEEDING STRATEGY (humates, fish hydrolysate, molasses, cover crops)\n"
-        . "4. TIMING & INTEGRATION with the mineral balancing program\n"
-        . "Reference Albrecht's work on the relationship between mineral balance and soil biology.",
-];
+    $plantData = "=====================================\n"
+        . "PLANT TISSUE ANALYSIS — COMPLETE\n"
+        . "=====================================\n"
+        . "Client:       {$p['client_name']}\n"
+        . "Crop Type:    {$p['crop_type']}\n"
+        . "Sample ID:    {$p['sample_id']}\n"
+        . "Site ID:      {$p['site_id']}\n"
+        . "Lab No:       {$p['lab_no']}\n"
+        . "Date Sampled: {$p['date_sampled']}\n\n"
+        . "--- ELEMENT RESULTS ---\n"
+        . $elementLines . "\n"
+        . "Deficient: " . (empty($plantDeficiencies) ? 'None detected' : implode(', ', $plantDeficiencies)) . "\n"
+        . "In Excess: " . (empty($plantExcesses)     ? 'None detected' : implode(', ', $plantExcesses))     . "\n"
+        . "=====================================\n";
+
+    $ragChunks        = retrieveRelevantChunks($pdo, $plantData, $section, RAG_TOP_K);
+    $knowledgeContext = '';
+    if (!empty($ragChunks)) {
+        $knowledgeContext = "\n\n===================================================\n"
+                          . "RELEVANT PASSAGES FROM SOIL & PLANT SCIENCE LITERATURE\n"
+                          . "===================================================\n";
+        foreach ($ragChunks as $i => $chunk) {
+            $knowledgeContext .= sprintf("\n[%d] \"%s\" — %s (p.%d)\n%s\n",
+                $i + 1, $chunk['source'], $chunk['author'], $chunk['page'], $chunk['chunk_text']);
+        }
+    }
+
+    $ctx = $plantData . $knowledgeContext;
+
+    $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, "
+            . "(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"
+            . "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.",
+
+        'recommended' =>
+            "{$system}\n\n{$ctx}\n\n"
+            . "TASK: Design a recommended remedial soil and fertiliser program to correct the deficiencies shown. "
+            . "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.",
+
+        'foliar' =>
+            "{$system}\n\n{$ctx}\n\n"
+            . "TASK: Design a foliar spray program to rapidly correct deficiencies shown in the tissue test. "
+            . "Format as a numbered list or table: "
+            . "Growth Stage | Product Type (generic) | Active Element | Rate (L or kg/ha) | Timing. "
+            . "Prioritise elements marked [DEFICIENT]. "
+            . "Note carrier water pH requirements and any tank-mix incompatibilities.",
+    ];
+
+} else {
+
+    // ── Soil: RAG + prompts ───────────────────────────────────────────────────
+    $ragChunks        = retrieveRelevantChunks($pdo, $soilData, $section, RAG_TOP_K);
+    $knowledgeContext = '';
+    if (!empty($ragChunks)) {
+        $knowledgeContext = "\n\n===================================================\n"
+                          . "RELEVANT PASSAGES FROM SOIL SCIENCE LITERATURE\n"
+                          . "(William A. Albrecht and other authorities)\n"
+                          . "===================================================\n";
+        foreach ($ragChunks as $i => $chunk) {
+            $knowledgeContext .= sprintf("\n[%d] \"%s\" — %s (p.%d)\n%s\n",
+                $i + 1, $chunk['source'], $chunk['author'], $chunk['page'], $chunk['chunk_text']);
+        }
+    }
+
+    $ctx = $soilData . $knowledgeContext;
+
+    $prompts = [
+        'overview' =>
+            "{$system}\n\n{$ctx}\n\n"
+            . "TASK: Write an executive overview of these soil test results (3–4 paragraphs). "
+            . "Cover: (1) overall soil health and fertility level, "
+            . "(2) the most significant deficiencies or imbalances and their likely effect on crop performance, "
+            . "(3) any positive attributes. "
+            . "Use the Albrecht philosophy as a framework. Do not recommend specific product names.",
+
+        'ai_interpretation' =>
+            "{$system}\n\n{$ctx}\n\n"
+            . "TASK: Write a detailed technical interpretation structured with these headings:\n"
+            . "1. SOIL REACTION (pH, EC, Paramagnetic)\n"
+            . "2. ORGANIC MATTER & BIOLOGY (C, N, C:N ratio)\n"
+            . "3. CATION EXCHANGE CAPACITY & BASE SATURATIONS\n"
+            . "4. MAJOR ELEMENTS (Ca, Mg, K, Na, P — ppm and saturation %)\n"
+            . "5. TRACE ELEMENTS (S, B, Mn, Cu, Zn, Fe, Al, Si, Co, Mo, Se)\n"
+            . "6. ELEMENTAL RATIOS & INTERACTIONS (Ca:Mg, C:N, K:Mg antagonisms)\n"
+            . "7. OVERALL SOIL BALANCE ASSESSMENT\n"
+            . "For each element marked [DEFICIENT] or [EXCESS], explain agronomic significance "
+            . "and interactions with other elements. Reference the Albrecht literature where relevant.",
+
+        'foliar' =>
+            "{$system}\n\n{$ctx}\n\n"
+            . "TASK: Design a foliar nutrition program to address the deficiencies shown. "
+            . "Format as a numbered list or table: "
+            . "Growth Stage | Product Type (generic) | Active Element | Rate (L or kg/ha) | Timing. "
+            . "Prioritise elements marked [DEFICIENT]. "
+            . "Note antagonisms (e.g. Ca/Mg competition, Zn/P, K/Mg lockout). "
+            . "Add a note on carrier water pH and adjuvant recommendations.",
+
+        'microbial' =>
+            "{$system}\n\n{$ctx}\n\n"
+            . "TASK: Design a biological/microbial soil improvement program. "
+            . "Structure your response:\n"
+            . "1. CURRENT BIOLOGY ASSESSMENT (based on OM%, C:N ratio, pH)\n"
+            . "2. RECOMMENDED INOCULANTS (mycorrhizae, rhizobia, EM, compost tea etc.)\n"
+            . "3. CARBON FEEDING STRATEGY (humates, fish hydrolysate, molasses, cover crops)\n"
+            . "4. TIMING & INTEGRATION with the mineral balancing program\n"
+            . "Reference Albrecht's work on the relationship between mineral balance and soil biology.",
+    ];
+
+}
 
 // ── Call Ollama ───────────────────────────────────────────────────────────────
 $payload = json_encode([

+ 4 - 3
dashboard/crop-analysis/plant-test-data/plant-report-save.php

@@ -27,8 +27,8 @@ header('Content-Type: application/json');
 $pdo    = getDBConnection();
 $userId = getCurrentUserId();
 
-$recordId = (int)   ($_GET['rid']  ?? 0);
-$randId   = (float) ($_GET['rand'] ?? 0);
+$recordId = (int)  ($_GET['rid']  ?? 0);
+$randId   = trim(  $_GET['rand']  ?? '');
 
 if ($recordId <= 0) {
     http_response_code(400);
@@ -49,6 +49,7 @@ if (!$check->fetch()) {
 
 $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']      ?? ''),
 ];
@@ -60,6 +61,6 @@ $stmt = $pdo->prepare('
     VALUES (?, ?, ?, ?, CURDATE())
     ON DUPLICATE KEY UPDATE comment = VALUES(comment), dateTime = CURDATE()
 ');
-$stmt->execute([$userId, $recordId, (int) $randId, $comment]);
+$stmt->execute([$userId, $recordId, $randId, $comment]);
 
 echo json_encode(['success' => true, 'saved' => date('H:i:s')]);

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

@@ -1,196 +1,339 @@
 <?php
 /**
- * plant-report.php
+ * dashboard/crop-analysis/plant-test-data/plant-report.php
  *
- * Displays the plant analysis consultant notes / PDF report.
- * Accessed via rid + rand URL params.
- * Notes auto-save to the reports table via plant-report-save.php.
+ * Plant analysis report — editable sections with auto-save and Ollama AI interpretation.
  */
 
 require_once __DIR__ . '/../../../config/database.php';
 require_once __DIR__ . '/../../../lib/auth.php';
-
-if (session_status() === PHP_SESSION_NONE) {
-    session_start();
-}
+require_once __DIR__ . '/../../../lib/csrf.php';
 
 requireLogin();
 
-$pdo    = getDBConnection();
-$userId = getCurrentUserId();
+$recordId = (int)  ($_GET['rid']  ?? 0);
+$randId   = trim(  $_GET['rand']  ?? '');
+$clientId = (int)  ($_GET['cid']  ?? 0);
 
-$recordId = (int)   ($_GET['rid']  ?? 0);
-$randId   = (float) ($_GET['rand'] ?? 0);
+if (!$recordId || $randId === '') {
+    http_response_code(400);
+    die('Invalid request parameters');
+}
 
-$row   = null;
-$notes = ['general_details' => '', 'recommended_details' => '', 'foliar_details' => ''];
+try {
+    $pdo    = getDBConnection();
+    $userId = getCurrentUserId();
 
-if ($recordId > 0) {
-    $stmt = $pdo->prepare('SELECT * FROM plant_records WHERE id = ? AND rand = ? LIMIT 1');
+    $stmt = $pdo->prepare('SELECT * FROM plant_records WHERE id = ? AND rand = ?');
     $stmt->execute([$recordId, $randId]);
-    $row = $stmt->fetch();
-}
+    $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) ?: [];
+    }
 
-if ($row) {
-    $rpt = $pdo->prepare(
+    // 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'
     );
-    $rpt->execute([$recordId, $userId]);
-    $saved = $rpt->fetchColumn();
-    if ($saved) {
-        $decoded = json_decode($saved, true);
+    $stmtRpt->execute([$recordId, $userId]);
+    $savedRow = $stmtRpt->fetchColumn();
+    if ($savedRow) {
+        $decoded = json_decode($savedRow, true);
         if (is_array($decoded)) {
-            $notes = array_merge($notes, $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');
+$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';
+
+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>Plant 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" 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; }
-        }
-        .chart-header     { background: #343a40; color: white; text-align: center; padding: 6px; }
-        .chart-header-sub { background: #6c757d; color: white; text-align: center; padding: 4px; }
-        textarea { width: 100%; min-height: 120px; border: 1px solid #dee2e6; padding: 8px; font-size: 0.9rem; }
-        .save-status { font-size: 0.75rem; color: #6c757d; }
-    </style>
-</head>
-<body>
-<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">
-        <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 d-flex align-items-center gap-2">
-        <button class="btn btn-success btn-sm downloadPDF">
-            <i class="fas fa-download me-1"></i>Download PDF
-        </button>
-        <span class="save-status" id="saveStatus"></span>
+<div id="layoutSidenav">
+    <div id="layoutSidenav_nav">
+        <?php include __DIR__ . '/../../../layouts/sidebar.php'; ?>
     </div>
+    <div id="layoutSidenav_content">
+        <main>
+            <div class="container-fluid px-4">
 
-    <!-- Client info -->
-    <table class="table table-sm table-bordered mb-3" style="font-size:0.85rem;">
-        <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>
-            </tr>
-            <tr>
-                <th>Sample ID</th>
-                <td><?= $h($row['sample_id'] ?? '') ?></td>
-                <th>Site ID</th>
-                <td><?= $h($row['site_id'] ?? '') ?></td>
-            </tr>
-            <tr>
-                <th>Crop</th>
-                <td><?= $h($row['crop_type'] ?? '') ?></td>
-                <th>Date Sampled</th>
-                <td><?= $h($row['date_sampled'] ?? '') ?></td>
-            </tr>
-        </tbody>
-    </table>
-
-    <div class="text-center fw-bold h5 mb-3">PLANT ANALYSIS SUMMARY</div>
-
-    <form class="report-form">
-        <!-- General Comment -->
-        <div class="chart-header mb-1">General Comment</div>
-        <div class="mb-3">
-            <textarea id="general_details" name="general_details"><?= $h($notes['general_details']) ?></textarea>
-        </div>
-
-        <!-- Recommended Program -->
-        <div class="chart-header mb-1">Recommended Remedial Program</div>
-        <div class="mb-3">
-            <textarea id="recommended_details" name="recommended_details"><?= $h($notes['recommended_details']) ?></textarea>
-        </div>
-
-        <!-- Foliar Program -->
-        <div class="chart-header mb-1">Foliar Program</div>
-        <div class="mb-3">
-            <textarea id="foliar_details" name="foliar_details"><?= $h($notes['foliar_details']) ?></textarea>
-        </div>
-    </form>
-
-    <div class="mt-4 small text-muted">
-        <p><i class="fa fa-leaf" style="color:green"></i> It is always an advantage to assess tissue analysis results along with a corresponding soil analysis for more accurate diagnosis of plant nutrient status.</p>
-        <p><i class="fa fa-leaf" style="color:green"></i> Trace element levels — manganese, copper and zinc — can all be affected by fungicide spray residues, giving misleading results.</p>
-        <p><i class="fa fa-leaf" style="color:green"></i> Talk to your qualified consultant to make a plan for correction or maintenance of the found nutrient levels.</p>
-        <p style="font-style:italic;font-size:9px;">Desired ranges indexed from: CSIRO Plant Analysis Handbook 2nd Ed. Hill Laboratories consultants guide. PIRSA Soil and Plant Analysis.</p>
-        <p style="font-style:italic;font-size:9px;">Any recommendations provided by Cropmonitor are advice only. We are not paid consultants and are not covered to accept responsibility for any of our suggestions.</p>
-    </div>
+                <!-- ── Page heading ─────────────────────────────────────────── -->
+                <div class="d-flex align-items-center justify-content-between mt-4 mb-3">
+                    <h1 class="h3 mb-0">Plant Analysis Report</h1>
+                    <div class="d-flex gap-2 d-print-none">
+                        <a href="/dashboard/crop-analysis/plant-test-data/plant-analysis.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
+                           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">
+                            <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 ?>"
+                           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>Crop Type:</strong> <?= $h($row['crop_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>
+                            <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 class="card-body">
+                            <textarea id="general_details" name="general_details"
+                                      class="form-control report-textarea" rows="6"
+                                      placeholder="Enter a general comment on this plant analysis..."
+                            ><?= $h($savedComments['general_details']) ?></textarea>
+                        </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 Plant Interpretation</span>
+                            <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 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>
+                    </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>
+                            <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 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>
+                    </div>
+
+                    <!-- ── 4. Foliar Program ──────────────────────────────────── -->
+                    <div class="card mb-4">
+                        <div class="card-header d-flex justify-content-between align-items-center fw-bold">
+                            <span>Foliar Program</span>
+                            <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 class="card-body">
+                            <textarea id="foliar_details" name="foliar_details"
+                                      class="form-control report-textarea" rows="6"
+                                      placeholder="Enter foliar spray program details..."
+                            ><?= $h($savedComments['foliar_details']) ?></textarea>
+                        </div>
+                    </div>
 
-    <?php endif; ?>
+                    <!-- ── Disclaimer ─────────────────────────────────────────── -->
+                    <div class="card mb-4 border-0 bg-light">
+                        <div class="card-body">
+                            <p class="text-muted mb-1" style="font-size:0.75rem;">
+                                <i class="fa fa-leaf" style="color:green"></i>
+                                It is always an advantage to assess tissue analysis results along with a corresponding soil analysis for more accurate diagnosis of plant nutrient status.
+                            </p>
+                            <p class="text-muted mb-1" style="font-size:0.75rem;">
+                                <i class="fa fa-leaf" style="color:green"></i>
+                                Trace element levels — manganese, copper and zinc — can all be affected by fungicide spray residues, giving misleading results.
+                            </p>
+                            <p class="text-muted mb-0 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>
+                    </div>
+
+                </form>
+
+            </div><!-- /.container-fluid -->
+        </main>
+        <?php include __DIR__ . '/../../../layouts/footer.php'; ?>
+    </div>
 </div>
 
-<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
 <script>
 (function () {
-    var timeoutId;
-    var form = document.querySelector('.report-form');
-    if (!form) return;
-
-    form.querySelectorAll('textarea').forEach(function (el) {
-        el.addEventListener('input', function () {
-            clearTimeout(timeoutId);
-            timeoutId = setTimeout(saveToDB, 1200);
+    'use strict';
+
+    var saveTimer  = null;
+    var statusEl   = document.getElementById('save-status');
+    var SAVE_URL   = '/dashboard/crop-analysis/plant-test-data/plant-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');
+    }
+
+    // ── Auto-save ────────────────────────────────────────────────────────── //
+    document.querySelectorAll('.report-form .report-textarea')
+        .forEach(function (el) {
+            el.addEventListener('input', function () {
+                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 saveToDB() {
-        var data = new FormData(form);
-        var params = new URLSearchParams();
-        data.forEach(function (v, k) { params.append(k, v); });
+    // ── AI generation ────────────────────────────────────────────────────── //
+    function generateSection(btn, section, targetSelector) {
+        var textarea = document.querySelector(targetSelector);
+        if (!textarea) return;
 
-        document.getElementById('saveStatus').textContent = 'Saving…';
+        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('/dashboard/crop-analysis/plant-test-data/plant-report-save.php?rid=<?= (int)$recordId ?>&rand=<?= (float)$randId ?>', {
+        fetch(AI_URL, {
             method: 'POST',
             headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
-            body: params.toString()
+            body: new URLSearchParams({
+                csrf_token:  CSRF_TOKEN,
+                rid:         <?= $recordId ?>,
+                rand:        <?= json_encode($randId) ?>,
+                section:     section,
+                record_type: 'plant',
+            }),
         })
         .then(function (r) { return r.json(); })
         .then(function (d) {
-            document.getElementById('saveStatus').textContent = d.success ? ('Saved ' + d.saved) : 'Save failed';
+            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 () {
-            document.getElementById('saveStatus').textContent = 'Save error';
+            setStatus('Could not reach AI service. Is Ollama running on port 11434?', 'danger');
+        })
+        .finally(function () {
+            btn.disabled = false;
+            btn.innerHTML = origHTML;
         });
     }
-})();
 
-document.querySelector('.downloadPDF') && document.querySelector('.downloadPDF').addEventListener('click', function () {
-    var opt = {
-        margin: 3,
-        filename: 'plant-report.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();
-});
+    document.querySelectorAll('.ai-generate-btn').forEach(function (btn) {
+        btn.addEventListener('click', function () {
+            generateSection(btn, btn.dataset.section, btn.dataset.target);
+        });
+    });
+
+    // "Interpret All" — stagger requests
+    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>
-</body>
-</html>