Files
siop2/apps/api/prisma/seed.ts
pr-daaif 4f3af5af4f feat(r0.11): apps/web — connexion + sélecteur démo, coquille, /design, client typé
- React 19 + Vite + Tailwind v4 : tokens.css copié tel quel (source de
  vérité), classes composants extraites de la maquette validée, Manrope
  auto-hébergée, primitives shadcn-style possédées (Button cva, menu Radix)
- connexion fidèle à la maquette ; liste démo affichée seulement si
  /auth/demo-accounts répond (404 ⇒ rien — ADR-002, l'API seule source)
- coquille : sidebar 4 groupes avec rail safran actif (écrans futurs marqués
  R1-R3), topbar recherche/thème/chip DÉMO + sélecteur de rôle (bascule 101 ms,
  critère < 3 s) ; page /design = référence vivante des revues pixel
- règle d'or : schema.d.ts généré depuis docs/openapi.json et committé
  (openapi-typescript + openapi-fetch)
- tokens.css : tokens --nav-* ajoutés (valeurs de la maquette validée) ;
  seed réaligné sur les personas de la maquette
- vérifié en navigateur réel (Playwright + Chrome headless) : 7 captures,
  parcours démo complet, bi-thème, zéro erreur console ; typecheck + vitest + build OK

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 22:09:40 +01:00

172 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Seed R0 — idempotent (upserts) : 7 rôles, matrice COMPLÈTE (une ligne par
* rôle × catégorie, invariant R0) et 7 comptes de démonstration (ADR-002).
* La matrice ci-dessous est le point de DÉPART pédagogique : elle vit en base
* et sera administrable (R1) — la modifier ici ne change pas une base déjà
* seedée (les lignes existantes ne sont pas écrasées, voir plus bas).
* Usage : pnpm seed (ou prisma db seed)
*/
import 'dotenv/config';
import { PrismaClient } from '@prisma/client';
import {
OBJECT_CATEGORIES,
ROLE_NAMES,
type ObjectCategory,
type RoleName,
} from '@siop/shared';
import * as argon2 from 'argon2';
type Grant = Partial<{
view: boolean;
viewOther: boolean;
create: boolean;
edit: boolean;
delete: boolean;
}>;
const FULL: Grant = { view: true, viewOther: true, create: true, edit: true, delete: true };
const READ: Grant = { view: true, viewOther: true };
/** Matrice de départ (rôle → catégorie → droits ; absent = tout à false). */
const MATRIX: Record<RoleName, Partial<Record<ObjectCategory, Grant>>> = {
Administrateur: Object.fromEntries(
OBJECT_CATEGORIES.map((c) => [c, FULL]),
) as Record<ObjectCategory, Grant>,
Gestionnaire: {
WORK_ORDERS: FULL,
REQUESTS: FULL,
ASSETS: FULL,
LOCATIONS: FULL,
METERS: FULL,
PARTS: FULL,
PURCHASE_ORDERS: FULL,
PEOPLE_TEAMS: { view: true, viewOther: true, create: true, edit: true },
ANALYTICS: READ,
SETTINGS: { view: true },
},
Dispatcher: {
WORK_ORDERS: { view: true, viewOther: true, create: true, edit: true },
REQUESTS: { view: true, viewOther: true, create: true, edit: true },
ASSETS: READ,
LOCATIONS: READ,
METERS: READ,
PARTS: { view: true },
PEOPLE_TEAMS: READ,
ANALYTICS: READ,
},
Technicien: {
WORK_ORDERS: { view: true, edit: true }, // ses OT uniquement (viewOther=false)
REQUESTS: { view: true },
ASSETS: READ,
LOCATIONS: READ,
METERS: { view: true, viewOther: true, create: true }, // relevés
PARTS: { view: true },
},
'Technicien limité': {
WORK_ORDERS: { view: true, edit: true }, // ses OT, sans consultation du parc
ASSETS: { view: true },
},
Demandeur: {
REQUESTS: { view: true, create: true }, // ses demandes uniquement
},
'Vue seule': {
WORK_ORDERS: READ,
REQUESTS: READ,
ASSETS: READ,
LOCATIONS: READ,
METERS: READ,
PARTS: READ,
PURCHASE_ORDERS: READ,
PEOPLE_TEAMS: READ,
ANALYTICS: READ,
},
};
/** 7 comptes démo — un par rôle. Les noms sont CEUX de la maquette validée
* (maquette-web.html) : la revue pixel compare écrans et maquettes. */
const DEMO_USERS: { email: string; displayName: string; role: RoleName }[] = [
{ email: 'admin@demo.siop.ma', displayName: 'Yasmine Alaoui', role: 'Administrateur' },
{ email: 'dispatcher@demo.siop.ma', displayName: 'Salma Idrissi', role: 'Dispatcher' },
{ email: 'technicien@demo.siop.ma', displayName: 'Ahmed Benali', role: 'Technicien' },
{ email: 'technicien-limite@demo.siop.ma', displayName: 'Youssef Tazi', role: 'Technicien limité' },
{ email: 'gestionnaire@demo.siop.ma', displayName: 'Nadia Berrada', role: 'Gestionnaire' },
{ email: 'demandeur@demo.siop.ma', displayName: 'Karim Doukkali', role: 'Demandeur' },
{ email: 'vue-seule@demo.siop.ma', displayName: 'Rachid Bennis', role: 'Vue seule' },
];
export async function seed(prisma: PrismaClient): Promise<void> {
const roleIds = new Map<RoleName, string>();
for (const name of ROLE_NAMES) {
const role = await prisma.role.upsert({
where: { name },
update: {},
create: { name },
});
roleIds.set(name, role.id);
}
// Matrice complète : une ligne par rôle × catégorie. Les lignes existantes
// ne sont PAS écrasées (la base est la source de vérité, pas ce fichier).
for (const name of ROLE_NAMES) {
const roleId = roleIds.get(name)!;
for (const category of OBJECT_CATEGORIES) {
const g = MATRIX[name][category] ?? {};
await prisma.permission.upsert({
where: { roleId_objectCategory: { roleId, objectCategory: category } },
update: {},
create: {
roleId,
objectCategory: category,
canView: g.view ?? false,
canViewOther: g.viewOther ?? false,
canCreate: g.create ?? false,
canEdit: g.edit ?? false,
canDelete: g.delete ?? false,
},
});
}
}
// Mot de passe commun des comptes démo (la connexion classique reste testable).
const passwordHash = await argon2.hash(
process.env.SEED_DEMO_PASSWORD ?? 'Demo!2026',
);
for (const u of DEMO_USERS) {
// Les comptes démo appartiennent au seed : nom et rôle sont réalignés
// à chaque exécution (jamais le mot de passe d'un compte existant).
await prisma.user.upsert({
where: { email: u.email },
update: { displayName: u.displayName, roleId: roleIds.get(u.role)! },
create: {
email: u.email,
displayName: u.displayName,
passwordHash,
roleId: roleIds.get(u.role)!,
isDemo: true,
},
});
}
}
/* c8 ignore start — wrapper CLI */
if (require.main === module) {
const prisma = new PrismaClient();
seed(prisma)
.then(async () => {
const [roles, permissions, users] = await Promise.all([
prisma.role.count(),
prisma.permission.count(),
prisma.user.count({ where: { isDemo: true } }),
]);
console.log(
`Seed OK — ${roles} rôles, ${permissions} lignes de matrice, ${users} comptes démo.`,
);
})
.catch((e) => {
console.error(e);
process.exitCode = 1;
})
.finally(() => prisma.$disconnect());
}
/* c8 ignore stop */