prepare("SELECT * FROM client_records WHERE id = ? AND modx_user_id = ?"); $stmt->execute([$clientId, $userId]); $client = $stmt->fetch(); if (!$client) { http_response_code(404); die('Client not found or access denied.'); } // ── Load soil tests ─────────────────────────────────────────────────────────── $stmtSoil = $pdo->prepare(" SELECT id, date_sampled, lab_no, sample_id, site_id, crop_type, soil_type, ph_cacl2, ph_h2o, NO3_N, p_morgan, k_morgan, ca_morgan, mg_morgan, ec, ocarbon, rand, status FROM soil_records WHERE CAST(client_records_id AS UNSIGNED) = ? ORDER BY date_sampled DESC, id DESC "); $stmtSoil->execute([$clientId]); $soilTests = $stmtSoil->fetchAll(); // ── Load plant tests ────────────────────────────────────────────────────────── $stmtPlant = $pdo->prepare(" SELECT id, date_sampled, lab_no, sample_id, site_id, crop_type, n, p, k, s, ca, mg, rand, status FROM plant_records WHERE client_records_id = ? ORDER BY date_sampled DESC, id DESC "); $stmtPlant->execute([$clientId]); $plantTests = $stmtPlant->fetchAll(); // ── Load water tests ────────────────────────────────────────────────────────── $stmtWater = $pdo->prepare(" SELECT id, date_sampled, lab_no, sample_id, site_id, crop_type, ph, cond_dsm, no3, p, k, ca, mg, na, rand, status FROM water_records WHERE client_records_id = ? ORDER BY date_sampled DESC, id DESC "); $stmtWater->execute([$clientId]); $waterTests = $stmtWater->fetchAll(); // ── Alerts from latest soil test ────────────────────────────────────────────── $alertData = ['summary' => ['critical' => 0, 'watch' => 0], 'items' => []]; $latestSoil = null; if (!empty($soilTests)) { // Fetch full row for latest test (we only selected subset above) $stmtFull = $pdo->prepare("SELECT * FROM soil_records WHERE id = ? LIMIT 1"); $stmtFull->execute([$soilTests[0]['id']]); $latestSoil = $stmtFull->fetch(); $spec = null; if (!empty($latestSoil['soil_type'])) { $stmtSpec = $pdo->prepare("SELECT * FROM soil_specifications WHERE soil_type = ? LIMIT 1"); $stmtSpec->execute([$latestSoil['soil_type']]); $spec = $stmtSpec->fetch() ?: null; } $alertData = generateAlerts($latestSoil, $spec); } // ── Timeline: union across all test types ───────────────────────────────────── $timeline = []; foreach ($soilTests as $r) { $timeline[] = [ 'type' => 'soil', 'date' => $r['date_sampled'], 'lab_no' => $r['lab_no'], 'sample_id' => $r['sample_id'], 'site_id' => $r['site_id'], 'crop_type' => $r['crop_type'], 'id' => $r['id'], 'rand' => $r['rand'], ]; } foreach ($plantTests as $r) { $timeline[] = [ 'type' => 'plant', 'date' => $r['date_sampled'], 'lab_no' => $r['lab_no'], 'sample_id' => $r['sample_id'], 'site_id' => $r['site_id'], 'crop_type' => $r['crop_type'], 'id' => $r['id'], 'rand' => $r['rand'], ]; } foreach ($waterTests as $r) { $timeline[] = [ 'type' => 'water', 'date' => $r['date_sampled'], 'lab_no' => $r['lab_no'], 'sample_id' => $r['sample_id'], 'site_id' => $r['site_id'], 'crop_type' => $r['crop_type'], 'id' => $r['id'], 'rand' => $r['rand'], ]; } usort($timeline, fn($a, $b) => strcmp($b['date'] ?? '', $a['date'] ?? '')); // ── Chart.js data (soil tests in chronological order for trend lines) ───────── $chartSoil = array_reverse($soilTests); // oldest first $chartLabels = []; $chartPhCaCl2 = []; $chartPhH2O = []; $chartNO3N = []; $chartP = []; $chartK = []; $chartCa = []; $chartMg = []; foreach ($chartSoil as $r) { if (!$r['date_sampled']) continue; $chartLabels[] = date('j M Y', strtotime($r['date_sampled'])); $chartPhCaCl2[] = $r['ph_cacl2'] !== '' && $r['ph_cacl2'] !== null ? (float) $r['ph_cacl2'] : null; $chartPhH2O[] = $r['ph_h2o'] !== '' && $r['ph_h2o'] !== null ? (float) $r['ph_h2o'] : null; $chartNO3N[] = $r['NO3_N'] !== '' && $r['NO3_N'] !== null ? (float) $r['NO3_N'] : null; $chartP[] = $r['p_morgan'] !== '' && $r['p_morgan'] !== null ? (float) $r['p_morgan'] : null; $chartK[] = $r['k_morgan'] !== '' && $r['k_morgan'] !== null ? (float) $r['k_morgan'] : null; $chartCa[] = $r['ca_morgan']!== '' && $r['ca_morgan']!== null ? (float) $r['ca_morgan']: null; $chartMg[] = $r['mg_morgan']!== '' && $r['mg_morgan']!== null ? (float) $r['mg_morgan']: null; } // ── Page setup ──────────────────────────────────────────────────────────────── $clientName = htmlspecialchars($client['client'] ?? '—', ENT_QUOTES, 'UTF-8'); $company = htmlspecialchars($client['company'] ?? '', ENT_QUOTES, 'UTF-8'); $pageTitle = $clientName . ' — Client Detail'; $siteName = 'Crop Monitor'; $activeTab = $_GET['tab'] ?? 'soil'; $totalAlerts = $alertData['summary']['critical'] + $alertData['summary']['watch']; include __DIR__ . '/../../layouts/header.php'; include __DIR__ . '/../../layouts/navbar.php'; ?>

Soil Tests Plant Tests Water Tests 0): ?> Critical  ·  Watch All nutrients in range

No soil tests recorded for this client.

Soil Test History New Test
Date Sampled Lab No. Sample / Site Crop pH CaCl₂ pH H₂O NO₃-N P K EC

View
= 2): ?>
pH Trend
Key Nutrients (mg/kg)

No plant tests recorded for this client.

Plant Test History New Test
Date Sampled Lab No. Sample / Site Crop N% P% K% S% Ca% Mg%

View

No water tests recorded for this client.

Water Test History New Test
Date Sampled Lab No. Sample / Site Crop pH EC (dS/m) NO₃ P K Na

View

No soil tests available to generate alerts.

All measured nutrients are within acceptable ranges based on the latest soil test ().

Based on soil test sampled . Soil type: .

0) { $pct = round(($alert['measured'] / $alert['min']) * 100); } ?>
Measured Target min
% of target minimum

No tests recorded yet.

['label' => 'Soil', 'icon' => 'globe-asia', 'colour' => 'success'], 'plant' => ['label' => 'Plant', 'icon' => 'pagelines', 'colour' => 'info', 'fa' => 'fab'], 'water' => ['label' => 'Water', 'icon' => 'tint', 'colour' => 'primary'], ]; $prevDate = null; foreach ($timeline as $entry): $cfg = $typeConfig[$entry['type']]; $entryDate = $entry['date'] ? date('F Y', strtotime($entry['date'])) : 'Unknown'; $fa = $cfg['fa'] ?? 'fas'; // Month/year separator if ($entryDate !== $prevDate): $prevDate = $entryDate; ?>
< class="fa- text- fa-sm">
Lab: ·
= 2): ?> —'; $v = (float) $raw; $class = ''; if ($min !== null && $v < $min) { $class = $v < ($min * 0.5) ? 'text-danger fw-bold' : 'text-warning fw-bold'; } elseif ($max !== null && $v > $max) { $class = 'text-warning fw-bold'; } else { $class = 'text-success'; } return '' . htmlspecialchars($raw, ENT_QUOTES, 'UTF-8') . ''; } /** * Render a plain numeric cell — dash if empty. */ function numCell(?string $raw): string { if ($raw === null || $raw === '') return ''; return htmlspecialchars($raw, ENT_QUOTES, 'UTF-8'); } ?>