Quatre écrans manquaient faute de spécification. Les captures du produit audité les fournissent : identité de l'entreprise, comportement des plannings, droits ouverts aux salariés, décompte de paie. Les préférences sont trois blocs typés, pas un sac de clés-valeurs. Ce n'est pas de la rigueur gratuite : `smoothOvertimeMonthly` change le calcul des heures supplémentaires, et un JSON libre rendrait indétectable une clé mal orthographiée — le réglage paraîtrait actif sans l'être. Chaque formulaire déclare les clés qu'il gouverne. Une case décochée n'arrive pas dans le FormData ; sans cette déclaration, enregistrer « Plannings » remettrait à faux tout « Droits », un formulaire partiel effaçant ce qu'il n'affiche pas. Trois réglages portent un avertissement en évidence parce qu'ils déplacent des heures et non des pixels. Le lissage mensuel des heures supplémentaires suppose un accord d'aménagement du temps de travail : le décompte hebdomadaire est le principe, et sans accord l'aménagement est inopposable. Les pauses rémunérées et l'inclusion des repos dans les heures normales changent la répartition entre heures normales et majorées. Un interrupteur qui coûte de l'argent ne doit pas se présenter comme un réglage d'affichage. La ligne de préférences est créée à la demande : son absence vaut « tous les défauts ». Semer une ligne à l'installation obligerait à migrer chaque compte au premier réglage ajouté. L'adresse va sur le compte et non sur l'établissement : le siège porte l'identité juridique et figure au registre du personnel, là où l'établissement porte le SIRET, le fuseau effectif et les plannings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
'use client';
|
|
|
|
import { useActionState } from 'react';
|
|
|
|
import { FormError, SubmitButton } from '@/components/ui/Form';
|
|
import { PersistentForm } from '@/components/ui/PersistentForm';
|
|
import {
|
|
minutesToTime,
|
|
togglesOf,
|
|
type PreferenceGroup,
|
|
} from '@/domain/settings/preferences';
|
|
import {
|
|
savePreferencesAction,
|
|
type ActionState,
|
|
type Preferences,
|
|
} from '@/server/settings/account';
|
|
|
|
const empty: ActionState = {};
|
|
|
|
/**
|
|
* Un bloc de préférences.
|
|
*
|
|
* Chaque formulaire déclare les clés qu'il gouverne dans des champs `scope`.
|
|
* Sans eux, enregistrer « Plannings » remettrait à faux tout « Droits » : une
|
|
* case décochée n'arrive pas dans le `FormData`, et le serveur ne saurait pas
|
|
* distinguer « décoché » de « pas dans ce formulaire ».
|
|
*/
|
|
export function PreferenceBlock({
|
|
group,
|
|
preferences,
|
|
withEveningTime = false,
|
|
}: {
|
|
group: PreferenceGroup;
|
|
preferences: Preferences;
|
|
withEveningTime?: boolean;
|
|
}) {
|
|
const [state, action] = useActionState(savePreferencesAction, empty);
|
|
const toggles = togglesOf(group);
|
|
|
|
return (
|
|
<PersistentForm action={action} className="flex flex-col gap-4 px-4 py-4">
|
|
{toggles.map((toggle) => (
|
|
<input key={`scope-${toggle.key}`} type="hidden" name="scope" value={toggle.key} />
|
|
))}
|
|
|
|
{toggles.map((toggle) => (
|
|
<div key={toggle.key} className="flex flex-col gap-1">
|
|
<label className="flex items-start gap-2.5 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
name={toggle.key}
|
|
defaultChecked={Boolean(
|
|
preferences[toggle.key as keyof Preferences],
|
|
)}
|
|
className="mt-0.5"
|
|
/>
|
|
<span>
|
|
<span className="font-medium">{toggle.label}</span>
|
|
<span className="block text-micro leading-[var(--lh-prose)] text-ink-3">
|
|
{toggle.hint}
|
|
</span>
|
|
</span>
|
|
</label>
|
|
|
|
{toggle.warning ? (
|
|
<p className="ml-6 rounded-2 border border-warn bg-warn-soft px-2.5 py-1.5 text-micro leading-[var(--lh-prose)] text-warn-soft-ink">
|
|
{toggle.warning}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
|
|
{withEveningTime ? (
|
|
<label className="flex max-w-xs flex-col gap-1.5">
|
|
<span className="text-sm font-medium">
|
|
Un créneau est « du soir » à partir de
|
|
</span>
|
|
<input
|
|
type="time"
|
|
name="eveningShiftStart"
|
|
defaultValue={minutesToTime(preferences.eveningShiftStartMinutes)}
|
|
className="h-9 rounded-2 border border-line-2 bg-surface px-3 text-sm text-ink-1"
|
|
/>
|
|
<span className="text-micro text-ink-3">
|
|
Sert au repérage à l’écran et aux majorations de soirée, pas au
|
|
travail de nuit, qui relève de la convention.
|
|
</span>
|
|
</label>
|
|
) : null}
|
|
|
|
<FormError>{state.error}</FormError>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<SubmitButton>Enregistrer</SubmitButton>
|
|
{state.ok ? (
|
|
<span className="text-xs text-ok-soft-ink">Réglages enregistrés.</span>
|
|
) : null}
|
|
</div>
|
|
</PersistentForm>
|
|
);
|
|
}
|