summaryrefslogtreecommitdiff
path: root/app/views/dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'app/views/dashboard')
-rw-r--r--app/views/dashboard/_nutrient_measurements.html.erb22
-rw-r--r--app/views/dashboard/_nutrient_measurements_table.html.erb18
-rw-r--r--app/views/dashboard/_nutrient_profile_allocator.html.erb187
-rw-r--r--app/views/dashboard/_nutrient_target_table.html.erb74
-rw-r--r--app/views/dashboard/_target_table.html.erb3
-rw-r--r--app/views/dashboard/index.html.erb4
6 files changed, 95 insertions, 213 deletions
diff --git a/app/views/dashboard/_nutrient_measurements.html.erb b/app/views/dashboard/_nutrient_measurements.html.erb
deleted file mode 100644
index bc63a60..0000000
--- a/app/views/dashboard/_nutrient_measurements.html.erb
+++ /dev/null
@@ -1,22 +0,0 @@
-<div class="card shadow mb-4">
- <div class="card-header d-flex justify-content-between align-items-center">
- <h5 class="mb-0">Nutrient Measurements</h5>
- <div class="btn-group">
- <%#= link_to "Add new measurement", editor_rafts_path, class: "btn btn-sm btn-primary" %>
- <%#= link_to "View all", editor_rafts_path, class: "btn btn-sm btn-secondary" %>
- </div>
- </div>
-
- <div class="card-body p-0">
- <div class="container mb-3">
- <%= line_chart @npk_measurement_data,
- title: "NPK",
- ytitle: "Concentration (mg/L)" %>
- </div>
- <div class="container mb-3">
- <%= line_chart @ammonium_measurement_data,
- title: "Ammonium",
- ytitle: "Concentration (mg/L)" %>
- </div>
- </div>
-</div>
diff --git a/app/views/dashboard/_nutrient_measurements_table.html.erb b/app/views/dashboard/_nutrient_measurements_table.html.erb
new file mode 100644
index 0000000..82ea6ff
--- /dev/null
+++ b/app/views/dashboard/_nutrient_measurements_table.html.erb
@@ -0,0 +1,18 @@
+<div class="card shadow my-3">
+ <div class="card-header d-flex justify-content-between align-items-center">
+ <h4 class="mb-0">Relevé des Nutriments</h4>
+ <div class="btn-group">
+ <%= link_to "Ajouter un relevé", new_nutrient_measurement_path, class: "btn btn-sm btn-primary" %>
+ <%= link_to "Liste des relevés", nutrient_measurements_path, class: "btn btn-sm btn-secondary" %>
+ </div>
+ </div>
+
+ <div class="card-body">
+ <%= line_chart @npk_measurement_data,
+ title: "NPK",
+ ytitle: "Concentration (mg/L)" %>
+ <%= line_chart @ammonium_measurement_data,
+ title: "Ammonium",
+ ytitle: "Concentration (mg/L)" %>
+ </div>
+</div>
diff --git a/app/views/dashboard/_nutrient_profile_allocator.html.erb b/app/views/dashboard/_nutrient_profile_allocator.html.erb
deleted file mode 100644
index d402ace..0000000
--- a/app/views/dashboard/_nutrient_profile_allocator.html.erb
+++ /dev/null
@@ -1,187 +0,0 @@
-<%# Props: nutrient_profiles: ActiveRecord::Relation<NutrientProfile> %>
-<%# Fallback if controller didn't set @nutrient_profiles yet %>
-<% profiles = (local_assigns[:nutrient_profiles] || []).presence || [] %>
-
-<%# We'll render a form purely for structure (no real submit yet) %>
-<%= form_with url: "#", method: :post, local: true, html: { id: "np-mix-form", "data-controller": "np-mix" } do %>
- <div class="card shadow">
- <div class="card-body">
-
- <div class="d-flex justify-content-between align-items-center mb-2">
- <div class="small text-muted">
- Choisissez des <strong>profils de croissance</strong> et répartissez-les pour totaliser <strong>100%</strong>.
- </div>
- <div>
- Somme&nbsp;: <span id="np-mix-sum" class="badge bg-secondary">0%</span>
- </div>
- </div>
-
- <div id="np-mix-rows" class="vstack gap-2">
- <%# Rows are injected by JS from the template below, including defaults %>
- </div>
-
- <div class="mt-3 d-flex gap-2">
- <button type="button" class="btn btn-outline-primary" id="np-mix-add">
- + Ajouter un profil
- </button>
-
- <%# Placeholder "save" button for later backend wiring; disabled until total == 100 %>
- <button type="submit" class="btn btn-success ms-auto" id="np-mix-save" disabled>
- Enregistrer (à venir)
- </button>
- </div>
- </div>
- </div>
-
- <%# --- Hidden template for a single row --- %>
- <template id="np-mix-row-template">
- <div class="np-mix-row d-flex align-items-center gap-2 border rounded p-2">
- <button type="button" class="btn btn-outline-danger btn-sm np-mix-delete" aria-label="Supprimer la ligne">
- Suppr.
- </button>
-
- <div class="flex-grow-1">
- <select name="mix[items][][profile_id]" class="form-select form-select-sm np-mix-select" required>
- <% if profiles.any? %>
- <% profiles.each do |p| %>
- <option value="<%= p.id %>"><%= p.name %></option>
- <% end %>
- <% else %>
- <%# If no collection provided yet, at least show placeholders to demo the UI %>
- <option value="">-- Sélectionner un profil --</option>
- <option value="gen-croissance">Générique croissance</option>
- <option value="tomate-cycle">Tomate (cycle entier)</option>
- <option value="gen-floraison">Générique floraison</option>
- <% end %>
- </select>
- </div>
-
- <div class="input-group input-group-sm" style="max-width: 140px;">
- <input type="number"
- name="mix[items][][percentage]"
- class="form-control text-end np-mix-percent"
- min="0" max="100" step="1" value="0" required>
- <span class="input-group-text">%</span>
- </div>
- </div>
- </template>
-
- <%# --- Defaults to inject on load --- %>
- <script type="application/json" id="np-mix-defaults">
- {
- "items": [
- { "name": "G\u00E9n\u00E9rique croissance", "percent": 50 },
- { "name": "Tomate (cycle entier)", "percent": 30 },
- { "name": "G\u00E9n\u00E9rique floraison", "percent": 20 }
- ]
- }
- </script>
-
- <%# --- Tiny inline JS to keep this self-contained (no Stimulus required) --- %>
- <script>
- (() => {
- const rowsContainer = document.getElementById('np-mix-rows');
- const addBtn = document.getElementById('np-mix-add');
- const saveBtn = document.getElementById('np-mix-save');
- const sumBadge = document.getElementById('np-mix-sum');
- const tpl = document.getElementById('np-mix-row-template');
- const defaultsJSON = document.getElementById('np-mix-defaults')?.textContent || "{}";
- const defaults = JSON.parse(defaultsJSON);
-
- function currentSum() {
- return Array.from(rowsContainer.querySelectorAll('.np-mix-percent'))
- .reduce((acc, el) => acc + (parseFloat(el.value) || 0), 0);
- }
-
- function refreshSum() {
- const sum = currentSum();
- sumBadge.textContent = `${sum}%`;
- sumBadge.classList.remove('bg-secondary','bg-danger','bg-success','bg-warning');
-
- if (sum === 100) {
- sumBadge.classList.add('bg-success');
- saveBtn?.removeAttribute('disabled');
- } else if (sum > 100) {
- sumBadge.classList.add('bg-danger');
- saveBtn?.setAttribute('disabled', 'disabled');
- } else {
- sumBadge.classList.add('bg-warning');
- saveBtn?.setAttribute('disabled', 'disabled');
- }
- }
-
- function setSelectByName(selectEl, targetName) {
- // Try to match by visible name; fall back to first option.
- const options = Array.from(selectEl.options);
- const found = options.find(o => o.text.trim().toLowerCase() === String(targetName || '').trim().toLowerCase());
- if (found) {
- selectEl.value = found.value;
- }
- }
-
- function installRow({ name = null, percent = 0 } = {}) {
- const node = tpl.content.firstElementChild.cloneNode(true);
-
- // Hook up events
- node.querySelector('.np-mix-delete').addEventListener('click', () => {
- node.remove();
- refreshSum();
- });
-
- const selectEl = node.querySelector('.np-mix-select');
- const percentEl = node.querySelector('.np-mix-percent');
-
- // Default selection (by name) and percent
- if (name) setSelectByName(selectEl, name);
- percentEl.value = percent;
-
- // Input events
- selectEl.addEventListener('change', () => { /* reserved for later linkage */ });
- percentEl.addEventListener('input', () => {
- // Clamp and refresh
- let v = parseFloat(percentEl.value);
- if (isNaN(v)) v = 0;
- v = Math.max(0, Math.min(100, Math.round(v)));
- percentEl.value = v;
- refreshSum();
- });
-
- rowsContainer.appendChild(node);
- }
-
- // Init with three defaults
- const items = (defaults && defaults.items) ? defaults.items : [];
- if (items.length) {
- items.forEach(it => installRow({ name: it.name, percent: it.percent }));
- } else {
- // Fallback: create three blank rows
- for (let i = 0; i < 3; i++) installRow();
- }
- refreshSum();
-
- // Add new blank row
- addBtn.addEventListener('click', () => {
- installRow({ name: null, percent: 0 });
- refreshSum();
- // Scroll to the new row on mobile for better UX
- rowsContainer.lastElementChild?.scrollIntoView({ behavior: 'smooth', block: 'center' });
- });
-
- // Prevent real submit for now (frontend only)
- document.getElementById('np-mix-form')?.addEventListener('submit', (e) => {
- e.preventDefault();
- // Later: wire to Turbo/JSON post. For now just a gentle nudge.
- saveBtn.textContent = 'Enregistrer (backend à venir)';
- saveBtn.blur();
- });
- })();
- </script>
-
- <style>
- /* Small touch targets & tidy spacing on mobile */
- @media (max-width: 576px) {
- .np-mix-row { padding: .5rem; }
- .np-mix-row .btn { padding: .25rem .5rem; }
- }
- </style>
-<% end %>
diff --git a/app/views/dashboard/_nutrient_target_table.html.erb b/app/views/dashboard/_nutrient_target_table.html.erb
new file mode 100644
index 0000000..7cf294c
--- /dev/null
+++ b/app/views/dashboard/_nutrient_target_table.html.erb
@@ -0,0 +1,74 @@
+<div class="card shadow my-3">
+ <div class="card-header d-flex justify-content-between align-items-center">
+ <h4 class="mb-0">Complémentation</h4>
+ <div class="btn-group">
+ <%= link_to "Nouvelle cible", new_target_path, class: "btn btn-sm btn-primary" %>
+ <%= link_to "Voir la recette", root_path, class: "btn btn-sm btn-secondary" %>
+ </div>
+ </div>
+
+ <div class="card-body p-0">
+ <div class="table-responsive">
+ <table class="table table-sm table-striped table-hover align-middle mb-0">
+ <thead class="table-light">
+ <tr>
+ <th>Nutriment</th>
+ <th class="text-end">Relevé</th>
+ <th class="text-end">Cible</th>
+ <th class="text-end">Delta</th>
+ </tr>
+ </thead>
+ <tbody>
+ <% wr = @weighted || {} %>
+ <% lm = @latest_measurements || {} %>
+
+ <% keys = (wr.keys + lm.keys).map(&:to_s).uniq.sort %>
+ <% keys.each do |nut| %>
+ <% measured = lm[nut] %>
+ <% target = wr[nut] %>
+ <% delta = (measured.to_f - target.to_f) if measured || target %>
+ <tr>
+ <td class="fw-semibold"><%= nut.upcase %></td>
+
+ <td class="text-end">
+ <% if measured.nil? %>
+ <span class="text-muted">—</span>
+ <% else %>
+ <%= number_with_precision(measured, precision: 2) %>
+ <% end %>
+ </td>
+
+ <td class="text-end">
+ <% if target.nil? %>
+ <span class="text-muted">—</span>
+ <% else %>
+ <%= number_with_precision(target, precision: 2) %>
+ <% end %>
+ </td>
+
+ <td class="text-end">
+ <% if measured.nil? && target.nil? %>
+ <span class="text-muted">—</span>
+ <% else %>
+ <% badge =
+ if delta.nil?
+ "text-bg-secondary"
+ elsif delta.abs <= 0.01
+ "text-bg-success"
+ elsif delta > 0
+ "text-bg-warning"
+ else
+ "text-bg-danger"
+ end %>
+ <span class="badge <%= badge %>">
+ <%= number_with_precision(delta.to_f, precision: 2) %>
+ </span>
+ <% end %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+ </div>
+ </div>
+</div>
diff --git a/app/views/dashboard/_target_table.html.erb b/app/views/dashboard/_target_table.html.erb
index b8f7e66..b1553dc 100644
--- a/app/views/dashboard/_target_table.html.erb
+++ b/app/views/dashboard/_target_table.html.erb
@@ -9,8 +9,7 @@
<thead class="table-light">
<tr>
<th scope="col" class="text-nowrap">Nutrient</th>
- <th scope="col" class="text-end"> Latest (mg/L)
- </th>
+ <th scope="col" class="text-end"> Latest (mg/L)</th>
<th scope="col" class="text-end">Target (mg/L)</th>
<th scope="col" class="text-end">Δ %</th>
</tr>
diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb
index b8f9145..a954e4c 100644
--- a/app/views/dashboard/index.html.erb
+++ b/app/views/dashboard/index.html.erb
@@ -1,9 +1,9 @@
<h1 class="display-1">Ferti</h1>
-<%= render "nutrient_profile_allocator", nutrient_profiles: @nutrient_profiles %>
+<%= render "nutrient_target_table", nutrient_profiles: @nutrient_profiles %>
<%#= render "raft_allocation" %>
<%#= render "target_table" %>
-<%#= render "nutrient_measurements" %>
+<%= render "nutrient_measurements_table" %>
Copyright 2019--2025 Marius PETER