prepare('SELECT * FROM soil_records WHERE id = ? AND rand = ?'); $stmt->execute([$record_id, $rand_id]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { http_response_code(404); die('Soil record not found'); } // 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('DB error in soil-report.php: ' . $e->getMessage()); die('Database error occurred'); } // ── 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'], ]; /** * 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; } // Parse and render markdown text from AI-generated report sections function formatReportText(string $text): string { if (trim($text) === '') { return '

No content saved.

'; } $parsedown = new Parsedown(); $parsedown->setSafeMode(true); return $parsedown->text($text); } include __DIR__ . '/../../../layouts/header.php'; ?>

Soil Analysis Report

Crop Monitor
Client:
Sample ID:
Date Sampled:
Address: ,
Crop:
Lab No:
Soil Type:
Batch:
Report Date:
Total kilograms per hectare of each element needed to balance soil
Ideal Soil Balancing Program — 5-Year Plan (kg/ha per year)
0 ? round($total / 5, 2) : 0; ?>
Element Total Deficit Year
0 ? $total . ' ' . $unit : '✓ Adequate' ?> 0 ? $perYear . ' ' . $unit : '—' ?>
Overview
AI Soil Interpretation

AI-generated agronomic interpretation. Review and edit before including in the final report.

Microbial Program

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.