WP-04: planning sur données réelles — grille semaine, vue jour, publication par équipe
Le planning quitte le module de démonstration : la grille lit `WeeklySchedule` et `Shift`, et les quatre derniers fichiers de `src/lib/demo/` qui portaient des salariés fictifs disparaissent. Modèle - `TeamMember` : rattachement d'un salarié à une équipe, distinct de `MembershipScope` (qui dit ce qu'un manager a le droit de voir). Sans lui, un salarié sans créneau n'apparaîtrait pas dans la grille — c'est-à-dire l'état de départ de toute semaine en construction. - RLS sur `WeeklySchedule`, `Shift`, `Rest`, `DailyNote` et `TeamMember`. Temps - `src/domain/planning/week.ts` : repérage par couple année ISO + semaine ISO, jamais par date de début. Le lundi 29 décembre 2025 appartient à la semaine 1 de 2026 ; une clé fondée sur la date ferait apparaître deux semaines 1. - `zonedInstant` / `zonedMidnight` corrigent le décalage mesuré **à l'instant visé**. Le 29 mars 2026, minuit est en UTC+1 et 09 h en UTC+2 : ajouter neuf heures à minuit donnerait 10 h locales. - La semaine du retour à l'heure d'hiver dure 169 h, celle du passage à l'heure d'été 167 — vérifié par test. Écritures - Création, modification, suppression de créneau ; publication et dépublication **par équipe**, avec verrou optimiste sur `version` : deux managers sur la même grille est le cas normal, pas l'exception. - Chevauchement refusé en transaction, pas seulement dans le formulaire. - Modifier une semaine publiée exige `planning.edit_published`, capacité que le rôle manager n'a pas : un salarié a organisé sa semaine sur ce qu'il a lu. - Toute mutation laisse une entrée d'audit ; la suppression écrit sa trace **avant** l'effacement, sinon l'état supprimé serait perdu. Lecture - `planning.view_unpublished` filtre en base : sans cette capacité, les brouillons ne sont pas chargés du tout. Un test vérifie que les horaires n'apparaissent pas dans le HTML servi — un masquage CSS les y laisserait. - Vue jour reconstruite sur les mêmes données, amplitude déduite de la journée réelle plutôt que figée à 06 h–21 h. Vérification - 26 tests unitaires sur le repérage des semaines et la mise en grille. - Parcours e2e : poser un créneau, refus de chevauchement, publier, dépublier ; et ce que voient un salarié et un manager sur la même semaine. - `scripts/dev-db.sh` : la base de développement est éphémère dans cet environnement, la remonter ne doit pas être une redécouverte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
This commit is contained in:
27 files changed
+3373
-674
No files matched your search
@@ -0,0 +1,102 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ScheduleStatus" AS ENUM ('DRAFT', 'VALIDATED', 'PUBLISHED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RestType" AS ENUM ('WEEKLY_REST', 'COMPENSATORY_REST');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WeeklySchedule" (
|
||||
"id" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"locationId" TEXT NOT NULL,
|
||||
"teamId" TEXT NOT NULL,
|
||||
"isoYear" INTEGER NOT NULL,
|
||||
"isoWeek" INTEGER NOT NULL,
|
||||
"status" "ScheduleStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"publishedAt" TIMESTAMP(3),
|
||||
"publishedBy" TEXT,
|
||||
"version" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "WeeklySchedule_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Shift" (
|
||||
"id" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"weeklyScheduleId" TEXT NOT NULL,
|
||||
"membershipId" TEXT,
|
||||
"localDate" DATE NOT NULL,
|
||||
"startAt" TIMESTAMPTZ NOT NULL,
|
||||
"endAt" TIMESTAMPTZ NOT NULL,
|
||||
"breakMinutes" INTEGER NOT NULL DEFAULT 0,
|
||||
"actualStartAt" TIMESTAMPTZ,
|
||||
"actualEndAt" TIMESTAMPTZ,
|
||||
"actualBreakMinutes" INTEGER,
|
||||
"labelId" TEXT,
|
||||
"mealCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"isValidated" BOOLEAN NOT NULL DEFAULT false,
|
||||
"validatedAt" TIMESTAMP(3),
|
||||
"validatedBy" TEXT,
|
||||
"note" TEXT,
|
||||
"version" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "Shift_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Rest" (
|
||||
"id" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"weeklyScheduleId" TEXT NOT NULL,
|
||||
"membershipId" TEXT NOT NULL,
|
||||
"localDate" DATE NOT NULL,
|
||||
"restType" "RestType" NOT NULL,
|
||||
"minutes" INTEGER,
|
||||
|
||||
CONSTRAINT "Rest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "DailyNote" (
|
||||
"id" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"weeklyScheduleId" TEXT NOT NULL,
|
||||
"localDate" DATE NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "DailyNote_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WeeklySchedule_accountId_idx" ON "WeeklySchedule"("accountId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WeeklySchedule_locationId_isoYear_isoWeek_idx" ON "WeeklySchedule"("locationId", "isoYear", "isoWeek");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WeeklySchedule_teamId_isoYear_isoWeek_key" ON "WeeklySchedule"("teamId", "isoYear", "isoWeek");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Shift_accountId_idx" ON "Shift"("accountId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Shift_weeklyScheduleId_localDate_idx" ON "Shift"("weeklyScheduleId", "localDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Shift_membershipId_localDate_idx" ON "Shift"("membershipId", "localDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Rest_accountId_idx" ON "Rest"("accountId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "DailyNote_accountId_idx" ON "DailyNote"("accountId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Shift" ADD CONSTRAINT "Shift_weeklyScheduleId_fkey" FOREIGN KEY ("weeklyScheduleId") REFERENCES "WeeklySchedule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Rest" ADD CONSTRAINT "Rest_weeklyScheduleId_fkey" FOREIGN KEY ("weeklyScheduleId") REFERENCES "WeeklySchedule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DailyNote" ADD CONSTRAINT "DailyNote_weeklyScheduleId_fkey" FOREIGN KEY ("weeklyScheduleId") REFERENCES "WeeklySchedule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- Étend l'isolation aux tables du planning.
|
||||
--
|
||||
-- La grille est la table la plus lue de l'application : c'est aussi celle où
|
||||
-- une fuite inter-comptes serait la plus visible. Le test « toute table portant
|
||||
-- accountId est protégée » échoue sans ceci.
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
t text;
|
||||
BEGIN
|
||||
FOREACH t IN ARRAY ARRAY['WeeklySchedule', 'Shift', 'Rest', 'DailyNote']
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
|
||||
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', t);
|
||||
EXECUTE format(
|
||||
'CREATE POLICY tenant_isolation ON %I USING ("accountId" = planflow_current_account())',
|
||||
t
|
||||
);
|
||||
EXECUTE format(
|
||||
'CREATE POLICY tenant_insert ON %I FOR INSERT WITH CHECK ("accountId" = planflow_current_account())',
|
||||
t
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "TeamMember" (
|
||||
"id" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"teamId" TEXT NOT NULL,
|
||||
"membershipId" TEXT NOT NULL,
|
||||
"isPrimary" BOOLEAN NOT NULL DEFAULT true,
|
||||
"position" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TeamMember_accountId_idx" ON "TeamMember"("accountId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TeamMember_membershipId_idx" ON "TeamMember"("membershipId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TeamMember_teamId_membershipId_key" ON "TeamMember"("teamId", "membershipId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "Membership"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Isolation : `TeamMember` porte accountId, donc la politique est exigible.
|
||||
ALTER TABLE "TeamMember" ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "TeamMember" FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON "TeamMember"
|
||||
USING ("accountId" = planflow_current_account());
|
||||
CREATE POLICY tenant_insert ON "TeamMember"
|
||||
FOR INSERT WITH CHECK ("accountId" = planflow_current_account());
|
||||
Reference in new issue
Block a user