Explorar o código

Stage 1 Consultants

Benjamin Harris hai 2 meses
pai
achega
b338726e1b
Modificáronse 6 ficheiros con 1382 adicións e 42 borrados
  1. 1 0
      .gitignore
  2. 846 0
      animal-dietary-balance.php
  3. 12 42
      controllers/soilTestSubmit.php
  4. 287 0
      dashboard/consultant/index.php
  5. 10 0
      layouts/sidebar.php
  6. 226 0
      lib/consultant.php

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/doc

+ 846 - 0
animal-dietary-balance.php

@@ -0,0 +1,846 @@
+<?php
+$pageTitle = 'Animal Dietary Mineral Balance Report';
+$siteName  = 'Crop Monitor';
+$siteUrl   = '/';
+?>
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title><?= htmlspecialchars($pageTitle . ' | ' . $siteName, ENT_QUOTES, 'UTF-8') ?></title>
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  
+  <meta name="description" content="Animal Dietary Mineral Balance Report — assess your pasture's mineral content against your animal's true daily requirements. Available for dairy cows, beef, sheep, goats, deer and horses.">
+  
+  <link rel="preconnect" href="https://fonts.googleapis.com">
+  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400&display=swap" rel="stylesheet">
+  <link href="client-assets/css/home.css" rel="stylesheet" type="text/css" media="screen">
+
+  <!-- Page-specific styles — extend home.css without overriding it -->
+  <style>
+    /* ── Hero right — mineral panel ── */
+    .hero-mineral-panel {
+      position: absolute;
+      top: 50%; left: 50%;
+      transform: translate(-50%, -50%);
+      width: 360px;
+    }
+    .hmp-title {
+      font-family: 'DM Mono', monospace;
+      font-size: 0.62rem;
+      letter-spacing: 0.14em;
+      text-transform: uppercase;
+      color: rgba(255,255,255,0.28);
+      margin-bottom: 1.5rem;
+      padding-left: 0.25rem;
+    }
+    .mineral-row {
+      display: grid;
+      grid-template-columns: 108px 1fr 64px;
+      align-items: center;
+      gap: 0.75rem;
+      margin-bottom: 1rem;
+    }
+    .mineral-name {
+      font-family: 'DM Mono', monospace;
+      font-size: 0.72rem;
+      color: rgba(255,255,255,0.65);
+    }
+    .mineral-bar-track {
+      height: 6px;
+      background: rgba(255,255,255,0.07);
+      border-radius: 3px;
+      overflow: hidden;
+    }
+    .mineral-bar-fill {
+      height: 100%;
+      border-radius: 3px;
+      animation: barGrow 1.4s ease-out both;
+    }
+    .mineral-status {
+      font-family: 'DM Mono', monospace;
+      font-size: 0.62rem;
+      text-align: right;
+      letter-spacing: 0.04em;
+      text-transform: uppercase;
+    }
+    .ms-ok   { color: #7dc87a; }
+    .ms-low  { color: var(--straw); }
+    .ms-high { color: #e07070; }
+
+    .hmp-divider {
+      margin: 1.5rem 0;
+      border: none;
+      border-top: 1px solid rgba(255,255,255,0.08);
+    }
+    .hmp-indices {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 0.6rem;
+    }
+    .hmp-index {
+      background: rgba(255,255,255,0.06);
+      border: 1px solid rgba(255,255,255,0.1);
+      border-radius: 6px;
+      padding: 0.65rem 0.85rem;
+    }
+    .hmp-index-label {
+      font-family: 'DM Mono', monospace;
+      font-size: 0.55rem;
+      letter-spacing: 0.1em;
+      text-transform: uppercase;
+      color: rgba(255,255,255,0.32);
+    }
+    .hmp-index-val {
+      font-family: 'Playfair Display', serif;
+      font-size: 1.15rem;
+      font-weight: 700;
+      color: var(--straw);
+      line-height: 1.2;
+      margin-top: 0.15rem;
+    }
+    .hmp-index-risk { font-size: 0.62rem; margin-top: 0.1rem; }
+
+    /* ── Stock grid ── */
+    .stock-grid {
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 1.25rem;
+      margin-top: 2.5rem;
+    }
+    .stock-card {
+      background: white;
+      border: 1px solid var(--border);
+      border-radius: 10px;
+      padding: 1.5rem 1.75rem;
+      transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
+    }
+    .stock-card:hover {
+      transform: translateY(-4px);
+      box-shadow: 0 8px 32px rgba(44,26,14,0.1);
+      border-color: var(--sage);
+    }
+    .stock-emoji { font-size: 2rem; margin-bottom: 0.75rem; }
+    .stock-card h4 {
+      font-family: 'Playfair Display', serif;
+      font-size: 1rem;
+      color: var(--soil);
+      margin-bottom: 0.4rem;
+    }
+    .stock-weight {
+      font-family: 'DM Mono', monospace;
+      font-size: 0.65rem;
+      letter-spacing: 0.08em;
+      color: var(--harvest);
+      margin-bottom: 0.5rem;
+    }
+    .stock-card p { font-size: 0.8rem; color: var(--text-light); line-height: 1.6; }
+
+    /* ── Index cards ── */
+    .index-card {
+      background: white;
+      border: 1px solid var(--border);
+      border-radius: 10px;
+      padding: 2rem 2.25rem;
+      transition: border-color 0.2s, box-shadow 0.2s;
+    }
+    .index-card:hover {
+      border-color: var(--sage);
+      box-shadow: 0 4px 24px rgba(61,107,66,0.08);
+    }
+    .index-card-head {
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-start;
+      margin-bottom: 1rem;
+      gap: 1rem;
+    }
+    .index-card h3 {
+      font-family: 'Playfair Display', serif;
+      font-size: 1.15rem;
+      color: var(--soil);
+    }
+    .index-formula {
+      font-family: 'DM Mono', monospace;
+      font-size: 0.6rem;
+      background: var(--mist);
+      color: var(--clay);
+      padding: 0.25rem 0.55rem;
+      border-radius: 4px;
+      white-space: nowrap;
+      flex-shrink: 0;
+    }
+    .index-card p { font-size: 0.85rem; color: var(--text-light); line-height: 1.7; margin-bottom: 1.25rem; }
+    .risk-zones { display: flex; gap: 0.5rem; flex-wrap: wrap; }
+    .risk-zone {
+      display: inline-flex;
+      align-items: center;
+      gap: 0.4rem;
+      font-family: 'DM Mono', monospace;
+      font-size: 0.65rem;
+      padding: 0.28rem 0.7rem;
+      border-radius: 99px;
+    }
+    .risk-zone .dot { width: 6px; height: 6px; border-radius: 50%; }
+    .rz-safe { background: rgba(61,107,66,0.08); color: #2e5e33; }
+    .rz-safe .dot { background: var(--leaf); }
+    .rz-warn { background: rgba(200,133,58,0.1); color: #7a4a1a; }
+    .rz-warn .dot { background: var(--harvest); }
+
+    /* ── Tip box ── */
+    .tip-box {
+      background: var(--mist);
+      border: 1px solid var(--border);
+      border-left: 3px solid var(--harvest);
+      border-radius: 0 8px 8px 0;
+      padding: 1.25rem 1.5rem;
+      font-size: 0.875rem;
+      color: var(--text-mid);
+      line-height: 1.7;
+    }
+    .tip-box strong { color: var(--soil); }
+
+    /* ── Highlight band ── */
+    .highlight-band {
+      background: var(--harvest);
+      padding: 1.1rem 4rem;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 2rem;
+    }
+    .highlight-band p { font-size: 0.92rem; color: var(--soil); font-weight: 400; }
+    .highlight-band a {
+      white-space: nowrap;
+      padding: 0.6rem 1.5rem;
+      background: var(--soil);
+      color: white;
+      border-radius: 4px;
+      font-size: 0.85rem;
+      font-weight: 500;
+      transition: background 0.2s;
+    }
+    .highlight-band a:hover { background: var(--earth); }
+
+    @media (max-width: 900px) {
+      .stock-grid { grid-template-columns: 1fr 1fr; }
+      .highlight-band { flex-direction: column; padding: 1.5rem; text-align: center; }
+      .index-grid { grid-template-columns: 1fr !important; }
+    }
+    @media (max-width: 600px) {
+      .stock-grid { grid-template-columns: 1fr; }
+    }
+  </style>
+</head>
+<body>
+
+<!-- ── NAVIGATION ──────────────────────────── -->
+<nav>
+  <a href="/" class="nav-logo">
+    <div class="nav-logo-mark">🌿</div>
+    <span class="nav-logo-text">Crop Monitor</span>
+  </a>
+  <ul class="nav-links">
+    <li><a href="#about">About ADMB</a></li>
+    <li><a href="#how-it-works">How It Works</a></li>
+    <li><a href="#indices">Key Indices</a></li>
+    <li><a href="#stock">Stock Classes</a></li>
+    <li><a href="#contact">Contact</a></li>
+  </ul>
+  <div class="nav-cta">
+    <a href="tel:0363524444" class="btn-outline">03 6352 4444</a>
+    <a href="#contact" class="btn-primary">Request a Test</a>
+  </div>
+</nav>
+
+<!-- ── HERO ────────────────────────────────── -->
+<section class="hero">
+  <div class="hero-left">
+    <div class="hero-eyebrow">Crop Monitor · Pasture Testing</div>
+    <h1>Know Exactly What<br>Your Animals Are<br><em>Getting</em></h1>
+    <p class="hero-desc">
+      The Animal Dietary Mineral Balance Report translates pasture analysis into daily mineral intake — matched against your animal's true requirements — so you can identify deficiencies and excesses before they become metabolic disorders.
+    </p>
+    <div class="hero-actions">
+      <a href="#contact" class="btn-hero">Request a Report</a>
+      <a href="#about" class="btn-hero-sec">How It Works</a>
+    </div>
+    <div class="hero-stats">
+      <div class="hero-stat">
+        <div class="hero-stat-num">6</div>
+        <div class="hero-stat-label">Animal classes covered</div>
+      </div>
+      <div class="hero-stat">
+        <div class="hero-stat-num">4</div>
+        <div class="hero-stat-label">Key risk indices</div>
+      </div>
+      <div class="hero-stat">
+        <div class="hero-stat-num">$0</div>
+        <div class="hero-stat-label">Added cost with MPast</div>
+      </div>
+    </div>
+  </div>
+
+  <!-- Hero right — sample mineral balance panel -->
+  <div class="hero-right">
+    <div class="hero-mineral-panel">
+      <div class="hmp-title">Sample ADMB · Dairy Cow 450 kg · Peak Lactation</div>
+
+      <div class="mineral-row">
+        <span class="mineral-name">Calcium (Ca)</span>
+        <div class="mineral-bar-track"><div class="mineral-bar-fill" style="width:72%;background:linear-gradient(90deg,#7dc87a,#a8e0aa);animation-delay:0.05s;"></div></div>
+        <span class="mineral-status ms-ok">OK</span>
+      </div>
+      <div class="mineral-row">
+        <span class="mineral-name">Magnesium (Mg)</span>
+        <div class="mineral-bar-track"><div class="mineral-bar-fill" style="width:45%;background:linear-gradient(90deg,#e8c87a,#f0d898);animation-delay:0.1s;"></div></div>
+        <span class="mineral-status ms-low">Low</span>
+      </div>
+      <div class="mineral-row">
+        <span class="mineral-name">Potassium (K)</span>
+        <div class="mineral-bar-track"><div class="mineral-bar-fill" style="width:96%;background:linear-gradient(90deg,#e07070,#e89090);animation-delay:0.15s;"></div></div>
+        <span class="mineral-status ms-high">High</span>
+      </div>
+      <div class="mineral-row">
+        <span class="mineral-name">Sodium (Na)</span>
+        <div class="mineral-bar-track"><div class="mineral-bar-fill" style="width:30%;background:linear-gradient(90deg,#e8c87a,#f0d898);animation-delay:0.2s;"></div></div>
+        <span class="mineral-status ms-low">Low</span>
+      </div>
+      <div class="mineral-row">
+        <span class="mineral-name">Copper (Cu)</span>
+        <div class="mineral-bar-track"><div class="mineral-bar-fill" style="width:60%;background:linear-gradient(90deg,#7dc87a,#a8e0aa);animation-delay:0.25s;"></div></div>
+        <span class="mineral-status ms-ok">OK</span>
+      </div>
+      <div class="mineral-row">
+        <span class="mineral-name">Selenium (Se)</span>
+        <div class="mineral-bar-track"><div class="mineral-bar-fill" style="width:38%;background:linear-gradient(90deg,#e8c87a,#f0d898);animation-delay:0.3s;"></div></div>
+        <span class="mineral-status ms-low">Low</span>
+      </div>
+      <div class="mineral-row">
+        <span class="mineral-name">Cobalt (Co)</span>
+        <div class="mineral-bar-track"><div class="mineral-bar-fill" style="width:78%;background:linear-gradient(90deg,#7dc87a,#a8e0aa);animation-delay:0.35s;"></div></div>
+        <span class="mineral-status ms-ok">OK</span>
+      </div>
+
+      <hr class="hmp-divider">
+
+      <div class="hmp-indices">
+        <div class="hmp-index">
+          <div class="hmp-index-label">Grass Staggers</div>
+          <div class="hmp-index-val">2.6</div>
+          <div class="hmp-index-risk" style="color:#e07070;">⚠ Increased risk</div>
+        </div>
+        <div class="hmp-index">
+          <div class="hmp-index-label">Bloat (K/Na)</div>
+          <div class="hmp-index-val">18.4</div>
+          <div class="hmp-index-risk" style="color:var(--straw);">⚠ Monitor</div>
+        </div>
+        <div class="hmp-index">
+          <div class="hmp-index-label">DCAD Milk Fever</div>
+          <div class="hmp-index-val">312</div>
+          <div class="hmp-index-risk" style="color:#e07070;">⚠ Increased risk</div>
+        </div>
+        <div class="hmp-index">
+          <div class="hmp-index-label">Ca/P Ratio</div>
+          <div class="hmp-index-val">1.8</div>
+          <div class="hmp-index-risk" style="color:#7dc87a;">✓ Recommended</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</section>
+
+<!-- ── MARQUEE ──────────────────────────────── -->
+<div class="marquee-band">
+  <div class="marquee-track">
+    <span class="marquee-item">Daily Mineral Intake</span>
+    <span class="marquee-item">Grass Staggers Index</span>
+    <span class="marquee-item">Milk Fever (DCAD)</span>
+    <span class="marquee-item">Bloat Risk Assessment</span>
+    <span class="marquee-item">Dairy · Beef · Sheep</span>
+    <span class="marquee-item">Selenium &amp; Copper</span>
+    <span class="marquee-item">Metabolic Disorder Prevention</span>
+    <span class="marquee-item">Deficit / Surplus Reporting</span>
+    <span class="marquee-item">Daily Mineral Intake</span>
+    <span class="marquee-item">Grass Staggers Index</span>
+    <span class="marquee-item">Milk Fever (DCAD)</span>
+    <span class="marquee-item">Bloat Risk Assessment</span>
+    <span class="marquee-item">Dairy · Beef · Sheep</span>
+    <span class="marquee-item">Selenium &amp; Copper</span>
+    <span class="marquee-item">Metabolic Disorder Prevention</span>
+    <span class="marquee-item">Deficit / Surplus Reporting</span>
+  </div>
+</div>
+
+<!-- ── ABOUT ADMB ────────────────────────────── -->
+<section class="albrecht" id="about">
+  <div class="albrecht-grid">
+
+    <div class="albrecht-left fade-up">
+      <div class="section-eyebrow">About the Report</div>
+      <h2 class="section-title">Animal Requirements,<br>Not Just Plant Levels</h2>
+      <div class="albrecht-body">
+        <p>
+          Standard pasture analysis compares mineral levels against <strong>plant and animal requirements combined</strong>. The ADMB report is different — it focuses exclusively on what your animal needs each day and how well the current feed delivers it.
+        </p>
+        <p>
+          Results are expressed as <strong>Daily Intake in grams and milligrams</strong>, not as a feed concentration. This makes it easy to calculate supplementation quantities and to account for changing dry matter intake across the season.
+        </p>
+        <p>
+          For dairy cows, the report factors in <strong>lactation stage and calving date</strong>. Requirements are scaled to liveweight and adjusted automatically when you supply your herd's details — or sensible defaults are applied if not.
+        </p>
+      </div>
+      <div class="albrecht-quote">
+        <p>"If the report shows a deficit of 8 mg of copper per animal per day, it is simple arithmetic to convert this into quantities to incorporate into feeds."</p>
+        <cite>— Crop Monitor Technical Note</cite>
+      </div>
+
+      <div class="principle-cards">
+        <div class="principle-card fade-up">
+          <div class="principle-icon">⚖️</div>
+          <div>
+            <h4>Meaningful Units</h4>
+            <p>Daily grams and milligrams let you calculate supplementation quantities directly — no conversion from feed concentrations required.</p>
+          </div>
+        </div>
+        <div class="principle-card fade-up">
+          <div class="principle-icon">📊</div>
+          <div>
+            <h4>Visual Bar Graph</h4>
+            <p>A quick-read bar chart shows the severity of each deficiency or surplus at a glance, alongside the numeric difference.</p>
+          </div>
+        </div>
+        <div class="principle-card fade-up">
+          <div class="principle-icon">🔗</div>
+          <div>
+            <h4>Nutrient Interactions</h4>
+            <p>Four validated ratios flag where mineral interactions are likely to raise the risk of grass staggers, bloat, or milk fever.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Right — DM intake chart + free-with-MPast callout -->
+    <div class="albrecht-right fade-up">
+      <div class="cation-chart">
+        <div class="cation-chart-title">Default Daily DM Intake — Dairy Cow (450 kg)</div>
+
+        <div class="cation-bar-row">
+          <div class="cation-label">Early Lact.</div>
+          <div class="cation-bar-track">
+            <div class="cation-bar-fill" style="width:75%;background:linear-gradient(90deg,#4a7a42,#7dc87a);animation-delay:0.05s;"></div>
+          </div>
+          <div class="cation-val">13.5 kg</div>
+        </div>
+        <div class="cation-bar-row">
+          <div class="cation-label">Peak Lact.</div>
+          <div class="cation-bar-track">
+            <div class="cation-bar-fill" style="width:100%;background:linear-gradient(90deg,#c8853a,#e8b870);animation-delay:0.15s;"></div>
+          </div>
+          <div class="cation-val">18 kg</div>
+        </div>
+        <div class="cation-bar-row">
+          <div class="cation-label">Late Lact.</div>
+          <div class="cation-bar-track">
+            <div class="cation-bar-fill" style="width:75%;background:linear-gradient(90deg,#4a7a42,#7dc87a);animation-delay:0.25s;"></div>
+          </div>
+          <div class="cation-val">13.5 kg</div>
+        </div>
+        <div class="cation-bar-row">
+          <div class="cation-label">Dry</div>
+          <div class="cation-bar-track">
+            <div class="cation-bar-fill" style="width:50%;background:linear-gradient(90deg,#6b8c5a,#9ab88a);animation-delay:0.35s;"></div>
+          </div>
+          <div class="cation-val">9 kg</div>
+        </div>
+
+        <div class="cation-note">
+          Values used when daily DM intake is not supplied. Requirements are scaled to liveweight; if calving date differs from July the lactation schedule adjusts accordingly.
+        </div>
+      </div>
+
+      <div style="margin-top:1.5rem;">
+        <div class="principle-card" style="border-left:3px solid var(--harvest);border-radius:0 8px 8px 0;">
+          <div class="principle-icon">✨</div>
+          <div>
+            <h4>Free with the Mixed Pasture Profile</h4>
+            <p>Add an ADMB report to any Mixed Pasture Profile [MPast] at no extra charge. Enter <strong>ADMB</strong> in the "Other Tests" column and supply animal details in the instructions section of your request form.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+
+  </div>
+</section>
+
+<!-- ── HOW IT WORKS ──────────────────────────── -->
+<section class="process" id="how-it-works">
+  <div class="process-inner">
+    <div class="process-header fade-up">
+      <div class="section-eyebrow">How It Works</div>
+      <h2 class="section-title">Three Calculations,<br>One Clear Picture</h2>
+      <p class="section-sub">The ADMB report is built on three straightforward steps that turn a pasture sample into actionable mineral management guidance.</p>
+    </div>
+
+    <div class="steps">
+      <div class="step fade-up">
+        <div class="step-num">1</div>
+        <h4>Sample Your Pasture</h4>
+        <p>Collect at least 500 g at grazing height from paddocks ready to graze. Avoid dunging patches, troughs and gateways. Send to Crop Monitor promptly — courier preferred.</p>
+      </div>
+      <div class="step fade-up">
+        <div class="step-num">2</div>
+        <h4>Provide Animal Details</h4>
+        <p>Record species, average liveweight and — for dairy — calving month and daily DM intake. Write <em>ADMB</em> in "Other Tests". Up to two species can be assessed from one sample.</p>
+      </div>
+      <div class="step fade-up">
+        <div class="step-num">3</div>
+        <h4>Daily Intake Calculated</h4>
+        <p>Mineral concentrations in the feed are multiplied by daily DM intake to give the actual quantities consumed in grams and milligrams per animal per day.</p>
+      </div>
+      <div class="step fade-up">
+        <div class="step-num">4</div>
+        <h4>Deficit or Surplus Shown</h4>
+        <p>The gap between daily intake and daily requirement is presented numerically and as a colour-coded bar graph — so action priorities are immediately clear.</p>
+      </div>
+    </div>
+
+    <div class="tip-box fade-up" style="max-width:820px;margin:3rem auto 0;">
+      <strong>Sampling tip:</strong> Use clean hands and pasture shears to minimise contamination. Cobalt readings are particularly sensitive to soil on the sward. Avoid powdered disposable gloves — the powder can introduce zinc contamination. Sample kits including sealable bags and request forms are available from Crop Monitor on request.
+    </div>
+  </div>
+</section>
+
+<!-- ── HIGHLIGHT BAND ────────────────────────── -->
+<div class="highlight-band">
+  <p>🐄 &nbsp;Two animal classes — for example dairy cows <em>and</em> beef cattle — can be assessed from a single pasture sample at no extra cost.</p>
+  <a href="#contact">Request a Report →</a>
+</div>
+
+<!-- ── KEY INDICES ───────────────────────────── -->
+<section class="services" id="indices">
+  <div class="services-inner">
+    <div class="services-header fade-up">
+      <div class="section-eyebrow">Risk Indices</div>
+      <h2 class="section-title">Four Indices That<br>Protect Your Herd</h2>
+      <p class="section-sub">Beyond per-mineral analysis, the ADMB report calculates four validated ratios that highlight interaction effects most likely to cause metabolic disorders.</p>
+    </div>
+
+    <div class="index-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;">
+
+      <div class="index-card fade-up">
+        <div class="index-card-head">
+          <h3>Grass Staggers (Tetany) Index</h3>
+          <span class="index-formula">K / (Ca+Mg) meq</span>
+        </div>
+        <p>High spring potassium reduces the availability of calcium and magnesium, raising hypomagnesaemia risk. The index flags when supplementation is warranted before clinical signs appear.</p>
+        <div class="risk-zones">
+          <span class="risk-zone rz-safe"><span class="dot"></span>&lt; 1.8 Recommended</span>
+          <span class="risk-zone rz-warn"><span class="dot"></span>&gt; 2.2 Increased risk</span>
+        </div>
+      </div>
+
+      <div class="index-card fade-up">
+        <div class="index-card-head">
+          <h3>K/Na Ratio — Bloat Index</h3>
+          <span class="index-formula">K : Na</span>
+        </div>
+        <p>High potassium is linked to low pasture sodium, poor dietary sodium uptake and increased bloat incidence in grazing animals. Adequate sodium is especially important during dairy lactation.</p>
+        <div class="risk-zones">
+          <span class="risk-zone rz-safe"><span class="dot"></span>&lt; 10 Recommended</span>
+          <span class="risk-zone rz-warn"><span class="dot"></span>&gt; 20 Increased risk</span>
+        </div>
+      </div>
+
+      <div class="index-card fade-up">
+        <div class="index-card-head">
+          <h3>DCAD — Milk Fever Index</h3>
+          <span class="index-formula">(K+Na) − (S+Cl) meq/kg</span>
+        </div>
+        <p>Dietary Cation Anion Difference estimates whether a diet is acidogenic or alkalogenic. Milk fever risk rises progressively above 200. Increasing sulphur + chloride or reducing potassium + sodium through supplement selection is the standard management response.</p>
+        <div class="risk-zones">
+          <span class="risk-zone rz-safe"><span class="dot"></span>&lt; 200 Recommended</span>
+          <span class="risk-zone rz-warn"><span class="dot"></span>&gt; 200 Increased risk</span>
+        </div>
+      </div>
+
+      <div class="index-card fade-up">
+        <div class="index-card-head">
+          <h3>Ca/P Ratio</h3>
+          <span class="index-formula">Ca : P</span>
+        </div>
+        <p>Calcium and phosphorus interact: when one is marginal, a high level of the other can worsen the outcome. NZ pastures tend to be low in calcium, so high phosphorus giving a low Ca/P ratio is a recognised milk fever risk factor.</p>
+        <div class="risk-zones">
+          <span class="risk-zone rz-safe"><span class="dot"></span>&gt; 1.5 Recommended</span>
+          <span class="risk-zone rz-warn"><span class="dot"></span>&lt; 1.2 Increased risk</span>
+        </div>
+      </div>
+
+    </div>
+
+    <div class="tip-box fade-up" style="margin-top:2rem;">
+      <strong>Note on ratios:</strong> Crop Monitor only reports indices that have established validity as risk indicators. The bar graph shows each mineral individually — the four ratios are calculated separately to highlight interaction effects that would not be visible from individual mineral levels alone.
+    </div>
+  </div>
+</section>
+
+<!-- ── STOCK CLASSES ─────────────────────────── -->
+<section class="science" id="stock">
+  <div class="science-inner">
+    <div class="fade-up">
+      <div class="section-eyebrow">Stock Classes</div>
+      <h2 class="section-title">Reports for Every<br>Animal on Your Farm</h2>
+      <p class="section-sub">ADMB reports are calibrated to six animal classifications, each with its own default liveweight and requirement set. Supply actual values for the most precise results.</p>
+    </div>
+
+    <div class="stock-grid">
+
+      <div class="stock-card fade-up">
+        <div class="stock-emoji">🐄</div>
+        <h4>Dairy Cows</h4>
+        <div class="stock-weight">Default: 450 kg (milking season wt)</div>
+        <p>Requirements vary by lactation stage. Provide calving month, daily DM intake and average liveweight for a fully tailored report. The most seasonally complex animal class.</p>
+      </div>
+
+      <div class="stock-card fade-up">
+        <div class="stock-emoji">🐂</div>
+        <h4>Beef Cattle</h4>
+        <div class="stock-weight">Default: 400 kg</div>
+        <p>Assumes animals are well fed on the sampled diet in an active growth stage. Provide average liveweight to scale requirements proportionately.</p>
+      </div>
+
+      <div class="stock-card fade-up">
+        <div class="stock-emoji">🐑</div>
+        <h4>Sheep &amp; Lambs</h4>
+        <div class="stock-weight">Default: 60 kg / 30 kg</div>
+        <p>Separate defaults for ewes and lambs. Provide average liveweight per mob for site-specific recommendations across your flock.</p>
+      </div>
+
+      <div class="stock-card fade-up">
+        <div class="stock-emoji">🐐</div>
+        <h4>Dairy Goats</h4>
+        <div class="stock-weight">Default: 60 kg</div>
+        <p>Mineral requirements assessed against dairy goat standards. Particularly relevant for selenium, copper and iodine status on intensive operations.</p>
+      </div>
+
+      <div class="stock-card fade-up">
+        <div class="stock-emoji">🦌</div>
+        <h4>Deer</h4>
+        <div class="stock-weight">Default: 90 kg</div>
+        <p>Calibrated to deer physiology. Useful for velvet and venison operations where trace mineral balance is a key production and health factor.</p>
+      </div>
+
+      <div class="stock-card fade-up">
+        <div class="stock-emoji">🐴</div>
+        <h4>Horses</h4>
+        <div class="stock-weight">Default: 500 kg</div>
+        <p>Pasture mineral assessment with requirements calibrated for horses, where calcium, phosphorus and selenium balance are priorities for bone and reproductive health.</p>
+      </div>
+
+    </div>
+  </div>
+</section>
+
+<!-- ── SELENIUM & COPPER NOTE ────────────────── -->
+<section class="about" id="elements">
+  <div class="about-inner">
+    <div class="fade-up">
+      <div class="section-eyebrow">Elements of Interest</div>
+      <h2 class="section-title">Selenium &amp; Copper —<br>Handle With Care</h2>
+      <div class="about-text">
+        <p>Two elements warrant particular attention when interpreting the ADMB report.</p>
+        <p>
+          <strong style="color:var(--straw);">Selenium</strong> — Crop Monitor' interpretive scale targets 0.05–0.15 mg/kg in feed for high-producing dairy cows: a margin above the deficiency threshold allowing for seasonal and analytical variation. The high vitamin-E levels typical in NZ pastures complement selenium metabolism in grazed dairy cows — an advantage that does not apply to housed animals in overseas conditions.
+        </p>
+        <p>
+          <strong style="color:var(--straw);">Copper</strong> — High levels of molybdenum, sulphur, iron and zinc all reduce dietary copper availability. There is no reliable formula to quantify these interaction effects. The report flags individual copper status; any suspected secondary deficiency warrants further investigation with your veterinarian or consultant.
+        </p>
+        <p>
+          Some feeds — grain-based concentrates, maize silage, Sudax grass — are inherently low in minerals. The ADMB report will highlight this without implying inferiority. These feeds are valuable energy supplements; the report simply signals when additional mineral management is needed in the overall diet.
+        </p>
+      </div>
+    </div>
+
+    <div class="fade-up">
+      <div class="about-metrics">
+        <div class="about-metric">
+          <div class="about-metric-num">0.05–0.15</div>
+          <div class="about-metric-label">mg/kg Se target range (dairy)</div>
+        </div>
+        <div class="about-metric">
+          <div class="about-metric-num">Mo, S, Fe, Zn</div>
+          <div class="about-metric-label">Elements that reduce Cu availability</div>
+        </div>
+        <div class="about-metric">
+          <div class="about-metric-num">2</div>
+          <div class="about-metric-label">ADMB animal classes per sample</div>
+        </div>
+        <div class="about-metric">
+          <div class="about-metric-num">g &amp; mg</div>
+          <div class="about-metric-label">Results as daily intake units</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</section>
+
+<!-- ── CONTACT ───────────────────────────────── -->
+<section class="contact-section" id="contact">
+  <div class="contact-inner">
+    <div class="fade-up">
+      <div class="section-eyebrow">Contact Us</div>
+      <h2 class="section-title">Request an ADMB<br>Report Today</h2>
+      <p class="section-sub" style="margin-bottom:2rem;">
+        Get in touch with our Agriculture Client Service team to order a sample kit, ask a technical question, or discuss which pasture profile best suits your operation.
+      </p>
+
+      <form class="contact-form" action="/controllers/contactSubmit.php" method="post">
+        <input type="hidden" name="report_type" value="ADMB">
+        <div class="form-row">
+          <div class="form-field">
+            <label>First Name</label>
+            <input type="text" name="first_name" placeholder="Jane" required>
+          </div>
+          <div class="form-field">
+            <label>Last Name</label>
+            <input type="text" name="last_name" placeholder="Wilson" required>
+          </div>
+        </div>
+        <div class="form-field">
+          <label>Email Address</label>
+          <input type="email" name="email" placeholder="jane@farm.co.nz" required>
+        </div>
+        <div class="form-field">
+          <label>Primary Stock Class</label>
+          <select name="stock_class">
+            <option value="">Select your primary stock class</option>
+            <option>Dairy Cows</option>
+            <option>Beef Cattle</option>
+            <option>Sheep &amp; Lambs</option>
+            <option>Dairy Goats</option>
+            <option>Deer</option>
+            <option>Horses</option>
+            <option>Mixed</option>
+          </select>
+        </div>
+        <div class="form-field">
+          <label>Message</label>
+          <textarea name="message" placeholder="Tell us about your operation and what you'd like to assess…"></textarea>
+        </div>
+        <button type="submit" class="form-submit">Send Enquiry</button>
+      </form>
+    </div>
+
+    <div class="contact-info fade-up">
+      <div class="contact-info-item">
+        <div class="contact-info-icon">📞</div>
+        <div>
+          <div class="contact-info-label">Freephone</div>
+          <div class="contact-info-val">03 6352 4444</div>
+        </div>
+      </div>
+      <div class="contact-info-item">
+        <div class="contact-info-icon">✉️</div>
+        <div>
+          <div class="contact-info-label">Email</div>
+          <div class="contact-info-val">mail@cropmonitor.info</div>
+        </div>
+      </div>
+      <div class="contact-info-item">
+        <div class="contact-info-icon">🌐</div>
+        <div>
+          <div class="contact-info-label">Website</div>
+          <div class="contact-info-val">cropmonitor.info</div>
+        </div>
+      </div>
+      <div class="contact-info-item">
+        <div class="contact-info-icon">📋</div>
+        <div>
+          <div class="contact-info-label">How to Order</div>
+          <div class="contact-info-val">Enter <strong>ADMB</strong> in "Other Tests" on your plant sample request form. Add animal details in the instructions section. Sample kits available on request.</div>
+        </div>
+      </div>
+      <div class="contact-info-item">
+        <div class="contact-info-icon">💡</div>
+        <div>
+          <div class="contact-info-label">Related Profiles</div>
+          <div class="contact-info-val">Mixed Pasture [MPast] · Extended Pasture Feed [ExtFed] · Pasture Feed Quality [Feed]</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</section>
+
+<!-- ── FOOTER ───────────────────────────────── -->
+<footer>
+  <div class="footer-inner">
+    <div class="footer-top">
+      <div>
+        <a href="/" class="footer-logo">
+          <div class="footer-logo-mark">🌿</div>
+          <span class="footer-logo-text">Crop Monitor</span>
+        </a>
+        <p class="footer-tagline">Pasture and feed quality testing for New Zealand's pastoral industry.</p>
+      </div>
+      <div class="footer-nav-group">
+        <h5>ADMB Report</h5>
+        <ul>
+          <li><a href="#about">About the Report</a></li>
+          <li><a href="#how-it-works">How It Works</a></li>
+          <li><a href="#indices">Key Indices</a></li>
+          <li><a href="#stock">Stock Classes</a></li>
+        </ul>
+      </div>
+      <div class="footer-nav-group">
+        <h5>Pasture Profiles</h5>
+        <ul>
+          <li><a href="#">Mixed Pasture [MPast]</a></li>
+          <li><a href="#">Feed Quality [Feed]</a></li>
+          <li><a href="#">Extended Feed [ExtFed]</a></li>
+          <li><a href="#">Feed + Majors [FeedMaj]</a></li>
+        </ul>
+      </div>
+      <div class="footer-nav-group">
+        <h5>Contact</h5>
+        <ul>
+          <li><a href="tel:0363524444">0363 524449</a></li>
+          <li><a href="mailto:mail@cropmonitor.info">mail@cropmonitor.info</a></li>
+          <li><a href="https://cropmonitor.info">cropmonitor.info</a></li>
+        </ul>
+      </div>
+    </div>
+    <div class="footer-bottom">
+      <p class="footer-copy">© <?= date('Y') ?> Crop Monitor. All Rights Reserved.</p>
+      <p class="footer-badge">Animal Dietary Mineral Balance</p>
+    </div>
+  </div>
+</footer>
+
+<script>
+  // Fade-up on scroll
+  const observer = new IntersectionObserver((entries) => {
+    entries.forEach(e => {
+      if (e.isIntersecting) { e.target.classList.add('visible'); }
+    });
+  }, { threshold: 0.1 });
+  document.querySelectorAll('.fade-up').forEach(el => observer.observe(el));
+
+  // Nav scroll shadow
+  window.addEventListener('scroll', () => {
+    document.querySelector('nav').style.boxShadow =
+      window.scrollY > 10 ? '0 2px 20px rgba(44,26,14,0.08)' : 'none';
+  });
+
+  // Smooth scroll for anchor links
+  document.querySelectorAll('a[href^="#"]').forEach(a => {
+    a.addEventListener('click', e => {
+      const target = document.querySelector(a.getAttribute('href'));
+      if (target) {
+        e.preventDefault();
+        target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+      }
+    });
+  });
+</script>
+</body>
+</html>

+ 12 - 42
controllers/soilTestSubmit.php

@@ -1,4 +1,7 @@
 <?php
+error_reporting(E_ALL);
+//error_reporting(E_ALL ^ E_NOTICE);
+ini_set('display_errors', 1);
 /**
  * controllers/soilTestSubmit.php
  *
@@ -77,9 +80,14 @@ function validateSoilTestData(array $post): array
     $validated = [];
 
     // Client information
-    $validated['client_id'] = filter_var($post['client_id'] ?? '', FILTER_VALIDATE_INT);
-    if ($validated['client_id'] === false) {
-        throw new ValidationException('Invalid client ID');
+    $rawClientId = $post['client_id'] ?? '';
+    if ($rawClientId === '' || $rawClientId === 'new') {
+        $validated['client_id'] = null;
+    } else {
+        $validated['client_id'] = filter_var($rawClientId, FILTER_VALIDATE_INT);
+        if ($validated['client_id'] === false) {
+            throw new ValidationException('Invalid client ID');
+        }
     }
 
     $validated['name'] = sanitizeString($post['name'] ?? '', 100);
@@ -373,42 +381,4 @@ function insertSoilRecord(array $data, array $calculations, int $rand): int
     return $pdo->lastInsertId();
 }
 
-/**
- * Custom exception for validation errors
- */
-class ValidationException extends Exception {}
-
-/**
- * Sanitize string input
- */
-function sanitizeString(?string $value, int $maxLength = 255): string
-{
-    if ($value === null) return '';
-    $sanitized = trim($value);
-    $sanitized = filter_var($sanitized, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
-    return substr($sanitized, 0, $maxLength);
-}
-
-/**
- * Validate numeric input
- */
-function validateNumeric(?string $value, float $min = null, float $max = null): ?float
-{
-    if ($value === '' || $value === null) return null;
-
-    $numeric = filter_var($value, FILTER_VALIDATE_FLOAT);
-    if ($numeric === false) {
-        throw new ValidationException('Invalid numeric value: ' . $value);
-    }
-
-    if ($min !== null && $numeric < $min) {
-        throw new ValidationException('Value below minimum: ' . $numeric);
-    }
-
-    if ($max !== null && $numeric > $max) {
-        throw new ValidationException('Value above maximum: ' . $numeric);
-    }
-
-    return $numeric;
-}
-?>
+// ValidationException, sanitizeString(), validateNumeric() are provided by lib/validation.php

+ 287 - 0
dashboard/consultant/index.php

@@ -0,0 +1,287 @@
+<?php
+/**
+ * dashboard/consultant/index.php
+ *
+ * Consultant overview — shows all clients managed by the logged-in consultant
+ * with test counts, last activity, and out-of-range nutrient alert badges.
+ */
+
+require_once __DIR__ . '/../../config/database.php';
+require_once __DIR__ . '/../../lib/auth.php';
+require_once __DIR__ . '/../../lib/consultant.php';
+
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+requireLogin();
+
+$pageTitle  = 'Consultant Dashboard';
+$siteName   = 'Crop Monitor';
+$activeItem = 'Consultant Dashboard';
+
+$pdo     = getDBConnection();
+$userId  = (int) getCurrentUserId();
+$clients = getConsultantClients($pdo, $userId);
+
+// ── Summary totals for the header stat cards ──────────────────────────────────
+$totalClients  = count($clients);
+$totalSoil     = array_sum(array_column($clients, 'soil_count'));
+$totalPlant    = array_sum(array_column($clients, 'plant_count'));
+$totalAlerts   = array_sum(array_map(fn($c) => $c['alerts']['critical'] + $c['alerts']['watch'], $clients));
+$criticalCount = array_sum(array_map(fn($c) => $c['alerts']['critical'], $clients));
+
+include __DIR__ . '/../../layouts/header.php';
+include __DIR__ . '/../../layouts/navbar.php';
+?>
+
+<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">
+
+                <h1 class="mt-4"><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?></h1>
+                <ol class="breadcrumb mb-4">
+                    <li class="breadcrumb-item"><a href="/dashboard/dashboard.php">Dashboard</a></li>
+                    <li class="breadcrumb-item active">Consultant Overview</li>
+                </ol>
+
+                <!-- ── Summary stat cards ─────────────────────────────────── -->
+                <div class="row g-3 mb-4">
+
+                    <div class="col-xl-3 col-sm-6">
+                        <div class="card border-0 shadow-sm h-100">
+                            <div class="card-body d-flex align-items-center gap-3">
+                                <div class="rounded-circle bg-primary bg-opacity-10 p-3 flex-shrink-0">
+                                    <i class="fas fa-users fa-lg text-primary"></i>
+                                </div>
+                                <div>
+                                    <div class="fs-2 fw-bold lh-1"><?= $totalClients ?></div>
+                                    <div class="text-muted small">Clients</div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="col-xl-3 col-sm-6">
+                        <div class="card border-0 shadow-sm h-100">
+                            <div class="card-body d-flex align-items-center gap-3">
+                                <div class="rounded-circle bg-success bg-opacity-10 p-3 flex-shrink-0">
+                                    <i class="fas fa-globe-asia fa-lg text-success"></i>
+                                </div>
+                                <div>
+                                    <div class="fs-2 fw-bold lh-1"><?= $totalSoil ?></div>
+                                    <div class="text-muted small">Soil Tests</div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="col-xl-3 col-sm-6">
+                        <div class="card border-0 shadow-sm h-100">
+                            <div class="card-body d-flex align-items-center gap-3">
+                                <div class="rounded-circle bg-info bg-opacity-10 p-3 flex-shrink-0">
+                                    <i class="fab fa-pagelines fa-lg text-info"></i>
+                                </div>
+                                <div>
+                                    <div class="fs-2 fw-bold lh-1"><?= $totalPlant ?></div>
+                                    <div class="text-muted small">Plant Tests</div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="col-xl-3 col-sm-6">
+                        <div class="card border-0 shadow-sm h-100">
+                            <div class="card-body d-flex align-items-center gap-3">
+                                <div class="rounded-circle bg-<?= $criticalCount > 0 ? 'danger' : ($totalAlerts > 0 ? 'warning' : 'success') ?> bg-opacity-10 p-3 flex-shrink-0">
+                                    <i class="fas fa-exclamation-triangle fa-lg text-<?= $criticalCount > 0 ? 'danger' : ($totalAlerts > 0 ? 'warning' : 'success') ?>"></i>
+                                </div>
+                                <div>
+                                    <div class="fs-2 fw-bold lh-1"><?= $totalAlerts ?></div>
+                                    <div class="text-muted small">Nutrient Alerts</div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                </div>
+
+                <!-- ── Toolbar: search + filter ───────────────────────────── -->
+                <div class="row mb-3 g-2 align-items-center">
+                    <div class="col-md-5">
+                        <div class="input-group input-group-sm">
+                            <span class="input-group-text"><i class="fas fa-search"></i></span>
+                            <input type="text"
+                                   id="client-search"
+                                   class="form-control"
+                                   placeholder="Search by client or company…">
+                        </div>
+                    </div>
+                    <div class="col-md-3">
+                        <select id="alert-filter" class="form-select form-select-sm">
+                            <option value="">All clients</option>
+                            <option value="critical">Critical alerts only</option>
+                            <option value="watch">Watch alerts</option>
+                            <option value="ok">No alerts</option>
+                        </select>
+                    </div>
+                    <div class="col-md-4 text-md-end">
+                        <span class="text-muted small" id="client-count-label">
+                            Showing <?= $totalClients ?> client<?= $totalClients !== 1 ? 's' : '' ?>
+                        </span>
+                    </div>
+                </div>
+
+                <!-- ── Client cards grid ──────────────────────────────────── -->
+                <?php if (empty($clients)): ?>
+                    <div class="alert alert-info">
+                        No clients found for your account. Add a client using the
+                        <a href="/dashboard/crop-analysis/soil-test-data/index.php">Soil Test Data</a> page.
+                    </div>
+                <?php else: ?>
+
+                <div class="row g-3" id="client-grid">
+                    <?php foreach ($clients as $c):
+                        $alerts      = $c['alerts'];
+                        $badgeClass  = alertBadgeClass($alerts['critical'], $alerts['watch']);
+                        $hasActivity = $c['last_activity'] && $c['last_activity'] !== '1970-01-01';
+                        $alertStatus = $alerts['critical'] > 0 ? 'critical' : ($alerts['watch'] > 0 ? 'watch' : 'ok');
+                    ?>
+                    <div class="col-xl-4 col-lg-6 client-card-col"
+                         data-name="<?= htmlspecialchars(strtolower($c['client'] . ' ' . $c['company']), ENT_QUOTES, 'UTF-8') ?>"
+                         data-alert="<?= $alertStatus ?>">
+
+                        <div class="card h-100 shadow-sm border-0 border-start border-4 border-<?= $badgeClass ?>">
+                            <div class="card-body">
+
+                                <!-- Client name + alert badge -->
+                                <div class="d-flex justify-content-between align-items-start mb-2">
+                                    <div>
+                                        <h6 class="card-title mb-0 fw-bold">
+                                            <?= htmlspecialchars($c['client'] ?: '—', ENT_QUOTES, 'UTF-8') ?>
+                                        </h6>
+                                        <div class="text-muted small">
+                                            <?= htmlspecialchars($c['company'] ?: '', ENT_QUOTES, 'UTF-8') ?>
+                                        </div>
+                                    </div>
+                                    <?php if ($alerts['critical'] > 0 || $alerts['watch'] > 0): ?>
+                                    <span class="badge bg-<?= $badgeClass ?> ms-2 flex-shrink-0">
+                                        <?php if ($alerts['critical'] > 0): ?>
+                                            <i class="fas fa-exclamation-circle me-1"></i><?= $alerts['critical'] ?> critical
+                                        <?php else: ?>
+                                            <i class="fas fa-exclamation-triangle me-1"></i><?= $alerts['watch'] ?> watch
+                                        <?php endif; ?>
+                                    </span>
+                                    <?php else: ?>
+                                    <span class="badge bg-success ms-2 flex-shrink-0">
+                                        <i class="fas fa-check me-1"></i>All clear
+                                    </span>
+                                    <?php endif; ?>
+                                </div>
+
+                                <!-- Location -->
+                                <?php if ($c['address'] || $c['state_postcode']): ?>
+                                <div class="text-muted small mb-2">
+                                    <i class="fas fa-map-marker-alt me-1"></i>
+                                    <?= htmlspecialchars(trim($c['address'] . ' ' . $c['state_postcode']), ENT_QUOTES, 'UTF-8') ?>
+                                </div>
+                                <?php endif; ?>
+
+                                <!-- Test count badges -->
+                                <div class="d-flex flex-wrap gap-2 mb-3">
+                                    <span class="badge rounded-pill bg-success bg-opacity-15 text-success border border-success border-opacity-25">
+                                        <i class="fas fa-globe-asia me-1"></i>
+                                        <?= (int) $c['soil_count'] ?> Soil
+                                    </span>
+                                    <span class="badge rounded-pill bg-info bg-opacity-15 text-info border border-info border-opacity-25">
+                                        <i class="fab fa-pagelines me-1"></i>
+                                        <?= (int) $c['plant_count'] ?> Plant
+                                    </span>
+                                    <span class="badge rounded-pill bg-primary bg-opacity-15 text-primary border border-primary border-opacity-25">
+                                        <i class="fas fa-tint me-1"></i>
+                                        <?= (int) $c['water_count'] ?> Water
+                                    </span>
+                                </div>
+
+                                <!-- Alert summary (if any) -->
+                                <?php if (!empty($alerts['critical']) || !empty($alerts['watch'])): ?>
+                                <div class="d-flex gap-2 mb-3">
+                                    <?php if ($alerts['critical'] > 0): ?>
+                                    <div class="flex-fill rounded p-2 bg-danger bg-opacity-10 text-center">
+                                        <div class="fw-bold text-danger"><?= $alerts['critical'] ?></div>
+                                        <div class="text-danger" style="font-size:.7rem">CRITICAL</div>
+                                    </div>
+                                    <?php endif; ?>
+                                    <?php if ($alerts['watch'] > 0): ?>
+                                    <div class="flex-fill rounded p-2 bg-warning bg-opacity-10 text-center">
+                                        <div class="fw-bold text-warning"><?= $alerts['watch'] ?></div>
+                                        <div class="text-warning" style="font-size:.7rem">WATCH</div>
+                                    </div>
+                                    <?php endif; ?>
+                                </div>
+                                <?php endif; ?>
+
+                            </div>
+
+                            <!-- Card footer: last activity + action links -->
+                            <div class="card-footer bg-transparent border-top-0 pt-0 pb-3 px-3">
+                                <div class="d-flex justify-content-between align-items-center">
+                                    <span class="text-muted" style="font-size:.75rem">
+                                        <i class="fas fa-clock me-1"></i>
+                                        Last test: <?= fmtDate($c['last_activity']) ?>
+                                    </span>
+                                    <a href="/dashboard/consultant/client.php?cid=<?= (int) $c['id'] ?>"
+                                       class="btn btn-sm btn-outline-secondary">
+                                        View <i class="fas fa-arrow-right ms-1"></i>
+                                    </a>
+                                </div>
+                            </div>
+                        </div>
+
+                    </div>
+                    <?php endforeach; ?>
+                </div>
+
+                <?php endif; ?>
+
+            </div>
+        </main>
+    </div>
+</div>
+
+<script>
+(function () {
+    const searchInput  = document.getElementById('client-search');
+    const alertFilter  = document.getElementById('alert-filter');
+    const grid         = document.getElementById('client-grid');
+    const countLabel   = document.getElementById('client-count-label');
+    const cards        = grid ? Array.from(grid.querySelectorAll('.client-card-col')) : [];
+
+    function applyFilters() {
+        const query  = searchInput.value.toLowerCase().trim();
+        const status = alertFilter.value;
+        let visible  = 0;
+
+        cards.forEach(card => {
+            const nameMatch   = !query  || card.dataset.name.includes(query);
+            const alertMatch  = !status || card.dataset.alert === status;
+            const show        = nameMatch && alertMatch;
+            card.style.display = show ? '' : 'none';
+            if (show) visible++;
+        });
+
+        if (countLabel) {
+            countLabel.textContent = `Showing ${visible} client${visible !== 1 ? 's' : ''}`;
+        }
+    }
+
+    if (searchInput) searchInput.addEventListener('input',  applyFilters);
+    if (alertFilter) alertFilter.addEventListener('change', applyFilters);
+})();
+</script>
+
+<?php include __DIR__ . '/../../layouts/footer.php'; ?>

+ 10 - 0
layouts/sidebar.php

@@ -30,6 +30,7 @@ $groupActive = function (array $children) use ($currentPath): bool {
 };
 
 // Child paths per collapsible group — used to keep the right group open
+$consultantChildren = ['/dashboard/consultant/'];
 $weatherChildren   = ['/dashboard/weather/'];
 $cropChildren      = [
     '/dashboard/crop-analysis/soil-test-data/',
@@ -49,6 +50,15 @@ $settingsChildren  = [
     <div class="sb-sidenav-menu">
         <div class="nav">
 
+            <!-- Consultant Dashboard -->
+            <a href="/dashboard/consultant/index.php"
+               class="nav-link<?= $isActive('/dashboard/consultant/') ?>">
+                <div class="sb-nav-link-icon">
+                    <i class="fas fa-chart-line nav_icon"></i>
+                </div>
+                Consultant Dashboard
+            </a>
+
             <!-- Planning Calendar -->
             <a href="/dashboard/planning-calendar.php"
                class="nav-link<?= $isActive('/dashboard/planning-calendar.php') ?>">

+ 226 - 0
lib/consultant.php

@@ -0,0 +1,226 @@
+<?php
+/**
+ * lib/consultant.php
+ *
+ * Shared functions for the Consultant Dashboard.
+ * Provides client listing with aggregated test counts and
+ * alert generation from soil test values vs. specifications.
+ */
+
+// ─── Nutrient alert thresholds ────────────────────────────────────────────────
+//
+// These nutrient columns exist on both soil_records and soil_specifications.
+// Spec values are treated as the minimum acceptable target.
+// Where no spec row exists, we fall back to the hardcoded pH sanity ranges.
+
+const ALERT_NUTRIENTS = [
+    'ph_cacl2'  => 'pH (CaCl₂)',
+    'ph_h2o'    => 'pH (H₂O)',
+    'ec'        => 'EC',
+    'NO3_N'     => 'Nitrate-N',
+    'NH3_N'     => 'Ammonium-N',
+    'p_morgan'  => 'Phosphorus',
+    'k_morgan'  => 'Potassium',
+    'ca_morgan' => 'Calcium',
+    'mg_morgan' => 'Magnesium',
+    's_morgan'  => 'Sulphur',
+    'ocarbon'   => 'Organic Carbon',
+    'b_cacl2'   => 'Boron',
+    'zn_dtpa'   => 'Zinc',
+    'mn_dtpa'   => 'Manganese',
+    'cu_dtpa'   => 'Copper',
+    'fe_dtpa'   => 'Iron',
+];
+
+// Hardcoded sanity ranges used when no soil_specifications row is found.
+// Format: [min, max] — null means unchecked on that side.
+const FALLBACK_RANGES = [
+    'ph_cacl2' => [5.5, 7.5],
+    'ph_h2o'   => [6.0, 8.0],
+    'ec'       => [null, 1.5],
+];
+
+/**
+ * Load all clients belonging to a consultant with aggregated test counts,
+ * most-recent test date, and alert counts.
+ *
+ * @param PDO $pdo
+ * @param int $userId  consultant's modx_user_id
+ * @return array[]
+ */
+function getConsultantClients(PDO $pdo, int $userId): array
+{
+    $sql = "
+        SELECT
+            cr.id,
+            cr.client,
+            cr.company,
+            cr.address,
+            cr.state_postcode,
+            cr.email,
+            cr.phone,
+
+            COUNT(DISTINCT sr.id)  AS soil_count,
+            COUNT(DISTINCT pr.id)  AS plant_count,
+            COUNT(DISTINCT wr.id)  AS water_count,
+
+            MAX(sr.date_sampled)   AS last_soil_date,
+            MAX(pr.date_sampled)   AS last_plant_date,
+            MAX(wr.date_sampled)   AS last_water_date,
+
+            -- Most recent activity across all test types
+            GREATEST(
+                COALESCE(MAX(sr.date_sampled), '1970-01-01'),
+                COALESCE(MAX(pr.date_sampled), '1970-01-01'),
+                COALESCE(MAX(wr.date_sampled), '1970-01-01')
+            ) AS last_activity
+
+        FROM client_records cr
+        LEFT JOIN soil_records  sr ON CAST(sr.client_records_id AS UNSIGNED) = cr.id
+        LEFT JOIN plant_records pr ON pr.client_records_id = cr.id
+        LEFT JOIN water_records wr ON wr.client_records_id = cr.id
+        WHERE cr.modx_user_id = ?
+        GROUP BY cr.id
+        ORDER BY last_activity DESC, cr.client ASC
+    ";
+
+    $stmt = $pdo->prepare($sql);
+    $stmt->execute([$userId]);
+    $clients = $stmt->fetchAll();
+
+    // Attach alert summary to each client
+    foreach ($clients as &$client) {
+        $client['alerts'] = getClientAlertSummary($pdo, $client['id']);
+        $client['last_activity'] = $client['last_activity'] === '1970-01-01' ? null : $client['last_activity'];
+    }
+    unset($client);
+
+    return $clients;
+}
+
+/**
+ * Return ['critical' => int, 'watch' => int] for a client's most recent soil test.
+ */
+function getClientAlertSummary(PDO $pdo, int $clientId): array
+{
+    // Fetch the most recent soil test for this client
+    $stmt = $pdo->prepare("
+        SELECT * FROM soil_records
+        WHERE CAST(client_records_id AS UNSIGNED) = ?
+        ORDER BY date_sampled DESC, id DESC
+        LIMIT 1
+    ");
+    $stmt->execute([$clientId]);
+    $soilRow = $stmt->fetch();
+
+    if (!$soilRow) {
+        return ['critical' => 0, 'watch' => 0];
+    }
+
+    // Try to load matching spec row
+    $spec = null;
+    if (!empty($soilRow['soil_type'])) {
+        $stmt2 = $pdo->prepare("
+            SELECT * FROM soil_specifications
+            WHERE soil_type = ?
+            ORDER BY id ASC LIMIT 1
+        ");
+        $stmt2->execute([$soilRow['soil_type']]);
+        $spec = $stmt2->fetch() ?: null;
+    }
+
+    return generateAlerts($soilRow, $spec)['summary'];
+}
+
+/**
+ * Generate full alert list for a soil record vs its spec.
+ *
+ * @param array      $soilRow  Row from soil_records
+ * @param array|null $spec     Row from soil_specifications (or null)
+ * @return array  ['summary' => ['critical'=>int,'watch'=>int], 'items' => [...]]
+ */
+function generateAlerts(array $soilRow, ?array $spec): array
+{
+    $critical = 0;
+    $watch    = 0;
+    $items    = [];
+
+    foreach (ALERT_NUTRIENTS as $col => $label) {
+        $measured = isset($soilRow[$col]) && $soilRow[$col] !== ''
+            ? (float) $soilRow[$col]
+            : null;
+
+        if ($measured === null) {
+            continue;
+        }
+
+        // Determine range from spec or fallback
+        $min = null;
+        $max = null;
+
+        if ($spec !== null && isset($spec[$col]) && $spec[$col] !== '') {
+            // Spec value is the minimum target
+            $min = (float) $spec[$col];
+        } elseif (isset(FALLBACK_RANGES[$col])) {
+            [$min, $max] = FALLBACK_RANGES[$col];
+        }
+
+        if ($min === null && $max === null) {
+            continue; // no reference — skip
+        }
+
+        $severity = null;
+
+        if ($min !== null && $measured < $min) {
+            // Below minimum — how far below determines severity
+            $ratio = $min > 0 ? ($measured / $min) : 0;
+            $severity = $ratio < 0.5 ? 'critical' : 'watch';
+        } elseif ($max !== null && $measured > $max) {
+            $severity = 'watch'; // above max is always a watch
+        }
+
+        if ($severity === null) {
+            continue;
+        }
+
+        if ($severity === 'critical') $critical++;
+        else                          $watch++;
+
+        $items[] = [
+            'nutrient'  => $label,
+            'col'       => $col,
+            'measured'  => $measured,
+            'min'       => $min,
+            'max'       => $max,
+            'severity'  => $severity,
+        ];
+    }
+
+    // Sort: critical first
+    usort($items, fn($a, $b) => $a['severity'] === 'critical' ? -1 : 1);
+
+    return [
+        'summary' => ['critical' => $critical, 'watch' => $watch],
+        'items'   => $items,
+    ];
+}
+
+/**
+ * Format a date string for display ("12 Mar 2024"), returns '—' if null/empty.
+ */
+function fmtDate(?string $date): string
+{
+    if (!$date || $date === '1970-01-01') return '—';
+    $ts = strtotime($date);
+    return $ts ? date('j M Y', $ts) : '—';
+}
+
+/**
+ * Return the Bootstrap badge class for an alert severity.
+ */
+function alertBadgeClass(int $critical, int $watch): string
+{
+    if ($critical > 0) return 'danger';
+    if ($watch > 0)    return 'warning';
+    return 'success';
+}