dashboard.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <?php
  2. require_once __DIR__ . '/../config/database.php';
  3. require_once __DIR__ . '/../lib/auth.php';
  4. if (session_status() === PHP_SESSION_NONE) {
  5. session_start();
  6. }
  7. requireLogin();
  8. $pageTitle = 'Dashboard';
  9. $siteName = 'Crop Monitor';
  10. include __DIR__ . '/../layouts/header.php';
  11. include __DIR__ . '/../layouts/navbar.php';
  12. ?>
  13. <div id="layoutSidenav">
  14. <div id="layoutSidenav_nav">
  15. <?php include __DIR__ . '/../layouts/sidebar.php'; ?>
  16. </div>
  17. <div id="layoutSidenav_content">
  18. <main>
  19. <div class="container-fluid px-4">
  20. <h1 class="mt-4"><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?></h1>
  21. <ol class="breadcrumb mb-4">
  22. <li class="breadcrumb-item active">Dashboard</li>
  23. </ol>
  24. <!-- ── Summary cards ──────────────────────────────────────── -->
  25. <div class="row">
  26. <div class="col-xl-3 col-sm-6 mb-3">
  27. <div class="card text-white bg-primary o-hidden h-100">
  28. <div class="card-body">
  29. <div class="card-body-icon"><i class="fas fa-fw fa-comments"></i></div>
  30. <div class="me-5">26 New Messages!</div>
  31. </div>
  32. <a class="card-footer text-white clearfix small z-1" href="/dashboard/inbox.php">
  33. <span class="float-start">View Details</span>
  34. <span class="float-end"><i class="fas fa-angle-right"></i></span>
  35. </a>
  36. </div>
  37. </div>
  38. <div class="col-xl-3 col-sm-6 mb-3">
  39. <div class="card text-white bg-warning o-hidden h-100">
  40. <div class="card-body">
  41. <div class="card-body-icon"><i class="fas fa-fw fa-list"></i></div>
  42. <div class="me-5">11 New Tasks!</div>
  43. </div>
  44. <a class="card-footer text-white clearfix small z-1" href="#">
  45. <span class="float-start">View Details</span>
  46. <span class="float-end"><i class="fas fa-angle-right"></i></span>
  47. </a>
  48. </div>
  49. </div>
  50. <div class="col-xl-3 col-sm-6 mb-3">
  51. <div class="card text-white bg-success o-hidden h-100">
  52. <div class="card-body">
  53. <div class="card-body-icon"><i class="fas fa-fw fa-seedling"></i></div>
  54. <div class="me-5">New Soil Tests</div>
  55. </div>
  56. <a class="card-footer text-white clearfix small z-1"
  57. href="/dashboard/crop-analysis/soil-test-data/soil-test-data.php">
  58. <span class="float-start">View Details</span>
  59. <span class="float-end"><i class="fas fa-angle-right"></i></span>
  60. </a>
  61. </div>
  62. </div>
  63. <div class="col-xl-3 col-sm-6 mb-3">
  64. <div class="card text-white bg-danger o-hidden h-100">
  65. <div class="card-body">
  66. <div class="card-body-icon"><i class="fas fa-fw fa-life-ring"></i></div>
  67. <div class="me-5">13 New Tickets!</div>
  68. </div>
  69. <a class="card-footer text-white clearfix small z-1" href="#">
  70. <span class="float-start">View Details</span>
  71. <span class="float-end"><i class="fas fa-angle-right"></i></span>
  72. </a>
  73. </div>
  74. </div>
  75. </div><!-- /row: summary cards -->
  76. <!-- ── Recent Analysis heading ─────────────────────────────── -->
  77. <div class="row mb-3">
  78. <div class="col-md-12">
  79. <h5>Recent Analysis</h5>
  80. </div>
  81. </div>
  82. <!-- ── Weather widget + Calendar ──────────────────────────── -->
  83. <div class="row">
  84. <!-- Weather widget -->
  85. <div class="col-md-7">
  86. <!-- Skeleton shown while loading -->
  87. <div id="weather-loading" class="weather d-flex align-items-center justify-content-center" style="min-height:180px;">
  88. <div class="text-muted small"><span class="spinner-border spinner-border-sm me-2"></span>Loading weather…</div>
  89. </div>
  90. <!-- Populated by JS -->
  91. <div id="weather-widget" class="weather" style="display:none;">
  92. <div class="weather-top">
  93. <div class="weather-top-left">
  94. <div class="degree">
  95. <figure class="icons">
  96. <canvas id="wx-hero-canvas" width="64" height="64"></canvas>
  97. </figure>
  98. <span id="wx-temp">—</span>
  99. <div class="clearfix"></div>
  100. </div>
  101. <p>
  102. <?= date('l') ?>
  103. <label><?= date('j') ?></label><sup><?= date('S') ?></sup>
  104. <?= date('M') ?>
  105. </p>
  106. </div>
  107. <div class="weather-top-right">
  108. <p><i class="fa fa-map-marker"></i> <span id="wx-location">—</span></p>
  109. <label id="wx-condition">—</label>
  110. <div class="small text-muted mt-1">
  111. <span title="Humidity"><i class="fa fa-tint"></i> <span id="wx-humidity">—</span>%</span>
  112. &nbsp;
  113. <span title="Wind"><i class="fa fa-wind"></i> <span id="wx-wind">—</span> km/h</span>
  114. &nbsp;
  115. <span title="Rain now"><i class="fa fa-cloud-rain"></i> <span id="wx-rain">—</span> mm</span>
  116. </div>
  117. </div>
  118. <div class="clearfix"></div>
  119. </div>
  120. <!-- 7-day forecast (future days) -->
  121. <div class="weather-bottom" id="wx-forecast-row">
  122. <!-- filled by JS -->
  123. <div class="clearfix"></div>
  124. </div>
  125. </div>
  126. <!-- Past 7-day rainfall chart -->
  127. <div id="wx-rainfall-card" class="card mt-3" style="display:none;">
  128. <div class="card-header py-1 small fw-bold">Past 7 Days Rainfall (mm)</div>
  129. <div class="card-body py-2">
  130. <canvas id="wx-rainfall-chart" height="80"></canvas>
  131. </div>
  132. </div>
  133. <div id="weather-error" class="alert alert-warning mt-2 small" style="display:none;"></div>
  134. </div><!-- /col: weather -->
  135. <!-- Calendar widget -->
  136. <div class="col-md-5">
  137. <div class="cal1 cal_2">
  138. <div id="clndr"></div>
  139. </div>
  140. <link rel="stylesheet" href="/client-assets/css/clndr.css">
  141. <script src="/client-assets/js/underscore-min.js"></script>
  142. <script src="/client-assets/js/moment-2.2.1.js"></script>
  143. <script src="/client-assets/js/clndr.js"></script>
  144. <script src="/client-assets/js/site.js"></script>
  145. </div><!-- /col: calendar -->
  146. </div><!-- /row: weather + calendar -->
  147. <div class="content-bottom mt-4"></div>
  148. <div class="clearfix"></div>
  149. </div><!-- /container-fluid -->
  150. <?php include __DIR__ . '/../layouts/footer.php'; ?>
  151. <script>
  152. (function () {
  153. 'use strict';
  154. var heroSkycons = null;
  155. var fcSkycons = null;
  156. var rainfallChart = null;
  157. function renderWeather(data) {
  158. // ── Current conditions ───────────────────────────────────────────────
  159. document.getElementById('wx-temp').textContent = data.current.temp + '°';
  160. document.getElementById('wx-condition').textContent = data.current.label;
  161. document.getElementById('wx-location').textContent = data.location;
  162. document.getElementById('wx-humidity').textContent = data.current.humidity;
  163. document.getElementById('wx-wind').textContent = data.current.wind;
  164. document.getElementById('wx-rain').textContent = data.current.rain;
  165. // Hero Skycon
  166. var heroCanvas = document.getElementById('wx-hero-canvas');
  167. if (!heroSkycons) {
  168. heroSkycons = new Skycons({ color: '#1ABC9C' });
  169. }
  170. heroSkycons.set(heroCanvas, data.current.icon);
  171. heroSkycons.play();
  172. // ── Forecast row (future days only, up to 7) ─────────────────────────
  173. var forecastRow = document.getElementById('wx-forecast-row');
  174. var futureDays = data.days.filter(function (d) { return !d.is_past && !d.is_today; }).slice(0, 5);
  175. var fcHtml = '';
  176. if (!fcSkycons) {
  177. fcSkycons = new Skycons({ color: '#999' });
  178. }
  179. futureDays.forEach(function (d, i) {
  180. var cid = 'wx-fc-' + i;
  181. fcHtml +=
  182. '<div class="weather-bottom1">' +
  183. '<div class="weather-head">' +
  184. '<h4>' + d.label + '</h4>' +
  185. '<figure class="icons"><canvas id="' + cid + '" width="58" height="58"></canvas></figure>' +
  186. '<h6>' + d.temp_max + '°</h6>' +
  187. '<div class="bottom-head">' +
  188. '<p>' + d.day_short + '</p>' +
  189. '<p>' + d.day_name + '</p>' +
  190. '</div>' +
  191. '</div>' +
  192. '</div>';
  193. });
  194. fcHtml += '<div class="clearfix"></div>';
  195. forecastRow.innerHTML = fcHtml;
  196. futureDays.forEach(function (d, i) {
  197. var el = document.getElementById('wx-fc-' + i);
  198. if (el) { fcSkycons.set(el, d.icon); }
  199. });
  200. fcSkycons.play();
  201. // ── Past 7-day rainfall chart ─────────────────────────────────────────
  202. var pastDays = data.days.filter(function (d) { return d.is_past; }).slice(-7);
  203. if (pastDays.length > 0) {
  204. var labels = pastDays.map(function (d) { return d.day_name + ' ' + d.day_short; });
  205. var values = pastDays.map(function (d) { return d.rain; });
  206. var ctx = document.getElementById('wx-rainfall-chart').getContext('2d');
  207. if (rainfallChart) { rainfallChart.destroy(); }
  208. rainfallChart = new Chart(ctx, {
  209. type: 'bar',
  210. data: {
  211. labels: labels,
  212. datasets: [{
  213. label: 'Rainfall (mm)',
  214. data: values,
  215. backgroundColor: 'rgba(54, 162, 235, 0.6)',
  216. borderColor: 'rgba(54, 162, 235, 1)',
  217. borderWidth: 1,
  218. }],
  219. },
  220. options: {
  221. responsive: true,
  222. plugins: { legend: { display: false } },
  223. scales: {
  224. y: { beginAtZero: true, ticks: { font: { size: 10 } } },
  225. x: { ticks: { font: { size: 10 } } },
  226. },
  227. },
  228. });
  229. document.getElementById('wx-rainfall-card').style.display = '';
  230. }
  231. // Show widget, hide skeleton
  232. document.getElementById('weather-loading').style.display = 'none';
  233. document.getElementById('weather-widget').style.display = '';
  234. }
  235. function loadWeather() {
  236. fetch('/api/weather.php')
  237. .then(function (r) {
  238. if (!r.ok) { throw new Error('HTTP ' + r.status); }
  239. return r.json();
  240. })
  241. .then(function (data) {
  242. if (data.error) { throw new Error(data.error); }
  243. renderWeather(data);
  244. })
  245. .catch(function (err) {
  246. document.getElementById('weather-loading').style.display = 'none';
  247. var errEl = document.getElementById('weather-error');
  248. errEl.textContent = 'Weather unavailable: ' + err.message;
  249. errEl.style.display = '';
  250. });
  251. }
  252. document.addEventListener('DOMContentLoaded', loadWeather);
  253. })();
  254. </script>