1007 lines
45 KiB
JavaScript
1007 lines
45 KiB
JavaScript
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 { createZip, readZip } from '../utils/zip.js';
|
|
import { HttpError } from '../middleware/errorHandler.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const logosDir = path.resolve(__dirname, '../../../data/logos');
|
|
fs.mkdirSync(logosDir, { recursive: true });
|
|
|
|
// ── Traitement image (réutilisé depuis plateformes.js) ─────────────────────
|
|
|
|
async function processLogoFile(filePath) {
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
if (ext === '.svg') {
|
|
try {
|
|
let svg = fs.readFileSync(filePath, 'utf8');
|
|
// Supprimer fond blanc SVG (background-color, rect blanc, etc.)
|
|
svg = svg.replace(/(<svg\b[^>]*)\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('[ref-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('[ref-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, `ref_${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(100).default('france'),
|
|
fiscalite: z.enum(['flat_tax', 'sans_fiscalite_locale', 'avec_fiscalite_locale']).default('flat_tax'),
|
|
taux_fiscalite_locale: z.number().min(0).max(100).nullable().optional(),
|
|
type_produit_fiscal: z.enum(['2TT', '2TR']).default('2TT'),
|
|
methode_remboursement: z.enum(['portefeuille', 'compte_courant', 'choix_investisseur']).optional().default('portefeuille'),
|
|
type_pret_defaut: z.enum(['in_fine', 'amortissable', 'differe']).nullable().optional(),
|
|
freq_interets_defaut: z.enum(['mensuel', 'trimestriel', 'in_fine']).nullable().optional(),
|
|
logo_filename: z.string().nullable().optional(),
|
|
description: z.string().nullable().optional(),
|
|
// Profil enrichi
|
|
annee_creation: z.number().int().nullable().optional(),
|
|
investisseurs_types: z.string().nullable().optional(),
|
|
regulateur: z.string().nullable().optional(),
|
|
numero_licence: z.string().nullable().optional(),
|
|
is_regule: z.union([z.boolean(), z.number()]).optional().default(false),
|
|
pays_inscription: z.string().nullable().optional(),
|
|
pays_siege: z.string().nullable().optional(),
|
|
pays_operation: z.array(z.string()).optional().default([]),
|
|
investissement_minimum: z.number().nullable().optional(),
|
|
rendement_annonce: z.number().nullable().optional(),
|
|
nb_investisseurs: z.number().int().nullable().optional(),
|
|
volume_total_finance: z.number().nullable().optional(),
|
|
duree_moyenne_pret: z.number().nullable().optional(),
|
|
garantie_rachat: z.union([z.boolean(), z.number()]).optional().default(false),
|
|
statistiques_publiques: z.union([z.boolean(), z.number()]).optional().default(false),
|
|
bonus_inscription: z.union([z.boolean(), z.number()]).optional().default(false),
|
|
marche_secondaire: z.union([z.boolean(), z.number()]).optional().default(false),
|
|
investissement_auto: z.union([z.boolean(), z.number()]).optional().default(false),
|
|
url_trustpilot: z.string().url().optional().or(z.literal('')).nullable(),
|
|
url_linkedin: z.string().url().optional().or(z.literal('')).nullable(),
|
|
// Catégories : tableau de noms (strings) — indépendant des catégories user (legacy)
|
|
categories: z.array(z.string().min(1)).optional().default([]),
|
|
// 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([]),
|
|
// Notation : tableau de critères
|
|
notation: z.array(z.object({
|
|
nom: z.string().min(1),
|
|
type: z.enum(['etoiles', 'lettres', 'score', 'custom']).default('etoiles'),
|
|
valeurs: z.array(z.string()).nullable().optional(),
|
|
min_val: z.number().nullable().optional(),
|
|
max_val: z.number().nullable().optional(),
|
|
description: z.string().nullable().optional(),
|
|
ordre: z.number().int().default(0),
|
|
})).optional().default([]),
|
|
});
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
/** Catégories attachées au référentiel (stockées comme strings dans referentiel_categories.nom) */
|
|
function attachCats(rows) {
|
|
if (rows.length === 0) return rows;
|
|
const ids = rows.map(r => r.id);
|
|
const cats = db.prepare(`
|
|
SELECT rc.referentiel_id, rc.categorie_nom
|
|
FROM referentiel_categories rc
|
|
WHERE rc.referentiel_id IN (${ids.map(() => '?').join(',')})
|
|
ORDER BY rc.categorie_nom
|
|
`).all(...ids);
|
|
|
|
const map = {};
|
|
for (const c of cats) {
|
|
if (!map[c.referentiel_id]) map[c.referentiel_id] = [];
|
|
map[c.referentiel_id].push(c.categorie_nom);
|
|
}
|
|
return rows.map(r => ({ ...r, categories: map[r.id] || [] }));
|
|
}
|
|
|
|
/** Catégories d'investissement globales (categories_inv) */
|
|
function attachCatsInv(rows) {
|
|
if (rows.length === 0) return rows;
|
|
const ids = rows.map(r => r.id);
|
|
const links = db.prepare(`
|
|
SELECT rc.referentiel_id, c.id AS categorie_id, c.nom
|
|
FROM referentiel_categories_inv rc
|
|
JOIN categories_inv c ON c.id = rc.categorie_id
|
|
WHERE rc.referentiel_id IN (${ids.map(() => '?').join(',')})
|
|
ORDER BY c.nom
|
|
`).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.categorie_id, nom: l.nom });
|
|
}
|
|
return rows.map(r => ({ ...r, categories_inv: map[r.id] || [] }));
|
|
}
|
|
|
|
/** Secteurs d'investissement globaux (secteurs_inv) */
|
|
function attachSecteursInv(rows) {
|
|
if (rows.length === 0) return rows;
|
|
const ids = rows.map(r => r.id);
|
|
const links = db.prepare(`
|
|
SELECT rs.referentiel_id, s.id AS secteur_id, s.nom
|
|
FROM referentiel_secteurs_inv rs
|
|
JOIN secteurs_inv s ON s.id = rs.secteur_id
|
|
WHERE rs.referentiel_id IN (${ids.map(() => '?').join(',')})
|
|
ORDER BY s.nom
|
|
`).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.secteur_id, nom: l.nom });
|
|
}
|
|
return rows.map(r => ({ ...r, secteurs_inv: map[r.id] || [] }));
|
|
}
|
|
|
|
/** Critères de notation du référentiel */
|
|
function attachNotation(rows) {
|
|
if (rows.length === 0) return rows;
|
|
const ids = rows.map(r => r.id);
|
|
const notations = db.prepare(`
|
|
SELECT * FROM referentiel_notation
|
|
WHERE referentiel_id IN (${ids.map(() => '?').join(',')})
|
|
ORDER BY referentiel_id, ordre, id
|
|
`).all(...ids);
|
|
|
|
const map = {};
|
|
for (const n of notations) {
|
|
if (!map[n.referentiel_id]) map[n.referentiel_id] = [];
|
|
map[n.referentiel_id].push({
|
|
...n,
|
|
valeurs: n.valeurs ? JSON.parse(n.valeurs) : null,
|
|
});
|
|
}
|
|
return rows.map(r => ({ ...r, notation: map[r.id] || [] }));
|
|
}
|
|
|
|
/** Parse pays_operation (JSON TEXT → array) */
|
|
function parsePaysOp(rows) {
|
|
return rows.map(r => ({
|
|
...r,
|
|
pays_operation: r.pays_operation
|
|
? (typeof r.pays_operation === 'string' ? JSON.parse(r.pays_operation) : r.pays_operation)
|
|
: [],
|
|
}));
|
|
}
|
|
|
|
/** Enregistre catégories, secteurs et notation pour un référentiel */
|
|
function saveRelations(tx, refId, categories, notation, categoriesInvIds, secteursInvIds) {
|
|
tx(() => {
|
|
// Legacy string categories
|
|
db.prepare('DELETE FROM referentiel_categories WHERE referentiel_id = ?').run(refId);
|
|
if (categories.length > 0) {
|
|
const ins = db.prepare(
|
|
'INSERT OR IGNORE INTO referentiel_categories (referentiel_id, categorie_nom) VALUES (?, ?)'
|
|
);
|
|
for (const nom of categories) ins.run(refId, nom.trim());
|
|
}
|
|
|
|
// Nouvelles catégories globales (IDs)
|
|
db.prepare('DELETE FROM referentiel_categories_inv WHERE referentiel_id = ?').run(refId);
|
|
if (categoriesInvIds && categoriesInvIds.length > 0) {
|
|
const ins = db.prepare('INSERT OR IGNORE INTO referentiel_categories_inv (referentiel_id, categorie_id) VALUES (?,?)');
|
|
for (const id of categoriesInvIds) ins.run(refId, id);
|
|
}
|
|
|
|
// Secteurs globaux (IDs)
|
|
db.prepare('DELETE FROM referentiel_secteurs_inv WHERE referentiel_id = ?').run(refId);
|
|
if (secteursInvIds && secteursInvIds.length > 0) {
|
|
const ins = db.prepare('INSERT OR IGNORE INTO referentiel_secteurs_inv (referentiel_id, secteur_id) VALUES (?,?)');
|
|
for (const id of secteursInvIds) ins.run(refId, id);
|
|
}
|
|
|
|
db.prepare('DELETE FROM referentiel_notation WHERE referentiel_id = ?').run(refId);
|
|
if (notation.length > 0) {
|
|
const ins = db.prepare(`
|
|
INSERT INTO referentiel_notation (referentiel_id, nom, type, valeurs, min_val, max_val, description, ordre)
|
|
VALUES (?,?,?,?,?,?,?,?)
|
|
`);
|
|
for (const n of notation) {
|
|
ins.run(
|
|
refId, n.nom, n.type || 'etoiles',
|
|
(n.valeurs && n.valeurs.length) ? JSON.stringify(n.valeurs) : null,
|
|
n.min_val ?? null, n.max_val ?? null,
|
|
n.description ?? null, n.ordre ?? 0
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── GET /api/referentiel ───────────────────────────────────────────────────
|
|
router.get('/', (_req, res) => {
|
|
let rows = db.prepare(`
|
|
SELECT pr.*,
|
|
(SELECT COUNT(*) FROM plateformes p WHERE p.referentiel_id = pr.id) AS nb_plateformes_liees
|
|
FROM plateformes_referentiel pr
|
|
ORDER BY pr.nom
|
|
`).all();
|
|
rows = attachCats(rows);
|
|
rows = attachCatsInv(rows);
|
|
rows = attachSecteursInv(rows);
|
|
rows = attachNotation(rows);
|
|
rows = parsePaysOp(rows);
|
|
res.json(rows);
|
|
});
|
|
|
|
// ── Multer for ZIP uploads ─────────────────────────────────────────────────
|
|
const zipUpload = multer({
|
|
storage: multer.memoryStorage(),
|
|
limits: { fileSize: 50 * 1024 * 1024 },
|
|
fileFilter: (_req, file, cb) => {
|
|
if (file.mimetype === 'application/zip' || file.originalname.endsWith('.zip')) cb(null, true);
|
|
else cb(new HttpError(400, 'Fichier ZIP attendu'));
|
|
},
|
|
});
|
|
|
|
// ── GET /api/referentiel/export — export tout le référentiel ───────────────
|
|
router.get('/export', (_req, res, next) => {
|
|
try {
|
|
let rows = db.prepare('SELECT * FROM plateformes_referentiel ORDER BY nom').all();
|
|
rows = attachCatsInv(rows);
|
|
rows = attachSecteursInv(rows);
|
|
rows = parsePaysOp(rows);
|
|
|
|
const entries = [];
|
|
const manifest = {
|
|
version: '1.0',
|
|
app: 'crowdlending',
|
|
exported_at: new Date().toISOString(),
|
|
count: rows.length,
|
|
type: 'referentiel',
|
|
};
|
|
entries.push({ name: 'manifest.json', data: JSON.stringify(manifest, null, 2) });
|
|
|
|
const dataRows = rows.map(r => ({
|
|
nom: r.nom,
|
|
url: r.url,
|
|
domiciliation: r.domiciliation,
|
|
fiscalite: r.fiscalite,
|
|
taux_fiscalite_locale: r.taux_fiscalite_locale,
|
|
type_produit_fiscal: r.type_produit_fiscal,
|
|
description: r.description,
|
|
annee_creation: r.annee_creation,
|
|
investisseurs_types: r.investisseurs_types,
|
|
regulateur: r.regulateur,
|
|
numero_licence: r.numero_licence,
|
|
is_regule: r.is_regule,
|
|
pays_inscription: r.pays_inscription,
|
|
pays_siege: r.pays_siege,
|
|
pays_operation: r.pays_operation || [],
|
|
investissement_minimum: r.investissement_minimum,
|
|
rendement_annonce: r.rendement_annonce,
|
|
nb_investisseurs: r.nb_investisseurs,
|
|
volume_total_finance: r.volume_total_finance,
|
|
duree_moyenne_pret: r.duree_moyenne_pret,
|
|
garantie_rachat: r.garantie_rachat,
|
|
statistiques_publiques: r.statistiques_publiques,
|
|
bonus_inscription: r.bonus_inscription,
|
|
marche_secondaire: r.marche_secondaire,
|
|
investissement_auto: r.investissement_auto,
|
|
url_trustpilot: r.url_trustpilot,
|
|
url_linkedin: r.url_linkedin,
|
|
methode_remboursement: r.methode_remboursement,
|
|
type_pret_defaut: r.type_pret_defaut,
|
|
freq_interets_defaut: r.freq_interets_defaut,
|
|
logo_filename: r.logo_filename,
|
|
icone_filename: r.icone_filename,
|
|
categories_inv: (r.categories_inv || []).map(c => c.nom),
|
|
secteurs_inv: (r.secteurs_inv || []).map(s => s.nom),
|
|
}));
|
|
|
|
entries.push({ name: 'data.json', data: JSON.stringify(dataRows, null, 2) });
|
|
|
|
// Include logo / icon files
|
|
for (const r of rows) {
|
|
for (const fname of [r.logo_filename, r.icone_filename]) {
|
|
if (!fname) continue;
|
|
const fpath = path.join(logosDir, fname);
|
|
if (fs.existsSync(fpath)) {
|
|
entries.push({ name: `logos/${fname}`, data: fs.readFileSync(fpath) });
|
|
}
|
|
}
|
|
}
|
|
|
|
const zipBuf = createZip(entries);
|
|
const slug = 'referentiel-' + new Date().toISOString().slice(0, 10);
|
|
res.setHeader('Content-Type', 'application/zip');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${slug}.zip"`);
|
|
res.send(zipBuf);
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── GET /api/referentiel/:id/export — export une seule entrée ───────────────
|
|
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 entries = [];
|
|
const manifest = {
|
|
version: '1.0',
|
|
app: 'crowdlending',
|
|
exported_at: new Date().toISOString(),
|
|
count: 1,
|
|
type: 'referentiel',
|
|
};
|
|
entries.push({ name: 'manifest.json', data: JSON.stringify(manifest, null, 2) });
|
|
|
|
const dataRow = {
|
|
nom: enriched.nom,
|
|
url: enriched.url,
|
|
domiciliation: enriched.domiciliation,
|
|
fiscalite: enriched.fiscalite,
|
|
taux_fiscalite_locale: enriched.taux_fiscalite_locale,
|
|
type_produit_fiscal: enriched.type_produit_fiscal,
|
|
description: enriched.description,
|
|
annee_creation: enriched.annee_creation,
|
|
investisseurs_types: enriched.investisseurs_types,
|
|
regulateur: enriched.regulateur,
|
|
numero_licence: enriched.numero_licence,
|
|
is_regule: enriched.is_regule,
|
|
pays_inscription: enriched.pays_inscription,
|
|
pays_siege: enriched.pays_siege,
|
|
pays_operation: enriched.pays_operation || [],
|
|
investissement_minimum: enriched.investissement_minimum,
|
|
rendement_annonce: enriched.rendement_annonce,
|
|
nb_investisseurs: enriched.nb_investisseurs,
|
|
volume_total_finance: enriched.volume_total_finance,
|
|
duree_moyenne_pret: enriched.duree_moyenne_pret,
|
|
garantie_rachat: enriched.garantie_rachat,
|
|
statistiques_publiques: enriched.statistiques_publiques,
|
|
bonus_inscription: enriched.bonus_inscription,
|
|
marche_secondaire: enriched.marche_secondaire,
|
|
investissement_auto: enriched.investissement_auto,
|
|
url_trustpilot: enriched.url_trustpilot,
|
|
url_linkedin: enriched.url_linkedin,
|
|
methode_remboursement: enriched.methode_remboursement,
|
|
type_pret_defaut: enriched.type_pret_defaut,
|
|
freq_interets_defaut: enriched.freq_interets_defaut,
|
|
logo_filename: enriched.logo_filename,
|
|
icone_filename: enriched.icone_filename,
|
|
categories_inv: (enriched.categories_inv || []).map(c => c.nom),
|
|
secteurs_inv: (enriched.secteurs_inv || []).map(s => s.nom),
|
|
};
|
|
|
|
entries.push({ name: 'data.json', data: JSON.stringify([dataRow], null, 2) });
|
|
|
|
for (const fname of [enriched.logo_filename, enriched.icone_filename]) {
|
|
if (!fname) continue;
|
|
const fpath = path.join(logosDir, fname);
|
|
if (fs.existsSync(fpath)) {
|
|
entries.push({ name: `logos/${fname}`, data: fs.readFileSync(fpath) });
|
|
}
|
|
}
|
|
|
|
const zipBuf = createZip(entries);
|
|
const slug = enriched.nom.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '-' + new Date().toISOString().slice(0, 10);
|
|
res.setHeader('Content-Type', 'application/zip');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${slug}.zip"`);
|
|
res.send(zipBuf);
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── POST /api/referentiel/import-zip — import depuis un fichier ZIP ─────────
|
|
router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
|
try {
|
|
if (!req.file) throw new HttpError(400, 'Fichier ZIP manquant');
|
|
const zipEntries = readZip(req.file.buffer);
|
|
|
|
const manifestEntry = zipEntries.find(e => e.name === 'manifest.json');
|
|
const dataEntry = zipEntries.find(e => e.name === 'data.json');
|
|
if (!dataEntry) throw new HttpError(400, 'ZIP invalide : data.json manquant');
|
|
|
|
const platforms = JSON.parse(dataEntry.data.toString('utf8'));
|
|
if (!Array.isArray(platforms)) throw new HttpError(400, 'data.json doit être un tableau');
|
|
|
|
// Normalise tag names to IDs (create global if missing)
|
|
function resolveTagIds(names, table) {
|
|
const ids = [];
|
|
for (const nom of (names || [])) {
|
|
const trimmed = nom.trim();
|
|
if (!trimmed) continue;
|
|
let row = db.prepare(`SELECT id FROM ${table} WHERE nom = ? AND user_id IS NULL`).get(trimmed);
|
|
if (!row) {
|
|
const r = db.prepare(`INSERT INTO ${table} (nom, user_id) VALUES (?, NULL)`).run(trimmed);
|
|
row = { id: r.lastInsertRowid };
|
|
}
|
|
ids.push(row.id);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
// Build a map of images from the ZIP
|
|
const imageMap = {};
|
|
for (const e of zipEntries) {
|
|
if (e.name.startsWith('logos/') && e.name.length > 6) {
|
|
imageMap[path.basename(e.name)] = e.data;
|
|
}
|
|
}
|
|
|
|
let created = 0;
|
|
let updated = 0;
|
|
const tx = db.transaction(() => {
|
|
for (const p of platforms) {
|
|
if (!p.nom) continue;
|
|
|
|
const catsIds = resolveTagIds(p.categories_inv, 'categories_inv');
|
|
const sectsIds = resolveTagIds(p.secteurs_inv, 'secteurs_inv');
|
|
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);
|
|
|
|
const fields = [
|
|
p.url || null, p.domiciliation || 'france', p.fiscalite || 'flat_tax',
|
|
p.taux_fiscalite_locale ?? null, p.type_produit_fiscal || '2TT',
|
|
p.description ?? null, p.annee_creation ?? null,
|
|
p.investisseurs_types ?? null,
|
|
p.regulateur ?? null, p.numero_licence ?? null, p.is_regule ? 1 : 0,
|
|
p.pays_inscription ?? null, p.pays_siege ?? null, paysOp,
|
|
p.investissement_minimum ?? null, p.rendement_annonce ?? null,
|
|
p.nb_investisseurs ?? null, p.volume_total_finance ?? null, p.duree_moyenne_pret ?? null,
|
|
p.garantie_rachat ? 1 : 0, p.statistiques_publiques ? 1 : 0,
|
|
p.bonus_inscription ? 1 : 0, p.marche_secondaire ? 1 : 0, p.investissement_auto ? 1 : 0,
|
|
p.url_trustpilot || null, p.url_linkedin || null,
|
|
p.logo_filename ?? null, p.icone_filename ?? null,
|
|
];
|
|
|
|
let refId;
|
|
if (existing) {
|
|
db.prepare(`
|
|
UPDATE plateformes_referentiel SET
|
|
url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?, type_produit_fiscal=?,
|
|
description=?, annee_creation=?, investisseurs_types=?,
|
|
regulateur=?, numero_licence=?, is_regule=?,
|
|
pays_inscription=?, pays_siege=?, pays_operation=?,
|
|
investissement_minimum=?, rendement_annonce=?,
|
|
nb_investisseurs=?, volume_total_finance=?, duree_moyenne_pret=?,
|
|
garantie_rachat=?, statistiques_publiques=?, bonus_inscription=?, marche_secondaire=?, investissement_auto=?,
|
|
url_trustpilot=?, url_linkedin=?,
|
|
logo_filename=?, icone_filename=?,
|
|
updated_at=datetime('now')
|
|
WHERE id=?
|
|
`).run(...fields, existing.id);
|
|
refId = existing.id;
|
|
updated++;
|
|
} else {
|
|
const r = db.prepare(`
|
|
INSERT INTO plateformes_referentiel
|
|
(nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal,
|
|
description, annee_creation, investisseurs_types,
|
|
regulateur, numero_licence, is_regule,
|
|
pays_inscription, pays_siege, pays_operation,
|
|
investissement_minimum, rendement_annonce,
|
|
nb_investisseurs, volume_total_finance, duree_moyenne_pret,
|
|
garantie_rachat, statistiques_publiques, bonus_inscription, marche_secondaire, investissement_auto,
|
|
url_trustpilot, url_linkedin,
|
|
logo_filename, icone_filename, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
|
|
`).run(p.nom, ...fields);
|
|
refId = r.lastInsertRowid;
|
|
created++;
|
|
}
|
|
|
|
// Sync categories_inv
|
|
db.prepare('DELETE FROM referentiel_categories_inv WHERE referentiel_id = ?').run(refId);
|
|
for (const id of catsIds) {
|
|
db.prepare('INSERT OR IGNORE INTO referentiel_categories_inv (referentiel_id, categorie_id) VALUES (?,?)').run(refId, id);
|
|
}
|
|
// Sync secteurs_inv
|
|
db.prepare('DELETE FROM referentiel_secteurs_inv WHERE referentiel_id = ?').run(refId);
|
|
for (const id of sectsIds) {
|
|
db.prepare('INSERT OR IGNORE INTO referentiel_secteurs_inv (referentiel_id, secteur_id) VALUES (?,?)').run(refId, id);
|
|
}
|
|
|
|
// Write logo / icon images from ZIP
|
|
for (const fname of [p.logo_filename, p.icone_filename]) {
|
|
if (!fname || !imageMap[fname]) continue;
|
|
const dest = path.join(logosDir, fname);
|
|
fs.writeFileSync(dest, imageMap[fname]);
|
|
}
|
|
}
|
|
});
|
|
|
|
tx();
|
|
res.json({ ok: true, created, updated, total: platforms.length });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
|
|
// ── GET /api/referentiel/:id ───────────────────────────────────────────────
|
|
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])))));
|
|
res.json(enriched);
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── POST /api/referentiel ──────────────────────────────────────────────────
|
|
router.post('/', (req, res, next) => {
|
|
try {
|
|
const data = Schema.parse(req.body);
|
|
const existing = db.prepare('SELECT id FROM plateformes_referentiel WHERE nom = ?').get(data.nom);
|
|
if (existing) throw new HttpError(409, `Une entrée référentiel "${data.nom}" existe déjà`);
|
|
|
|
const r = db.prepare(`
|
|
INSERT INTO plateformes_referentiel
|
|
(nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal,
|
|
methode_remboursement, type_pret_defaut, freq_interets_defaut,
|
|
logo_filename, description,
|
|
annee_creation, investisseurs_types,
|
|
regulateur, numero_licence, is_regule,
|
|
pays_inscription, pays_siege, pays_operation,
|
|
investissement_minimum, rendement_annonce, nb_investisseurs, volume_total_finance, duree_moyenne_pret,
|
|
garantie_rachat, statistiques_publiques, bonus_inscription, marche_secondaire, investissement_auto,
|
|
url_trustpilot, url_linkedin, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, datetime('now'))
|
|
`).run(
|
|
data.nom, data.url || null, data.domiciliation, data.fiscalite,
|
|
data.taux_fiscalite_locale ?? null, data.type_produit_fiscal,
|
|
data.methode_remboursement || 'portefeuille', data.type_pret_defaut ?? null, data.freq_interets_defaut ?? null,
|
|
data.logo_filename ?? null, data.description ?? null,
|
|
data.annee_creation ?? null, data.investisseurs_types ?? null,
|
|
data.regulateur ?? null, data.numero_licence ?? null, data.is_regule ? 1 : 0,
|
|
data.pays_inscription ?? null, data.pays_siege ?? null,
|
|
data.pays_operation?.length ? JSON.stringify(data.pays_operation) : null,
|
|
data.investissement_minimum ?? null, data.rendement_annonce ?? null,
|
|
data.nb_investisseurs ?? null, data.volume_total_finance ?? null, data.duree_moyenne_pret ?? null,
|
|
data.garantie_rachat ? 1 : 0, data.statistiques_publiques ? 1 : 0,
|
|
data.bonus_inscription ? 1 : 0, data.marche_secondaire ? 1 : 0, data.investissement_auto ? 1 : 0,
|
|
data.url_trustpilot || null, data.url_linkedin || null
|
|
);
|
|
|
|
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);
|
|
|
|
const row = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(refId);
|
|
const [enriched] = parsePaysOp(attachNotation(attachSecteursInv(attachCatsInv(attachCats([row])))));
|
|
enriched.nb_plateformes_liees = 0;
|
|
res.status(201).json(enriched);
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── PUT /api/referentiel/:id ───────────────────────────────────────────────
|
|
router.put('/: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 data = Schema.parse(req.body);
|
|
|
|
// Vérifier unicité du nom si changé
|
|
if (data.nom !== row.nom) {
|
|
const dup = db.prepare('SELECT id FROM plateformes_referentiel WHERE nom = ? AND id != ?').get(data.nom, row.id);
|
|
if (dup) throw new HttpError(409, `Une entrée référentiel "${data.nom}" existe déjà`);
|
|
}
|
|
|
|
db.prepare(`
|
|
UPDATE plateformes_referentiel
|
|
SET nom=?, url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?,
|
|
type_produit_fiscal=?, methode_remboursement=?, type_pret_defaut=?, freq_interets_defaut=?,
|
|
logo_filename=?, description=?,
|
|
annee_creation=?, investisseurs_types=?,
|
|
regulateur=?, numero_licence=?, is_regule=?,
|
|
pays_inscription=?, pays_siege=?, pays_operation=?,
|
|
investissement_minimum=?, rendement_annonce=?, nb_investisseurs=?,
|
|
volume_total_finance=?, duree_moyenne_pret=?,
|
|
garantie_rachat=?, statistiques_publiques=?, bonus_inscription=?,
|
|
marche_secondaire=?, investissement_auto=?,
|
|
url_trustpilot=?, url_linkedin=?, updated_at=datetime('now')
|
|
WHERE id=?
|
|
`).run(
|
|
data.nom, data.url || null, data.domiciliation, data.fiscalite,
|
|
data.taux_fiscalite_locale ?? null, data.type_produit_fiscal,
|
|
data.methode_remboursement || 'portefeuille', data.type_pret_defaut ?? null, data.freq_interets_defaut ?? null,
|
|
data.logo_filename ?? null, data.description ?? null,
|
|
data.annee_creation ?? null, data.investisseurs_types ?? null,
|
|
data.regulateur ?? null, data.numero_licence ?? null, data.is_regule ? 1 : 0,
|
|
data.pays_inscription ?? null, data.pays_siege ?? null,
|
|
data.pays_operation?.length ? JSON.stringify(data.pays_operation) : null,
|
|
data.investissement_minimum ?? null, data.rendement_annonce ?? null,
|
|
data.nb_investisseurs ?? null, data.volume_total_finance ?? null, data.duree_moyenne_pret ?? null,
|
|
data.garantie_rachat ? 1 : 0, data.statistiques_publiques ? 1 : 0,
|
|
data.bonus_inscription ? 1 : 0, data.marche_secondaire ? 1 : 0, data.investissement_auto ? 1 : 0,
|
|
data.url_trustpilot || null, data.url_linkedin || null, row.id
|
|
);
|
|
|
|
const tx = db.transaction(fn => fn());
|
|
saveRelations(tx, row.id, data.categories, data.notation, data.categories_inv_ids, data.secteurs_inv_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])))));
|
|
enriched.nb_plateformes_liees = nb;
|
|
res.json(enriched);
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── DELETE /api/referentiel/:id ────────────────────────────────────────────
|
|
// Délie les plateformes liées (referentiel_id → null) avant suppression
|
|
router.delete('/:id', (req, res, next) => {
|
|
try {
|
|
const row = db.prepare('SELECT id FROM plateformes_referentiel WHERE id = ?').get(req.params.id);
|
|
if (!row) throw new HttpError(404, 'Référentiel introuvable');
|
|
|
|
db.transaction(() => {
|
|
// Délier les plateformes (on ne supprime pas les données user)
|
|
db.prepare('UPDATE plateformes SET referentiel_id = NULL WHERE referentiel_id = ?').run(row.id);
|
|
db.prepare('DELETE FROM plateformes_referentiel WHERE id = ?').run(row.id);
|
|
})();
|
|
|
|
res.json({ deleted: true });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── GET /api/referentiel/:id/plateformes ───────────────────────────────────
|
|
// Liste les plateformes user liées à ce référentiel
|
|
router.get('/:id/plateformes', (req, res, next) => {
|
|
try {
|
|
const row = db.prepare('SELECT id FROM plateformes_referentiel WHERE id = ?').get(req.params.id);
|
|
if (!row) throw new HttpError(404, 'Référentiel introuvable');
|
|
|
|
const plateformes = db.prepare(`
|
|
SELECT p.id, p.nom, p.domiciliation, p.fiscalite, p.overridden_fields,
|
|
u.email AS user_email,
|
|
inv.nom AS detenteur_nom, inv.prenom AS detenteur_prenom
|
|
FROM plateformes p
|
|
JOIN users u ON u.id = p.user_id
|
|
LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id
|
|
WHERE p.referentiel_id = ?
|
|
ORDER BY u.email, p.nom
|
|
`).all(req.params.id);
|
|
|
|
res.json(plateformes.map(p => ({
|
|
...p,
|
|
overridden_fields: JSON.parse(p.overridden_fields || '[]'),
|
|
})));
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── POST /api/referentiel/:id/push ─────────────────────────────────────────
|
|
// Pousse les champs du référentiel vers les plateformes liées.
|
|
// Body { force: true } → écrase tous les champs (méthode dure), y compris les overrides utilisateur.
|
|
// Défaut (force absent ou false) → respecte overridden_fields (méthode douce).
|
|
router.post('/:id/push', (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 force = req.body?.force === true;
|
|
|
|
// Champs scalaires poussables
|
|
const PUSHABLE = ['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'logo_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut'];
|
|
|
|
// Catégories/secteurs du référentiel
|
|
const refCatIds = db.prepare(
|
|
'SELECT categorie_id FROM referentiel_categories_inv WHERE referentiel_id = ?'
|
|
).all(ref.id).map(r => r.categorie_id);
|
|
const refSectIds = db.prepare(
|
|
'SELECT secteur_id FROM referentiel_secteurs_inv WHERE referentiel_id = ?'
|
|
).all(ref.id).map(r => r.secteur_id);
|
|
|
|
const plateformes = db.prepare(
|
|
'SELECT id, overridden_fields FROM plateformes WHERE referentiel_id = ?'
|
|
).all(ref.id);
|
|
|
|
let nb_updated = 0;
|
|
|
|
db.transaction(() => {
|
|
const insCat = db.prepare('INSERT OR IGNORE INTO plateforme_categories_inv (plateforme_id, categorie_id) VALUES (?, ?)');
|
|
const insSect = db.prepare('INSERT OR IGNORE INTO plateforme_secteurs_inv (plateforme_id, secteur_id) VALUES (?, ?)');
|
|
const insInvCat = db.prepare('INSERT OR IGNORE INTO investissement_categories_inv (investissement_id, categorie_id) VALUES (?, ?)');
|
|
const insInvSect = db.prepare('INSERT OR IGNORE INTO investissement_secteurs_inv (investissement_id, secteur_id) VALUES (?, ?)');
|
|
|
|
for (const plat of plateformes) {
|
|
// ── Champs scalaires ──
|
|
let fields;
|
|
if (force) {
|
|
fields = PUSHABLE;
|
|
} else {
|
|
const overridden = JSON.parse(plat.overridden_fields || '[]');
|
|
fields = PUSHABLE.filter(f => !overridden.includes(f));
|
|
}
|
|
if (fields.length > 0) {
|
|
const setParts = fields.map(f => `${f} = ?`).join(', ');
|
|
const values = fields.map(f => ref[f] ?? null);
|
|
const extraSet = force ? ', overridden_fields = \'[]\'' : '';
|
|
db.prepare(`UPDATE plateformes SET ${setParts}${extraSet} WHERE id = ?`).run(...values, plat.id);
|
|
}
|
|
|
|
// ── Catégories/secteurs ──
|
|
if (force) {
|
|
// Méthode dure : remplace toutes les associations
|
|
db.prepare('DELETE FROM plateforme_categories_inv WHERE plateforme_id = ?').run(plat.id);
|
|
db.prepare('DELETE FROM plateforme_secteurs_inv WHERE plateforme_id = ?').run(plat.id);
|
|
}
|
|
// Méthode douce : INSERT OR IGNORE (n'écrase pas les associations existantes)
|
|
for (const catId of refCatIds) insCat.run(plat.id, catId);
|
|
for (const sectId of refSectIds) insSect.run(plat.id, sectId);
|
|
|
|
// Sync investissements actifs de la plateforme
|
|
const invs = db.prepare(
|
|
"SELECT id FROM investissements WHERE plateforme_id = ? AND statut NOT IN ('rembourse','cloture')"
|
|
).all(plat.id);
|
|
for (const inv of invs) {
|
|
if (force) {
|
|
db.prepare('DELETE FROM investissement_categories_inv WHERE investissement_id = ?').run(inv.id);
|
|
db.prepare('DELETE FROM investissement_secteurs_inv WHERE investissement_id = ?').run(inv.id);
|
|
}
|
|
for (const catId of refCatIds) insInvCat.run(inv.id, catId);
|
|
for (const sectId of refSectIds) insInvSect.run(inv.id, sectId);
|
|
}
|
|
|
|
nb_updated++;
|
|
}
|
|
})();
|
|
|
|
res.json({ pushed: true, force, nb_plateformes: plateformes.length, nb_updated });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── POST /api/referentiel/:id/logo ─────────────────────────────────────────
|
|
// Upload d'un logo pour une entrée du référentiel ; auto-push vers les plateformes liées
|
|
router.post('/:id/logo', upload.single('file'), async (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');
|
|
if (!req.file) throw new HttpError(400, 'Fichier requis');
|
|
|
|
// Traiter l'image (fond blanc transparent, conversion en PNG si besoin)
|
|
const finalPath = await processLogoFile(req.file.path);
|
|
const filename = path.basename(finalPath);
|
|
|
|
// Supprimer l'ancien logo s'il existe
|
|
if (ref.logo_filename) {
|
|
const old = path.join(logosDir, ref.logo_filename);
|
|
if (fs.existsSync(old)) fs.unlink(old, () => {});
|
|
}
|
|
|
|
// Mettre à jour le référentiel
|
|
db.prepare(`UPDATE plateformes_referentiel SET logo_filename=?, updated_at=datetime('now') WHERE id=?`)
|
|
.run(filename, ref.id);
|
|
|
|
// Auto-push logo_filename vers les plateformes liées (sauf si overridé)
|
|
const plateformes = db.prepare('SELECT id, overridden_fields FROM plateformes WHERE referentiel_id = ?').all(ref.id);
|
|
db.transaction(() => {
|
|
for (const plat of plateformes) {
|
|
const overridden = JSON.parse(plat.overridden_fields || '[]');
|
|
if (!overridden.includes('logo_filename')) {
|
|
db.prepare('UPDATE plateformes SET logo_filename=? WHERE id=?').run(filename, plat.id);
|
|
}
|
|
}
|
|
})();
|
|
|
|
res.json({
|
|
logo_filename: filename,
|
|
nb_plateformes_updated: plateformes.filter(p => !JSON.parse(p.overridden_fields || '[]').includes('logo_filename')).length
|
|
});
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── DELETE /api/referentiel/:id/logo ───────────────────────────────────────
|
|
router.delete('/:id/logo', async (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');
|
|
|
|
if (ref.logo_filename) {
|
|
const filePath = path.join(logosDir, ref.logo_filename);
|
|
if (fs.existsSync(filePath)) fs.unlink(filePath, () => {});
|
|
}
|
|
|
|
db.prepare(`UPDATE plateformes_referentiel SET logo_filename=NULL, updated_at=datetime('now') WHERE id=?`).run(ref.id);
|
|
|
|
// Pousser null vers les plateformes liées non-overridées
|
|
const plateformes = db.prepare('SELECT id, overridden_fields FROM plateformes WHERE referentiel_id = ?').all(ref.id);
|
|
db.transaction(() => {
|
|
for (const plat of plateformes) {
|
|
const overridden = JSON.parse(plat.overridden_fields || '[]');
|
|
if (!overridden.includes('logo_filename')) {
|
|
db.prepare('UPDATE plateformes SET logo_filename=NULL WHERE id=?').run(plat.id);
|
|
}
|
|
}
|
|
})();
|
|
|
|
res.json({ deleted: true });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── POST /api/referentiel/:id/icone ──────────────────────────────────────────
|
|
// Upload d'une icone pour une entree du referentiel ; auto-push vers les plateformes liees
|
|
router.post('/:id/icone', upload.single('file'), async (req, res, next) => {
|
|
try {
|
|
const ref = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(req.params.id);
|
|
if (!ref) throw new HttpError(404, 'Referentiel 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 plateformes_referentiel SET icone_filename=?, updated_at=datetime('now') WHERE id=?`)
|
|
.run(filename, ref.id);
|
|
|
|
// Auto-push icone_filename vers les plateformes liées (sauf si overridé)
|
|
const plateformesIcn = db.prepare('SELECT id, overridden_fields FROM plateformes WHERE referentiel_id = ?').all(ref.id);
|
|
db.transaction(() => {
|
|
for (const plat of plateformesIcn) {
|
|
const overridden = JSON.parse(plat.overridden_fields || '[]');
|
|
if (!overridden.includes('icone_filename')) {
|
|
db.prepare('UPDATE plateformes SET icone_filename=? WHERE id=?').run(filename, plat.id);
|
|
}
|
|
}
|
|
})();
|
|
|
|
res.json({
|
|
icone_filename: filename,
|
|
nb_plateformes_updated: plateformesIcn.filter(p => !JSON.parse(p.overridden_fields || '[]').includes('icone_filename')).length
|
|
});
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// ── DELETE /api/referentiel/:id/icone ─────────────────────────────────────────
|
|
router.delete('/:id/icone', async (req, res, next) => {
|
|
try {
|
|
const ref = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(req.params.id);
|
|
if (!ref) throw new HttpError(404, 'Referentiel introuvable');
|
|
|
|
if (ref.icone_filename) {
|
|
const filePath = path.join(logosDir, ref.icone_filename);
|
|
if (fs.existsSync(filePath)) fs.unlink(filePath, () => {});
|
|
}
|
|
|
|
db.prepare(`UPDATE plateformes_referentiel SET icone_filename=NULL, updated_at=datetime('now') WHERE id=?`).run(ref.id);
|
|
|
|
const plateformesIcn = db.prepare('SELECT id, overridden_fields FROM plateformes WHERE referentiel_id = ?').all(ref.id);
|
|
db.transaction(() => {
|
|
for (const plat of plateformesIcn) {
|
|
const overridden = JSON.parse(plat.overridden_fields || '[]');
|
|
if (!overridden.includes('icone_filename')) {
|
|
db.prepare('UPDATE plateformes SET icone_filename=NULL WHERE id=?').run(plat.id);
|
|
}
|
|
}
|
|
})();
|
|
|
|
res.json({ deleted: true });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
|
|
// -- NOTATION -- criteres par referentiel -----------------------------------------
|
|
|
|
const TYPES_VALIDES_NOT = ['etoiles', 'lettres', 'score', 'custom'];
|
|
|
|
function enrichNot(row) {
|
|
return { ...row, valeurs: row.valeurs ? JSON.parse(row.valeurs) : null };
|
|
}
|
|
|
|
// GET /api/referentiel/:id/notation
|
|
router.get('/:id/notation', (req, res, next) => {
|
|
try {
|
|
const ref = db.prepare('SELECT id FROM plateformes_referentiel WHERE id = ?').get(req.params.id);
|
|
if (!ref) throw new HttpError(404, 'Referentiel introuvable');
|
|
const rows = db.prepare(
|
|
'SELECT * FROM referentiel_notation WHERE referentiel_id = ? ORDER BY ordre, id'
|
|
).all(ref.id);
|
|
res.json(rows.map(enrichNot));
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// POST /api/referentiel/:id/notation
|
|
router.post('/:id/notation', (req, res, next) => {
|
|
try {
|
|
const ref = db.prepare('SELECT id FROM plateformes_referentiel WHERE id = ?').get(req.params.id);
|
|
if (!ref) throw new HttpError(404, 'Referentiel introuvable');
|
|
|
|
const { nom, type = 'etoiles', valeurs, min_val, max_val, description, ordre } = req.body || {};
|
|
if (!nom?.trim()) throw new HttpError(400, 'nom est requis');
|
|
if (!TYPES_VALIDES_NOT.includes(type)) throw new HttpError(400, 'type invalide');
|
|
|
|
const r = db.prepare(`
|
|
INSERT INTO referentiel_notation (referentiel_id, nom, type, valeurs, min_val, max_val, description, ordre)
|
|
VALUES (?,?,?,?,?,?,?,?)
|
|
`).run(
|
|
ref.id, nom.trim(), type,
|
|
(type === 'lettres' || type === 'custom')
|
|
? JSON.stringify(Array.isArray(valeurs) ? valeurs : String(valeurs || '').split(',').map(v => v.trim()).filter(Boolean))
|
|
: null,
|
|
type === 'score' ? Number(min_val ?? 0) : null,
|
|
type === 'score' ? Number(max_val ?? 10) : null,
|
|
description?.trim() || null,
|
|
Number(ordre ?? 0),
|
|
);
|
|
res.status(201).json(enrichNot(db.prepare('SELECT * FROM referentiel_notation WHERE id = ?').get(r.lastInsertRowid)));
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// PUT /api/referentiel/notation/:notationId
|
|
router.put('/notation/:notationId', (req, res, next) => {
|
|
try {
|
|
const existing = db.prepare('SELECT * FROM referentiel_notation WHERE id = ?').get(req.params.notationId);
|
|
if (!existing) throw new HttpError(404, 'Critere introuvable');
|
|
|
|
const { nom, type, valeurs, min_val, max_val, description, ordre } = req.body || {};
|
|
const t = type || existing.type;
|
|
if (!nom?.trim()) throw new HttpError(400, 'nom est requis');
|
|
|
|
db.prepare(`
|
|
UPDATE referentiel_notation SET nom=?, type=?, valeurs=?, min_val=?, max_val=?, description=?, ordre=?
|
|
WHERE id=?
|
|
`).run(
|
|
nom.trim(), t,
|
|
(t === 'lettres' || t === 'custom')
|
|
? JSON.stringify(Array.isArray(valeurs) ? valeurs : String(valeurs || '').split(',').map(v => v.trim()).filter(Boolean))
|
|
: null,
|
|
t === 'score' ? Number(min_val ?? 0) : null,
|
|
t === 'score' ? Number(max_val ?? 10) : null,
|
|
description?.trim() || null,
|
|
Number(ordre ?? 0),
|
|
req.params.notationId,
|
|
);
|
|
res.json(enrichNot(db.prepare('SELECT * FROM referentiel_notation WHERE id = ?').get(req.params.notationId)));
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
// DELETE /api/referentiel/notation/:notationId
|
|
router.delete('/notation/:notationId', (req, res, next) => {
|
|
try {
|
|
const existing = db.prepare('SELECT id FROM referentiel_notation WHERE id = ?').get(req.params.notationId);
|
|
if (!existing) throw new HttpError(404, 'Critere introuvable');
|
|
db.prepare('DELETE FROM referentiel_notation WHERE id = ?').run(req.params.notationId);
|
|
res.json({ deleted: true });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
export default router;
|