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 | |
parent | 8ba568ae0ebe715b5da453681eb141886f1977a8 (diff) |
Create Target and nutrient target table on dashboard.
Diffstat (limited to 'app/views')
-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 | ||||
-rw-r--r-- | app/views/nutrient_measurement/index.html.erb | 27 | ||||
-rw-r--r-- | app/views/nutrient_measurements/_form.html.erb | 49 | ||||
-rw-r--r-- | app/views/nutrient_measurements/index.html.erb | 41 | ||||
-rw-r--r-- | app/views/nutrient_measurements/new.html.erb | 11 | ||||
-rw-r--r-- | app/views/targets/create.html.erb | 2 | ||||
-rw-r--r-- | app/views/targets/edit.html.erb | 2 | ||||
-rw-r--r-- | app/views/targets/index.html.erb | 68 | ||||
-rw-r--r-- | app/views/targets/new.html.erb | 85 | ||||
-rw-r--r-- | app/views/targets/show.html.erb | 3 | ||||
-rw-r--r-- | app/views/targets/update.html.erb | 2 |
16 files changed, 358 insertions, 240 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" %> diff --git a/app/views/nutrient_measurement/index.html.erb b/app/views/nutrient_measurement/index.html.erb deleted file mode 100644 index d9f522e..0000000 --- a/app/views/nutrient_measurement/index.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -<h1>NutrientMeasurement#index</h1> -<p>Find me in app/views/nutrient_measurement/index.html.erb</p> - -<div class="table-responsive"> - <table class="table table-sm table-striped table-hover align-middle table-nutrient mb-0"> - <thead class="table-light"> - <tr> - <th>Date</th> - <th class="numeric" title="Total N = NO₃‑N + NH₄‑N">N (total)</th> - <th class="numeric">P</th> - <th class="numeric">K</th> - <th class="numeric" title="Ammonia nitrogen">NH₄‑N</th> - </tr> - </thead> - <tbody> - <% @measurements.each do |m| %> - <tr> - <td><%= l(m.measured_on) %></td> - <td class="numeric"><%= fmt2(total_n(m)) %></td> - <td class="numeric"><%= fmt2(m.p) %></td> - <td class="numeric"><%= fmt2(m.k) %></td> - <td class="numeric"><%= fmt2(m.nnh4) %></td> - </tr> - <% end %> - </tbody> - </table> -</div> diff --git a/app/views/nutrient_measurements/_form.html.erb b/app/views/nutrient_measurements/_form.html.erb new file mode 100644 index 0000000..9689c57 --- /dev/null +++ b/app/views/nutrient_measurements/_form.html.erb @@ -0,0 +1,49 @@ +<%= form_with(model: nutrient_measurement) do |form| %> + <% if nutrient_measurement.errors.any? %> + <div class="alert alert-danger"> + <p class="mb-1"><strong><%= pluralize(nutrient_measurement.errors.count, "erreur") %></strong> empêchent l’enregistrement :</p> + <ul class="mb-0"> + <% nutrient_measurement.errors.full_messages.each do |msg| %> + <li><%= msg %></li> + <% end %> + </ul> + </div> + <% end %> + + <div class="mb-3"> + <%= form.label :measured_on, "Date du relevé", class: "form-label" %> + <%= form.date_field :measured_on, class: "form-control", required: true %> + </div> + + <h2 class="h6 mt-4 mb-2">Concentrations de nutriments — laisser vide si non mesuré</h2> + + <div class="row g-2"> + <% # You can reorganize into macros/micros if you prefer %> + <% labels = { + nno3: "Nitrate (N-NO₃)", p: "Phosphore (P)", k: "Potassium (K)", + ca: "Calcium (Ca)", mg: "Magnésium (Mg)", s: "Soufre (S)", + na: "Sodium (Na)", cl: "Chlore (Cl)", si: "Silicium (Si)", + fe: "Fer (Fe)", zn: "Zinc (Zn)", b: "Bore (B)", + mn: "Manganèse (Mn)", cu: "Cuivre (Cu)", mo: "Molybdène (Mo)", + nnh4: "Ammonium (N-NH₄)" + } %> + + <% NutrientMeasurement::NUTRIENT_FIELDS.each do |field| %> + <div class="col-6 col-md-3"> + <div class="input-group"> + <%= form.number_field field, + class: "form-control", + placeholder: "—", + step: "0.01", + min: "0" %> + <span class="input-group-text">mg/L</span> + </div> + <label class="form-label d-block small text-muted mt-1"><%= labels[field] %></label> + </div> + <% end %> + </div> + + <div> + <%= form.submit "Ajouter le relevé", class: "btn btn-primary" %> + </div> +<% end %> diff --git a/app/views/nutrient_measurements/index.html.erb b/app/views/nutrient_measurements/index.html.erb new file mode 100644 index 0000000..c01d0cc --- /dev/null +++ b/app/views/nutrient_measurements/index.html.erb @@ -0,0 +1,41 @@ +<% content_for :title, "Liste des Relevé" %> + +<h1 class="display-1">Liste des Relevés</h1> + +<div class="d-flex justify-content-between align-items-center mb-3"> + <div class="btn-group"> + <%= link_to "Nouvelle mesure", new_nutrient_measurement_path, class: "btn btn-primary" %> + <%= link_to "Retour", root_path, class: "btn btn-outline-secondary" %> + </div> +</div> + +<div class="table-responsive"> + <table class="table table-sm table-striped table-hover align-middle table-nutrient mb-0"> + <thead class="table-light"> + <tr> + <th>Date</th> + <th class="text-end" title="Total N = NO₃-N + NH₄-N">N (total)</th> + <th class="text-end">P</th> + <th class="text-end">K</th> + <th class="text-end" title="Ammonia nitrogen">NH₄-N</th> + </tr> + </thead> + <tbody> + <% @nutrient_measurements.each do |m| %> + <tr> + <td><%= l(m.measured_on) %></td> + <td class="text-end"> + <%= number_with_precision(m.nno3.to_f + m.nnh4.to_f, precision: 2) if m.nno3 || m.nnh4 %> + </td> + <td class="text-end"><%= number_with_precision(m.p, precision: 2) if m.p %></td> + <td class="text-end"><%= number_with_precision(m.k, precision: 2) if m.k %></td> + <td class="text-end"><%= number_with_precision(m.nnh4, precision: 2) if m.nnh4 %></td> + </tr> + <% end %> + </tbody> + </table> +</div> + +<%= line_chart @npk_measurement_data, + title: "NPK", + ytitle: "Concentration (mg/L)" %> diff --git a/app/views/nutrient_measurements/new.html.erb b/app/views/nutrient_measurements/new.html.erb new file mode 100644 index 0000000..82a913e --- /dev/null +++ b/app/views/nutrient_measurements/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "Ajouter un Relevé" %> + +<h1 class="display-1">Ajouter un Relevé</h1> + +<%= render "form", nutrient_measurement: @nutrient_measurement %> + +<br> + +<div> + <%= link_to "Retour", root_path, class: "btn btn-secondary" %> +</div> diff --git a/app/views/targets/create.html.erb b/app/views/targets/create.html.erb new file mode 100644 index 0000000..51e2782 --- /dev/null +++ b/app/views/targets/create.html.erb @@ -0,0 +1,2 @@ +<h1>Targets#create</h1> +<p>Find me in app/views/targets/create.html.erb</p> diff --git a/app/views/targets/edit.html.erb b/app/views/targets/edit.html.erb new file mode 100644 index 0000000..849bf7f --- /dev/null +++ b/app/views/targets/edit.html.erb @@ -0,0 +1,2 @@ +<h1>Targets#edit</h1> +<p>Find me in app/views/targets/edit.html.erb</p> diff --git a/app/views/targets/index.html.erb b/app/views/targets/index.html.erb new file mode 100644 index 0000000..b552038 --- /dev/null +++ b/app/views/targets/index.html.erb @@ -0,0 +1,68 @@ +<h1 class="display-1">Cibles</h1> + +<div class="btn-group my-3"> + <%= link_to "Nouvelle Cible", new_target_path, class: "btn btn-primary" %> +</div> + +<div class="table-responsive"> + <table class="table table-sm table-striped table-hover align-middle mb-0"> + <thead class="table-light"> + <tr> + <th>Nom</th> + <th>Répartition</tr> + <th class="text-end" style="width: 140px;">Total %</th> + <th class="text-nowrap" style="width: 190px;">Créé le</th> + <th class="text-end" style="width: 180px;">Actions</th> + </tr> + </thead> + <tbody> + <% if @targets.present? %> + <% @targets.each do |t| %> + <% sum_pct = t.target_allocations.sum { |a| a.percentage.to_f } %> + <% badge_class = (sum_pct - 100.0).abs <= 0.01 ? "bg-success" : "bg-danger" %> + <tr> + <td class="fw-semibold"> + <%= link_to t.name.presence || "Objectif ##{t.id}", t %> + </td> + <td> + <% if t.target_allocations.empty? %> + <span class="text-muted">Aucune répartition définie</span> + <% else %> + <ul class="list-unstyled mb-0 d-flex flex-wrap gap-2"> + <% t.target_allocations.each do |a| %> + <li class="badge text-bg-light border"> + <%= a.nutrient_profile&.name || "Profil ##{a.nutrient_profile_id}" %> + — <%= number_with_precision(a.percentage.to_f, precision: 2) %>% + </li> + <% end %> + </ul> + <% end %> + </td> + <td class="text-end"> + <span class="badge <%= badge_class %>"> + <%= number_with_precision(sum_pct, precision: 2) %>% + </span> + </td> + <td class="text-nowrap"> + <%= l(t.created_at, format: :short) %> + </td> + <td class="text-end text-nowrap"> + <%= link_to "Voir", t, class: "btn btn-outline-secondary btn-sm" %> + <%= link_to "Modifier", edit_target_path(t), class: "btn btn-outline-primary btn-sm" %> + <%# FIXME: Doesn't work. %> + <%= link_to "Supprimer", t, class: "btn btn-outline-danger btn-sm", + data: { turbo_method: :delete, turbo_confirm: "Supprimer cet objectif ?" } %> + </td> + </tr> + <% end %> + <% else %> + <tr> + <td colspan="5" class="text-center py-4 text-muted"> + Aucun objectif pour le moment. + <%= link_to "Créer le premier", new_target_path %>. + </td> + </tr> + <% end %> + </tbody> + </table> +</div> diff --git a/app/views/targets/new.html.erb b/app/views/targets/new.html.erb new file mode 100644 index 0000000..42cb7bd --- /dev/null +++ b/app/views/targets/new.html.erb @@ -0,0 +1,85 @@ +<% content_for :title, "Ajouter une Cible" %> + +<h1 class="display-1">Ajouter une Cible</h1> + +<%= form_with(model: @target) do |f| %> + <div class="card shadow-sm"> + <div class="card-header"> + <%= f.text_field :name, class: "form-control", placeholder: "Nom de la cible" %> + </div> + + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0"> + <thead class="table-light"> + <tr> + <th>Profil</th> + <th class="text-end" style="width: 180px;">Proportion</th> + </tr> + </thead> + <tbody id="alloc-table-body"> + <%= f.fields_for :target_allocations do |af| %> + <% np = af.object.nutrient_profile %> + <tr> + <td> + <%= af.hidden_field :nutrient_profile_id %> + <strong><%= np&.name.capitalize || "Profil ##{af.object.nutrient_profile_id}" %></strong> + </td> + <td class="text-end"> + <div class="input-group input-group-sm" style="max-width: 160px; margin-left:auto;"> + <%= af.number_field :percentage, + in: 0..100, step: 0.5, + class: "form-control text-end alloc-input", + placeholder: "0.0", + data: { action: "input->alloc#sum" } %> + <span class="input-group-text">%</span> + </div> + </td> + </tr> + <% end %> + </tbody> + <tfoot> + <tr> + <td class="small text-muted">Ajustez chaque pourcentage pour totaliser 100%.</td> + <td class="text-end"> + <span class="badge bg-secondary" id="alloc-total">Total : 0%</span> + </td> + </tr> + </tfoot> + </table> + </div> + </div> + + <div class="card-footer d-flex gap-2 justify-content-end"> + <div class="btn-group"> + <%= f.submit "Enregistrer l’objectif", class: "btn btn-primary", id: "submit-btn" %> + <%= link_to "Annuler", targets_path, class: "btn btn-secondary" %> + </div> + </div> + </div> +<% end %> + +<script> + // Lightweight client-side sum check (no Stimulus required). + document.addEventListener("turbo:load", initAllocSum); + document.addEventListener("DOMContentLoaded", initAllocSum); + + function initAllocSum() { + const inputs = document.querySelectorAll(".alloc-input"); + const totalBadge = document.getElementById("alloc-total"); + const submitBtn = document.getElementById("submit-btn"); + if (!inputs.length || !totalBadge) return; + + function updateTotal() { + let sum = 0; + inputs.forEach(i => sum += parseFloat(i.value || "0")); + const rounded = Math.round(sum * 100) / 100; + totalBadge.textContent = `Total : ${rounded}%`; + totalBadge.className = "badge " + (Math.abs(rounded - 100) < 0.01 ? "bg-success" : "bg-danger"); + if (submitBtn) submitBtn.disabled = !(Math.abs(rounded - 100) < 0.01); + } + + inputs.forEach(i => i.addEventListener("input", updateTotal)); + updateTotal(); + } +</script> diff --git a/app/views/targets/show.html.erb b/app/views/targets/show.html.erb new file mode 100644 index 0000000..1525609 --- /dev/null +++ b/app/views/targets/show.html.erb @@ -0,0 +1,3 @@ +<h1>Targets#show</h1> + +<%# TODO: add table comparing this target with the most recent measurement. %> diff --git a/app/views/targets/update.html.erb b/app/views/targets/update.html.erb new file mode 100644 index 0000000..a39287c --- /dev/null +++ b/app/views/targets/update.html.erb @@ -0,0 +1,2 @@ +<h1>Targets#update</h1> +<p>Find me in app/views/targets/update.html.erb</p> |