|
|
@@ -1,27 +1,34 @@
|
|
|
<?php
|
|
|
-require_once __DIR__.'/../../../config/database.php';
|
|
|
-require_once __DIR__.'/../../../lib/auth.php';
|
|
|
-require_once __DIR__.'/../../../lib/validation.php';
|
|
|
-require_once __DIR__.'/../../../lib/soil_calculations.php';
|
|
|
+/**
|
|
|
+ * dashboard/crop-analysis/soil-test-data/soil-report.php
|
|
|
+ *
|
|
|
+ * Soil Analysis Report — editable sections with auto-save and Ollama AI interpretation.
|
|
|
+ */
|
|
|
|
|
|
-if (session_status() === PHP_SESSION_NONE) {
|
|
|
- session_start();
|
|
|
-}
|
|
|
+require_once __DIR__ . '/../../../config/database.php';
|
|
|
+require_once __DIR__ . '/../../../lib/auth.php';
|
|
|
+require_once __DIR__ . '/../../../lib/csrf.php';
|
|
|
+require_once __DIR__ . '/../../../lib/soil_calculations.php';
|
|
|
|
|
|
requireLogin();
|
|
|
|
|
|
-$client_id = (int)($_GET['cid'] ?? 0);
|
|
|
-$record_id = (int)($_GET['rid'] ?? 0);
|
|
|
-$rand_id = (float)($_GET['rand'] ?? 0);
|
|
|
+$client_id = (int) ($_GET['cid'] ?? 0);
|
|
|
+$record_id = (int) ($_GET['rid'] ?? 0);
|
|
|
+$rand_id = trim( $_GET['rand'] ?? '');
|
|
|
|
|
|
-if (!$record_id || !$rand_id) {
|
|
|
+if (!$record_id || $rand_id === '') {
|
|
|
http_response_code(400);
|
|
|
die('Invalid request parameters');
|
|
|
}
|
|
|
|
|
|
+$acHa = 2.4710559990832394739; // kg/Ac → kg/ha conversion
|
|
|
+
|
|
|
try {
|
|
|
- $pdo = getDBConnection();
|
|
|
- $stmt = $pdo->prepare("SELECT * FROM soil_records WHERE id = ? AND rand = ?");
|
|
|
+ $pdo = getDBConnection();
|
|
|
+ $userId = getCurrentUserId();
|
|
|
+
|
|
|
+ // Load soil record — verify ownership via rand token
|
|
|
+ $stmt = $pdo->prepare('SELECT * FROM soil_records WHERE id = ? AND rand = ?');
|
|
|
$stmt->execute([$record_id, $rand_id]);
|
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
|
|
@@ -30,213 +37,416 @@ try {
|
|
|
die('Soil record not found');
|
|
|
}
|
|
|
|
|
|
- $client = htmlspecialchars($row['client_name'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
- $address = htmlspecialchars($row['site_address'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
- $state = htmlspecialchars($row['state_postcode'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
- $email = htmlspecialchars($row['email'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
- $labNo = htmlspecialchars($row['lab_no'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
- $sampleDate = htmlspecialchars($row['date_sampled'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
- $sample = htmlspecialchars($row['site_id'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
- $crop = htmlspecialchars($row['sample_id'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+ // Load specification ranges for this soil type
|
|
|
+ $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) ?: [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Load saved report comments (JSON blob in reports table)
|
|
|
+ $savedComments = [];
|
|
|
+ $stmtRpt = $pdo->prepare(
|
|
|
+ 'SELECT comment FROM reports WHERE record_id = ? AND modx_user_id = ? ORDER BY id DESC LIMIT 1'
|
|
|
+ );
|
|
|
+ $stmtRpt->execute([$record_id, $userId]);
|
|
|
+ $savedRow = $stmtRpt->fetchColumn();
|
|
|
+ if ($savedRow) {
|
|
|
+ $decoded = json_decode($savedRow, true);
|
|
|
+ if (is_array($decoded)) {
|
|
|
+ $savedComments = $decoded;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
} catch (PDOException $e) {
|
|
|
- error_log("Database error in soil-report.php: " . $e->getMessage());
|
|
|
+ error_log('DB error in soil-report.php: ' . $e->getMessage());
|
|
|
die('Database error occurred');
|
|
|
}
|
|
|
|
|
|
-$today = date('jS F Y');
|
|
|
-$pageTitle = 'Soil Report - ' . ($client ?: 'Crop Monitoring');
|
|
|
-$siteName = 'Crop Management Platform';
|
|
|
-$activeItem = 'Soil Report';
|
|
|
+// ── Escaped display vars ────────────────────────────────────────────────────
|
|
|
+$client = htmlspecialchars($row['client_name'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+$address = htmlspecialchars($row['site_address'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+$state = htmlspecialchars($row['state_postcode'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+$email = htmlspecialchars($row['email'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+$labNo = htmlspecialchars($row['lab_no'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+$sampleDate = htmlspecialchars($row['date_sampled'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+$sample = htmlspecialchars($row['site_id'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+$cropName = htmlspecialchars($row['sample_id'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+$soilType = htmlspecialchars($row['soil_type'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+$batchNo = htmlspecialchars($row['batch_no'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
|
+
|
|
|
+$today = date('jS F Y');
|
|
|
+$pageTitle = 'Soil Report' . ($client !== '' ? ' — ' . $client : '');
|
|
|
+$siteName = 'Crop Monitor';
|
|
|
+
|
|
|
+// ── Five-year plan element definitions ─────────────────────────────────────
|
|
|
+// [label, soil_records column, min column (record), max column (record), unit]
|
|
|
+// When min/max are empty the spec column of the same name provides the target.
|
|
|
+$planElements = [
|
|
|
+ ['Calcium', 'BS_ca_ppm', 'ca_ppm_min', 'ca_ppm_max', 'kg/ha'],
|
|
|
+ ['Magnesium', 'BS_mg_ppm', 'mg_ppm_min', 'mg_ppm_max', 'kg/ha'],
|
|
|
+ ['Potassium', 'BS_k_ppm', 'k_ppm_min', 'k_ppm_max', 'kg/ha'],
|
|
|
+ ['Sodium', 'BS_na_ppm', 'na_ppm_min', 'na_ppm_max', 'kg/ha'],
|
|
|
+ ['Phosphate', 'p_colwell', '', '', 'kg/ha'],
|
|
|
+ ['Sulfur', 's_morgan', '', '', 'kg/ha'],
|
|
|
+ ['Boron', 'b_cacl2', '', '', 'kg/ha'],
|
|
|
+ ['Manganese', 'mn_dtpa', '', '', 'kg/ha'],
|
|
|
+ ['Zinc', 'zn_dtpa', '', '', 'kg/ha'],
|
|
|
+ ['Copper', 'cu_dtpa', '', '', 'kg/ha'],
|
|
|
+];
|
|
|
|
|
|
-include __DIR__.'/../../../layouts/header.php';
|
|
|
+/**
|
|
|
+ * Calculate total kg/ha deficit for one element, given soil row + spec row.
|
|
|
+ * Returns 0 if already at or above target.
|
|
|
+ */
|
|
|
+function calcDeficit(array $row, array $spec, string $col, string $minCol, string $maxCol): float
|
|
|
+{
|
|
|
+ global $acHa;
|
|
|
+ $value = (float)($row[$col] ?? 0);
|
|
|
+ if ($maxCol !== '') {
|
|
|
+ $maxVal = (float)($row[$maxCol] ?? 0);
|
|
|
+ } else {
|
|
|
+ $maxVal = (float)($spec[$col] ?? 0);
|
|
|
+ }
|
|
|
+ $deficit = ($maxVal - $value) * $acHa;
|
|
|
+ return $deficit > 0 ? round($deficit, 2) : 0.0;
|
|
|
+}
|
|
|
|
|
|
+include __DIR__ . '/../../../layouts/header.php';
|
|
|
?>
|
|
|
|
|
|
-<div class="container">
|
|
|
- <div class="row">
|
|
|
- <div class="col-md-3">
|
|
|
- <img class="img-fluid" src="/client-assets/images/crop-monitor.png" alt="Crop Monitor">
|
|
|
- </div>
|
|
|
- <div class="col-md-9">
|
|
|
- <h4 class="mt-2">Soil Analysis for <?php echo $client; ?></h4>
|
|
|
- <p><strong>Sample:</strong> <?php echo $sample; ?> | <strong>Date:</strong> <?php echo $sampleDate; ?></p>
|
|
|
+<style>
|
|
|
+ .element-required-module .card-group .col {
|
|
|
+ padding: 0 10px;
|
|
|
+ }
|
|
|
+</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">Soil Analysis Report</h1>
|
|
|
+ <div class="d-flex gap-2 d-print-none">
|
|
|
+ <a href="/dashboard/crop-analysis/soil-test-data/soil-analysis.php?rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>&cid=<?= $client_id ?>"
|
|
|
+ class="btn btn-outline-secondary btn-sm">
|
|
|
+ ← Analysis
|
|
|
+ </a>
|
|
|
+ <a href="/dashboard/crop-analysis/soil-test-data/soil-report-pdf.php?rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>"
|
|
|
+ class="btn btn-success btn-sm" target="_blank">
|
|
|
+ <i class="fas fa-file-pdf me-1"></i>PDF 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>
|
|
|
|
|
|
- <!-- display client metadata -->
|
|
|
- <div class="row">
|
|
|
- <div class="col-md-12">
|
|
|
- <table class="table table-sm table-borderless">
|
|
|
- <tr><th>Address</th><td><?php echo $address; ?></td><th>State/Postcode</th><td><?php echo $state; ?></td></tr>
|
|
|
- <tr><th>Email</th><td><?php echo $email; ?></td><th>Lab Number</th><td><?php echo $labNo; ?></td></tr>
|
|
|
- <tr><th>Crop</th><td><?php echo $crop; ?></td><th>Test Date</th><td><?php echo $sampleDate; ?></td></tr>
|
|
|
- </table>
|
|
|
+ <!-- ── Client / Sample info card ─────────────────────────────────────── -->
|
|
|
+ <div class="card mb-4">
|
|
|
+ <div class="card-body py-2">
|
|
|
+ <div class="row g-2">
|
|
|
+ <div class="col-md-2">
|
|
|
+ <img class="img-fluid" src="/client-assets/images/crop-monitor.png" alt="Crop Monitor"
|
|
|
+ style="max-height:60px;">
|
|
|
+ </div>
|
|
|
+ <div class="col-md-10">
|
|
|
+ <div class="row row-cols-2 row-cols-md-3 g-1 small">
|
|
|
+ <div><strong>Client:</strong> <?= $client ?></div>
|
|
|
+ <div><strong>Sample ID:</strong> <?= $sample ?></div>
|
|
|
+ <div><strong>Date Sampled:</strong> <?= $sampleDate ?></div>
|
|
|
+ <div><strong>Address:</strong> <?= $address ?>, <?= $state ?></div>
|
|
|
+ <div><strong>Crop:</strong> <?= $cropName ?></div>
|
|
|
+ <div><strong>Lab No:</strong> <?= $labNo ?></div>
|
|
|
+ <div><strong>Soil Type:</strong> <?= $soilType ?></div>
|
|
|
+ <div><strong>Batch:</strong> <?= $batchNo ?></div>
|
|
|
+ <div><strong>Report Date:</strong> <?= htmlspecialchars($today, ENT_QUOTES, 'UTF-8') ?></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Graph Button -->
|
|
|
- <div class="d-print-none">
|
|
|
- <div class="row p-2">
|
|
|
- <div class="col">
|
|
|
- <a href="/dashboard/crop-analysis/soil-test-data/soil-report-pdf.php?rid=<?= (int)$record_id ?>&rand=<?= (float)$rand_id ?>"
|
|
|
- class="btn btn-success btn-sm" target="_blank">
|
|
|
- <i class="fas fa-file-pdf me-1"></i>View PDF Report
|
|
|
- </a>
|
|
|
+ <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="<?= $record_id ?>">
|
|
|
+ <input type="hidden" name="rand" value="<?= htmlspecialchars($rand_id, ENT_QUOTES, 'UTF-8') ?>">
|
|
|
+
|
|
|
+ <!-- ── 1. Element requirements ────────────────────────────────────── -->
|
|
|
+ <div class="card mb-4">
|
|
|
+ <div class="card-header fw-bold">
|
|
|
+ Total kilograms per hectare of each element needed to balance soil
|
|
|
</div>
|
|
|
- <div class="col">
|
|
|
- <div class="form-status-holder"></div>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-6 g-2">
|
|
|
+ <?php
|
|
|
+ echo soilAnalysisReportCalcs('Ca', 'BS_ca_ppm', 'ca_ppm_min', 'ca_ppm_max', 'Calcium', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('Mg', 'BS_mg_ppm', 'mg_ppm_min', 'mg_ppm_max', 'Magnesium', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('K', 'BS_k_ppm', 'k_ppm_min', 'k_ppm_max', 'Potassium', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('Na', 'BS_na_ppm', 'na_ppm_min', 'na_ppm_max', 'Sodium', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('P', 'p_colwell', '', '', 'Phosphate', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('S', 's_morgan', '', '', 'Sulfur', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('Mn', 'mn_dtpa', '', '', 'Manganese', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('Fe', 'fe_dtpa', '', '', 'Iron', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('Zn', 'zn_dtpa', '', '', 'Zinc', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('Cu', 'cu_dtpa', '', '', 'Copper', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('AmN', 'NH3_N', '', '', 'Amm. Nitrogen', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('B', 'b_cacl2', '', '', 'Boron', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ echo soilAnalysisReportCalcs('NN', 'NO3_N', '', '', 'Nit. Nitrogen', 'kg', 'col', $record_id, $rand_id);
|
|
|
+ ?>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <!-- GRAPH BANNER -->
|
|
|
- <div class="row">
|
|
|
- <div class="col-md-12 text-center fw-bold h4">Soil Analysis Summary</div>
|
|
|
- </div>
|
|
|
|
|
|
-
|
|
|
- <!-- Element Required Module -->
|
|
|
- <div class="element-required-module">
|
|
|
- <div class="alert alert-secondary" role="alert">
|
|
|
- <div class="text-center h5">Total kilograms per hectare of each element needed to balance soil in this test</div>
|
|
|
+ <!-- ── 2. Five-year balancing plan ───────────────────────────────── -->
|
|
|
+ <div class="card mb-4">
|
|
|
+ <div class="card-header fw-bold">
|
|
|
+ Ideal Soil Balancing Program — 5-Year Plan (kg/ha per year)
|
|
|
+ </div>
|
|
|
+ <div class="card-body p-0">
|
|
|
+ <div class="table-responsive">
|
|
|
+ <table class="table table-sm table-bordered table-hover mb-0">
|
|
|
+ <thead class="table-light">
|
|
|
+ <tr>
|
|
|
+ <th>Element</th>
|
|
|
+ <th class="text-center">Total Deficit</th>
|
|
|
+ <?php for ($y = 1; $y <= 5; $y++): ?>
|
|
|
+ <th class="text-center">Year <?= $y ?></th>
|
|
|
+ <?php endfor; ?>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <?php foreach ($planElements as [$label, $col, $minCol, $maxCol, $unit]):
|
|
|
+ $total = calcDeficit($row, $spec, $col, $minCol, $maxCol);
|
|
|
+ $perYear = $total > 0 ? round($total / 5, 2) : 0;
|
|
|
+ ?>
|
|
|
+ <tr>
|
|
|
+ <td><?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?></td>
|
|
|
+ <td class="text-center <?= $total > 0 ? 'text-danger fw-semibold' : 'text-success' ?>">
|
|
|
+ <?= $total > 0 ? $total . ' ' . $unit : '✓ Adequate' ?>
|
|
|
+ </td>
|
|
|
+ <?php for ($y = 1; $y <= 5; $y++): ?>
|
|
|
+ <td class="text-center">
|
|
|
+ <?= $perYear > 0 ? $perYear . ' ' . $unit : '—' ?>
|
|
|
+ </td>
|
|
|
+ <?php endfor; ?>
|
|
|
+ </tr>
|
|
|
+ <?php endforeach; ?>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="card-group">
|
|
|
- <?php
|
|
|
- echo soilAnalysisReportCalcs('Ca', 'BS_ca_ppm', 'ca_ppm_min', 'ca_ppm_max', 'Calcium', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('Mg', 'BS_mg_ppm', 'mg_ppm_min', 'mg_ppm_max', 'Magnesium', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('K', 'BS_k_ppm', 'k_ppm_min', 'k_ppm_max', 'Potasium', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('Na', 'BS_na_ppm', 'na_ppm_min', 'na_ppm_max', 'Sodium', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('P', 'p_colwell', '', '', 'Phosphate', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('S', 's_morgan', '', '', 'Sulfur', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('Mn', 'mn_dtpa', '', '', 'Manganese', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('Fe', 'fe_dtpa', '', '', 'Iron', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('Zn', 'zn_dtpa', '', '', 'Zinc', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('Cu', 'cu_dtpa', '', '', 'Copper', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('AmN', 'NH3_N', '', '', 'AmNitrogen', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('B', 'b_cacl2', '', '', 'Boron', 'kg', 'col', $record_id, $rand_id);
|
|
|
- echo soilAnalysisReportCalcs('NN', 'NO3_N', '', '', 'NNitrogen', 'kg', 'col', $record_id, $rand_id);
|
|
|
- ?>
|
|
|
- <form class="report-form" method="post">
|
|
|
- <input class="" hidden type="text" name="id" id="id" value="<?= (int)getCurrentUserId() ?>">
|
|
|
-
|
|
|
- <!-- Overview Module -->
|
|
|
- <div class="overview-module py-2">
|
|
|
- <div class="alert alert-secondary" role="alert">
|
|
|
- <div class="text-center h5">Overview</div>
|
|
|
+
|
|
|
+ <!-- ── 3. Overview / Executive Summary ───────────────────────────── -->
|
|
|
+ <div class="card mb-4">
|
|
|
+ <div class="card-header d-flex justify-content-between align-items-center fw-bold">
|
|
|
+ <span>Overview</span>
|
|
|
+ <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
|
|
|
+ data-section="overview" data-target="#overview">
|
|
|
+ <i class="fas fa-robot me-1"></i>Generate with AI
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <textarea id="overview" name="overview" class="form-control report-textarea" rows="6"
|
|
|
+ placeholder="Enter an overview of the soil analysis results..."
|
|
|
+ ><?= htmlspecialchars($savedComments['overview'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
|
|
|
</div>
|
|
|
- <textarea id="overview" name="overview" >This is some text within a card body.</textarea>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- Overview Module -->
|
|
|
- <div class="overview-module py-2">
|
|
|
- <div class="alert alert-secondary" role="alert">
|
|
|
- <div class="text-center h5">Ideal Soil Balancing Program for One Season of a FIVE YEAR Plan</div>
|
|
|
+
|
|
|
+ <!-- ── 4. AI Interpretation ───────────────────────────────────────── -->
|
|
|
+ <div class="card mb-4">
|
|
|
+ <div class="card-header d-flex justify-content-between align-items-center fw-bold">
|
|
|
+ <span>AI Soil 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">
|
|
|
- <div class="card-body">
|
|
|
- <?php
|
|
|
- for ($year = 1; $year <= 5; $year++) {
|
|
|
- echo soilProgramCalcs('Ca', 'BS_ca_ppm', 'ca_ppm_min', 'ca_ppm_max', 'Calcium', 'kg', $record_id, $rand_id);
|
|
|
- }
|
|
|
- ?>
|
|
|
- </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 agronomic interpretation, or type manually..."
|
|
|
+ ><?= htmlspecialchars($savedComments['ai_interpretation'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ── 5. Foliar program ──────────────────────────────────────────── -->
|
|
|
+ <div class="card mb-4">
|
|
|
+ <div class="card-header d-flex justify-content-between align-items-center fw-bold">
|
|
|
+ <span>
|
|
|
+ <input type="text" name="header1" class="form-control form-control-sm d-inline-block w-auto fw-bold"
|
|
|
+ value="<?= htmlspecialchars($savedComments['header1'] ?? 'Foliar Program', ENT_QUOTES, 'UTF-8') ?>">
|
|
|
+ </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 the foliar spray program details..."
|
|
|
+ ><?= htmlspecialchars($savedComments['foliar_details'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
-
|
|
|
- <hr>
|
|
|
+ <!-- ── 6. Microbial program ───────────────────────────────────────── -->
|
|
|
+ <div class="card mb-4">
|
|
|
+ <div class="card-header d-flex justify-content-between align-items-center fw-bold">
|
|
|
+ <span>Microbial Program</span>
|
|
|
+ <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
|
|
|
+ data-section="microbial" data-target="#microbe_Program">
|
|
|
+ <i class="fas fa-robot me-1"></i>Generate with AI
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <textarea id="microbe_Program" name="microbe_Program"
|
|
|
+ class="form-control report-textarea" rows="6"
|
|
|
+ placeholder="Enter the microbial / biological program details..."
|
|
|
+ ><?= htmlspecialchars($savedComments['microbe_program'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="row pt-4">
|
|
|
- <p style="font-style: italic; font-size: 9px;">Any recommendations provided by Cropmonitor are advice only, We are not paid consultants and we are not covered to accept responsibiliy for any of our suggestions. As no control can be exercised over storage, handling, mixing application or use, or weather, plant or soil conditions before, during or after application (all of which may affect the preformance of our program), no responsibility for, or liability for any failure in performance, losses, damage or injuries consequential or otherwise, arisiing form such storage mixng application or use will be accepted under any circumstances whatsoever. The buyer assumes all responsibility for the use of any of our products.</p>
|
|
|
+ <!-- ── 7. Disclaimer ──────────────────────────────────────────────── -->
|
|
|
+ <div class="card mb-4 border-0 bg-light">
|
|
|
+ <div class="card-body">
|
|
|
+ <p class="text-muted mb-0" 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. 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>
|
|
|
</div>
|
|
|
-
|
|
|
+
|
|
|
</form>
|
|
|
-</div>
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- <script type="text/javascript">
|
|
|
- $(document).ready(function(){
|
|
|
- var timeoutId;
|
|
|
- $('form textarea, form input').on('input propertychange change', function() {
|
|
|
- console.log('Textarea Change');
|
|
|
-
|
|
|
- clearTimeout(timeoutId);
|
|
|
- timeoutId = setTimeout(function() {
|
|
|
- // Runs 1 second (1000 ms) after the last change
|
|
|
- saveToDB();
|
|
|
- }, 1000);
|
|
|
- });
|
|
|
-
|
|
|
- function saveToDB() {
|
|
|
- console.log('Saving to the db');
|
|
|
- form = $('.report-form');
|
|
|
- $.ajax({
|
|
|
- url: "/dashboard/crop-analysis/updatecomment.php?rid=<?= (int)$record_id ?>&rand=<?= (float)$rand_id ?>",
|
|
|
- type: "POST",
|
|
|
- data: form.serialize(), // serializes the form's elements.
|
|
|
- beforeSend: function(xhr) {
|
|
|
- // Let them know we are saving
|
|
|
- $('.form-status-holder').html('Saving...');
|
|
|
- },
|
|
|
- success: function(data) {
|
|
|
- var jqObj = jQuery(data); // You can get data returned from your ajax call here. ex. jqObj.find('.returned-data').html()
|
|
|
- // Now show them we saved and when we did
|
|
|
- var d = new Date();
|
|
|
- $('.form-status-holder').html('Saved! Last: ' + d.toLocaleTimeString());
|
|
|
- },
|
|
|
- });
|
|
|
+
|
|
|
+</div><!-- /.container-fluid -->
|
|
|
+
|
|
|
+
|
|
|
+<script>
|
|
|
+(function () {
|
|
|
+ 'use strict';
|
|
|
+
|
|
|
+ var saveTimer = null;
|
|
|
+ var statusEl = document.getElementById('save-status');
|
|
|
+ var SAVE_URL = '/dashboard/crop-analysis/updatecomment.php'
|
|
|
+ + '?rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>';
|
|
|
+ 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, .report-form input[name="header1"]')
|
|
|
+ .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) {
|
|
|
+ var t = new Date();
|
|
|
+ setStatus('Saved — ' + t.toLocaleTimeString(), 'success');
|
|
|
+ } else {
|
|
|
+ setStatus('Save failed: ' + (d.message || 'unknown error'), 'danger');
|
|
|
}
|
|
|
-
|
|
|
- // This is just so we don't go anywhere
|
|
|
- // and still save if you submit the form
|
|
|
- $('.report-form').submit(function(e) {
|
|
|
- saveToDB();
|
|
|
- e.preventDefault();
|
|
|
- });
|
|
|
+ })
|
|
|
+ .catch(function () {
|
|
|
+ setStatus('Network error — not saved', 'danger');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ document.querySelector('.report-form').addEventListener('submit', function (e) {
|
|
|
+ e.preventDefault();
|
|
|
+ saveReport();
|
|
|
+ });
|
|
|
+
|
|
|
+ // ── AI generation ────────────────────────────────────────────────────── //
|
|
|
+ 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: <?= $record_id ?>,
|
|
|
+ rand: <?= json_encode($rand_id) ?>,
|
|
|
+ section: section,
|
|
|
+ }),
|
|
|
+ })
|
|
|
+ .then(function (r) { return r.json(); })
|
|
|
+ .then(function (d) {
|
|
|
+ if (d.success && d.text) {
|
|
|
+ textarea.value = d.text;
|
|
|
+ textarea.dispatchEvent(new Event('input')); // trigger auto-save
|
|
|
+ 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;
|
|
|
});
|
|
|
- </script>
|
|
|
-
|
|
|
-</div>
|
|
|
-
|
|
|
- <!--
|
|
|
- <script src="https://cloud.tinymce.com/stable/tinymce.min.js?apiKey=xcotawi18mg1imp8im144buq68h9g3ndd3c9c8215w8qu3ld"></script>
|
|
|
- <script>
|
|
|
- tinymce.init({
|
|
|
- selector: 'textarea',
|
|
|
- menubar: false,
|
|
|
- toolbar: 'bold italic | alignleft aligncenter alignright alignjustify | bullist numlist | removeformat',
|
|
|
- plugins: 'autosave',
|
|
|
- autosave_interval: '20s'
|
|
|
+ }
|
|
|
+
|
|
|
+ document.querySelectorAll('.ai-generate-btn').forEach(function (btn) {
|
|
|
+ btn.addEventListener('click', function () {
|
|
|
+ generateSection(btn, btn.dataset.section, btn.dataset.target);
|
|
|
});
|
|
|
- </script>
|
|
|
- -->
|
|
|
-
|
|
|
-
|
|
|
- <script>
|
|
|
- //https://github.com/eKoopmans/html2pdf.js
|
|
|
- $('.downloadPDF').click(function () {
|
|
|
- var element = document.getElementById('content'); //document.createElement("body");
|
|
|
- element.classList.remove('screen');
|
|
|
- element.classList.add('print');
|
|
|
- var opt = {
|
|
|
- margin: 3,
|
|
|
- filename: 'soil-analysis.pdf',
|
|
|
- image: { type: 'jpeg', quality: 1.0 },
|
|
|
- html2canvas: { scale: 2, letterRendering: true, windowWidth: 1024 }, //, windowWidth: 1024
|
|
|
- jsPDF: { orientation: 'portrait', unit: 'mm', format: 'a4', putOnlyUsedFonts: true, floatPrecision: 'smart', }
|
|
|
- };
|
|
|
- html2pdf()
|
|
|
- .from(element)
|
|
|
- .toPdf()
|
|
|
- .set(opt)
|
|
|
- .save()
|
|
|
- .then(function(){
|
|
|
- element.classList.remove('print');
|
|
|
- element.classList.add('screen');
|
|
|
- });
|
|
|
-
|
|
|
+ });
|
|
|
+
|
|
|
+ // "Interpret All" — stagger requests so Ollama isn't flooded
|
|
|
+ document.getElementById('btn-generate-all').addEventListener('click', function () {
|
|
|
+ var sections = [
|
|
|
+ { section: 'overview', target: '#overview' },
|
|
|
+ { section: 'ai_interpretation', target: '#ai_interpretation' },
|
|
|
+ { section: 'foliar', target: '#foliar_Details' },
|
|
|
+ { section: 'microbial', target: '#microbe_Program' },
|
|
|
+ ];
|
|
|
+ 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>
|
|
|
+ });
|
|
|
+
|
|
|
+})();
|
|
|
+</script>
|
|
|
+
|
|
|
+<?php include __DIR__ . '/../../../layouts/footer.php'; ?>
|