summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMarius Peter <dev@marius-peter.com>2025-11-23 17:54:45 +0100
committerMarius Peter <dev@marius-peter.com>2025-11-23 17:54:45 +0100
commitfa77a691ce0cc8941fe470a762f352b27f4f0563 (patch)
tree08916174840a7896fc59633cc59fab931e7012c2 /app
parent73283f2f5153c77f72b6a29e98f173628f5e1057 (diff)
Last commit.HEADmaster
Diffstat (limited to 'app')
-rw-r--r--app/controllers/dashboard_controller.rb43
-rw-r--r--app/controllers/targets_controller.rb41
-rw-r--r--app/helpers/targets_helper.rb2
-rw-r--r--app/models/concerns/nutrient_vector.rb55
-rw-r--r--app/models/nutrient.rb4
-rw-r--r--app/models/nutrient_measurement.rb12
-rw-r--r--app/models/target.rb67
-rw-r--r--app/models/target_allocation.rb3
-rw-r--r--app/views/dashboard/_nutrient_measurements_table.html.erb4
-rw-r--r--app/views/dashboard/_nutrient_target_table.html.erb35
-rw-r--r--app/views/dashboard/_raft_allocation.html.erb9
-rw-r--r--app/views/dashboard/_target_table.html.erb39
-rw-r--r--app/views/dashboard/index.html.erb4
-rw-r--r--app/views/targets/index.html.erb34
-rw-r--r--app/views/targets/new.html.erb85
15 files changed, 152 insertions, 285 deletions
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index a315eda..fc8cbd8 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,50 +1,11 @@
class DashboardController < ApplicationController
def index
- # Raft allocation by crop type
- # @raft_data = raft_data_series
-
- @nutrient_profiles = NutrientProfile.order(:name)
-
- # Nutrient target table
- # @latest_measurement = NutrientMeasurement.order(measured_on: :desc, created_at: :desc).first
- # @target = TargetNutrientCalculator.call
+ @latest_measurement = NutrientMeasurement.order(measured_on: :desc).first
+ @latest_target = Target.order(created_at: :desc).first
# Measurement history table
@measurements = NutrientMeasurement.order(measured_on: :desc).limit(10)
@npk_measurement_data = NutrientMeasurement.data_series_for(:nno3, :p, :k)
@ammonium_measurement_data = NutrientMeasurement.data_series_for(:nnh4)
-
- @weighted = Target.first.weighted_requirements # => { "nno3"=>..., "p"=>..., ... }
-
- last = NutrientMeasurement.order(measured_on: :desc, created_at: :desc).first
- @latest_measurements = {}
-
- if last
- # Use the same keys as NutrientProfile to keep naming consistent.
- keys = (NutrientProfile::NUTRIENT_KEYS rescue []).map(&:to_s)
- keys.each do |k|
- @latest_measurements[k] = last.send(k) if last.respond_to?(k)
- end
- end
- end
-
- private
-
- def raft_data_series
- data_series = []
-
- counts = Raft.left_outer_joins(:crop)
- .group("crops.name", "crops.crop_type")
- .count
-
- counts.each do |(crop_name, crop_type), count|
- name = (crop_name || "unassigned").titleize
- type = (crop_type || "unassigned").titleize
- data = { type => count }
- data_series << { name:, data: }
- end
-
- unassigned, assigned = data_series.partition { |s| s[:name].casecmp("unassigned").zero? }
- assigned + unassigned
end
end
diff --git a/app/controllers/targets_controller.rb b/app/controllers/targets_controller.rb
index a11ddd7..679cb75 100644
--- a/app/controllers/targets_controller.rb
+++ b/app/controllers/targets_controller.rb
@@ -6,16 +6,24 @@ class TargetsController < ApplicationController
end
def new
- @target = Target.new(name: "Cible #{Date.today + 1.month}")
- seed_allocations
+ @target = Target.new
+ @nutrient_profiles = NutrientProfile.order(:name)
+ # Build one allocation per profile so each appears as a row
+ @nutrient_profiles.each do |np|
+ @target.target_allocations.build(nutrient_profile: np, percentage: 12.5)
+ end
end
def create
@target = Target.new(target_params)
if @target.save
- redirect_to @target, notice: "Cible enregistrée."
+ redirect_to @target, notice: "Cible créée."
else
- seed_allocations if @target.target_allocations.blank?
+ # Rebuild rows for any profiles missing (e.g., after validation errors)
+ existing_ids = @target.target_allocations.map(&:nutrient_profile_id)
+ (NutrientProfile.where.not(id: existing_ids)).order(:name).each do |np|
+ @target.target_allocations.build(nutrient_profile: np, percentage: 0)
+ end
render :new, status: :unprocessable_entity
end
end
@@ -31,18 +39,12 @@ class TargetsController < ApplicationController
end
end
- def show
- @weighted = @target.weighted_requirements # => { "nno3"=>..., "p"=>..., ... }
-
- last = NutrientMeasurement.order(measured_on: :desc, created_at: :desc).first
- @latest_measurements = {}
-
- if last
- # Use the same keys as NutrientProfile to keep naming consistent.
- keys = (NutrientProfile::NUTRIENT_KEYS rescue []).map(&:to_s)
- keys.each do |k|
- @latest_measurements[k] = last.send(k) if last.respond_to?(k)
- end
+ def destroy
+ @target = Target.find(params[:id])
+ @target.destroy
+ respond_to do |format|
+ format.turbo_stream { render turbo_stream: turbo_stream.remove(dom_id(@target)) }
+ format.html { redirect_to targets_path, notice: "Cible supprimé." }
end
end
@@ -52,13 +54,6 @@ class TargetsController < ApplicationController
@target = Target.find(params[:id])
end
- def seed_allocations
- existing_ids = @target.target_allocations.map(&:nutrient_profile_id).compact
- (NutrientProfile.order(:name).pluck(:id) - existing_ids).each do |np_id|
- @target.target_allocations.build(nutrient_profile_id: np_id, percentage: 12.5)
- end
- end
-
def target_params
params.require(:target).permit(
:name,
diff --git a/app/helpers/targets_helper.rb b/app/helpers/targets_helper.rb
deleted file mode 100644
index 8484878..0000000
--- a/app/helpers/targets_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module TargetsHelper
-end
diff --git a/app/models/concerns/nutrient_vector.rb b/app/models/concerns/nutrient_vector.rb
new file mode 100644
index 0000000..ba4f28a
--- /dev/null
+++ b/app/models/concerns/nutrient_vector.rb
@@ -0,0 +1,55 @@
+module NutrientVector
+ extend ActiveSupport::Concern
+
+ NUTRIENT_KEYS = %i[
+ nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4
+ ].freeze
+
+ included do
+ # Define simple readers (nno3, p, k, ...) that read from #nutrient_values
+ NUTRIENT_KEYS.each do |k|
+ define_method(k) { nutrient_values[k] }
+ end
+ end
+
+ # Hash-like access
+ def [](key)
+ key = key.to_sym
+ return nil unless NUTRIENT_KEYS.include?(key)
+ public_send(key)
+ end
+
+ def keys = NUTRIENT_KEYS
+
+ # Returns a copy to avoid accidental mutation
+ def to_h
+ NUTRIENT_KEYS.index_with { |k| public_send(k) }
+ end
+
+ # Iterate over pairs
+ def each_pair
+ return enum_for(:each_pair) unless block_given?
+ NUTRIENT_KEYS.each { |k| yield k, public_send(k) }
+ end
+
+ # Simple difference (self - other), useful for “how far from target?”
+ def delta_against(other)
+ NUTRIENT_KEYS.index_with { |k| (public_send(k).to_f) - (other.public_send(k).to_f) }
+ end
+
+ # Percent difference relative to other (e.g., measurement vs target)
+ # Returns 0 when both are 0, and nil when target is 0 but measurement isn’t.
+ def percent_diff_against(other)
+ NUTRIENT_KEYS.index_with do |k|
+ a = public_send(k).to_f
+ b = other.public_send(k).to_f
+ if b.zero? && a.zero?
+ 0.0
+ elsif b.zero?
+ nil
+ else
+ ((a - b) / b) * 100.0
+ end
+ end
+ end
+end
diff --git a/app/models/nutrient.rb b/app/models/nutrient.rb
new file mode 100644
index 0000000..0840086
--- /dev/null
+++ b/app/models/nutrient.rb
@@ -0,0 +1,4 @@
+class Nutrient < ApplicationRecord
+ validates :formula, presence: true, uniqueness: true
+ validates :name, presence: true
+end
diff --git a/app/models/nutrient_measurement.rb b/app/models/nutrient_measurement.rb
index 1139af7..b56c5ed 100644
--- a/app/models/nutrient_measurement.rb
+++ b/app/models/nutrient_measurement.rb
@@ -1,7 +1,5 @@
class NutrientMeasurement < ApplicationRecord
- NUTRIENT_FIELDS = %i[
- nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4
- ].freeze
+ include NutrientVector
validates :measured_on, presence: true
validates :measured_on, uniqueness: true
@@ -12,7 +10,11 @@ class NutrientMeasurement < ApplicationRecord
end
end
- def self.nutrient_fields
- NUTRIENT_FIELDS
+ private
+
+ def nutrient_values
+ @nutrient_values ||= NutrientVector::NUTRIENT_KEYS.index_with do |k|
+ read_attribute(k).to_f
+ end
end
end
diff --git a/app/models/target.rb b/app/models/target.rb
index 9211e74..2efd3b2 100644
--- a/app/models/target.rb
+++ b/app/models/target.rb
@@ -1,42 +1,53 @@
-# app/models/target.rb
class Target < ApplicationRecord
- has_many :target_allocations, dependent: :destroy
- has_many :nutrient_profiles, through: :target_allocations
+ include NutrientVector
- accepts_nested_attributes_for :target_allocations, allow_destroy: true
- # validate :percentages_sum_to_100
+ has_many :target_allocations, inverse_of: :target, dependent: :destroy
- def weighted_requirements
- totals = Hash.new(0.0)
- denom = 100.0
+ after_initialize :set_defaults, if: :new_record?
- target_allocations.includes(:nutrient_profile).each do |alloc|
- profile = alloc.nutrient_profile
- next unless profile
+ validate :allocations_sum_to_100
- weight = (alloc.percentage || 0).to_f / denom
- next if weight <= 0
+ # Recompute when allocations change; keep it simple (memoized per instance)
+ def recompute_nutrients!
+ @nutrient_values = nil
+ nutrient_values
+ end
- # Prefer the helper, but gracefully fall back to slicing attributes.
- reqs = if profile.respond_to?(:requirements_hash)
- profile.requirements_hash
- else
- profile.attributes.slice(*NutrientProfile::NUTRIENT_KEYS.map(&:to_s))
- end
+ private
+
+ def nutrient_values
+ @nutrient_values ||= get_nutrient_values
+ end
+
+ def get_nutrient_values
+ sums = Hash.new(0.0)
+ allocs = target_allocations.includes(:nutrient_profile)
- reqs.each do |nutrient_key, value|
- next if value.nil?
- totals[nutrient_key.to_s] += value.to_f * weight
+ allocs.each do |alloc|
+ weight = alloc.percentage.to_f / 100.0
+ profile = alloc.nutrient_profile
+ NutrientVector::NUTRIENT_KEYS.each do |k|
+ sums[k] += profile.public_send(k).to_f * weight
end
end
- totals
+ # Ensure all keys exist, even when there are no allocations
+ NutrientVector::NUTRIENT_KEYS.each { |k| sums[k] ||= 0.0 }
+ sums.freeze
end
- private
+ def set_defaults
+ self.name ||= "Cible #{Date.today + 1.month}"
+ if target_allocations.empty?
+ nutrient_profiles = NutrientProfile.limit(3)
+ nutrient_profiles.each do |profile|
+ target_allocations.build(nutrient_profile: profile, percentage: 0)
+ end
+ end
+ end
- # def percentages_sum_to_100
- # sum = target_allocations.sum { |a| a.percentage.to_f }
- # errors.add(:base, "La somme des pourcentages doit être égale à 100%") unless (sum - 100.0).abs <= 0.1
- # end
+ def allocations_sum_to_100
+ sum = target_allocations.reject(&:marked_for_destruction?).sum { |a| a.percentage.to_f }
+ errors.add(:base, "La somme des pourcentages doit être 100%") unless (sum - 100).abs < 0.01
+ end
end
diff --git a/app/models/target_allocation.rb b/app/models/target_allocation.rb
index 6aa1dcb..5e971a0 100644
--- a/app/models/target_allocation.rb
+++ b/app/models/target_allocation.rb
@@ -1,7 +1,6 @@
class TargetAllocation < ApplicationRecord
- belongs_to :target
+ belongs_to :target, inverse_of: :target_allocations, touch: true
belongs_to :nutrient_profile
validates :percentage, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
- validates :nutrient_profile_id, uniqueness: { scope: :target_id }
end
diff --git a/app/views/dashboard/_nutrient_measurements_table.html.erb b/app/views/dashboard/_nutrient_measurements_table.html.erb
index 82ea6ff..4d516bd 100644
--- a/app/views/dashboard/_nutrient_measurements_table.html.erb
+++ b/app/views/dashboard/_nutrient_measurements_table.html.erb
@@ -2,8 +2,8 @@
<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" %>
+ <%= link_to "Ajouter un relevé", new_nutrient_measurement_path, class: "btn btn-sm btn-outline-primary" %>
+ <%= link_to "Liste des relevés", nutrient_measurements_path, class: "btn btn-sm btn-outline-secondary" %>
</div>
</div>
diff --git a/app/views/dashboard/_nutrient_target_table.html.erb b/app/views/dashboard/_nutrient_target_table.html.erb
index 7cf294c..006d262 100644
--- a/app/views/dashboard/_nutrient_target_table.html.erb
+++ b/app/views/dashboard/_nutrient_target_table.html.erb
@@ -2,40 +2,33 @@
<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" %>
+ <%= link_to "Nouvelle cible", new_target_path, class: "btn btn-sm btn-outline-primary" %>
+ <%= link_to "Liste des cibles", targets_path, class: "btn btn-sm btn-outline-secondary" %>
+ <%= link_to "Voir la recette", root_path, class: "btn btn-sm btn-outline-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">
+ <table class="table 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>
+ <th class="text-end"><%= @latest_target&.name || "Cible" %></th>
+ <th class="text-end">Delta (%)</th>
</tr>
</thead>
<tbody>
- <% wr = @weighted || {} %>
- <% lm = @latest_measurements || {} %>
+ <% Nutrient.all.each do |nutrient| %>
+ <% measured = @latest_measurement.send(nutrient.formula.downcase) %>
+ <% target = @latest_target.send(nutrient.formula.downcase) %>
+ <% delta = (target.to_f - measured.to_f) / 100.0 if measured || target %>
- <% 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="fw-semibold"><%= nutrient.formula %></td>
<td class="text-end">
- <% if measured.nil? %>
- <span class="text-muted">—</span>
- <% else %>
- <%= number_with_precision(measured, precision: 2) %>
- <% end %>
+ <%= measured.present? ? number_with_precision(measured, precision: 2) : — %>
</td>
<td class="text-end">
@@ -50,7 +43,7 @@
<% if measured.nil? && target.nil? %>
<span class="text-muted">—</span>
<% else %>
- <% badge =
+ <% badge_class =
if delta.nil?
"text-bg-secondary"
elsif delta.abs <= 0.01
@@ -60,7 +53,7 @@
else
"text-bg-danger"
end %>
- <span class="badge <%= badge %>">
+ <span class="badge <%= badge_class %>">
<%= number_with_precision(delta.to_f, precision: 2) %>
</span>
<% end %>
diff --git a/app/views/dashboard/_raft_allocation.html.erb b/app/views/dashboard/_raft_allocation.html.erb
deleted file mode 100644
index 1c9ef9a..0000000
--- a/app/views/dashboard/_raft_allocation.html.erb
+++ /dev/null
@@ -1,9 +0,0 @@
-<div class="card shadow mb-4">
- <div class="card-header d-flex justify-content-between align-items-center">
- <h5 class="mb-0">Crop Allocation</h5>
- <%#= link_to "Edit allocation", beds_path, class: "btn btn-sm btn-primary" %>
- </div>
-
- <%= bar_chart @raft_data, stacked: true %>
-</div>
-
diff --git a/app/views/dashboard/_target_table.html.erb b/app/views/dashboard/_target_table.html.erb
deleted file mode 100644
index b1553dc..0000000
--- a/app/views/dashboard/_target_table.html.erb
+++ /dev/null
@@ -1,39 +0,0 @@
-<div class="card shadow mb-4">
- <div class="card-header d-flex justify-content-between align-items-center">
- <h5 class="mb-0">Target nutrient concentrations</h5>
- <%= link_to "Get Ferti© recipe", ferti_recipe_path, class: "btn btn-sm btn-primary" %>
- </div>
-
- <div class="table-responsive">
- <table class="table table-sm align-middle mb-0">
- <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">Target (mg/L)</th>
- <th scope="col" class="text-end">Δ %</th>
- </tr>
- </thead>
- <tbody>
- <% NutrientsHelper::NUTRIENTS.each do |n| %>
- <% latest = @latest_measurement[n] || 0 %>
- <% target = @target[n] || 0 %>
- <% delta = target - latest %>
- <tr>
- <th scope="row" class="text-nowrap"><%= n.upcase %></th>
- <td class="text-end"><%= fmt(latest) %></td>
- <td class="text-end"><%= fmt(target) %></td>
- <td class="text-end">
- <span class="badge <%= delta_badge_class(delta) %>">
- <%= fmt(delta) %>%
- </span>
- </td>
- </tr>
- <% end %>
- </tbody>
- </table>
- </div>
- <div class="card-footer">
- Latest measurement: <%= @latest_measurement.measured_on || "none yet" %>
- </div>
-</div>
diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb
index a954e4c..50fab18 100644
--- a/app/views/dashboard/index.html.erb
+++ b/app/views/dashboard/index.html.erb
@@ -2,8 +2,4 @@
<%= render "nutrient_target_table", nutrient_profiles: @nutrient_profiles %>
-<%#= render "raft_allocation" %>
-
-<%#= render "target_table" %>
-
<%= render "nutrient_measurements_table" %>
diff --git a/app/views/targets/index.html.erb b/app/views/targets/index.html.erb
index b552038..e536deb 100644
--- a/app/views/targets/index.html.erb
+++ b/app/views/targets/index.html.erb
@@ -5,31 +5,29 @@
</div>
<div class="table-responsive">
- <table class="table table-sm table-striped table-hover align-middle mb-0">
+ <table class="table table-hover align-middle">
<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>
+ <th>Répartition</th>
+ <th>Date de Création</th>
</tr>
</thead>
<tbody>
<% if @targets.present? %>
- <% @targets.each do |t| %>
- <% sum_pct = t.target_allocations.sum { |a| a.percentage.to_f } %>
+ <% @targets.each do |target| %>
+ <% sum_pct = 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 %>
+ <%= link_to target.name.presence || "Cible ##{t.id}", target %>
</td>
<td>
- <% if t.target_allocations.empty? %>
+ <% if 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| %>
+ <ul class="list-unstyled mb-0 d-flex flex-wrap gap-3">
+ <% 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) %>%
@@ -38,20 +36,8 @@
</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 ?" } %>
+ <%= l(target.created_at, format: :short) %>
</td>
</tr>
<% end %>
diff --git a/app/views/targets/new.html.erb b/app/views/targets/new.html.erb
index 42cb7bd..e69de29 100644
--- a/app/views/targets/new.html.erb
+++ b/app/views/targets/new.html.erb
@@ -1,85 +0,0 @@
-<% 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>
Copyright 2019--2026 Marius PETER