WP-04 (suite) : vues par poste et par mois, duplication, édition, impression
Édition
- Le panneau de créneau sert aussi à la modification : déplacer un créneau,
c'est changer son jour, son salarié ou ses heures — exactement les champs de
la création. Deux écrans distincts finiraient par diverger.
- Duplication d'une semaine vers une autre, refusée si la destination contient
déjà des créneaux. Le report se fait **par heure locale**, pas par décalage de
sept jours : entre mars et avril, sept jours d'écart ne redonnent pas la même
heure.
Vues
- `/planning/etiquettes` : la semaine par poste. C'est la question du
responsable d'ouverture — « la caisse est-elle tenue samedi ? » — illisible
sur une grille dont les lignes sont des personnes. Les postes sans créneau
restent affichés : un poste vide toute la semaine est une information.
- `/planning/mois` : les heures par salarié et par jour, avec les journées de
plus de 10 h signalées. Vue de contrôle, pas d'édition.
- Les deux lisent `Shift` directement. Un test e2e pose un créneau et le
retrouve identique dans les quatre vues — c'est ce qui empêche l'une d'elles
de dériver vers son propre calcul.
Impression
- Feuille d'impression A4 paysage : les zones à défilement se déplient, les
lignes ne se coupent pas entre deux pages, et les aplats de poste sortent
(`print-color-adjust: exact`) sans être nécessaires à la lecture, puisque le
code du poste est imprimé dans chaque bloc.
- Pas de moteur PDF côté serveur : le navigateur sait paginer et enregistrer en
PDF. Un second moteur de rendu serait un second endroit où la grille peut
diverger de ce qui est à l'écran.
Accessibilité
- La grille porte enfin ses rôles ARIA (`grid`, `row`, `columnheader`,
`rowheader`, `gridcell`). Elle est faite de div pour la mise en page, mais se
lit comme un tableau : un lecteur d'écran doit pouvoir annoncer « ligne
Camille Ferrand, colonne mercredi ».
Cette structure a d'ailleurs révélé un test faux : `locator('div')` remontait au
conteneur de la grille, et le créneau du test atterrissait sur le premier
salarié de l'équipe au lieu de celui visé — une erreur invisible à l'écran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
This commit is contained in:
15 files changed
+1377
-56
No files matched your search
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Repérage des mois de planning.
|
||||
*
|
||||
* Le mois est la maille de la paie ; la semaine est celle du travail. Les deux
|
||||
* ne s'alignent jamais, et c'est précisément pourquoi la vue mois existe :
|
||||
* elle montre ce que la vue semaine ne peut pas montrer, à savoir un mois qui
|
||||
* commence un jeudi.
|
||||
*/
|
||||
|
||||
export interface Month {
|
||||
year: number;
|
||||
/** 1 = janvier. */
|
||||
month: number;
|
||||
}
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'janvier',
|
||||
'février',
|
||||
'mars',
|
||||
'avril',
|
||||
'mai',
|
||||
'juin',
|
||||
'juillet',
|
||||
'août',
|
||||
'septembre',
|
||||
'octobre',
|
||||
'novembre',
|
||||
'décembre',
|
||||
];
|
||||
|
||||
export function monthOf(date: Date): Month {
|
||||
return { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 };
|
||||
}
|
||||
|
||||
export function daysInMonth({ year, month }: Month): number {
|
||||
// Le jour 0 du mois suivant est le dernier du mois courant : la règle
|
||||
// bissextile est celle du calendrier, pas une table à maintenir.
|
||||
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||
}
|
||||
|
||||
/** Les dates civiles du mois, en `YYYY-MM-DD`. */
|
||||
export function monthDates(month: Month): string[] {
|
||||
return Array.from({ length: daysInMonth(month) }, (_, index) => {
|
||||
const day = String(index + 1).padStart(2, '0');
|
||||
return `${month.year}-${String(month.month).padStart(2, '0')}-${day}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function previousMonth({ year, month }: Month): Month {
|
||||
return month === 1 ? { year: year - 1, month: 12 } : { year, month: month - 1 };
|
||||
}
|
||||
|
||||
export function nextMonth({ year, month }: Month): Month {
|
||||
return month === 12 ? { year: year + 1, month: 1 } : { year, month: month + 1 };
|
||||
}
|
||||
|
||||
/** Numéro du jour de la semaine, 1 = lundi … 7 = dimanche. */
|
||||
export function isoDayOfWeek(isoDate: string): number {
|
||||
const day = new Date(`${isoDate}T00:00:00Z`).getUTCDay();
|
||||
return day === 0 ? 7 : day;
|
||||
}
|
||||
|
||||
export function monthLabel({ year, month }: Month): string {
|
||||
return `${MONTH_NAMES[month - 1] ?? ''} ${year}`;
|
||||
}
|
||||
|
||||
export function formatMonthParam({ year, month }: Month): string {
|
||||
return `${year}-${String(month).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** Analyse « 2026-08 » ; renvoie `null` si la forme ou les bornes sont fausses. */
|
||||
export function parseMonthParam(value: string | undefined): Month | null {
|
||||
if (!value) return null;
|
||||
const match = /^(\d{4})-(\d{2})$/.exec(value);
|
||||
if (!match) return null;
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
if (month < 1 || month > 12) return null;
|
||||
return { year, month };
|
||||
}
|
||||
Reference in new issue
Block a user