soil-recommendations.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <?php
  2. /**
  3. * dashboard/crop-analysis/soil-test-data/soil-recommendations.php
  4. *
  5. * Soil Specifications / Recommendations page.
  6. * Displays and allows inline editing of soil_specifications reference ranges
  7. * for the current user.
  8. */
  9. require_once __DIR__ . '/../../../config/database.php';
  10. require_once __DIR__ . '/../../../lib/auth.php';
  11. require_once __DIR__ . '/../../../lib/csrf.php';
  12. requireLogin();
  13. // URL params retained for back-link context
  14. $record_id = (int)($_GET['rid'] ?? 0);
  15. $rand_id = $_GET['rand'] ?? '';
  16. $client_id = (int)($_GET['cid'] ?? 0);
  17. // Columns not shown in the editable table
  18. $hiddenCols = ['id', 'modx_user_id', 'crop', 'tec', 'paramag', 'texture', 'gravel', 'colour'];
  19. try {
  20. $pdo = getDBConnection();
  21. $userId = getCurrentUserId();
  22. $stmt = $pdo->prepare('SELECT * FROM soil_specifications WHERE modx_user_id = ?');
  23. $stmt->execute([$userId]);
  24. $specs = $stmt->fetchAll(PDO::FETCH_ASSOC);
  25. $allColumns = $specs ? array_keys($specs[0]) : [];
  26. $visibleColumns = array_values(array_diff($allColumns, $hiddenCols));
  27. } catch (PDOException $e) {
  28. error_log('Database error in soil-recommendations.php: ' . $e->getMessage());
  29. die('Database error occurred');
  30. }
  31. $pageTitle = 'Soil Recommendations';
  32. $siteName = 'Crop Monitor';
  33. $csrfToken = generateCsrfToken();
  34. include __DIR__ . '/../../../layouts/header.php';
  35. include __DIR__ . '/../../../layouts/navbar.php';
  36. include __DIR__ . '/../../../layouts/sidebar.php';
  37. ?>
  38. <div class="container-fluid px-4" id="content">
  39. <div class="d-flex align-items-center justify-content-between mt-4 mb-3">
  40. <h1 class="h3 mb-0">Soil Recommendations</h1>
  41. <?php if ($record_id && $rand_id): ?>
  42. <a href="/dashboard/crop-analysis/soil-test-data/soil-analysis.php?rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>&cid=<?= $client_id ?>"
  43. class="btn btn-outline-secondary btn-sm">
  44. &larr; Back to Analysis
  45. </a>
  46. <?php endif; ?>
  47. </div>
  48. <p class="text-muted mb-3">
  49. Variables used in Soil Analysis recommendation programs.
  50. Click any value in the table to edit it — changes save automatically.
  51. </p>
  52. <?php if (empty($specs)): ?>
  53. <div class="alert alert-info">No soil specification records found for your account.</div>
  54. <?php else: ?>
  55. <div class="card mb-4">
  56. <div class="card-header d-flex justify-content-between align-items-center">
  57. <span>Specification Ranges</span>
  58. <button type="button" class="btn btn-primary btn-sm"
  59. data-bs-toggle="modal" data-bs-target="#addProductModal">
  60. + Add Soil Recommendation
  61. </button>
  62. </div>
  63. <div class="card-body p-0">
  64. <div class="table-responsive">
  65. <table class="table table-sm table-striped table-hover table-bordered mb-0">
  66. <thead class="table-dark">
  67. <tr>
  68. <?php foreach ($visibleColumns as $col): ?>
  69. <th class="text-center text-capitalize text-nowrap px-2">
  70. <?= htmlspecialchars(str_replace('_', ' ', $col), ENT_QUOTES, 'UTF-8') ?>
  71. </th>
  72. <?php endforeach; ?>
  73. </tr>
  74. </thead>
  75. <tbody>
  76. <?php foreach ($specs as $spec): ?>
  77. <tr>
  78. <?php foreach ($visibleColumns as $col):
  79. $raw = $spec[$col] ?? '';
  80. if ($raw === '' || $raw === 'N/A') {
  81. $display = '0.0';
  82. } elseif (is_numeric($raw)) {
  83. $display = number_format((float)$raw, 2, '.', '');
  84. } else {
  85. $display = $raw;
  86. }
  87. ?>
  88. <td class="text-center spec-cell px-2"
  89. contenteditable="true"
  90. data-id="<?= (int)$spec['id'] ?>"
  91. data-col="<?= htmlspecialchars($col, ENT_QUOTES, 'UTF-8') ?>">
  92. <?= htmlspecialchars($display, ENT_QUOTES, 'UTF-8') ?>
  93. </td>
  94. <?php endforeach; ?>
  95. </tr>
  96. <?php endforeach; ?>
  97. </tbody>
  98. </table>
  99. </div>
  100. </div>
  101. </div>
  102. <div id="save-status" class="text-muted small mb-3" style="min-height:1.25rem;"></div>
  103. <?php endif; ?>
  104. </div><!-- /.container-fluid -->
  105. <!-- ── Add Product Modal ─────────────────────────────────────────────────── -->
  106. <div class="modal fade" id="addProductModal" tabindex="-1"
  107. aria-labelledby="addProductModalLabel" aria-hidden="true">
  108. <div class="modal-dialog">
  109. <div class="modal-content">
  110. <div class="modal-header">
  111. <h5 class="modal-title" id="addProductModalLabel">Add New Product</h5>
  112. <button type="button" class="btn-close"
  113. data-bs-dismiss="modal" aria-label="Close"></button>
  114. </div>
  115. <form method="post" action="/controllers/newProductSubmit.php">
  116. <input type="hidden" name="csrf_token"
  117. value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
  118. <div class="modal-body">
  119. <div class="row g-2">
  120. <div class="col-md-8">
  121. <label class="form-label" for="name">Product Name</label>
  122. <input type="text" class="form-control" name="name"
  123. id="name" placeholder="Product Name" required>
  124. </div>
  125. <div class="col-md-4">
  126. <label class="form-label" for="chemical">Chemical Symbol</label>
  127. <input type="text" class="form-control" name="chemical"
  128. id="chemical" placeholder="e.g. CaSO4">
  129. </div>
  130. </div>
  131. <hr class="my-3">
  132. <p class="text-muted small mb-2">Nutrient composition (%)</p>
  133. <div class="row g-2">
  134. <?php
  135. $nutrients = [
  136. 'N' => 'Nitrogen',
  137. 'P' => 'Phosphorus',
  138. 'K' => 'Potassium',
  139. 'Na' => 'Sodium',
  140. 'Ca' => 'Calcium',
  141. 'Mg' => 'Magnesium',
  142. 'B' => 'Boron',
  143. 'Zn' => 'Zinc',
  144. 'Cu' => 'Copper',
  145. 'Mn' => 'Manganese',
  146. 'Fe' => 'Iron',
  147. 'Co' => 'Cobalt',
  148. 'Mo' => 'Molybdenum',
  149. ];
  150. foreach ($nutrients as $key => $label):
  151. ?>
  152. <div class="col-6 col-md-4">
  153. <label class="form-label small" for="field_<?= $key ?>">
  154. <?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?> — <?= htmlspecialchars($key, ENT_QUOTES, 'UTF-8') ?>
  155. </label>
  156. <input type="number" step="any" min="0"
  157. class="form-control form-control-sm"
  158. name="<?= htmlspecialchars($key, ENT_QUOTES, 'UTF-8') ?>"
  159. id="field_<?= $key ?>"
  160. placeholder="0">
  161. </div>
  162. <?php endforeach; ?>
  163. </div>
  164. </div>
  165. <div class="modal-footer">
  166. <button type="button" class="btn btn-secondary"
  167. data-bs-dismiss="modal">Close</button>
  168. <button type="submit" class="btn btn-primary">Save Product</button>
  169. </div>
  170. </form>
  171. </div>
  172. </div>
  173. </div>
  174. <script>
  175. (function () {
  176. const CSRF = <?= json_encode($csrfToken) ?>;
  177. const status = document.getElementById('save-status');
  178. let timer = null;
  179. let active = null;
  180. function setStatus(msg, cls) {
  181. status.textContent = msg;
  182. status.className = 'text-' + cls + ' small mb-3';
  183. }
  184. document.querySelectorAll('.spec-cell').forEach(function (cell) {
  185. cell.addEventListener('focus', function () {
  186. this.style.background = '#d1f0d1';
  187. });
  188. cell.addEventListener('blur', function () {
  189. this.style.background = '';
  190. const el = this;
  191. const id = el.dataset.id;
  192. const col = el.dataset.col;
  193. const val = el.textContent.trim();
  194. clearTimeout(timer);
  195. timer = setTimeout(function () {
  196. active = el;
  197. setStatus('Saving…', 'secondary');
  198. fetch('/controllers/updateSoilSpecification.php', {
  199. method: 'POST',
  200. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  201. body: new URLSearchParams({
  202. csrf_token: CSRF,
  203. spec_id: id,
  204. column: col,
  205. value: val,
  206. }),
  207. })
  208. .then(function (r) { return r.json(); })
  209. .then(function (data) {
  210. if (data.success) {
  211. var d = new Date();
  212. setStatus('Saved — ' + d.toLocaleTimeString(), 'success');
  213. } else {
  214. setStatus('Error: ' + (data.error || 'save failed'), 'danger');
  215. }
  216. })
  217. .catch(function () {
  218. setStatus('Network error — change not saved', 'danger');
  219. });
  220. }, 600);
  221. });
  222. });
  223. })();
  224. </script>
  225. <?php include __DIR__ . '/../../../layouts/footer.php'; ?>