index.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. <?php
  2. /**
  3. * dashboard/consultant/index.php
  4. *
  5. * Consultant overview — shows all clients managed by the logged-in consultant
  6. * with test counts, last activity, and out-of-range nutrient alert badges.
  7. */
  8. require_once __DIR__ . '/../../config/database.php';
  9. require_once __DIR__ . '/../../lib/auth.php';
  10. require_once __DIR__ . '/../../lib/consultant.php';
  11. if (session_status() === PHP_SESSION_NONE) {
  12. session_start();
  13. }
  14. requireLogin();
  15. $pageTitle = 'Consultant Dashboard';
  16. $siteName = 'Crop Monitor';
  17. $activeItem = 'Consultant Dashboard';
  18. $pdo = getDBConnection();
  19. $userId = (int) getCurrentUserId();
  20. $clients = getConsultantClients($pdo, $userId);
  21. // ── Summary totals for the header stat cards ──────────────────────────────────
  22. $totalClients = count($clients);
  23. $totalSoil = array_sum(array_column($clients, 'soil_count'));
  24. $totalPlant = array_sum(array_column($clients, 'plant_count'));
  25. $totalAlerts = array_sum(array_map(fn($c) => $c['alerts']['critical'] + $c['alerts']['watch'], $clients));
  26. $criticalCount = array_sum(array_map(fn($c) => $c['alerts']['critical'], $clients));
  27. include __DIR__ . '/../../layouts/header.php';
  28. include __DIR__ . '/../../layouts/navbar.php';
  29. ?>
  30. <div id="layoutSidenav">
  31. <div id="layoutSidenav_nav">
  32. <?php include __DIR__ . '/../../layouts/sidebar.php'; ?>
  33. </div>
  34. <div id="layoutSidenav_content">
  35. <main>
  36. <div class="container-fluid px-4">
  37. <h1 class="mt-4"><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?></h1>
  38. <ol class="breadcrumb mb-4">
  39. <li class="breadcrumb-item"><a href="/dashboard/dashboard.php">Dashboard</a></li>
  40. <li class="breadcrumb-item active">Consultant Overview</li>
  41. </ol>
  42. <!-- ── Summary stat cards ─────────────────────────────────── -->
  43. <div class="row g-3 mb-4">
  44. <div class="col-xl-3 col-sm-6">
  45. <div class="card border-0 shadow-sm h-100">
  46. <div class="card-body d-flex align-items-center gap-3">
  47. <div class="rounded-circle bg-primary bg-opacity-10 p-3 flex-shrink-0">
  48. <i class="fas fa-users fa-lg text-primary"></i>
  49. </div>
  50. <div>
  51. <div class="fs-2 fw-bold lh-1"><?= $totalClients ?></div>
  52. <div class="text-muted small">Clients</div>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. <div class="col-xl-3 col-sm-6">
  58. <div class="card border-0 shadow-sm h-100">
  59. <div class="card-body d-flex align-items-center gap-3">
  60. <div class="rounded-circle bg-success bg-opacity-10 p-3 flex-shrink-0">
  61. <i class="fas fa-globe-asia fa-lg text-success"></i>
  62. </div>
  63. <div>
  64. <div class="fs-2 fw-bold lh-1"><?= $totalSoil ?></div>
  65. <div class="text-muted small">Soil Tests</div>
  66. </div>
  67. </div>
  68. </div>
  69. </div>
  70. <div class="col-xl-3 col-sm-6">
  71. <div class="card border-0 shadow-sm h-100">
  72. <div class="card-body d-flex align-items-center gap-3">
  73. <div class="rounded-circle bg-info bg-opacity-10 p-3 flex-shrink-0">
  74. <i class="fab fa-pagelines fa-lg text-info"></i>
  75. </div>
  76. <div>
  77. <div class="fs-2 fw-bold lh-1"><?= $totalPlant ?></div>
  78. <div class="text-muted small">Plant Tests</div>
  79. </div>
  80. </div>
  81. </div>
  82. </div>
  83. <div class="col-xl-3 col-sm-6">
  84. <div class="card border-0 shadow-sm h-100">
  85. <div class="card-body d-flex align-items-center gap-3">
  86. <div class="rounded-circle bg-<?= $criticalCount > 0 ? 'danger' : ($totalAlerts > 0 ? 'warning' : 'success') ?> bg-opacity-10 p-3 flex-shrink-0">
  87. <i class="fas fa-exclamation-triangle fa-lg text-<?= $criticalCount > 0 ? 'danger' : ($totalAlerts > 0 ? 'warning' : 'success') ?>"></i>
  88. </div>
  89. <div>
  90. <div class="fs-2 fw-bold lh-1"><?= $totalAlerts ?></div>
  91. <div class="text-muted small">Nutrient Alerts</div>
  92. </div>
  93. </div>
  94. </div>
  95. </div>
  96. </div>
  97. <!-- ── Toolbar: search + filter ───────────────────────────── -->
  98. <div class="row mb-3 g-2 align-items-center">
  99. <div class="col-md-5">
  100. <div class="input-group input-group-sm">
  101. <span class="input-group-text"><i class="fas fa-search"></i></span>
  102. <input type="text"
  103. id="client-search"
  104. class="form-control"
  105. placeholder="Search by client or company…">
  106. </div>
  107. </div>
  108. <div class="col-md-3">
  109. <select id="alert-filter" class="form-select form-select-sm">
  110. <option value="">All clients</option>
  111. <option value="critical">Critical alerts only</option>
  112. <option value="watch">Watch alerts</option>
  113. <option value="ok">No alerts</option>
  114. </select>
  115. </div>
  116. <div class="col-md-4 text-md-end">
  117. <span class="text-muted small" id="client-count-label">
  118. Showing <?= $totalClients ?> client<?= $totalClients !== 1 ? 's' : '' ?>
  119. </span>
  120. </div>
  121. </div>
  122. <!-- ── Client cards grid ──────────────────────────────────── -->
  123. <?php if (empty($clients)): ?>
  124. <div class="alert alert-info">
  125. No clients found for your account. Add a client using the
  126. <a href="/dashboard/crop-analysis/soil-test-data/index.php">Soil Test Data</a> page.
  127. </div>
  128. <?php else: ?>
  129. <div class="row g-3" id="client-grid">
  130. <?php foreach ($clients as $c):
  131. $alerts = $c['alerts'];
  132. $badgeClass = alertBadgeClass($alerts['critical'], $alerts['watch']);
  133. $hasActivity = $c['last_activity'] && $c['last_activity'] !== '1970-01-01';
  134. $alertStatus = $alerts['critical'] > 0 ? 'critical' : ($alerts['watch'] > 0 ? 'watch' : 'ok');
  135. ?>
  136. <div class="col-xl-4 col-lg-6 client-card-col"
  137. data-name="<?= htmlspecialchars(strtolower($c['client'] . ' ' . $c['company']), ENT_QUOTES, 'UTF-8') ?>"
  138. data-alert="<?= $alertStatus ?>">
  139. <div class="card h-100 shadow-sm border-0 border-start border-4 border-<?= $badgeClass ?>">
  140. <div class="card-body">
  141. <!-- Client name + alert badge -->
  142. <div class="d-flex justify-content-between align-items-start mb-2">
  143. <div>
  144. <h6 class="card-title mb-0 fw-bold">
  145. <?= htmlspecialchars($c['client'] ?: '—', ENT_QUOTES, 'UTF-8') ?>
  146. </h6>
  147. <div class="text-muted small">
  148. <?= htmlspecialchars($c['company'] ?: '', ENT_QUOTES, 'UTF-8') ?>
  149. </div>
  150. </div>
  151. <?php if ($alerts['critical'] > 0 || $alerts['watch'] > 0): ?>
  152. <span class="badge bg-<?= $badgeClass ?> ms-2 flex-shrink-0">
  153. <?php if ($alerts['critical'] > 0): ?>
  154. <i class="fas fa-exclamation-circle me-1"></i><?= $alerts['critical'] ?> critical
  155. <?php else: ?>
  156. <i class="fas fa-exclamation-triangle me-1"></i><?= $alerts['watch'] ?> watch
  157. <?php endif; ?>
  158. </span>
  159. <?php else: ?>
  160. <span class="badge bg-success ms-2 flex-shrink-0">
  161. <i class="fas fa-check me-1"></i>All clear
  162. </span>
  163. <?php endif; ?>
  164. </div>
  165. <!-- Location -->
  166. <?php if ($c['address'] || $c['state_postcode']): ?>
  167. <div class="text-muted small mb-2">
  168. <i class="fas fa-map-marker-alt me-1"></i>
  169. <?= htmlspecialchars(trim($c['address'] . ' ' . $c['state_postcode']), ENT_QUOTES, 'UTF-8') ?>
  170. </div>
  171. <?php endif; ?>
  172. <!-- Test count badges -->
  173. <div class="d-flex flex-wrap gap-2 mb-3">
  174. <span class="badge rounded-pill bg-success bg-opacity-15 text-success border border-success border-opacity-25">
  175. <i class="fas fa-globe-asia me-1"></i>
  176. <?= (int) $c['soil_count'] ?> Soil
  177. </span>
  178. <span class="badge rounded-pill bg-info bg-opacity-15 text-info border border-info border-opacity-25">
  179. <i class="fab fa-pagelines me-1"></i>
  180. <?= (int) $c['plant_count'] ?> Plant
  181. </span>
  182. <span class="badge rounded-pill bg-primary bg-opacity-15 text-primary border border-primary border-opacity-25">
  183. <i class="fas fa-tint me-1"></i>
  184. <?= (int) $c['water_count'] ?> Water
  185. </span>
  186. </div>
  187. <!-- Alert summary (if any) -->
  188. <?php if (!empty($alerts['critical']) || !empty($alerts['watch'])): ?>
  189. <div class="d-flex gap-2 mb-3">
  190. <?php if ($alerts['critical'] > 0): ?>
  191. <div class="flex-fill rounded p-2 bg-danger bg-opacity-10 text-center">
  192. <div class="fw-bold text-danger"><?= $alerts['critical'] ?></div>
  193. <div class="text-danger" style="font-size:.7rem">CRITICAL</div>
  194. </div>
  195. <?php endif; ?>
  196. <?php if ($alerts['watch'] > 0): ?>
  197. <div class="flex-fill rounded p-2 bg-warning bg-opacity-10 text-center">
  198. <div class="fw-bold text-warning"><?= $alerts['watch'] ?></div>
  199. <div class="text-warning" style="font-size:.7rem">WATCH</div>
  200. </div>
  201. <?php endif; ?>
  202. </div>
  203. <?php endif; ?>
  204. </div>
  205. <!-- Card footer: last activity + action links -->
  206. <div class="card-footer bg-transparent border-top-0 pt-0 pb-3 px-3">
  207. <div class="d-flex justify-content-between align-items-center">
  208. <span class="text-muted" style="font-size:.75rem">
  209. <i class="fas fa-clock me-1"></i>
  210. Last test: <?= fmtDate($c['last_activity']) ?>
  211. </span>
  212. <a href="/dashboard/consultant/client.php?cid=<?= (int) $c['id'] ?>"
  213. class="btn btn-sm btn-outline-secondary">
  214. View <i class="fas fa-arrow-right ms-1"></i>
  215. </a>
  216. </div>
  217. </div>
  218. </div>
  219. </div>
  220. <?php endforeach; ?>
  221. </div>
  222. <?php endif; ?>
  223. </div>
  224. </main>
  225. </div>
  226. </div>
  227. <script>
  228. (function () {
  229. const searchInput = document.getElementById('client-search');
  230. const alertFilter = document.getElementById('alert-filter');
  231. const grid = document.getElementById('client-grid');
  232. const countLabel = document.getElementById('client-count-label');
  233. const cards = grid ? Array.from(grid.querySelectorAll('.client-card-col')) : [];
  234. function applyFilters() {
  235. const query = searchInput.value.toLowerCase().trim();
  236. const status = alertFilter.value;
  237. let visible = 0;
  238. cards.forEach(card => {
  239. const nameMatch = !query || card.dataset.name.includes(query);
  240. const alertMatch = !status || card.dataset.alert === status;
  241. const show = nameMatch && alertMatch;
  242. card.style.display = show ? '' : 'none';
  243. if (show) visible++;
  244. });
  245. if (countLabel) {
  246. countLabel.textContent = `Showing ${visible} client${visible !== 1 ? 's' : ''}`;
  247. }
  248. }
  249. if (searchInput) searchInput.addEventListener('input', applyFilters);
  250. if (alertFilter) alertFilter.addEventListener('change', applyFilters);
  251. })();
  252. </script>
  253. <?php include __DIR__ . '/../../layouts/footer.php'; ?>