[Rails] Fertilizer recipe solver for the FAPG.
1
<!-- app/views/recipes/show.html.erb -->
2
<%# Controls header %>
3
<div class="card shadow my-4">
4
<div class="card-header d-flex flex-wrap gap-2 justify-content-between align-items-center">
5
<h5 class="mb-0">Ferti© Recipe</h5>
6
7
<div class="d-flex flex-wrap gap-2 align-items-center">
8
<%= form_with url: ferti_recipe_path, method: :get, local: true, class: "d-flex flex-wrap gap-2 align-items-center", id: "recipe-form" do %>
9
<div class="d-flex align-items-center gap-2">
10
<label class="mb-0 small text-nowrap" for="volume_select">Volume</label>
11
<select id="volume_select" name="volume" class="form-select form-select-sm">
12
<% options = [["100 000 L", 100_000], ["200 000 L", 200_000], ["300 000 L", 300_000]] %>
13
<% current_volume = (params[:volume].presence || 100_000).to_i %>
14
<% options.each do |label, val| %>
15
<option value="<%= val %>" <%= 'selected' if val == current_volume %>><%= label %></option>
16
<% end %>
17
</select>
18
</div>
19
20
<div class="vr" style="height: 24px;"></div>
21
22
<div class="d-flex align-items-center gap-2">
23
<label class="mb-0 small text-nowrap" for="portions_input">Portions</label>
24
<input id="portions_input" type="number" min="2" max="10" step="1"
25
class="form-control form-control-sm" value="<%= params[:portions].presence || 2 %>">
26
</div>
27
<% end %>
28
29
<%= link_to "Back", root_path, class: "btn btn-sm btn-secondary" %>
30
</div>
31
</div>
32
33
<div class="table-responsive">
34
<table class="table table-sm align-middle mb-0" id="recipe-table">
35
<thead class="table-light">
36
<tr>
37
<th>Product</th>
38
<th class="text-muted">Fertilizer</th>
39
<th class="text-end">Qty (kg)</th>
40
<th class="text-end">Per portion (kg)</th>
41
</tr>
42
</thead>
43
<tbody>
44
<% @recipe.each do |component, kg| %>
45
<% prod_name = commercial_name_for(component) %>
46
<tr data-total-kg="<%= kg %>">
47
<td><strong><%= prod_name %></strong></td>
48
<td class="text-muted"><small><%= component.name %></small></td>
49
<td class="text-end fw-semibold"><%= fmt_kg(kg) %></td>
50
<td class="text-end per-portion-cell"><%= fmt_kg(kg / (params[:portions].presence || 2).to_f) %></td>
51
</tr>
52
<% end %>
53
<% if @recipe.blank? %>
54
<tr><td colspan="4" class="text-center text-muted">No supplementation required.</td></tr>
55
<% end %>
56
</tbody>
57
</table>
58
</div>
59
60
<div class="card-footer small text-muted">
61
Recipe based on latest measurement: <%= @latest.measured_on %>
62
</div>
63
</div>
64
65
<%# Tiny vanilla JS: auto-submit on volume change; live per-portion update %>
66
<script>
67
(function() {
68
const form = document.getElementById('recipe-form');
69
const volumeSelect = document.getElementById('volume_select');
70
const portionsInput = document.getElementById('portions_input');
71
const rows = document.querySelectorAll('#recipe-table tbody tr[data-total-kg]');
72
73
function clampPortions(n) {
74
n = parseInt(n || 2, 10);
75
if (isNaN(n)) n = 2;
76
return Math.min(10, Math.max(2, n));
77
}
78
79
function updatePerPortion() {
80
const n = clampPortions(portionsInput.value);
81
portionsInput.value = n;
82
rows.forEach(row => {
83
const total = parseFloat(row.getAttribute('data-total-kg') || '0');
84
const cell = row.querySelector('.per-portion-cell');
85
const per = (n > 0) ? (total / n) : 0;
86
cell.textContent = formatKg(per);
87
});
88
}
89
90
function formatKg(v) {
91
// match Rails number_with_precision(... precision: 2, strip zeros-ish)
92
const s = (Math.round(v * 100) / 100).toFixed(2);
93
return s.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1');
94
}
95
96
volumeSelect.addEventListener('change', () => {
97
// submit with selected volume, keeping portions as a query param
98
const portions = clampPortions(portionsInput.value);
99
const url = new URL(form.action, window.location.origin);
100
url.searchParams.set('volume', volumeSelect.value);
101
url.searchParams.set('portions', portions);
102
window.location.assign(url.toString());
103
});
104
105
portionsInput.addEventListener('input', updatePerPortion);
106
updatePerPortion();
107
})();
108
</script>
109