feat(minuteur): la case « Répartir » reste cochée une fois la répartition entaméeUnverified
Quand une mesure a déjà été partiellement répartie, revenir sur l'étape 2 pré-coche « Répartir le temps restant » dès que la durée saisie laisse un reste : commencer à répartir engage par défaut à aller jusqu'au bout. Le serveur pose data-want (1 si timers.split n'est pas vide) sur le bloc ; le JS l'applique quand la case redevient active et y mémorise le dernier choix explicite de l'utilisateur, au lieu de repartir décoché à chaque variation de durée. Sans JS, rien ne change (bloc masqué).
5745456 parent: ac0c9d7 modified
src/routes/entries.rs +6 -0 | @@ -110,6 +110,10 @@ pub struct AddStep2Page { | ||
| 110 | 110 | /// durée en est préremplie et le toggle « Répartir » existe. None pour |
| 111 | 111 | /// une saisie manuelle. |
| 112 | 112 | pub remaining: Option<i64>, |
| 113 | + /// Une répartition est entamée (au moins une saisie créditée sur la | |
| 114 | + /// mesure) : la case « Répartir » est alors cochée d'office — qui a | |
| 115 | + /// commencé à répartir va par défaut jusqu'au bout. | |
| 116 | + pub split_started: bool, | |
| 113 | 117 | } |
| 114 | 118 | |
| 115 | 119 | #[derive(Deserialize)] |
| @@ -212,6 +216,7 @@ pub async fn entry_new( | ||
| 212 | 216 | .and_then(|id| all_tasks.iter().find(|t| t.id == id && !t.archived)) |
| 213 | 217 | { |
| 214 | 218 | let remaining = pending.as_ref().and_then(|t| t.remaining_minutes()); |
| 219 | + let split_started = pending.as_ref().is_some_and(|t| t.split_started()); | |
| 215 | 220 | let minutes = q |
| 216 | 221 | .minutes |
| 217 | 222 | .or(remaining.map(|m| m.min(i32::MAX as i64) as i32)) |
| @@ -244,6 +249,7 @@ pub async fn entry_new( | ||
| 244 | 249 | day_next: (date < paris_today()) |
| 245 | 250 | .then(|| step2_href(picked.id, minutes, date + Duration::days(1))), |
| 246 | 251 | remaining, |
| 252 | + split_started, | |
| 247 | 253 | }) |
| 248 | 254 | .into_response()); |
| 249 | 255 | } |
| @@ -110,6 +110,10 @@ pub struct AddStep2Page { | |||
| 110 | /// durée en est préremplie et le toggle « Répartir » existe. None pour | 110 | /// durée en est préremplie et le toggle « Répartir » existe. None pour |
| 111 | /// une saisie manuelle. | 111 | /// une saisie manuelle. |
| 112 | pub remaining: Option<i64>, | 112 | pub remaining: Option<i64>, |
| 113 | + /// Une répartition est entamée (au moins une saisie créditée sur la | ||
| 114 | + /// mesure) : la case « Répartir » est alors cochée d'office — qui a | ||
| 115 | + /// commencé à répartir va par défaut jusqu'au bout. | ||
| 116 | + pub split_started: bool, | ||
| 113 | } | 117 | } |
| 114 | 118 | ||
| 115 | #[derive(Deserialize)] | 119 | #[derive(Deserialize)] |
| @@ -212,6 +216,7 @@ pub async fn entry_new( | |||
| 212 | .and_then(|id| all_tasks.iter().find(|t| t.id == id && !t.archived)) | 216 | .and_then(|id| all_tasks.iter().find(|t| t.id == id && !t.archived)) |
| 213 | { | 217 | { |
| 214 | let remaining = pending.as_ref().and_then(|t| t.remaining_minutes()); | 218 | let remaining = pending.as_ref().and_then(|t| t.remaining_minutes()); |
| 219 | + let split_started = pending.as_ref().is_some_and(|t| t.split_started()); | ||
| 215 | let minutes = q | 220 | let minutes = q |
| 216 | .minutes | 221 | .minutes |
| 217 | .or(remaining.map(|m| m.min(i32::MAX as i64) as i32)) | 222 | .or(remaining.map(|m| m.min(i32::MAX as i64) as i32)) |
| @@ -244,6 +249,7 @@ pub async fn entry_new( | |||
| 244 | day_next: (date < paris_today()) | 249 | day_next: (date < paris_today()) |
| 245 | .then(|| step2_href(picked.id, minutes, date + Duration::days(1))), | 250 | .then(|| step2_href(picked.id, minutes, date + Duration::days(1))), |
| 246 | remaining, | 251 | remaining, |
| 252 | + split_started, | ||
| 247 | }) | 253 | }) |
| 248 | .into_response()); | 254 | .into_response()); |
| 249 | } | 255 | } |
modified
templates/add_step2.html +6 -3 | @@ -35,11 +35,14 @@ | ||
| 35 | 35 | {#- Répartition d'une mesure (0018) — réservée au JS, écart assumé à la |
| 36 | 36 | règle zéro-JS : rendu hidden + case disabled, le JS révèle le bloc et |
| 37 | 37 | pilote la case d'après la durée saisie (app.html, form[data-duration]). |
| 38 | - Sans JS le bloc n'existe pas et la mesure est consommée comme avant. -#} | |
| 39 | - <div class="split-row" hidden data-split data-remaining="{{ r }}"> | |
| 38 | + Sans JS le bloc n'existe pas et la mesure est consommée comme avant. | |
| 39 | + data-want = choix par défaut de la case quand elle s'active : cochée | |
| 40 | + dès qu'une répartition est entamée (aller jusqu'au bout), puis le JS y | |
| 41 | + mémorise le dernier choix explicite de l'utilisateur. -#} | |
| 42 | + <div class="split-row" hidden data-split data-remaining="{{ r }}" data-want="{% if split_started %}1{% else %}0{% endif %}"> | |
| 40 | 43 | <label class="swt-check split-check"> |
| 41 | 44 | <span class="split-label" data-pat="{{ shell.tr.split_pat }}" data-idle="{{ shell.tr.split_idle }}">{{ shell.tr.split_idle }}</span> |
| 42 | - <input type="checkbox" name="repartir" value="1" disabled> | |
| 45 | + <input type="checkbox" name="repartir" value="1" disabled{% if split_started %} checked{% endif %}> | |
| 43 | 46 | <span class="swt"><span class="knob"></span></span> |
| 44 | 47 | </label> |
| 45 | 48 | </div> |
| @@ -35,11 +35,14 @@ | |||
| 35 | {#- Répartition d'une mesure (0018) — réservée au JS, écart assumé à la | 35 | {#- Répartition d'une mesure (0018) — réservée au JS, écart assumé à la |
| 36 | règle zéro-JS : rendu hidden + case disabled, le JS révèle le bloc et | 36 | règle zéro-JS : rendu hidden + case disabled, le JS révèle le bloc et |
| 37 | pilote la case d'après la durée saisie (app.html, form[data-duration]). | 37 | pilote la case d'après la durée saisie (app.html, form[data-duration]). |
| 38 | - Sans JS le bloc n'existe pas et la mesure est consommée comme avant. -#} | 38 | + Sans JS le bloc n'existe pas et la mesure est consommée comme avant. |
| 39 | - <div class="split-row" hidden data-split data-remaining="{{ r }}"> | 39 | + data-want = choix par défaut de la case quand elle s'active : cochée |
| 40 | + dès qu'une répartition est entamée (aller jusqu'au bout), puis le JS y | ||
| 41 | + mémorise le dernier choix explicite de l'utilisateur. -#} | ||
| 42 | + <div class="split-row" hidden data-split data-remaining="{{ r }}" data-want="{% if split_started %}1{% else %}0{% endif %}"> | ||
| 40 | <label class="swt-check split-check"> | 43 | <label class="swt-check split-check"> |
| 41 | <span class="split-label" data-pat="{{ shell.tr.split_pat }}" data-idle="{{ shell.tr.split_idle }}">{{ shell.tr.split_idle }}</span> | 44 | <span class="split-label" data-pat="{{ shell.tr.split_pat }}" data-idle="{{ shell.tr.split_idle }}">{{ shell.tr.split_idle }}</span> |
| 42 | - <input type="checkbox" name="repartir" value="1" disabled> | 45 | + <input type="checkbox" name="repartir" value="1" disabled{% if split_started %} checked{% endif %}> |
| 43 | <span class="swt"><span class="knob"></span></span> | 46 | <span class="swt"><span class="knob"></span></span> |
| 44 | </label> | 47 | </label> |
| 45 | </div> | 48 | </div> |
modified
templates/app.html +9 -1 | @@ -181,6 +181,10 @@ | ||
| 181 | 181 | // Répartition d'une mesure de minuteur (réservée au JS) : le toggle vit |
| 182 | 182 | // quand la durée saisie laisse un reste, se décoche sinon, et le bouton |
| 183 | 183 | // d'envoi annonce la suite. Les textes viennent des data-* du serveur. |
| 184 | + // Quand il redevient actif, il reprend data-want : le dernier choix | |
| 185 | + // explicite de l'utilisateur, sinon « coché » si une répartition est | |
| 186 | + // déjà entamée (le serveur le pose) — commencer à répartir engage à | |
| 187 | + // aller jusqu'au bout. | |
| 184 | 188 | function refreshSplit(root, v) { |
| 185 | 189 | var box = root.querySelector('[data-split]'); |
| 186 | 190 | if (!box) return; |
| @@ -188,6 +192,7 @@ | ||
| 188 | 192 | var label = box.querySelector('.split-label'); |
| 189 | 193 | var rest = (+box.dataset.remaining) - v; |
| 190 | 194 | var on = v > 0 && rest > 0; |
| 195 | + if (on && cb.disabled) cb.checked = box.dataset.want === '1'; | |
| 191 | 196 | cb.disabled = !on; |
| 192 | 197 | if (!on) cb.checked = false; |
| 193 | 198 | label.textContent = on ? label.dataset.pat.replace('{}', fmtMinutes(rest)) : label.dataset.idle; |
| @@ -268,7 +273,10 @@ | ||
| 268 | 273 | if (!root) return; |
| 269 | 274 | if (e.target.id === 'minutes') refreshMinutes(root); |
| 270 | 275 | else if (e.target.type === 'date') refreshDate(root, e.target); |
| 271 | - else if (e.target.closest('[data-split]')) refreshSubmit(root); | |
| 276 | + else if (e.target.closest('[data-split]')) { | |
| 277 | + if (ev === 'change') e.target.closest('[data-split]').dataset.want = e.target.checked ? '1' : '0'; | |
| 278 | + refreshSubmit(root); | |
| 279 | + } | |
| 272 | 280 | }); |
| 273 | 281 | }); |
| 274 | 282 | })(); |
| @@ -181,6 +181,10 @@ | |||
| 181 | // Répartition d'une mesure de minuteur (réservée au JS) : le toggle vit | 181 | // Répartition d'une mesure de minuteur (réservée au JS) : le toggle vit |
| 182 | // quand la durée saisie laisse un reste, se décoche sinon, et le bouton | 182 | // quand la durée saisie laisse un reste, se décoche sinon, et le bouton |
| 183 | // d'envoi annonce la suite. Les textes viennent des data-* du serveur. | 183 | // d'envoi annonce la suite. Les textes viennent des data-* du serveur. |
| 184 | + // Quand il redevient actif, il reprend data-want : le dernier choix | ||
| 185 | + // explicite de l'utilisateur, sinon « coché » si une répartition est | ||
| 186 | + // déjà entamée (le serveur le pose) — commencer à répartir engage à | ||
| 187 | + // aller jusqu'au bout. | ||
| 184 | function refreshSplit(root, v) { | 188 | function refreshSplit(root, v) { |
| 185 | var box = root.querySelector('[data-split]'); | 189 | var box = root.querySelector('[data-split]'); |
| 186 | if (!box) return; | 190 | if (!box) return; |
| @@ -188,6 +192,7 @@ | |||
| 188 | var label = box.querySelector('.split-label'); | 192 | var label = box.querySelector('.split-label'); |
| 189 | var rest = (+box.dataset.remaining) - v; | 193 | var rest = (+box.dataset.remaining) - v; |
| 190 | var on = v > 0 && rest > 0; | 194 | var on = v > 0 && rest > 0; |
| 195 | + if (on && cb.disabled) cb.checked = box.dataset.want === '1'; | ||
| 191 | cb.disabled = !on; | 196 | cb.disabled = !on; |
| 192 | if (!on) cb.checked = false; | 197 | if (!on) cb.checked = false; |
| 193 | label.textContent = on ? label.dataset.pat.replace('{}', fmtMinutes(rest)) : label.dataset.idle; | 198 | label.textContent = on ? label.dataset.pat.replace('{}', fmtMinutes(rest)) : label.dataset.idle; |
| @@ -268,7 +273,10 @@ | |||
| 268 | if (!root) return; | 273 | if (!root) return; |
| 269 | if (e.target.id === 'minutes') refreshMinutes(root); | 274 | if (e.target.id === 'minutes') refreshMinutes(root); |
| 270 | else if (e.target.type === 'date') refreshDate(root, e.target); | 275 | else if (e.target.type === 'date') refreshDate(root, e.target); |
| 271 | - else if (e.target.closest('[data-split]')) refreshSubmit(root); | 276 | + else if (e.target.closest('[data-split]')) { |
| 277 | + if (ev === 'change') e.target.closest('[data-split]').dataset.want = e.target.checked ? '1' : '0'; | ||
| 278 | + refreshSubmit(root); | ||
| 279 | + } | ||
| 272 | }); | 280 | }); |
| 273 | }); | 281 | }); |
| 274 | })(); | 282 | })(); |