mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
- 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>
172 lines
5.6 KiB
TypeScript
172 lines
5.6 KiB
TypeScript
/**
|
||
* 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 */
|