Mise à jour des pages d'accueil

This commit is contained in:
2026-06-16 15:12:25 +02:00
parent 720634971d
commit 80506ca2dc
13 changed files with 168 additions and 217 deletions
+7
View File
@@ -1870,6 +1870,13 @@ console.log('[DB] Migrations 2FA OK');
db.exec("ALTER TABLE smtp_config ADD COLUMN min_password_length INTEGER NOT NULL DEFAULT 8");
console.log('[DB] Colonne smtp_config.min_password_length ajoutée');
}
// ── Migration : email sur investisseurs ───────────────────────────────
const invColsEmail = db.prepare('PRAGMA table_info(investisseurs)').all().map(c => c.name);
if (!invColsEmail.includes('email')) {
db.exec('ALTER TABLE investisseurs ADD COLUMN email TEXT');
console.log('[DB] Colonne investisseurs.email ajoutée');
}
}
export default db;
+25 -7
View File
@@ -11,12 +11,13 @@ const Schema = z.object({
type: z.enum(['famille', 'entreprise']).default('famille'),
type_fiscal: z.string().optional(),
notes: z.string().optional(),
email: z.string().email().optional().or(z.literal('')),
});
router.get('/', (req, res) => {
const rows = db
.prepare(
`SELECT id, nom, prenom, type, type_fiscal, is_principal, notes, created_at
`SELECT id, nom, prenom, type, type_fiscal, is_principal, notes, email, created_at
FROM investisseurs WHERE user_id = ?
ORDER BY is_principal DESC, type, nom`
)
@@ -29,7 +30,7 @@ router.post('/', (req, res, next) => {
const body = Schema.parse(req.body);
const r = db
.prepare(
'INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, notes) VALUES (?,?,?,?,?,?)'
'INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, notes, email) VALUES (?,?,?,?,?,?,?)'
)
.run(
req.user.id,
@@ -38,6 +39,7 @@ router.post('/', (req, res, next) => {
body.type,
body.type_fiscal || null,
body.notes || null,
body.email || null,
);
const invId = r.lastInsertRowid;
// Auto-créer un compte courant pour ce nouveau profil
@@ -56,7 +58,7 @@ router.put('/:id', (req, res, next) => {
const body = Schema.parse(req.body);
const r = db
.prepare(
`UPDATE investisseurs SET nom=?, prenom=?, type=?, type_fiscal=?, notes=?, updated_at=datetime('now')
`UPDATE investisseurs SET nom=?, prenom=?, type=?, type_fiscal=?, notes=?, email=?, updated_at=datetime('now')
WHERE id=? AND user_id=?`
)
.run(
@@ -65,6 +67,7 @@ router.put('/:id', (req, res, next) => {
body.type,
body.type_fiscal || null,
body.notes || null,
body.email || null,
req.params.id,
req.user.id,
);
@@ -115,10 +118,25 @@ router.delete('/:id', (req, res, next) => {
.get(req.user.id).n;
if (count <= 1) throw new HttpError(400, 'Impossible de supprimer le dernier profil.');
const r = db
.prepare('DELETE FROM investisseurs WHERE id=? AND user_id=?')
.run(req.params.id, req.user.id);
if (r.changes === 0) throw new HttpError(404, 'Not found');
// Récupérer le compte principal pour la réassignation
const principal = db
.prepare('SELECT id FROM investisseurs WHERE user_id=? AND is_principal=1')
.get(req.user.id);
if (!principal) throw new HttpError(500, 'Aucun compte principal trouvé pour la réassignation.');
const principalId = principal.id;
const targetId = Number(req.params.id);
// Réassigner toutes les données liées au compte principal avant suppression
const deleteWithReassign = db.transaction(() => {
db.prepare('UPDATE investissements SET investisseur_id=? WHERE investisseur_id=?').run(principalId, targetId);
db.prepare('UPDATE depots_retraits SET investisseur_id=? WHERE investisseur_id=?').run(principalId, targetId);
db.prepare('UPDATE plateformes SET investisseur_id=? WHERE investisseur_id=? AND user_id=?').run(principalId, targetId, req.user.id);
db.prepare('UPDATE comptes SET investisseur_id=? WHERE investisseur_id=? AND user_id=?').run(principalId, targetId, req.user.id);
db.prepare('DELETE FROM investisseurs WHERE id=? AND user_id=?').run(targetId, req.user.id);
});
deleteWithReassign();
res.status(204).end();
} catch (e) { next(e); }
});
+24 -15
View File
@@ -314,7 +314,7 @@ const zipUpload = multer({
router.get('/export', (req, res, next) => {
try {
let rows = db.prepare(`
SELECT p.*, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
SELECT p.*, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom, inv.email AS investisseur_email
FROM plateformes p
LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id
WHERE p.user_id = ?
@@ -358,6 +358,7 @@ router.get('/export', (req, res, next) => {
icone_filename: r.icone_filename,
investisseur_nom: r.investisseur_nom,
investisseur_prenom: r.investisseur_prenom,
investisseur_email: r.investisseur_email,
categories: legacyMap[r.id] || [],
categories_inv: (r.categories_inv || []).map(c => c.nom),
secteurs_inv: (r.secteurs_inv || []).map(s => s.nom),
@@ -368,7 +369,7 @@ router.get('/export', (req, res, next) => {
// Export des investisseurs (détenteurs) liés aux plateformes
const invIds = [...new Set(rows.map(r => r.investisseur_id).filter(Boolean))];
const invRows = invIds.length > 0
? db.prepare(`SELECT nom, prenom, type, type_fiscal, notes FROM investisseurs WHERE id IN (${invIds.map(() => '?').join(',')}) AND user_id = ?`).all(...invIds, req.user.id)
? db.prepare(`SELECT nom, prenom, type, type_fiscal, notes, email FROM investisseurs WHERE id IN (${invIds.map(() => '?').join(',')}) AND user_id = ?`).all(...invIds, req.user.id)
: [];
if (invRows.length > 0) {
entries.push({ name: 'investisseurs.json', data: JSON.stringify(invRows, null, 2) });
@@ -394,7 +395,7 @@ router.get('/export', (req, res, next) => {
router.get('/:id/export', (req, res, next) => {
try {
const p = db.prepare(`
SELECT p.*, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
SELECT p.*, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom, inv.email AS investisseur_email
FROM plateformes p LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id
WHERE p.id = ? AND p.user_id = ?
`).get(req.params.id, req.user.id);
@@ -421,7 +422,7 @@ router.get('/:id/export', (req, res, next) => {
date_ouverture: withAll.date_ouverture,
type_pret_defaut: withAll.type_pret_defaut, freq_interets_defaut: withAll.freq_interets_defaut,
logo_filename: withAll.logo_filename, icone_filename: withAll.icone_filename,
investisseur_nom: withAll.investisseur_nom, investisseur_prenom: withAll.investisseur_prenom,
investisseur_nom: withAll.investisseur_nom, investisseur_prenom: withAll.investisseur_prenom, investisseur_email: withAll.investisseur_email,
categories: catsLegacy,
categories_inv: (withAll.categories_inv || []).map(c => c.nom),
secteurs_inv: (withAll.secteurs_inv || []).map(s => s.nom),
@@ -430,7 +431,7 @@ router.get('/:id/export', (req, res, next) => {
// Export du détenteur lié à la plateforme
if (withAll.investisseur_id) {
const inv = db.prepare('SELECT nom, prenom, type, type_fiscal, notes FROM investisseurs WHERE id = ? AND user_id = ?').get(withAll.investisseur_id, req.user.id);
const inv = db.prepare('SELECT nom, prenom, type, type_fiscal, notes, email FROM investisseurs WHERE id = ? AND user_id = ?').get(withAll.investisseur_id, req.user.id);
if (inv) entries.push({ name: 'investisseurs.json', data: JSON.stringify([inv], null, 2) });
}
@@ -459,26 +460,34 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
const platforms = JSON.parse(dataEntry.data.toString('utf8'));
if (!Array.isArray(platforms)) throw new HttpError(400, 'data.json doit être un tableau');
// Importer les investisseurs du ZIP (créer les manquants)
// Importer les investisseurs du ZIP (créer les manquants, dédupliquer par email puis nom)
const invEntry = zipEntries.find(e => e.name === 'investisseurs.json');
if (invEntry) {
const invList = JSON.parse(invEntry.data.toString('utf8'));
for (const inv of (Array.isArray(invList) ? invList : [])) {
if (!inv.nom) continue;
const exists = db.prepare('SELECT id FROM investisseurs WHERE nom = ? AND user_id = ?').get(inv.nom, req.user.id);
if (!exists) {
db.prepare('INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, notes) VALUES (?,?,?,?,?,?)')
.run(req.user.id, inv.nom, inv.prenom ?? null, inv.type || 'famille', inv.type_fiscal ?? null, inv.notes ?? null);
// Déduplication : priorité à l'email, puis correspondance insensible à la casse sur le nom
const existsByEmail = inv.email
? db.prepare('SELECT id FROM investisseurs WHERE LOWER(email) = LOWER(?) AND user_id = ?').get(inv.email, req.user.id)
: null;
const existsByNom = db.prepare('SELECT id FROM investisseurs WHERE LOWER(nom) = LOWER(?) AND user_id = ?').get(inv.nom, req.user.id);
if (!existsByEmail && !existsByNom) {
db.prepare('INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, notes, email) VALUES (?,?,?,?,?,?,?)')
.run(req.user.id, inv.nom, inv.prenom ?? null, inv.type || 'famille', inv.type_fiscal ?? null, inv.notes ?? null, inv.email ?? null);
}
}
}
// Résoudre investisseur par nom+prénom (après création éventuelle)
// Résoudre investisseur — priorité email, puis nom+prénom insensible à la casse
const userInvestisseurs = db.prepare('SELECT * FROM investisseurs WHERE user_id = ?').all(req.user.id);
function resolveInvestisseur(nom, prenom) {
if (!nom) return userInvestisseurs[0]?.id ?? null;
function resolveInvestisseur(nom, prenom, email) {
if (!nom && !email) return userInvestisseurs[0]?.id ?? null;
if (email) {
const byEmail = userInvestisseurs.find(i => i.email?.toLowerCase() === email.toLowerCase());
if (byEmail) return byEmail.id;
}
const match = userInvestisseurs.find(i =>
i.nom?.toLowerCase() === nom?.toLowerCase() && i.prenom?.toLowerCase() === prenom?.toLowerCase()
i.nom?.toLowerCase() === nom?.toLowerCase() && i.prenom?.toLowerCase() === (prenom || '').toLowerCase()
) || userInvestisseurs.find(i => i.nom?.toLowerCase() === nom?.toLowerCase());
return match?.id ?? userInvestisseurs[0]?.id ?? null;
}
@@ -527,7 +536,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
if (!p.nom) continue;
const fiscalite = p.domiciliation === 'FR' ? 'flat_tax' : (p.fiscalite || 'flat_tax');
const taux = fiscalite === 'avec_fiscalite_locale' ? (p.taux_fiscalite_locale ?? null) : null;
const investisseurId = resolveInvestisseur(p.investisseur_nom, p.investisseur_prenom);
const investisseurId = resolveInvestisseur(p.investisseur_nom, p.investisseur_prenom, p.investisseur_email);
const catsInvIds = resolveTagIds(p.categories_inv, 'categories_inv');
const sectsInvIds = resolveTagIds(p.secteurs_inv, 'secteurs_inv');
const legacyCatIds = resolveLegacyCatIds(p.categories);