summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarius Peter <dev@marius-peter.com>2025-09-04 20:55:03 +0200
committerMarius Peter <dev@marius-peter.com>2025-09-04 20:55:03 +0200
commit8ba568ae0ebe715b5da453681eb141886f1977a8 (patch)
tree3bf3a3d5b41286d0a6da8895857314393bae1db2
parent8f8cea2a0408918629dc8bab03495530861a71f3 (diff)
Simpler, Better, Faster... StrongerHEADmaster
Start small, address critical customer needs, /then/ scale.
-rw-r--r--app/controllers/beds_controller.rb66
-rw-r--r--app/controllers/dashboard_controller.rb14
-rw-r--r--app/controllers/fertilizer_products_controller.rb70
-rw-r--r--app/controllers/fertilizers_controller.rb70
-rw-r--r--app/controllers/nutrient_measurement_controller.rb4
-rw-r--r--app/controllers/nutrient_profiles_controller.rb (renamed from app/controllers/crops_controller.rb)2
-rw-r--r--app/controllers/rafts_controller.rb29
-rw-r--r--app/controllers/recipes_controller.rb8
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/beds_helper.rb2
-rw-r--r--app/helpers/dashboard_helper.rb8
-rw-r--r--app/helpers/nutrients_helper.rb21
-rw-r--r--app/helpers/recipes_helper.rb16
-rw-r--r--app/javascript/application.js2
-rw-r--r--app/javascript/controllers/application.js9
-rw-r--r--app/javascript/controllers/hello_controller.js7
-rw-r--r--app/javascript/controllers/index.js4
-rw-r--r--app/models/bed.rb5
-rw-r--r--app/models/crop.rb4
-rw-r--r--app/models/fertilizer_component.rb3
-rw-r--r--app/models/fertilizer_composition.rb6
-rw-r--r--app/models/fertilizer_product.rb7
-rw-r--r--app/models/nutrient.rb7
-rw-r--r--app/models/nutrient_profile.rb2
-rw-r--r--app/models/raft.rb5
-rw-r--r--app/services/fertilizer_recipe_calculator.rb136
-rw-r--r--app/services/target_nutrient_calculator.rb33
-rw-r--r--app/views/dashboard/_nutrient_profile_allocator.html.erb187
-rw-r--r--app/views/dashboard/_raft_allocation.html.erb2
-rw-r--r--app/views/dashboard/index.html.erb8
-rw-r--r--app/views/layouts/application.html.erb5
-rw-r--r--config/importmap.rb4
-rw-r--r--config/routes.rb20
-rw-r--r--db/data/dolibarr_plant_requirements.csv9
-rw-r--r--db/data/nutrient_profiles.csv (renamed from db/data/nutrient_requirements.csv)0
-rw-r--r--db/migrate/20250901112954_rename_crops_to_nutrient_profiles.rb10
-rw-r--r--db/schema.rb53
-rw-r--r--db/seeds.rb13
-rw-r--r--db/seeds/1_nutrients.rb24
-rw-r--r--db/seeds/3_crops.rb125
-rw-r--r--db/seeds/4_beds_and_rafts.rb24
-rw-r--r--db/seeds/5_fertilizer_components.rb.bkp38
-rw-r--r--db/seeds/6_fertilizer_products.rb143
-rw-r--r--db/seeds/NutrientMeasurement.rb (renamed from db/seeds/2_nutrient_measurements.rb)2
-rw-r--r--db/seeds/NutrientProfile.rb109
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&nbsp;: <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
Copyright 2019--2025 Marius PETER