soil-recommendations.php 10 KB

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