1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
|
<%# Props: nutrient_profiles: ActiveRecord::Relation<NutrientProfile> %>
<%# Fallback if controller didn't set @nutrient_profiles yet %>
<% profiles = (local_assigns[:nutrient_profiles] || []).presence || [] %>
<%# We'll render a form purely for structure (no real submit yet) %>
<%= form_with url: "#", method: :post, local: true, html: { id: "np-mix-form", "data-controller": "np-mix" } do %>
<div class="card shadow">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="small text-muted">
Choisissez des <strong>profils de croissance</strong> et répartissez-les pour totaliser <strong>100%</strong>.
</div>
<div>
Somme : <span id="np-mix-sum" class="badge bg-secondary">0%</span>
</div>
</div>
<div id="np-mix-rows" class="vstack gap-2">
<%# Rows are injected by JS from the template below, including defaults %>
</div>
<div class="mt-3 d-flex gap-2">
<button type="button" class="btn btn-outline-primary" id="np-mix-add">
+ Ajouter un profil
</button>
<%# Placeholder "save" button for later backend wiring; disabled until total == 100 %>
<button type="submit" class="btn btn-success ms-auto" id="np-mix-save" disabled>
Enregistrer (à venir)
</button>
</div>
</div>
</div>
<%# --- Hidden template for a single row --- %>
<template id="np-mix-row-template">
<div class="np-mix-row d-flex align-items-center gap-2 border rounded p-2">
<button type="button" class="btn btn-outline-danger btn-sm np-mix-delete" aria-label="Supprimer la ligne">
Suppr.
</button>
<div class="flex-grow-1">
<select name="mix[items][][profile_id]" class="form-select form-select-sm np-mix-select" required>
<% if profiles.any? %>
<% profiles.each do |p| %>
<option value="<%= p.id %>"><%= p.name %></option>
<% end %>
<% else %>
<%# If no collection provided yet, at least show placeholders to demo the UI %>
<option value="">-- Sélectionner un profil --</option>
<option value="gen-croissance">Générique croissance</option>
<option value="tomate-cycle">Tomate (cycle entier)</option>
<option value="gen-floraison">Générique floraison</option>
<% end %>
</select>
</div>
<div class="input-group input-group-sm" style="max-width: 140px;">
<input type="number"
name="mix[items][][percentage]"
class="form-control text-end np-mix-percent"
min="0" max="100" step="1" value="0" required>
<span class="input-group-text">%</span>
</div>
</div>
</template>
<%# --- Defaults to inject on load --- %>
<script type="application/json" id="np-mix-defaults">
{
"items": [
{ "name": "G\u00E9n\u00E9rique croissance", "percent": 50 },
{ "name": "Tomate (cycle entier)", "percent": 30 },
{ "name": "G\u00E9n\u00E9rique floraison", "percent": 20 }
]
}
</script>
<%# --- Tiny inline JS to keep this self-contained (no Stimulus required) --- %>
<script>
(() => {
const rowsContainer = document.getElementById('np-mix-rows');
const addBtn = document.getElementById('np-mix-add');
const saveBtn = document.getElementById('np-mix-save');
const sumBadge = document.getElementById('np-mix-sum');
const tpl = document.getElementById('np-mix-row-template');
const defaultsJSON = document.getElementById('np-mix-defaults')?.textContent || "{}";
const defaults = JSON.parse(defaultsJSON);
function currentSum() {
return Array.from(rowsContainer.querySelectorAll('.np-mix-percent'))
.reduce((acc, el) => acc + (parseFloat(el.value) || 0), 0);
}
function refreshSum() {
const sum = currentSum();
sumBadge.textContent = `${sum}%`;
sumBadge.classList.remove('bg-secondary','bg-danger','bg-success','bg-warning');
if (sum === 100) {
sumBadge.classList.add('bg-success');
saveBtn?.removeAttribute('disabled');
} else if (sum > 100) {
sumBadge.classList.add('bg-danger');
saveBtn?.setAttribute('disabled', 'disabled');
} else {
sumBadge.classList.add('bg-warning');
saveBtn?.setAttribute('disabled', 'disabled');
}
}
function setSelectByName(selectEl, targetName) {
// Try to match by visible name; fall back to first option.
const options = Array.from(selectEl.options);
const found = options.find(o => o.text.trim().toLowerCase() === String(targetName || '').trim().toLowerCase());
if (found) {
selectEl.value = found.value;
}
}
function installRow({ name = null, percent = 0 } = {}) {
const node = tpl.content.firstElementChild.cloneNode(true);
// Hook up events
node.querySelector('.np-mix-delete').addEventListener('click', () => {
node.remove();
refreshSum();
});
const selectEl = node.querySelector('.np-mix-select');
const percentEl = node.querySelector('.np-mix-percent');
// Default selection (by name) and percent
if (name) setSelectByName(selectEl, name);
percentEl.value = percent;
// Input events
selectEl.addEventListener('change', () => { /* reserved for later linkage */ });
percentEl.addEventListener('input', () => {
// Clamp and refresh
let v = parseFloat(percentEl.value);
if (isNaN(v)) v = 0;
v = Math.max(0, Math.min(100, Math.round(v)));
percentEl.value = v;
refreshSum();
});
rowsContainer.appendChild(node);
}
// Init with three defaults
const items = (defaults && defaults.items) ? defaults.items : [];
if (items.length) {
items.forEach(it => installRow({ name: it.name, percent: it.percent }));
} else {
// Fallback: create three blank rows
for (let i = 0; i < 3; i++) installRow();
}
refreshSum();
// Add new blank row
addBtn.addEventListener('click', () => {
installRow({ name: null, percent: 0 });
refreshSum();
// Scroll to the new row on mobile for better UX
rowsContainer.lastElementChild?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
// Prevent real submit for now (frontend only)
document.getElementById('np-mix-form')?.addEventListener('submit', (e) => {
e.preventDefault();
// Later: wire to Turbo/JSON post. For now just a gentle nudge.
saveBtn.textContent = 'Enregistrer (backend à venir)';
saveBtn.blur();
});
})();
</script>
<style>
/* Small touch targets & tidy spacing on mobile */
@media (max-width: 576px) {
.np-mix-row { padding: .5rem; }
.np-mix-row .btn { padding: .25rem .5rem; }
}
</style>
<% end %>
|