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
@@ -0,0 +1,61 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Pauses détaillées d'un créneau — PLAN.md §4.4.
|
||||
--
|
||||
-- `Shift.breakMinutes` garde son sens : la somme des pauses **non rémunérées**,
|
||||
-- déduite du temps de travail. Tous les calculs de paie et de compteurs
|
||||
-- continuent de la lire sans changer d'un caractère.
|
||||
--
|
||||
-- `paidBreakMinutes` s'ajoute à côté. Une pause payée n'est pas déduite du
|
||||
-- temps travaillé mais reste une pause : sans cette colonne, la règle de pause
|
||||
-- minimale au-delà de six heures verrait zéro et alerterait à tort sur un
|
||||
-- créneau qui respecte la convention.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ALTER TABLE "Shift" ADD COLUMN "paidBreakMinutes" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE "ShiftBreak" (
|
||||
"id" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"shiftId" TEXT NOT NULL,
|
||||
"startMinutes" INTEGER,
|
||||
"durationMinutes" INTEGER NOT NULL,
|
||||
"isPaid" BOOLEAN NOT NULL DEFAULT false,
|
||||
"label" TEXT,
|
||||
"position" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "ShiftBreak_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "ShiftBreak_accountId_idx" ON "ShiftBreak"("accountId");
|
||||
CREATE INDEX "ShiftBreak_shiftId_idx" ON "ShiftBreak"("shiftId");
|
||||
|
||||
ALTER TABLE "ShiftBreak" ADD CONSTRAINT "ShiftBreak_shiftId_fkey"
|
||||
FOREIGN KEY ("shiftId") REFERENCES "Shift"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Reprise des créneaux existants : leur total devient une pause unique, non
|
||||
-- rémunérée et non située. Laisser la table vide ferait disparaître de l'écran
|
||||
-- des pauses qui comptent pourtant toujours dans les heures payées.
|
||||
INSERT INTO "ShiftBreak" ("id", "accountId", "shiftId", "durationMinutes", "isPaid", "position")
|
||||
SELECT
|
||||
gen_random_uuid()::text,
|
||||
"accountId",
|
||||
"id",
|
||||
"breakMinutes",
|
||||
false,
|
||||
0
|
||||
FROM "Shift"
|
||||
WHERE "breakMinutes" > 0;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Isolation par compte — une pause dit à quelle heure quelqu'un s'absente de
|
||||
-- son poste. Même défense en profondeur que le créneau qui la porte.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ALTER TABLE "ShiftBreak" ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "ShiftBreak" FORCE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY tenant_isolation ON "ShiftBreak"
|
||||
USING ("accountId" = planflow_current_account());
|
||||
|
||||
CREATE POLICY tenant_insert ON "ShiftBreak"
|
||||
FOR INSERT WITH CHECK ("accountId" = planflow_current_account());
|
||||
@@ -0,0 +1,14 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Nature d'envoi distincte pour l'affectation d'un créneau.
|
||||
--
|
||||
-- Réutiliser PLANNING_PUBLISHED aurait rendu le journal d'envois illisible : le
|
||||
-- jour où un salarié affirme n'avoir pas été prévenu, il faut pouvoir
|
||||
-- distinguer « la semaine a été publiée » de « un créneau a été ajouté après
|
||||
-- coup ».
|
||||
--
|
||||
-- Dans sa propre migration : PostgreSQL refuse qu'une valeur d'énumération soit
|
||||
-- ajoutée puis employée dans la même transaction. Les séparer évite d'avoir à
|
||||
-- s'en souvenir le jour où la migration suivante voudra s'en servir.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ALTER TYPE "EmailKind" ADD VALUE IF NOT EXISTS 'SHIFT_ASSIGNED';
|
||||
@@ -819,7 +819,13 @@ model Shift {
|
||||
startAt DateTime @db.Timestamptz
|
||||
endAt DateTime @db.Timestamptz
|
||||
|
||||
/// Somme des pauses **non rémunérées**, déduite du temps de travail. C'est
|
||||
/// le total qui compte en paie ; le détail vit dans `ShiftBreak`.
|
||||
breakMinutes Int @default(0)
|
||||
/// Somme des pauses **rémunérées**. Non déduite du temps travaillé, mais
|
||||
/// comptée comme repos : la règle de pause minimale regarde les deux, sans
|
||||
/// quoi une pause payée passerait pour une absence de pause.
|
||||
paidBreakMinutes Int @default(0)
|
||||
actualStartAt DateTime? @db.Timestamptz
|
||||
actualEndAt DateTime? @db.Timestamptz
|
||||
actualBreakMinutes Int?
|
||||
@@ -833,12 +839,41 @@ model Shift {
|
||||
version Int @default(0)
|
||||
|
||||
schedule WeeklySchedule @relation(fields: [weeklyScheduleId], references: [id], onDelete: Cascade)
|
||||
breaks ShiftBreak[]
|
||||
|
||||
@@index([accountId])
|
||||
@@index([weeklyScheduleId, localDate])
|
||||
@@index([membershipId, localDate])
|
||||
}
|
||||
|
||||
/// Pause d'un créneau.
|
||||
///
|
||||
/// Plusieurs pauses par créneau, et pas un total unique : une coupure de deux
|
||||
/// heures en milieu de journée et deux pauses de vingt minutes ne se planifient
|
||||
/// pas de la même façon et ne se contrôlent pas pareil — la seconde satisfait
|
||||
/// la pause minimale de six heures, la première interroge l'amplitude.
|
||||
///
|
||||
/// `startMinutes` 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.
|
||||
model ShiftBreak {
|
||||
id String @id @default(cuid())
|
||||
accountId String
|
||||
shiftId String
|
||||
/// Minutes depuis le début du créneau. Null = pause non située.
|
||||
startMinutes Int?
|
||||
durationMinutes Int
|
||||
/// Rémunérée : la durée n'est pas déduite du temps de travail effectif.
|
||||
isPaid Boolean @default(false)
|
||||
label String?
|
||||
position Int @default(0)
|
||||
|
||||
shift Shift @relation(fields: [shiftId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([accountId])
|
||||
@@index([shiftId])
|
||||
}
|
||||
|
||||
/// Repos de journée — hebdomadaire ou compensateur. Les pauses *dans* un
|
||||
/// créneau sont `Shift.breakMinutes`.
|
||||
model Rest {
|
||||
@@ -1430,6 +1465,8 @@ enum EmailKind {
|
||||
INVITATION
|
||||
PASSWORD_RESET
|
||||
PLANNING_PUBLISHED
|
||||
/// Créneau ajouté ou déplacé hors d'une publication de semaine.
|
||||
SHIFT_ASSIGNED
|
||||
TIMEOFF_DECISION
|
||||
LEAVE_NOTICE
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user