Amélioration de la notion de workspace
This commit is contained in:
1 parent
1d5737703b
commit
c918de1e7d
11 files changed
+202
-58
No files matched your search
@@ -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 } });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user