Implementation de la notion d'espace d'investissement

This commit is contained in:
ocroguennec committed 2026-09-17 18:25:42 +02:00
1 parent 6b6cad7cd6
commit 4b7df66862
54 files changed
+6782 -288

No files matched your search

+64 -7
View File
@@ -110,6 +110,8 @@ const Schema = z.object({
// Nouvelles listes gérées globalement (IDs)
categories_inv_ids: z.array(z.number().int()).optional().default([]),
secteurs_inv_ids: z.array(z.number().int()).optional().default([]),
// Espaces de travail auxquels ce référentiel est affecté (au moins un requis)
workspace_ids: z.array(z.number().int()).min(1, 'Au moins un espace de travail doit être sélectionné'),
// Notation : tableau de critères
notation: z.array(z.object({
nom: z.string().min(1),
@@ -181,6 +183,25 @@ function attachSecteursInv(rows) {
return rows.map(r => ({ ...r, secteurs_inv: map[r.id] || [] }));
}
/** Espaces de travail auxquels le référentiel est affecté */
function attachWorkspaces(rows) {
if (rows.length === 0) return rows;
const ids = rows.map(r => r.id);
const links = db.prepare(`
SELECT rw.referentiel_id, w.id AS workspace_id, w.slug, w.nom
FROM referentiel_workspaces rw
JOIN workspaces w ON w.id = rw.workspace_id
WHERE rw.referentiel_id IN (${ids.map(() => '?').join(',')})
ORDER BY w.ordre, w.id
`).all(...ids);
const map = {};
for (const l of links) {
if (!map[l.referentiel_id]) map[l.referentiel_id] = [];
map[l.referentiel_id].push({ id: l.workspace_id, slug: l.slug, nom: l.nom });
}
return rows.map(r => ({ ...r, workspaces: map[r.id] || [] }));
}
/** Critères de notation du référentiel */
function attachNotation(rows) {
if (rows.length === 0) return rows;
@@ -213,7 +234,7 @@ function parsePaysOp(rows) {
}
/** Enregistre catégories, secteurs et notation pour un référentiel */
function saveRelations(tx, refId, categories, notation, categoriesInvIds, secteursInvIds) {
function saveRelations(tx, refId, categories, notation, categoriesInvIds, secteursInvIds, workspaceIds) {
tx(() => {
// Legacy string categories
db.prepare('DELETE FROM referentiel_categories WHERE referentiel_id = ?').run(refId);
@@ -238,6 +259,13 @@ function saveRelations(tx, refId, categories, notation, categoriesInvIds, secteu
for (const id of secteursInvIds) ins.run(refId, id);
}
// Espaces de travail (IDs) — au moins un requis, déjà validé par le schéma Zod
db.prepare('DELETE FROM referentiel_workspaces WHERE referentiel_id = ?').run(refId);
if (workspaceIds && workspaceIds.length > 0) {
const ins = db.prepare('INSERT OR IGNORE INTO referentiel_workspaces (referentiel_id, workspace_id) VALUES (?,?)');
for (const id of workspaceIds) ins.run(refId, id);
}
db.prepare('DELETE FROM referentiel_notation WHERE referentiel_id = ?').run(refId);
if (notation.length > 0) {
const ins = db.prepare(`
@@ -267,6 +295,7 @@ router.get('/', (_req, res) => {
rows = attachCats(rows);
rows = attachCatsInv(rows);
rows = attachSecteursInv(rows);
rows = attachWorkspaces(rows);
rows = attachNotation(rows);
rows = parsePaysOp(rows);
res.json(rows);
@@ -288,6 +317,7 @@ router.get('/export', (req, res, next) => {
let rows = db.prepare('SELECT * FROM plateformes_referentiel ORDER BY nom').all();
rows = attachCatsInv(rows);
rows = attachSecteursInv(rows);
rows = attachWorkspaces(rows);
rows = parsePaysOp(rows);
const entries = [];
@@ -353,6 +383,7 @@ router.get('/export', (req, res, next) => {
icone_filename: r.icone_filename,
categories_inv: (r.categories_inv || []).map(c => c.nom),
secteurs_inv: (r.secteurs_inv || []).map(s => s.nom),
workspace_slugs: (r.workspaces || []).map(w => w.slug),
notation: notationByRef[r.id] || [],
}));
@@ -388,7 +419,7 @@ router.get('/:id/export', (req, res, next) => {
try {
const ref = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(req.params.id);
if (!ref) throw new HttpError(404, 'Référentiel introuvable');
const [enriched] = parsePaysOp(attachSecteursInv(attachCatsInv([ref])));
const [enriched] = parsePaysOp(attachWorkspaces(attachSecteursInv(attachCatsInv([ref]))));
const entries = [];
const manifest = {
@@ -450,6 +481,7 @@ router.get('/:id/export', (req, res, next) => {
icone_filename: enriched.icone_filename,
categories_inv: (enriched.categories_inv || []).map(c => c.nom),
secteurs_inv: (enriched.secteurs_inv || []).map(s => s.nom),
workspace_slugs: (enriched.workspaces || []).map(w => w.slug),
notation,
};
@@ -506,6 +538,25 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
return ids;
}
// Résout des slugs de workspaces vers leurs IDs DANS CE catalogue —
// contrairement aux catégories/secteurs, les workspaces sont un
// catalogue géré par l'admin, jamais créés automatiquement à l'import.
// Si aucun slug ne correspond (vieux ZIP sans workspace_slugs, ou
// catalogue cible différent), on retombe sur "crowdlending" pour ne
// jamais importer une entrée référentiel sans aucune affectation.
function resolveWorkspaceIds(slugs) {
const ids = [];
for (const slug of (slugs || [])) {
const row = db.prepare('SELECT id FROM workspaces WHERE slug = ?').get(slug);
if (row) ids.push(row.id);
}
if (ids.length === 0) {
const cl = db.prepare("SELECT id FROM workspaces WHERE slug = 'crowdlending'").get();
if (cl) ids.push(cl.id);
}
return ids;
}
// Build a map of images from the ZIP
const imageMap = {};
for (const e of zipEntries) {
@@ -522,6 +573,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
const catsIds = resolveTagIds(p.categories_inv, 'categories_inv');
const sectsIds = resolveTagIds(p.secteurs_inv, 'secteurs_inv');
const wsIds = resolveWorkspaceIds(p.workspace_slugs);
const paysOp = Array.isArray(p.pays_operation) && p.pays_operation.length ? JSON.stringify(p.pays_operation) : null;
const existing = db.prepare('SELECT id FROM plateformes_referentiel WHERE nom = ?').get(p.nom);
@@ -590,6 +642,11 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
for (const id of sectsIds) {
db.prepare('INSERT OR IGNORE INTO referentiel_secteurs_inv (referentiel_id, secteur_id) VALUES (?,?)').run(refId, id);
}
// Sync referentiel_workspaces
db.prepare('DELETE FROM referentiel_workspaces WHERE referentiel_id = ?').run(refId);
for (const id of wsIds) {
db.prepare('INSERT OR IGNORE INTO referentiel_workspaces (referentiel_id, workspace_id) VALUES (?,?)').run(refId, id);
}
// Sync referentiel_notation
if (Array.isArray(p.notation) && p.notation.length) {
@@ -659,7 +716,7 @@ router.get('/:id', (req, res, next) => {
try {
const row = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(req.params.id);
if (!row) throw new HttpError(404, 'Référentiel introuvable');
const [enriched] = parsePaysOp(attachNotation(attachSecteursInv(attachCatsInv(attachCats([row])))));
const [enriched] = parsePaysOp(attachWorkspaces(attachNotation(attachSecteursInv(attachCatsInv(attachCats([row]))))));
res.json(enriched);
} catch (e) { next(e); }
});
@@ -701,10 +758,10 @@ router.post('/', (req, res, next) => {
const refId = r.lastInsertRowid;
const tx = db.transaction(fn => fn());
saveRelations(tx, refId, data.categories, data.notation, data.categories_inv_ids, data.secteurs_inv_ids);
saveRelations(tx, refId, data.categories, data.notation, data.categories_inv_ids, data.secteurs_inv_ids, data.workspace_ids);
const row = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(refId);
const [enriched] = parsePaysOp(attachNotation(attachSecteursInv(attachCatsInv(attachCats([row])))));
const [enriched] = parsePaysOp(attachWorkspaces(attachNotation(attachSecteursInv(attachCatsInv(attachCats([row]))))));
enriched.nb_plateformes_liees = 0;
res.status(201).json(enriched);
} catch (e) { next(e); }
@@ -755,11 +812,11 @@ router.put('/:id', (req, res, next) => {
);
const tx = db.transaction(fn => fn());
saveRelations(tx, row.id, data.categories, data.notation, data.categories_inv_ids, data.secteurs_inv_ids);
saveRelations(tx, row.id, data.categories, data.notation, data.categories_inv_ids, data.secteurs_inv_ids, data.workspace_ids);
const updated = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(row.id);
const nb = db.prepare('SELECT COUNT(*) AS n FROM plateformes WHERE referentiel_id = ?').get(row.id).n;
const [enriched] = parsePaysOp(attachNotation(attachSecteursInv(attachCatsInv(attachCats([updated])))));
const [enriched] = parsePaysOp(attachWorkspaces(attachNotation(attachSecteursInv(attachCatsInv(attachCats([updated]))))));
enriched.nb_plateformes_liees = nb;
res.json(enriched);
} catch (e) { next(e); }