Corriger le bandeau de compteurs, et régler impression et productivité

Le bandeau de la grille affichait contrat, planifié, écart, dimanches et jours
de repos. PLAN.md §7.4 en demande cinq autres : heures contractuelles,
planifié, absences, écart, repos compensateur. La capture du produit audité le
confirme.

L'écart n'était pas cosmétique. Les dimanches travaillés et les jours de repos
se lisent déjà sur la grille, colonne par colonne. Le repos compensateur, lui,
ne se lit nulle part ailleurs : c'est une contrepartie due — au dimanche
travaillé, au dépassement de contingent — et l'omettre du bandeau masquait la
seule valeur qui dit qu'une dette existe.

Le compteur RC n'était alimenté par rien : les repos posés n'étaient tout
simplement pas chargés avec la semaine. Ils le sont, et seul le repos
compensateur y entre — le repos hebdomadaire est un droit déjà pris, les
additionner ferait disparaître la dette dans un total qui ne veut rien dire.

Deux écrans de réglages complètent le lot. L'impression gouverne le planning
affiché en salle : orientation, densité, totaux, dimanche, colonne
d'émargement. Cette dernière porte un avertissement — un émargement papier ne
vaut pas décompte du temps de travail, et le décompte reste celui de PlanFlow.

L'objectif de productivité est nullable et non « zéro par défaut » : zéro est un
objectif inatteignable, l'absence de valeur dit qu'aucun objectif n'a été fixé.
Vider le champ efface donc l'objectif au lieu de le mettre à zéro. L'écran
rappelle aussi ce que l'indicateur n'est pas : il éclaire une décision
d'organisation, il ne mesure pas le travail d'une personne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-11 13:18:31 +02:00
1 parent d8060eb4a9
commit 5f656d05af
17 files changed
+541 -13

No files matched your search

@@ -0,0 +1,102 @@
'use client';
import { useActionState } from 'react';
import { FormError, SubmitButton } from '@/components/ui/Form';
import { PersistentForm } from '@/components/ui/PersistentForm';
import { PRINT_DENSITIES, togglesOf } from '@/domain/settings/preferences';
import {
savePreferencesAction,
type ActionState,
type Preferences,
} from '@/server/settings/account';
const empty: ActionState = {};
export function PrintForm({ preferences }: { preferences: Preferences }) {
const [state, action] = useActionState(savePreferencesAction, empty);
const toggles = togglesOf('print');
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} />
))}
<fieldset className="flex flex-col gap-2">
<legend className="text-sm font-medium">Orientation</legend>
<p className="text-micro text-ink-3">
Une semaine se lit en largeur : le paysage évite de couper le
dimanche sur une seconde page.
</p>
{[
{ value: 'landscape', label: 'Paysage' },
{ value: 'portrait', label: 'Portrait' },
].map((option) => (
<label key={option.value} className="flex items-center gap-2 text-sm">
<input
type="radio"
name="printOrientation"
value={option.value}
defaultChecked={
(option.value === 'landscape') === preferences.printLandscape
}
/>
{option.label}
</label>
))}
</fieldset>
<label className="flex max-w-sm flex-col gap-1.5">
<span className="text-sm font-medium">Densité</span>
<select
name="printDensity"
defaultValue={preferences.printDensity}
className="h-9 rounded-2 border border-line-2 bg-surface px-2 text-sm text-ink-1"
>
{PRINT_DENSITIES.map((density) => (
<option key={density.key} value={density.key}>
{density.label} — {density.hint}
</option>
))}
</select>
</label>
{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>
))}
<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>
);
}
@@ -0,0 +1,37 @@
import Link from 'next/link';
import { PrintForm } from '@/app/(app)/reglages/impression/PrintForm';
import { PageBody, PageHeader } from '@/components/shell/PageHeader';
import { Card, CardHeader } from '@/components/ui/Card';
import { getPreferences } from '@/server/settings/account';
export const metadata = { title: 'Impression · PlanFlow' };
export const dynamic = 'force-dynamic';
export default async function ImpressionReglagesPage() {
const preferences = await getPreferences();
return (
<PageBody>
<PageHeader
title="Impression"
subtitle="Mise en page du planning affiché en salle"
/>
<Card>
<CardHeader title="Planning imprimé" />
<PrintForm preferences={preferences} />
</Card>
<p className="max-w-[70ch] text-xs leading-[var(--lh-prose)] text-ink-3">
Ces réglages gouvernent{' '}
<Link href="/planning/impression" className="underline">
le planning à afficher
</Link>
. Le rendu passe par la boîte d’impression du navigateur, qui sait aussi
enregistrer en PDF : un second moteur de rendu serait un second endroit
où la feuille peut diverger de l’écran.
</p>
</PageBody>
);
}
@@ -0,0 +1,42 @@
'use client';
import { useActionState } from 'react';
import { Field, FormError, SubmitButton } from '@/components/ui/Form';
import { PersistentForm } from '@/components/ui/PersistentForm';
import {
saveProductivityTargetAction,
type ActionState,
} from '@/server/settings/account';
const empty: ActionState = {};
export function ProductivityForm({ target }: { target: string | null }) {
const [state, action] = useActionState(saveProductivityTargetAction, empty);
return (
<PersistentForm action={action} className="flex flex-col gap-3 p-4">
<div className="flex max-w-sm items-end gap-2">
<Field
label="Objectif de productivité"
name="target"
type="number"
step="0.01"
min="0"
defaultValue={target ?? ''}
hint="Chiffre d’affaires attendu par heure travaillée, en euros. Laisser vide pour n’en fixer aucun."
/>
<span className="pb-2.5 text-sm text-ink-2">€/h</span>
</div>
<FormError>{state.error}</FormError>
<div className="flex items-center gap-3">
<SubmitButton>Enregistrer</SubmitButton>
{state.ok ? (
<span className="text-xs text-ok-soft-ink">Objectif enregistré.</span>
) : null}
</div>
</PersistentForm>
);
}
@@ -0,0 +1,48 @@
import { ProductivityForm } from '@/app/(app)/reglages/productivite/ProductivityForm';
import { PageBody, PageHeader } from '@/components/shell/PageHeader';
import { Badge } from '@/components/ui/Badge';
import { Card, CardHeader } from '@/components/ui/Card';
import { getPreferences } from '@/server/settings/account';
export const metadata = { title: 'Productivité · PlanFlow' };
export const dynamic = 'force-dynamic';
export default async function ProductivitePage() {
const preferences = await getPreferences();
const target = preferences.productivityTargetPerHour;
return (
<PageBody>
<PageHeader
title="Productivité"
subtitle={
target
? `${target} € de chiffre d’affaires par heure travaillée`
: 'Aucun objectif fixé'
}
/>
<Card>
<CardHeader
title="Objectif global"
badge={
target ? null : <Badge tone="neutral">non fixé</Badge>
}
/>
<ProductivityForm target={target} />
</Card>
<p className="max-w-[70ch] text-xs leading-[var(--lh-prose)] text-ink-3">
L’objectif vaut pour l’ensemble des établissements. Vider le champ
l’efface plutôt que de le ramener à zéro : zéro serait un objectif
inatteignable, l’absence de valeur dit qu’aucun objectif n’a été fixé.
</p>
<p className="max-w-[70ch] text-xs leading-[var(--lh-prose)] text-ink-3">
Un objectif de productivité rapporte un chiffre d’affaires à des heures
planifiées. Il éclaire une décision d’organisation ; il ne mesure pas le
travail d’une personne, et n’a pas à être opposé à un salarié.
</p>
</PageBody>
);
}