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

Ferti

-<%= render "raft_allocation" %> +<%= render "nutrient_profile_allocator", nutrient_profiles: @nutrient_profiles %> -<%= render "target_table" %> +<%#= render "raft_allocation" %> -<%= render "nutrient_measurements" %> +<%#= render "target_table" %> + +<%#= render "nutrient_measurements" %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index bf61c9d..dc61670 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -14,8 +14,8 @@ <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> - - + + <%# Includes all stylesheet files in app/assets/stylesheets %> <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> @@ -25,7 +25,6 @@ - <%= javascript_importmap_tags %> 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_profiles.csv b/db/data/nutrient_profiles.csv new file mode 100644 index 0000000..61406b8 --- /dev/null +++ b/db/data/nutrient_profiles.csv @@ -0,0 +1,9 @@ +Nom,NNO3,P,K,Ca,Mg,S,Na,Cl,Si,Fe,Zn,B,Mn,Cu,Mo,NNH4 +Générique croissance,160,30,230,100,30,60,0,0,0,5,"0,15","0,3","0,5","0,15","0,05",0 +Générique floraison,130,60,300,100,30,60,0,0,0,2,"0,1","0,5","0,5","0,05","0,05",0 +Laitue,190,50,210,200,50,66,0,0,0,5,"0,15","0,3","0,5","0,15","0,05",0 +Tomate (cycle entier),140,50,352,180,50,168,0,0,0,5,"0,1","0,3","0,8","0,07","0,03",0 +Tomate 10-14 jours,100,40,200,100,20,53,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0 +Tomate 1ere grappe,130,55,300,150,33,109,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0 +Tomate à maturité,180,65,400,400,45,144,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0 +Framboise - tous stades,70,12,88,90,24,48,0,0,50,0.56,"0,325",11,"0,11","0,032","0,01",0 diff --git a/db/data/nutrient_requirements.csv b/db/data/nutrient_requirements.csv deleted file mode 100644 index 61406b8..0000000 --- a/db/data/nutrient_requirements.csv +++ /dev/null @@ -1,9 +0,0 @@ -Nom,NNO3,P,K,Ca,Mg,S,Na,Cl,Si,Fe,Zn,B,Mn,Cu,Mo,NNH4 -Générique croissance,160,30,230,100,30,60,0,0,0,5,"0,15","0,3","0,5","0,15","0,05",0 -Générique floraison,130,60,300,100,30,60,0,0,0,2,"0,1","0,5","0,5","0,05","0,05",0 -Laitue,190,50,210,200,50,66,0,0,0,5,"0,15","0,3","0,5","0,15","0,05",0 -Tomate (cycle entier),140,50,352,180,50,168,0,0,0,5,"0,1","0,3","0,8","0,07","0,03",0 -Tomate 10-14 jours,100,40,200,100,20,53,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0 -Tomate 1ere grappe,130,55,300,150,33,109,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0 -Tomate à maturité,180,65,400,400,45,144,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0 -Framboise - tous stades,70,12,88,90,24,48,0,0,50,0.56,"0,325",11,"0,11","0,032","0,01",0 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/2_nutrient_measurements.rb b/db/seeds/2_nutrient_measurements.rb deleted file mode 100644 index 0ca14f1..0000000 --- a/db/seeds/2_nutrient_measurements.rb +++ /dev/null @@ -1,27 +0,0 @@ -require "csv" - -csv_path = Rails.root.join("db", "data", "dolibarr_measurements.csv") -abort "CSV not found: #{csv_path}" unless File.exist?(csv_path) - -CSV.foreach(csv_path, headers: true) do |row| - NutrientMeasurement.find_or_create_by!(measured_on: Date.parse(row["date"])) do |m| - m.nno3 = row["nno3"] - m.p = row["p"] - m.k = row["k"] - m.ca = row["ca"] - m.mg = row["mg"] - m.s = row["s"] - m.na = row["na"] - m.cl = row["cl"] - m.si = row["si"] - m.fe = row["fe"] - m.zn = row["zn"] - m.b = row["b"] - m.mn = row["mn"] - m.cu = row["cu"] - m.mo = row["mo"] - m.nnh4 = row["nnh4"] - end -end - -puts "NutrientMeasurements: #{NutrientMeasurement.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/NutrientMeasurement.rb b/db/seeds/NutrientMeasurement.rb new file mode 100644 index 0000000..fcf8cfa --- /dev/null +++ b/db/seeds/NutrientMeasurement.rb @@ -0,0 +1,25 @@ +require "csv" + +csv_path = Rails.root.join("db", "data", "dolibarr_measurements.csv") +abort "CSV not found: #{csv_path}" unless File.exist?(csv_path) + +CSV.foreach(csv_path, headers: true) do |row| + NutrientMeasurement.find_or_create_by!(measured_on: Date.parse(row["date"])) do |m| + m.nno3 = row["nno3"] + m.p = row["p"] + m.k = row["k"] + m.ca = row["ca"] + m.mg = row["mg"] + m.s = row["s"] + m.na = row["na"] + m.cl = row["cl"] + m.si = row["si"] + m.fe = row["fe"] + m.zn = row["zn"] + m.b = row["b"] + m.mn = row["mn"] + m.cu = row["cu"] + m.mo = row["mo"] + m.nnh4 = row["nnh4"] + end +end 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 -- cgit v1.2.3