From 8ba568ae0ebe715b5da453681eb141886f1977a8 Mon Sep 17 00:00:00 2001 From: Marius Peter Date: Thu, 4 Sep 2025 20:55:03 +0200 Subject: Simpler, Better, Faster... Stronger Start small, address critical customer needs, /then/ scale. --- app/controllers/beds_controller.rb | 66 -------- app/controllers/crops_controller.rb | 58 ------- app/controllers/dashboard_controller.rb | 14 +- app/controllers/fertilizer_products_controller.rb | 70 -------- app/controllers/fertilizers_controller.rb | 70 -------- app/controllers/nutrient_measurement_controller.rb | 4 - app/controllers/nutrient_profiles_controller.rb | 58 +++++++ app/controllers/rafts_controller.rb | 29 ---- app/controllers/recipes_controller.rb | 8 - app/helpers/application_helper.rb | 2 - app/helpers/beds_helper.rb | 2 - app/helpers/dashboard_helper.rb | 8 - app/helpers/nutrients_helper.rb | 21 --- app/helpers/recipes_helper.rb | 16 -- app/javascript/application.js | 2 + app/javascript/controllers/application.js | 9 + app/javascript/controllers/hello_controller.js | 7 + app/javascript/controllers/index.js | 4 + app/models/bed.rb | 5 - app/models/crop.rb | 4 - app/models/fertilizer_component.rb | 3 - app/models/fertilizer_composition.rb | 6 - app/models/fertilizer_product.rb | 7 - app/models/nutrient.rb | 7 - app/models/nutrient_profile.rb | 2 + app/models/raft.rb | 5 - app/services/fertilizer_recipe_calculator.rb | 136 --------------- app/services/target_nutrient_calculator.rb | 33 ---- .../dashboard/_nutrient_profile_allocator.html.erb | 187 +++++++++++++++++++++ app/views/dashboard/_raft_allocation.html.erb | 2 +- app/views/dashboard/index.html.erb | 8 +- app/views/layouts/application.html.erb | 5 +- 32 files changed, 285 insertions(+), 573 deletions(-) delete mode 100644 app/controllers/beds_controller.rb delete mode 100644 app/controllers/crops_controller.rb delete mode 100644 app/controllers/fertilizer_products_controller.rb delete mode 100644 app/controllers/fertilizers_controller.rb delete mode 100644 app/controllers/nutrient_measurement_controller.rb create mode 100644 app/controllers/nutrient_profiles_controller.rb delete mode 100644 app/controllers/rafts_controller.rb delete mode 100644 app/controllers/recipes_controller.rb delete mode 100644 app/helpers/application_helper.rb delete mode 100644 app/helpers/beds_helper.rb delete mode 100644 app/helpers/dashboard_helper.rb delete mode 100644 app/helpers/nutrients_helper.rb delete mode 100644 app/helpers/recipes_helper.rb create mode 100644 app/javascript/controllers/application.js create mode 100644 app/javascript/controllers/hello_controller.js create mode 100644 app/javascript/controllers/index.js delete mode 100644 app/models/bed.rb delete mode 100644 app/models/crop.rb delete mode 100644 app/models/fertilizer_component.rb delete mode 100644 app/models/fertilizer_composition.rb delete mode 100644 app/models/fertilizer_product.rb delete mode 100644 app/models/nutrient.rb create mode 100644 app/models/nutrient_profile.rb delete mode 100644 app/models/raft.rb delete mode 100644 app/services/fertilizer_recipe_calculator.rb delete mode 100644 app/services/target_nutrient_calculator.rb create mode 100644 app/views/dashboard/_nutrient_profile_allocator.html.erb (limited to 'app') diff --git a/app/controllers/beds_controller.rb b/app/controllers/beds_controller.rb deleted file mode 100644 index 2e4d94c..0000000 --- a/app/controllers/beds_controller.rb +++ /dev/null @@ -1,66 +0,0 @@ -class BedsController < ApplicationController - before_action :set_bed, only: %i[ edit update ] - before_action :get_crops, only: %i[ index edit update ] - - def index - @beds = Bed.all - end - - def edit - end - - def update - if @bed.update(bed_params) - redirect_to beds_path, notice: "Bed #{@bed.id} successfully updated." - else - render :edit, status: :unprocessable_entity - end - end - - def bulk_assign_crops - crop = Crop.find(params[:crop_id]) - Raft.update_all(crop_id: crop.id) - redirect_back fallback_location: root_path, notice: "All rafts set to #{crop.name}." - end - - - def reset_seed_crops - # mirrors seed logic - tomatoes = Crop.find_by!(name: "tomatoes") - hot_peppers = Crop.find_by!(name: "hot peppers") - chives = Crop.find_by!(name: "chives") - italian_basil = Crop.find_by!(name: "italian basil") - cabbage_chinese = Crop.find_by!(name: "cabbage, chinese") - lettuce = Crop.find_by!(name: "lettuce") - - Bed.includes(:rafts).find_each do |bed| - default_crop = case bed.location - when 1..2 then tomatoes - when 3 then hot_peppers - when 4 then chives - when 5 then italian_basil - when 6..7 then cabbage_chinese - else lettuce - end - bed.rafts.update_all(crop_id: default_crop.id) - end - redirect_back fallback_location: root_path, notice: "Raft crops reset to default seed layout." - end - - private - - def set_bed - @bed = Bed.find(params[:id]) - end - - def get_crops - @crops = Crop.order(:name) - end - - def bed_params - params.require(:bed).permit( - :location, - rafts_attributes: %i[id crop_id] - ) - end -end diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb deleted file mode 100644 index 951d380..0000000 --- a/app/controllers/crops_controller.rb +++ /dev/null @@ -1,58 +0,0 @@ -class CropsController < ApplicationController - before_action :set_crop, only: %i[ show edit update destroy ] - - def index - @crops = Crop.all - end - - def show - end - - def new - @crop = Crop.new - end - - def edit - end - - def create - @crop = Crop.new(crop_params) - - if @crop.save - redirect_to @crop, notice: "Crop was successfully created." - else - render :new, status: :unprocessable_entity - end - end - - def update - respond_to do |format| - if @crop.update(crop_params) - format.html { redirect_to @crop, notice: "Crop was successfully updated.", status: :see_other } - format.json { render :show, status: :ok, location: @crop } - else - format.html { render :edit, status: :unprocessable_entity } - format.json { render json: @crop.errors, status: :unprocessable_entity } - end - end - end - - def destroy - @crop.destroy! - - respond_to do |format| - format.html { redirect_to crops_path, notice: "Crop was successfully destroyed.", status: :see_other } - format.json { head :no_content } - end - end - - private - - def set_crop - @crop = Crop.find(params.expect(:id)) - end - - def crop_params - params.expect(crop: [ :name, :crop_type, :nno3, :p, :k, :ca, :mg, :s, :na, :cl, :si, :fe, :zn, :b, :mn, :cu, :mo, :nnh4 ]) - end -end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 4e9d560..6f323a9 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,17 +1,19 @@ class DashboardController < ApplicationController def index # Raft allocation by crop type - @raft_data = raft_data_series + # @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, created_at: :desc).first + # @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 = measurement_data_series(:nno3, :p, :k) - @ammonium_measurement_data = measurement_data_series(:nnh4) + # @npk_measurement_data = measurement_data_series(:nno3, :p, :k) + # @ammonium_measurement_data = measurement_data_series(:nnh4) end private diff --git a/app/controllers/fertilizer_products_controller.rb b/app/controllers/fertilizer_products_controller.rb deleted file mode 100644 index ff0d945..0000000 --- a/app/controllers/fertilizer_products_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -class FertilizerProductsController < ApplicationController - before_action :set_fertilizer_product, only: %i[ show edit update destroy ] - - # GET /fertilizer_products or /fertilizer_products.json - def index - @fertilizer_products = FertilizerProduct.all - end - - # GET /fertilizer_products/1 or /fertilizer_products/1.json - def show - end - - # GET /fertilizer_products/new - def new - @fertilizer_product = FertilizerProduct.new - end - - # GET /fertilizer_products/1/edit - def edit - end - - # POST /fertilizer_products or /fertilizer_products.json - def create - @fertilizer_product = FertilizerProduct.new(fertilizer_product_params) - - respond_to do |format| - if @fertilizer_product.save - format.html { redirect_to @fertilizer_product, notice: "Fertilizer product was successfully created." } - format.json { render :show, status: :created, location: @fertilizer_product } - else - format.html { render :new, status: :unprocessable_entity } - format.json { render json: @fertilizer_product.errors, status: :unprocessable_entity } - end - end - end - - # PATCH/PUT /fertilizer_products/1 or /fertilizer_products/1.json - def update - respond_to do |format| - if @fertilizer_product.update(fertilizer_product_params) - format.html { redirect_to @fertilizer_product, notice: "Fertilizer product was successfully updated.", status: :see_other } - format.json { render :show, status: :ok, location: @fertilizer_product } - else - format.html { render :edit, status: :unprocessable_entity } - format.json { render json: @fertilizer_product.errors, status: :unprocessable_entity } - end - end - end - - # DELETE /fertilizer_products/1 or /fertilizer_products/1.json - def destroy - @fertilizer_product.destroy! - - respond_to do |format| - format.html { redirect_to fertilizer_products_path, notice: "Fertilizer product was successfully destroyed.", status: :see_other } - format.json { head :no_content } - end - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_fertilizer_product - @fertilizer_product = FertilizerProduct.find(params.expect(:id)) - end - - # Only allow a list of trusted parameters through. - def fertilizer_product_params - params.expect(fertilizer_product: [ :name, :purity ]) - end -end diff --git a/app/controllers/fertilizers_controller.rb b/app/controllers/fertilizers_controller.rb deleted file mode 100644 index 040c462..0000000 --- a/app/controllers/fertilizers_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -class FertilizersController < ApplicationController - before_action :set_fertilizer, only: %i[ show edit update destroy ] - - # GET /fertilizers or /fertilizers.json - def index - @fertilizers = Fertilizer.all - end - - # GET /fertilizers/1 or /fertilizers/1.json - def show - end - - # GET /fertilizers/new - def new - @fertilizer = Fertilizer.new - end - - # GET /fertilizers/1/edit - def edit - end - - # POST /fertilizers or /fertilizers.json - def create - @fertilizer = Fertilizer.new(fertilizer_params) - - respond_to do |format| - if @fertilizer.save - format.html { redirect_to @fertilizer, notice: "Fertilizer was successfully created." } - format.json { render :show, status: :created, location: @fertilizer } - else - format.html { render :new, status: :unprocessable_entity } - format.json { render json: @fertilizer.errors, status: :unprocessable_entity } - end - end - end - - # PATCH/PUT /fertilizers/1 or /fertilizers/1.json - def update - respond_to do |format| - if @fertilizer.update(fertilizer_params) - format.html { redirect_to @fertilizer, notice: "Fertilizer was successfully updated.", status: :see_other } - format.json { render :show, status: :ok, location: @fertilizer } - else - format.html { render :edit, status: :unprocessable_entity } - format.json { render json: @fertilizer.errors, status: :unprocessable_entity } - end - end - end - - # DELETE /fertilizers/1 or /fertilizers/1.json - def destroy - @fertilizer.destroy! - - respond_to do |format| - format.html { redirect_to fertilizers_path, notice: "Fertilizer was successfully destroyed.", status: :see_other } - format.json { head :no_content } - end - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_fertilizer - @fertilizer = Fertilizer.find(params.expect(:id)) - end - - # Only allow a list of trusted parameters through. - def fertilizer_params - params.expect(fertilizer: [ :name, :formula, :nno3, :nnh4, :p, :k, :ca, :mg, :s, :na, :cl, :si, :fe, :zn, :b, :mn, :cu, :mo ]) - end -end diff --git a/app/controllers/nutrient_measurement_controller.rb b/app/controllers/nutrient_measurement_controller.rb deleted file mode 100644 index 6d26bfa..0000000 --- a/app/controllers/nutrient_measurement_controller.rb +++ /dev/null @@ -1,4 +0,0 @@ -class NutrientMeasurementController < ApplicationController - def index - end -end diff --git a/app/controllers/nutrient_profiles_controller.rb b/app/controllers/nutrient_profiles_controller.rb new file mode 100644 index 0000000..99489d5 --- /dev/null +++ b/app/controllers/nutrient_profiles_controller.rb @@ -0,0 +1,58 @@ +class NutrientProfilesController < ApplicationController + before_action :set_crop, only: %i[ show edit update destroy ] + + def index + @crops = Crop.all + end + + def show + end + + def new + @crop = Crop.new + end + + def edit + end + + def create + @crop = Crop.new(crop_params) + + if @crop.save + redirect_to @crop, notice: "Crop was successfully created." + else + render :new, status: :unprocessable_entity + end + end + + def update + respond_to do |format| + if @crop.update(crop_params) + format.html { redirect_to @crop, notice: "Crop was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @crop } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @crop.errors, status: :unprocessable_entity } + end + end + end + + def destroy + @crop.destroy! + + respond_to do |format| + format.html { redirect_to crops_path, notice: "Crop was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + + def set_crop + @crop = Crop.find(params.expect(:id)) + end + + def crop_params + params.expect(crop: [ :name, :crop_type, :nno3, :p, :k, :ca, :mg, :s, :na, :cl, :si, :fe, :zn, :b, :mn, :cu, :mo, :nnh4 ]) + end +end diff --git a/app/controllers/rafts_controller.rb b/app/controllers/rafts_controller.rb deleted file mode 100644 index c1c8a72..0000000 --- a/app/controllers/rafts_controller.rb +++ /dev/null @@ -1,29 +0,0 @@ -class RaftsController < ApplicationController - before_action :set_raft, only: %i[ edit update ] - before_action :set_crop, only: %i[ edit update ] - - def edit - end - - def update - if @raft.update(raft_params) - redirect_to beds_path, notice: "Raft #{@raft.id} successfully updated." - else - render :edit, status: :unprocessable_entity - end - end - - private - - def set_raft - @raft = Raft.find(params[:id]) - end - - def set_crop - @crops = Crop.order(:name) - end - - def raft_params - params.require(:raft).permit(:location, :bed_id, :crop_id) - end -end diff --git a/app/controllers/recipes_controller.rb b/app/controllers/recipes_controller.rb deleted file mode 100644 index bcc29e7..0000000 --- a/app/controllers/recipes_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class RecipesController < ApplicationController - def show - @latest = NutrientMeasurement.order(:measured_on).last || NutrientMeasurement.new - @target = TargetNutrientCalculator.call - volume = (params[:volume].presence || 100_000).to_i - @recipe = FertilizerRecipeCalculator.call(@latest, @target, water_volume_l: volume) - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb deleted file mode 100644 index de6be79..0000000 --- a/app/helpers/application_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ApplicationHelper -end diff --git a/app/helpers/beds_helper.rb b/app/helpers/beds_helper.rb deleted file mode 100644 index 2f3e2cb..0000000 --- a/app/helpers/beds_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module BedsHelper -end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb deleted file mode 100644 index 5d64d17..0000000 --- a/app/helpers/dashboard_helper.rb +++ /dev/null @@ -1,8 +0,0 @@ -module DashboardHelper - def fmt2(v) = number_with_precision(v, precision: 2) - - # Total nitrogen (NO3-N + NH4-N) - def total_n(measurement) - (measurement.nno3.to_f) + (measurement.nnh4.to_f) - end -end diff --git a/app/helpers/nutrients_helper.rb b/app/helpers/nutrients_helper.rb deleted file mode 100644 index de98dc3..0000000 --- a/app/helpers/nutrients_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -module NutrientsHelper - NUTRIENTS = %i[nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4].freeze - - def percent_delta(measured, target) - return 0.0 if target.to_f.zero? - ((measured.to_f - target.to_f) / target.to_f) * 100.0 - end - - def fmt(v) - number_with_precision(v, precision: 2, strip_insignificant_zeros: true) - end - - def delta_badge_class(delta) - d = delta.abs - case d - when d < 0 then "bg-info" - when 0..5 then "bg-secondary" - else "bg-warning" - end - end -end diff --git a/app/helpers/recipes_helper.rb b/app/helpers/recipes_helper.rb deleted file mode 100644 index 02cb1a7..0000000 --- a/app/helpers/recipes_helper.rb +++ /dev/null @@ -1,16 +0,0 @@ -module RecipesHelper - def commercial_name_for(component) - # Try to find a FertilizerProduct via a join, but gracefully fallback. - if defined?(FertilizerProduct) && defined?(FertilizerComposition) - prod = FertilizerProduct.joins(:fertilizer_compositions) - .where(fertilizer_compositions: { fertilizer_component_id: component.id }) - .first - return prod.name if prod&.name.present? - end - component.name.presence || component.formula - end - - def fmt_kg(v) - number_with_precision(v.to_f, precision: 2, strip_insignificant_zeros: true) - end -end diff --git a/app/javascript/application.js b/app/javascript/application.js index beff742..0d7b494 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1 +1,3 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000..1213e85 --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000..5975c07 --- /dev/null +++ b/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.textContent = "Hello World!" + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000..1156bf8 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,4 @@ +// Import and register all your controllers from the importmap via controllers/**/*_controller +import { application } from "controllers/application" +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) diff --git a/app/models/bed.rb b/app/models/bed.rb deleted file mode 100644 index d41afe6..0000000 --- a/app/models/bed.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Bed < ApplicationRecord - has_many :rafts, -> { order(:location) }, dependent: :destroy - accepts_nested_attributes_for :rafts - validates :location, presence: true, uniqueness: true -end diff --git a/app/models/crop.rb b/app/models/crop.rb deleted file mode 100644 index b0f168d..0000000 --- a/app/models/crop.rb +++ /dev/null @@ -1,4 +0,0 @@ -class Crop < ApplicationRecord - has_many :rafts - enum :crop_type, { leafy: 0, fruit: 1, herb: 2 } -end diff --git a/app/models/fertilizer_component.rb b/app/models/fertilizer_component.rb deleted file mode 100644 index 701ae9b..0000000 --- a/app/models/fertilizer_component.rb +++ /dev/null @@ -1,3 +0,0 @@ -class FertilizerComponent < ApplicationRecord - validates :name, presence: true -end diff --git a/app/models/fertilizer_composition.rb b/app/models/fertilizer_composition.rb deleted file mode 100644 index cf2bb93..0000000 --- a/app/models/fertilizer_composition.rb +++ /dev/null @@ -1,6 +0,0 @@ -class FertilizerComposition < ApplicationRecord - belongs_to :fertilizer_product - belongs_to :fertilizer_component - - validates :percent_w, numericality: { greater_than: 0, less_than_or_equal_to: 100 } -end diff --git a/app/models/fertilizer_product.rb b/app/models/fertilizer_product.rb deleted file mode 100644 index e41316b..0000000 --- a/app/models/fertilizer_product.rb +++ /dev/null @@ -1,7 +0,0 @@ -class FertilizerProduct < ApplicationRecord - has_many :fertilizer_compositions, dependent: :destroy - has_many :fertilizer_components, through: :fertilizer_compositions - - validates :name, presence: true, uniqueness: true - validates :purity, numericality: { greater_than: 0, less_than_or_equal_to: 100 } -end diff --git a/app/models/nutrient.rb b/app/models/nutrient.rb deleted file mode 100644 index c584668..0000000 --- a/app/models/nutrient.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Nutrient < ApplicationRecord - validates :formula, presence: true, uniqueness: true - validates :name, presence: true - - before_update { raise ActiveRecord::ReadOnlyRecord } - before_destroy { raise ActiveRecord::ReadOnlyRecord } -end diff --git a/app/models/nutrient_profile.rb b/app/models/nutrient_profile.rb new file mode 100644 index 0000000..22f2704 --- /dev/null +++ b/app/models/nutrient_profile.rb @@ -0,0 +1,2 @@ +class NutrientProfile < ApplicationRecord +end diff --git a/app/models/raft.rb b/app/models/raft.rb deleted file mode 100644 index 3fe5928..0000000 --- a/app/models/raft.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Raft < ApplicationRecord - belongs_to :bed - belongs_to :crop, optional: true - validates :location, presence: true, uniqueness: { scope: :bed_id } -end diff --git a/app/services/fertilizer_recipe_calculator.rb b/app/services/fertilizer_recipe_calculator.rb deleted file mode 100644 index 28ba8aa..0000000 --- a/app/services/fertilizer_recipe_calculator.rb +++ /dev/null @@ -1,136 +0,0 @@ -class FertilizerRecipeCalculator - NUTRIENTS = %i[nno3 p k ca mg s nnh4].freeze - - def self.call(latest, target, water_volume_l: 100_000) - new(latest, target, water_volume_l).call - end - - def initialize(latest, target, water_volume_l) - @latest = latest || NutrientMeasurement.new - @target = target || NutrientMeasurement.new - @vol_l = water_volume_l.to_f - end - - # Returns a Hash { FertilizerComponent => qty_kg } - def call - deltas_mg_per_l = NUTRIENTS.index_with do |k| - [ (value(@target, k) - value(@latest, k)), 0 ].max - end - - # mg/L * L = mg -> kg - needed_kg = deltas_mg_per_l.transform_values { |mg_l| mg_l * @vol_l / 1_000_000.0 } - - recipe = Hash.new(0.0) - - # 1) P via MAP or DAP (accounts NH4-N) - if (p_need = needed_kg[:p]) > 0 - map = fc_by_formula("MAP") || fc_by_name_like("monoammonium phosphate") - dap = fc_by_formula("DAP") || fc_by_name_like("diammonium phosphate") - carrier = [ map, dap ].compact.find { |c| positive?(c.p) } - if carrier - kg = kg_for(p_need, carrier.p) - recipe[carrier] += kg - needed_kg[:nnh4] = [ needed_kg[:nnh4] - kg * pct(carrier.nnh4), 0 ].max if positive?(carrier.nnh4) - needed_kg[:p] = 0 - end - end - - # 2) NO3-N via KNO3 then Ca(NO3)2 (accounts K/Ca) - if (n_need = needed_kg[:nno3]) > 0 - kno3 = fc_by_formula("KNO3") || fc_by_name_like("potassium nitrate") - can = fc_by_formula("Ca(NO3)2") || fc_by_name_like("calcium nitrate") - - if kno3&.nno3.to_f > 0 - kg = kg_for(n_need, kno3.nno3) - recipe[kno3] += kg - needed_kg[:k] = [ needed_kg[:k] - kg * pct(kno3.k), 0 ].max - n_need -= kg * pct(kno3.nno3) - end - - if n_need > 0 && can&.nno3.to_f > 0 - kg = kg_for(n_need, can.nno3) - recipe[can] += kg - needed_kg[:ca] = [ needed_kg[:ca] - kg * pct(can.ca), 0 ].max - n_need = 0 - end - - needed_kg[:nno3] = [ n_need, 0 ].max - end - - # 3) K via K2SO4 (accounts S) - if (k_need = needed_kg[:k]) > 0 - sop = fc_by_formula("K2SO4") || fc_by_name_like("potassium sulfate") - if sop&.k.to_f > 0 - kg = kg_for(k_need, sop.k) - recipe[sop] += kg - needed_kg[:s] = [ needed_kg[:s] - kg * pct(sop.s), 0 ].max - needed_kg[:k] = 0 - end - end - - # 4) Ca via Ca(NO3)2 (accounts NO3-N) - if (ca_need = needed_kg[:ca]) > 0 - can = recipe.keys.find { |c| norm_formula(c) == "ca(no3)2" } || - fc_by_formula("Ca(NO3)2") || fc_by_name_like("calcium nitrate") - if can&.ca.to_f > 0 - kg = kg_for(ca_need, can.ca) - recipe[can] += kg - needed_kg[:nno3] = [ needed_kg[:nno3] - kg * pct(can.nno3), 0 ].max - needed_kg[:ca] = 0 - end - end - - # 5) Mg via MgSO4 (accounts S) - if (mg_need = needed_kg[:mg]) > 0 - mgs = fc_by_formula("MgSO4") || fc_by_name_like("magnesium sulfate") - if mgs&.mg.to_f > 0 - kg = kg_for(mg_need, mgs.mg) - recipe[mgs] += kg - needed_kg[:s] = [ needed_kg[:s] - kg * pct(mgs.s), 0 ].max - needed_kg[:mg] = 0 - end - end - - # 6) S via K2SO4 or MgSO4 (whichever we already used or find) - if (s_need = needed_kg[:s]) > 0 - sop = recipe.keys.find { |c| norm_formula(c) == "k2so4" } || - fc_by_formula("K2SO4") || fc_by_name_like("potassium sulfate") - mgs = recipe.keys.find { |c| norm_formula(c) == "mgso4" } || - fc_by_formula("MgSO4") || fc_by_name_like("magnesium sulfate") - carrier = [ sop, mgs ].compact.find { |c| positive?(c.s) } - if carrier - kg = kg_for(s_need, carrier.s) - recipe[carrier] += kg - needed_kg[:k] = [ needed_kg[:k] - kg * pct(carrier.k), 0 ].max if positive?(carrier.k) - needed_kg[:mg] = [ needed_kg[:mg] - kg * pct(carrier.mg), 0 ].max if positive?(carrier.mg) - needed_kg[:s] = 0 - end - end - - recipe.delete_if { |_c, kg| kg < 0.01 } - recipe - end - - private - - def value(obj, key) = obj.public_send(key).to_f - - def fc_by_formula(formula) - FertilizerComponent.where("LOWER(formula) = ?", formula.to_s.downcase).first - end - - def fc_by_name_like(name) - FertilizerComponent.where("LOWER(name) LIKE ?", "%#{name.downcase}%").first - end - - def kg_for(need_kg_element, percent_in_product) - return 0.0 unless positive?(percent_in_product) - need_kg_element / pct(percent_in_product) - end - - def pct(v) = v.to_f / 100.0 - - def positive?(v) = v.to_f > 0.0 - - def norm_formula(c) = c.formula.to_s.downcase.gsub(/\s+/, "") -end diff --git a/app/services/target_nutrient_calculator.rb b/app/services/target_nutrient_calculator.rb deleted file mode 100644 index e6cd378..0000000 --- a/app/services/target_nutrient_calculator.rb +++ /dev/null @@ -1,33 +0,0 @@ -class TargetNutrientCalculator - # Derive nutrient columns from the NutrientMeasurement table - NUTRIENT_COLUMNS = (NutrientMeasurement.column_names - %w[id measured_on created_at updated_at]) - .map!(&:to_sym) - .freeze - - # Returns an unsaved NutrientMeasurement with target concentrations (e.g., mg/L) - def self.call - rafts = Raft.includes(:crop).where.not(crop_id: nil) - total = rafts.count - return empty_measurement if total.zero? - - sums = Hash.new(0.0) - - rafts.each do |raft| - NUTRIENT_COLUMNS.each do |col| - v = raft.crop.public_send(col) - sums[col] += v.to_f if v - end - end - - targets = sums.transform_values { |s| s / total } - NutrientMeasurement.new({ measured_on: Date.current }.merge(targets)) - end - - private - - def empty_measurement - NutrientMeasurement.new( - { measured_on: Date.current }.merge(NUTRIENT_COLUMNS.index_with { 0.0 }) - ) - end -end diff --git a/app/views/dashboard/_nutrient_profile_allocator.html.erb b/app/views/dashboard/_nutrient_profile_allocator.html.erb new file mode 100644 index 0000000..d402ace --- /dev/null +++ b/app/views/dashboard/_nutrient_profile_allocator.html.erb @@ -0,0 +1,187 @@ +<%# 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/_raft_allocation.html.erb b/app/views/dashboard/_raft_allocation.html.erb index a2128ea..1c9ef9a 100644 --- a/app/views/dashboard/_raft_allocation.html.erb +++ b/app/views/dashboard/_raft_allocation.html.erb @@ -1,7 +1,7 @@
Crop Allocation
- <%= link_to "Edit allocation", beds_path, class: "btn btn-sm btn-primary" %> + <%#= link_to "Edit allocation", beds_path, class: "btn btn-sm btn-primary" %>
<%= bar_chart @raft_data, stacked: true %> diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index b1b2d87..b8f9145 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -1,7 +1,9 @@

Ferti

-<%= render "raft_allocation" %> +<%= render "nutrient_profile_allocator", nutrient_profiles: @nutrient_profiles %> -<%= render "target_table" %> +<%#= render "raft_allocation" %> -<%= render "nutrient_measurements" %> +<%#= render "target_table" %> + +<%#= render "nutrient_measurements" %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index bf61c9d..dc61670 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -14,8 +14,8 @@ <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> - - + + <%# Includes all stylesheet files in app/assets/stylesheets %> <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> @@ -25,7 +25,6 @@ - <%= javascript_importmap_tags %> -- cgit v1.2.3