diff options
author | Marius Peter <dev@marius-peter.com> | 2025-09-08 21:21:56 +0200 |
---|---|---|
committer | Marius Peter <dev@marius-peter.com> | 2025-09-08 21:21:56 +0200 |
commit | 7116826b854188604e21e2a613ac6672b6fd81f3 (patch) | |
tree | 33150bf2e04e69b8e1fa7d37901d2643b1955534 /app/views/dashboard | |
parent | 8ba568ae0ebe715b5da453681eb141886f1977a8 (diff) |
Create Target and nutrient target table on dashboard.
Diffstat (limited to 'app/views/dashboard')
-rw-r--r-- | app/views/dashboard/_nutrient_measurements.html.erb | 22 | ||||
-rw-r--r-- | app/views/dashboard/_nutrient_measurements_table.html.erb | 18 | ||||
-rw-r--r-- | app/views/dashboard/_nutrient_profile_allocator.html.erb | 187 | ||||
-rw-r--r-- | app/views/dashboard/_nutrient_target_table.html.erb | 74 | ||||
-rw-r--r-- | app/views/dashboard/_target_table.html.erb | 3 | ||||
-rw-r--r-- | app/views/dashboard/index.html.erb | 4 |
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 : <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" %> |