Amélioration de la notion de workspace

This commit is contained in:
ocroguennec committed 2026-09-19 17:19:53 +02:00
1 parent 1d5737703b
commit c918de1e7d
11 files changed
+202 -58

No files matched your search

+25 -6
View File
@@ -14,6 +14,14 @@ import { audit } from '../utils/audit.js';
const router = Router();
// Types de workspace reconnus par l'app (19/09/26 — cf. db/index.js pour le contexte complet
// de cette migration). 'crowdlending' est le singleton historique (un seul exemplaire, imposé
// par l'index unique partiel côté DB et re-vérifié ici pour un message d'erreur clair) ;
// 'private_equity' peut avoir plusieurs exemplaires (ex. si Olivier veut un jour séparer deux
// portefeuilles PE distincts). Un 3e type nécessiterait d'abord une page/route dédiée avant
// d'avoir un sens ici — l'ajouter à cette liste seul ne suffit pas.
const WORKSPACE_TYPES = ['crowdlending', 'private_equity'];
router.get('/', (_req, res) => {
const rows = db.prepare(`
SELECT w.*,
@@ -31,6 +39,11 @@ const WorkspaceCreateSchema = z.object({
nom: z.string().min(1),
libelleMenu: z.string().min(1),
description: z.string().optional(),
// Choisi une fois à la création, jamais modifiable ensuite (même régime que slug, cf.
// WorkspaceUpdateSchema ci-dessous qui n'inclut ni l'un ni l'autre) — changer le type d'un
// workspace déjà peuplé de données changerait silencieusement le modèle de page/API qui
// s'applique à ces données.
type: z.enum(WORKSPACE_TYPES, { errorMap: () => ({ message: `Type invalide (valeurs acceptées : ${WORKSPACE_TYPES.join(', ')})` }) }),
actifGlobal: z.boolean().default(true),
ordre: z.number().int().default(0),
});
@@ -40,14 +53,20 @@ router.post('/', (req, res, next) => {
const body = WorkspaceCreateSchema.parse(req.body);
const exists = db.prepare('SELECT id FROM workspaces WHERE slug = ?').get(body.slug);
if (exists) throw new HttpError(409, 'Ce slug est déjà utilisé');
// Cf. l'index unique partiel côté DB (idx_workspaces_type_crowdlending_singleton) : cette
// vérification applicative donne un message clair avant même d'atteindre la contrainte SQL.
if (body.type === 'crowdlending') {
const alreadyCl = db.prepare("SELECT id FROM workspaces WHERE type = 'crowdlending'").get();
if (alreadyCl) throw new HttpError(409, 'Un workspace de type Crowdlending existe déjà');
}
const r = db.prepare(`
INSERT INTO workspaces (slug, nom, libelle_menu, description, actif_global, ordre)
VALUES (?,?,?,?,?,?)
`).run(body.slug, body.nom, body.libelleMenu, body.description || null, body.actifGlobal ? 1 : 0, body.ordre);
INSERT INTO workspaces (slug, nom, libelle_menu, description, type, actif_global, ordre)
VALUES (?,?,?,?,?,?,?)
`).run(body.slug, body.nom, body.libelleMenu, body.description || null, body.type, body.actifGlobal ? 1 : 0, body.ordre);
audit(req, { action: 'workspace_created', category: 'workspaces', actorId: req.user.id, details: { slug: body.slug } });
res.status(201).json({ id: r.lastInsertRowid, slug: body.slug, nom: body.nom, libelle_menu: body.libelleMenu });
audit(req, { action: 'workspace_created', category: 'workspaces', actorId: req.user.id, details: { slug: body.slug, type: body.type } });
res.status(201).json({ id: r.lastInsertRowid, slug: body.slug, nom: body.nom, libelle_menu: body.libelleMenu, type: body.type });
} catch (e) { next(e); }
});
@@ -82,7 +101,7 @@ router.delete('/:id', (req, res, next) => {
const ws = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(id);
if (!ws) throw new HttpError(404, 'Espace de travail introuvable');
// Le workspace socle ne peut pas être supprimé — toute l'app actuelle en dépend.
if (ws.slug === 'crowdlending') throw new HttpError(400, 'Le workspace Crowdlending ne peut pas être supprimé');
if (ws.type === 'crowdlending') throw new HttpError(400, 'Le workspace Crowdlending ne peut pas être supprimé');
db.prepare('DELETE FROM workspaces WHERE id = ?').run(id);
audit(req, { action: 'workspace_deleted', category: 'workspaces', actorId: req.user.id, details: { slug: ws.slug } });
+2 -2
View File
@@ -137,8 +137,8 @@ router.get('/', (req, res, next) => {
`).all(Number(investissement_pe_id));
} else {
const workspaceId = resolveActiveWorkspaceId(req);
const ws = db.prepare('SELECT slug FROM workspaces WHERE id = ?').get(workspaceId);
if (!ws || ws.slug === 'crowdlending') {
const ws = db.prepare('SELECT type FROM workspaces WHERE id = ?').get(workspaceId);
if (!ws || ws.type === 'crowdlending') {
rows = db.prepare(`
SELECT f.*, c.nom AS compte_nom, i.nom_projet, p.nom AS plateforme_nom, inv.nom AS investisseur_nom
FROM frais_operations f
+1 -1
View File
@@ -626,7 +626,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
if (row && userAllowedWs.has(row.id)) ids.push(row.id);
}
if (ids.length === 0) {
const cl = db.prepare("SELECT id FROM workspaces WHERE slug = 'crowdlending'").get();
const cl = db.prepare("SELECT id FROM workspaces WHERE type = 'crowdlending'").get();
if (cl && userAllowedWs.has(cl.id)) ids.push(cl.id);
}
return ids;
+1 -1
View File
@@ -551,7 +551,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
if (row) ids.push(row.id);
}
if (ids.length === 0) {
const cl = db.prepare("SELECT id FROM workspaces WHERE slug = 'crowdlending'").get();
const cl = db.prepare("SELECT id FROM workspaces WHERE type = 'crowdlending'").get();
if (cl) ids.push(cl.id);
}
return ids;
+5 -1
View File
@@ -14,8 +14,12 @@ import { COLOR_THEME_VALUES } from './preferences.js';
const router = Router();
router.get('/', (req, res) => {
// w.type (19/09/26) : c'est ce champ, PAS w.slug, que le frontend doit utiliser pour choisir
// entre les pages "Crowdlending" et "Private Equity" (App.jsx, Layout.jsx) — cf. db/index.js
// pour le contexte. slug reste exposé (identifiant technique stable, ex. exports/imports ZIP)
// mais ne porte plus la sémantique de routage.
const rows = db.prepare(`
SELECT w.id, w.slug, w.nom, w.libelle_menu, w.description, uw.active_by_user, uw.color_theme
SELECT w.id, w.slug, w.nom, w.libelle_menu, w.description, w.type, uw.active_by_user, uw.color_theme
FROM user_workspaces uw
JOIN workspaces w ON w.id = uw.workspace_id
WHERE uw.user_id = ? AND uw.granted_by_admin = 1 AND w.actif_global = 1