diff options
Diffstat (limited to 'app/views')
27 files changed, 840 insertions, 0 deletions
diff --git a/app/views/application/_copyright_footer.html.erb b/app/views/application/_copyright_footer.html.erb new file mode 100644 index 0000000..4a6d240 --- /dev/null +++ b/app/views/application/_copyright_footer.html.erb @@ -0,0 +1,10 @@ +<footer class="footer mt-auto py-3"> + <div class="container d-flex justify-content-between"> + <span class="text-muted">© 2025 FAPG. All rights reserved.</span> + <ul class="list-inline"> + <li class="list-inline-item"><a href="#" class="text-muted">Privacy Policy</a></li> + <li class="list-inline-item"><a href="#" class="text-muted">Terms of Use</a></li> + <li class="list-inline-item"><a href="#" class="text-muted">Contact Us</a></li> + </ul> + </div> +</footer> diff --git a/app/views/application/_navbar.html.erb b/app/views/application/_navbar.html.erb new file mode 100644 index 0000000..9fa250a --- /dev/null +++ b/app/views/application/_navbar.html.erb @@ -0,0 +1,33 @@ +<nav class="navbar navbar-expand-lg navbar-light bg-light"> + <div class="container-fluid"> + <span class="navbar-brand">FAPG</span> + + <button class="navbar-toggler" + type="button" + data-bs-toggle="collapse" + data-bs-target="#navbarSupportedContent" + aria-controls="navbarSupportedContent" + aria-expanded="false" + aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + + <div class="collapse navbar-collapse" id="navbarSupportedContent"> + <ul class="navbar-nav me-auto mb-2 mb-lg-0"> + <li class="nav-item"> + <%= link_to "Home", root_path, class: "nav-link" %> + </li> + <li class="nav-item"> + <%= link_to "Ferti", root_path, class: "nav-link" %> + </li> + <li class="nav-item"> + <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Inventory</a> + </li> + <li class="nav-item"> + <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Billing</a> + </li> + </ul> + + </div> + </div> +</nav> diff --git a/app/views/crops/_crop.html.erb b/app/views/crops/_crop.html.erb new file mode 100644 index 0000000..e9b9b4d --- /dev/null +++ b/app/views/crops/_crop.html.erb @@ -0,0 +1,92 @@ +<div id="<%= dom_id crop %>"> + <p> + <strong>Name:</strong> + <%= crop.name %> + </p> + + <p> + <strong>Crop type:</strong> + <%= crop.crop_type %> + </p> + + <p> + <strong>Nno3:</strong> + <%= crop.nno3 %> + </p> + + <p> + <strong>P:</strong> + <%= crop.p %> + </p> + + <p> + <strong>K:</strong> + <%= crop.k %> + </p> + + <p> + <strong>Ca:</strong> + <%= crop.ca %> + </p> + + <p> + <strong>Mg:</strong> + <%= crop.mg %> + </p> + + <p> + <strong>S:</strong> + <%= crop.s %> + </p> + + <p> + <strong>Na:</strong> + <%= crop.na %> + </p> + + <p> + <strong>Cl:</strong> + <%= crop.cl %> + </p> + + <p> + <strong>Si:</strong> + <%= crop.si %> + </p> + + <p> + <strong>Fe:</strong> + <%= crop.fe %> + </p> + + <p> + <strong>Zn:</strong> + <%= crop.zn %> + </p> + + <p> + <strong>B:</strong> + <%= crop.b %> + </p> + + <p> + <strong>Mn:</strong> + <%= crop.mn %> + </p> + + <p> + <strong>Cu:</strong> + <%= crop.cu %> + </p> + + <p> + <strong>Mo:</strong> + <%= crop.mo %> + </p> + + <p> + <strong>Nnh4:</strong> + <%= crop.nnh4 %> + </p> + +</div> diff --git a/app/views/crops/_form.html.erb b/app/views/crops/_form.html.erb new file mode 100644 index 0000000..90226b6 --- /dev/null +++ b/app/views/crops/_form.html.erb @@ -0,0 +1,107 @@ +<%= form_with(model: crop) do |form| %> + <% if crop.errors.any? %> + <div style="color: red"> + <h2><%= pluralize(crop.errors.count, "error") %> prohibited this crop from being saved:</h2> + + <ul> + <% crop.errors.each do |error| %> + <li><%= error.full_message %></li> + <% end %> + </ul> + </div> + <% end %> + + <div> + <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> + </div> + + <div> + <%= form.label :crop_type, style: "display: block" %> + <%= form.number_field :crop_type %> + </div> + + <div> + <%= form.label :nno3, style: "display: block" %> + <%= form.text_field :nno3 %> + </div> + + <div> + <%= form.label :p, style: "display: block" %> + <%= form.text_field :p %> + </div> + + <div> + <%= form.label :k, style: "display: block" %> + <%= form.text_field :k %> + </div> + + <div> + <%= form.label :ca, style: "display: block" %> + <%= form.text_field :ca %> + </div> + + <div> + <%= form.label :mg, style: "display: block" %> + <%= form.text_field :mg %> + </div> + + <div> + <%= form.label :s, style: "display: block" %> + <%= form.text_field :s %> + </div> + + <div> + <%= form.label :na, style: "display: block" %> + <%= form.text_field :na %> + </div> + + <div> + <%= form.label :cl, style: "display: block" %> + <%= form.text_field :cl %> + </div> + + <div> + <%= form.label :si, style: "display: block" %> + <%= form.text_field :si %> + </div> + + <div> + <%= form.label :fe, style: "display: block" %> + <%= form.text_field :fe %> + </div> + + <div> + <%= form.label :zn, style: "display: block" %> + <%= form.text_field :zn %> + </div> + + <div> + <%= form.label :b, style: "display: block" %> + <%= form.text_field :b %> + </div> + + <div> + <%= form.label :mn, style: "display: block" %> + <%= form.text_field :mn %> + </div> + + <div> + <%= form.label :cu, style: "display: block" %> + <%= form.text_field :cu %> + </div> + + <div> + <%= form.label :mo, style: "display: block" %> + <%= form.text_field :mo %> + </div> + + <div> + <%= form.label :nnh4, style: "display: block" %> + <%= form.text_field :nnh4 %> + </div> + + <div> + <%= form.submit %> + </div> +<% end %> diff --git a/app/views/crops/edit.html.erb b/app/views/crops/edit.html.erb new file mode 100644 index 0000000..54c616c --- /dev/null +++ b/app/views/crops/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing crop" %> + +<h1>Editing crop</h1> + +<%= render "form", crop: @crop %> + +<br> + +<div> + <%= link_to "Show this crop", @crop %> | + <%= link_to "Back to crops", crops_path %> +</div> diff --git a/app/views/crops/index.html.erb b/app/views/crops/index.html.erb new file mode 100644 index 0000000..bae09fa --- /dev/null +++ b/app/views/crops/index.html.erb @@ -0,0 +1,16 @@ +<p style="color: green"><%= notice %></p> + +<% content_for :title, "Crops" %> + +<h1>Crops</h1> + +<div id="crops"> + <% @crops.each do |crop| %> + <%= render crop %> + <p> + <%= link_to "Show this crop", crop %> + </p> + <% end %> +</div> + +<%= link_to "New crop", new_crop_path %> diff --git a/app/views/crops/new.html.erb b/app/views/crops/new.html.erb new file mode 100644 index 0000000..4ef4da5 --- /dev/null +++ b/app/views/crops/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New crop" %> + +<h1>New crop</h1> + +<%= render "form", crop: @crop %> + +<br> + +<div> + <%= link_to "Back to crops", crops_path %> +</div> diff --git a/app/views/crops/show.html.erb b/app/views/crops/show.html.erb new file mode 100644 index 0000000..971f097 --- /dev/null +++ b/app/views/crops/show.html.erb @@ -0,0 +1,10 @@ +<p style="color: green"><%= notice %></p> + +<%= render @crop %> + +<div> + <%= link_to "Edit this crop", edit_crop_path(@crop) %> | + <%= link_to "Back to crops", crops_path %> + + <%= button_to "Destroy this crop", @crop, method: :delete %> +</div> diff --git a/app/views/dashboard/_raft_allocation.html.erb b/app/views/dashboard/_raft_allocation.html.erb new file mode 100644 index 0000000..ef95cdd --- /dev/null +++ b/app/views/dashboard/_raft_allocation.html.erb @@ -0,0 +1,9 @@ +<div class="card shadow mb-4"> + <div class="card-header d-flex justify-content-between align-items-center"> + <h5 class="mb-0">Raft Allocation</h5> + <%= link_to "Edit raft allocation", editor_rafts_path, class: "btn btn-sm btn-primary" %> + </div> + + <%= bar_chart @raft_data, stacked: true %> +</div> + diff --git a/app/views/dashboard/_recent_measurements.html.erb b/app/views/dashboard/_recent_measurements.html.erb new file mode 100644 index 0000000..bc63a60 --- /dev/null +++ b/app/views/dashboard/_recent_measurements.html.erb @@ -0,0 +1,22 @@ +<div class="card shadow mb-4"> + <div class="card-header d-flex justify-content-between align-items-center"> + <h5 class="mb-0">Nutrient Measurements</h5> + <div class="btn-group"> + <%#= link_to "Add new measurement", editor_rafts_path, class: "btn btn-sm btn-primary" %> + <%#= link_to "View all", editor_rafts_path, class: "btn btn-sm btn-secondary" %> + </div> + </div> + + <div class="card-body p-0"> + <div class="container mb-3"> + <%= line_chart @npk_measurement_data, + title: "NPK", + ytitle: "Concentration (mg/L)" %> + </div> + <div class="container mb-3"> + <%= line_chart @ammonium_measurement_data, + title: "Ammonium", + ytitle: "Concentration (mg/L)" %> + </div> + </div> +</div> diff --git a/app/views/dashboard/_target_table.html.erb b/app/views/dashboard/_target_table.html.erb new file mode 100644 index 0000000..b8f7e66 --- /dev/null +++ b/app/views/dashboard/_target_table.html.erb @@ -0,0 +1,40 @@ +<div class="card shadow mb-4"> + <div class="card-header d-flex justify-content-between align-items-center"> + <h5 class="mb-0">Target nutrient concentrations</h5> + <%= link_to "Get Ferti© recipe", ferti_recipe_path, class: "btn btn-sm btn-primary" %> + </div> + + <div class="table-responsive"> + <table class="table table-sm align-middle mb-0"> + <thead class="table-light"> + <tr> + <th scope="col" class="text-nowrap">Nutrient</th> + <th scope="col" class="text-end"> Latest (mg/L) + </th> + <th scope="col" class="text-end">Target (mg/L)</th> + <th scope="col" class="text-end">Δ %</th> + </tr> + </thead> + <tbody> + <% NutrientsHelper::NUTRIENTS.each do |n| %> + <% latest = @latest_measurement[n] || 0 %> + <% target = @target[n] || 0 %> + <% delta = target - latest %> + <tr> + <th scope="row" class="text-nowrap"><%= n.upcase %></th> + <td class="text-end"><%= fmt(latest) %></td> + <td class="text-end"><%= fmt(target) %></td> + <td class="text-end"> + <span class="badge <%= delta_badge_class(delta) %>"> + <%= fmt(delta) %>% + </span> + </td> + </tr> + <% end %> + </tbody> + </table> + </div> + <div class="card-footer"> + Latest measurement: <%= @latest_measurement.measured_on || "none yet" %> + </div> +</div> diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb new file mode 100644 index 0000000..2902ada --- /dev/null +++ b/app/views/dashboard/index.html.erb @@ -0,0 +1,7 @@ +<h1 class="display-1">Ferti</h1> + +<%= render "raft_allocation" %> + +<%= render "target_table" %> + +<%= render "recent_measurements" %> diff --git a/app/views/fertilizer_products/_fertilizer_product.html.erb b/app/views/fertilizer_products/_fertilizer_product.html.erb new file mode 100644 index 0000000..d84a03d --- /dev/null +++ b/app/views/fertilizer_products/_fertilizer_product.html.erb @@ -0,0 +1,12 @@ +<div id="<%= dom_id fertilizer_product %>"> + <p> + <strong>Name:</strong> + <%= fertilizer_product.name %> + </p> + + <p> + <strong>Purity:</strong> + <%= fertilizer_product.purity %> + </p> + +</div> diff --git a/app/views/fertilizer_products/_form.html.erb b/app/views/fertilizer_products/_form.html.erb new file mode 100644 index 0000000..517fe09 --- /dev/null +++ b/app/views/fertilizer_products/_form.html.erb @@ -0,0 +1,27 @@ +<%= form_with(model: fertilizer_product) do |form| %> + <% if fertilizer_product.errors.any? %> + <div style="color: red"> + <h2><%= pluralize(fertilizer_product.errors.count, "error") %> prohibited this fertilizer_product from being saved:</h2> + + <ul> + <% fertilizer_product.errors.each do |error| %> + <li><%= error.full_message %></li> + <% end %> + </ul> + </div> + <% end %> + + <div> + <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> + </div> + + <div> + <%= form.label :purity, style: "display: block" %> + <%= form.text_field :purity %> + </div> + + <div> + <%= form.submit %> + </div> +<% end %> diff --git a/app/views/fertilizer_products/edit.html.erb b/app/views/fertilizer_products/edit.html.erb new file mode 100644 index 0000000..aa88dad --- /dev/null +++ b/app/views/fertilizer_products/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing fertilizer product" %> + +<h1>Editing fertilizer product</h1> + +<%= render "form", fertilizer_product: @fertilizer_product %> + +<br> + +<div> + <%= link_to "Show this fertilizer product", @fertilizer_product %> | + <%= link_to "Back to fertilizer products", fertilizer_products_path %> +</div> diff --git a/app/views/fertilizer_products/index.html.erb b/app/views/fertilizer_products/index.html.erb new file mode 100644 index 0000000..f624ffc --- /dev/null +++ b/app/views/fertilizer_products/index.html.erb @@ -0,0 +1,16 @@ +<p style="color: green"><%= notice %></p> + +<% content_for :title, "Fertilizer products" %> + +<h1>Fertilizer products</h1> + +<div id="fertilizer_products"> + <% @fertilizer_products.each do |fertilizer_product| %> + <%= render fertilizer_product %> + <p> + <%= link_to "Show this fertilizer product", fertilizer_product %> + </p> + <% end %> +</div> + +<%= link_to "New fertilizer product", new_fertilizer_product_path %> diff --git a/app/views/fertilizer_products/new.html.erb b/app/views/fertilizer_products/new.html.erb new file mode 100644 index 0000000..81aad79 --- /dev/null +++ b/app/views/fertilizer_products/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New fertilizer product" %> + +<h1>New fertilizer product</h1> + +<%= render "form", fertilizer_product: @fertilizer_product %> + +<br> + +<div> + <%= link_to "Back to fertilizer products", fertilizer_products_path %> +</div> diff --git a/app/views/fertilizer_products/show.html.erb b/app/views/fertilizer_products/show.html.erb new file mode 100644 index 0000000..ea97041 --- /dev/null +++ b/app/views/fertilizer_products/show.html.erb @@ -0,0 +1,10 @@ +<p style="color: green"><%= notice %></p> + +<%= render @fertilizer_product %> + +<div> + <%= link_to "Edit this fertilizer product", edit_fertilizer_product_path(@fertilizer_product) %> | + <%= link_to "Back to fertilizer products", fertilizer_products_path %> + + <%= button_to "Destroy this fertilizer product", @fertilizer_product, method: :delete %> +</div> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..bf61c9d --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> + <head> + <title><%= content_for(:title) || "Ferti" %></title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <meta name="apple-mobile-web-app-capable" content="yes"> + <meta name="mobile-web-app-capable" content="yes"> + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + <link rel="icon" href="/icon.png" type="image/png"> + <link rel="icon" href="/icon.svg" type="image/svg+xml"> + <link rel="apple-touch-icon" href="/icon.png"> + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + + <!-- Bootstrap 5 CSS --> + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> + + <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> + <script src="https://cdn.jsdelivr.net/npm/chartkick@5"></script> + + <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script> + + <%= javascript_importmap_tags %> + </head> + + <body> + <%= render "application/navbar" %> + + <div class="container"> + <%= yield %> + </div> + + <%= render "application/copyright_footer" %> + + <!-- Bootstrap JS (optional, for dropdowns, etc.) --> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> + </body> +</html> diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style> + /* Email styles need to be inline */ + </style> + </head> + + <body> + <%= yield %> + </body> +</html> diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/nutrient_measurement/index.html.erb b/app/views/nutrient_measurement/index.html.erb new file mode 100644 index 0000000..d9f522e --- /dev/null +++ b/app/views/nutrient_measurement/index.html.erb @@ -0,0 +1,27 @@ +<h1>NutrientMeasurement#index</h1> +<p>Find me in app/views/nutrient_measurement/index.html.erb</p> + +<div class="table-responsive"> + <table class="table table-sm table-striped table-hover align-middle table-nutrient mb-0"> + <thead class="table-light"> + <tr> + <th>Date</th> + <th class="numeric" title="Total N = NO₃‑N + NH₄‑N">N (total)</th> + <th class="numeric">P</th> + <th class="numeric">K</th> + <th class="numeric" title="Ammonia nitrogen">NH₄‑N</th> + </tr> + </thead> + <tbody> + <% @measurements.each do |m| %> + <tr> + <td><%= l(m.measured_on) %></td> + <td class="numeric"><%= fmt2(total_n(m)) %></td> + <td class="numeric"><%= fmt2(m.p) %></td> + <td class="numeric"><%= fmt2(m.k) %></td> + <td class="numeric"><%= fmt2(m.nnh4) %></td> + </tr> + <% end %> + </tbody> + </table> +</div> diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000..f8908f0 --- /dev/null +++ b/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "Ferti", + "icons": [ + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ], + "start_url": "/", + "display": "standalone", + "scope": "/", + "description": "Ferti.", + "theme_color": "red", + "background_color": "red" +} diff --git a/app/views/pwa/service-worker.js b/app/views/pwa/service-worker.js new file mode 100644 index 0000000..b3a13fb --- /dev/null +++ b/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/app/views/rafts/editor.html.erb b/app/views/rafts/editor.html.erb new file mode 100644 index 0000000..cde56d8 --- /dev/null +++ b/app/views/rafts/editor.html.erb @@ -0,0 +1,108 @@ +<div class="d-flex justify-content-between align-items-center mb-3"> + <h1 class="display-1">Rafts editor</h1> + <%= link_to "Back to dashboard", root_path, class: "btn btn-outline-secondary" %> +</div> + +<!-- Bulk assign all rafts --> +<div class="card"> + <div class="card-header">Assign ALL rafts</div> + <div class="card-body"> + <%= form_with url: assign_all_rafts_path, method: :patch, class: "row g-2", data: { turbo: false } do %> + <div class="col-12 col-md-6"> + <select class="form-select" name="crop_id"> + <option value="">— Unassigned (clear) —</option> + <% @crops.each do |crop| %> + <option value="<%= crop.id %>"><%= crop.name %></option> + <% end %> + </select> + </div> + <div class="col-12 col-md-auto"> + <button class="btn btn-primary">Apply to all</button> + </div> + <% end %> + </div> +</div> + +<div class="table-responsive"> + <table class="table table-sm table-striped text-center align-middle my-3"> + <thead> + <tr> + <th class="text-start">Bed</th> + <% max_cols = @beds.map { |b| b.rafts.count }.max %> + <% (1..max_cols).each do |i| %> + <th>R<%= i %></th> + <% end %> + </tr> + </thead> + <tbody> + <% @beds.each do |bed| %> + <tr> + <th><%= bed.location %></th> + <% bed.rafts.order(:location).each do |raft| %> + <td> + <% if raft.crop %> + <%= raft.crop.name %> + <% else %> + — + <% end %> + </td> + <% end %> + </tr> + <% end %> + </tbody> + </table> +</div> + +<!-- Per-bed tables --> +<% @beds.each do |bed| %> + <div class="card mb-3" id="bed-<%= bed.id %>"> + <div class="card-header d-flex flex-wrap gap-2 align-items-center"> + <span class="me-auto">Bed <strong>#<%= bed.location %></strong></span> + <%= form_with url: assign_bed_rafts_path, method: :patch, class: "d-flex gap-2 align-items-center", data: { turbo: false } do %> + <input type="hidden" name="bed_id" value="<%= bed.id %>"> + <select class="form-select" name="crop_id"> + <option value="">— Unassigned (clear) —</option> + <% @crops.each do |crop| %> + <option value="<%= crop.id %>"><%= crop.name %></option> + <% end %> + </select> + <button class="btn btn-primary">Apply</button> + <% end %> + </div> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-sm mb-0 align-middle"> + <thead class="table-light"> + <tr> + <th class="text-nowrap text-center">Raft</th> + <th>Crop</th> + <th class="text-end">Actions</th> + </tr> + </thead> + <tbody> + <% bed.rafts.order(:location).each do |raft| %> + <tr> + <td class="text-center"><strong><%= raft.location %></strong></td> + <td style="max-width: 280px;"> + <%= form_with url: assign_one_raft_path(raft), method: :patch, class: "d-flex gap-2", data: { turbo: false } do %> + <select class="form-select" name="crop_id"> + <option value="">— Unassigned (clear) —</option> + <% @crops.each do |crop| %> + <option value="<%= crop.id %>" <%= "selected" if raft.crop_id == crop.id %>><%= crop.name %></option> + <% end %> + </select> + <button class="btn btn-outline-primary">Save</button> + <% end %> + </td> + <td class="text-end"> + <%= button_to "Clear", assign_one_raft_path(raft, crop_id: ""), method: :patch, + form: { data: { turbo: false } }, class: "btn btn-outline-secondary btn-sm" %> + </td> + </tr> + <% end %> + </tbody> + </table> + </div> + </div> + </div> +<% end %> diff --git a/app/views/recipes/_table.html.erb b/app/views/recipes/_table.html.erb new file mode 100644 index 0000000..a42a0b9 --- /dev/null +++ b/app/views/recipes/_table.html.erb @@ -0,0 +1,32 @@ +<div class="table-responsive"> + <table class="table table-sm align-middle mb-0"> + <thead class="table-light"> + <tr> + <th>Fertilizer product</th> + <th class="text-muted">Component</th> + <th class="text-end">Qty (kg)</th> + <th class="text-end">Per portion (kg)</th> + </tr> + </thead> + <tbody> + <% pcount = [[portions.to_i, 2].max, 10].min %> + <% recipe&.each do |component, kg| %> + <% prod_name = commercial_name_for(component) %> + <tr> + <td><strong><%= prod_name %></strong></td> + <td class="text-muted"><small><%= component.name %></small></td> + <td class="text-end fw-semibold"><%= fmt_kg(kg) %></td> + <td class="text-end"><%= fmt_kg(kg / pcount.to_f) %></td> + </tr> + <% end %> + <% if recipe.blank? %> + <tr><td colspan="4" class="text-center text-muted">No supplementation required.</td></tr> + <% end %> + </tbody> + </table> +</div> + +<div class="card-footer small text-muted"> + Volume: <%= number_with_delimiter(volume) %> L — + Portions: <%= pcount %> +</div> diff --git a/app/views/recipes/show.html.erb b/app/views/recipes/show.html.erb new file mode 100644 index 0000000..180fdae --- /dev/null +++ b/app/views/recipes/show.html.erb @@ -0,0 +1,108 @@ +<!-- app/views/recipes/show.html.erb --> +<%# Controls header %> +<div class="card shadow my-4"> + <div class="card-header d-flex flex-wrap gap-2 justify-content-between align-items-center"> + <h5 class="mb-0">Ferti© Recipe</h5> + + <div class="d-flex flex-wrap gap-2 align-items-center"> + <%= form_with url: ferti_recipe_path, method: :get, local: true, class: "d-flex flex-wrap gap-2 align-items-center", id: "recipe-form" do %> + <div class="d-flex align-items-center gap-2"> + <label class="mb-0 small text-nowrap" for="volume_select">Volume</label> + <select id="volume_select" name="volume" class="form-select form-select-sm"> + <% options = [["100 000 L", 100_000], ["200 000 L", 200_000], ["300 000 L", 300_000]] %> + <% current_volume = (params[:volume].presence || 100_000).to_i %> + <% options.each do |label, val| %> + <option value="<%= val %>" <%= 'selected' if val == current_volume %>><%= label %></option> + <% end %> + </select> + </div> + + <div class="vr" style="height: 24px;"></div> + + <div class="d-flex align-items-center gap-2"> + <label class="mb-0 small text-nowrap" for="portions_input">Portions</label> + <input id="portions_input" type="number" min="2" max="10" step="1" + class="form-control form-control-sm" value="<%= params[:portions].presence || 2 %>"> + </div> + <% end %> + + <%= link_to "Back", root_path, class: "btn btn-sm btn-secondary" %> + </div> + </div> + + <div class="table-responsive"> + <table class="table table-sm align-middle mb-0" id="recipe-table"> + <thead class="table-light"> + <tr> + <th>Product</th> + <th class="text-muted">Fertilizer</th> + <th class="text-end">Qty (kg)</th> + <th class="text-end">Per portion (kg)</th> + </tr> + </thead> + <tbody> + <% @recipe.each do |component, kg| %> + <% prod_name = commercial_name_for(component) %> + <tr data-total-kg="<%= kg %>"> + <td><strong><%= prod_name %></strong></td> + <td class="text-muted"><small><%= component.name %></small></td> + <td class="text-end fw-semibold"><%= fmt_kg(kg) %></td> + <td class="text-end per-portion-cell"><%= fmt_kg(kg / (params[:portions].presence || 2).to_f) %></td> + </tr> + <% end %> + <% if @recipe.blank? %> + <tr><td colspan="4" class="text-center text-muted">No supplementation required.</td></tr> + <% end %> + </tbody> + </table> + </div> + + <div class="card-footer small text-muted"> + Recipe based on latest measurement: <%= @latest.measured_on %> + </div> +</div> + +<%# Tiny vanilla JS: auto-submit on volume change; live per-portion update %> +<script> + (function() { + const form = document.getElementById('recipe-form'); + const volumeSelect = document.getElementById('volume_select'); + const portionsInput = document.getElementById('portions_input'); + const rows = document.querySelectorAll('#recipe-table tbody tr[data-total-kg]'); + + function clampPortions(n) { + n = parseInt(n || 2, 10); + if (isNaN(n)) n = 2; + return Math.min(10, Math.max(2, n)); + } + + function updatePerPortion() { + const n = clampPortions(portionsInput.value); + portionsInput.value = n; + rows.forEach(row => { + const total = parseFloat(row.getAttribute('data-total-kg') || '0'); + const cell = row.querySelector('.per-portion-cell'); + const per = (n > 0) ? (total / n) : 0; + cell.textContent = formatKg(per); + }); + } + + function formatKg(v) { + // match Rails number_with_precision(... precision: 2, strip zeros-ish) + const s = (Math.round(v * 100) / 100).toFixed(2); + return s.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1'); + } + + volumeSelect.addEventListener('change', () => { + // submit with selected volume, keeping portions as a query param + const portions = clampPortions(portionsInput.value); + const url = new URL(form.action, window.location.origin); + url.searchParams.set('volume', volumeSelect.value); + url.searchParams.set('portions', portions); + window.location.assign(url.toString()); + }); + + portionsInput.addEventListener('input', updatePerPortion); + updatePerPortion(); + })(); +</script> |