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
570 lines
19 KiB
TypeScript
570 lines
19 KiB
TypeScript
import 'dotenv/config';
|
|
|
|
import { PrismaPg } from '@prisma/adapter-pg';
|
|
import { PrismaClient } from '@prisma/client';
|
|
import { hash } from '@node-rs/argon2';
|
|
|
|
import {
|
|
DEFAULT_ROLE_PERMISSIONS,
|
|
PERMISSION_DEFINITIONS,
|
|
SYSTEM_ROLES,
|
|
} from '../src/domain/access/permissions';
|
|
import {
|
|
isoWeekOf,
|
|
previousIsoWeek,
|
|
weekDates,
|
|
zonedInstant,
|
|
} from '../src/domain/planning/week';
|
|
import { POSTE_CODES, POSTE_LABELS } from '../src/lib/design/postes';
|
|
|
|
/**
|
|
* Jeu de données de départ — PLAN.md §11.
|
|
*
|
|
* **Entièrement fictif.** Deux établissements, des équipes, et les rôles
|
|
* fournis. Le mot de passe de démonstration n'a de sens qu'en développement ;
|
|
* il est refusé si NODE_ENV vaut production.
|
|
*/
|
|
|
|
const DEMO_PASSWORD = 'planflow-demo-2026';
|
|
|
|
const adapter = new PrismaPg({
|
|
connectionString: process.env.DATABASE_URL ?? '',
|
|
});
|
|
const prisma = new PrismaClient({ adapter });
|
|
|
|
async function main() {
|
|
if (process.env.NODE_ENV === 'production') {
|
|
throw new Error(
|
|
'Le seed installe un compte de démonstration : refusé en production.',
|
|
);
|
|
}
|
|
|
|
console.log('→ Capacités');
|
|
for (const permission of PERMISSION_DEFINITIONS) {
|
|
await prisma.permission.upsert({
|
|
where: { code: permission.code },
|
|
update: { category: permission.category, label: permission.label },
|
|
create: permission,
|
|
});
|
|
}
|
|
console.log(` ${PERMISSION_DEFINITIONS.length} capacités`);
|
|
|
|
console.log('→ Compte');
|
|
const account = await prisma.account.upsert({
|
|
where: { id: 'demo-account' },
|
|
update: {},
|
|
create: {
|
|
id: 'demo-account',
|
|
name: 'Maison Rivage',
|
|
siren: '000000000',
|
|
apeCode: '4759B',
|
|
collectiveAgreementId: '1517',
|
|
},
|
|
});
|
|
|
|
console.log('→ Rôles');
|
|
const roleIds = new Map<string, string>();
|
|
for (const role of SYSTEM_ROLES) {
|
|
const created = await prisma.role.upsert({
|
|
where: { accountId_key: { accountId: account.id, key: role.key } },
|
|
update: { name: role.name },
|
|
create: {
|
|
accountId: account.id,
|
|
key: role.key,
|
|
name: role.name,
|
|
isSystem: true,
|
|
},
|
|
});
|
|
roleIds.set(role.key, created.id);
|
|
|
|
const codes = DEFAULT_ROLE_PERMISSIONS[role.key];
|
|
const permissions = await prisma.permission.findMany({
|
|
where: { code: { in: codes } },
|
|
});
|
|
|
|
await prisma.rolePermission.deleteMany({ where: { roleId: created.id } });
|
|
await prisma.rolePermission.createMany({
|
|
data: permissions.map((permission) => ({
|
|
roleId: created.id,
|
|
permissionId: permission.id,
|
|
})),
|
|
skipDuplicates: true,
|
|
});
|
|
console.log(` ${role.name} — ${permissions.length} capacités`);
|
|
}
|
|
|
|
console.log('→ Établissements');
|
|
const locations = [
|
|
{ id: 'loc-nantes', name: 'Nantes Atlantis' },
|
|
{ id: 'loc-rennes', name: 'Rennes Alma' },
|
|
];
|
|
for (const location of locations) {
|
|
await prisma.location.upsert({
|
|
where: { id: location.id },
|
|
update: { name: location.name },
|
|
create: {
|
|
id: location.id,
|
|
accountId: account.id,
|
|
name: location.name,
|
|
employerContributionRate: 42,
|
|
},
|
|
});
|
|
for (const [index, team] of ['Vente', 'Caisse', 'Réserve'].entries()) {
|
|
await prisma.team.upsert({
|
|
where: { id: `${location.id}-${index}` },
|
|
update: {},
|
|
create: {
|
|
id: `${location.id}-${index}`,
|
|
accountId: account.id,
|
|
locationId: location.id,
|
|
name: team,
|
|
position: index,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log('→ Comptes utilisateurs');
|
|
const passwordHash = await hash(DEMO_PASSWORD, {
|
|
memoryCost: 19_456,
|
|
timeCost: 2,
|
|
parallelism: 1,
|
|
});
|
|
|
|
const people = [
|
|
{ email: 'direction@example.test', firstName: 'Camille', lastName: 'Ferrand', role: 'owner', all: true },
|
|
{ email: 'manager.nantes@example.test', firstName: 'Jonas', lastName: 'Meyer', role: 'manager', all: false },
|
|
{ email: 'manager.rennes@example.test', firstName: 'Inès', lastName: 'Bakhti', role: 'manager', all: false },
|
|
{ email: 'salarie@example.test', firstName: 'Rémi', lastName: 'Chartier', role: 'employee', all: false },
|
|
];
|
|
|
|
for (const [index, person] of people.entries()) {
|
|
const user = await prisma.user.upsert({
|
|
where: { email: person.email },
|
|
// Remet le compteur d'échecs à zéro : sans cela, des exécutions
|
|
// répétées des tests finissent par verrouiller le compte au bout de
|
|
// huit tentatives, et l'échec suivant est incompréhensible.
|
|
update: { failedAttempts: 0, lockedUntil: null, passwordHash },
|
|
create: {
|
|
email: person.email,
|
|
firstName: person.firstName,
|
|
lastName: person.lastName,
|
|
passwordHash,
|
|
},
|
|
});
|
|
|
|
const membership = await prisma.membership.upsert({
|
|
where: {
|
|
accountId_employeeNumber: {
|
|
accountId: account.id,
|
|
employeeNumber: `E${String(index + 1).padStart(4, '0')}`,
|
|
},
|
|
},
|
|
update: { userId: user.id, status: 'ACTIVE' },
|
|
create: {
|
|
accountId: account.id,
|
|
userId: user.id,
|
|
roleId: roleIds.get(person.role) as string,
|
|
employeeNumber: `E${String(index + 1).padStart(4, '0')}`,
|
|
status: 'ACTIVE',
|
|
},
|
|
});
|
|
|
|
await prisma.membershipScope.deleteMany({
|
|
where: { membershipId: membership.id },
|
|
});
|
|
await prisma.membershipScope.create({
|
|
data: {
|
|
accountId: account.id,
|
|
membershipId: membership.id,
|
|
allLocations: person.all,
|
|
locationId: person.all
|
|
? null
|
|
: person.email.includes('rennes')
|
|
? 'loc-rennes'
|
|
: 'loc-nantes',
|
|
},
|
|
});
|
|
console.log(` ${person.email} — ${person.role}`);
|
|
}
|
|
|
|
console.log('→ Contrats et dossiers');
|
|
const contractSpecs = [
|
|
{ number: 'E0001', type: 'CDI', hours: 39, forfait: false, location: 'loc-nantes' },
|
|
{ number: 'E0002', type: 'CDI', hours: 0, forfait: true, location: 'loc-nantes' },
|
|
{ number: 'E0003', type: 'CDI', hours: 35, forfait: false, location: 'loc-rennes' },
|
|
{ number: 'E0004', type: 'CDD', hours: 24, forfait: false, location: 'loc-nantes' },
|
|
] as const;
|
|
|
|
for (const spec of contractSpecs) {
|
|
const membership = await prisma.membership.findUnique({
|
|
where: {
|
|
accountId_employeeNumber: {
|
|
accountId: account.id,
|
|
employeeNumber: spec.number,
|
|
},
|
|
},
|
|
});
|
|
if (!membership) continue;
|
|
|
|
const holder = membership.userId
|
|
? await prisma.user.findUnique({ where: { id: membership.userId } })
|
|
: null;
|
|
|
|
await prisma.employeeProfile.upsert({
|
|
where: { membershipId: membership.id },
|
|
update: {},
|
|
create: {
|
|
membershipId: membership.id,
|
|
accountId: account.id,
|
|
firstName: holder?.firstName ?? 'Prénom',
|
|
lastName: holder?.lastName ?? 'À compléter',
|
|
city: 'Nantes',
|
|
phone: '00 00 00 00 00',
|
|
},
|
|
});
|
|
|
|
const existing = await prisma.userContract.findFirst({
|
|
where: { membershipId: membership.id },
|
|
});
|
|
if (existing) continue;
|
|
|
|
await prisma.userContract.create({
|
|
data: {
|
|
accountId: account.id,
|
|
membershipId: membership.id,
|
|
locationId: spec.location,
|
|
contractType: spec.type,
|
|
startDate: new Date('2024-01-08'),
|
|
workTimeArrangement: spec.forfait ? 'FORFAIT_JOURS' : 'HOURLY',
|
|
weeklyHours: spec.forfait ? 0 : spec.hours,
|
|
forfaitDaysPerYear: spec.forfait ? 218 : null,
|
|
forfaitAgreementRef: spec.forfait ? 'CONV-2024-002' : null,
|
|
forfaitAgreedAt: spec.forfait ? new Date('2024-01-05') : null,
|
|
monthlySalary: 2100,
|
|
},
|
|
});
|
|
}
|
|
console.log(` ${contractSpecs.length} contrats`);
|
|
|
|
|
|
console.log('→ Étiquettes de planning');
|
|
for (const [index, code] of POSTE_CODES.entries()) {
|
|
await prisma.label.upsert({
|
|
where: { accountId_code: { accountId: account.id, code: code.toUpperCase() } },
|
|
update: { name: POSTE_LABELS[code], paletteKey: code, position: index },
|
|
create: {
|
|
accountId: account.id,
|
|
code: code.toUpperCase(),
|
|
name: POSTE_LABELS[code],
|
|
paletteKey: code,
|
|
position: index,
|
|
},
|
|
});
|
|
}
|
|
console.log(` ${POSTE_CODES.length} étiquettes`);
|
|
|
|
console.log('→ Intitulés de poste');
|
|
const jobTitles = [
|
|
'Responsable de magasin',
|
|
'Adjoint·e de direction',
|
|
'Vendeur·se conseil',
|
|
'Hôte·sse de caisse',
|
|
'Employé·e de réserve',
|
|
];
|
|
const jobTitleIds = new Map<string, string>();
|
|
for (const name of jobTitles) {
|
|
const created = await prisma.jobTitle.upsert({
|
|
where: { accountId_name: { accountId: account.id, name } },
|
|
update: {},
|
|
create: { accountId: account.id, name },
|
|
});
|
|
jobTitleIds.set(name, created.id);
|
|
}
|
|
|
|
console.log('→ Salariés sans compte applicatif');
|
|
// La majorité d'une équipe de vente ne se connecte jamais à l'outil : ces
|
|
// salariés existent en planning et en paie, sans identifiants.
|
|
const staff = [
|
|
{ number: 'E0005', first: 'Sofia', last: 'Marchetti', job: 'Vendeur·se conseil', hours: 35, team: 0, location: 'loc-nantes' },
|
|
{ number: 'E0006', first: 'Yanis', last: 'Trabelsi', job: 'Vendeur·se conseil', hours: 30, team: 0, location: 'loc-nantes' },
|
|
{ number: 'E0007', first: 'Léa', last: 'Nguyen', job: 'Hôte·sse de caisse', hours: 24, team: 1, location: 'loc-nantes' },
|
|
{ number: 'E0008', first: 'Marius', last: 'Kowalski', job: 'Hôte·sse de caisse', hours: 35, team: 1, location: 'loc-nantes' },
|
|
{ number: 'E0009', first: 'Awa', last: 'Diallo', job: 'Employé·e de réserve', hours: 35, team: 2, location: 'loc-nantes' },
|
|
{ number: 'E0010', first: 'Théo', last: 'Berger', job: 'Vendeur·se conseil', hours: 28, team: 0, location: 'loc-rennes' },
|
|
{ number: 'E0011', first: 'Clara', last: 'Fontaine', job: 'Hôte·sse de caisse', hours: 35, team: 1, location: 'loc-rennes' },
|
|
{ number: 'E0012', first: 'Noé', last: 'Perrin', job: 'Employé·e de réserve', hours: 20, team: 2, location: 'loc-rennes' },
|
|
] as const;
|
|
|
|
for (const person of staff) {
|
|
const membership = await prisma.membership.upsert({
|
|
where: {
|
|
accountId_employeeNumber: {
|
|
accountId: account.id,
|
|
employeeNumber: person.number,
|
|
},
|
|
},
|
|
update: { status: 'ACTIVE' },
|
|
create: {
|
|
accountId: account.id,
|
|
roleId: roleIds.get('employee') as string,
|
|
employeeNumber: person.number,
|
|
status: 'ACTIVE',
|
|
},
|
|
});
|
|
|
|
await prisma.employeeProfile.upsert({
|
|
where: { membershipId: membership.id },
|
|
update: { firstName: person.first, lastName: person.last },
|
|
create: {
|
|
membershipId: membership.id,
|
|
accountId: account.id,
|
|
firstName: person.first,
|
|
lastName: person.last,
|
|
city: person.location === 'loc-rennes' ? 'Rennes' : 'Nantes',
|
|
},
|
|
});
|
|
|
|
const existing = await prisma.userContract.findFirst({
|
|
where: { membershipId: membership.id },
|
|
});
|
|
if (!existing) {
|
|
await prisma.userContract.create({
|
|
data: {
|
|
accountId: account.id,
|
|
membershipId: membership.id,
|
|
locationId: person.location,
|
|
contractType: 'CDI',
|
|
startDate: new Date('2024-09-02'),
|
|
workTimeArrangement: 'HOURLY',
|
|
weeklyHours: person.hours,
|
|
jobTitleId: jobTitleIds.get(person.job) ?? null,
|
|
monthlySalary: 1900,
|
|
},
|
|
});
|
|
}
|
|
|
|
await prisma.teamMember.upsert({
|
|
where: {
|
|
teamId_membershipId: {
|
|
teamId: `${person.location}-${person.team}`,
|
|
membershipId: membership.id,
|
|
},
|
|
},
|
|
update: {},
|
|
create: {
|
|
accountId: account.id,
|
|
teamId: `${person.location}-${person.team}`,
|
|
membershipId: membership.id,
|
|
},
|
|
});
|
|
}
|
|
console.log(` ${staff.length} salariés`);
|
|
|
|
console.log('→ Rattachements des titulaires de comptes');
|
|
// Les quatre comptes applicatifs planifient aussi : sans rattachement, ils
|
|
// n'apparaîtraient pas dans la grille.
|
|
const accountHolders = [
|
|
{ number: 'E0001', team: 'loc-nantes-0', job: 'Responsable de magasin' },
|
|
{ number: 'E0002', team: 'loc-nantes-0', job: 'Adjoint·e de direction' },
|
|
{ number: 'E0003', team: 'loc-rennes-0', job: 'Responsable de magasin' },
|
|
{ number: 'E0004', team: 'loc-nantes-1', job: 'Hôte·sse de caisse' },
|
|
] as const;
|
|
|
|
for (const holder of accountHolders) {
|
|
const membership = await prisma.membership.findUnique({
|
|
where: {
|
|
accountId_employeeNumber: {
|
|
accountId: account.id,
|
|
employeeNumber: holder.number,
|
|
},
|
|
},
|
|
});
|
|
if (!membership) continue;
|
|
|
|
await prisma.userContract.updateMany({
|
|
where: { membershipId: membership.id },
|
|
data: { jobTitleId: jobTitleIds.get(holder.job) ?? null },
|
|
});
|
|
|
|
await prisma.teamMember.upsert({
|
|
where: {
|
|
teamId_membershipId: {
|
|
teamId: holder.team,
|
|
membershipId: membership.id,
|
|
},
|
|
},
|
|
update: {},
|
|
create: {
|
|
accountId: account.id,
|
|
teamId: holder.team,
|
|
membershipId: membership.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
console.log('→ Plannings');
|
|
await seedPlanning(account.id);
|
|
|
|
console.log('→ Durées de conservation');
|
|
const retention = [
|
|
['Shift', 12, 'creation', 'Décompte des horaires : 1 an minimum (matrice n° 21).'],
|
|
['ForfaitDayEntry', 36, 'creation', 'Décompte des jours de forfait : 3 ans minimum.'],
|
|
['UserContract', 60, 'contract_end', 'Pièces contractuelles : 5 ans.'],
|
|
['PersonnelRegister', 60, 'employee_departure', 'Registre du personnel : 5 ans après le départ.'],
|
|
['PayrollVariable', 72, 'period_end', "Éléments d'assiette transmis à Silae : 6 ans."],
|
|
] as const;
|
|
|
|
for (const [objectType, durationMonths, startPoint, justification] of retention) {
|
|
await prisma.retentionPolicy.upsert({
|
|
where: {
|
|
accountId_objectType_effectiveFrom: {
|
|
accountId: account.id,
|
|
objectType,
|
|
effectiveFrom: new Date('2026-01-01'),
|
|
},
|
|
},
|
|
update: { durationMonths, startPoint, justification },
|
|
create: {
|
|
accountId: account.id,
|
|
objectType,
|
|
durationMonths,
|
|
startPoint,
|
|
justification,
|
|
effectiveFrom: new Date('2026-01-01'),
|
|
},
|
|
});
|
|
}
|
|
console.log(` ${retention.length} politiques`);
|
|
|
|
console.log(`\nMot de passe de démonstration : ${DEMO_PASSWORD}`);
|
|
}
|
|
|
|
/**
|
|
* Deux semaines de planning : la précédente publiée, la courante en brouillon.
|
|
*
|
|
* Semer les deux états est délibéré — c'est la seule façon de voir en un coup
|
|
* d'œil que la publication est bien **par équipe** et que les créneaux d'une
|
|
* semaine publiée se distinguent d'un brouillon.
|
|
*/
|
|
async function seedPlanning(accountId: string): Promise<void> {
|
|
const current = isoWeekOf(new Date());
|
|
const previous = previousIsoWeek(current);
|
|
|
|
const teams = await prisma.team.findMany({
|
|
where: { accountId },
|
|
orderBy: [{ locationId: 'asc' }, { position: 'asc' }],
|
|
});
|
|
const locations = await prisma.location.findMany({ where: { accountId } });
|
|
const timezoneOf = new Map(locations.map((l) => [l.id, l.timezone]));
|
|
|
|
const labels = await prisma.label.findMany({ where: { accountId } });
|
|
const labelOf = new Map(labels.map((label) => [label.paletteKey, label.id]));
|
|
|
|
/** Poste dominant de l'équipe, par position. */
|
|
const teamPoste = ['vte', 'cai', 'res'];
|
|
|
|
/** Trois plages types du commerce de détail. */
|
|
const patterns = [
|
|
{ start: '09:00', end: '17:00', pause: 60 },
|
|
{ start: '11:00', end: '19:00', pause: 60 },
|
|
{ start: '13:00', end: '20:00', pause: 30 },
|
|
];
|
|
|
|
let shiftsCreated = 0;
|
|
|
|
for (const week of [previous, current]) {
|
|
const dates = weekDates(week);
|
|
|
|
for (const team of teams) {
|
|
const timezone = timezoneOf.get(team.locationId) ?? 'Europe/Paris';
|
|
const members = await prisma.teamMember.findMany({
|
|
where: { teamId: team.id },
|
|
orderBy: { position: 'asc' },
|
|
});
|
|
if (members.length === 0) continue;
|
|
|
|
const schedule = await prisma.weeklySchedule.upsert({
|
|
where: {
|
|
teamId_isoYear_isoWeek: {
|
|
teamId: team.id,
|
|
isoYear: week.isoYear,
|
|
isoWeek: week.isoWeek,
|
|
},
|
|
},
|
|
update: {},
|
|
create: {
|
|
accountId,
|
|
teamId: team.id,
|
|
locationId: team.locationId,
|
|
isoYear: week.isoYear,
|
|
isoWeek: week.isoWeek,
|
|
status: week === previous ? 'PUBLISHED' : 'DRAFT',
|
|
publishedAt: week === previous ? new Date() : null,
|
|
},
|
|
});
|
|
|
|
const already = await prisma.shift.count({
|
|
where: { weeklyScheduleId: schedule.id },
|
|
});
|
|
if (already > 0) continue;
|
|
|
|
const poste = teamPoste[team.position] ?? 'vte';
|
|
const labelId = labelOf.get(poste) ?? null;
|
|
|
|
for (const [index, member] of members.entries()) {
|
|
const pattern = patterns[index % patterns.length] as (typeof patterns)[number];
|
|
// Deux jours de repos glissants : la semaine de six jours consécutifs
|
|
// est justement ce que les règles de convention interdisent.
|
|
const rest = new Set([(index * 2) % 7, ((index * 2) + 1) % 7]);
|
|
|
|
for (const [dayIndex, date] of dates.entries()) {
|
|
if (rest.has(dayIndex)) continue;
|
|
await prisma.shift.create({
|
|
data: {
|
|
accountId,
|
|
weeklyScheduleId: schedule.id,
|
|
membershipId: member.membershipId,
|
|
localDate: new Date(`${date}T00:00:00Z`),
|
|
startAt: zonedInstant(date, pattern.start, timezone),
|
|
endAt: zonedInstant(date, pattern.end, timezone),
|
|
breakMinutes: pattern.pause,
|
|
labelId,
|
|
isValidated: week === previous,
|
|
},
|
|
});
|
|
shiftsCreated += 1;
|
|
}
|
|
}
|
|
|
|
// Un besoin non couvert par équipe sur la semaine courante : c'est l'état
|
|
// normal d'un planning en construction, et la ligne dédiée doit s'afficher.
|
|
if (week === current) {
|
|
const saturday = dates[5] as string;
|
|
await prisma.shift.create({
|
|
data: {
|
|
accountId,
|
|
weeklyScheduleId: schedule.id,
|
|
membershipId: null,
|
|
localDate: new Date(`${saturday}T00:00:00Z`),
|
|
startAt: zonedInstant(saturday, '10:00', timezone),
|
|
endAt: zonedInstant(saturday, '18:00', timezone),
|
|
breakMinutes: 60,
|
|
labelId,
|
|
},
|
|
});
|
|
shiftsCreated += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(` ${shiftsCreated} créneaux sur 2 semaines`);
|
|
}
|
|
|
|
main()
|
|
.catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
})
|
|
.finally(() => prisma.$disconnect());
|