diff options
author | Marius Peter <dev@marius-peter.com> | 2025-09-04 20:55:03 +0200 |
---|---|---|
committer | Marius Peter <dev@marius-peter.com> | 2025-09-04 20:55:03 +0200 |
commit | 8ba568ae0ebe715b5da453681eb141886f1977a8 (patch) | |
tree | 3bf3a3d5b41286d0a6da8895857314393bae1db2 | |
parent | 8f8cea2a0408918629dc8bab03495530861a71f3 (diff) |
Start small, address critical customer needs, /then/ scale.
45 files changed, 407 insertions, 911 deletions
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/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/crops_controller.rb b/app/controllers/nutrient_profiles_controller.rb index 951d380..99489d5 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/nutrient_profiles_controller.rb @@ -1,4 +1,4 @@ -class CropsController < ApplicationController +class NutrientProfilesController < ApplicationController before_action :set_crop, only: %i[ show edit update destroy ] def index 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<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/_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 @@ <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" %> + <%#= link_to "Edit allocation", beds_path, class: "btn btn-sm btn-primary" %> </div> <%= 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 @@ <h1 class="display-1">Ferti</h1> -<%= 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) %> <link rel="icon" href="/icon.png" type="image/png"> - <link rel="icon" href="/icon.svg" type="image/svg+xml"> - <link rel="apple-touch-icon" href="/icon.png"> + <!-- <link rel="icon" href="/icon.svg" type="image/svg+xml"> --> + <!-- <link rel="apple-touch-icon" href="/icon.png"> --> <%# Includes all stylesheet files in app/assets/stylesheets %> <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> @@ -25,7 +25,6 @@ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chartkick@5"></script> - <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script> <%= javascript_importmap_tags %> diff --git a/config/importmap.rb b/config/importmap.rb index 0086a32..909dfc5 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -1,3 +1,7 @@ # Pin npm packages by running ./bin/importmap pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers" diff --git a/config/routes.rb b/config/routes.rb index 1dd57b5..50950d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,19 +1,19 @@ Rails.application.routes.draw do root "dashboard#index" - get "ferti_recipe", to: "recipes#show" + # get "ferti_recipe", to: "recipes#show" - resources :beds, only: [ :index, :edit, :update ] do - collection do - patch :bulk_assign_crops - post :reset_seed_crops - end + # resources :beds, only: [ :index, :edit, :update ] do + # collection do + # patch :bulk_assign_crops + # post :reset_seed_crops + # end - resources :rafts, only: [ :index, :edit, :update ], shallow: true - end + # resources :rafts, only: [ :index, :edit, :update ], shallow: true + # end - resources :fertilizer_products - resources :crops + # resources :fertilizer_products + resources :nutrient_profiles resources :nutrient_measurements # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html diff --git a/db/data/dolibarr_plant_requirements.csv b/db/data/dolibarr_plant_requirements.csv new file mode 100644 index 0000000..333ea38 --- /dev/null +++ b/db/data/dolibarr_plant_requirements.csv @@ -0,0 +1,9 @@ +Cycles et productions,NNO3,P,K,Ca,Mg,S,Fe,Zn,B,Mn,Cu,Mo +Formule moyenne générale,160.00,30.00,230.00,100.00,30.00,60.00,5.00,0.15,0.30,0.50,0.15,0.05 +Salades,130.00,60.00,300.00,100.00,30.00,60.00,2.00,0.10,0.50,0.50,0.05,0.05 +Développement floral,190.00,50.00,210.00,200.00,50.00,66.00,5.00,0.15,0.30,0.50,0.15,0.05 +Tomates général,140.00,50.00,352.00,180.00,50.00,168.00,5.00,0.10,0.30,0.80,0.07,0.03 +Jeunes tomates,100.00,40.00,200.00,100.00,20.00,53.00,3.00,0.10,0.30,0.80,0.07,0.03 +Tomate premiers fruits,130.00,55.00,300.00,150.00,33.00,109.00,3.00,0.10,0.30,0.80,0.07,0.03 +Tomate mûre,180.00,65.00,400.00,400.00,45.00,144.00,3.00,0.10,0.30,0.80,0.07,0.03 +Framboise - tous stades,70.00,12.00,88.00,90.00,24.00,48.00,0.56,0.33,0.11,0.11,0.03,0.01 diff --git a/db/data/nutrient_requirements.csv b/db/data/nutrient_profiles.csv index 61406b8..61406b8 100644 --- a/db/data/nutrient_requirements.csv +++ b/db/data/nutrient_profiles.csv diff --git a/db/migrate/20250901112954_rename_crops_to_nutrient_profiles.rb b/db/migrate/20250901112954_rename_crops_to_nutrient_profiles.rb new file mode 100644 index 0000000..5b46df1 --- /dev/null +++ b/db/migrate/20250901112954_rename_crops_to_nutrient_profiles.rb @@ -0,0 +1,10 @@ +class RenameCropsToNutrientProfiles < ActiveRecord::Migration[8.0] + def change + rename_table :crops, :nutrient_profiles + + rename_column :rafts, :crop_id, :crop_nutrient_need_id + add_index :rafts, :crop_nutrient_need_id unless index_exists?(:rafts, :crop_nutrient_need_id) + + remove_column :nutrient_profiles, :crop_type + end +end diff --git a/db/schema.rb b/db/schema.rb index b5f2e03..feb2fd0 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_08_24_163257) do +ActiveRecord::Schema[8.0].define(version: 2025_09_01_112954) do create_table "beds", force: :cascade do |t| t.integer "location", null: false t.datetime "created_at", null: false @@ -18,29 +18,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_24_163257) do t.index ["location"], name: "index_beds_on_location", unique: true end - create_table "crops", force: :cascade do |t| - t.string "name" - t.integer "crop_type", default: 1, null: false - t.float "nno3" - t.float "p" - t.float "k" - t.float "ca" - t.float "mg" - t.float "s" - t.float "na" - t.float "cl" - t.float "si" - t.float "fe" - t.float "zn" - t.float "b" - t.float "mn" - t.float "cu" - t.float "mo" - t.float "nnh4" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - create_table "fertilizer_components", force: :cascade do |t| t.string "name" t.string "formula" @@ -105,6 +82,28 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_24_163257) do t.index ["measured_on"], name: "index_nutrient_measurements_on_measured_on", unique: true end + create_table "nutrient_profiles", force: :cascade do |t| + t.string "name" + t.float "nno3" + t.float "p" + t.float "k" + t.float "ca" + t.float "mg" + t.float "s" + t.float "na" + t.float "cl" + t.float "si" + t.float "fe" + t.float "zn" + t.float "b" + t.float "mn" + t.float "cu" + t.float "mo" + t.float "nnh4" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "nutrients", force: :cascade do |t| t.string "formula" t.string "name" @@ -115,16 +114,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_24_163257) do create_table "rafts", force: :cascade do |t| t.integer "bed_id", null: false t.integer "location", null: false - t.integer "crop_id" + t.integer "crop_nutrient_need_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["bed_id", "location"], name: "index_rafts_on_bed_id_and_location", unique: true t.index ["bed_id"], name: "index_rafts_on_bed_id" - t.index ["crop_id"], name: "index_rafts_on_crop_id" + t.index ["crop_nutrient_need_id"], name: "index_rafts_on_crop_nutrient_need_id" end add_foreign_key "fertilizer_compositions", "fertilizer_components" add_foreign_key "fertilizer_compositions", "fertilizer_products" add_foreign_key "rafts", "beds" - add_foreign_key "rafts", "crops" + add_foreign_key "rafts", "nutrient_profiles", column: "crop_nutrient_need_id" end diff --git a/db/seeds.rb b/db/seeds.rb index 848db0d..245247e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -8,6 +8,15 @@ # MovieGenre.find_or_create_by!(name: genre_name) # end -Dir[Rails.root.join("db/seeds/*.rb")].sort.each do |seed| - load seed +MODELS = [ NutrientMeasurement, + NutrientProfile + ].freeze + +def seed_file_for(model_class) + Rails.root.join("db", "seeds", "#{model_class.name}.rb") +end + +MODELS.each do |model| + load seed_file_for(model) + printf "%-22s %3s\n", model.name, model.count end diff --git a/db/seeds/1_nutrients.rb b/db/seeds/1_nutrients.rb deleted file mode 100644 index dab2249..0000000 --- a/db/seeds/1_nutrients.rb +++ /dev/null @@ -1,24 +0,0 @@ -NUTRIENTS = [ - [ "nno3", "nitrate" ], - [ "p", "phosphore" ], - [ "k", "potassium" ], - [ "ca", "calcium" ], - [ "mg", "magnésium" ], - [ "s", "soufre" ], - [ "na", "sodium" ], - [ "cl", "chlore" ], - [ "si", "silice" ], - [ "fe", "fer" ], - [ "zn", "zinc" ], - [ "b", "bore" ], - [ "mn", "manganèse" ], - [ "cu", "cuivre" ], - [ "mo", "molybdène" ], - [ "nnh4", "ammonium" ] -] - -NUTRIENTS.each do |formula, name| - Nutrient.find_or_create_by!(formula:) { |n| n.name = name } -end - -puts "Nutrients: #{Nutrient.count}" diff --git a/db/seeds/3_crops.rb b/db/seeds/3_crops.rb deleted file mode 100644 index 6a5c10f..0000000 --- a/db/seeds/3_crops.rb +++ /dev/null @@ -1,125 +0,0 @@ -# crop_type: 0=leafy greens, 1=fruits, 2=herbs - -LEAFY = { - nno3: 150, - p: 31, - k: 210, - ca: 90, - mg: 24, - s: 32, - fe: 1.0, - mn: 0.25, - zn: 0.13, - b: 0.16, - cu: 0.023, - mo: 0.024, - nnh4: 5, - # keep low for leafy - na: 10, - cl: 5, - si: 20 -} - -HERB = LEAFY # Herbs run well on the Cornell leafy recipe - -TOMATO = { - # CEAC Stage 3 "multi-crop" / mature tomato - nno3: 190, - p: 47, - k: 350, - ca: 200, - mg: 65, - s: 102, - fe: 2.0, - mn: 0.55, - zn: 0.33, - b: 0.28, - cu: 0.05, - mo: 0.05, - nnh4: 0, - # CEAC recipes are NO3-N; keep NH4 minimal - na: 20, - cl: 25, - si: 20 -} - -HOT_PEPPER = { - # Mature greenhouse pepper (HortAmericas summary of UA recipes) - nno3: 180, - p: 50, - k: 280, - ca: 200, - mg: 45, - s: 20, - fe: 1.0, - mn: 0.55, - zn: 0.33, - b: 0.30, - cu: 0.05, - mo: 0.05, - nnh4: 15, - na: 20, - cl: 10, - si: 20 -} - -STRAWBERRY = { - # UA strawberry (Yamazaki) emphasizes lower EC; use conservative macros - nno3: 120, - p: 30, - k: 200, - ca: 120, - mg: 35, - s: 50, - fe: 1.5, - mn: 0.50, - zn: 0.20, - b: 0.30, - cu: 0.05, - mo: 0.05, - nnh4: 5, - na: 10, - cl: 5, - si: 20 -} - -RASPBERRY = { - # Sparse data; align with strawberry but a touch higher vigor - nno3: 140, - p: 35, - k: 230, - ca: 150, - mg: 40, - s: 50, - fe: 1.5, - mn: 0.50, - zn: 0.20, - b: 0.30, - cu: 0.05, - mo: 0.05, - nnh4: 5, - na: 15, - cl: 5, - si: 20 -} - -[ - [ "lettuce", 0, LEAFY ], - [ "kale", 0, LEAFY ], - [ "cabbage, chinese", 0, LEAFY ], - [ "tomatoes", 1, TOMATO ], - [ "raspberries", 1, RASPBERRY ], - [ "strawberries", 1, STRAWBERRY ], - [ "hot peppers", 1, HOT_PEPPER ], - [ "parsley", 2, HERB ], - [ "chives", 2, HERB ], - [ "italian basil", 2, HERB ], - [ "dill", 2, HERB ] -].each do |name, type, nutrient_requirements| - Crop.find_or_create_by!(name: name) do |c| - c.crop_type = type - c.attributes = nutrient_requirements - end -end - -puts "Crops: #{Crop.count}" diff --git a/db/seeds/4_beds_and_rafts.rb b/db/seeds/4_beds_and_rafts.rb deleted file mode 100644 index 1cd95dc..0000000 --- a/db/seeds/4_beds_and_rafts.rb +++ /dev/null @@ -1,24 +0,0 @@ -BEDS = 14 -RAFTS = 10 - -1.upto(BEDS) do |b| - bed = Bed.find_or_create_by!(location: b) - - crop_name = case b - 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 - - 1.upto(RAFTS) do |r| - raft = bed.rafts.find_or_create_by!(location: r) do |raft| - raft.crop = Crop.find_by!(name: crop_name) - end - end -end - -puts "Beds: #{Bed.count}" -puts "Rafts: #{Raft.count}" diff --git a/db/seeds/5_fertilizer_components.rb.bkp b/db/seeds/5_fertilizer_components.rb.bkp deleted file mode 100644 index 8a1fc54..0000000 --- a/db/seeds/5_fertilizer_components.rb.bkp +++ /dev/null @@ -1,38 +0,0 @@ -FERTILIZER_COMPONENTS = [ - # Macros / bases - { name: "Potassium Nitrate", formula: "KNO3", nno3: 13.50, k: 38.60 }, - { name: "Calcium Nitrate", formula: "Ca(NO3)2·xH2O", nno3: 15.50, ca: 18.94 }, - { name: "Ammonium Nitrate", formula: "NH4NO3", nno3: 13.50, nnh4: 13.50 }, # total N ≈ 27 - { name: "Diammonium Phosphate", formula: "(NH4)2HPO4", p: 21.00 }, # NH4 can be added later if desired - - # Acids / buffers - { name: "Nitric Acid 53%", formula: "HNO3", nno3: 11.70 }, - { name: "Potassium Bicarbonate", formula: "KHCO3", k: 39.05 }, - { name: "Calcium Carbonate", formula: "CaCO3", ca: 38.00 }, - - # K–Mg–S complex (Patentkali-type blend) - { name: "Potassium Sulfate blend (K–Mg–S + NaCl)", formula: "K2SO4+MgSO4+NaCl", - p: 0.44, k: 22.66, mg: 6.16, s: 17.77, na: 2.16, cl: 3.34 }, - - # Sulfates (micros / secondary) - { name: "Magnesium Sulfate", formula: "MgSO4·7H2O", mg: 9.648, s: 13.016 }, - { name: "Manganese Sulfate", formula: "MnSO4", s: 7.17, mn: 12.00 }, - { name: "Zinc Sulfate (Fiza Zinc)", formula: "ZnSO4", zn: 12.00 }, - - # Chelates / traces - { name: "Iron DTPA 11.8%", formula: "Fe-DTPA", fe: 11.80 }, - { name: "HelioCopper (Cu chelate)", formula: "Cu-chelate", cu: 40.00 }, - - # Boron & Mo sources - { name: "Boron–Molybdenum (Boronia LS)", formula: "B+Mo", b: 13.50, mo: 0.028 }, - { name: "Boronia MO12 (10L)", formula: "B+Mn+Cu", b: 8.90, mn: 0.089, cu: 0.89 }, - { name: "Sodium Molybdate", formula: "Na2MoO4", mo: 39.50 } -] - -FERTILIZER_COMPONENTS.each do |attrs| - FertilizerComponent.find_or_create_by!(name: attrs[:name]) do |c| - c.attributes = attrs - end -end - -puts "Fertilizer components: #{FertilizerComponent.count}" diff --git a/db/seeds/6_fertilizer_products.rb b/db/seeds/6_fertilizer_products.rb deleted file mode 100644 index 6769bde..0000000 --- a/db/seeds/6_fertilizer_products.rb +++ /dev/null @@ -1,143 +0,0 @@ -FERTILIZER_RECIPES = [ - { - name: "Multi K Reci", - purity: 99.0, - composition: [ - { component: { - name: "Potassium nitrate", - formula: "KNO3", - nno3: 13.0, - k: 46.0 }, - percent_w: 100.0 } - ] - }, - { - name: "Fixa Mn", - purity: 98.0, - composition: [ - { component: { - name: "Manganese sulfate", - formula: "MnSO4·H2O", - mn: 32.0, - s: 18.0 }, - percent_w: 100.0 } - ] - }, - { - name: "Multi-Cal Haïfa", - purity: 99.0, - composition: [ - { component: { - name: "Calcium nitrate", - formula: "Ca(NO3)2·4H2O", - nno3: 15.5, - ca: 19.0 }, - percent_w: 100.0 } - ] - }, - { - name: "DAP 18/46/00", - purity: 100.0, - composition: [ - { component: { - name: "Diammonium phosphate", - formula: "(NH4)2HPO4", - nnh4: 18.0, - p: 20.0 }, - percent_w: 100.0 } - ] - }, - { - name: "Patenkali", - purity: 100.0, - composition: [ - { component: { - name: "Potassium sulfate", - formula: "K2SO4", - k: 50.0, - s: 18.0 - }, percent_w: 100.0 } - ] - }, - { - name: "Eso Top", - purity: 100.0, - composition: [ - { component: { - name: "Magnesium sulfate", - formula: "MgSO4·7H2O", - mg: 9.8, - s: 13.0 }, - percent_w: 100.0 } - ] - }, - { - name: "Ammonitrate 27", - purity: 100.0, - composition: [ - { component: { - name: "Ammonium nitrate", - formula: "NH4NO3", - nno3: 13.5, - nnh4: 13.5 }, - percent_w: 100.0 } - ] - }, - { - name: "Fer chélaté", - purity: 100.0, - composition: [ - { component: { - name: "Iron chelate (EDDHA)", - formula: "Fe-EDDHA", - fe: 6.0 }, - percent_w: 100.0 } - ] - }, - { - name: "Carbonate de calcium", - purity: 100.0, - composition: [ - { component: { - name: "Calcium carbonate", - formula: "CaCO3", - ca: 40.0 }, - percent_w: 100.0 } - ] - }, - { - name: "Héliocuivre", - purity: 100.0, - composition: [ - { component: { - name: "Copper chelate (EDTA)", - formula: "Cu-EDTA", - cu: 14.0 }, - percent_w: 100.0 } - ] - } -] - -FERTILIZER_RECIPES.each do |recipe| - product = FertilizerProduct.find_or_create_by!(name: recipe[:name]) do |fp| - fp.purity = recipe[:purity] - end - - recipe[:composition].each do |c| - comp_attrs = c[:component] - component = FertilizerComponent.find_or_create_by!(name: comp_attrs[:name]) do |fc| - fc.formula = comp_attrs[:formula] - end - # update nutrient fields if missing - component.update!(comp_attrs.except(:name, :formula)) - - FertilizerComposition.find_or_create_by!( - fertilizer_product: product, - fertilizer_component: component - ) do |fc| - fc.percent_w = c[:percent_w] - end - end -end - -puts "FertilizerProducts: #{FertilizerProduct.count}" diff --git a/db/seeds/2_nutrient_measurements.rb b/db/seeds/NutrientMeasurement.rb index 0ca14f1..fcf8cfa 100644 --- a/db/seeds/2_nutrient_measurements.rb +++ b/db/seeds/NutrientMeasurement.rb @@ -23,5 +23,3 @@ CSV.foreach(csv_path, headers: true) do |row| m.nnh4 = row["nnh4"] end end - -puts "NutrientMeasurements: #{NutrientMeasurement.count}" diff --git a/db/seeds/NutrientProfile.rb b/db/seeds/NutrientProfile.rb new file mode 100644 index 0000000..04e16cc --- /dev/null +++ b/db/seeds/NutrientProfile.rb @@ -0,0 +1,109 @@ +[ { name: "formule moyenne générale", + nno3: 160.00, + p: 30.00, + k: 230.00, + ca: 100.00, + mg: 30.00, + s: 60.00, + fe: 5.00, + zn: 0.15, + b: 0.30, + mn: 0.50, + cu: 0.15, + mo: 0.05 }, + { name: "salades", + nno3: 130.00, + p: 60.00, + k: 300.00, + ca: 100.00, + mg: 30.00, + s: 60.00, + fe: 2.00, + zn: 0.10, + b: 0.50, + mn: 0.50, + cu: 0.05, + mo: 0.05 }, + { name: "développement floral", + nno3: 190.00, + p: 50.00, + k: 210.00, + ca: 200.00, + mg: 50.00, + s: 66.00, + fe: 5.00, + zn: 0.15, + b: 0.30, + mn: 0.50, + cu: 0.15, + mo: 0.05 }, + { name: "tomates général", + nno3: 140.00, + p: 50.00, + k: 352.00, + ca: 180.00, + mg: 50.00, + s: 168.00, + fe: 5.00, + zn: 0.10, + b: 0.30, + mn: 0.80, + cu: 0.07, + mo: 0.03 }, + { name: "jeunes tomates", + nno3: 100.00, + p: 40.00, + k: 200.00, + ca: 100.00, + mg: 20.00, + s: 53.00, + fe: 3.00, + zn: 0.10, + b: 0.30, + mn: 0.80, + cu: 0.07, + mo: 0.03 }, + { name: "tomate premiers fruits", + nno3: 130.00, + p: 55.00, + k: 300.00, + ca: 150.00, + mg: 33.00, + s: 109.00, + fe: 3.00, + zn: 0.10, + b: 0.30, + mn: 0.80, + cu: 0.07, + mo: 0.03 }, + { name: "tomate mûre", + nno3: 180.00, + p: 65.00, + k: 400.00, + ca: 400.00, + mg: 45.00, + s: 144.00, + fe: 3.00, + zn: 0.10, + b: 0.30, + mn: 0.80, + cu: 0.07, + mo: 0.03 }, + { name: "framboise - tous stades", + nno3: 70.00, + p: 12.00, + k: 88.00, + ca: 90.00, + mg: 24.00, + s: 48.00, + fe: 0.56, + zn: 0.33, + b: 0.11, + mn: 0.11, + cu: 0.03, + mo: 0.01 }, +].each do |profile| + NutrientProfile.find_or_create_by!(name: profile[:name]) do |p| + p.attributes = profile + end +end |