Browse Source

Paddock Updates

Benjamin Harris 2 tháng trước cách đây
mục cha
commit
786512ddba

+ 4 - 1
.claude/settings.local.json

@@ -9,7 +9,10 @@
       "Read(//c/Users/lumion/AppData/Roaming/Composer/vendor/**)",
       "Bash(find /c -name composer.phar)",
       "WebSearch",
-      "WebFetch(domain:github.com)"
+      "WebFetch(domain:github.com)",
+      "Bash(grep -A 20 \"CREATE TABLE \\\\`field_sensors\\\\`\" \"f:/GIT_REPO/crop_monitor/cropmonitor.sql\")",
+      "Bash(grep -A 20 \"CREATE TABLE \\\\`sensor_id\\\\`\" \"f:/GIT_REPO/crop_monitor/cropmonitor.sql\")",
+      "Bash(grep -A 20 \"CREATE TABLE \\\\`crop_info\\\\`\" \"f:/GIT_REPO/crop_monitor/cropmonitor.sql\")"
     ]
   }
 }

+ 9 - 1
controllers/blockSubmit.php

@@ -83,7 +83,15 @@ if ($action === 'edit') {
     $stmt->execute([$name, $blockId, $location, (int) $areaHa, $gps, $recordId, $userId]);
 
     $_SESSION['flash_success'] = 'Paddock "' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '" updated.';
-    header('Location: /dashboard/crop-cards/');
+
+    // Return to paddock dashboard if the edit came from there
+    $referer = $_POST['_referer'] ?? '';
+    if (str_contains($referer, 'block-detail.php')) {
+        header('Location: /dashboard/crop-cards/block-detail.php?rid=' . $recordId
+            . '&id=' . urlencode($blockId));
+    } else {
+        header('Location: /dashboard/crop-cards/');
+    }
     exit;
 }
 

+ 699 - 0
dashboard/crop-cards/block-detail.php

@@ -0,0 +1,699 @@
+<?php
+/**
+ * dashboard/crop-cards/block-detail.php
+ *
+ * Paddock dashboard — per-block view showing:
+ *   - Paddock metadata
+ *   - Mini live weather (Open-Meteo via api/weather.php)
+ *   - Recent soil, plant, water tests linked to this block
+ *   - Latest sensor readings
+ *   - Quick-action links (add tests, view calendar)
+ *
+ * GET params:
+ *   rid   — block_info.id  (int)
+ *   id    — block_info.block_id (string, secondary key)
+ *   block — display name (urldecoded)
+ */
+
+require_once __DIR__ . '/../../config/database.php';
+require_once __DIR__ . '/../../lib/auth.php';
+require_once __DIR__ . '/../../lib/csrf.php';
+
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+requireLogin();
+
+$rid     = (int)  ($_GET['rid']   ?? 0);
+$blockId = trim(  $_GET['id']     ?? '');
+
+if ($rid <= 0) {
+    http_response_code(400);
+    die('Invalid request');
+}
+
+$pdo    = getDBConnection();
+$userId = getCurrentUserId();
+$h      = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
+
+// ── Load paddock ─────────────────────────────────────────────────────────────
+$stmt = $pdo->prepare('SELECT * FROM block_info WHERE id = ? AND modx_user_id = ? LIMIT 1');
+$stmt->execute([$rid, $userId]);
+$block = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if (!$block) {
+    http_response_code(404);
+    die('Paddock not found or access denied');
+}
+
+$areaHa = number_format((float)$block['area'], 1);
+$areaAc = number_format((float)$block['area'] * 2.47105, 1);
+
+// ── Crop name(s) for this paddock ────────────────────────────────────────────
+$stmtCrops = $pdo->prepare(
+    'SELECT name FROM crop_info WHERE modx_user_id = ? AND paddock_id = ? ORDER BY id DESC'
+);
+$stmtCrops->execute([$userId, $block['block_id']]);
+$crops = $stmtCrops->fetchAll(PDO::FETCH_COLUMN);
+
+// ── Recent soil tests ─────────────────────────────────────────────────────────
+$stmtSoil = $pdo->prepare(
+    'SELECT id, rand, date, site_id, analysis_type, crop_type, client_name
+     FROM soil_records
+     WHERE modx_user_id = ? AND block_id = ?
+     ORDER BY date DESC LIMIT 5'
+);
+$stmtSoil->execute([$userId, $block['block_id']]);
+$soilTests = $stmtSoil->fetchAll(PDO::FETCH_ASSOC);
+
+// ── Recent plant tests ────────────────────────────────────────────────────────
+$stmtPlant = $pdo->prepare(
+    'SELECT id, rand, date_sampled AS date, site_id, crop_type, client_name
+     FROM plant_records
+     WHERE modx_user_id = ? AND site_id = ?
+     ORDER BY date_sampled DESC LIMIT 5'
+);
+$stmtPlant->execute([$userId, $block['block_id']]);
+$plantTests = $stmtPlant->fetchAll(PDO::FETCH_ASSOC);
+
+// ── Recent water tests ────────────────────────────────────────────────────────
+$stmtWater = $pdo->prepare(
+    'SELECT id, rand, date_sampled AS date, site_id, analysis_type, client_name
+     FROM water_records
+     WHERE modx_user_id = ? AND site_id = ?
+     ORDER BY date_sampled DESC LIMIT 5'
+);
+$stmtWater->execute([$userId, $block['block_id']]);
+$waterTests = $stmtWater->fetchAll(PDO::FETCH_ASSOC);
+
+// ── Sensor readings (latest per sensor) ──────────────────────────────────────
+$stmtSensors = $pdo->prepare(
+    'SELECT fs.sensor_id, fs.sensor_name, fs.value, fs.DATEUTC
+     FROM field_sensors fs
+     INNER JOIN (
+         SELECT sensor_id, MAX(DATEUTC) AS latest
+         FROM field_sensors
+         WHERE modx_user_id = ?
+         GROUP BY sensor_id
+     ) latest ON fs.sensor_id = latest.sensor_id AND fs.DATEUTC = latest.latest
+     WHERE fs.modx_user_id = ?
+     ORDER BY fs.sensor_name'
+);
+$stmtSensors->execute([$userId, $userId]);
+$sensors = $stmtSensors->fetchAll(PDO::FETCH_ASSOC);
+
+// ── GPS lat/lng for weather override ─────────────────────────────────────────
+$weatherLat = $weatherLng = null;
+if (!empty($block['gps'])) {
+    if (preg_match('/(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)/', $block['gps'], $m)) {
+        $weatherLat = (float)$m[1];
+        $weatherLng = (float)$m[2];
+    }
+}
+$weatherUrl = '/api/weather.php' . ($weatherLat ? '?lat=' . $weatherLat . '&lng=' . $weatherLng : '');
+
+$pageTitle = $h($block['name']) . ' — Paddock Dashboard';
+$siteName  = 'Crop Monitor';
+
+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">
+
+                <!-- ── Breadcrumb ──────────────────────────────────────────── -->
+                <div class="d-flex align-items-center justify-content-between mt-4 mb-1">
+                    <div>
+                        <h1 class="h3 mb-0">
+                            <i class="fas fa-seedling text-success me-2"></i><?= $h($block['name']) ?>
+                        </h1>
+                        <ol class="breadcrumb mb-0 small">
+                            <li class="breadcrumb-item"><a href="/dashboard/dashboard.php">Dashboard</a></li>
+                            <li class="breadcrumb-item"><a href="/dashboard/crop-cards/">Crop Cards</a></li>
+                            <li class="breadcrumb-item active"><?= $h($block['name']) ?></li>
+                        </ol>
+                    </div>
+                    <div class="d-flex gap-2 flex-wrap">
+                        <a href="/dashboard/crop-cards/" class="btn btn-outline-secondary btn-sm">
+                            <i class="fas fa-arrow-left me-1"></i>All Paddocks
+                        </a>
+                        <button class="btn btn-outline-primary btn-sm"
+                                data-bs-toggle="modal" data-bs-target="#editPaddockModal">
+                            <i class="fas fa-edit me-1"></i>Edit
+                        </button>
+                    </div>
+                </div>
+
+                <hr class="mb-3">
+
+                <!-- ── Top row: paddock info + weather ────────────────────── -->
+                <div class="row g-3 mb-4">
+
+                    <!-- Paddock info card -->
+                    <div class="col-md-4 col-lg-3">
+                        <div class="card h-100 border-success">
+                            <div class="card-header bg-success text-white py-2 fw-bold small">
+                                <i class="fas fa-map-marked-alt me-1"></i>Paddock Details
+                            </div>
+                            <div class="card-body py-2 small">
+                                <table class="table table-sm table-borderless mb-0">
+                                    <tr>
+                                        <th class="text-muted pe-2 text-nowrap">Block ID</th>
+                                        <td><?= $h($block['block_id']) ?></td>
+                                    </tr>
+                                    <tr>
+                                        <th class="text-muted pe-2 text-nowrap">Location</th>
+                                        <td><?= $h($block['location']) ?: '—' ?></td>
+                                    </tr>
+                                    <tr>
+                                        <th class="text-muted pe-2 text-nowrap">Area</th>
+                                        <td><?= $areaHa ?> ha &nbsp;/&nbsp; <?= $areaAc ?> ac</td>
+                                    </tr>
+                                    <tr>
+                                        <th class="text-muted pe-2 text-nowrap">GPS</th>
+                                        <td>
+                                            <?php if (!empty($block['gps'])): ?>
+                                                <a href="https://maps.google.com/?q=<?= urlencode($block['gps']) ?>"
+                                                   target="_blank" rel="noopener"
+                                                   class="text-decoration-none">
+                                                    <i class="fas fa-map-pin text-danger me-1"></i><?= $h($block['gps']) ?>
+                                                </a>
+                                            <?php else: ?>
+                                                <span class="text-muted">—</span>
+                                            <?php endif; ?>
+                                        </td>
+                                    </tr>
+                                    <tr>
+                                        <th class="text-muted pe-2 text-nowrap">Crops</th>
+                                        <td>
+                                            <?php if ($crops): ?>
+                                                <?= $h(implode(', ', $crops)) ?>
+                                            <?php else: ?>
+                                                <span class="text-muted">None recorded</span>
+                                            <?php endif; ?>
+                                        </td>
+                                    </tr>
+                                    <tr>
+                                        <th class="text-muted pe-2 text-nowrap">Added</th>
+                                        <td><?= $h($block['date_added']) ?: '—' ?></td>
+                                    </tr>
+                                </table>
+                            </div>
+                        </div>
+                    </div>
+
+                    <!-- Weather card -->
+                    <div class="col-md-8 col-lg-5">
+                        <div class="card h-100">
+                            <div class="card-header py-2 fw-bold small">
+                                <i class="fas fa-cloud-sun me-1 text-warning"></i>Current Weather
+                                <span class="text-muted fw-normal ms-1 small" id="wx-pd-location"></span>
+                            </div>
+                            <div class="card-body py-2">
+
+                                <!-- Skeleton -->
+                                <div id="wx-pd-loading" class="d-flex align-items-center justify-content-center py-3">
+                                    <span class="spinner-border spinner-border-sm me-2 text-secondary"></span>
+                                    <span class="text-muted small">Loading weather…</span>
+                                </div>
+
+                                <!-- Content -->
+                                <div id="wx-pd-content" style="display:none;">
+                                    <div class="d-flex align-items-center gap-3 mb-2">
+                                        <canvas id="wx-pd-icon" width="64" height="64"></canvas>
+                                        <div>
+                                            <div class="display-6 lh-1 fw-bold" id="wx-pd-temp">—</div>
+                                            <div class="text-muted" id="wx-pd-condition">—</div>
+                                        </div>
+                                        <div class="ms-auto text-end small text-muted">
+                                            <div><i class="fa fa-tint"></i> <span id="wx-pd-humidity">—</span>% humidity</div>
+                                            <div><i class="fa fa-wind"></i> <span id="wx-pd-wind">—</span> km/h wind</div>
+                                            <div><i class="fa fa-cloud-rain"></i> <span id="wx-pd-rain">—</span> mm now</div>
+                                            <div><i class="fa fa-thermometer-half"></i> Feels <span id="wx-pd-feels">—</span>°</div>
+                                        </div>
+                                    </div>
+
+                                    <!-- 5-day mini forecast strip -->
+                                    <div class="d-flex gap-2 overflow-auto pb-1" id="wx-pd-forecast">
+                                        <!-- filled by JS -->
+                                    </div>
+                                </div>
+
+                                <div id="wx-pd-error" class="alert alert-warning small py-1 mb-0" style="display:none;"></div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <!-- Quick actions card -->
+                    <div class="col-md-12 col-lg-4">
+                        <div class="card h-100">
+                            <div class="card-header py-2 fw-bold small">
+                                <i class="fas fa-bolt me-1 text-warning"></i>Quick Actions
+                            </div>
+                            <div class="card-body py-2">
+                                <div class="d-grid gap-2">
+                                    <a href="/dashboard/crop-analysis/soil-test-data/soil-test-data.php?block_id=<?= urlencode($block['block_id']) ?>"
+                                       class="btn btn-outline-success btn-sm text-start">
+                                        <i class="fas fa-vial me-2 text-success"></i>New Soil Test
+                                    </a>
+                                    <a href="/dashboard/crop-analysis/plant-test-data/plant-test-data.php?block_id=<?= urlencode($block['block_id']) ?>"
+                                       class="btn btn-outline-primary btn-sm text-start">
+                                        <i class="fas fa-leaf me-2 text-primary"></i>New Plant Test
+                                    </a>
+                                    <a href="/dashboard/crop-analysis/water-test-data/water-test-data.php?block_id=<?= urlencode($block['block_id']) ?>"
+                                       class="btn btn-outline-info btn-sm text-start">
+                                        <i class="fas fa-tint me-2 text-info"></i>New Water Test
+                                    </a>
+                                    <a href="/dashboard/planning-calendar.php?paddock=<?= urlencode($block['block_id']) ?>"
+                                       class="btn btn-outline-secondary btn-sm text-start">
+                                        <i class="fas fa-calendar me-2 text-secondary"></i>View Calendar
+                                    </a>
+                                    <a href="/dashboard/inbox.php"
+                                       class="btn btn-outline-dark btn-sm text-start">
+                                        <i class="fas fa-inbox me-2"></i>Inbox / Reports
+                                    </a>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                </div><!-- /top row -->
+
+                <!-- ── Rainfall history chart ──────────────────────────────── -->
+                <div class="row g-3 mb-4">
+                    <div class="col-12">
+                        <div class="card" id="wx-pd-rainfall-card" style="display:none;">
+                            <div class="card-header py-2 fw-bold small">
+                                <i class="fas fa-chart-bar me-1 text-info"></i>Past 7 Days Rainfall (mm)
+                            </div>
+                            <div class="card-body py-2">
+                                <canvas id="wx-pd-rainfall-chart" height="60"></canvas>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- ── Sensor readings ─────────────────────────────────────── -->
+                <?php if ($sensors): ?>
+                <div class="row g-3 mb-4">
+                    <div class="col-12">
+                        <h5 class="mb-2"><i class="fas fa-satellite-dish me-2 text-secondary"></i>Live Sensor Readings</h5>
+                    </div>
+                    <?php foreach ($sensors as $sensor): ?>
+                    <div class="col-6 col-sm-4 col-md-3 col-xl-2">
+                        <div class="card border-0 shadow-sm text-center">
+                            <div class="card-body py-2 px-2">
+                                <div class="small text-muted text-truncate" title="<?= $h($sensor['sensor_name']) ?>">
+                                    <?= $h($sensor['sensor_name'] ?: $sensor['sensor_id']) ?>
+                                </div>
+                                <div class="h4 mb-0 fw-bold"><?= number_format((float)$sensor['value'], 1) ?></div>
+                                <div class="text-muted" style="font-size:0.68rem;">
+                                    <?= $h(date('j M H:i', strtotime($sensor['DATEUTC']))) ?>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <?php endforeach; ?>
+                </div>
+                <?php endif; ?>
+
+                <!-- ── Analysis tabs ──────────────────────────────────────── -->
+                <div class="row mb-4">
+                    <div class="col-12">
+                        <h5 class="mb-2"><i class="fas fa-flask me-2 text-success"></i>Analysis Records</h5>
+
+                        <ul class="nav nav-tabs mb-0" id="analysisTabs">
+                            <li class="nav-item">
+                                <a class="nav-link active" href="#tab-soil" data-bs-toggle="list">
+                                    <i class="fas fa-vial me-1 text-success"></i>Soil
+                                    <span class="badge bg-success ms-1"><?= count($soilTests) ?></span>
+                                </a>
+                            </li>
+                            <li class="nav-item">
+                                <a class="nav-link" href="#tab-plant" data-bs-toggle="list">
+                                    <i class="fas fa-leaf me-1 text-primary"></i>Plant
+                                    <span class="badge bg-primary ms-1"><?= count($plantTests) ?></span>
+                                </a>
+                            </li>
+                            <li class="nav-item">
+                                <a class="nav-link" href="#tab-water" data-bs-toggle="list">
+                                    <i class="fas fa-tint me-1 text-info"></i>Water
+                                    <span class="badge bg-info ms-1"><?= count($waterTests) ?></span>
+                                </a>
+                            </li>
+                        </ul>
+
+                        <div class="tab-content border border-top-0 rounded-bottom p-3">
+
+                            <!-- Soil tests tab -->
+                            <div class="tab-pane fade show active" id="tab-soil">
+                                <?php if (empty($soilTests)): ?>
+                                    <p class="text-muted mb-0 small">No soil tests recorded for block ID <strong><?= $h($block['block_id']) ?></strong>.</p>
+                                <?php else: ?>
+                                <div class="table-responsive">
+                                    <table class="table table-sm table-hover align-middle mb-0">
+                                        <thead class="table-light">
+                                            <tr>
+                                                <th>Date</th>
+                                                <th>Site ID</th>
+                                                <th>Client</th>
+                                                <th>Soil Type</th>
+                                                <th>Crop</th>
+                                                <th class="text-end">Actions</th>
+                                            </tr>
+                                        </thead>
+                                        <tbody>
+                                        <?php foreach ($soilTests as $t): ?>
+                                            <tr>
+                                                <td><?= $h($t['date'] ? date('j M Y', strtotime($t['date'])) : '—') ?></td>
+                                                <td><?= $h($t['site_id']) ?></td>
+                                                <td><?= $h($t['client_name']) ?></td>
+                                                <td><?= $h($t['analysis_type']) ?></td>
+                                                <td><?= $h($t['crop_type']) ?></td>
+                                                <td class="text-end text-nowrap">
+                                                    <a href="/dashboard/crop-analysis/soil-test-data/soil-analysis.php?rid=<?= $t['id'] ?>&rand=<?= urlencode($t['rand']) ?>"
+                                                       class="btn btn-outline-success btn-sm py-0 px-2" title="View Analysis">
+                                                        <i class="fas fa-chart-bar"></i>
+                                                    </a>
+                                                    <a href="/dashboard/crop-analysis/soil-test-data/soil-report.php?rid=<?= $t['id'] ?>&rand=<?= urlencode($t['rand']) ?>"
+                                                       class="btn btn-outline-primary btn-sm py-0 px-2" title="View Report">
+                                                        <i class="fas fa-file-alt"></i>
+                                                    </a>
+                                                    <a href="/pdf-files/headlessChrome_pdf.php?type=soil&rid=<?= $t['id'] ?>&rand=<?= urlencode($t['rand']) ?>"
+                                                       class="btn btn-outline-secondary btn-sm py-0 px-2" title="Download PDF">
+                                                        <i class="fas fa-file-pdf"></i>
+                                                    </a>
+                                                </td>
+                                            </tr>
+                                        <?php endforeach; ?>
+                                        </tbody>
+                                    </table>
+                                </div>
+                                <?php endif; ?>
+                            </div>
+
+                            <!-- Plant tests tab -->
+                            <div class="tab-pane fade" id="tab-plant">
+                                <?php if (empty($plantTests)): ?>
+                                    <p class="text-muted mb-0 small">No plant tissue tests recorded for block ID <strong><?= $h($block['block_id']) ?></strong>.</p>
+                                <?php else: ?>
+                                <div class="table-responsive">
+                                    <table class="table table-sm table-hover align-middle mb-0">
+                                        <thead class="table-light">
+                                            <tr>
+                                                <th>Date</th>
+                                                <th>Site ID</th>
+                                                <th>Client</th>
+                                                <th>Crop</th>
+                                                <th class="text-end">Actions</th>
+                                            </tr>
+                                        </thead>
+                                        <tbody>
+                                        <?php foreach ($plantTests as $t): ?>
+                                            <tr>
+                                                <td><?= $h($t['date'] ? date('j M Y', strtotime($t['date'])) : '—') ?></td>
+                                                <td><?= $h($t['site_id']) ?></td>
+                                                <td><?= $h($t['client_name']) ?></td>
+                                                <td><?= $h($t['crop_type']) ?></td>
+                                                <td class="text-end text-nowrap">
+                                                    <a href="/dashboard/crop-analysis/plant-test-data/plant-analysis.php?rid=<?= $t['id'] ?>&rand=<?= urlencode($t['rand']) ?>"
+                                                       class="btn btn-outline-primary btn-sm py-0 px-2" title="View Analysis">
+                                                        <i class="fas fa-chart-bar"></i>
+                                                    </a>
+                                                    <a href="/dashboard/crop-analysis/plant-test-data/plant-report.php?rid=<?= $t['id'] ?>&rand=<?= urlencode($t['rand']) ?>"
+                                                       class="btn btn-outline-success btn-sm py-0 px-2" title="View Report">
+                                                        <i class="fas fa-file-alt"></i>
+                                                    </a>
+                                                    <a href="/pdf-files/headlessChrome_pdf.php?type=plant&rid=<?= $t['id'] ?>&rand=<?= urlencode($t['rand']) ?>"
+                                                       class="btn btn-outline-secondary btn-sm py-0 px-2" title="Download PDF">
+                                                        <i class="fas fa-file-pdf"></i>
+                                                    </a>
+                                                </td>
+                                            </tr>
+                                        <?php endforeach; ?>
+                                        </tbody>
+                                    </table>
+                                </div>
+                                <?php endif; ?>
+                            </div>
+
+                            <!-- Water tests tab -->
+                            <div class="tab-pane fade" id="tab-water">
+                                <?php if (empty($waterTests)): ?>
+                                    <p class="text-muted mb-0 small">No water quality tests recorded for block ID <strong><?= $h($block['block_id']) ?></strong>.</p>
+                                <?php else: ?>
+                                <div class="table-responsive">
+                                    <table class="table table-sm table-hover align-middle mb-0">
+                                        <thead class="table-light">
+                                            <tr>
+                                                <th>Date</th>
+                                                <th>Site ID</th>
+                                                <th>Client</th>
+                                                <th>Analysis Type</th>
+                                                <th class="text-end">Actions</th>
+                                            </tr>
+                                        </thead>
+                                        <tbody>
+                                        <?php foreach ($waterTests as $t): ?>
+                                            <tr>
+                                                <td><?= $h($t['date'] ? date('j M Y', strtotime($t['date'])) : '—') ?></td>
+                                                <td><?= $h($t['site_id']) ?></td>
+                                                <td><?= $h($t['client_name']) ?></td>
+                                                <td><?= $h($t['analysis_type']) ?></td>
+                                                <td class="text-end text-nowrap">
+                                                    <a href="/dashboard/crop-analysis/water-test-data/water-analysis.php?rid=<?= $t['id'] ?>&rand=<?= urlencode($t['rand']) ?>"
+                                                       class="btn btn-outline-info btn-sm py-0 px-2" title="View Analysis">
+                                                        <i class="fas fa-chart-bar"></i>
+                                                    </a>
+                                                    <a href="/pdf-files/headlessChrome_pdf.php?type=water&rid=<?= $t['id'] ?>&rand=<?= urlencode($t['rand']) ?>"
+                                                       class="btn btn-outline-secondary btn-sm py-0 px-2" title="Download PDF">
+                                                        <i class="fas fa-file-pdf"></i>
+                                                    </a>
+                                                </td>
+                                            </tr>
+                                        <?php endforeach; ?>
+                                        </tbody>
+                                    </table>
+                                </div>
+                                <?php endif; ?>
+                            </div>
+
+                        </div><!-- /tab-content -->
+                    </div>
+                </div><!-- /analysis tabs row -->
+
+            </div><!-- /container-fluid -->
+        </main>
+
+        <!-- ── Edit Paddock modal ──────────────────────────────────────────── -->
+        <div class="modal fade" id="editPaddockModal" tabindex="-1" aria-hidden="true">
+            <div class="modal-dialog modal-lg modal-dialog-centered">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title">Edit Paddock — <?= $h($block['name']) ?></h5>
+                        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+                    </div>
+                    <form method="post" action="/controllers/blockSubmit.php">
+                        <div class="modal-body">
+                            <input type="hidden" name="csrf_token"
+                                   value="<?= $h(generateCsrfToken()) ?>">
+                            <input type="hidden" name="action"    value="edit">
+                            <input type="hidden" name="record_id" value="<?= $rid ?>">
+                            <input type="hidden" name="_referer"  value="block-detail.php">
+
+                            <div class="row mb-3">
+                                <div class="col">
+                                    <label class="form-label">Block ID</label>
+                                    <input type="text" class="form-control form-control-sm"
+                                           name="block_id" value="<?= $h($block['block_id']) ?>" required>
+                                </div>
+                                <div class="col">
+                                    <label class="form-label">Block Name</label>
+                                    <input type="text" class="form-control form-control-sm"
+                                           name="name" value="<?= $h($block['name']) ?>" required>
+                                </div>
+                            </div>
+
+                            <div class="mb-3">
+                                <label class="form-label">Location / Address</label>
+                                <input type="text" class="form-control form-control-sm"
+                                       name="location" value="<?= $h($block['location']) ?>">
+                            </div>
+
+                            <div class="row mb-3">
+                                <div class="col">
+                                    <label class="form-label">Area (hectares)</label>
+                                    <input type="number" step="0.01" class="form-control form-control-sm"
+                                           id="ep_area_ha" name="area_ha"
+                                           value="<?= $h(number_format((float)$block['area'], 2)) ?>"
+                                           oninput="epAreaConvert('ha', this.value)">
+                                </div>
+                                <div class="col">
+                                    <label class="form-label">Area (acres)</label>
+                                    <input type="number" step="0.01" class="form-control form-control-sm"
+                                           id="ep_area_ac"
+                                           value="<?= $h(number_format((float)$block['area'] * 2.47105, 2)) ?>"
+                                           oninput="epAreaConvert('ac', this.value)">
+                                </div>
+                                <div class="col">
+                                    <label class="form-label">GPS Coordinates</label>
+                                    <input type="text" class="form-control form-control-sm"
+                                           name="gps" value="<?= $h($block['gps']) ?>"
+                                           placeholder="e.g. -33.8688, 151.2093">
+                                </div>
+                            </div>
+
+                            <div>
+                                <label class="form-label">Soil Type</label>
+                                <select class="form-select form-select-sm" name="analysis_type">
+                                    <option value="">Select soil type...</option>
+                                    <?php foreach (['sandy','light','medium','heavy'] as $st): ?>
+                                    <option value="<?= $st ?>"><?= ucfirst($st) ?></option>
+                                    <?php endforeach; ?>
+                                </select>
+                            </div>
+                        </div>
+                        <div class="modal-footer">
+                            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
+                            <button type="submit" class="btn btn-success">Save Changes</button>
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+
+<?php include __DIR__ . '/../../layouts/footer.php'; ?>
+
+<script>
+(function () {
+    'use strict';
+
+    // ── Edit modal area converter ──────────────────────────────────────────── //
+    window.epAreaConvert = function (source, val) {
+        val = parseFloat(val) || 0;
+        var haEl = document.getElementById('ep_area_ha');
+        var acEl = document.getElementById('ep_area_ac');
+        if (source === 'ha') { acEl.value = (val * 2.47105).toFixed(2); }
+        else                  { haEl.value = (val / 2.47105).toFixed(2); }
+    };
+
+    // ── Weather ────────────────────────────────────────────────────────────── //
+    var wxSkycons     = null;
+    var wxFcSkycons   = null;
+    var rainfallChart = null;
+
+    function renderWeather(data) {
+        document.getElementById('wx-pd-temp').textContent      = data.current.temp + '°';
+        document.getElementById('wx-pd-condition').textContent = data.current.label;
+        document.getElementById('wx-pd-humidity').textContent  = data.current.humidity;
+        document.getElementById('wx-pd-wind').textContent      = data.current.wind;
+        document.getElementById('wx-pd-rain').textContent      = data.current.rain;
+        document.getElementById('wx-pd-feels').textContent     = data.current.feels_like;
+        document.getElementById('wx-pd-location').textContent  = '— ' + data.location;
+
+        // Hero icon
+        if (!wxSkycons) { wxSkycons = new Skycons({ color: '#1ABC9C' }); }
+        wxSkycons.set(document.getElementById('wx-pd-icon'), data.current.icon);
+        wxSkycons.play();
+
+        // 5-day forecast strip
+        var futureDays = data.days.filter(function (d) { return !d.is_past && !d.is_today; }).slice(0, 5);
+        var forecastEl = document.getElementById('wx-pd-forecast');
+        var html       = '';
+
+        if (!wxFcSkycons) { wxFcSkycons = new Skycons({ color: '#888' }); }
+
+        futureDays.forEach(function (d, i) {
+            var cid = 'wx-pd-fc-' + i;
+            html +=
+                '<div class="text-center border rounded px-2 py-1" style="min-width:70px;">' +
+                    '<div class="small fw-bold">' + d.day_name + '</div>' +
+                    '<canvas id="' + cid + '" width="40" height="40"></canvas>' +
+                    '<div class="small">' + d.temp_max + '°</div>' +
+                    '<div class="text-muted" style="font-size:0.65rem;">' + d.rain + ' mm</div>' +
+                '</div>';
+        });
+        forecastEl.innerHTML = html;
+
+        futureDays.forEach(function (d, i) {
+            var el = document.getElementById('wx-pd-fc-' + i);
+            if (el) { wxFcSkycons.set(el, d.icon); }
+        });
+        wxFcSkycons.play();
+
+        // Rainfall history chart
+        var pastDays = data.days.filter(function (d) { return d.is_past; }).slice(-7);
+        if (pastDays.length > 0) {
+            var labels = pastDays.map(function (d) { return d.day_name; });
+            var values = pastDays.map(function (d) { return d.rain; });
+            var ctx    = document.getElementById('wx-pd-rainfall-chart').getContext('2d');
+            if (rainfallChart) { rainfallChart.destroy(); }
+            rainfallChart = new Chart(ctx, {
+                type: 'bar',
+                data: {
+                    labels: labels,
+                    datasets: [{
+                        label: 'mm',
+                        data:  values,
+                        backgroundColor: 'rgba(54,162,235,0.6)',
+                        borderColor:     'rgba(54,162,235,1)',
+                        borderWidth: 1,
+                    }],
+                },
+                options: {
+                    responsive: true,
+                    plugins: { legend: { display: false } },
+                    scales: {
+                        y: { beginAtZero: true, ticks: { font: { size: 10 } } },
+                        x: { ticks: { font: { size: 10 } } },
+                    },
+                },
+            });
+            document.getElementById('wx-pd-rainfall-card').style.display = '';
+        }
+
+        document.getElementById('wx-pd-loading').style.display  = 'none';
+        document.getElementById('wx-pd-content').style.display  = '';
+    }
+
+    fetch(<?= json_encode($weatherUrl) ?>)
+        .then(function (r) {
+            if (!r.ok) { throw new Error('HTTP ' + r.status); }
+            return r.json();
+        })
+        .then(function (data) {
+            if (data.error) { throw new Error(data.error); }
+            renderWeather(data);
+        })
+        .catch(function (err) {
+            document.getElementById('wx-pd-loading').style.display = 'none';
+            var el = document.getElementById('wx-pd-error');
+            el.textContent = 'Weather unavailable: ' + err.message;
+            el.style.display = '';
+        });
+
+    // ── Bootstrap tab fix: nav-tabs need data-bs-toggle="tab" ─────────────── //
+    document.querySelectorAll('#analysisTabs .nav-link').forEach(function (el) {
+        el.addEventListener('click', function (e) {
+            e.preventDefault();
+            document.querySelectorAll('#analysisTabs .nav-link').forEach(function (x) {
+                x.classList.remove('active');
+            });
+            document.querySelectorAll('.tab-pane').forEach(function (x) {
+                x.classList.remove('show', 'active');
+            });
+            el.classList.add('active');
+            var target = document.querySelector(el.getAttribute('href'));
+            if (target) { target.classList.add('show', 'active'); }
+        });
+    });
+
+})();
+</script>