Détailler les pauses d'un créneau, et prévenir le salarié
Un créneau portait un total de pause. Deux pauses de vingt minutes et une coupure de deux heures s'écrivaient toutes deux « 120 », et rien ne permettait de les distinguer — ni à l'écran, ni au contrôle. Or elles ne se planifient pas de la même façon : la première satisfait la pause minimale au-delà de six heures, la seconde interroge l'amplitude de la journée. Chaque pause a désormais sa ligne : durée, début facultatif, libellé, et si elle est rémunérée. Le début reste facultatif à dessein — beaucoup de pauses se prennent « quand c'est calme », et imposer une heure inventerait une précision que le planning n'a pas et qu'un contrôle prendrait pour un engagement. Le créneau garde deux totaux dérivés, et la distinction porte de l'argent. `breakMinutes` reste ce qu'il était, la part non rémunérée déduite du temps de travail : la paie, les compteurs et l'export continuent de le lire sans changer d'un caractère. `paidBreakMinutes` s'ajoute à côté, et la règle de pause minimale regarde la somme des deux — sans quoi un créneau dont la pause de vingt minutes est payée passerait pour un créneau sans pause, et l'employeur le plus généreux serait le seul à recevoir une alerte. La migration reprend les créneaux existants : leur total devient une pause unique, non rémunérée. Laisser la table vide aurait fait disparaître de l'écran des pauses qui comptent pourtant toujours dans les heures payées. En modification, les pauses sont remplacées et non fusionnées : le formulaire porte l'état voulu au complet, et rapprocher ligne à ligne ferait survivre une pause qu'on vient de retirer. L'avis d'affectation part enfin, sous sa propre nature d'envoi. Il est différé à la fin de la transaction : un refus sur le dernier jour d'une répétition annule les autres, et personne ne doit être prévenu d'un créneau qui n'existe pas. La case est décochée par défaut, à l'inverse du produit audité — c'est la publication de la semaine qui prévient, et cette case sert au créneau ajouté après coup, le cas où le salarié ne verrait rien sans elle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa7336d749
commit
fab497acdc
17 files changed
+802
-21
No files matched your search
@@ -444,18 +444,10 @@ function ShiftComposer({
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Pause (min)" htmlFor="composer-break">
|
||||
<input
|
||||
id="composer-break"
|
||||
name="breakMinutes"
|
||||
type="number"
|
||||
min={0}
|
||||
max={600}
|
||||
step={5}
|
||||
defaultValue={editing ? target.shift.breakMinutes : 0}
|
||||
className="h-8 w-20 rounded-2 border border-line-2 bg-surface px-2 text-sm text-ink-1"
|
||||
/>
|
||||
</Field>
|
||||
<BreakRows
|
||||
initial={editing ? target.shift.breaks : []}
|
||||
fallbackMinutes={editing ? target.shift.breakMinutes : 0}
|
||||
/>
|
||||
|
||||
<Field label="Repas" htmlFor="composer-meals">
|
||||
<input
|
||||
@@ -501,7 +493,18 @@ function ShiftComposer({
|
||||
</Field>
|
||||
|
||||
{editing ? null : (
|
||||
<RepeatDays dates={dates} days={days} selected={localDate} />
|
||||
<>
|
||||
<RepeatDays dates={dates} days={days} selected={localDate} />
|
||||
|
||||
{/* Décoché par défaut, à l'inverse du produit audité. Un avis par
|
||||
créneau posé remplit vite une boîte : c'est la publication de la
|
||||
semaine qui prévient, et cette case sert au créneau ajouté après
|
||||
coup — le cas où le salarié ne verrait rien sans elle. */}
|
||||
<label className="flex items-center gap-2 text-sm text-ink-2">
|
||||
<input type="checkbox" name="notify" />
|
||||
Prévenir le salarié par courriel
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -522,6 +525,139 @@ function ShiftComposer({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses d'un créneau, une ligne par pause.
|
||||
*
|
||||
* Un total unique ne disait pas ce qui composait la journée : deux pauses de
|
||||
* vingt minutes et une coupure de deux heures s'écrivaient « 120 », et rien ne
|
||||
* permettait de les distinguer — ni à l'écran, ni au contrôle.
|
||||
*
|
||||
* Le début est facultatif. Beaucoup de pauses se prennent « quand c'est
|
||||
* calme » : imposer une heure obligerait à inventer une précision que le
|
||||
* planning n'a pas, et qu'un contrôle prendrait pour un engagement.
|
||||
*/
|
||||
function BreakRows({
|
||||
initial,
|
||||
fallbackMinutes,
|
||||
}: {
|
||||
initial: ReadonlyArray<{
|
||||
startMinutes: number | null;
|
||||
durationMinutes: number;
|
||||
isPaid: boolean;
|
||||
label: string | null;
|
||||
}>;
|
||||
fallbackMinutes: number;
|
||||
}) {
|
||||
// Un créneau enregistré avant le détail des pauses n'a qu'un total : il est
|
||||
// repris comme une pause unique plutôt que perdu.
|
||||
const seeded =
|
||||
initial.length > 0
|
||||
? initial
|
||||
: fallbackMinutes > 0
|
||||
? [
|
||||
{
|
||||
startMinutes: null,
|
||||
durationMinutes: fallbackMinutes,
|
||||
isPaid: false,
|
||||
label: null,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const [rows, setRows] = useState(seeded);
|
||||
|
||||
return (
|
||||
<fieldset className="flex w-full flex-col gap-1.5">
|
||||
<legend className="text-micro font-medium tracking-[0.04em] text-ink-3 uppercase">
|
||||
Pauses
|
||||
</legend>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<p className="text-micro text-ink-3">Aucune pause.</p>
|
||||
) : null}
|
||||
|
||||
{rows.map((row, index) => (
|
||||
<div key={index} className="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
name="breakDuration"
|
||||
type="number"
|
||||
min={1}
|
||||
max={600}
|
||||
step={5}
|
||||
required
|
||||
defaultValue={row.durationMinutes}
|
||||
aria-label={`Durée de la pause ${index + 1}, en minutes`}
|
||||
className="h-8 w-20 rounded-2 border border-line-2 bg-surface px-2 text-sm text-ink-1"
|
||||
/>
|
||||
<span className="text-micro text-ink-3">min</span>
|
||||
|
||||
<input
|
||||
name="breakStart"
|
||||
type="number"
|
||||
min={0}
|
||||
step={5}
|
||||
defaultValue={row.startMinutes ?? ''}
|
||||
placeholder="début"
|
||||
aria-label={`Début de la pause ${index + 1}, en minutes après la prise de poste`}
|
||||
className="h-8 w-24 rounded-2 border border-line-2 bg-surface px-2 text-sm text-ink-1"
|
||||
/>
|
||||
|
||||
<input
|
||||
name="breakLabel"
|
||||
type="text"
|
||||
maxLength={60}
|
||||
defaultValue={row.label ?? ''}
|
||||
placeholder="libellé"
|
||||
aria-label={`Libellé de la pause ${index + 1}`}
|
||||
className="h-8 w-28 rounded-2 border border-line-2 bg-surface px-2 text-sm text-ink-1"
|
||||
/>
|
||||
|
||||
<label className="flex items-center gap-1 text-micro text-ink-2">
|
||||
{/* La valeur porte l'index : une case non cochée n'est pas envoyée,
|
||||
et « on » seul ne dirait pas quelle ligne est payée. */}
|
||||
<input
|
||||
type="checkbox"
|
||||
name="breakPaid"
|
||||
value={String(index)}
|
||||
defaultChecked={row.isPaid}
|
||||
/>
|
||||
rémunérée
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRows(rows.filter((_, at) => at !== index))}
|
||||
className="rounded-2 px-1.5 py-0.5 text-micro text-ink-3 hover:text-danger"
|
||||
>
|
||||
Retirer
|
||||
<span className="sr-only">{` la pause ${index + 1}`}</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setRows([
|
||||
...rows,
|
||||
{
|
||||
startMinutes: null,
|
||||
durationMinutes: 20,
|
||||
isPaid: false,
|
||||
label: null,
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
+ Ajouter une pause
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Répétition du créneau sur plusieurs jours.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user