| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- <?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'; ?>
|