Simpler, Better, Faster... Stronger

Start small, address critical customer needs, /then/ scale.

Commit
8ba568ae0ebe715b5da453681eb141886f1977a8
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
app/controllers/beds_controller.rb
index 2e4d94c1..00000000 100644..000000
@@ -1,66 +0,0 @@
1 Removed: class BedsController < ApplicationController
2 Removed: before_action :set_bed, only: %i[ edit update ]
3 Removed: before_action :get_crops, only: %i[ index edit update ]
4 Removed:
5 Removed: def index
6 Removed: @beds = Bed.all
7 Removed: end
8 Removed:
9 Removed: def edit
10 Removed: end
11 Removed:
12 Removed: def update
13 Removed: if @bed.update(bed_params)
14 Removed: redirect_to beds_path, notice: "Bed #{@bed.id} successfully updated."
15 Removed: else
16 Removed: render :edit, status: :unprocessable_entity
17 Removed: end
18 Removed: end
19 Removed:
20 Removed: def bulk_assign_crops
21 Removed: crop = Crop.find(params[:crop_id])
22 Removed: Raft.update_all(crop_id: crop.id)
23 Removed: redirect_back fallback_location: root_path, notice: "All rafts set to #{crop.name}."
24 Removed: end
25 Removed:
26 Removed:
27 Removed: def reset_seed_crops
28 Removed: # mirrors seed logic
29 Removed: tomatoes = Crop.find_by!(name: "tomatoes")
30 Removed: hot_peppers = Crop.find_by!(name: "hot peppers")
31 Removed: chives = Crop.find_by!(name: "chives")
32 Removed: italian_basil = Crop.find_by!(name: "italian basil")
33 Removed: cabbage_chinese = Crop.find_by!(name: "cabbage, chinese")
34 Removed: lettuce = Crop.find_by!(name: "lettuce")
35 Removed:
36 Removed: Bed.includes(:rafts).find_each do |bed|
37 Removed: default_crop = case bed.location
38 Removed: when 1..2 then tomatoes
39 Removed: when 3 then hot_peppers
40 Removed: when 4 then chives
41 Removed: when 5 then italian_basil
42 Removed: when 6..7 then cabbage_chinese
43 Removed: else lettuce
44 Removed: end
45 Removed: bed.rafts.update_all(crop_id: default_crop.id)
46 Removed: end
47 Removed: redirect_back fallback_location: root_path, notice: "Raft crops reset to default seed layout."
48 Removed: end
49 Removed:
50 Removed: private
51 Removed:
52 Removed: def set_bed
53 Removed: @bed = Bed.find(params[:id])
54 Removed: end
55 Removed:
56 Removed: def get_crops
57 Removed: @crops = Crop.order(:name)
58 Removed: end
59 Removed:
60 Removed: def bed_params
61 Removed: params.require(:bed).permit(
62 Removed: :location,
63 Removed: rafts_attributes: %i[id crop_id]
64 Removed: )
65 Removed: end
66 Removed: end
app/controllers/crops_controller.rb
index 951d3805..00000000 100644..000000
@@ -1,58 +0,0 @@
1 Removed: class CropsController < ApplicationController
2 Removed: before_action :set_crop, only: %i[ show edit update destroy ]
3 Removed:
4 Removed: def index
5 Removed: @crops = Crop.all
6 Removed: end
7 Removed:
8 Removed: def show
9 Removed: end
10 Removed:
11 Removed: def new
12 Removed: @crop = Crop.new
13 Removed: end
14 Removed:
15 Removed: def edit
16 Removed: end
17 Removed:
18 Removed: def create
19 Removed: @crop = Crop.new(crop_params)
20 Removed:
21 Removed: if @crop.save
22 Removed: redirect_to @crop, notice: "Crop was successfully created."
23 Removed: else
24 Removed: render :new, status: :unprocessable_entity
25 Removed: end
26 Removed: end
27 Removed:
28 Removed: def update
29 Removed: respond_to do |format|
30 Removed: if @crop.update(crop_params)
31 Removed: format.html { redirect_to @crop, notice: "Crop was successfully updated.", status: :see_other }
32 Removed: format.json { render :show, status: :ok, location: @crop }
33 Removed: else
34 Removed: format.html { render :edit, status: :unprocessable_entity }
35 Removed: format.json { render json: @crop.errors, status: :unprocessable_entity }
36 Removed: end
37 Removed: end
38 Removed: end
39 Removed:
40 Removed: def destroy
41 Removed: @crop.destroy!
42 Removed:
43 Removed: respond_to do |format|
44 Removed: format.html { redirect_to crops_path, notice: "Crop was successfully destroyed.", status: :see_other }
45 Removed: format.json { head :no_content }
46 Removed: end
47 Removed: end
48 Removed:
49 Removed: private
50 Removed:
51 Removed: def set_crop
52 Removed: @crop = Crop.find(params.expect(:id))
53 Removed: end
54 Removed:
55 Removed: def crop_params
56 Removed: params.expect(crop: [ :name, :crop_type, :nno3, :p, :k, :ca, :mg, :s, :na, :cl, :si, :fe, :zn, :b, :mn, :cu, :mo, :nnh4 ])
57 Removed: end
58 Removed: end
app/controllers/dashboard_controller.rb
index 4e9d5606..6f323a91 100644..100644
@@ -1,17 +1,19 @@
1 1 class DashboardController < ApplicationController
2 2 def index
3 3 # Raft allocation by crop type
4 Removed: @raft_data = raft_data_series
4 Added: # @raft_data = raft_data_series
5 5
6 Added: @nutrient_profiles = NutrientProfile.order(:name)
7 Added:
6 8 # Nutrient target table
7 Removed: @latest_measurement = NutrientMeasurement.order(measured_on: :desc, created_at: :desc).first
8 Removed: @target = TargetNutrientCalculator.call
9 Added: # @latest_measurement = NutrientMeasurement.order(measured_on: :desc, created_at: :desc).first
10 Added: # @target = TargetNutrientCalculator.call
9 11
10 12 # Measurement history table
11 Removed: @measurements = NutrientMeasurement.order(measured_on: :desc).limit(10)
13 Added: # @measurements = NutrientMeasurement.order(measured_on: :desc).limit(10)
12 14
13 Removed: @npk_measurement_data = measurement_data_series(:nno3, :p, :k)
14 Removed: @ammonium_measurement_data = measurement_data_series(:nnh4)
15 Added: # @npk_measurement_data = measurement_data_series(:nno3, :p, :k)
16 Added: # @ammonium_measurement_data = measurement_data_series(:nnh4)
15 17 end
16 18
17 19 private
app/controllers/fertilizer_products_controller.rb
index ff0d9455..00000000 100644..000000
@@ -1,70 +0,0 @@
1 Removed: class FertilizerProductsController < ApplicationController
2 Removed: before_action :set_fertilizer_product, only: %i[ show edit update destroy ]
3 Removed:
4 Removed: # GET /fertilizer_products or /fertilizer_products.json
5 Removed: def index
6 Removed: @fertilizer_products = FertilizerProduct.all
7 Removed: end
8 Removed:
9 Removed: # GET /fertilizer_products/1 or /fertilizer_products/1.json
10 Removed: def show
11 Removed: end
12 Removed:
13 Removed: # GET /fertilizer_products/new
14 Removed: def new
15 Removed: @fertilizer_product = FertilizerProduct.new
16 Removed: end
17 Removed:
18 Removed: # GET /fertilizer_products/1/edit
19 Removed: def edit
20 Removed: end
21 Removed:
22 Removed: # POST /fertilizer_products or /fertilizer_products.json
23 Removed: def create
24 Removed: @fertilizer_product = FertilizerProduct.new(fertilizer_product_params)
25 Removed:
26 Removed: respond_to do |format|
27 Removed: if @fertilizer_product.save
28 Removed: format.html { redirect_to @fertilizer_product, notice: "Fertilizer product was successfully created." }
29 Removed: format.json { render :show, status: :created, location: @fertilizer_product }
30 Removed: else
31 Removed: format.html { render :new, status: :unprocessable_entity }
32 Removed: format.json { render json: @fertilizer_product.errors, status: :unprocessable_entity }
33 Removed: end
34 Removed: end
35 Removed: end
36 Removed:
37 Removed: # PATCH/PUT /fertilizer_products/1 or /fertilizer_products/1.json
38 Removed: def update
39 Removed: respond_to do |format|
40 Removed: if @fertilizer_product.update(fertilizer_product_params)
41 Removed: format.html { redirect_to @fertilizer_product, notice: "Fertilizer product was successfully updated.", status: :see_other }
42 Removed: format.json { render :show, status: :ok, location: @fertilizer_product }
43 Removed: else
44 Removed: format.html { render :edit, status: :unprocessable_entity }
45 Removed: format.json { render json: @fertilizer_product.errors, status: :unprocessable_entity }
46 Removed: end
47 Removed: end
48 Removed: end
49 Removed:
50 Removed: # DELETE /fertilizer_products/1 or /fertilizer_products/1.json
51 Removed: def destroy
52 Removed: @fertilizer_product.destroy!
53 Removed:
54 Removed: respond_to do |format|
55 Removed: format.html { redirect_to fertilizer_products_path, notice: "Fertilizer product was successfully destroyed.", status: :see_other }
56 Removed: format.json { head :no_content }
57 Removed: end
58 Removed: end
59 Removed:
60 Removed: private
61 Removed: # Use callbacks to share common setup or constraints between actions.
62 Removed: def set_fertilizer_product
63 Removed: @fertilizer_product = FertilizerProduct.find(params.expect(:id))
64 Removed: end
65 Removed:
66 Removed: # Only allow a list of trusted parameters through.
67 Removed: def fertilizer_product_params
68 Removed: params.expect(fertilizer_product: [ :name, :purity ])
69 Removed: end
70 Removed: end
app/controllers/fertilizers_controller.rb
index 040c462d..00000000 100644..000000
@@ -1,70 +0,0 @@
1 Removed: class FertilizersController < ApplicationController
2 Removed: before_action :set_fertilizer, only: %i[ show edit update destroy ]
3 Removed:
4 Removed: # GET /fertilizers or /fertilizers.json
5 Removed: def index
6 Removed: @fertilizers = Fertilizer.all
7 Removed: end
8 Removed:
9 Removed: # GET /fertilizers/1 or /fertilizers/1.json
10 Removed: def show
11 Removed: end
12 Removed:
13 Removed: # GET /fertilizers/new
14 Removed: def new
15 Removed: @fertilizer = Fertilizer.new
16 Removed: end
17 Removed:
18 Removed: # GET /fertilizers/1/edit
19 Removed: def edit
20 Removed: end
21 Removed:
22 Removed: # POST /fertilizers or /fertilizers.json
23 Removed: def create
24 Removed: @fertilizer = Fertilizer.new(fertilizer_params)
25 Removed:
26 Removed: respond_to do |format|
27 Removed: if @fertilizer.save
28 Removed: format.html { redirect_to @fertilizer, notice: "Fertilizer was successfully created." }
29 Removed: format.json { render :show, status: :created, location: @fertilizer }
30 Removed: else
31 Removed: format.html { render :new, status: :unprocessable_entity }
32 Removed: format.json { render json: @fertilizer.errors, status: :unprocessable_entity }
33 Removed: end
34 Removed: end
35 Removed: end
36 Removed:
37 Removed: # PATCH/PUT /fertilizers/1 or /fertilizers/1.json
38 Removed: def update
39 Removed: respond_to do |format|
40 Removed: if @fertilizer.update(fertilizer_params)
41 Removed: format.html { redirect_to @fertilizer, notice: "Fertilizer was successfully updated.", status: :see_other }
42 Removed: format.json { render :show, status: :ok, location: @fertilizer }
43 Removed: else
44 Removed: format.html { render :edit, status: :unprocessable_entity }
45 Removed: format.json { render json: @fertilizer.errors, status: :unprocessable_entity }
46 Removed: end
47 Removed: end
48 Removed: end
49 Removed:
50 Removed: # DELETE /fertilizers/1 or /fertilizers/1.json
51 Removed: def destroy
52 Removed: @fertilizer.destroy!
53 Removed:
54 Removed: respond_to do |format|
55 Removed: format.html { redirect_to fertilizers_path, notice: "Fertilizer was successfully destroyed.", status: :see_other }
56 Removed: format.json { head :no_content }
57 Removed: end
58 Removed: end
59 Removed:
60 Removed: private
61 Removed: # Use callbacks to share common setup or constraints between actions.
62 Removed: def set_fertilizer
63 Removed: @fertilizer = Fertilizer.find(params.expect(:id))
64 Removed: end
65 Removed:
66 Removed: # Only allow a list of trusted parameters through.
67 Removed: def fertilizer_params
68 Removed: params.expect(fertilizer: [ :name, :formula, :nno3, :nnh4, :p, :k, :ca, :mg, :s, :na, :cl, :si, :fe, :zn, :b, :mn, :cu, :mo ])
69 Removed: end
70 Removed: end
app/controllers/nutrient_measurement_controller.rb
index 6d26bfac..00000000 100644..000000
@@ -1,4 +0,0 @@
1 Removed: class NutrientMeasurementController < ApplicationController
2 Removed: def index
3 Removed: end
4 Removed: end
app/controllers/nutrient_profiles_controller.rb
index 00000000..99489d57 000000..100644
@@ -0,0 +1,58 @@
1 Added: class NutrientProfilesController < ApplicationController
2 Added: before_action :set_crop, only: %i[ show edit update destroy ]
3 Added:
4 Added: def index
5 Added: @crops = Crop.all
6 Added: end
7 Added:
8 Added: def show
9 Added: end
10 Added:
11 Added: def new
12 Added: @crop = Crop.new
13 Added: end
14 Added:
15 Added: def edit
16 Added: end
17 Added:
18 Added: def create
19 Added: @crop = Crop.new(crop_params)
20 Added:
21 Added: if @crop.save
22 Added: redirect_to @crop, notice: "Crop was successfully created."
23 Added: else
24 Added: render :new, status: :unprocessable_entity
25 Added: end
26 Added: end
27 Added:
28 Added: def update
29 Added: respond_to do |format|
30 Added: if @crop.update(crop_params)
31 Added: format.html { redirect_to @crop, notice: "Crop was successfully updated.", status: :see_other }
32 Added: format.json { render :show, status: :ok, location: @crop }
33 Added: else
34 Added: format.html { render :edit, status: :unprocessable_entity }
35 Added: format.json { render json: @crop.errors, status: :unprocessable_entity }
36 Added: end
37 Added: end
38 Added: end
39 Added:
40 Added: def destroy
41 Added: @crop.destroy!
42 Added:
43 Added: respond_to do |format|
44 Added: format.html { redirect_to crops_path, notice: "Crop was successfully destroyed.", status: :see_other }
45 Added: format.json { head :no_content }
46 Added: end
47 Added: end
48 Added:
49 Added: private
50 Added:
51 Added: def set_crop
52 Added: @crop = Crop.find(params.expect(:id))
53 Added: end
54 Added:
55 Added: def crop_params
56 Added: params.expect(crop: [ :name, :crop_type, :nno3, :p, :k, :ca, :mg, :s, :na, :cl, :si, :fe, :zn, :b, :mn, :cu, :mo, :nnh4 ])
57 Added: end
58 Added: end
app/controllers/rafts_controller.rb
index c1c8a72c..00000000 100644..000000
@@ -1,29 +0,0 @@
1 Removed: class RaftsController < ApplicationController
2 Removed: before_action :set_raft, only: %i[ edit update ]
3 Removed: before_action :set_crop, only: %i[ edit update ]
4 Removed:
5 Removed: def edit
6 Removed: end
7 Removed:
8 Removed: def update
9 Removed: if @raft.update(raft_params)
10 Removed: redirect_to beds_path, notice: "Raft #{@raft.id} successfully updated."
11 Removed: else
12 Removed: render :edit, status: :unprocessable_entity
13 Removed: end
14 Removed: end
15 Removed:
16 Removed: private
17 Removed:
18 Removed: def set_raft
19 Removed: @raft = Raft.find(params[:id])
20 Removed: end
21 Removed:
22 Removed: def set_crop
23 Removed: @crops = Crop.order(:name)
24 Removed: end
25 Removed:
26 Removed: def raft_params
27 Removed: params.require(:raft).permit(:location, :bed_id, :crop_id)
28 Removed: end
29 Removed: end
app/controllers/recipes_controller.rb
index bcc29e70..00000000 100644..000000
@@ -1,8 +0,0 @@
1 Removed: class RecipesController < ApplicationController
2 Removed: def show
3 Removed: @latest = NutrientMeasurement.order(:measured_on).last || NutrientMeasurement.new
4 Removed: @target = TargetNutrientCalculator.call
5 Removed: volume = (params[:volume].presence || 100_000).to_i
6 Removed: @recipe = FertilizerRecipeCalculator.call(@latest, @target, water_volume_l: volume)
7 Removed: end
8 Removed: end
app/helpers/application_helper.rb
index de6be794..00000000 100644..000000
@@ -1,2 +0,0 @@
1 Removed: module ApplicationHelper
2 Removed: end
app/helpers/beds_helper.rb
index 2f3e2cba..00000000 100644..000000
@@ -1,2 +0,0 @@
1 Removed: module BedsHelper
2 Removed: end
app/helpers/dashboard_helper.rb
index 5d64d170..00000000 100644..000000
@@ -1,8 +0,0 @@
1 Removed: module DashboardHelper
2 Removed: def fmt2(v) = number_with_precision(v, precision: 2)
3 Removed:
4 Removed: # Total nitrogen (NO3-N + NH4-N)
5 Removed: def total_n(measurement)
6 Removed: (measurement.nno3.to_f) + (measurement.nnh4.to_f)
7 Removed: end
8 Removed: end
app/helpers/nutrients_helper.rb
index de98dc3b..00000000 100644..000000
@@ -1,21 +0,0 @@
1 Removed: module NutrientsHelper
2 Removed: NUTRIENTS = %i[nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4].freeze
3 Removed:
4 Removed: def percent_delta(measured, target)
5 Removed: return 0.0 if target.to_f.zero?
6 Removed: ((measured.to_f - target.to_f) / target.to_f) * 100.0
7 Removed: end
8 Removed:
9 Removed: def fmt(v)
10 Removed: number_with_precision(v, precision: 2, strip_insignificant_zeros: true)
11 Removed: end
12 Removed:
13 Removed: def delta_badge_class(delta)
14 Removed: d = delta.abs
15 Removed: case d
16 Removed: when d < 0 then "bg-info"
17 Removed: when 0..5 then "bg-secondary"
18 Removed: else "bg-warning"
19 Removed: end
20 Removed: end
21 Removed: end
app/helpers/recipes_helper.rb
index 02cb1a79..00000000 100644..000000
@@ -1,16 +0,0 @@
1 Removed: module RecipesHelper
2 Removed: def commercial_name_for(component)
3 Removed: # Try to find a FertilizerProduct via a join, but gracefully fallback.
4 Removed: if defined?(FertilizerProduct) && defined?(FertilizerComposition)
5 Removed: prod = FertilizerProduct.joins(:fertilizer_compositions)
6 Removed: .where(fertilizer_compositions: { fertilizer_component_id: component.id })
7 Removed: .first
8 Removed: return prod.name if prod&.name.present?
9 Removed: end
10 Removed: component.name.presence || component.formula
11 Removed: end
12 Removed:
13 Removed: def fmt_kg(v)
14 Removed: number_with_precision(v.to_f, precision: 2, strip_insignificant_zeros: true)
15 Removed: end
16 Removed: end
app/javascript/application.js
index beff742e..0d7b4940 100644..100644
@@ -1,1 +1,3 @@
1 1 // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2 Added: import "@hotwired/turbo-rails"
3 Added: import "controllers"
app/javascript/controllers/application.js
index 00000000..1213e85c 000000..100644
@@ -0,0 +1,9 @@
1 Added: import { Application } from "@hotwired/stimulus"
2 Added:
3 Added: const application = Application.start()
4 Added:
5 Added: // Configure Stimulus development experience
6 Added: application.debug = false
7 Added: window.Stimulus = application
8 Added:
9 Added: export { application }
app/javascript/controllers/hello_controller.js
index 00000000..5975c078 000000..100644
@@ -0,0 +1,7 @@
1 Added: import { Controller } from "@hotwired/stimulus"
2 Added:
3 Added: export default class extends Controller {
4 Added: connect() {
5 Added: this.element.textContent = "Hello World!"
6 Added: }
7 Added: }
app/javascript/controllers/index.js
index 00000000..1156bf83 000000..100644
@@ -0,0 +1,4 @@
1 Added: // Import and register all your controllers from the importmap via controllers/**/*_controller
2 Added: import { application } from "controllers/application"
3 Added: import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
4 Added: eagerLoadControllersFrom("controllers", application)
app/models/bed.rb
index d41afe68..00000000 100644..000000
@@ -1,5 +0,0 @@
1 Removed: class Bed < ApplicationRecord
2 Removed: has_many :rafts, -> { order(:location) }, dependent: :destroy
3 Removed: accepts_nested_attributes_for :rafts
4 Removed: validates :location, presence: true, uniqueness: true
5 Removed: end
app/models/crop.rb
index b0f168dd..00000000 100644..000000
@@ -1,4 +0,0 @@
1 Removed: class Crop < ApplicationRecord
2 Removed: has_many :rafts
3 Removed: enum :crop_type, { leafy: 0, fruit: 1, herb: 2 }
4 Removed: end
app/models/fertilizer_component.rb
index 701ae9bc..00000000 100644..000000
@@ -1,3 +0,0 @@
1 Removed: class FertilizerComponent < ApplicationRecord
2 Removed: validates :name, presence: true
3 Removed: end
app/models/fertilizer_composition.rb
index cf2bb934..00000000 100644..000000
@@ -1,6 +0,0 @@
1 Removed: class FertilizerComposition < ApplicationRecord
2 Removed: belongs_to :fertilizer_product
3 Removed: belongs_to :fertilizer_component
4 Removed:
5 Removed: validates :percent_w, numericality: { greater_than: 0, less_than_or_equal_to: 100 }
6 Removed: end
app/models/fertilizer_product.rb
index e41316b4..00000000 100644..000000
@@ -1,7 +0,0 @@
1 Removed: class FertilizerProduct < ApplicationRecord
2 Removed: has_many :fertilizer_compositions, dependent: :destroy
3 Removed: has_many :fertilizer_components, through: :fertilizer_compositions
4 Removed:
5 Removed: validates :name, presence: true, uniqueness: true
6 Removed: validates :purity, numericality: { greater_than: 0, less_than_or_equal_to: 100 }
7 Removed: end
app/models/nutrient.rb
index c584668d..00000000 100644..000000
@@ -1,7 +0,0 @@
1 Removed: class Nutrient < ApplicationRecord
2 Removed: validates :formula, presence: true, uniqueness: true
3 Removed: validates :name, presence: true
4 Removed:
5 Removed: before_update { raise ActiveRecord::ReadOnlyRecord }
6 Removed: before_destroy { raise ActiveRecord::ReadOnlyRecord }
7 Removed: end
app/models/nutrient_profile.rb
index 00000000..22f27047 000000..100644
@@ -0,0 +1,2 @@
1 Added: class NutrientProfile < ApplicationRecord
2 Added: end
app/models/raft.rb
index 3fe59282..00000000 100644..000000
@@ -1,5 +0,0 @@
1 Removed: class Raft < ApplicationRecord
2 Removed: belongs_to :bed
3 Removed: belongs_to :crop, optional: true
4 Removed: validates :location, presence: true, uniqueness: { scope: :bed_id }
5 Removed: end
app/services/fertilizer_recipe_calculator.rb
index 28ba8aa7..00000000 100644..000000
@@ -1,136 +0,0 @@
1 Removed: class FertilizerRecipeCalculator
2 Removed: NUTRIENTS = %i[nno3 p k ca mg s nnh4].freeze
3 Removed:
4 Removed: def self.call(latest, target, water_volume_l: 100_000)
5 Removed: new(latest, target, water_volume_l).call
6 Removed: end
7 Removed:
8 Removed: def initialize(latest, target, water_volume_l)
9 Removed: @latest = latest || NutrientMeasurement.new
10 Removed: @target = target || NutrientMeasurement.new
11 Removed: @vol_l = water_volume_l.to_f
12 Removed: end
13 Removed:
14 Removed: # Returns a Hash { FertilizerComponent => qty_kg }
15 Removed: def call
16 Removed: deltas_mg_per_l = NUTRIENTS.index_with do |k|
17 Removed: [ (value(@target, k) - value(@latest, k)), 0 ].max
18 Removed: end
19 Removed:
20 Removed: # mg/L * L = mg -> kg
21 Removed: needed_kg = deltas_mg_per_l.transform_values { |mg_l| mg_l * @vol_l / 1_000_000.0 }
22 Removed:
23 Removed: recipe = Hash.new(0.0)
24 Removed:
25 Removed: # 1) P via MAP or DAP (accounts NH4-N)
26 Removed: if (p_need = needed_kg[:p]) > 0
27 Removed: map = fc_by_formula("MAP") || fc_by_name_like("monoammonium phosphate")
28 Removed: dap = fc_by_formula("DAP") || fc_by_name_like("diammonium phosphate")
29 Removed: carrier = [ map, dap ].compact.find { |c| positive?(c.p) }
30 Removed: if carrier
31 Removed: kg = kg_for(p_need, carrier.p)
32 Removed: recipe[carrier] += kg
33 Removed: needed_kg[:nnh4] = [ needed_kg[:nnh4] - kg * pct(carrier.nnh4), 0 ].max if positive?(carrier.nnh4)
34 Removed: needed_kg[:p] = 0
35 Removed: end
36 Removed: end
37 Removed:
38 Removed: # 2) NO3-N via KNO3 then Ca(NO3)2 (accounts K/Ca)
39 Removed: if (n_need = needed_kg[:nno3]) > 0
40 Removed: kno3 = fc_by_formula("KNO3") || fc_by_name_like("potassium nitrate")
41 Removed: can = fc_by_formula("Ca(NO3)2") || fc_by_name_like("calcium nitrate")
42 Removed:
43 Removed: if kno3&.nno3.to_f > 0
44 Removed: kg = kg_for(n_need, kno3.nno3)
45 Removed: recipe[kno3] += kg
46 Removed: needed_kg[:k] = [ needed_kg[:k] - kg * pct(kno3.k), 0 ].max
47 Removed: n_need -= kg * pct(kno3.nno3)
48 Removed: end
49 Removed:
50 Removed: if n_need > 0 && can&.nno3.to_f > 0
51 Removed: kg = kg_for(n_need, can.nno3)
52 Removed: recipe[can] += kg
53 Removed: needed_kg[:ca] = [ needed_kg[:ca] - kg * pct(can.ca), 0 ].max
54 Removed: n_need = 0
55 Removed: end
56 Removed:
57 Removed: needed_kg[:nno3] = [ n_need, 0 ].max
58 Removed: end
59 Removed:
60 Removed: # 3) K via K2SO4 (accounts S)
61 Removed: if (k_need = needed_kg[:k]) > 0
62 Removed: sop = fc_by_formula("K2SO4") || fc_by_name_like("potassium sulfate")
63 Removed: if sop&.k.to_f > 0
64 Removed: kg = kg_for(k_need, sop.k)
65 Removed: recipe[sop] += kg
66 Removed: needed_kg[:s] = [ needed_kg[:s] - kg * pct(sop.s), 0 ].max
67 Removed: needed_kg[:k] = 0
68 Removed: end
69 Removed: end
70 Removed:
71 Removed: # 4) Ca via Ca(NO3)2 (accounts NO3-N)
72 Removed: if (ca_need = needed_kg[:ca]) > 0
73 Removed: can = recipe.keys.find { |c| norm_formula(c) == "ca(no3)2" } ||
74 Removed: fc_by_formula("Ca(NO3)2") || fc_by_name_like("calcium nitrate")
75 Removed: if can&.ca.to_f > 0
76 Removed: kg = kg_for(ca_need, can.ca)
77 Removed: recipe[can] += kg
78 Removed: needed_kg[:nno3] = [ needed_kg[:nno3] - kg * pct(can.nno3), 0 ].max
79 Removed: needed_kg[:ca] = 0
80 Removed: end
81 Removed: end
82 Removed:
83 Removed: # 5) Mg via MgSO4 (accounts S)
84 Removed: if (mg_need = needed_kg[:mg]) > 0
85 Removed: mgs = fc_by_formula("MgSO4") || fc_by_name_like("magnesium sulfate")
86 Removed: if mgs&.mg.to_f > 0
87 Removed: kg = kg_for(mg_need, mgs.mg)
88 Removed: recipe[mgs] += kg
89 Removed: needed_kg[:s] = [ needed_kg[:s] - kg * pct(mgs.s), 0 ].max
90 Removed: needed_kg[:mg] = 0
91 Removed: end
92 Removed: end
93 Removed:
94 Removed: # 6) S via K2SO4 or MgSO4 (whichever we already used or find)
95 Removed: if (s_need = needed_kg[:s]) > 0
96 Removed: sop = recipe.keys.find { |c| norm_formula(c) == "k2so4" } ||
97 Removed: fc_by_formula("K2SO4") || fc_by_name_like("potassium sulfate")
98 Removed: mgs = recipe.keys.find { |c| norm_formula(c) == "mgso4" } ||
99 Removed: fc_by_formula("MgSO4") || fc_by_name_like("magnesium sulfate")
100 Removed: carrier = [ sop, mgs ].compact.find { |c| positive?(c.s) }
101 Removed: if carrier
102 Removed: kg = kg_for(s_need, carrier.s)
103 Removed: recipe[carrier] += kg
104 Removed: needed_kg[:k] = [ needed_kg[:k] - kg * pct(carrier.k), 0 ].max if positive?(carrier.k)
105 Removed: needed_kg[:mg] = [ needed_kg[:mg] - kg * pct(carrier.mg), 0 ].max if positive?(carrier.mg)
106 Removed: needed_kg[:s] = 0
107 Removed: end
108 Removed: end
109 Removed:
110 Removed: recipe.delete_if { |_c, kg| kg < 0.01 }
111 Removed: recipe
112 Removed: end
113 Removed:
114 Removed: private
115 Removed:
116 Removed: def value(obj, key) = obj.public_send(key).to_f
117 Removed:
118 Removed: def fc_by_formula(formula)
119 Removed: FertilizerComponent.where("LOWER(formula) = ?", formula.to_s.downcase).first
120 Removed: end
121 Removed:
122 Removed: def fc_by_name_like(name)
123 Removed: FertilizerComponent.where("LOWER(name) LIKE ?", "%#{name.downcase}%").first
124 Removed: end
125 Removed:
126 Removed: def kg_for(need_kg_element, percent_in_product)
127 Removed: return 0.0 unless positive?(percent_in_product)
128 Removed: need_kg_element / pct(percent_in_product)
129 Removed: end
130 Removed:
131 Removed: def pct(v) = v.to_f / 100.0
132 Removed:
133 Removed: def positive?(v) = v.to_f > 0.0
134 Removed:
135 Removed: def norm_formula(c) = c.formula.to_s.downcase.gsub(/\s+/, "")
136 Removed: end
app/services/target_nutrient_calculator.rb
index e6cd3787..00000000 100644..000000
@@ -1,33 +0,0 @@
1 Removed: class TargetNutrientCalculator
2 Removed: # Derive nutrient columns from the NutrientMeasurement table
3 Removed: NUTRIENT_COLUMNS = (NutrientMeasurement.column_names - %w[id measured_on created_at updated_at])
4 Removed: .map!(&:to_sym)
5 Removed: .freeze
6 Removed:
7 Removed: # Returns an unsaved NutrientMeasurement with target concentrations (e.g., mg/L)
8 Removed: def self.call
9 Removed: rafts = Raft.includes(:crop).where.not(crop_id: nil)
10 Removed: total = rafts.count
11 Removed: return empty_measurement if total.zero?
12 Removed:
13 Removed: sums = Hash.new(0.0)
14 Removed:
15 Removed: rafts.each do |raft|
16 Removed: NUTRIENT_COLUMNS.each do |col|
17 Removed: v = raft.crop.public_send(col)
18 Removed: sums[col] += v.to_f if v
19 Removed: end
20 Removed: end
21 Removed:
22 Removed: targets = sums.transform_values { |s| s / total }
23 Removed: NutrientMeasurement.new({ measured_on: Date.current }.merge(targets))
24 Removed: end
25 Removed:
26 Removed: private
27 Removed:
28 Removed: def empty_measurement
29 Removed: NutrientMeasurement.new(
30 Removed: { measured_on: Date.current }.merge(NUTRIENT_COLUMNS.index_with { 0.0 })
31 Removed: )
32 Removed: end
33 Removed: end
app/views/dashboard/_nutrient_profile_allocator.html.erb
index 00000000..d402aceb 000000..100644
@@ -0,0 +1,187 @@
1 Added: <%# Props: nutrient_profiles: ActiveRecord::Relation<NutrientProfile> %>
2 Added: <%# Fallback if controller didn't set @nutrient_profiles yet %>
3 Added: <% profiles = (local_assigns[:nutrient_profiles] || []).presence || [] %>
4 Added:
5 Added: <%# We'll render a form purely for structure (no real submit yet) %>
6 Added: <%= form_with url: "#", method: :post, local: true, html: { id: "np-mix-form", "data-controller": "np-mix" } do %>
7 Added: <div class="card shadow">
8 Added: <div class="card-body">
9 Added:
10 Added: <div class="d-flex justify-content-between align-items-center mb-2">
11 Added: <div class="small text-muted">
12 Added: Choisissez des <strong>profils de croissance</strong> et répartissez-les pour totaliser <strong>100%</strong>.
13 Added: </div>
14 Added: <div>
15 Added: Somme&nbsp;: <span id="np-mix-sum" class="badge bg-secondary">0%</span>
16 Added: </div>
17 Added: </div>
18 Added:
19 Added: <div id="np-mix-rows" class="vstack gap-2">
20 Added: <%# Rows are injected by JS from the template below, including defaults %>
21 Added: </div>
22 Added:
23 Added: <div class="mt-3 d-flex gap-2">
24 Added: <button type="button" class="btn btn-outline-primary" id="np-mix-add">
25 Added: + Ajouter un profil
26 Added: </button>
27 Added:
28 Added: <%# Placeholder "save" button for later backend wiring; disabled until total == 100 %>
29 Added: <button type="submit" class="btn btn-success ms-auto" id="np-mix-save" disabled>
30 Added: Enregistrer (à venir)
31 Added: </button>
32 Added: </div>
33 Added: </div>
34 Added: </div>
35 Added:
36 Added: <%# --- Hidden template for a single row --- %>
37 Added: <template id="np-mix-row-template">
38 Added: <div class="np-mix-row d-flex align-items-center gap-2 border rounded p-2">
39 Added: <button type="button" class="btn btn-outline-danger btn-sm np-mix-delete" aria-label="Supprimer la ligne">
40 Added: Suppr.
41 Added: </button>
42 Added:
43 Added: <div class="flex-grow-1">
44 Added: <select name="mix[items][][profile_id]" class="form-select form-select-sm np-mix-select" required>
45 Added: <% if profiles.any? %>
46 Added: <% profiles.each do |p| %>
47 Added: <option value="<%= p.id %>"><%= p.name %></option>
48 Added: <% end %>
49 Added: <% else %>
50 Added: <%# If no collection provided yet, at least show placeholders to demo the UI %>
51 Added: <option value="">-- Sélectionner un profil --</option>
52 Added: <option value="gen-croissance">Générique croissance</option>
53 Added: <option value="tomate-cycle">Tomate (cycle entier)</option>
54 Added: <option value="gen-floraison">Générique floraison</option>
55 Added: <% end %>
56 Added: </select>
57 Added: </div>
58 Added:
59 Added: <div class="input-group input-group-sm" style="max-width: 140px;">
60 Added: <input type="number"
61 Added: name="mix[items][][percentage]"
62 Added: class="form-control text-end np-mix-percent"
63 Added: min="0" max="100" step="1" value="0" required>
64 Added: <span class="input-group-text">%</span>
65 Added: </div>
66 Added: </div>
67 Added: </template>
68 Added:
69 Added: <%# --- Defaults to inject on load --- %>
70 Added: <script type="application/json" id="np-mix-defaults">
71 Added: {
72 Added: "items": [
73 Added: { "name": "G\u00E9n\u00E9rique croissance", "percent": 50 },
74 Added: { "name": "Tomate (cycle entier)", "percent": 30 },
75 Added: { "name": "G\u00E9n\u00E9rique floraison", "percent": 20 }
76 Added: ]
77 Added: }
78 Added: </script>
79 Added:
80 Added: <%# --- Tiny inline JS to keep this self-contained (no Stimulus required) --- %>
81 Added: <script>
82 Added: (() => {
83 Added: const rowsContainer = document.getElementById('np-mix-rows');
84 Added: const addBtn = document.getElementById('np-mix-add');
85 Added: const saveBtn = document.getElementById('np-mix-save');
86 Added: const sumBadge = document.getElementById('np-mix-sum');
87 Added: const tpl = document.getElementById('np-mix-row-template');
88 Added: const defaultsJSON = document.getElementById('np-mix-defaults')?.textContent || "{}";
89 Added: const defaults = JSON.parse(defaultsJSON);
90 Added:
91 Added: function currentSum() {
92 Added: return Array.from(rowsContainer.querySelectorAll('.np-mix-percent'))
93 Added: .reduce((acc, el) => acc + (parseFloat(el.value) || 0), 0);
94 Added: }
95 Added:
96 Added: function refreshSum() {
97 Added: const sum = currentSum();
98 Added: sumBadge.textContent = `${sum}%`;
99 Added: sumBadge.classList.remove('bg-secondary','bg-danger','bg-success','bg-warning');
100 Added:
101 Added: if (sum === 100) {
102 Added: sumBadge.classList.add('bg-success');
103 Added: saveBtn?.removeAttribute('disabled');
104 Added: } else if (sum > 100) {
105 Added: sumBadge.classList.add('bg-danger');
106 Added: saveBtn?.setAttribute('disabled', 'disabled');
107 Added: } else {
108 Added: sumBadge.classList.add('bg-warning');
109 Added: saveBtn?.setAttribute('disabled', 'disabled');
110 Added: }
111 Added: }
112 Added:
113 Added: function setSelectByName(selectEl, targetName) {
114 Added: // Try to match by visible name; fall back to first option.
115 Added: const options = Array.from(selectEl.options);
116 Added: const found = options.find(o => o.text.trim().toLowerCase() === String(targetName || '').trim().toLowerCase());
117 Added: if (found) {
118 Added: selectEl.value = found.value;
119 Added: }
120 Added: }
121 Added:
122 Added: function installRow({ name = null, percent = 0 } = {}) {
123 Added: const node = tpl.content.firstElementChild.cloneNode(true);
124 Added:
125 Added: // Hook up events
126 Added: node.querySelector('.np-mix-delete').addEventListener('click', () => {
127 Added: node.remove();
128 Added: refreshSum();
129 Added: });
130 Added:
131 Added: const selectEl = node.querySelector('.np-mix-select');
132 Added: const percentEl = node.querySelector('.np-mix-percent');
133 Added:
134 Added: // Default selection (by name) and percent
135 Added: if (name) setSelectByName(selectEl, name);
136 Added: percentEl.value = percent;
137 Added:
138 Added: // Input events
139 Added: selectEl.addEventListener('change', () => { /* reserved for later linkage */ });
140 Added: percentEl.addEventListener('input', () => {
141 Added: // Clamp and refresh
142 Added: let v = parseFloat(percentEl.value);
143 Added: if (isNaN(v)) v = 0;
144 Added: v = Math.max(0, Math.min(100, Math.round(v)));
145 Added: percentEl.value = v;
146 Added: refreshSum();
147 Added: });
148 Added:
149 Added: rowsContainer.appendChild(node);
150 Added: }
151 Added:
152 Added: // Init with three defaults
153 Added: const items = (defaults && defaults.items) ? defaults.items : [];
154 Added: if (items.length) {
155 Added: items.forEach(it => installRow({ name: it.name, percent: it.percent }));
156 Added: } else {
157 Added: // Fallback: create three blank rows
158 Added: for (let i = 0; i < 3; i++) installRow();
159 Added: }
160 Added: refreshSum();
161 Added:
162 Added: // Add new blank row
163 Added: addBtn.addEventListener('click', () => {
164 Added: installRow({ name: null, percent: 0 });
165 Added: refreshSum();
166 Added: // Scroll to the new row on mobile for better UX
167 Added: rowsContainer.lastElementChild?.scrollIntoView({ behavior: 'smooth', block: 'center' });
168 Added: });
169 Added:
170 Added: // Prevent real submit for now (frontend only)
171 Added: document.getElementById('np-mix-form')?.addEventListener('submit', (e) => {
172 Added: e.preventDefault();
173 Added: // Later: wire to Turbo/JSON post. For now just a gentle nudge.
174 Added: saveBtn.textContent = 'Enregistrer (backend à venir)';
175 Added: saveBtn.blur();
176 Added: });
177 Added: })();
178 Added: </script>
179 Added:
180 Added: <style>
181 Added: /* Small touch targets & tidy spacing on mobile */
182 Added: @media (max-width: 576px) {
183 Added: .np-mix-row { padding: .5rem; }
184 Added: .np-mix-row .btn { padding: .25rem .5rem; }
185 Added: }
186 Added: </style>
187 Added: <% end %>
app/views/dashboard/_raft_allocation.html.erb
index a2128ea2..1c9ef9ab 100644..100644
@@ -1,7 +1,7 @@
1 1 <div class="card shadow mb-4">
2 2 <div class="card-header d-flex justify-content-between align-items-center">
3 3 <h5 class="mb-0">Crop Allocation</h5>
4 Removed: <%= link_to "Edit allocation", beds_path, class: "btn btn-sm btn-primary" %>
4 Added: <%#= link_to "Edit allocation", beds_path, class: "btn btn-sm btn-primary" %>
5 5 </div>
6 6
7 7 <%= bar_chart @raft_data, stacked: true %>
app/views/dashboard/index.html.erb
index b1b2d87c..b8f91452 100644..100644
@@ -1,7 +1,9 @@
1 1 <h1 class="display-1">Ferti</h1>
2 2
3 Removed: <%= render "raft_allocation" %>
3 Added: <%= render "nutrient_profile_allocator", nutrient_profiles: @nutrient_profiles %>
4 4
5 Removed: <%= render "target_table" %>
5 Added: <%#= render "raft_allocation" %>
6 6
7 Removed: <%= render "nutrient_measurements" %>
7 Added: <%#= render "target_table" %>
8 Added:
9 Added: <%#= render "nutrient_measurements" %>
app/views/layouts/application.html.erb
index bf61c9d8..dc616707 100644..100644
@@ -14,8 +14,8 @@
14 14 <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
15 15
16 16 <link rel="icon" href="/icon.png" type="image/png">
17 Removed: <link rel="icon" href="/icon.svg" type="image/svg+xml">
18 Removed: <link rel="apple-touch-icon" href="/icon.png">
17 Added: <!-- <link rel="icon" href="/icon.svg" type="image/svg+xml"> -->
18 Added: <!-- <link rel="apple-touch-icon" href="/icon.png"> -->
19 19
20 20 <%# Includes all stylesheet files in app/assets/stylesheets %>
21 21 <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
@@ -25,7 +25,6 @@
25 25
26 26 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
27 27 <script src="https://cdn.jsdelivr.net/npm/chartkick@5"></script>
28 Removed:
29 28 <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
30 29
31 30 <%= javascript_importmap_tags %>
config/importmap.rb
index 0086a327..909dfc54 100644..100644
@@ -1,3 +1,7 @@
1 1 # Pin npm packages by running ./bin/importmap
2 2
3 3 pin "application"
4 Added: pin "@hotwired/turbo-rails", to: "turbo.min.js"
5 Added: pin "@hotwired/stimulus", to: "stimulus.min.js"
6 Added: pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
7 Added: pin_all_from "app/javascript/controllers", under: "controllers"
config/routes.rb
index 1dd57b51..50950d48 100644..100644
@@ -1,19 +1,19 @@
1 1 Rails.application.routes.draw do
2 2 root "dashboard#index"
3 3
4 Removed: get "ferti_recipe", to: "recipes#show"
4 Added: # get "ferti_recipe", to: "recipes#show"
5 5
6 Removed: resources :beds, only: [ :index, :edit, :update ] do
7 Removed: collection do
8 Removed: patch :bulk_assign_crops
9 Removed: post :reset_seed_crops
10 Removed: end
6 Added: # resources :beds, only: [ :index, :edit, :update ] do
7 Added: # collection do
8 Added: # patch :bulk_assign_crops
9 Added: # post :reset_seed_crops
10 Added: # end
11 11
12 Removed: resources :rafts, only: [ :index, :edit, :update ], shallow: true
13 Removed: end
12 Added: # resources :rafts, only: [ :index, :edit, :update ], shallow: true
13 Added: # end
14 14
15 Removed: resources :fertilizer_products
16 Removed: resources :crops
15 Added: # resources :fertilizer_products
16 Added: resources :nutrient_profiles
17 17 resources :nutrient_measurements
18 18
19 19 # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
db/data/dolibarr_plant_requirements.csv
index 00000000..333ea380 000000..100644
@@ -0,0 +1,9 @@
1 Added: Cycles et productions,NNO3,P,K,Ca,Mg,S,Fe,Zn,B,Mn,Cu,Mo
2 Added: 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
3 Added: 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
4 Added: 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
5 Added: 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
6 Added: 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
7 Added: 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
8 Added: 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
9 Added: 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
db/data/nutrient_profiles.csv
index 00000000..61406b8f 000000..100644
@@ -0,0 +1,9 @@
1 Added: Nom,NNO3,P,K,Ca,Mg,S,Na,Cl,Si,Fe,Zn,B,Mn,Cu,Mo,NNH4
2 Added: 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
3 Added: 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
4 Added: Laitue,190,50,210,200,50,66,0,0,0,5,"0,15","0,3","0,5","0,15","0,05",0
5 Added: Tomate (cycle entier),140,50,352,180,50,168,0,0,0,5,"0,1","0,3","0,8","0,07","0,03",0
6 Added: 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
7 Added: Tomate 1ere grappe,130,55,300,150,33,109,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0
8 Added: Tomate à maturité,180,65,400,400,45,144,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0
9 Added: Framboise - tous stades,70,12,88,90,24,48,0,0,50,0.56,"0,325",11,"0,11","0,032","0,01",0
db/data/nutrient_requirements.csv
index 61406b8f..00000000 100644..000000
@@ -1,9 +0,0 @@
1 Removed: Nom,NNO3,P,K,Ca,Mg,S,Na,Cl,Si,Fe,Zn,B,Mn,Cu,Mo,NNH4
2 Removed: 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
3 Removed: 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
4 Removed: Laitue,190,50,210,200,50,66,0,0,0,5,"0,15","0,3","0,5","0,15","0,05",0
5 Removed: Tomate (cycle entier),140,50,352,180,50,168,0,0,0,5,"0,1","0,3","0,8","0,07","0,03",0
6 Removed: 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
7 Removed: Tomate 1ere grappe,130,55,300,150,33,109,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0
8 Removed: Tomate à maturité,180,65,400,400,45,144,0,0,0,3,"0,1","0,3","0,8","0,07","0,03",0
9 Removed: Framboise - tous stades,70,12,88,90,24,48,0,0,50,0.56,"0,325",11,"0,11","0,032","0,01",0
db/migrate/20250901112954_rename_crops_to_nutrient_profiles.rb
index 00000000..5b46df19 000000..100644
@@ -0,0 +1,10 @@
1 Added: class RenameCropsToNutrientProfiles < ActiveRecord::Migration[8.0]
2 Added: def change
3 Added: rename_table :crops, :nutrient_profiles
4 Added:
5 Added: rename_column :rafts, :crop_id, :crop_nutrient_need_id
6 Added: add_index :rafts, :crop_nutrient_need_id unless index_exists?(:rafts, :crop_nutrient_need_id)
7 Added:
8 Added: remove_column :nutrient_profiles, :crop_type
9 Added: end
10 Added: end
db/schema.rb
index b5f2e036..feb2fd03 100644..100644
@@ -10,7 +10,7 @@
10 10 #
11 11 # It's strongly recommended that you check this file into your version control system.
12 12
13 Removed: ActiveRecord::Schema[8.0].define(version: 2025_08_24_163257) do
13 Added: ActiveRecord::Schema[8.0].define(version: 2025_09_01_112954) do
14 14 create_table "beds", force: :cascade do |t|
15 15 t.integer "location", null: false
16 16 t.datetime "created_at", null: false
@@ -18,29 +18,6 @@
18 18 t.index ["location"], name: "index_beds_on_location", unique: true
19 19 end
20 20
21 Removed: create_table "crops", force: :cascade do |t|
22 Removed: t.string "name"
23 Removed: t.integer "crop_type", default: 1, null: false
24 Removed: t.float "nno3"
25 Removed: t.float "p"
26 Removed: t.float "k"
27 Removed: t.float "ca"
28 Removed: t.float "mg"
29 Removed: t.float "s"
30 Removed: t.float "na"
31 Removed: t.float "cl"
32 Removed: t.float "si"
33 Removed: t.float "fe"
34 Removed: t.float "zn"
35 Removed: t.float "b"
36 Removed: t.float "mn"
37 Removed: t.float "cu"
38 Removed: t.float "mo"
39 Removed: t.float "nnh4"
40 Removed: t.datetime "created_at", null: false
41 Removed: t.datetime "updated_at", null: false
42 Removed: end
43 Removed:
44 21 create_table "fertilizer_components", force: :cascade do |t|
45 22 t.string "name"
46 23 t.string "formula"
@@ -105,6 +82,28 @@
105 82 t.index ["measured_on"], name: "index_nutrient_measurements_on_measured_on", unique: true
106 83 end
107 84
85 Added: create_table "nutrient_profiles", force: :cascade do |t|
86 Added: t.string "name"
87 Added: t.float "nno3"
88 Added: t.float "p"
89 Added: t.float "k"
90 Added: t.float "ca"
91 Added: t.float "mg"
92 Added: t.float "s"
93 Added: t.float "na"
94 Added: t.float "cl"
95 Added: t.float "si"
96 Added: t.float "fe"
97 Added: t.float "zn"
98 Added: t.float "b"
99 Added: t.float "mn"
100 Added: t.float "cu"
101 Added: t.float "mo"
102 Added: t.float "nnh4"
103 Added: t.datetime "created_at", null: false
104 Added: t.datetime "updated_at", null: false
105 Added: end
106 Added:
108 107 create_table "nutrients", force: :cascade do |t|
109 108 t.string "formula"
110 109 t.string "name"
@@ -115,16 +114,16 @@
115 114 create_table "rafts", force: :cascade do |t|
116 115 t.integer "bed_id", null: false
117 116 t.integer "location", null: false
118 Removed: t.integer "crop_id"
117 Added: t.integer "crop_nutrient_need_id"
119 118 t.datetime "created_at", null: false
120 119 t.datetime "updated_at", null: false
121 120 t.index ["bed_id", "location"], name: "index_rafts_on_bed_id_and_location", unique: true
122 121 t.index ["bed_id"], name: "index_rafts_on_bed_id"
123 Removed: t.index ["crop_id"], name: "index_rafts_on_crop_id"
122 Added: t.index ["crop_nutrient_need_id"], name: "index_rafts_on_crop_nutrient_need_id"
124 123 end
125 124
126 125 add_foreign_key "fertilizer_compositions", "fertilizer_components"
127 126 add_foreign_key "fertilizer_compositions", "fertilizer_products"
128 127 add_foreign_key "rafts", "beds"
129 Removed: add_foreign_key "rafts", "crops"
128 Added: add_foreign_key "rafts", "nutrient_profiles", column: "crop_nutrient_need_id"
130 129 end
db/seeds.rb
index 848db0d7..245247e1 100644..100644
@@ -8,6 +8,15 @@
8 8 # MovieGenre.find_or_create_by!(name: genre_name)
9 9 # end
10 10
11 Removed: Dir[Rails.root.join("db/seeds/*.rb")].sort.each do |seed|
12 Removed: load seed
11 Added: MODELS = [ NutrientMeasurement,
12 Added: NutrientProfile
13 Added: ].freeze
14 Added:
15 Added: def seed_file_for(model_class)
16 Added: Rails.root.join("db", "seeds", "#{model_class.name}.rb")
17 Added: end
18 Added:
19 Added: MODELS.each do |model|
20 Added: load seed_file_for(model)
21 Added: printf "%-22s %3s\n", model.name, model.count
13 22 end
db/seeds/1_nutrients.rb
index dab22496..00000000 100644..000000
@@ -1,24 +0,0 @@
1 Removed: NUTRIENTS = [
2 Removed: [ "nno3", "nitrate" ],
3 Removed: [ "p", "phosphore" ],
4 Removed: [ "k", "potassium" ],
5 Removed: [ "ca", "calcium" ],
6 Removed: [ "mg", "magnésium" ],
7 Removed: [ "s", "soufre" ],
8 Removed: [ "na", "sodium" ],
9 Removed: [ "cl", "chlore" ],
10 Removed: [ "si", "silice" ],
11 Removed: [ "fe", "fer" ],
12 Removed: [ "zn", "zinc" ],
13 Removed: [ "b", "bore" ],
14 Removed: [ "mn", "manganèse" ],
15 Removed: [ "cu", "cuivre" ],
16 Removed: [ "mo", "molybdène" ],
17 Removed: [ "nnh4", "ammonium" ]
18 Removed: ]
19 Removed:
20 Removed: NUTRIENTS.each do |formula, name|
21 Removed: Nutrient.find_or_create_by!(formula:) { |n| n.name = name }
22 Removed: end
23 Removed:
24 Removed: puts "Nutrients: #{Nutrient.count}"
db/seeds/2_nutrient_measurements.rb
index 0ca14f19..00000000 100644..000000
@@ -1,27 +0,0 @@
1 Removed: require "csv"
2 Removed:
3 Removed: csv_path = Rails.root.join("db", "data", "dolibarr_measurements.csv")
4 Removed: abort "CSV not found: #{csv_path}" unless File.exist?(csv_path)
5 Removed:
6 Removed: CSV.foreach(csv_path, headers: true) do |row|
7 Removed: NutrientMeasurement.find_or_create_by!(measured_on: Date.parse(row["date"])) do |m|
8 Removed: m.nno3 = row["nno3"]
9 Removed: m.p = row["p"]
10 Removed: m.k = row["k"]
11 Removed: m.ca = row["ca"]
12 Removed: m.mg = row["mg"]
13 Removed: m.s = row["s"]
14 Removed: m.na = row["na"]
15 Removed: m.cl = row["cl"]
16 Removed: m.si = row["si"]
17 Removed: m.fe = row["fe"]
18 Removed: m.zn = row["zn"]
19 Removed: m.b = row["b"]
20 Removed: m.mn = row["mn"]
21 Removed: m.cu = row["cu"]
22 Removed: m.mo = row["mo"]
23 Removed: m.nnh4 = row["nnh4"]
24 Removed: end
25 Removed: end
26 Removed:
27 Removed: puts "NutrientMeasurements: #{NutrientMeasurement.count}"
db/seeds/3_crops.rb
index 6a5c10fc..00000000 100644..000000
@@ -1,125 +0,0 @@
1 Removed: # crop_type: 0=leafy greens, 1=fruits, 2=herbs
2 Removed:
3 Removed: LEAFY = {
4 Removed: nno3: 150,
5 Removed: p: 31,
6 Removed: k: 210,
7 Removed: ca: 90,
8 Removed: mg: 24,
9 Removed: s: 32,
10 Removed: fe: 1.0,
11 Removed: mn: 0.25,
12 Removed: zn: 0.13,
13 Removed: b: 0.16,
14 Removed: cu: 0.023,
15 Removed: mo: 0.024,
16 Removed: nnh4: 5,
17 Removed: # keep low for leafy
18 Removed: na: 10,
19 Removed: cl: 5,
20 Removed: si: 20
21 Removed: }
22 Removed:
23 Removed: HERB = LEAFY # Herbs run well on the Cornell leafy recipe
24 Removed:
25 Removed: TOMATO = {
26 Removed: # CEAC Stage 3 "multi-crop" / mature tomato
27 Removed: nno3: 190,
28 Removed: p: 47,
29 Removed: k: 350,
30 Removed: ca: 200,
31 Removed: mg: 65,
32 Removed: s: 102,
33 Removed: fe: 2.0,
34 Removed: mn: 0.55,
35 Removed: zn: 0.33,
36 Removed: b: 0.28,
37 Removed: cu: 0.05,
38 Removed: mo: 0.05,
39 Removed: nnh4: 0,
40 Removed: # CEAC recipes are NO3-N; keep NH4 minimal
41 Removed: na: 20,
42 Removed: cl: 25,
43 Removed: si: 20
44 Removed: }
45 Removed:
46 Removed: HOT_PEPPER = {
47 Removed: # Mature greenhouse pepper (HortAmericas summary of UA recipes)
48 Removed: nno3: 180,
49 Removed: p: 50,
50 Removed: k: 280,
51 Removed: ca: 200,
52 Removed: mg: 45,
53 Removed: s: 20,
54 Removed: fe: 1.0,
55 Removed: mn: 0.55,
56 Removed: zn: 0.33,
57 Removed: b: 0.30,
58 Removed: cu: 0.05,
59 Removed: mo: 0.05,
60 Removed: nnh4: 15,
61 Removed: na: 20,
62 Removed: cl: 10,
63 Removed: si: 20
64 Removed: }
65 Removed:
66 Removed: STRAWBERRY = {
67 Removed: # UA strawberry (Yamazaki) emphasizes lower EC; use conservative macros
68 Removed: nno3: 120,
69 Removed: p: 30,
70 Removed: k: 200,
71 Removed: ca: 120,
72 Removed: mg: 35,
73 Removed: s: 50,
74 Removed: fe: 1.5,
75 Removed: mn: 0.50,
76 Removed: zn: 0.20,
77 Removed: b: 0.30,
78 Removed: cu: 0.05,
79 Removed: mo: 0.05,
80 Removed: nnh4: 5,
81 Removed: na: 10,
82 Removed: cl: 5,
83 Removed: si: 20
84 Removed: }
85 Removed:
86 Removed: RASPBERRY = {
87 Removed: # Sparse data; align with strawberry but a touch higher vigor
88 Removed: nno3: 140,
89 Removed: p: 35,
90 Removed: k: 230,
91 Removed: ca: 150,
92 Removed: mg: 40,
93 Removed: s: 50,
94 Removed: fe: 1.5,
95 Removed: mn: 0.50,
96 Removed: zn: 0.20,
97 Removed: b: 0.30,
98 Removed: cu: 0.05,
99 Removed: mo: 0.05,
100 Removed: nnh4: 5,
101 Removed: na: 15,
102 Removed: cl: 5,
103 Removed: si: 20
104 Removed: }
105 Removed:
106 Removed: [
107 Removed: [ "lettuce", 0, LEAFY ],
108 Removed: [ "kale", 0, LEAFY ],
109 Removed: [ "cabbage, chinese", 0, LEAFY ],
110 Removed: [ "tomatoes", 1, TOMATO ],
111 Removed: [ "raspberries", 1, RASPBERRY ],
112 Removed: [ "strawberries", 1, STRAWBERRY ],
113 Removed: [ "hot peppers", 1, HOT_PEPPER ],
114 Removed: [ "parsley", 2, HERB ],
115 Removed: [ "chives", 2, HERB ],
116 Removed: [ "italian basil", 2, HERB ],
117 Removed: [ "dill", 2, HERB ]
118 Removed: ].each do |name, type, nutrient_requirements|
119 Removed: Crop.find_or_create_by!(name: name) do |c|
120 Removed: c.crop_type = type
121 Removed: c.attributes = nutrient_requirements
122 Removed: end
123 Removed: end
124 Removed:
125 Removed: puts "Crops: #{Crop.count}"
db/seeds/4_beds_and_rafts.rb
index 1cd95dcb..00000000 100644..000000
@@ -1,24 +0,0 @@
1 Removed: BEDS = 14
2 Removed: RAFTS = 10
3 Removed:
4 Removed: 1.upto(BEDS) do |b|
5 Removed: bed = Bed.find_or_create_by!(location: b)
6 Removed:
7 Removed: crop_name = case b
8 Removed: when 1..2 then "tomatoes"
9 Removed: when 3 then "hot peppers"
10 Removed: when 4 then "chives"
11 Removed: when 5 then "italian basil"
12 Removed: when 6..7 then "cabbage, chinese"
13 Removed: else "lettuce"
14 Removed: end
15 Removed:
16 Removed: 1.upto(RAFTS) do |r|
17 Removed: raft = bed.rafts.find_or_create_by!(location: r) do |raft|
18 Removed: raft.crop = Crop.find_by!(name: crop_name)
19 Removed: end
20 Removed: end
21 Removed: end
22 Removed:
23 Removed: puts "Beds: #{Bed.count}"
24 Removed: puts "Rafts: #{Raft.count}"
db/seeds/5_fertilizer_components.rb.bkp
index 8a1fc543..00000000 100644..000000
@@ -1,38 +0,0 @@
1 Removed: FERTILIZER_COMPONENTS = [
2 Removed: # Macros / bases
3 Removed: { name: "Potassium Nitrate", formula: "KNO3", nno3: 13.50, k: 38.60 },
4 Removed: { name: "Calcium Nitrate", formula: "Ca(NO3)2·xH2O", nno3: 15.50, ca: 18.94 },
5 Removed: { name: "Ammonium Nitrate", formula: "NH4NO3", nno3: 13.50, nnh4: 13.50 }, # total N ≈ 27
6 Removed: { name: "Diammonium Phosphate", formula: "(NH4)2HPO4", p: 21.00 }, # NH4 can be added later if desired
7 Removed:
8 Removed: # Acids / buffers
9 Removed: { name: "Nitric Acid 53%", formula: "HNO3", nno3: 11.70 },
10 Removed: { name: "Potassium Bicarbonate", formula: "KHCO3", k: 39.05 },
11 Removed: { name: "Calcium Carbonate", formula: "CaCO3", ca: 38.00 },
12 Removed:
13 Removed: # K–Mg–S complex (Patentkali-type blend)
14 Removed: { name: "Potassium Sulfate blend (K–Mg–S + NaCl)", formula: "K2SO4+MgSO4+NaCl",
15 Removed: p: 0.44, k: 22.66, mg: 6.16, s: 17.77, na: 2.16, cl: 3.34 },
16 Removed:
17 Removed: # Sulfates (micros / secondary)
18 Removed: { name: "Magnesium Sulfate", formula: "MgSO4·7H2O", mg: 9.648, s: 13.016 },
19 Removed: { name: "Manganese Sulfate", formula: "MnSO4", s: 7.17, mn: 12.00 },
20 Removed: { name: "Zinc Sulfate (Fiza Zinc)", formula: "ZnSO4", zn: 12.00 },
21 Removed:
22 Removed: # Chelates / traces
23 Removed: { name: "Iron DTPA 11.8%", formula: "Fe-DTPA", fe: 11.80 },
24 Removed: { name: "HelioCopper (Cu chelate)", formula: "Cu-chelate", cu: 40.00 },
25 Removed:
26 Removed: # Boron & Mo sources
27 Removed: { name: "Boron–Molybdenum (Boronia LS)", formula: "B+Mo", b: 13.50, mo: 0.028 },
28 Removed: { name: "Boronia MO12 (10L)", formula: "B+Mn+Cu", b: 8.90, mn: 0.089, cu: 0.89 },
29 Removed: { name: "Sodium Molybdate", formula: "Na2MoO4", mo: 39.50 }
30 Removed: ]
31 Removed:
32 Removed: FERTILIZER_COMPONENTS.each do |attrs|
33 Removed: FertilizerComponent.find_or_create_by!(name: attrs[:name]) do |c|
34 Removed: c.attributes = attrs
35 Removed: end
36 Removed: end
37 Removed:
38 Removed: puts "Fertilizer components: #{FertilizerComponent.count}"
db/seeds/6_fertilizer_products.rb
index 6769bdef..00000000 100644..000000
@@ -1,143 +0,0 @@
1 Removed: FERTILIZER_RECIPES = [
2 Removed: {
3 Removed: name: "Multi K Reci",
4 Removed: purity: 99.0,
5 Removed: composition: [
6 Removed: { component: {
7 Removed: name: "Potassium nitrate",
8 Removed: formula: "KNO3",
9 Removed: nno3: 13.0,
10 Removed: k: 46.0 },
11 Removed: percent_w: 100.0 }
12 Removed: ]
13 Removed: },
14 Removed: {
15 Removed: name: "Fixa Mn",
16 Removed: purity: 98.0,
17 Removed: composition: [
18 Removed: { component: {
19 Removed: name: "Manganese sulfate",
20 Removed: formula: "MnSO4·H2O",
21 Removed: mn: 32.0,
22 Removed: s: 18.0 },
23 Removed: percent_w: 100.0 }
24 Removed: ]
25 Removed: },
26 Removed: {
27 Removed: name: "Multi-Cal Haïfa",
28 Removed: purity: 99.0,
29 Removed: composition: [
30 Removed: { component: {
31 Removed: name: "Calcium nitrate",
32 Removed: formula: "Ca(NO3)2·4H2O",
33 Removed: nno3: 15.5,
34 Removed: ca: 19.0 },
35 Removed: percent_w: 100.0 }
36 Removed: ]
37 Removed: },
38 Removed: {
39 Removed: name: "DAP 18/46/00",
40 Removed: purity: 100.0,
41 Removed: composition: [
42 Removed: { component: {
43 Removed: name: "Diammonium phosphate",
44 Removed: formula: "(NH4)2HPO4",
45 Removed: nnh4: 18.0,
46 Removed: p: 20.0 },
47 Removed: percent_w: 100.0 }
48 Removed: ]
49 Removed: },
50 Removed: {
51 Removed: name: "Patenkali",
52 Removed: purity: 100.0,
53 Removed: composition: [
54 Removed: { component: {
55 Removed: name: "Potassium sulfate",
56 Removed: formula: "K2SO4",
57 Removed: k: 50.0,
58 Removed: s: 18.0
59 Removed: }, percent_w: 100.0 }
60 Removed: ]
61 Removed: },
62 Removed: {
63 Removed: name: "Eso Top",
64 Removed: purity: 100.0,
65 Removed: composition: [
66 Removed: { component: {
67 Removed: name: "Magnesium sulfate",
68 Removed: formula: "MgSO4·7H2O",
69 Removed: mg: 9.8,
70 Removed: s: 13.0 },
71 Removed: percent_w: 100.0 }
72 Removed: ]
73 Removed: },
74 Removed: {
75 Removed: name: "Ammonitrate 27",
76 Removed: purity: 100.0,
77 Removed: composition: [
78 Removed: { component: {
79 Removed: name: "Ammonium nitrate",
80 Removed: formula: "NH4NO3",
81 Removed: nno3: 13.5,
82 Removed: nnh4: 13.5 },
83 Removed: percent_w: 100.0 }
84 Removed: ]
85 Removed: },
86 Removed: {
87 Removed: name: "Fer chélaté",
88 Removed: purity: 100.0,
89 Removed: composition: [
90 Removed: { component: {
91 Removed: name: "Iron chelate (EDDHA)",
92 Removed: formula: "Fe-EDDHA",
93 Removed: fe: 6.0 },
94 Removed: percent_w: 100.0 }
95 Removed: ]
96 Removed: },
97 Removed: {
98 Removed: name: "Carbonate de calcium",
99 Removed: purity: 100.0,
100 Removed: composition: [
101 Removed: { component: {
102 Removed: name: "Calcium carbonate",
103 Removed: formula: "CaCO3",
104 Removed: ca: 40.0 },
105 Removed: percent_w: 100.0 }
106 Removed: ]
107 Removed: },
108 Removed: {
109 Removed: name: "Héliocuivre",
110 Removed: purity: 100.0,
111 Removed: composition: [
112 Removed: { component: {
113 Removed: name: "Copper chelate (EDTA)",
114 Removed: formula: "Cu-EDTA",
115 Removed: cu: 14.0 },
116 Removed: percent_w: 100.0 }
117 Removed: ]
118 Removed: }
119 Removed: ]
120 Removed:
121 Removed: FERTILIZER_RECIPES.each do |recipe|
122 Removed: product = FertilizerProduct.find_or_create_by!(name: recipe[:name]) do |fp|
123 Removed: fp.purity = recipe[:purity]
124 Removed: end
125 Removed:
126 Removed: recipe[:composition].each do |c|
127 Removed: comp_attrs = c[:component]
128 Removed: component = FertilizerComponent.find_or_create_by!(name: comp_attrs[:name]) do |fc|
129 Removed: fc.formula = comp_attrs[:formula]
130 Removed: end
131 Removed: # update nutrient fields if missing
132 Removed: component.update!(comp_attrs.except(:name, :formula))
133 Removed:
134 Removed: FertilizerComposition.find_or_create_by!(
135 Removed: fertilizer_product: product,
136 Removed: fertilizer_component: component
137 Removed: ) do |fc|
138 Removed: fc.percent_w = c[:percent_w]
139 Removed: end
140 Removed: end
141 Removed: end
142 Removed:
143 Removed: puts "FertilizerProducts: #{FertilizerProduct.count}"
db/seeds/NutrientMeasurement.rb
index 00000000..fcf8cfa8 000000..100644
@@ -0,0 +1,25 @@
1 Added: require "csv"
2 Added:
3 Added: csv_path = Rails.root.join("db", "data", "dolibarr_measurements.csv")
4 Added: abort "CSV not found: #{csv_path}" unless File.exist?(csv_path)
5 Added:
6 Added: CSV.foreach(csv_path, headers: true) do |row|
7 Added: NutrientMeasurement.find_or_create_by!(measured_on: Date.parse(row["date"])) do |m|
8 Added: m.nno3 = row["nno3"]
9 Added: m.p = row["p"]
10 Added: m.k = row["k"]
11 Added: m.ca = row["ca"]
12 Added: m.mg = row["mg"]
13 Added: m.s = row["s"]
14 Added: m.na = row["na"]
15 Added: m.cl = row["cl"]
16 Added: m.si = row["si"]
17 Added: m.fe = row["fe"]
18 Added: m.zn = row["zn"]
19 Added: m.b = row["b"]
20 Added: m.mn = row["mn"]
21 Added: m.cu = row["cu"]
22 Added: m.mo = row["mo"]
23 Added: m.nnh4 = row["nnh4"]
24 Added: end
25 Added: end
db/seeds/NutrientProfile.rb
index 00000000..04e16cc0 000000..100644
@@ -0,0 +1,109 @@
1 Added: [ { name: "formule moyenne générale",
2 Added: nno3: 160.00,
3 Added: p: 30.00,
4 Added: k: 230.00,
5 Added: ca: 100.00,
6 Added: mg: 30.00,
7 Added: s: 60.00,
8 Added: fe: 5.00,
9 Added: zn: 0.15,
10 Added: b: 0.30,
11 Added: mn: 0.50,
12 Added: cu: 0.15,
13 Added: mo: 0.05 },
14 Added: { name: "salades",
15 Added: nno3: 130.00,
16 Added: p: 60.00,
17 Added: k: 300.00,
18 Added: ca: 100.00,
19 Added: mg: 30.00,
20 Added: s: 60.00,
21 Added: fe: 2.00,
22 Added: zn: 0.10,
23 Added: b: 0.50,
24 Added: mn: 0.50,
25 Added: cu: 0.05,
26 Added: mo: 0.05 },
27 Added: { name: "développement floral",
28 Added: nno3: 190.00,
29 Added: p: 50.00,
30 Added: k: 210.00,
31 Added: ca: 200.00,
32 Added: mg: 50.00,
33 Added: s: 66.00,
34 Added: fe: 5.00,
35 Added: zn: 0.15,
36 Added: b: 0.30,
37 Added: mn: 0.50,
38 Added: cu: 0.15,
39 Added: mo: 0.05 },
40 Added: { name: "tomates général",
41 Added: nno3: 140.00,
42 Added: p: 50.00,
43 Added: k: 352.00,
44 Added: ca: 180.00,
45 Added: mg: 50.00,
46 Added: s: 168.00,
47 Added: fe: 5.00,
48 Added: zn: 0.10,
49 Added: b: 0.30,
50 Added: mn: 0.80,
51 Added: cu: 0.07,
52 Added: mo: 0.03 },
53 Added: { name: "jeunes tomates",
54 Added: nno3: 100.00,
55 Added: p: 40.00,
56 Added: k: 200.00,
57 Added: ca: 100.00,
58 Added: mg: 20.00,
59 Added: s: 53.00,
60 Added: fe: 3.00,
61 Added: zn: 0.10,
62 Added: b: 0.30,
63 Added: mn: 0.80,
64 Added: cu: 0.07,
65 Added: mo: 0.03 },
66 Added: { name: "tomate premiers fruits",
67 Added: nno3: 130.00,
68 Added: p: 55.00,
69 Added: k: 300.00,
70 Added: ca: 150.00,
71 Added: mg: 33.00,
72 Added: s: 109.00,
73 Added: fe: 3.00,
74 Added: zn: 0.10,
75 Added: b: 0.30,
76 Added: mn: 0.80,
77 Added: cu: 0.07,
78 Added: mo: 0.03 },
79 Added: { name: "tomate mûre",
80 Added: nno3: 180.00,
81 Added: p: 65.00,
82 Added: k: 400.00,
83 Added: ca: 400.00,
84 Added: mg: 45.00,
85 Added: s: 144.00,
86 Added: fe: 3.00,
87 Added: zn: 0.10,
88 Added: b: 0.30,
89 Added: mn: 0.80,
90 Added: cu: 0.07,
91 Added: mo: 0.03 },
92 Added: { name: "framboise - tous stades",
93 Added: nno3: 70.00,
94 Added: p: 12.00,
95 Added: k: 88.00,
96 Added: ca: 90.00,
97 Added: mg: 24.00,
98 Added: s: 48.00,
99 Added: fe: 0.56,
100 Added: zn: 0.33,
101 Added: b: 0.11,
102 Added: mn: 0.11,
103 Added: cu: 0.03,
104 Added: mo: 0.01 },
105 Added: ].each do |profile|
106 Added: NutrientProfile.find_or_create_by!(name: profile[:name]) do |p|
107 Added: p.attributes = profile
108 Added: end
109 Added: end