maj
This commit is contained in:
1 parent
1e3994f17f
commit
624d32ff7c
17 files changed
+2192
-1
No files matched your search
@@ -0,0 +1,174 @@
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import db from '../db/index.js';
|
||||
import { HttpError } from '../middleware/errorHandler.js';
|
||||
|
||||
/**
|
||||
* routes/enveloppesReferentiel.js — Référentiel admin des enveloppes d'épargne/investissement
|
||||
* (Livret A, Assurance-vie, PEA, PER...) — chantier "Référentiel Enveloppes & Supports
|
||||
* d'épargne" (28/09/26, demande Olivier).
|
||||
*
|
||||
* Même esprit que institutionsReferentiel.js : catalogue transverse à tous les espaces de
|
||||
* travail, catégories (familles) en N-N via referentiel_categories_enveloppes. S'y ajoute une
|
||||
* seconde relation N-N propre à ce référentiel : enveloppes_supports_disponibles, qui indique
|
||||
* quels supports d'investissement (supports_referentiel — fonds euros, ETF, SCPI...) sont
|
||||
* disponibles dans chaque enveloppe (ex. Assurance-vie multisupport ↔ fonds euros, ETF, SCPI...).
|
||||
* Une enveloppe "cash" (Livret A, Compte courant...) n'a simplement aucune ligne dans cette
|
||||
* seconde relation.
|
||||
*
|
||||
* Catalogue seul pour l'instant (cadrage validé par Olivier via AskUserQuestion le 28/09/26) —
|
||||
* aucun rattachement à l'existant (comptes, investissements...), en préparation du futur module
|
||||
* patrimoine (cf. plan de déploiement, doc projet).
|
||||
*/
|
||||
|
||||
const router = Router();
|
||||
// Toutes ces routes sont montées sous requireAdmin dans server.js
|
||||
|
||||
const Schema = z.object({
|
||||
nom: z.string().min(1).max(200),
|
||||
description: z.string().nullable().optional(),
|
||||
plafond_eur: z.number().positive().nullable().optional(),
|
||||
regime_fiscal: z.string().nullable().optional(),
|
||||
categories_ids: z.array(z.number().int()).optional().default([]),
|
||||
supports_ids: z.array(z.number().int()).optional().default([]),
|
||||
});
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Catégories (familles) attachées à chaque enveloppe */
|
||||
function attachCategories(rows) {
|
||||
if (rows.length === 0) return rows;
|
||||
const ids = rows.map(r => r.id);
|
||||
const links = db.prepare(`
|
||||
SELECT rc.enveloppe_id, c.id AS categorie_id, c.nom
|
||||
FROM referentiel_categories_enveloppes rc
|
||||
JOIN categories_enveloppes c ON c.id = rc.categorie_id
|
||||
WHERE rc.enveloppe_id IN (${ids.map(() => '?').join(',')})
|
||||
ORDER BY c.nom
|
||||
`).all(...ids);
|
||||
const map = {};
|
||||
for (const l of links) {
|
||||
if (!map[l.enveloppe_id]) map[l.enveloppe_id] = [];
|
||||
map[l.enveloppe_id].push({ id: l.categorie_id, nom: l.nom });
|
||||
}
|
||||
return rows.map(r => ({ ...r, categories: map[r.id] || [] }));
|
||||
}
|
||||
|
||||
/** Supports disponibles attachés à chaque enveloppe */
|
||||
function attachSupports(rows) {
|
||||
if (rows.length === 0) return rows;
|
||||
const ids = rows.map(r => r.id);
|
||||
const links = db.prepare(`
|
||||
SELECT esd.enveloppe_id, s.id AS support_id, s.nom
|
||||
FROM enveloppes_supports_disponibles esd
|
||||
JOIN supports_referentiel s ON s.id = esd.support_id
|
||||
WHERE esd.enveloppe_id IN (${ids.map(() => '?').join(',')})
|
||||
ORDER BY s.nom
|
||||
`).all(...ids);
|
||||
const map = {};
|
||||
for (const l of links) {
|
||||
if (!map[l.enveloppe_id]) map[l.enveloppe_id] = [];
|
||||
map[l.enveloppe_id].push({ id: l.support_id, nom: l.nom });
|
||||
}
|
||||
return rows.map(r => ({ ...r, supports: map[r.id] || [] }));
|
||||
}
|
||||
|
||||
function saveCategories(enveloppeId, categoriesIds) {
|
||||
db.prepare('DELETE FROM referentiel_categories_enveloppes WHERE enveloppe_id = ?').run(enveloppeId);
|
||||
if (categoriesIds && categoriesIds.length > 0) {
|
||||
const ins = db.prepare('INSERT OR IGNORE INTO referentiel_categories_enveloppes (enveloppe_id, categorie_id) VALUES (?,?)');
|
||||
for (const id of categoriesIds) ins.run(enveloppeId, id);
|
||||
}
|
||||
}
|
||||
|
||||
function saveSupports(enveloppeId, supportsIds) {
|
||||
db.prepare('DELETE FROM enveloppes_supports_disponibles WHERE enveloppe_id = ?').run(enveloppeId);
|
||||
if (supportsIds && supportsIds.length > 0) {
|
||||
const ins = db.prepare('INSERT OR IGNORE INTO enveloppes_supports_disponibles (enveloppe_id, support_id) VALUES (?,?)');
|
||||
for (const id of supportsIds) ins.run(enveloppeId, id);
|
||||
}
|
||||
}
|
||||
|
||||
// ── GET /api/enveloppes-referentiel ─────────────────────────────────────────
|
||||
router.get('/', (_req, res) => {
|
||||
let rows = db.prepare('SELECT * FROM enveloppes_referentiel ORDER BY nom').all();
|
||||
rows = attachSupports(attachCategories(rows));
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// ── GET /api/enveloppes-referentiel/:id ─────────────────────────────────────
|
||||
router.get('/:id', (req, res, next) => {
|
||||
try {
|
||||
const row = db.prepare('SELECT * FROM enveloppes_referentiel WHERE id = ?').get(req.params.id);
|
||||
if (!row) throw new HttpError(404, 'Enveloppe introuvable');
|
||||
const [enriched] = attachSupports(attachCategories([row]));
|
||||
res.json(enriched);
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── POST /api/enveloppes-referentiel ────────────────────────────────────────
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const data = Schema.parse(req.body);
|
||||
const existing = db.prepare('SELECT id FROM enveloppes_referentiel WHERE nom = ?').get(data.nom);
|
||||
if (existing) throw new HttpError(409, `Une enveloppe "${data.nom}" existe déjà`);
|
||||
|
||||
const r = db.prepare(`
|
||||
INSERT INTO enveloppes_referentiel (nom, description, plafond_eur, regime_fiscal, updated_at)
|
||||
VALUES (?,?,?,?, datetime('now'))
|
||||
`).run(data.nom, data.description || null, data.plafond_eur ?? null, data.regime_fiscal || null);
|
||||
|
||||
const enveloppeId = r.lastInsertRowid;
|
||||
db.transaction(() => {
|
||||
saveCategories(enveloppeId, data.categories_ids);
|
||||
saveSupports(enveloppeId, data.supports_ids);
|
||||
})();
|
||||
|
||||
const row = db.prepare('SELECT * FROM enveloppes_referentiel WHERE id = ?').get(enveloppeId);
|
||||
const [enriched] = attachSupports(attachCategories([row]));
|
||||
res.status(201).json(enriched);
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── PUT /api/enveloppes-referentiel/:id ─────────────────────────────────────
|
||||
router.put('/:id', (req, res, next) => {
|
||||
try {
|
||||
const row = db.prepare('SELECT * FROM enveloppes_referentiel WHERE id = ?').get(req.params.id);
|
||||
if (!row) throw new HttpError(404, 'Enveloppe introuvable');
|
||||
|
||||
const data = Schema.parse(req.body);
|
||||
if (data.nom !== row.nom) {
|
||||
const dup = db.prepare('SELECT id FROM enveloppes_referentiel WHERE nom = ? AND id != ?').get(data.nom, row.id);
|
||||
if (dup) throw new HttpError(409, `Une enveloppe "${data.nom}" existe déjà`);
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE enveloppes_referentiel
|
||||
SET nom=?, description=?, plafond_eur=?, regime_fiscal=?, updated_at=datetime('now')
|
||||
WHERE id=?
|
||||
`).run(data.nom, data.description || null, data.plafond_eur ?? null, data.regime_fiscal || null, row.id);
|
||||
|
||||
db.transaction(() => {
|
||||
saveCategories(row.id, data.categories_ids);
|
||||
saveSupports(row.id, data.supports_ids);
|
||||
})();
|
||||
|
||||
const updated = db.prepare('SELECT * FROM enveloppes_referentiel WHERE id = ?').get(row.id);
|
||||
const [enriched] = attachSupports(attachCategories([updated]));
|
||||
res.json(enriched);
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── DELETE /api/enveloppes-referentiel/:id ──────────────────────────────────
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
const row = db.prepare('SELECT id FROM enveloppes_referentiel WHERE id = ?').get(req.params.id);
|
||||
if (!row) throw new HttpError(404, 'Enveloppe introuvable');
|
||||
// referentiel_categories_enveloppes et enveloppes_supports_disponibles ont toutes deux
|
||||
// ON DELETE CASCADE sur enveloppe_id : aucune purge manuelle nécessaire.
|
||||
db.prepare('DELETE FROM enveloppes_referentiel WHERE id = ?').run(row.id);
|
||||
res.json({ deleted: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import db from '../db/index.js';
|
||||
import { HttpError } from '../middleware/errorHandler.js';
|
||||
|
||||
/**
|
||||
* routes/ref-categories-enveloppes.js — liste globale des catégories (familles) du référentiel
|
||||
* "Enveloppes & Supports d'épargne" (Comptes courants, Épargne réglementée, Assurance-vie...).
|
||||
* Même patron que routes/ref-categories-institutions.js (categories_institutions), table dédiée
|
||||
* (categories_enveloppes) — cf. enveloppesReferentiel.js.
|
||||
*/
|
||||
|
||||
const router = Router();
|
||||
// Toutes ces routes sont montées sous requireAdmin dans server.js
|
||||
|
||||
/* GET /api/ref-categories-enveloppes — liste complète */
|
||||
router.get('/', (_req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT c.id, c.nom,
|
||||
COUNT(rc.enveloppe_id) AS nb_utilises
|
||||
FROM categories_enveloppes c
|
||||
LEFT JOIN referentiel_categories_enveloppes rc ON rc.categorie_id = c.id
|
||||
GROUP BY c.id
|
||||
ORDER BY c.nom
|
||||
`).all();
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
/* POST /api/ref-categories-enveloppes — créer */
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const { nom } = z.object({ nom: z.string().min(1).max(200) }).parse(req.body);
|
||||
const exists = db.prepare('SELECT id FROM categories_enveloppes WHERE nom = ?').get(nom.trim());
|
||||
if (exists) throw new HttpError(409, `La catégorie "${nom.trim()}" existe déjà.`);
|
||||
const r = db.prepare('INSERT INTO categories_enveloppes (nom) VALUES (?)').run(nom.trim());
|
||||
res.status(201).json({ id: r.lastInsertRowid, nom: nom.trim(), nb_utilises: 0 });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/* PUT /api/ref-categories-enveloppes/:id — renommer (propage automatiquement) */
|
||||
router.put('/:id', (req, res, next) => {
|
||||
try {
|
||||
const { nom } = z.object({ nom: z.string().min(1).max(200) }).parse(req.body);
|
||||
const row = db.prepare('SELECT id, nom FROM categories_enveloppes WHERE id = ?').get(req.params.id);
|
||||
if (!row) throw new HttpError(404, 'Catégorie introuvable');
|
||||
const dup = db.prepare('SELECT id FROM categories_enveloppes WHERE nom = ? AND id != ?').get(nom.trim(), row.id);
|
||||
if (dup) throw new HttpError(409, `La catégorie "${nom.trim()}" existe déjà.`);
|
||||
db.prepare('UPDATE categories_enveloppes SET nom = ? WHERE id = ?').run(nom.trim(), row.id);
|
||||
const nb = db.prepare('SELECT COUNT(*) AS n FROM referentiel_categories_enveloppes WHERE categorie_id = ?').get(row.id).n;
|
||||
res.json({ id: row.id, nom: nom.trim(), nb_utilises: nb });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/* DELETE /api/ref-categories-enveloppes/:id — supprimer si non utilisée */
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
const row = db.prepare('SELECT id FROM categories_enveloppes WHERE id = ?').get(req.params.id);
|
||||
if (!row) throw new HttpError(404, 'Catégorie introuvable');
|
||||
const nb = db.prepare('SELECT COUNT(*) AS n FROM referentiel_categories_enveloppes WHERE categorie_id = ?').get(row.id).n;
|
||||
if (nb > 0) throw new HttpError(409, `Catégorie utilisée par ${nb} enveloppe(s) — retirez-la d'abord.`);
|
||||
db.prepare('DELETE FROM categories_enveloppes WHERE id = ?').run(row.id);
|
||||
res.status(204).end();
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/* POST /api/ref-categories-enveloppes/:id/merge — fusionner dans une autre catégorie */
|
||||
router.post('/:id/merge', (req, res, next) => {
|
||||
try {
|
||||
const { target_id } = z.object({ target_id: z.number().int() }).parse(req.body);
|
||||
const source = db.prepare('SELECT id, nom FROM categories_enveloppes WHERE id = ?').get(req.params.id);
|
||||
if (!source) throw new HttpError(404, 'Catégorie source introuvable');
|
||||
const target = db.prepare('SELECT id, nom FROM categories_enveloppes WHERE id = ?').get(target_id);
|
||||
if (!target) throw new HttpError(404, 'Catégorie cible introuvable');
|
||||
if (source.id === target.id) throw new HttpError(400, 'Source et cible identiques');
|
||||
|
||||
const result = db.transaction(() => {
|
||||
const refs = db.prepare(
|
||||
'SELECT enveloppe_id FROM referentiel_categories_enveloppes WHERE categorie_id = ?'
|
||||
).all(source.id);
|
||||
let added = 0, skipped = 0;
|
||||
for (const { enveloppe_id } of refs) {
|
||||
const exists = db.prepare(
|
||||
'SELECT 1 FROM referentiel_categories_enveloppes WHERE enveloppe_id = ? AND categorie_id = ?'
|
||||
).get(enveloppe_id, target.id);
|
||||
if (!exists) {
|
||||
db.prepare(
|
||||
'INSERT INTO referentiel_categories_enveloppes (enveloppe_id, categorie_id) VALUES (?, ?)'
|
||||
).run(enveloppe_id, target.id);
|
||||
added++;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
db.prepare('DELETE FROM referentiel_categories_enveloppes WHERE categorie_id = ?').run(source.id);
|
||||
db.prepare('DELETE FROM categories_enveloppes WHERE id = ?').run(source.id);
|
||||
return { added, skipped, total: refs.length };
|
||||
})();
|
||||
|
||||
res.json({ ok: true, ...result,
|
||||
message: `Fusion terminée : ${result.added} lien(s) ajouté(s), ${result.skipped} déjà présent(s). "${source.nom}" supprimée.` });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import db from '../db/index.js';
|
||||
import { HttpError } from '../middleware/errorHandler.js';
|
||||
|
||||
/**
|
||||
* routes/supportsReferentiel.js — Référentiel admin des supports d'investissement (fonds
|
||||
* euros, ETF, SCPI, titres vifs...) — chantier "Référentiel Enveloppes & Supports d'épargne"
|
||||
* (28/09/26, demande Olivier).
|
||||
*
|
||||
* Catalogue à plat (pas de sous-catégories — une dizaine d'entrées seulement, cf. "Types de
|
||||
* supports (UC) disponibles" dans la liste fournie par Olivier), transverse à tous les espaces
|
||||
* de travail comme les institutions financières. Consommé par enveloppesReferentiel.js pour la
|
||||
* relation N-N enveloppe ↔ supports disponibles (enveloppes_supports_disponibles).
|
||||
*
|
||||
* Catalogue seul pour l'instant (cadrage validé par Olivier via AskUserQuestion le 28/09/26) —
|
||||
* aucun rattachement à l'existant (comptes, investissements...), en préparation du futur module
|
||||
* patrimoine (cf. plan de déploiement, doc projet).
|
||||
*/
|
||||
|
||||
const router = Router();
|
||||
// Toutes ces routes sont montées sous requireAdmin dans server.js
|
||||
|
||||
const Schema = z.object({
|
||||
nom: z.string().min(1).max(200),
|
||||
description: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
// ── GET /api/supports-referentiel ───────────────────────────────────────────
|
||||
router.get('/', (_req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT s.*, COUNT(esd.enveloppe_id) AS nb_enveloppes
|
||||
FROM supports_referentiel s
|
||||
LEFT JOIN enveloppes_supports_disponibles esd ON esd.support_id = s.id
|
||||
GROUP BY s.id
|
||||
ORDER BY s.nom
|
||||
`).all();
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// ── GET /api/supports-referentiel/:id ───────────────────────────────────────
|
||||
router.get('/:id', (req, res, next) => {
|
||||
try {
|
||||
const row = db.prepare('SELECT * FROM supports_referentiel WHERE id = ?').get(req.params.id);
|
||||
if (!row) throw new HttpError(404, 'Support introuvable');
|
||||
res.json(row);
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── POST /api/supports-referentiel ──────────────────────────────────────────
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const data = Schema.parse(req.body);
|
||||
const existing = db.prepare('SELECT id FROM supports_referentiel WHERE nom = ?').get(data.nom);
|
||||
if (existing) throw new HttpError(409, `Un support "${data.nom}" existe déjà`);
|
||||
|
||||
const r = db.prepare(`
|
||||
INSERT INTO supports_referentiel (nom, description, updated_at)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
`).run(data.nom, data.description || null);
|
||||
|
||||
const row = db.prepare('SELECT * FROM supports_referentiel WHERE id = ?').get(r.lastInsertRowid);
|
||||
res.status(201).json(row);
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── PUT /api/supports-referentiel/:id ───────────────────────────────────────
|
||||
router.put('/:id', (req, res, next) => {
|
||||
try {
|
||||
const row = db.prepare('SELECT * FROM supports_referentiel WHERE id = ?').get(req.params.id);
|
||||
if (!row) throw new HttpError(404, 'Support introuvable');
|
||||
|
||||
const data = Schema.parse(req.body);
|
||||
if (data.nom !== row.nom) {
|
||||
const dup = db.prepare('SELECT id FROM supports_referentiel WHERE nom = ? AND id != ?').get(data.nom, row.id);
|
||||
if (dup) throw new HttpError(409, `Un support "${data.nom}" existe déjà`);
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE supports_referentiel
|
||||
SET nom=?, description=?, updated_at=datetime('now')
|
||||
WHERE id=?
|
||||
`).run(data.nom, data.description || null, row.id);
|
||||
|
||||
const updated = db.prepare('SELECT * FROM supports_referentiel WHERE id = ?').get(row.id);
|
||||
res.json(updated);
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── DELETE /api/supports-referentiel/:id ────────────────────────────────────
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
const row = db.prepare('SELECT id FROM supports_referentiel WHERE id = ?').get(req.params.id);
|
||||
if (!row) throw new HttpError(404, 'Support introuvable');
|
||||
// enveloppes_supports_disponibles a ON DELETE CASCADE sur support_id : les liens sont
|
||||
// nettoyés automatiquement, aucune purge manuelle nécessaire ici.
|
||||
db.prepare('DELETE FROM supports_referentiel WHERE id = ?').run(row.id);
|
||||
res.json({ deleted: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in new issue
Block a user