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';
?>
= htmlspecialchars(trim($client['address'] . ' ' . $client['state_postcode']), ENT_QUOTES, 'UTF-8') ?>
= count($soilTests) ?> Soil Tests
= count($plantTests) ?> Plant Tests
= count($waterTests) ?> Water Tests
0): ?>
= $alertData['summary']['critical'] ?> Critical · = $alertData['summary']['watch'] ?> Watch
All nutrients in range
-
-
-
-
-
No soil tests recorded for this client.
| Date Sampled |
Lab No. |
Sample / Site |
Crop |
pH CaCl₂ |
pH H₂O |
NO₃-N |
P |
K |
EC |
|
| = fmtDate($r['date_sampled']) ?> |
= htmlspecialchars($r['lab_no'] ?? '—', ENT_QUOTES, 'UTF-8') ?> |
= htmlspecialchars($r['sample_id'] ?? '', ENT_QUOTES, 'UTF-8') ?>
= htmlspecialchars($r['site_id'], ENT_QUOTES, 'UTF-8') ?>
|
= htmlspecialchars($r['crop_type'] ?? '—', ENT_QUOTES, 'UTF-8') ?> |
= soilCell($r['ph_cacl2'], 5.5, 7.5) ?> |
= soilCell($r['ph_h2o'], 6.0, 8.0) ?> |
= numCell($r['NO3_N']) ?> |
= numCell($r['p_morgan']) ?> |
= numCell($r['k_morgan']) ?> |
= soilCell($r['ec'], null, 1.5) ?> |
View
|
= 2): ?>
No plant tests recorded for this client.
| Date Sampled |
Lab No. |
Sample / Site |
Crop |
N% |
P% |
K% |
S% |
Ca% |
Mg% |
|
| = fmtDate($r['date_sampled']) ?> |
= htmlspecialchars($r['lab_no'] ?? '—', ENT_QUOTES, 'UTF-8') ?> |
= htmlspecialchars($r['sample_id'] ?? '', ENT_QUOTES, 'UTF-8') ?>
= htmlspecialchars($r['site_id'], ENT_QUOTES, 'UTF-8') ?>
|
= htmlspecialchars($r['crop_type'] ?? '—', ENT_QUOTES, 'UTF-8') ?> |
= numCell($r['n']) ?> |
= numCell($r['p']) ?> |
= numCell($r['k']) ?> |
= numCell($r['s']) ?> |
= numCell($r['ca']) ?> |
= numCell($r['mg']) ?> |
View
|
No water tests recorded for this client.
| Date Sampled |
Lab No. |
Sample / Site |
Crop |
pH |
EC (dS/m) |
NO₃ |
P |
K |
Na |
|
| = fmtDate($r['date_sampled']) ?> |
= htmlspecialchars($r['lab_no'] ?? '—', ENT_QUOTES, 'UTF-8') ?> |
= htmlspecialchars($r['sample_id'] ?? '', ENT_QUOTES, 'UTF-8') ?>
= htmlspecialchars($r['site_id'], ENT_QUOTES, 'UTF-8') ?>
|
= htmlspecialchars($r['crop_type'] ?? '—', ENT_QUOTES, 'UTF-8') ?> |
= soilCell($r['ph'], 6.0, 8.0) ?> |
= soilCell($r['cond_dsm'], null, 1.5) ?> |
= numCell($r['no3']) ?> |
= numCell($r['p']) ?> |
= numCell($r['k']) ?> |
= numCell($r['na']) ?> |
View
|
No soil tests available to generate alerts.
All measured nutrients are within acceptable ranges based on the latest soil test
(= fmtDate($soilTests[0]['date_sampled']) ?>).
Based on soil test
= htmlspecialchars($soilTests[0]['lab_no'] ?? '#' . $soilTests[0]['id'], ENT_QUOTES, 'UTF-8') ?>
sampled = fmtDate($soilTests[0]['date_sampled']) ?>.
Soil type: = htmlspecialchars($latestSoil['soil_type'], ENT_QUOTES, 'UTF-8') ?>.
0) {
$pct = round(($alert['measured'] / $alert['min']) * 100);
}
?>
= htmlspecialchars($alert['nutrient'], ENT_QUOTES, 'UTF-8') ?>
= strtoupper($alert['severity']) ?>
Measured
Target min
= $alert['measured'] ?>
= $alert['min'] ?? '—' ?>
= $pct ?>% 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;
?>
= htmlspecialchars($entryDate, ENT_QUOTES, 'UTF-8') ?>
<= $fa ?> class="fa-= $cfg['icon'] ?> text-= $cfg['colour'] ?> fa-sm">
= $cfg['label'] ?>
= htmlspecialchars($entry['sample_id'] ?: ($entry['site_id'] ?: ('Test #' . $entry['id'])), ENT_QUOTES, 'UTF-8') ?>
— = htmlspecialchars($entry['site_id'], ENT_QUOTES, 'UTF-8') ?>
= fmtDate($entry['date']) ?>
Lab: = htmlspecialchars($entry['lab_no'], ENT_QUOTES, 'UTF-8') ?>
· = htmlspecialchars($entry['crop_type'], ENT_QUOTES, 'UTF-8') ?>
= 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');
}
?>