|
@@ -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 / <?= $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>
|