From 1fb8328fe18eaff1713e402c3eba0b1cf81d385b Mon Sep 17 00:00:00 2001 From: Manus Agent Date: Tue, 28 Apr 2026 04:27:46 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20conformit=C3=A9=20stricte=20skill=20itin?= =?UTF-8?q?ova-user-management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rôles : remplacement admin/approbateur/validateur/operateur → admin/standard/readonly - schema.ts, migrate.ts : ENUM MySQL mis à jour (3 rôles skill) - routes/auth.ts : rôle par défaut standard, validRoles, modèle CSV corrigé - routes/invoices.ts : permissions readonly/standard/admin - routes/dashboard.ts : compteurs dashboard selon standard/admin - frontend/types/index.ts : type User role mis à jour - frontend/utils/helpers.ts : roleLabels admin/standard/readonly - frontend/pages/InvoiceDetail.tsx : actions disponibles selon standard/readonly/admin - frontend/pages/UserList.tsx : rôle par défaut standard, labels import corrigés --- backend/src/db/migrate.ts | 2 +- backend/src/db/schema.ts | 2 +- backend/src/routes/auth.ts | 12 ++++++------ backend/src/routes/dashboard.ts | 12 ++++++++---- backend/src/routes/invoices.ts | 10 ++++++---- frontend/src/pages/InvoiceDetail.tsx | 14 +++++++++----- frontend/src/pages/UserList.tsx | 6 +++--- frontend/src/types/index.ts | 2 +- frontend/src/utils/helpers.ts | 5 ++--- 9 files changed, 37 insertions(+), 28 deletions(-) diff --git a/backend/src/db/migrate.ts b/backend/src/db/migrate.ts index 121ceb8..99054fa 100644 --- a/backend/src/db/migrate.ts +++ b/backend/src/db/migrate.ts @@ -31,7 +31,7 @@ async function migrate() { password VARCHAR(255) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, - role ENUM('admin', 'approbateur', 'validateur', 'operateur') NOT NULL DEFAULT 'operateur', + role ENUM('admin', 'standard', 'readonly') NOT NULL DEFAULT 'standard', is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 65952e8..3e75f73 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -22,7 +22,7 @@ export const users = mysqlTable('users', { password: varchar('password', { length: 255 }).notNull(), firstName: varchar('first_name', { length: 100 }).notNull(), lastName: varchar('last_name', { length: 100 }).notNull(), - role: mysqlEnum('role', ['admin', 'approbateur', 'validateur', 'operateur']).notNull().default('operateur'), + role: mysqlEnum('role', ['admin', 'standard', 'readonly']).notNull().default('standard'), isActive: boolean('is_active').notNull().default(true), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 1b7bbde..f0ca16e 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -153,7 +153,7 @@ router.post('/users', authenticate, authorize('admin'), async (req: AuthRequest, const [result]: any = await pool.execute( 'INSERT INTO users (email, password, first_name, last_name, role) VALUES (?, ?, ?, ?, ?)', - [email, hashedPassword, firstName, lastName, role || 'operateur'] + [email, hashedPassword, firstName, lastName, role || 'standard'] ); res.status(201).json({ @@ -161,7 +161,7 @@ router.post('/users', authenticate, authorize('admin'), async (req: AuthRequest, email, firstName, lastName, - role: role || 'operateur', + role: role || 'standard', }); } catch (error: any) { if (error.code === 'ER_DUP_ENTRY') { @@ -218,7 +218,7 @@ router.delete('/users/:id', authenticate, authorize('admin'), async (req: AuthRe // GET /api/auth/users/import-template - Télécharger le modèle CSV router.get('/users/import-template', authenticate, authorize('admin'), (_req: AuthRequest, res: Response) => { - const csvContent = 'email,prenom,nom,role\nadmin@exemple.com,Jean,Dupont,admin\nutilisateur@exemple.com,Marie,Martin,operateur\n'; + const csvContent = 'email,prenom,nom,role\nadmin@exemple.com,Jean,Dupont,admin\nstandard@exemple.com,Marie,Martin,standard\nreadonly@exemple.com,Paul,Durand,readonly\n'; res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Disposition', 'attachment; filename="modele_import_utilisateurs.csv"'); res.send('\uFEFF' + csvContent); // BOM UTF-8 pour Excel @@ -232,7 +232,7 @@ router.post('/users/import', authenticate, authorize('admin'), uploadMemory.sing } const pool = getPool(); - const validRoles = ['admin', 'approbateur', 'validateur', 'operateur']; + const validRoles = ['admin', 'standard', 'readonly']; const results = { created: 0, updated: 0, errors: [] as string[] }; // Parsing CSV ou Excel @@ -265,7 +265,7 @@ router.post('/users/import', authenticate, authorize('admin'), uploadMemory.sing const email = String(row[0] || '').trim().toLowerCase(); const prenom = String(row[1] || '').trim(); const nom = String(row[2] || '').trim(); - const role = String(row[3] || 'operateur').trim().toLowerCase(); + const role = String(row[3] || 'standard').trim().toLowerCase(); // Validation if (!email || !email.includes('@')) { @@ -276,7 +276,7 @@ router.post('/users/import', authenticate, authorize('admin'), uploadMemory.sing results.errors.push(`Ligne ${lineNum} : prénom ou nom manquant`); continue; } - const finalRole = validRoles.includes(role) ? role : 'operateur'; + const finalRole = validRoles.includes(role) ? role : 'standard'; // Vérifier si l'utilisateur existe const [existing]: any = await pool.execute( diff --git a/backend/src/routes/dashboard.ts b/backend/src/routes/dashboard.ts index fff2a39..7f50ee3 100644 --- a/backend/src/routes/dashboard.ts +++ b/backend/src/routes/dashboard.ts @@ -67,28 +67,32 @@ router.get('/', authenticate, async (req: AuthRequest, res: Response) => { 'SELECT source, COUNT(*) as count FROM invoices GROUP BY source' ); - // Factures en attente d'action selon le rôle + // Factures en attente d'action selon le rôle (skill : admin / standard / readonly) let pendingForUser: any[] = []; const role = req.user!.role; - if (role === 'operateur' || role === 'admin') { + // standard et admin voient les factures reçues à vérifier + if (role === 'standard' || role === 'admin') { const [rows]: any = await pool.execute( `SELECT COUNT(*) as count FROM invoices WHERE status = 'recue'` ); pendingForUser.push({ action: 'À vérifier', count: rows[0].count }); } - if (role === 'validateur' || role === 'admin') { + // standard et admin voient les factures en vérification à valider + if (role === 'standard' || role === 'admin') { const [rows]: any = await pool.execute( `SELECT COUNT(*) as count FROM invoices WHERE status = 'en_verification'` ); pendingForUser.push({ action: 'À valider', count: rows[0].count }); } - if (role === 'approbateur' || role === 'admin') { + // standard et admin voient les factures validées à approuver + if (role === 'standard' || role === 'admin') { const [rows]: any = await pool.execute( `SELECT COUNT(*) as count FROM invoices WHERE status = 'validee'` ); pendingForUser.push({ action: 'À approuver', count: rows[0].count }); } + // admin uniquement voit les factures approuvées à payer if (role === 'admin') { const [rows]: any = await pool.execute( `SELECT COUNT(*) as count FROM invoices WHERE status = 'approuvee'` diff --git a/backend/src/routes/invoices.ts b/backend/src/routes/invoices.ts index a1a53a5..87049c6 100644 --- a/backend/src/routes/invoices.ts +++ b/backend/src/routes/invoices.ts @@ -401,12 +401,14 @@ router.post('/:id/status', authenticate, async (req: AuthRequest, res: Response) }); } - // Vérifier les permissions + // Vérifier les permissions (skill itinova-user-management : admin / standard / readonly) const role = req.user!.role; const rolePermissions: Record = { - 'operateur': ['en_verification', 'recue'], - 'validateur': ['en_verification', 'validee', 'rejetee', 'recue'], - 'approbateur': ['en_verification', 'validee', 'approuvee', 'rejetee', 'recue'], + // readonly : consultation uniquement, aucune modification de statut + 'readonly': [], + // standard : droits métiers complets (validation, approbation, rejet) + 'standard': ['en_verification', 'validee', 'approuvee', 'rejetee', 'recue'], + // admin : tous les droits y compris paiement et archivage 'admin': ['en_verification', 'validee', 'approuvee', 'payee', 'rejetee', 'recue', 'archivee'], }; diff --git a/frontend/src/pages/InvoiceDetail.tsx b/frontend/src/pages/InvoiceDetail.tsx index 0a3705b..50a24c0 100644 --- a/frontend/src/pages/InvoiceDetail.tsx +++ b/frontend/src/pages/InvoiceDetail.tsx @@ -101,13 +101,17 @@ export default function InvoiceDetail() { const role = user.role; const s = invoice.status; - if (s === 'recue' && ['operateur', 'validateur', 'approbateur', 'admin'].includes(role)) { + // Permissions selon le skill itinova-user-management (admin / standard / readonly) + // readonly : aucune action disponible + // standard : droits métiers complets (vérification, validation, approbation, rejet) + // admin : tous les droits y compris paiement et archivage + if (s === 'recue' && ['standard', 'admin'].includes(role)) { actions.push({ status: 'en_verification', label: 'Passer en vérification', color: 'btn-primary' }); } - if (s === 'en_verification' && ['validateur', 'approbateur', 'admin'].includes(role)) { + if (s === 'en_verification' && ['standard', 'admin'].includes(role)) { actions.push({ status: 'validee', label: 'Valider', color: 'btn-success' }); } - if (s === 'validee' && ['approbateur', 'admin'].includes(role)) { + if (s === 'validee' && ['standard', 'admin'].includes(role)) { actions.push({ status: 'approuvee', label: 'Approuver', color: 'btn-success' }); } if (s === 'approuvee' && role === 'admin') { @@ -116,10 +120,10 @@ export default function InvoiceDetail() { if (s === 'payee' && role === 'admin') { actions.push({ status: 'archivee', label: 'Archiver', color: 'btn-secondary' }); } - if (!['payee', 'archivee', 'rejetee'].includes(s) && ['validateur', 'approbateur', 'admin'].includes(role)) { + if (!['payee', 'archivee', 'rejetee'].includes(s) && ['standard', 'admin'].includes(role)) { actions.push({ status: 'rejetee', label: 'Rejeter', color: 'btn-danger' }); } - if (s === 'rejetee' && ['operateur', 'validateur', 'approbateur', 'admin'].includes(role)) { + if (s === 'rejetee' && ['standard', 'admin'].includes(role)) { actions.push({ status: 'recue', label: 'Remettre en réception', color: 'btn-secondary' }); } diff --git a/frontend/src/pages/UserList.tsx b/frontend/src/pages/UserList.tsx index 2b7fb31..a7cdb1c 100644 --- a/frontend/src/pages/UserList.tsx +++ b/frontend/src/pages/UserList.tsx @@ -23,7 +23,7 @@ export default function UserList() { const fileInputRef = useRef(null); const [form, setForm] = useState({ - email: '', password: '', firstName: '', lastName: '', role: 'operateur', + email: '', password: '', firstName: '', lastName: '', role: 'standard', }); useEffect(() => { loadUsers(); }, []); @@ -41,7 +41,7 @@ export default function UserList() { const openCreate = () => { setEditingUser(null); - setForm({ email: '', password: '', firstName: '', lastName: '', role: 'operateur' }); + setForm({ email: '', password: '', firstName: '', lastName: '', role: 'standard' }); setShowModal(true); }; @@ -242,7 +242,7 @@ export default function UserList() {

Format attendu (colonnes) :

email, prenom, nom, role

- Roles : admin, approbateur, validateur, operateur (défaut si vide). + Profils : admin, standard, readonly (défaut : standard si vide).

Utilisateurs existants (même email) : mis à jour. Nouveaux : mot de passe temporaire généré. diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index b718062..e37520d 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -3,7 +3,7 @@ export interface User { email: string; firstName: string; lastName: string; - role: 'admin' | 'approbateur' | 'validateur' | 'operateur'; + role: 'admin' | 'standard' | 'readonly'; isActive?: boolean; createdAt?: string; updatedAt?: string; diff --git a/frontend/src/utils/helpers.ts b/frontend/src/utils/helpers.ts index 1c904f6..11da69b 100644 --- a/frontend/src/utils/helpers.ts +++ b/frontend/src/utils/helpers.ts @@ -83,9 +83,8 @@ export const poStatusColors: Record = { export const roleLabels: Record = { admin: 'Administrateur', - approbateur: 'Approbateur', - validateur: 'Validateur', - operateur: 'Opérateur', + standard: 'Standard', + readonly: 'Lecture seule', }; export function isOverdue(dueDate: string | undefined | null, status: string): boolean {