From 1dcb33aa7418ad16f6815ace7cf8e409d19068f2 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 26 Sep 2026 17:59:08 +0200 Subject: [PATCH] =?UTF-8?q?Ref=C3=A9rentiel=20des=20institutions=20financi?= =?UTF-8?q?=C3=A8res?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/db/index.js | 133 ++++ backend/src/routes/institutionsReferentiel.js | 284 +++++++ .../src/routes/ref-categories-institutions.js | 104 +++ backend/src/server.js | 6 + .../logos institution/BNP Paribas - Logo.webp | Bin 0 -> 2128 bytes .../Banque Populaire - Logo.webp | Bin 0 -> 3420 bytes .../logos institution/BoursoBank - Logo.gif | Bin 0 -> 1575 bytes design/logos institution/CCF - Logo.webp | Bin 0 -> 1568 bytes design/logos institution/CIC Logo.webp | Bin 0 -> 1236 bytes .../Caisse d'epargne - Logo.webp | Bin 0 -> 1860 bytes .../Credit agricole -Logo.gif | Bin 0 -> 2509 bytes .../Crédit Mutuel - Logo.gif | Bin 0 -> 1387 bytes design/logos institution/Fortuneo - Logo.webp | Bin 0 -> 2562 bytes design/logos institution/LCL - Logo.webp | Bin 0 -> 1414 bytes .../La banque postale - Logo.webp | Bin 0 -> 2834 bytes .../Société Général Logo.webp | Bin 0 -> 1062 bytes frontend/src/App.jsx | 2 + frontend/src/pages/Admin.jsx | 2 + frontend/src/pages/AdminInstitutions.jsx | 734 ++++++++++++++++++ 19 files changed, 1265 insertions(+) create mode 100644 backend/src/routes/institutionsReferentiel.js create mode 100644 backend/src/routes/ref-categories-institutions.js create mode 100644 design/logos institution/BNP Paribas - Logo.webp create mode 100644 design/logos institution/Banque Populaire - Logo.webp create mode 100644 design/logos institution/BoursoBank - Logo.gif create mode 100644 design/logos institution/CCF - Logo.webp create mode 100644 design/logos institution/CIC Logo.webp create mode 100644 design/logos institution/Caisse d'epargne - Logo.webp create mode 100644 design/logos institution/Credit agricole -Logo.gif create mode 100644 design/logos institution/Crédit Mutuel - Logo.gif create mode 100644 design/logos institution/Fortuneo - Logo.webp create mode 100644 design/logos institution/LCL - Logo.webp create mode 100644 design/logos institution/La banque postale - Logo.webp create mode 100644 design/logos institution/Société Général Logo.webp create mode 100644 frontend/src/pages/AdminInstitutions.jsx diff --git a/backend/src/db/index.js b/backend/src/db/index.js index e2d670f..116fc9a 100644 --- a/backend/src/db/index.js +++ b/backend/src/db/index.js @@ -3524,4 +3524,137 @@ db.exec(` `); db.exec('CREATE INDEX IF NOT EXISTS idx_pe_pertes_inv ON investissement_pe_pertes(investissement_pe_id)'); +// ── Chantier "Référentiel Institutions financières" (26/09/26, demande Olivier) ────────── +// Catalogue admin de banques/courtiers/assureurs, sur le modèle de plateformes_referentiel +// (cf. project_referentiel_plateformes.md) mais SANS les champs spécifiques aux plateformes +// d'investissement (fiscalité, produit fiscal, frais...) et SANS rattachement workspace : une +// institution financière est transverse à tous les espaces de travail (décision d'Olivier, +// AskUserQuestion 26/09/26). Phase 1 uniquement : catalogue admin (logos/icônes/catégories). +// Le rattachement à comptes.banque (Phase 2) fera l'objet d'un chantier séparé — cf. plan de +// déploiement (doc projet claude/plan_referentiel_institutions_financieres.md). +db.exec(` + CREATE TABLE IF NOT EXISTS institutions_referentiel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nom TEXT NOT NULL UNIQUE, + url TEXT, + domiciliation TEXT NOT NULL DEFAULT 'FR', + description TEXT, + logo_filename TEXT, + icone_filename TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ) +`); +db.exec('CREATE INDEX IF NOT EXISTS idx_instref_nom ON institutions_referentiel(nom)'); + +// Catégories (axe unique "type d'établissement"), gérées par l'admin — même patron que +// categories_inv/referentiel_categories_inv (cf. plus haut) mais table dédiée : une catégorie +// d'établissement financier (Banque, Assureur...) n'a pas de sens pour une plateforme +// crowdlending/PE, on évite donc de mélanger les deux taxonomies. +db.exec(` + CREATE TABLE IF NOT EXISTS categories_institutions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nom TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ) +`); +db.exec(` + CREATE TABLE IF NOT EXISTS referentiel_categories_institutions ( + institution_id INTEGER NOT NULL REFERENCES institutions_referentiel(id) ON DELETE CASCADE, + categorie_id INTEGER NOT NULL REFERENCES categories_institutions(id) ON DELETE CASCADE, + PRIMARY KEY (institution_id, categorie_id) + ) +`); + +// Seed initial des catégories (idempotent — n'insère que si la table est vide ; l'admin reste +// libre de renommer/supprimer ensuite depuis l'écran dédié). +{ + const catInstCount = db.prepare('SELECT COUNT(*) AS n FROM categories_institutions').get().n; + if (catInstCount === 0) { + const insCatInst = db.prepare('INSERT OR IGNORE INTO categories_institutions (nom) VALUES (?)'); + db.transaction(() => { + for (const nom of ['Banque', 'Néobanque', 'Courtier', 'Assureur', 'Société de gestion', 'Établissement de paiement', 'Autre']) { + insCatInst.run(nom); + } + })(); + } +} + + +// Seed initial des institutions financières (proposition validée par Olivier le 26/09/26, +// via ajustements sur la liste par défaut) — idempotent, n'insère que si la table est vide. +// Chaque institution peut porter plusieurs catégories (ex. Trade Republic : Néobanque + Courtier). +{ + const instRefCount = db.prepare('SELECT COUNT(*) AS n FROM institutions_referentiel').get().n; + if (instRefCount === 0) { + const SEED_INSTITUTIONS = [ + { nom: 'BNP Paribas', categories: ['Banque'] }, + { nom: 'Société Générale', categories: ['Banque'] }, + { nom: 'Crédit Agricole', categories: ['Banque'] }, + { nom: 'Banque Populaire', categories: ['Banque'] }, + { nom: "Caisse d'Épargne", categories: ['Banque'] }, + { nom: 'Crédit Mutuel', categories: ['Banque'] }, + { nom: 'CIC', categories: ['Banque'] }, + { nom: 'La Banque Postale', categories: ['Banque'] }, + { nom: 'LCL', categories: ['Banque'] }, + { nom: 'HSBC Continental Europe (CCF)', categories: ['Banque'] }, + { nom: 'Boursorama Banque', categories: ['Néobanque'] }, + { nom: 'Hello bank!', categories: ['Néobanque'] }, + { nom: 'Fortuneo', categories: ['Néobanque'] }, + { nom: 'BforBank', categories: ['Néobanque'] }, + { nom: 'Monabanq', categories: ['Néobanque'] }, + { nom: 'N26', categories: ['Néobanque'] }, + { nom: 'Revolut', categories: ['Néobanque'] }, + { nom: 'Qonto', categories: ['Néobanque'] }, + { nom: 'Trade Republic', categories: ['Néobanque', 'Courtier'] }, + { nom: 'Saxo Banque France', categories: ['Néobanque'] }, + { nom: 'Wise', categories: ['Néobanque'] }, + { nom: 'Bourse Direct', categories: ['Courtier'] }, + { nom: 'DEGIRO', categories: ['Courtier'] }, + { nom: 'Interactive Brokers', categories: ['Courtier'] }, + { nom: 'EasyBourse', categories: ['Courtier'] }, + { nom: 'AXA France', categories: ['Assureur'] }, + { nom: 'Allianz France', categories: ['Assureur'] }, + { nom: 'Generali France', categories: ['Assureur'] }, + { nom: 'CNP Assurances', categories: ['Assureur'] }, + { nom: 'MAIF', categories: ['Assureur'] }, + { nom: 'MACIF', categories: ['Assureur'] }, + { nom: 'Groupama', categories: ['Assureur'] }, + { nom: 'Crédit Agricole Assurances (Predica)', categories: ['Assureur'] }, + { nom: 'BNP Paribas Cardif', categories: ['Assureur'] }, + { nom: 'Société Générale Assurances (Sogecap)', categories: ['Assureur'] }, + { nom: 'Swiss Life France', categories: ['Assureur'] }, + { nom: 'Abeille Assurances (ex-Aviva France)', categories: ['Assureur'] }, + { nom: 'Suravenir (Crédit Mutuel Arkéa)', categories: ['Assureur'] }, + { nom: 'Spirica', categories: ['Assureur'] }, + { nom: 'Amundi', categories: ['Société de gestion'] }, + { nom: 'Natixis Investment Managers', categories: ['Société de gestion'] }, + { nom: 'BNP Paribas Asset Management', categories: ['Société de gestion'] }, + { nom: 'Lazard Frères Gestion', categories: ['Société de gestion'] }, + { nom: 'Carmignac', categories: ['Société de gestion'] }, + { nom: 'Tikehau Capital', categories: ['Société de gestion'] }, + { nom: "La Financière de l'Échiquier", categories: ['Société de gestion'] }, + { nom: 'Eiffel Investment Group', categories: ['Société de gestion'] }, + { nom: 'Lydia (Sumeria)', categories: ['Établissement de paiement'] }, + { nom: 'PayPal', categories: ['Établissement de paiement'] }, + { nom: 'Nickel', categories: ['Établissement de paiement'] }, + ]; + const insInstRef = db.prepare('INSERT OR IGNORE INTO institutions_referentiel (nom) VALUES (?)'); + const getInstRefId = db.prepare('SELECT id FROM institutions_referentiel WHERE nom = ?'); + const getCatInstId = db.prepare('SELECT id FROM categories_institutions WHERE nom = ?'); + const linkInstCat = db.prepare('INSERT OR IGNORE INTO referentiel_categories_institutions (institution_id, categorie_id) VALUES (?, ?)'); + db.transaction(() => { + for (const { nom, categories } of SEED_INSTITUTIONS) { + insInstRef.run(nom); + const inst = getInstRefId.get(nom); + if (!inst) continue; + for (const catNom of categories) { + const cat = getCatInstId.get(catNom); + if (cat) linkInstCat.run(inst.id, cat.id); + } + } + })(); + } +} + export default db; diff --git a/backend/src/routes/institutionsReferentiel.js b/backend/src/routes/institutionsReferentiel.js new file mode 100644 index 0000000..03086a4 --- /dev/null +++ b/backend/src/routes/institutionsReferentiel.js @@ -0,0 +1,284 @@ +import { Router } from 'express'; +import { z } from 'zod'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import sharp from 'sharp'; +import db from '../db/index.js'; +import { HttpError } from '../middleware/errorHandler.js'; + +/** + * routes/institutionsReferentiel.js — Référentiel admin des institutions financières + * (banques, courtiers, assureurs...) — chantier "Référentiel Institutions financières" + * (26/09/26, demande Olivier). + * + * Phase 1 uniquement (cf. plan de déploiement, doc projet) : catalogue admin (logo, icône, + * catégories) — même patron que routes/referentiel.js (plateformes_referentiel), mais SANS les + * champs spécifiques aux plateformes d'investissement (fiscalité, produit fiscal, frais, + * méthode de remboursement...) et SANS rattachement workspace : une institution financière est + * transverse à tous les espaces de travail (décision d'Olivier, AskUserQuestion 26/09/26). + * Aucun consommateur applicatif pour l'instant (Phase 2, plus tard : comptes.banque) — donc pas + * de mécanique de "poussée" vers des enregistrements liés, contrairement à referentiel.js. + * + * Le dossier de stockage des images est partagé avec le référentiel des plateformes + * (/logos/) plutôt qu'un sous-dossier dédié — même volumétrie modeste, noms de + * fichiers déjà uniques (préfixe inst_ + id + timestamp) donc aucun risque de collision. + */ + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const dataDir = process.env.DATA_DIR + ? path.resolve(process.env.DATA_DIR) + : path.resolve(__dirname, '../../../data'); // fallback dev : 3 niveaux au-dessus de routes/ +const logosDir = path.join(dataDir, 'logos'); +fs.mkdirSync(logosDir, { recursive: true }); + +// ── Traitement image (dupliqué depuis referentiel.js/plateformes.js — même précédent +// déjà en place dans ce fichier : "réutilisé depuis plateformes.js" y est aussi une +// duplication volontaire plutôt qu'un module partagé) ────────────────────────────── +async function processLogoFile(filePath) { + const ext = path.extname(filePath).toLowerCase(); + if (ext === '.svg') { + try { + let svg = fs.readFileSync(filePath, 'utf8'); + svg = svg.replace(/(]*)\sstyle="([^"]*)"/i, (_, tag, style) => { + const cleaned = style.split(';').filter(s => !/^\s*background(-color)?\s*:/i.test(s)).join(';').replace(/^;+|;+$/g, ''); + return cleaned ? `${tag} style="${cleaned}"` : tag; + }); + svg = svg.replace(/\s+enable-background="[^"]*"/gi, ''); + fs.writeFileSync(filePath, svg, 'utf8'); + } catch (e) { console.warn('[inst-logos] SVG cleanup failed:', e.message); } + return filePath; + } + if (['.png', '.jpg', '.jpeg', '.webp'].includes(ext)) { + const pngPath = filePath.replace(/\.(jpg|jpeg|webp|png)$/i, '.png'); + try { + const { data, info } = await sharp(filePath).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); + for (let i = 0; i < data.length; i += 4) { + if (data[i] > 240 && data[i + 1] > 240 && data[i + 2] > 240) data[i + 3] = 0; + } + await sharp(data, { raw: { width: info.width, height: info.height, channels: 4 } }) + .png({ compressionLevel: 8 }).toFile(pngPath); + if (pngPath !== filePath) fs.unlinkSync(filePath); + return pngPath; + } catch (e) { console.warn('[inst-logos] sharp failed:', e.message); return filePath; } + } + return filePath; +} + +// ── Multer ────────────────────────────────────────────────────────────────── +const storage = multer.diskStorage({ + destination: (_req, _file, cb) => cb(null, logosDir), + filename: (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase() || '.png'; + cb(null, `inst_${req.params.id}_${Date.now()}${ext}`); + }, +}); +const upload = multer({ + storage, + limits: { fileSize: 2 * 1024 * 1024 }, + fileFilter: (_req, file, cb) => { + const allowed = ['.svg', '.png', '.jpg', '.jpeg', '.webp']; + if (allowed.includes(path.extname(file.originalname).toLowerCase())) cb(null, true); + else cb(new Error('Format non supporté — SVG, PNG, JPG ou WebP uniquement')); + }, +}); + +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), + url: z.string().url().optional().or(z.literal('')).nullable(), + domiciliation: z.string().min(1).max(10).default('FR'), + description: z.string().nullable().optional(), + categories_ids: z.array(z.number().int()).optional().default([]), +}); + +// ── Helpers ──────────────────────────────────────────────────────────────── + +/** Catégories (type d'établissement) attachées à chaque entrée */ +function attachCategories(rows) { + if (rows.length === 0) return rows; + const ids = rows.map(r => r.id); + const links = db.prepare(` + SELECT rc.institution_id, c.id AS categorie_id, c.nom + FROM referentiel_categories_institutions rc + JOIN categories_institutions c ON c.id = rc.categorie_id + WHERE rc.institution_id IN (${ids.map(() => '?').join(',')}) + ORDER BY c.nom + `).all(...ids); + const map = {}; + for (const l of links) { + if (!map[l.institution_id]) map[l.institution_id] = []; + map[l.institution_id].push({ id: l.categorie_id, nom: l.nom }); + } + return rows.map(r => ({ ...r, categories: map[r.id] || [] })); +} + +function saveCategories(institutionId, categoriesIds) { + db.prepare('DELETE FROM referentiel_categories_institutions WHERE institution_id = ?').run(institutionId); + if (categoriesIds && categoriesIds.length > 0) { + const ins = db.prepare('INSERT OR IGNORE INTO referentiel_categories_institutions (institution_id, categorie_id) VALUES (?,?)'); + for (const id of categoriesIds) ins.run(institutionId, id); + } +} + +// ── GET /api/institutions-referentiel ─────────────────────────────────────── +router.get('/', (_req, res) => { + let rows = db.prepare('SELECT * FROM institutions_referentiel ORDER BY nom').all(); + rows = attachCategories(rows); + res.json(rows); +}); + +// ── GET /api/institutions-referentiel/:id ─────────────────────────────────── +router.get('/:id', (req, res, next) => { + try { + const row = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(req.params.id); + if (!row) throw new HttpError(404, 'Institution introuvable'); + const [enriched] = attachCategories([row]); + res.json(enriched); + } catch (e) { next(e); } +}); + +// ── POST /api/institutions-referentiel ────────────────────────────────────── +router.post('/', (req, res, next) => { + try { + const data = Schema.parse(req.body); + const existing = db.prepare('SELECT id FROM institutions_referentiel WHERE nom = ?').get(data.nom); + if (existing) throw new HttpError(409, `Une institution "${data.nom}" existe déjà`); + + const r = db.prepare(` + INSERT INTO institutions_referentiel (nom, url, domiciliation, description, updated_at) + VALUES (?,?,?,?, datetime('now')) + `).run(data.nom, data.url || null, data.domiciliation, data.description || null); + + const institutionId = r.lastInsertRowid; + db.transaction(() => saveCategories(institutionId, data.categories_ids))(); + + const row = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(institutionId); + const [enriched] = attachCategories([row]); + res.status(201).json(enriched); + } catch (e) { next(e); } +}); + +// ── PUT /api/institutions-referentiel/:id ─────────────────────────────────── +router.put('/:id', (req, res, next) => { + try { + const row = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(req.params.id); + if (!row) throw new HttpError(404, 'Institution introuvable'); + + const data = Schema.parse(req.body); + if (data.nom !== row.nom) { + const dup = db.prepare('SELECT id FROM institutions_referentiel WHERE nom = ? AND id != ?').get(data.nom, row.id); + if (dup) throw new HttpError(409, `Une institution "${data.nom}" existe déjà`); + } + + db.prepare(` + UPDATE institutions_referentiel + SET nom=?, url=?, domiciliation=?, description=?, updated_at=datetime('now') + WHERE id=? + `).run(data.nom, data.url || null, data.domiciliation, data.description || null, row.id); + + db.transaction(() => saveCategories(row.id, data.categories_ids))(); + + const updated = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(row.id); + const [enriched] = attachCategories([updated]); + res.json(enriched); + } catch (e) { next(e); } +}); + +// ── DELETE /api/institutions-referentiel/:id ──────────────────────────────── +router.delete('/:id', (req, res, next) => { + try { + const row = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(req.params.id); + if (!row) throw new HttpError(404, 'Institution introuvable'); + + for (const fname of [row.logo_filename, row.icone_filename]) { + if (!fname) continue; + const fpath = path.join(logosDir, fname); + if (fs.existsSync(fpath)) fs.unlink(fpath, () => {}); + } + + db.prepare('DELETE FROM institutions_referentiel WHERE id = ?').run(row.id); + res.json({ deleted: true }); + } catch (e) { next(e); } +}); + +// ── POST /api/institutions-referentiel/:id/logo ───────────────────────────── +router.post('/:id/logo', upload.single('file'), async (req, res, next) => { + try { + const ref = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(req.params.id); + if (!ref) throw new HttpError(404, 'Institution introuvable'); + if (!req.file) throw new HttpError(400, 'Fichier requis'); + + const finalPath = await processLogoFile(req.file.path); + const filename = path.basename(finalPath); + + if (ref.logo_filename) { + const old = path.join(logosDir, ref.logo_filename); + if (fs.existsSync(old)) fs.unlink(old, () => {}); + } + + db.prepare(`UPDATE institutions_referentiel SET logo_filename=?, updated_at=datetime('now') WHERE id=?`) + .run(filename, ref.id); + + res.json({ logo_filename: filename }); + } catch (e) { next(e); } +}); + +// ── DELETE /api/institutions-referentiel/:id/logo ─────────────────────────── +router.delete('/:id/logo', (req, res, next) => { + try { + const ref = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(req.params.id); + if (!ref) throw new HttpError(404, 'Institution introuvable'); + + if (ref.logo_filename) { + const filePath = path.join(logosDir, ref.logo_filename); + if (fs.existsSync(filePath)) fs.unlink(filePath, () => {}); + } + + db.prepare(`UPDATE institutions_referentiel SET logo_filename=NULL, updated_at=datetime('now') WHERE id=?`).run(ref.id); + res.json({ deleted: true }); + } catch (e) { next(e); } +}); + +// ── POST /api/institutions-referentiel/:id/icone ──────────────────────────── +router.post('/:id/icone', upload.single('file'), async (req, res, next) => { + try { + const ref = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(req.params.id); + if (!ref) throw new HttpError(404, 'Institution introuvable'); + if (!req.file) throw new HttpError(400, 'Fichier requis'); + + const finalPath = await processLogoFile(req.file.path); + const filename = path.basename(finalPath); + + if (ref.icone_filename) { + const oldFile = path.join(logosDir, ref.icone_filename); + if (fs.existsSync(oldFile)) fs.unlink(oldFile, () => {}); + } + + db.prepare(`UPDATE institutions_referentiel SET icone_filename=?, updated_at=datetime('now') WHERE id=?`) + .run(filename, ref.id); + + res.json({ icone_filename: filename }); + } catch (e) { next(e); } +}); + +// ── DELETE /api/institutions-referentiel/:id/icone ────────────────────────── +router.delete('/:id/icone', (req, res, next) => { + try { + const ref = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(req.params.id); + if (!ref) throw new HttpError(404, 'Institution introuvable'); + + if (ref.icone_filename) { + const filePath = path.join(logosDir, ref.icone_filename); + if (fs.existsSync(filePath)) fs.unlink(filePath, () => {}); + } + + db.prepare(`UPDATE institutions_referentiel SET icone_filename=NULL, updated_at=datetime('now') WHERE id=?`).run(ref.id); + res.json({ deleted: true }); + } catch (e) { next(e); } +}); + +export default router; diff --git a/backend/src/routes/ref-categories-institutions.js b/backend/src/routes/ref-categories-institutions.js new file mode 100644 index 0000000..a9112d2 --- /dev/null +++ b/backend/src/routes/ref-categories-institutions.js @@ -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-institutions.js — liste globale des catégories du référentiel + * "Institutions financières" (type d'établissement : Banque, Courtier, Assureur...). + * Même patron que routes/ref-categories.js (categories_inv), table dédiée + * (categories_institutions) — cf. institutionsReferentiel.js. + */ + +const router = Router(); +// Toutes ces routes sont montées sous requireAdmin dans server.js + +/* GET /api/ref-categories-institutions — liste complète */ +router.get('/', (_req, res) => { + const rows = db.prepare(` + SELECT c.id, c.nom, + COUNT(rc.institution_id) AS nb_utilises + FROM categories_institutions c + LEFT JOIN referentiel_categories_institutions rc ON rc.categorie_id = c.id + GROUP BY c.id + ORDER BY c.nom + `).all(); + res.json(rows); +}); + +/* POST /api/ref-categories-institutions — 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_institutions 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_institutions (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-institutions/: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_institutions WHERE id = ?').get(req.params.id); + if (!row) throw new HttpError(404, 'Catégorie introuvable'); + const dup = db.prepare('SELECT id FROM categories_institutions 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_institutions SET nom = ? WHERE id = ?').run(nom.trim(), row.id); + const nb = db.prepare('SELECT COUNT(*) AS n FROM referentiel_categories_institutions 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-institutions/:id — supprimer si non utilisée */ +router.delete('/:id', (req, res, next) => { + try { + const row = db.prepare('SELECT id FROM categories_institutions 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_institutions WHERE categorie_id = ?').get(row.id).n; + if (nb > 0) throw new HttpError(409, `Catégorie utilisée par ${nb} institution(s) — retirez-la d'abord.`); + db.prepare('DELETE FROM categories_institutions WHERE id = ?').run(row.id); + res.status(204).end(); + } catch (e) { next(e); } +}); + +/* POST /api/ref-categories-institutions/: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_institutions 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_institutions 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 institution_id FROM referentiel_categories_institutions WHERE categorie_id = ?' + ).all(source.id); + let added = 0, skipped = 0; + for (const { institution_id } of refs) { + const exists = db.prepare( + 'SELECT 1 FROM referentiel_categories_institutions WHERE institution_id = ? AND categorie_id = ?' + ).get(institution_id, target.id); + if (!exists) { + db.prepare( + 'INSERT INTO referentiel_categories_institutions (institution_id, categorie_id) VALUES (?, ?)' + ).run(institution_id, target.id); + added++; + } else { + skipped++; + } + } + db.prepare('DELETE FROM referentiel_categories_institutions WHERE categorie_id = ?').run(source.id); + db.prepare('DELETE FROM categories_institutions 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; diff --git a/backend/src/server.js b/backend/src/server.js index a4faf05..97db360 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -50,6 +50,8 @@ import adminAiRouter from './routes/adminAi.js'; import tauxCreditImpotRouter from './routes/tauxCreditImpot.js'; import referentielRouter from './routes/referentiel.js'; import referentielPublicRouter from './routes/referentielPublic.js'; +import institutionsReferentielRouter from './routes/institutionsReferentiel.js'; +import refCategoriesInstitutionsRouter from './routes/ref-categories-institutions.js'; import refCategoriesRouter from './routes/ref-categories.js'; import refSecteursRouter from './routes/ref-secteurs.js'; import categoriesInvRouter from './routes/categories-inv.js'; @@ -168,6 +170,10 @@ app.use('/api/invitations', invitationsRouter); app.use('/api/taux-credit-impot', requireAuth, tauxCreditImpotRouter); app.use('/api/referentiel', requireAuth, requireAdmin, referentielRouter); app.use('/api/referentiel-public', requireAuth, referentielPublicRouter); +// Chantier "Référentiel Institutions financières" (26/09/26, demande Olivier) — Phase 1 : +// catalogue admin transverse aux workspaces, cf. institutionsReferentiel.js. +app.use('/api/institutions-referentiel', requireAuth, requireAdmin, institutionsReferentielRouter); +app.use('/api/ref-categories-institutions', requireAuth, requireAdmin, refCategoriesInstitutionsRouter); app.use('/api/ref-categories', requireAuth, requireAdmin, refCategoriesRouter); app.use('/api/ref-secteurs', requireAuth, requireAdmin, refSecteursRouter); app.use('/api/categories-inv', requireAuth, categoriesInvRouter); diff --git a/design/logos institution/BNP Paribas - Logo.webp b/design/logos institution/BNP Paribas - Logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..dcdd7509cb6a34b423e9873601eb708bdf211a2c GIT binary patch literal 2128 zcmV-W2(R~2Nk&FU2mkVe#jnBf3u$kUR7DBpUHm;`}qD@`4#6cgpT_AE&Ge@ z<@{%ZURnO{UllTVy zN&N%;r}v-UPxHV3{;s`!e%*T_e!ze8H%OBIy`Xofm5(4vQ%d!=%_4bRQXP406p8Gr!(b}dpnMUg4hjyN@f=4~8lEX9S;r~l+v z{0cv)G!Os!D#>^BQPS5kH4c2ll8xcivMQk{%=bz;<6c2_FGbK0k-eKH<#vkPi{xzc zW|olVke5wO31nX}c*e_b*WvNs-~HXKWawGv%zB6ejW1 z?+}q1%HzM%M3)(dza|S2JXMqN2Lxyy`nxt=$=Onl3*s^Noc7p9+u4oh@Gf5?33ypY zk@aJH-0k&Qw?yoGbf<SsDq*M|guvrT~{P%CWNxuQ! zf}xUxt@1KglB;Qr_nnWWv2m89i8M`16qLa33FWK@RUEz>u>;q9S-5`(h9fORWzJy%b=yZJZ7 zo2IE(35rE-|NaBn(|LN-*rJ*XndU@DY9u!vwhhRN73}MU%(CSM|NS^@Hx^P+;{WE* zgcLB=LEJ5?6x-Nw&2H7dBNxfK)(K*CLhi`SU@t*hNS2;mQBPpgFCN=7u{y&(cCVPE zJ+|o>-jG}Z2CbVW%^Bt8wCC_I1_$SQ0hlR%U2{6gk(Sm|IH2UlCFK5M48~t4QfBae018E=KD0Wgb=T&jTPdXxNzI@ z*u(Sf!NV`8ee5ggnkwdX4_NjRHGYk=cL{&lQ*B@%8E2W^TZ*!Vu%p=>2=!yPHZR!i zH3s@#ZC^cqmn6SGAd8>nE*cV2tzYcB;&lfUi@BrBTfj#q{6fDpBU+6Y*a`fAI>qi8 z`DtY)bEfmi#gtrX+2K3SWH^Zro_B^!LL2Wx2H98c>zKe8B=F(54PiGar!Mc4lUF~Aae~C!sfKFYYQ?I5w-7ul_Z1@PTNX* zsx;Q$e$n1t1iz+k`}an>fnpkXE-|uN0(W-fr|>W*X(EQ za{PS-JV1Pyj!tBO2Q z3dk5J&G!b-Eb|_JX9CiVN=viOzcmGIk|?RC$|6b>o}sfm!z3^1)!RN|hTbREZ~v>) zLqR#ZcJj;yw3<1yF=kU)(P+VBn~v|yj2)DT(Ne*j=cU5Yn&?8se{})F|DzywzfK~V z(Lzo2v2nNIJ&QDLn!kZkth2`<0YoPw9B2PW+gLo}6Huv|yQpDWgSL43PDfbFhXIHy zzJbzZ@nh9gl!QGO61~<}UJ`uquy^wW8DtSQ@?K=^TfzGD5x{;|`nypNZ!LJ&Y$+5o zQ0+JBY*6*>A;XHlaKLx_cRMweg&DKSKD7hI9$;7aLwZON!})yv((UW$$>ZGiyTR4R z3?77xk@Y{n{`T$KERVWtVHd{?F|@*x8Yj{)sVN5t-rz{qQ#S@uHzzK23x* zKj*B%KRf~j$@cOcw?SY0xV@+Hx&Ov57!OBV-IU_6SYWwxmO==D?zKIWRhf7w)PNV# G0001pD>b$N literal 0 HcmV?d00001 diff --git a/design/logos institution/Banque Populaire - Logo.webp b/design/logos institution/Banque Populaire - Logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..1419bd9dc3b6940ebbabc77aebf0558a36238004 GIT binary patch literal 3420 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 ZNzH0n^P1SqrZ%_9&2D=0o5~CU06P%Fvts}N literal 0 HcmV?d00001 diff --git a/design/logos institution/CCF - Logo.webp b/design/logos institution/CCF - Logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..a4d4e76a2466813a6c891e01f31cfcfeb0f6108a GIT binary patch literal 1568 zcmV+*2H*KoNk&E(1^@t8MM6+kP&gnA1^@u?9sr#ID#!pl06s|~jYOiNAri{2m?#8< zpbZz}6rcS;n|=qqSKaTq{*Tl*=ndh1U_9*oD*u?@L-YXYgUDs@*Y-}(Ke%$C`yG?% z@%}^itF*uN-^rd5fC2e;^N;j>^LmcS7my*+Kac;<{$I{l(qHra!2g#2UjK*v$Fv`z zf9(ncO@NiQv~o`c+JbrWXPi{qIC zVu;z*F~B3LqBf2W*9|o+10<_K*h%$JPqJ*Z7>wT6M}>zK0E4e2gEoHf56#`j%HOFt zeH|bDlHu}Q_mrZrp*90O*ND!nwUGb-{{9r9_JLP&uE6MW-E>Z^(mMrHV=Dw;k*Q84 zs%k0&x?6VvdrknykPV3mZ%s#?TF@AY>0DR^Y}nQACMiGeWVK)c)`Y2DW4Tja2Hr{-?& zNm(Z(pSnAUU2U-DF$h7_K?wZSTGV3HV@~pqq5}i`WOJkNA9Ii3%yZlS{ow~KtaQD@ zH=y?S^}oO64P;KB0yU98O1I8Z1unl;(tm2sxhFm>-I=!MaHjBp$KrxMyBoW7nUENTI|;%b0-bH#%ZfQOU^k_HvTPht0( zwmdCGI4BgKi~Q|~u3=e4Q%vlnVet|e{loDHspl7L^J7+x$G=P{Ul{@^WHyTm zL$2^&zNA2i$=Nun7hPH<73Kop z*9n)BEc&0c^=6AUtmcuP)mI&}p_bl2Ths&AdfIeAypLT9d<_YYHoG`nKk6|A=l8w3 zE*=davYedl9*OC_N#L3_20QZghJ1ieJT3;kz2gL6HZ?FnOn;ybQ}v={a;j13C?T?Z z%oRwmLCya%$`IG<)b~{tadbH=SN;tnjDpRh4+cwI!Noxe)ucUlepdhfMu-4y1Ky!e z5>L``ZnBt*YbY>{&M7!mfwaqY#({efQ(wgU8)b9I>b)?{aM5gkxj+BD-F)Dm^cd1v zemVRw>7NW*&5up*viZ9?p>Tt@1hjx2q2z^E_skm_+pvc-*j4yoV;EXwFPhRV{0a!- zEhQnzRjUIUHwElE>+tZPS5yWo%73%r0Vs68i>l6YR{M3W#}FA@3`{%iE+&BX zjL{Bai?a`pWs%=@Qf22p3F~P*veAIqsv$ub=QjL*CoI4`xyftVd^LzQ!y)k z=PH(-|CDLTxHABX6MA^RL|giS|7O0w*Hj_mQh18qs@$>O{(s@AcI_bXYxZ^RTV6R^ zA=!+hv{dzf_Q@Fqr>Y`O%Uk$I6YQgBI&yT+zjA6&%8gQ>Q+Bczkam(6KuICeUor%` zAN}9E(N9EDM(81YB2=)OgKv&HRh!JpX+i947y+)vT!c7Ps_=tHLC)DghIhOsH`3Sc zI~REj&32QlZG&+B9PF_!uEt*cYL~`HG&5!dtyuzieiA9EPU=6=YfB)p7$xA)M~50} S$nZ2_{r~^~0000000007WeLpy literal 0 HcmV?d00001 diff --git a/design/logos institution/CIC Logo.webp b/design/logos institution/CIC Logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..2abbd1de3f054f9f7bfc6b335e6c83972477b896 GIT binary patch literal 1236 zcmV;_1S|VeNk&G@1ONb6MM6+kP&gpK1ONc=8vvaFD#!pl06s|`jYJ}$p_a*Ycqjyf zu>fj77=8ngm#MrB`$yAqV>-zE&*i`QpMRdCKH0s%Immf{{eb;5_lo}Y)B*b?)`#c; z`w!`f|0CTq_Vf1}+E$OQKp*D#LE4{o%O?Mu;{xB8Jby&{2LCAjt^XsiU+ba~AA;mN zN8F;nJd4<=LHB~;dYp<{;|MW!i;YF(Vjj5t=wy95+N|;h83M;5K?YU{4mb;r^w}+g z`IH0}twLQ*!o0hJBB(XcIF+B{TEKLZSs=aq$Q=c34Yb>3h&a6`a7KeP{CyxxOpO2l z{{9rAyWddn@X+Vver<)`E9U`6&O3g5ZzD+@f|#cBIr&zfGE$ z0)7N#dbR}(aIMm${_I0T% z!|DzbB-}``CIC*deDr1E7U%Nk_qq58g24ZBtUy@ME8omX5h@@4Zq*)_-p4aJ8qHGE zu<6g@wVa4TT0Yor(B$z2n<#(l$M`dQlKe86^{oEzbe@9S>;If+;pXZ_qXkw*J;0`T zWR5B+&|jt2h)i#a$WuY5waEiAGHjP~aNm$hwS-3S9gp%_f!mUtOXI(CDl{?fS0&&oH8T5JM3Ouo2g!hw>d{Rab{>L zJl~sWiajoWIN!Igo8P1#dI+I7i5R)mlvJ=)7W(-eyzslEc(xjeV=_St!&Pn{p`tY6 zEhY#MM3?+h?*I4_GRr)-qB5<@-^O%}d~!IXY>l&u`z|nZ4sPSOy;3BKqK>;0DLEez zkeyT?sg&|$Oh5fbf=dlSwJrk9k(=3~XKMZFpk6!;BTZtT@YpHo%jOwQNHg*;Pz;7A ztk5)}#J!7%Q4$uRAJI$Zk=6@yhA|ccP!)QZ!jOvY57@bH@uZ5Gcm-TcHTf%;Ep4MUJk~Q@gEDda_q0= za`bq3bGX89A$4ka+>|+EQN0R!&O?+%9~9;+_S3NONTp29otMfwiAzS7;q=RQSOhod zAs*_*S!Ow~rJ!Dd7QgT_|Bm>2JC5LU$LALDxKuAApkjyWWs{vIg-OwdC!G}dCtKr` zZ+v}yR(m|gx?slS@@Rm5CMV9~Ym7MFUWZ_bx47pB z)T!@{dRl3qDEOsI8J+Z!l|{)nXXdkyfTCX_bW=-<+mT`66W`Iuokl+s5gn=$#k9B` zd;CM0zHY?kmb3_fUi61si;Y|sKA<9g@ps9MQ~#r`Nq^>Bll$5Gb+3mkOdyb)E$ttJ y*(|ctfCIQvPpMR*G^MQrTRz|^5@y@W!vB-Xwv^8wUnImO|N4hX$Am`s-~a&V)OFtg literal 0 HcmV?d00001 diff --git a/design/logos institution/Caisse d'epargne - Logo.webp b/design/logos institution/Caisse d'epargne - Logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..50038941ceda8253fe954ef63f7d65ae4eb12e88 GIT binary patch literal 1860 zcmV-K2fO%ENk&FI2LJ$9MM6+kP&gnk2LJ#NBmkWOD#!pl06s||jzuD(ArrW*uqXtC zq(l-L1c1to#hj(%8~g{b4X_?yKZXAy|1A>uD{ohciF*P5YxBPPL(iws-}L>$KUcroeZ_baEhv*0nupuf((JuO z_cy|VXcy1MhZJqzyyH{KHYJ*8dU8wsmwMD5iMor#An@%!#*P2!*G+83rO)cL56xs^ zQF_*Y1xD|nty(`*l75Fys;J%-_h_)B1OHoU=J66XjfzT(TRJW>DSh-{!9OaKWj*h> zi}^>ASzihb9Fdvs6l0jj0A$(O5tt=V;JzduJ0iM!^>vr@vHPpaYo zfeu!a>wDMdQgU@_@wjzbJn{?Cll_Y{7~i{|cjReV<-1U)Lq)~@F@6v62XW1x^R>9A zUllH+6BE72`CdzQEgs<+aD;N z@iOlvlo((h+Wi55m23ZL{M05yQg`>?`M)d+tNm3|N*(=7#D+1+2(4eNKl3OPg6X5d zn-8fhhJiq1PsgXtYXl2p7D|Yc(s~`8v=oZ(PPWsJ`}mPt$6h`Nf8~5bK@WT;r}&R2 z<7mV4;MMgRgbRBIYwZ5}B2)E}cUxqA4n!D-tOh51(xe)#}zQzo5(qH1Z#Xvh{ zkrw;|lK8y(uDt8fJ0sfN{pPLYC|A}CyB-xll~vw6aATS0Qb$GtO2@?MX(eC`QNPoza!fPN$ z|J2(4@BFv~a`bDWjS-~_kcUeL#BvnJe0I)W`RGIGbv_=NRn~pq z=CtKTTS!U6M3gRUjHXrU@k-rL7gA^}?Z8CBUqe|TwFF27*rYbm8`JrKb`xio_m5De z41>ZHZx}20#l%Gj&qbV|JvwT5BnOkHhd22s#lX)K1uC?AAX!xa$&s>}gmQ9>p1?-2 zt}`$TpVE{0%FGDsg!Z2K<2|(OW-anKL(dE6PZQ6j{|0IQ?D7ippj`Ep@!C)$NP+L)CJ^Qjx&P1eAGN!RI?Qj-th`(#66GDb{pU8O7}ohk zdVH1}3$T^nMFQMV=<=`}LNUuMMQqhdjfe?Ru|DRJofBP2- z_~*||>|aYL>Hq(D8viLI9rx6xzdVjPkvviNAvkquB@A2huqbGe2=DBzh`xI_dp+Yr zER+j-Lv-)dI2T097VK#k5(Vo2TB6RgG2l%(RcP~3UX!{G^N9aw5ldQ)_7pdyzDj}` zS#+j9B|q2wXH8LlKHPJs&-~sxqs1yJI0000sS-w30 literal 0 HcmV?d00001 diff --git a/design/logos institution/Credit agricole -Logo.gif b/design/logos institution/Credit agricole -Logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..642446ca1f0622e4c71d8b9ff0bfca8dfb503d90 GIT binary patch literal 2509 zcmaJ>dt8hAAAi=mYC>g_98c3#?LO_^Pjy>@JPH=ilWC+M0 zknjQ$i44*i1TqZ~@M)B2iog}5kSJ6liA*F@@ni~;7As&SUM!zK>4AgD1u~ITA(Du} z36C&elBjUU>7xCTMuIdb=$m7)obWj*f5B$~IEt{G$>4x8;m_MwpI z-c%nNgYC^F({TWXnNhq78`9S0+gSKi8Kj`VK!7EpUT{3<_Sz=RTLtzSqbfJI>Gw?zLrr>EL8Wj)IX$(9cA|ZSz zngqiz9Y@rKNSqj%e~i$iN;j7i;SZ*wG`Ehbzj<4jnB0`9Mi=QQ`i5KkeO9 zkiUCZUhd8vIoaE{ZQYWUnUTJEQ(EfAlw@VnhD3#2wq7a`C&aId6UD|vBSHZm=B-`x zW7O(ZD3%w8t9|-PypZx=+4An3((h+ z)c!Rs&`9?MEzfC}P;@Z1x;}p77pd!YMZ%D>ek^rNt16RnU9IiSygPn}n1^s^c0saJ z`R&^FGBB_vqOR!?MCviZ$Xnh*&G(5N%=#>BE{b=3UL;m8BQwKbj%5A%a5=moZYj}vOM|SUX>ro*H9KZ0c2!v9?>!1^vM?+v+KTutiUErQcJpkYuLK_fKh( z__MZQouNn-1>u+vIFO;@@(5d_9>OECPoI}lDa!H5Q-9k#Knc-1o3&ho_FWNSX83I7 zGs}?VzZ|kH9c2RnN-uN?+aA80fo_g8gcg+tTAztSxBa+tMHeax9koW)d2M<6@-+MK z_Qb^2Wu~~@uU4L=NdR}Q`hKD#;Xp(e>Ma-CnK!L(K6&mYWl3k~38N(@Bgp>ct-bB+ zt_P@mhiHp|q)(Vq8(=_FUL02N+l?)*KXpU$E{t$+g4xC=ajZK?dIiYZ}+~}|7Y9cy{fC1WwlXusx)eH+1kjqyZd}SZB49tQ-I*6Q00M~?HHxQ3kArU z>rpWH!yys4)^8@%3n+pT$IJEATdczQ4sQ!Og!k;B`^qZ)_R0;MS2HSS(6RebyxG-_ z7Ctx(V`)GjXuGsHKC-c|DE=(tG3R)hzox{(jyGIt(wKB5&vx|o&BDQOT4vEA-eG$~ zW4mf6J;vkw!Sb>OS#+h5p&6ow4dq?@%tRo)ylvcGIr~qF8Y}fN0SB zE(0|+YDTtCx~0s;(jwW-+IDK2V1Z|?dP-(BYP`acXE4qm)vjsdwF%I5&v2f${XHUf zv4^XH$bhzQ#-KA=J$$R7czB4ls0{VVw3_-CphcjsT26nY@u}VTJEqP^yPk zZA)qv1#>%%5MZze5JAIBik>>Sq@xQl8p1_AtMnV^t!4vUzix%4RE4F_@dRz?bGUx4 zg&Nfv>5=-L^W31TjP8$?<2JhtG>0~cuPR;^HiIzkpR`Ku$CP+r0e=$E?}L~oYG%Clqx&z zvC8`GwDP|(!Fe7g-5Z*TVF%Vz8#a`_LIc{wI_-tmX&0Y98eMRC;e`)|;s%GMt6e{I zd)n1Q1DVgBXpY#!`Hg-w!K?i6nptfPV^Q^~)NNI#a@Sv~o@d7KKCxSY7@E)5W)u~z z=F~+^Wu5dqxh=$nlIGD9UgLK^X?M!;e(`1x^Zvd&o)$$GQ*fCO1dMbeCn>@R_np(Hl*58jKuTGoDaR*8E{M_64h6?ugV>V9%-B3C1Zeck+a6eE?YWKPZO)5C8xG literal 0 HcmV?d00001 diff --git a/design/logos institution/Crédit Mutuel - Logo.gif b/design/logos institution/Crédit Mutuel - Logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..0282469c44435f962df49aeabf77ef810beae7eb GIT binary patch literal 1387 zcmV-x1(fmlEmjD0%EC2ui0LTD5000F4@X1N5y*TU5yZ>M)j$~<`XsWJk>%MR- z&vb3yc&_h!@BhG{a7Zi~kI1BQ$!t2G(5Q4uty-_xtai)odcWYXcuX#v&(OkP%`Ono ztOde=urk>A!2zB!1QY;)3IYowa|Q{890ZJS4~>o?1ZsI~4I7Y(7LbfaatoRy3jqKQ zrl$@72_XxCfpr`Lf(8$y01AZ*0s;XEkr)S+cm)~=u7Lp-#>dGZ3A+NYF17$B2nwj# zrmG(Ur`#L?r?l9qbqJ>lzZQ4GczYP=r~vEUsLdbZrUEeK4kZNq0T4hZfk0^-N;%-? zL7xM1rU0Bvw~j&sH3cXQ!+7a}JXEy*DEKplui-Zs`l^wzmnx)$fTsdf#E_sS2#2%~ z3Gnq`K}-z{4s>KdsQ`nQ2jD5wR+50ipEpmwNGa;6%L%O_oH3c%HhNFr_QyLTTLSbg^@%Xz2ENu+M->VpOyDG&{Bk$mTv3K=>jSuPn&Q@Oug z|EW;j@Y6j4gnBA0HHi^z)=;9cR>R} zEp`fnrf?=*DXO&hNJ{Z-qnd%f)E5a$bzGJXFIGtanF#e&7|%QgFhbY_(@plC1K>FH z5O7-c1XTsWnD<=)LOBVghx^zAKp|FMBP5CwD9BJs++^i|eGQ=8LD) zks~7u;Hl6mH@X?3jsi|rrhyYq=no?JO^{ZfACWmhFl(51fKeO@`s5nKOfcsI=>faX zqbEj`;zE>e^U|BAnu37;Psk}Ct+YRd3W9dt?P#i<+GtbLDHXEhU~94d(!ekbyb;ut zhYr91QFs{oQ8M}jkSA9PL27K5llI5xBg}H^z#QFb!am`B38qy)6)VQ2+S=b;qIjh;01ZLOk-^|DD)Jut=yApi|p%vDv|vx@KM z=e7bXu=7_6UFk3XPil6DxRMd=>`0(`W6;hpk?=iVAyZfWENNH!OJO1Ygcf)ErG4MPg1f=l?v z;|{oQ_axiq2nOhPk0Y^`<$mmvW>fwu*fy)Mg&p6WYaYGw6;v#RtjUe(B}NKf?BK>V z_tnU7*&vfYP@oGyfaWTfkrsa@KmZ11E;$zPLwyi2Em|;00ihYrv_Ob7kGaq&8@tsD zMYfn}spo_WfW;^@$U#{|gdl%lVO@fV!dx6j6$X(OLLl^%z$i*%odhv^345JvwNX9an@r-CpqZ-%9#x}a~jc|;k9HS5c06R$Aeue-5 literal 0 HcmV?d00001 diff --git a/design/logos institution/Fortuneo - Logo.webp b/design/logos institution/Fortuneo - Logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..dc5bb953b95c520fe9a02c6936cdf165b8327885 GIT binary patch literal 2562 zcmV+d3jOs`Nk&Hc2><|BMM6+kP&gp&2><{PF#w$bD#!pl06s|~jzprOAri{2P$&e1 zvVc6Co-eI!_skwZoR|KO?gO+2*bgusabAYK06I}U06emPX+9BubpK<$q_YQmJpS$B zkA-gwdky=azXho8$UfD63;NgXSLBbzzt_JYKOjAe^+W0f`4{oe?LU=2mE3Ot1M<)2 zzu~_sywm@;{DAiZ<^$XV`9Jf&^}m^Zu3z9hNW6>K3;AdEAM}6SKX5($|Iq(I`!DO4 z^PlnG^uNaZ1pg%dmHwmiWBhOb|NWjtKV-d?KV6Pj8b1`g)nQL1t(C(L3*HF5DxX5J zJc|&iD4g8FzhlnP?$*o&*XZye23abW&Q+}yVerz{oJN`1ah|8hJklILf zIbS8Ywi0vBX2w8-$D9h8ZqwUPl6-v%Yh4w)p?@*x8vs`x%F*vP@m&K4b72`zEBBST zasru~ofC|nf(IRs_15mrW8i(A96l*?SG^=k9Fb&O;T@nt;1LOBVxR4UV`{$`Yf}{-Tf~2mx`T$ctHx1O)4N7F>%AIU&Jtx+^jC7Wc>t!6`wC_`B}<*!K3MeL7I`S$+TjS zDmbWC3ow0Vt0wws$Wrh`2j0ZM5)~h004SHg+T39v&!Du&0xMib4rU`?ejvEY2v@&r z+xkol6{8!%N#P8$1iXpyt!G`thPg7+Z=cI9`=Rp66Xcgie_dhYpxUB_IMQc^4n?)` z@gSu>G)Riwl2ibhK-TWUnz>A&!E~E!=c$THWE533S|AV|o|+}J`fmSS!dS$M827&z zJ#B^~bV==DgoWu1GndZR8w?iKYH5rYqA&r$E{bZ-Gg|ZAF`02I2CYVU>*vLE?#VxJ zXU<&IcVfoYI-ECQrKZy@b%>Qg&Ug(fg-w+}_J1SBE@-r(OSUuV166{f>#tA63fD86 z7uN;u)q|q987}6CyOAY9UHzW&#_4Y7?0p><=6=Ki*dXRhv)JDWITDDg*Lp$!{4f`z zasH1plSJg^X@x@uRkQ=nQbQ~RcH;ER2$$hUWn_bK-qAr=SX4_5XMy>h*63m3M#%ed zrM7xa4b$bq`H1y=HMuT8~Rdz{J9-^CTl<52wq<56?p% zI0rvpS#kB#itXh4!363#6RfS<;{?D-A8HS;zdqxC`oy}|wg~UAV{kff2aC({OVat% zD%wGipUr=*Cu%QbJGFda?Pbz6@^hW}4xj*AzhQ9dIPHW|I_xaMurb!Y=Ol$65ThZa zW1%Qo2tKJ%UAEk`Z~>bP)gPzv;<7C(ylU-!xz}R$tvYdEgGWBO3SJ0x=NJ72J73vb zKioB0fxSyD)V%~81`o{1nug0#;Aha3NVmyCn^p3a@z5uKUR~&e$EM0(|Gf5Emxp1y z{5-CPQ^1u!2gwyq7|`Z`fYuJTI(C>>kqJxR0*D&V-Gp z|ME7H@-Uo(@1ylSBOqhP8Aod7Lk}Sa89jvKcj7Oa?x*oOT)JwNr5PdtO4AAG48AM&)27 zR@)aZ0)m0GF^*7`_6@fU#bWt7cKj<1s)L}!t!uM6A&kAxEPGD|vy@F2f`$A(b?7o< z!#-bc`Bcfm-*}3^-K%~!?FfB<@<+x$?%U{B?kl-?XOZ+?4l7+22I#8E7zcGB?J_zNpz zR?(aQ1{o8-rrXC`88>Pu5#=zdKl3%PXDim~>2ba2vdVMbrgCwODaa4a@q+D4kCjKo zYlc5*3UpQ7f%9VZ)_9UbKNqIpc@yn(VyWRl^Q<#JQ8Hw2BQAw~wA-|5L|)8Jl{Rtc zS=Obu>CN-SE?3>q3y)9~b8S2^iq9}H#RaX^J9Uqo5$G5wvfMfsj*^yW> z)}kUk82)cp&|NaTJ`5OU1poa}>JPZK)9@GAOSFC;AC8kuZ;mS$qT;LXK=TCge;!xu z)|&;;XbfKSbKt#j481$w^BIToMt99kAr=I%mpt_;OBURTQ7Kgm4^?+}Be5$LQh9G(Bxv3+C3c{DlN*;2f=NB7ZKCKg$9}kXtvDnxIASKn3;ryJP Y>%1-iurh%V4ou~FOq!h1SYdzw0NUOFy8r+H literal 0 HcmV?d00001 diff --git a/design/logos institution/LCL - Logo.webp b/design/logos institution/LCL - Logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..1322020a22faa0002c7dfba7a55b338accac1a5a GIT binary patch literal 1414 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 literal 0 HcmV?d00001 diff --git a/design/logos institution/Société Général Logo.webp b/design/logos institution/Société Général Logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..4a6d8a7cb02967f6f8a4966bf7132e444839bdb3 GIT binary patch literal 1062 zcmV+>1ljviNk&E<1ONb6MM6+kP&gnG1ONc=833IDD#!pl06s||jYT4%p%b{S7$^jU zvVd!F^FKiRfP@0*3+Nw_&_Exc91B0Oo{ZYBUKH#z`!7W2l)aqF2>31#cBcNxy-~w| zng5&oywscgqvl$er}n=+5BEH{{So^C@E!c0`d|BRR&J(G*u%_3x1B_8wJn;FZtW_) z&K*se{iu;=NcfNj{;t62gI;0yKwkr+?)IZlS8|(n<>a*7lvyp5-$7d~u8z9nu_Ft= zvBkhe4Ne1pKu!@jO{zaS)XUhDL2j#aOZV?UTQXI2l@yogW{T> z*EW6*T{`ubn6PhO{~9q_8=E<{-(L4~O#Uj0Ilt=Gst<^##CK|Yl#Nv4W@y#F+I4#J zO;RQ?WNWEEjBo*BRYY7=EI(V8sC*rzLI3r0REVLmr`q$~Pp`M5N}6^H{T0W!gr-V= z>Q|LL58{9Ra^t_#wJ}Hk$byDG{v6=P_Y*Ao1j{~fQ`Qr~sq+~B9$;nK3KV!K7Jy^$ zOmnku@LUDc0UvgDCiP(cKCZjnAclXEd5Nl=iy`;`8rSzN62QshTr&UL7rmnP%6Yq5 z*{#Cm59WYl{220+*OeKg_#NEFAN_e9b;=KWTGn=Q7XdU*>Gk=w*CNDkKjLTFmi`(J z7q0_jR;c|N=Qt9mIWte$@@1fkpsxgH&^fh)c~X;q8xP}R;IM75=3}S&B;CQkziv|~ z$Qs;j+YZkSORvl&bdzY0WBkD%<_vz}XZQ7b+L6*gDcb$U2yC#}0W9=zXoW&7XHGuu#!MGqzcop^ug@yzG4@x_k;4VmA^RA8<5m%U73b`d{1*5sgEUZSeam1 z{`@O!*m#%vH0!N$u7*F zKpEi8(YLVLOI;29CH*Zqe{%`?c>pu_kx}&KD(j4VKe2AQ;}lnIQ?~ao5Zbc}^fwUQ z=s5{4knwUkT`Q5#Oa~@4WLevkQ9Xn1X}>w=T-WORe%vr~ zxKw)Y_-5BFOxQi8O|m?U@2$VSkLyq43zz@v&xkCilPikRK1OL0i|=WSfD()WkUr|| gC;$acHcm1xiD<>uBVgqqU=;%N{H*$wlRE$a04v5FeE} /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 8b355d3..82edd04 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -16,6 +16,7 @@ function IconShield() { return ; } function IconTax() { return ; } function IconDatabase() { return ; } +function IconBank() { return ; } function IconSettings() { return ; } function IconMail() { return ; } function IconDownload() { return ; } @@ -41,6 +42,7 @@ const NAV = [ items: [ { id: 'plateformes', label: 'Plateformes', icon: , href: '/admin/plateformes' }, { id: 'fiscalite', label: 'Fiscalité', icon: , href: '/admin/fiscalite' }, + { id: 'institutions', label: 'Institutions financières', icon: , href: '/admin/institutions' }, ], }, { diff --git a/frontend/src/pages/AdminInstitutions.jsx b/frontend/src/pages/AdminInstitutions.jsx new file mode 100644 index 0000000..d8bbbfd --- /dev/null +++ b/frontend/src/pages/AdminInstitutions.jsx @@ -0,0 +1,734 @@ +import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { api } from '../api.js'; +import ConfirmModal from '../components/ConfirmModal.jsx'; +import Modal from '../components/Modal.jsx'; +import ResultBanner from '../components/ResultBanner.jsx'; +import CountrySelect, { COUNTRIES, FlagIcon } from '../components/CountrySelect.jsx'; +import { usePagination } from '../hooks/usePagination.js'; +import Pagination from '../components/Pagination.jsx'; + +/** + * pages/AdminInstitutions.jsx — Référentiel admin "Institutions financières" + * (banques, courtiers, assureurs...) — chantier du 26/09/26 (demande Olivier). + * + * Phase 1 uniquement (cf. plan de déploiement, doc projet) : catalogue admin — logo, icône, + * catégories (type d'établissement). Même esprit que AdminPlateformes.jsx (ReferentielSection / + * LogosBibliothequeSection / RefListSection), mais volontairement plus simple : pas de champs + * spécifiques aux plateformes d'investissement (fiscalité, méthode de remboursement...), pas de + * rattachement workspace (une institution est transverse à tous les espaces de travail), pas de + * mécanique de "poussée" (aucun enregistrement lié pour l'instant — Phase 2, plus tard). + * RefListSection est dupliqué ici plutôt qu'importé depuis AdminPlateformes.jsx : même + * précédent déjà en place dans ce fichier (le traitement image logo/icône est aussi dupliqué + * côté backend plutôt que partagé) — évite de coupler ce référentiel à l'évolution de celui des + * plateformes. + */ + +const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—'; + +/* ── Icônes ───────────────────────────────────────────────────── */ +function IconBank() { + return ; +} +function IconImage() { + return ; +} +function IconTag() { + return ; +} +function IconEdit() { + return ; +} +function IconTrash() { + return ; +} +function IconChevronLeft() { + return ; +} + +/* ── Modale de création/édition d'une institution ──────────────── + Formulaire simple à un seul écran (contrairement au RefModal des plateformes, à 5 étapes) : + une institution financière n'a qu'une poignée de champs, un wizard serait disproportionné. */ +function InstitutionModal({ row, onClose, onSaved }) { + const isNew = !row?.id; + const [categories, setCategories] = useState([]); + const [err, setErr] = useState(null); + const [saving, setSaving] = useState(false); + + const [form, setForm] = useState({ + nom: row?.nom || '', + url: row?.url || '', + domiciliation: row?.domiciliation || 'FR', + description: row?.description || '', + categories_ids: (row?.categories || []).map(c => c.id), + }); + + useEffect(() => { + api.get('/ref-categories-institutions').then(setCategories).catch(() => {}); + }, []); + + const set = (k, v) => setForm(f => ({ ...f, [k]: v })); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!form.nom.trim()) { setErr('Le nom est requis.'); return; } + setSaving(true); setErr(null); + try { + const payload = { + nom: form.nom.trim(), + url: form.url.trim() || null, + domiciliation: form.domiciliation, + description: form.description.trim() || null, + categories_ids: form.categories_ids || [], + }; + if (isNew) await api.post('/institutions-referentiel', payload); + else await api.put(`/institutions-referentiel/${row.id}`, payload); + onSaved(); onClose(); + } catch (ex) { setErr(ex.message); } + finally { setSaving(false); } + }; + + return ( + + + + + } + > +
+ {err &&
{err}
} +
+ + set('nom', e.target.value)} placeholder="Ex : BNP Paribas, Boursorama…" /> +
+
+ + set('url', e.target.value)} placeholder="https://…" /> +
+
+ + set('domiciliation', code)} showCode /> +
+
+ +
+ {categories.length === 0 && Aucune catégorie — gérez-les depuis l'onglet "Catégories".} + {categories.map(c => { + const checked = (form.categories_ids || []).includes(c.id); + return ( + + ); + })} +
+
+
+ +