Explorar o código

Recommendations

Benjamin Harris hai 2 meses
pai
achega
2ec58c6c66

+ 2 - 1
.claude/settings.local.json

@@ -1,7 +1,8 @@
 {
   "permissions": {
     "allow": [
-      "WebFetch(domain:raw.githubusercontent.com)"
+      "WebFetch(domain:raw.githubusercontent.com)",
+      "Bash(find /f/GIT_REPO/crop_monitor -name .env* -o -name *.env -o -name config.php)"
     ]
   }
 }

+ 8 - 17
.htaccess

@@ -14,34 +14,25 @@ RewriteBase /
 SetEnv TZ Australia/Hobart
 Options +SymLinksIfOwnerMatch
 
-# ── Allow .well-known for SSL/ACME challenges ──────────────────────────────
-RewriteRule ^\.well-known/ - [L]
-
 <FilesMatch "(?i)\.(tpl|ini|log)$">
 	Require all denied
 </FilesMatch>
 
 # Allow Robots.txt to pass through
 RewriteRule ^robots.txt - [L]
+RewriteCond %{REQUEST_URI} !^(/\.well-known)
 
+RewriteCond %{QUERY_STRING} ^route=common/home$
 RewriteCond %{REQUEST_METHOD} !^POST$
-RewriteRule ^index\.php$ / [R=301,L]
-
-
-Header set X-Frame-Options "SAMEORIGIN"
-Header set X-Content-Type-Options "nosniff"
-Header set Referrer-Policy "no-referrer"
-Header set X-Robots-Tag "index, follow"
+RewriteRule ^index\.php$ http://%{HTTP_HOST}? [R=301,L]
 
 # ── Block all other dotfiles ───────────────────────────────────────────────
-#RewriteRule (?:^|/)\. - [F,L]
+RewriteRule (?:^|/)\. - [F,L]
 
 # ── www → non-www (301) ────────────────────────────────────────────────────
-#RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
-#RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
+RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
+RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
 
 # ── Pass existing .php files directly (prevent vhost modX routing) ─────────
-RewriteCond %{REQUEST_FILENAME} !-f
-RewriteCond %{REQUEST_FILENAME} !-d
-RewriteCond %{REQUEST_URI} !.*\.(ico|gif|jpg|jpeg|png|js|css)
-RewriteRule ^([^?]*) index.php?_route_=$1 [L,QSA]
+RewriteCond %{REQUEST_FILENAME} -f
+RewriteRule \.php$ - [L]

+ 0 - 0
client-assets/css/dashboard-2021.css → client-assets/css/dashboard.css


+ 97 - 0
controllers/updateSoilSpecification.php

@@ -0,0 +1,97 @@
+<?php
+/**
+ * controllers/updateSoilSpecification.php
+ *
+ * AJAX POST handler: updates a single cell in soil_specifications.
+ * Returns JSON { success: true } or { success: false, error: "..." }.
+ */
+
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../lib/auth.php';
+require_once __DIR__ . '/../lib/csrf.php';
+
+header('Content-Type: application/json');
+
+if (!isLoggedIn()) {
+    http_response_code(401);
+    echo json_encode(['success' => false, 'error' => 'Not authenticated']);
+    exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+    http_response_code(405);
+    echo json_encode(['success' => false, 'error' => 'Method not allowed']);
+    exit;
+}
+
+if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
+    http_response_code(403);
+    echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
+    exit;
+}
+
+$specId = (int)($_POST['spec_id'] ?? 0);
+$column = (string)($_POST['column'] ?? '');
+$value  = trim((string)($_POST['value'] ?? ''));
+
+if (!$specId || $column === '') {
+    http_response_code(400);
+    echo json_encode(['success' => false, 'error' => 'Missing required fields']);
+    exit;
+}
+
+// Allowlist — only columns present in soil_specifications (excluding system cols)
+$allowed = [
+    'soil_type',
+    'cec', 'NO3_N', 'NH3_N',
+    'p_mehlick', 'p_bray2', 'p_morgan', 'p_colwell',
+    'k_morgan', 'ca_morgan', 'mg_morgan', 'na_morgan',
+    'ch_h2o', 'ocarbon', 'omatter', 'fe', 'ec',
+    'ph_cacl2', 'ph_h2o', 's_morgan',
+    'b_cacl2', 'mn_dtpa', 'zn_dtpa', 'fe_dtpa', 'cu_dtpa',
+    'al', 'sl_cacl2', 'm_dtpa', 'co_dtpa', 'se',
+    'ca_mehlick3', 'mg_mehlick3', 'k_mehlick3', 'na_mehlick3', 'al_mehlick3',
+    'BS_ca2', 'BS_ca_ppm', 'BS_mg2', 'BS_mg_ppm',
+    'BS_k', 'BS_k_ppm', 'BS_na', 'BS_na_ppm',
+    'BS_al3', 'BS_ob', 'BS_h',
+    'ca_mg_ratio', 'c_n_ratio',
+    // spec-range columns
+    'ca_ppm_min', 'ca_ppm_max', 'mg_ppm_min', 'mg_ppm_max',
+    'k_ppm_min',  'k_ppm_max',  'na_ppm_min', 'na_ppm_max',
+    'cabs_min',   'cabs_max',   'mgbs_min',   'mgbs_max',
+    'kbs_min',    'kbs_max',    'nabs_min',   'nabs_max',
+    'ob_rec', 'h_rec', 'ca_mg_ratio', 'c_n_ratio', 'ph',
+];
+
+if (!in_array($column, $allowed, true)) {
+    http_response_code(400);
+    echo json_encode(['success' => false, 'error' => 'Column not editable']);
+    exit;
+}
+
+try {
+    $pdo    = getDBConnection();
+    $userId = getCurrentUserId();
+
+    // The backtick-quoted column name is safe because it is validated against the allowlist above.
+    $stmt = $pdo->prepare("UPDATE soil_specifications SET `{$column}` = ? WHERE id = ? AND modx_user_id = ?");
+    $stmt->execute([$value, $specId, $userId]);
+
+    if ($stmt->rowCount() === 0) {
+        http_response_code(404);
+        echo json_encode(['success' => false, 'error' => 'Record not found']);
+        exit;
+    }
+
+    echo json_encode(['success' => true]);
+
+} catch (PDOException $e) {
+    error_log('DB error in updateSoilSpecification.php: ' . $e->getMessage());
+    http_response_code(500);
+    echo json_encode(['success' => false, 'error' => 'Database error']);
+}
+exit;

+ 246 - 8
dashboard/crop-analysis/soil-test-data/soil-recommendations.php

@@ -1,17 +1,255 @@
 <?php
 /**
- * soil-recommendations.php (soil-test-data)
+ * dashboard/crop-analysis/soil-test-data/soil-recommendations.php
  *
- * Redirects to the main soil recommendations page under client-settings.
+ * Soil Specifications / Recommendations page.
+ * Displays and allows inline editing of soil_specifications reference ranges
+ * for the current user.
  */
 
-if (session_status() === PHP_SESSION_NONE) {
-    session_start();
-}
-
+require_once __DIR__ . '/../../../config/database.php';
 require_once __DIR__ . '/../../../lib/auth.php';
+require_once __DIR__ . '/../../../lib/csrf.php';
 
 requireLogin();
 
-header('Location: /dashboard/client-settings/soil-recommendations.php');
-exit;
+// URL params retained for back-link context
+$record_id = (int)($_GET['rid']  ?? 0);
+$rand_id   = $_GET['rand'] ?? '';
+$client_id = (int)($_GET['cid']  ?? 0);
+
+// Columns not shown in the editable table
+$hiddenCols = ['id', 'modx_user_id', 'crop', 'tec', 'paramag', 'texture', 'gravel', 'colour'];
+
+try {
+    $pdo    = getDBConnection();
+    $userId = getCurrentUserId();
+
+    $stmt = $pdo->prepare('SELECT * FROM soil_specifications WHERE modx_user_id = ?');
+    $stmt->execute([$userId]);
+    $specs = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+    $allColumns     = $specs ? array_keys($specs[0]) : [];
+    $visibleColumns = array_values(array_diff($allColumns, $hiddenCols));
+
+} catch (PDOException $e) {
+    error_log('Database error in soil-recommendations.php: ' . $e->getMessage());
+    die('Database error occurred');
+}
+
+$pageTitle = 'Soil Recommendations';
+$siteName  = 'Crop Monitor';
+$csrfToken = generateCsrfToken();
+
+include __DIR__ . '/../../../layouts/header.php';
+include __DIR__ . '/../../../layouts/navbar.php';
+include __DIR__ . '/../../../layouts/sidebar.php';
+?>
+
+<div class="container-fluid px-4" id="content">
+
+    <div class="d-flex align-items-center justify-content-between mt-4 mb-3">
+        <h1 class="h3 mb-0">Soil Recommendations</h1>
+        <?php if ($record_id && $rand_id): ?>
+        <a href="/dashboard/crop-analysis/soil-test-data/soil-analysis.php?rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>&cid=<?= $client_id ?>"
+           class="btn btn-outline-secondary btn-sm">
+            &larr; Back to Analysis
+        </a>
+        <?php endif; ?>
+    </div>
+
+    <p class="text-muted mb-3">
+        Variables used in Soil Analysis recommendation programs.
+        Click any value in the table to edit it — changes save automatically.
+    </p>
+
+    <?php if (empty($specs)): ?>
+    <div class="alert alert-info">No soil specification records found for your account.</div>
+    <?php else: ?>
+
+    <div class="card mb-4">
+        <div class="card-header d-flex justify-content-between align-items-center">
+            <span>Specification Ranges</span>
+            <button type="button" class="btn btn-primary btn-sm"
+                    data-bs-toggle="modal" data-bs-target="#addProductModal">
+                + Add Soil Recommendation
+            </button>
+        </div>
+        <div class="card-body p-0">
+            <div class="table-responsive">
+                <table class="table table-sm table-striped table-hover table-bordered mb-0">
+                    <thead class="table-dark">
+                        <tr>
+                            <?php foreach ($visibleColumns as $col): ?>
+                            <th class="text-center text-capitalize text-nowrap px-2">
+                                <?= htmlspecialchars(str_replace('_', ' ', $col), ENT_QUOTES, 'UTF-8') ?>
+                            </th>
+                            <?php endforeach; ?>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php foreach ($specs as $spec): ?>
+                        <tr>
+                            <?php foreach ($visibleColumns as $col):
+                                $raw = $spec[$col] ?? '';
+                                if ($raw === '' || $raw === 'N/A') {
+                                    $display = '0.0';
+                                } elseif (is_numeric($raw)) {
+                                    $display = number_format((float)$raw, 2, '.', '');
+                                } else {
+                                    $display = $raw;
+                                }
+                            ?>
+                            <td class="text-center spec-cell px-2"
+                                contenteditable="true"
+                                data-id="<?= (int)$spec['id'] ?>"
+                                data-col="<?= htmlspecialchars($col, ENT_QUOTES, 'UTF-8') ?>">
+                                <?= htmlspecialchars($display, ENT_QUOTES, 'UTF-8') ?>
+                            </td>
+                            <?php endforeach; ?>
+                        </tr>
+                        <?php endforeach; ?>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+
+    <div id="save-status" class="text-muted small mb-3" style="min-height:1.25rem;"></div>
+
+    <?php endif; ?>
+
+</div><!-- /.container-fluid -->
+
+
+<!-- ── Add Product Modal ─────────────────────────────────────────────────── -->
+<div class="modal fade" id="addProductModal" tabindex="-1"
+     aria-labelledby="addProductModalLabel" aria-hidden="true">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="addProductModalLabel">Add New Product</h5>
+                <button type="button" class="btn-close"
+                        data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <form method="post" action="/controllers/newProductSubmit.php">
+                <input type="hidden" name="csrf_token"
+                       value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
+                <div class="modal-body">
+                    <div class="row g-2">
+                        <div class="col-md-8">
+                            <label class="form-label" for="name">Product Name</label>
+                            <input type="text" class="form-control" name="name"
+                                   id="name" placeholder="Product Name" required>
+                        </div>
+                        <div class="col-md-4">
+                            <label class="form-label" for="chemical">Chemical Symbol</label>
+                            <input type="text" class="form-control" name="chemical"
+                                   id="chemical" placeholder="e.g. CaSO4">
+                        </div>
+                    </div>
+
+                    <hr class="my-3">
+                    <p class="text-muted small mb-2">Nutrient composition (%)</p>
+
+                    <div class="row g-2">
+                        <?php
+                        $nutrients = [
+                            'N'  => 'Nitrogen',
+                            'P'  => 'Phosphorus',
+                            'K'  => 'Potassium',
+                            'Na' => 'Sodium',
+                            'Ca' => 'Calcium',
+                            'Mg' => 'Magnesium',
+                            'B'  => 'Boron',
+                            'Zn' => 'Zinc',
+                            'Cu' => 'Copper',
+                            'Mn' => 'Manganese',
+                            'Fe' => 'Iron',
+                            'Co' => 'Cobalt',
+                            'Mo' => 'Molybdenum',
+                        ];
+                        foreach ($nutrients as $key => $label):
+                        ?>
+                        <div class="col-6 col-md-4">
+                            <label class="form-label small" for="field_<?= $key ?>">
+                                <?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?> — <?= htmlspecialchars($key, ENT_QUOTES, 'UTF-8') ?>
+                            </label>
+                            <input type="number" step="any" min="0"
+                                   class="form-control form-control-sm"
+                                   name="<?= htmlspecialchars($key, ENT_QUOTES, 'UTF-8') ?>"
+                                   id="field_<?= $key ?>"
+                                   placeholder="0">
+                        </div>
+                        <?php endforeach; ?>
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-secondary"
+                            data-bs-dismiss="modal">Close</button>
+                    <button type="submit" class="btn btn-primary">Save Product</button>
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
+
+
+<script>
+(function () {
+    const CSRF   = <?= json_encode($csrfToken) ?>;
+    const status = document.getElementById('save-status');
+    let   timer  = null;
+    let   active = null;
+
+    function setStatus(msg, cls) {
+        status.textContent = msg;
+        status.className   = 'text-' + cls + ' small mb-3';
+    }
+
+    document.querySelectorAll('.spec-cell').forEach(function (cell) {
+        cell.addEventListener('focus', function () {
+            this.style.background = '#d1f0d1';
+        });
+
+        cell.addEventListener('blur', function () {
+            this.style.background = '';
+            const el  = this;
+            const id  = el.dataset.id;
+            const col = el.dataset.col;
+            const val = el.textContent.trim();
+
+            clearTimeout(timer);
+            timer = setTimeout(function () {
+                active = el;
+                setStatus('Saving…', 'secondary');
+
+                fetch('/controllers/updateSoilSpecification.php', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+                    body: new URLSearchParams({
+                        csrf_token: CSRF,
+                        spec_id:    id,
+                        column:     col,
+                        value:      val,
+                    }),
+                })
+                .then(function (r) { return r.json(); })
+                .then(function (data) {
+                    if (data.success) {
+                        var d = new Date();
+                        setStatus('Saved — ' + d.toLocaleTimeString(), 'success');
+                    } else {
+                        setStatus('Error: ' + (data.error || 'save failed'), 'danger');
+                    }
+                })
+                .catch(function () {
+                    setStatus('Network error — change not saved', 'danger');
+                });
+            }, 600);
+        });
+    });
+})();
+</script>
+
+<?php include __DIR__ . '/../../../layouts/footer.php'; ?>

+ 2 - 2
layouts/header.php

@@ -17,11 +17,11 @@
         <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.css" integrity="sha256-PF6MatZtiJ8/c9O9HQ8uSUXr++R9KBYu4gbNG5511WE=" crossorigin="anonymous" rel="stylesheet" type="text/css"  />
         <link type="text/css" href="/client-assets/weather-icons/css/weather-icons.min.css?version=1.16" rel="stylesheet" type="text/css" />
         <link href="https://cdnjs.cloudflare.com/ajax/libs/magnific-popup.js/1.1.0/magnific-popup.css" rel="stylesheet" type="text/css" />
-        <link href="/client-assets/css/dashboard-2021.css" rel="stylesheet" type="text/css" />
+        <link href="/client-assets/css/dashboard.css" rel="stylesheet" type="text/css" />
         <link href="/client-assets/home/css/graphing.css" rel="stylesheet" type="text/css" media="screen" />
         <link href="/client-assets/home/css/alux.min.css" rel="stylesheet" type="text/css" media="screen" />
 
-        <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
+        <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
         <script type="text/javascript" src="https://use.fontawesome.com/1e2844bb90.js"></script>
         <script src="https://unpkg.com/gijgo@1.9.11/js/gijgo.min.js" type="text/javascript"></script>
         <link href="https://unpkg.com/gijgo@1.9.11/css/gijgo.min.css" rel="stylesheet" type="text/css" />