From 7116826b854188604e21e2a613ac6672b6fd81f3 Mon Sep 17 00:00:00 2001 From: Marius Peter Date: Mon, 8 Sep 2025 21:21:56 +0200 Subject: Create Target and nutrient target table on dashboard. --- app/controllers/dashboard_controller.rb | 27 +-- .../nutrient_measurements_controller.rb | 26 +++ app/controllers/targets_controller.rb | 68 ++++++++ app/helpers/targets_helper.rb | 2 + app/models/nutrient_measurement.rb | 14 ++ app/models/nutrient_profile.rb | 11 ++ app/models/target.rb | 42 +++++ app/models/target_allocation.rb | 7 + .../dashboard/_nutrient_measurements.html.erb | 22 --- .../_nutrient_measurements_table.html.erb | 18 ++ .../dashboard/_nutrient_profile_allocator.html.erb | 187 --------------------- .../dashboard/_nutrient_target_table.html.erb | 74 ++++++++ app/views/dashboard/_target_table.html.erb | 3 +- app/views/dashboard/index.html.erb | 4 +- app/views/nutrient_measurement/index.html.erb | 27 --- app/views/nutrient_measurements/_form.html.erb | 49 ++++++ app/views/nutrient_measurements/index.html.erb | 41 +++++ app/views/nutrient_measurements/new.html.erb | 11 ++ app/views/targets/create.html.erb | 2 + app/views/targets/edit.html.erb | 2 + app/views/targets/index.html.erb | 68 ++++++++ app/views/targets/new.html.erb | 85 ++++++++++ app/views/targets/show.html.erb | 3 + app/views/targets/update.html.erb | 2 + config/routes.rb | 1 + db/migrate/20250908181137_create_targets.rb | 9 + .../20250908181147_create_target_allocations.rb | 11 ++ db/schema.rb | 20 ++- db/seeds/NutrientProfile.rb | 2 +- test/controllers/targets_controller_test.rb | 33 ++++ test/fixtures/target_allocations.yml | 11 ++ test/fixtures/targets.yml | 7 + test/models/target_allocation_test.rb | 7 + test/models/target_test.rb | 7 + 34 files changed, 649 insertions(+), 254 deletions(-) create mode 100644 app/controllers/nutrient_measurements_controller.rb create mode 100644 app/controllers/targets_controller.rb create mode 100644 app/helpers/targets_helper.rb create mode 100644 app/models/target.rb create mode 100644 app/models/target_allocation.rb delete mode 100644 app/views/dashboard/_nutrient_measurements.html.erb create mode 100644 app/views/dashboard/_nutrient_measurements_table.html.erb delete mode 100644 app/views/dashboard/_nutrient_profile_allocator.html.erb create mode 100644 app/views/dashboard/_nutrient_target_table.html.erb delete mode 100644 app/views/nutrient_measurement/index.html.erb create mode 100644 app/views/nutrient_measurements/_form.html.erb create mode 100644 app/views/nutrient_measurements/index.html.erb create mode 100644 app/views/nutrient_measurements/new.html.erb create mode 100644 app/views/targets/create.html.erb create mode 100644 app/views/targets/edit.html.erb create mode 100644 app/views/targets/index.html.erb create mode 100644 app/views/targets/new.html.erb create mode 100644 app/views/targets/show.html.erb create mode 100644 app/views/targets/update.html.erb create mode 100644 db/migrate/20250908181137_create_targets.rb create mode 100644 db/migrate/20250908181147_create_target_allocations.rb create mode 100644 test/controllers/targets_controller_test.rb create mode 100644 test/fixtures/target_allocations.yml create mode 100644 test/fixtures/targets.yml create mode 100644 test/models/target_allocation_test.rb create mode 100644 test/models/target_test.rb diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 6f323a9..a315eda 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -10,10 +10,22 @@ class DashboardController < ApplicationController # @target = TargetNutrientCalculator.call # Measurement history table - # @measurements = NutrientMeasurement.order(measured_on: :desc).limit(10) + @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) - # @npk_measurement_data = measurement_data_series(:nno3, :p, :k) - # @ammonium_measurement_data = measurement_data_series(: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 @@ -35,13 +47,4 @@ class DashboardController < ApplicationController unassigned, assigned = data_series.partition { |s| s[:name].casecmp("unassigned").zero? } assigned + unassigned end - - def measurement_data_series(*nutrients) - nutrients.map do |formula| - { name: Nutrient.find_by!(formula:).name, - data: NutrientMeasurement - .order(:measured_on) - .pluck(:measured_on, formula) } - end - end end diff --git a/app/controllers/nutrient_measurements_controller.rb b/app/controllers/nutrient_measurements_controller.rb new file mode 100644 index 0000000..71ca9e4 --- /dev/null +++ b/app/controllers/nutrient_measurements_controller.rb @@ -0,0 +1,26 @@ +class NutrientMeasurementsController < ApplicationController + def index + @nutrient_measurements = NutrientMeasurement.order(measured_on: :desc) + @npk_measurement_data = NutrientMeasurement.data_series_for(:nno3, :p, :k) + end + + def new + @nutrient_measurement = NutrientMeasurement.new(measured_on: Date.today) + end + + def create + @measurement = NutrientMeasurement.new(nutrient_measurement_params) + if @measurement.save + redirect_to @measurement, notice: "Relevé enregistré." + else + render :new, status: :unprocessable_entity + end + end + + private + + def nutrient_measurement_params + permitted = [ :measured_on ] + NutrientMeasurement::NUTRIENT_FIELDS + params.require(:nutrient_measurement).permit(*permitted) + end +end diff --git a/app/controllers/targets_controller.rb b/app/controllers/targets_controller.rb new file mode 100644 index 0000000..60d3523 --- /dev/null +++ b/app/controllers/targets_controller.rb @@ -0,0 +1,68 @@ +class TargetsController < ApplicationController + before_action :set_target, only: %i[show edit update] + + def index + @targets = Target.order(:name) + end + + def new + @target = Target.new(name: "Cible #{Date.today + 1.month}") + seed_allocations + end + + def create + @target = Target.new(target_params) + if @target.save + redirect_to @target, notice: "Cible enregistrée." + else + seed_allocations if @target.target_allocations.blank? + render :new, status: :unprocessable_entity + end + end + + def edit + end + + def update + if @target.update(target_params) + redirect_to @target, notice: "Cible mise à jour." + else + render :edit, status: :unprocessable_entity + 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 + end + end + + private + + def set_target + @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, + target_allocations_attributes: [ :id, :nutrient_profile_id, :percentage, :_destroy ] + ) + end +end diff --git a/app/helpers/targets_helper.rb b/app/helpers/targets_helper.rb new file mode 100644 index 0000000..8484878 --- /dev/null +++ b/app/helpers/targets_helper.rb @@ -0,0 +1,2 @@ +module TargetsHelper +end diff --git a/app/models/nutrient_measurement.rb b/app/models/nutrient_measurement.rb index f1d6d5b..1139af7 100644 --- a/app/models/nutrient_measurement.rb +++ b/app/models/nutrient_measurement.rb @@ -1,4 +1,18 @@ class NutrientMeasurement < ApplicationRecord + NUTRIENT_FIELDS = %i[ + nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4 + ].freeze + validates :measured_on, presence: true validates :measured_on, uniqueness: true + + def self.data_series_for(*nutrients) + nutrients.map do |formula| + { name: formula, data: self.order(:measured_on).pluck(:measured_on, formula) } + end + end + + def self.nutrient_fields + NUTRIENT_FIELDS + end end diff --git a/app/models/nutrient_profile.rb b/app/models/nutrient_profile.rb index 22f2704..0610855 100644 --- a/app/models/nutrient_profile.rb +++ b/app/models/nutrient_profile.rb @@ -1,2 +1,13 @@ class NutrientProfile < ApplicationRecord + # Align these keys with your schema columns (per your schema.txt) + NUTRIENT_KEYS = %i[ + nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4 + ].freeze + + # Returns a Hash of nutrient => numeric requirement (nil kept; caller can skip nils) + def requirements_hash + attributes + .slice(*NUTRIENT_KEYS.map(&:to_s)) # only nutrient columns + .transform_keys(&:to_s) + end end diff --git a/app/models/target.rb b/app/models/target.rb new file mode 100644 index 0000000..3063b42 --- /dev/null +++ b/app/models/target.rb @@ -0,0 +1,42 @@ +# app/models/target.rb +class Target < ApplicationRecord + has_many :target_allocations, dependent: :destroy + has_many :nutrient_profiles, through: :target_allocations + + accepts_nested_attributes_for :target_allocations, allow_destroy: true + validate :percentages_sum_to_100 + + def weighted_requirements + totals = Hash.new(0.0) + denom = 100.0 + + target_allocations.includes(:nutrient_profile).each do |alloc| + profile = alloc.nutrient_profile + next unless profile + + weight = (alloc.percentage || 0).to_f / denom + next if weight <= 0 + + # 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 + + reqs.each do |nutrient_key, value| + next if value.nil? + totals[nutrient_key.to_s] += value.to_f * weight + end + end + + totals + end + + private + + def percentages_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 égale à 100%") unless (sum - 100.0).abs <= 0.01 + end +end diff --git a/app/models/target_allocation.rb b/app/models/target_allocation.rb new file mode 100644 index 0000000..6aa1dcb --- /dev/null +++ b/app/models/target_allocation.rb @@ -0,0 +1,7 @@ +class TargetAllocation < ApplicationRecord + belongs_to :target + 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.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 @@ -
-
-
Nutrient Measurements
-
- <%#= 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" %> -
-
- -
-
- <%= line_chart @npk_measurement_data, - title: "NPK", - ytitle: "Concentration (mg/L)" %> -
-
- <%= line_chart @ammonium_measurement_data, - title: "Ammonium", - ytitle: "Concentration (mg/L)" %> -
-
-
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 @@ +
+
+

Relevé des Nutriments

+
+ <%= 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" %> +
+
+ +
+ <%= line_chart @npk_measurement_data, + title: "NPK", + ytitle: "Concentration (mg/L)" %> + <%= line_chart @ammonium_measurement_data, + title: "Ammonium", + ytitle: "Concentration (mg/L)" %> +
+
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 %> -<%# 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 %> -
-
- -
-
- Choisissez des profils de croissance et répartissez-les pour totaliser 100%. -
-
- Somme : 0% -
-
- -
- <%# Rows are injected by JS from the template below, including defaults %> -
- -
- - - <%# Placeholder "save" button for later backend wiring; disabled until total == 100 %> - -
-
-
- - <%# --- Hidden template for a single row --- %> - - - <%# --- Defaults to inject on load --- %> - - - <%# --- Tiny inline JS to keep this self-contained (no Stimulus required) --- %> - - - -<% 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 @@ +
+
+

Complémentation

+
+ <%= 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" %> +
+
+ +
+
+ + + + + + + + + + + <% 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 %> + + + + + + + + + + <% end %> + +
NutrimentRelevéCibleDelta
<%= nut.upcase %> + <% if measured.nil? %> + + <% else %> + <%= number_with_precision(measured, precision: 2) %> + <% end %> + + <% if target.nil? %> + + <% else %> + <%= number_with_precision(target, precision: 2) %> + <% end %> + + <% if measured.nil? && target.nil? %> + + <% 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 %> + + <%= number_with_precision(delta.to_f, precision: 2) %> + + <% end %> +
+
+
+
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 @@ Nutrient - Latest (mg/L) - + Latest (mg/L) Target (mg/L) Δ % 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 @@

Ferti

-<%= 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 @@ -

NutrientMeasurement#index

-

Find me in app/views/nutrient_measurement/index.html.erb

- -
- - - - - - - - - - - - <% @measurements.each do |m| %> - - - - - - - - <% end %> - -
DateN (total)PKNH₄‑N
<%= l(m.measured_on) %><%= fmt2(total_n(m)) %><%= fmt2(m.p) %><%= fmt2(m.k) %><%= fmt2(m.nnh4) %>
-
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? %> +
+

<%= pluralize(nutrient_measurement.errors.count, "erreur") %> empêchent l’enregistrement :

+
    + <% nutrient_measurement.errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :measured_on, "Date du relevé", class: "form-label" %> + <%= form.date_field :measured_on, class: "form-control", required: true %> +
+ +

Concentrations de nutriments — laisser vide si non mesuré

+ +
+ <% # 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| %> +
+
+ <%= form.number_field field, + class: "form-control", + placeholder: "—", + step: "0.01", + min: "0" %> + mg/L +
+ +
+ <% end %> +
+ +
+ <%= form.submit "Ajouter le relevé", class: "btn btn-primary" %> +
+<% 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é" %> + +

Liste des Relevés

+ +
+
+ <%= link_to "Nouvelle mesure", new_nutrient_measurement_path, class: "btn btn-primary" %> + <%= link_to "Retour", root_path, class: "btn btn-outline-secondary" %> +
+
+ +
+ + + + + + + + + + + + <% @nutrient_measurements.each do |m| %> + + + + + + + + <% end %> + +
DateN (total)PKNH₄-N
<%= l(m.measured_on) %> + <%= number_with_precision(m.nno3.to_f + m.nnh4.to_f, precision: 2) if m.nno3 || m.nnh4 %> + <%= number_with_precision(m.p, precision: 2) if m.p %><%= number_with_precision(m.k, precision: 2) if m.k %><%= number_with_precision(m.nnh4, precision: 2) if m.nnh4 %>
+
+ +<%= 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é" %> + +

Ajouter un Relevé

+ +<%= render "form", nutrient_measurement: @nutrient_measurement %> + +
+ +
+ <%= link_to "Retour", root_path, class: "btn btn-secondary" %> +
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 @@ +

Targets#create

+

Find me in app/views/targets/create.html.erb

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 @@ +

Targets#edit

+

Find me in app/views/targets/edit.html.erb

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 @@ +

Cibles

+ +
+ <%= link_to "Nouvelle Cible", new_target_path, class: "btn btn-primary" %> +
+ +
+ + + + + + + + + + + + <% 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" %> + + + + + + + + <% end %> + <% else %> + + + + <% end %> + +
NomRépartition
Total %Créé leActions
+ <%= link_to t.name.presence || "Objectif ##{t.id}", t %> + + <% if t.target_allocations.empty? %> + Aucune répartition définie + <% else %> +
    + <% t.target_allocations.each do |a| %> +
  • + <%= a.nutrient_profile&.name || "Profil ##{a.nutrient_profile_id}" %> + — <%= number_with_precision(a.percentage.to_f, precision: 2) %>% +
  • + <% end %> +
+ <% end %> +
+ + <%= number_with_precision(sum_pct, precision: 2) %>% + + + <%= l(t.created_at, format: :short) %> + + <%= 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 ?" } %> +
+ Aucun objectif pour le moment.  + <%= link_to "Créer le premier", new_target_path %>. +
+
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" %> + +

Ajouter une Cible

+ +<%= form_with(model: @target) do |f| %> +
+
+ <%= f.text_field :name, class: "form-control", placeholder: "Nom de la cible" %> +
+ +
+
+ + + + + + + + + <%= f.fields_for :target_allocations do |af| %> + <% np = af.object.nutrient_profile %> + + + + + <% end %> + + + + + + + +
ProfilProportion
+ <%= af.hidden_field :nutrient_profile_id %> + <%= np&.name.capitalize || "Profil ##{af.object.nutrient_profile_id}" %> + +
+ <%= 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" } %> + % +
+
Ajustez chaque pourcentage pour totaliser 100%. + Total : 0% +
+
+
+ + +
+<% end %> + + 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 @@ +

Targets#show

+ +<%# 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 @@ +

Targets#update

+

Find me in app/views/targets/update.html.erb

diff --git a/config/routes.rb b/config/routes.rb index 50950d4..d5cbeaf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,6 +13,7 @@ Rails.application.routes.draw do # end # resources :fertilizer_products + resources :targets resources :nutrient_profiles resources :nutrient_measurements diff --git a/db/migrate/20250908181137_create_targets.rb b/db/migrate/20250908181137_create_targets.rb new file mode 100644 index 0000000..9144e50 --- /dev/null +++ b/db/migrate/20250908181137_create_targets.rb @@ -0,0 +1,9 @@ +class CreateTargets < ActiveRecord::Migration[8.0] + def change + create_table :targets do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/db/migrate/20250908181147_create_target_allocations.rb b/db/migrate/20250908181147_create_target_allocations.rb new file mode 100644 index 0000000..9141bd3 --- /dev/null +++ b/db/migrate/20250908181147_create_target_allocations.rb @@ -0,0 +1,11 @@ +class CreateTargetAllocations < ActiveRecord::Migration[8.0] + def change + create_table :target_allocations do |t| + t.references :target, null: false, foreign_key: true + t.references :nutrient_profile, null: false, foreign_key: true + t.decimal :percentage + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index feb2fd0..d201f5a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_09_01_112954) do +ActiveRecord::Schema[8.0].define(version: 2025_09_08_181147) do create_table "beds", force: :cascade do |t| t.integer "location", null: false t.datetime "created_at", null: false @@ -122,8 +122,26 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_01_112954) do t.index ["crop_nutrient_need_id"], name: "index_rafts_on_crop_nutrient_need_id" end + create_table "target_allocations", force: :cascade do |t| + t.integer "target_id", null: false + t.integer "nutrient_profile_id", null: false + t.decimal "percentage" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["nutrient_profile_id"], name: "index_target_allocations_on_nutrient_profile_id" + t.index ["target_id"], name: "index_target_allocations_on_target_id" + end + + create_table "targets", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + add_foreign_key "fertilizer_compositions", "fertilizer_components" add_foreign_key "fertilizer_compositions", "fertilizer_products" add_foreign_key "rafts", "beds" add_foreign_key "rafts", "nutrient_profiles", column: "crop_nutrient_need_id" + add_foreign_key "target_allocations", "nutrient_profiles" + add_foreign_key "target_allocations", "targets" end diff --git a/db/seeds/NutrientProfile.rb b/db/seeds/NutrientProfile.rb index 04e16cc..0c3c152 100644 --- a/db/seeds/NutrientProfile.rb +++ b/db/seeds/NutrientProfile.rb @@ -101,7 +101,7 @@ b: 0.11, mn: 0.11, cu: 0.03, - mo: 0.01 }, + mo: 0.01 } ].each do |profile| NutrientProfile.find_or_create_by!(name: profile[:name]) do |p| p.attributes = profile diff --git a/test/controllers/targets_controller_test.rb b/test/controllers/targets_controller_test.rb new file mode 100644 index 0000000..911e3ae --- /dev/null +++ b/test/controllers/targets_controller_test.rb @@ -0,0 +1,33 @@ +require "test_helper" + +class TargetsControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get targets_index_url + assert_response :success + end + + test "should get new" do + get targets_new_url + assert_response :success + end + + test "should get create" do + get targets_create_url + assert_response :success + end + + test "should get edit" do + get targets_edit_url + assert_response :success + end + + test "should get update" do + get targets_update_url + assert_response :success + end + + test "should get show" do + get targets_show_url + assert_response :success + end +end diff --git a/test/fixtures/target_allocations.yml b/test/fixtures/target_allocations.yml new file mode 100644 index 0000000..0ba49ee --- /dev/null +++ b/test/fixtures/target_allocations.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + target: one + nutrient_profile: one + percentage: 9.99 + +two: + target: two + nutrient_profile: two + percentage: 9.99 diff --git a/test/fixtures/targets.yml b/test/fixtures/targets.yml new file mode 100644 index 0000000..7d41224 --- /dev/null +++ b/test/fixtures/targets.yml @@ -0,0 +1,7 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + +two: + name: MyString diff --git a/test/models/target_allocation_test.rb b/test/models/target_allocation_test.rb new file mode 100644 index 0000000..a89cd89 --- /dev/null +++ b/test/models/target_allocation_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TargetAllocationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/target_test.rb b/test/models/target_test.rb new file mode 100644 index 0000000..1d9b332 --- /dev/null +++ b/test/models/target_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TargetTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end -- cgit v1.2.3