Initial commit
This commit is contained in:
commit
48ed7fe65e
209 files changed
+49979
No files matched your search
@@ -0,0 +1,884 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../db/index.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/dashboard
|
||||
*
|
||||
* ?scope=all → agrège tous les investisseurs de l'utilisateur
|
||||
* (défaut) → filtre sur l'investisseur donné par X-Investisseur-Id
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
const userId = req.user.id;
|
||||
|
||||
/* ── Résolution de l'investisseur cible ─────────────────────── */
|
||||
let invWhere, invParams, invId;
|
||||
|
||||
if (scopeAll) {
|
||||
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
|
||||
invParams = [userId];
|
||||
} else {
|
||||
const raw = req.header('X-Investisseur-Id');
|
||||
invId = Number(raw);
|
||||
if (!invId) return res.status(400).json({ error: 'Missing investisseur id (header X-Investisseur-Id)' });
|
||||
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
|
||||
if (!row) return res.status(403).json({ error: 'Investisseur not found or not owned by user' });
|
||||
invWhere = 'investisseur_id = ?';
|
||||
invParams = [invId];
|
||||
}
|
||||
|
||||
/* ── Cash global ─────────────────────────────────────────────── */
|
||||
const cash = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN type='depot' THEN montant END),0) AS total_depots,
|
||||
COALESCE(SUM(CASE WHEN type='retrait' THEN montant END),0) AS total_retraits
|
||||
FROM depots_retraits WHERE ${invWhere}
|
||||
`).get(...invParams);
|
||||
|
||||
/* ── Cash par plateforme ─────────────────────────────────────── */
|
||||
const cashByPlatform = db.prepare(`
|
||||
SELECT p.id AS plateforme_id, p.nom,
|
||||
plat_inv.nom AS detenteur_nom,
|
||||
COALESCE(SUM(CASE WHEN dr.type='depot' THEN dr.montant END),0) AS depots,
|
||||
COALESCE(SUM(CASE WHEN dr.type='retrait' THEN dr.montant END),0) AS retraits,
|
||||
COALESCE(SUM(CASE WHEN dr.type='depot' THEN dr.montant
|
||||
WHEN dr.type='retrait' THEN -dr.montant END),0) AS solde_net
|
||||
FROM plateformes p
|
||||
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
|
||||
LEFT JOIN depots_retraits dr ON dr.plateforme_id = p.id AND ${invWhere.replace('investisseur_id', 'dr.investisseur_id')}
|
||||
WHERE p.user_id = ?
|
||||
GROUP BY p.id, p.nom, plat_inv.nom
|
||||
ORDER BY p.nom
|
||||
`).all(...invParams, userId);
|
||||
|
||||
/* ── Solde porte-monnaie par plateforme ──────────────────────── */
|
||||
const drManuelPerPlat = db.prepare(`
|
||||
SELECT plateforme_id,
|
||||
COALESCE(SUM(CASE WHEN type='depot' THEN montant ELSE 0 END), 0) AS depots,
|
||||
COALESCE(SUM(CASE WHEN type='retrait' AND COALESCE(source,'') != 'auto_remboursement'
|
||||
THEN montant ELSE 0 END), 0) AS retraits_manuels
|
||||
FROM depots_retraits
|
||||
WHERE ${invWhere}
|
||||
GROUP BY plateforme_id
|
||||
`).all(...invParams);
|
||||
|
||||
const rembWalletPerPlat = db.prepare(`
|
||||
SELECT i.plateforme_id,
|
||||
COALESCE(SUM(
|
||||
CASE p.fiscalite
|
||||
WHEN 'flat_tax' THEN r.net_recu
|
||||
-- Plateformes hors France : pas de PFU prélevé à la source.
|
||||
-- interets_bruts est déjà net de la retenue locale si applicable.
|
||||
ELSE r.capital + r.cashback + r.interets_bruts
|
||||
END
|
||||
), 0) AS remb_wallet
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND r.type = 'normal'
|
||||
AND r.methode_remboursement = 'portefeuille'
|
||||
GROUP BY i.plateforme_id
|
||||
`).all(...invParams);
|
||||
|
||||
const bonusWalletPerPlat = db.prepare(`
|
||||
SELECT bonus_plateforme_id AS plateforme_id,
|
||||
COALESCE(SUM(cashback), 0) AS bonus_wallet
|
||||
FROM remboursements
|
||||
WHERE ${invWhere.replace('investisseur_id', 'bonus_investisseur_id')}
|
||||
AND type IN ('bonus_parrainage', 'bonus_plateforme')
|
||||
GROUP BY bonus_plateforme_id
|
||||
`).all(...invParams);
|
||||
|
||||
const capitalInvestiPerPlat = db.prepare(`
|
||||
SELECT i.plateforme_id,
|
||||
COALESCE(SUM(
|
||||
i.montant_investi
|
||||
+ COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
|
||||
), 0) AS capital_investi
|
||||
FROM investissements i
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
GROUP BY i.plateforme_id
|
||||
`).all(...invParams);
|
||||
|
||||
// Corrections de solde (micro-ecarts fiscaux sur plateformes flat_tax)
|
||||
const correctionsPerPlat = db.prepare(`
|
||||
SELECT plateforme_id,
|
||||
COALESCE(SUM(montant), 0) AS total_correction
|
||||
FROM corrections_solde
|
||||
WHERE ${invWhere}
|
||||
GROUP BY plateforme_id
|
||||
`).all(...invParams);
|
||||
|
||||
// Fusion en une map plateforme_id → solde_portefeuille
|
||||
const walletMap = {};
|
||||
for (const r of drManuelPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) + r.depots - r.retraits_manuels;
|
||||
for (const r of rembWalletPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) + r.remb_wallet;
|
||||
for (const r of bonusWalletPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) + r.bonus_wallet;
|
||||
for (const r of capitalInvestiPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) - r.capital_investi;
|
||||
for (const r of correctionsPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) + r.total_correction;
|
||||
|
||||
const cashByPlatformEnriched = cashByPlatform.map(p => ({
|
||||
...p,
|
||||
solde_portefeuille: Math.round((walletMap[p.plateforme_id] || 0) * 100) / 100,
|
||||
}));
|
||||
|
||||
const solde_portefeuille_total = Math.round(
|
||||
cashByPlatformEnriched.reduce((s, p) => s + p.solde_portefeuille, 0) * 100
|
||||
) / 100;
|
||||
|
||||
/* ── Portfolio ───────────────────────────────────────────────── */
|
||||
const portfolio = db.prepare(`
|
||||
SELECT
|
||||
COUNT(*) AS nb_projets,
|
||||
COALESCE(SUM(i.montant_investi),0) AS total_investi,
|
||||
COALESCE(SUM(CASE WHEN i.statut='en_cours' THEN
|
||||
i.montant_investi
|
||||
+ COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
|
||||
- COALESCE((SELECT SUM(rb.capital) FROM remboursements rb WHERE rb.investissement_id = i.id AND rb.type = 'normal'), 0)
|
||||
END), 0) AS encours,
|
||||
COALESCE(SUM(CASE WHEN i.statut='rembourse' THEN i.montant_investi END),0) AS rembourse,
|
||||
COALESCE(SUM(CASE WHEN i.statut IN ('en_retard','procedure') THEN
|
||||
i.montant_investi
|
||||
+ COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
|
||||
- COALESCE((SELECT SUM(rb.capital) FROM remboursements rb WHERE rb.investissement_id = i.id AND rb.type = 'normal'), 0)
|
||||
END), 0) AS en_defaut
|
||||
FROM investissements i WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
`).get(...invParams);
|
||||
|
||||
/* ── Interets ────────────────────────────────────────────────── */
|
||||
const interets = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(r.interets_bruts),0) AS interets_bruts,
|
||||
COALESCE(SUM(r.prelev_sociaux),0) AS prelev_sociaux,
|
||||
COALESCE(SUM(r.prelev_forfaitaire),0) AS prelev_forfaitaire,
|
||||
COALESCE(SUM(r.net_recu),0) AS net_recu
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
`).get(...invParams);
|
||||
|
||||
/* ── Interets par annee ──────────────────────────────────────── */
|
||||
const interetsParAnnee = scopeAll
|
||||
? db.prepare(`
|
||||
SELECT annee,
|
||||
SUM(interets_bruts) AS interets_bruts,
|
||||
SUM(prelev_sociaux) AS prelev_sociaux,
|
||||
SUM(prelev_forfaitaire) AS prelev_forfaitaire,
|
||||
SUM(net_recu) AS net_recu
|
||||
FROM v_interets_annuels
|
||||
WHERE investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)
|
||||
GROUP BY annee ORDER BY annee DESC
|
||||
`).all(userId)
|
||||
: db.prepare(`
|
||||
SELECT * FROM v_interets_annuels WHERE investisseur_id = ? ORDER BY annee DESC
|
||||
`).all(invId);
|
||||
|
||||
/* ── Prochaines echeances ────────────────────────────────────── */
|
||||
const upcoming = db.prepare(`
|
||||
SELECT s.*, i.nom_projet, p.nom AS plateforme_nom,
|
||||
plat_inv.nom AS plateforme_detenteur_nom,
|
||||
(SELECT r.date_remb FROM remboursements r
|
||||
WHERE r.investissement_id = s.investissement_id
|
||||
AND r.date_remb = s.date_prevue
|
||||
LIMIT 1) AS remb_date_exact,
|
||||
(SELECT r.date_remb FROM remboursements r
|
||||
WHERE r.investissement_id = s.investissement_id
|
||||
AND strftime('%Y-%m', r.date_remb) = strftime('%Y-%m', s.date_prevue)
|
||||
AND r.date_remb != s.date_prevue
|
||||
LIMIT 1) AS remb_date_mois
|
||||
FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND s.date_prevue >= date('now','start of month')
|
||||
AND s.date_prevue < date('now','start of month','+1 month')
|
||||
ORDER BY s.date_prevue
|
||||
LIMIT 50
|
||||
`).all(...invParams);
|
||||
|
||||
/* ── En retard / procedure ───────────────────────────────────── */
|
||||
const enDefaut = db.prepare(`
|
||||
SELECT i.*, p.nom AS plateforme_nom,
|
||||
plat_inv.nom AS plateforme_detenteur_nom
|
||||
FROM investissements i
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND i.statut IN ('en_retard','procedure')
|
||||
ORDER BY i.date_souscription DESC
|
||||
`).all(...invParams);
|
||||
|
||||
res.json({ cash, cashByPlatform: cashByPlatformEnriched, solde_portefeuille_total, portfolio, interets, interetsParAnnee, upcoming, enDefaut });
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/dashboard/solde-historique
|
||||
*
|
||||
* Retourne le solde porte-monnaie calculé pour une plateforme à une date donnée.
|
||||
* Utilise la même formule que solde_portefeuille mais filtrée par date <= :date.
|
||||
*
|
||||
* Query params : plateforme_id (requis), date YYYY-MM-DD (requis)
|
||||
*/
|
||||
router.get('/solde-historique', (req, res) => {
|
||||
const { plateforme_id, date } = req.query;
|
||||
if (!plateforme_id || !date) {
|
||||
return res.status(400).json({ error: 'plateforme_id et date sont requis' });
|
||||
}
|
||||
|
||||
const userId = req.user.id;
|
||||
const platId = Number(plateforme_id);
|
||||
|
||||
// Vérifier que la plateforme appartient à l'utilisateur
|
||||
const plat = db.prepare('SELECT id FROM plateformes WHERE id = ? AND user_id = ?').get(platId, userId);
|
||||
if (!plat) return res.status(403).json({ error: 'Plateforme non trouvée' });
|
||||
|
||||
/* ── Résolution du scope investisseur (même logique que GET /) ── */
|
||||
let invWhere, invParams;
|
||||
const raw = req.header('X-Investisseur-Id');
|
||||
const invId = Number(raw);
|
||||
|
||||
if (invId) {
|
||||
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
|
||||
if (!row) return res.status(403).json({ error: 'Investisseur non trouvé' });
|
||||
invWhere = 'investisseur_id = ?';
|
||||
invParams = [invId];
|
||||
} else {
|
||||
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
|
||||
invParams = [userId];
|
||||
}
|
||||
|
||||
/* ── 1. Dépôts − retraits manuels (jusqu'à :date) ─────────────── */
|
||||
const dr = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN type='depot' THEN montant ELSE 0 END), 0) AS depots,
|
||||
COALESCE(SUM(CASE WHEN type='retrait'
|
||||
AND COALESCE(source,'') != 'auto_remboursement'
|
||||
THEN montant ELSE 0 END), 0) AS retraits_manuels
|
||||
FROM depots_retraits
|
||||
WHERE plateforme_id = ? AND ${invWhere} AND date_operation <= ?
|
||||
`).get(platId, ...invParams, date);
|
||||
|
||||
/* ── 2. Remboursements vers porte-monnaie (jusqu'à :date) ──────── */
|
||||
const remb = db.prepare(`
|
||||
SELECT COALESCE(SUM(
|
||||
CASE p.fiscalite
|
||||
WHEN 'flat_tax' THEN r.net_recu
|
||||
ELSE r.capital + r.cashback + r.interets_bruts
|
||||
END
|
||||
), 0) AS remb_wallet
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE i.plateforme_id = ? AND ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND r.type = 'normal' AND r.methode_remboursement = 'portefeuille'
|
||||
AND r.date_remb <= ?
|
||||
`).get(platId, ...invParams, date);
|
||||
|
||||
/* ── 3. Bonus porte-monnaie (jusqu'à :date) ────────────────────── */
|
||||
const bonus = db.prepare(`
|
||||
SELECT COALESCE(SUM(cashback), 0) AS bonus_wallet
|
||||
FROM remboursements
|
||||
WHERE bonus_plateforme_id = ? AND ${invWhere.replace('investisseur_id', 'bonus_investisseur_id')}
|
||||
AND type IN ('bonus_parrainage', 'bonus_plateforme')
|
||||
AND date_remb <= ?
|
||||
`).get(platId, ...invParams, date);
|
||||
|
||||
/* ── 4. Capital investi (souscriptions + réinvestissements jusqu'à :date) ── */
|
||||
const capital = db.prepare(`
|
||||
SELECT COALESCE(SUM(
|
||||
i.montant_investi
|
||||
+ COALESCE((
|
||||
SELECT SUM(rv.montant)
|
||||
FROM reinvestissements rv
|
||||
WHERE rv.investissement_id = i.id
|
||||
AND rv.date_reinvestissement <= ?
|
||||
), 0)
|
||||
), 0) AS capital_investi
|
||||
FROM investissements i
|
||||
WHERE i.plateforme_id = ? AND ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND i.date_souscription <= ?
|
||||
`).get(date, platId, ...invParams, date);
|
||||
|
||||
/* ── 5. Corrections de solde (jusqu'à :date) ───────────────────── */
|
||||
const corrections = db.prepare(`
|
||||
SELECT COALESCE(SUM(montant), 0) AS total_correction
|
||||
FROM corrections_solde
|
||||
WHERE plateforme_id = ? AND ${invWhere} AND date <= ?
|
||||
`).get(platId, ...invParams, date);
|
||||
|
||||
const solde = Math.round((
|
||||
dr.depots
|
||||
- dr.retraits_manuels
|
||||
+ remb.remb_wallet
|
||||
+ bonus.bonus_wallet
|
||||
- capital.capital_investi
|
||||
+ corrections.total_correction
|
||||
) * 100) / 100;
|
||||
|
||||
res.json({ solde });
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/dashboard/interets-mensuels
|
||||
*
|
||||
* Retourne les intérêts réels (remboursements) et projetés (simul_remboursements)
|
||||
* groupés par mois YYYY-MM pour l'année demandée.
|
||||
*
|
||||
* Query params : annee (int, défaut = année courante), scope=all (optionnel)
|
||||
*/
|
||||
router.get('/interets-mensuels', (req, res) => {
|
||||
const annee = parseInt(req.query.annee) || new Date().getFullYear();
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
const userId = req.user.id;
|
||||
const anneeStr = String(annee);
|
||||
|
||||
let invWhere, invParams, invId;
|
||||
|
||||
if (scopeAll) {
|
||||
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
|
||||
invParams = [userId];
|
||||
} else {
|
||||
const raw = req.header('X-Investisseur-Id');
|
||||
invId = Number(raw);
|
||||
if (!invId) return res.status(400).json({ error: 'Missing investisseur id (header X-Investisseur-Id)' });
|
||||
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
|
||||
if (!row) return res.status(403).json({ error: 'Investisseur not found or not owned by user' });
|
||||
invWhere = 'investisseur_id = ?';
|
||||
invParams = [invId];
|
||||
}
|
||||
|
||||
/* ── Intérêts réels groupés par mois ─────────────────────────── */
|
||||
// Inclut les remboursements normaux (via investissement) ET les bonus (via bonus_investisseur_id)
|
||||
let rembourses;
|
||||
if (scopeAll) {
|
||||
rembourses = db.prepare(`
|
||||
SELECT
|
||||
strftime('%Y-%m', r.date_remb) AS mois,
|
||||
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
|
||||
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
|
||||
COALESCE(SUM(r.capital), 0) AS capital,
|
||||
COALESCE(SUM(r.cashback), 0) AS cashback
|
||||
FROM remboursements r
|
||||
LEFT JOIN investissements inv ON inv.id = r.investissement_id
|
||||
LEFT JOIN investisseurs own ON own.id = inv.investisseur_id
|
||||
LEFT JOIN investisseurs bonus_own ON bonus_own.id = r.bonus_investisseur_id
|
||||
WHERE strftime('%Y', r.date_remb) = ?
|
||||
AND (
|
||||
(r.investissement_id IS NOT NULL AND own.user_id = ?)
|
||||
OR (r.bonus_investisseur_id IS NOT NULL AND bonus_own.user_id = ?)
|
||||
)
|
||||
GROUP BY mois
|
||||
ORDER BY mois
|
||||
`).all(anneeStr, userId, userId);
|
||||
} else {
|
||||
rembourses = db.prepare(`
|
||||
SELECT
|
||||
strftime('%Y-%m', r.date_remb) AS mois,
|
||||
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
|
||||
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
|
||||
COALESCE(SUM(r.capital), 0) AS capital,
|
||||
COALESCE(SUM(r.cashback), 0) AS cashback
|
||||
FROM remboursements r
|
||||
LEFT JOIN investissements inv ON inv.id = r.investissement_id
|
||||
LEFT JOIN investisseurs bonus_own ON bonus_own.id = r.bonus_investisseur_id
|
||||
WHERE strftime('%Y', r.date_remb) = ?
|
||||
AND (
|
||||
(r.investissement_id IS NOT NULL AND inv.investisseur_id = ?)
|
||||
OR (r.bonus_investisseur_id IS NOT NULL AND r.bonus_investisseur_id = ?)
|
||||
)
|
||||
GROUP BY mois
|
||||
ORDER BY mois
|
||||
`).all(anneeStr, invId, invId);
|
||||
}
|
||||
|
||||
/* ── Projections groupées par mois ───────────────────────────── */
|
||||
// Exclure les investissements qui ont déjà un remboursement réel dans le même mois
|
||||
// (évite le double-comptage pour le mois en cours)
|
||||
const projections = db.prepare(`
|
||||
SELECT
|
||||
strftime('%Y-%m', s.date_prevue) AS mois,
|
||||
COALESCE(SUM(s.interets_prevus), 0) AS interets_prevus,
|
||||
COALESCE(SUM(s.capital_prevu), 0) AS capital_prevu
|
||||
FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND strftime('%Y', s.date_prevue) = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM remboursements r
|
||||
WHERE r.investissement_id = s.investissement_id
|
||||
AND strftime('%Y-%m', r.date_remb) = strftime('%Y-%m', s.date_prevue)
|
||||
)
|
||||
GROUP BY mois
|
||||
ORDER BY mois
|
||||
`).all(...invParams, anneeStr);
|
||||
|
||||
/* ── Années disponibles (remboursements + projections) ─── */
|
||||
const annees = db.prepare(`
|
||||
SELECT DISTINCT CAST(strftime('%Y', r.date_remb) AS INTEGER) AS annee
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
UNION
|
||||
SELECT DISTINCT CAST(strftime('%Y', s.date_prevue) AS INTEGER) AS annee
|
||||
FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND CAST(strftime('%Y', s.date_prevue) AS INTEGER) <= CAST(strftime('%Y', 'now') AS INTEGER) + 10
|
||||
ORDER BY annee
|
||||
`).all(...invParams, ...invParams).map(r => r.annee);
|
||||
|
||||
res.json({ rembourses, projections, annees });
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/dashboard/interets-annuels
|
||||
*
|
||||
* Retourne les intérêts réels et projetés groupés par ANNÉE (toutes années).
|
||||
* Les projections excluent les échéances déjà passées (>= début du mois courant).
|
||||
*
|
||||
* Query params : scope=all (optionnel)
|
||||
*/
|
||||
router.get('/interets-annuels', (req, res) => {
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
const userId = req.user.id;
|
||||
|
||||
let invWhere, invParams, invId;
|
||||
|
||||
if (scopeAll) {
|
||||
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
|
||||
invParams = [userId];
|
||||
} else {
|
||||
const raw = req.header('X-Investisseur-Id');
|
||||
invId = Number(raw);
|
||||
if (!invId) return res.status(400).json({ error: 'Missing investisseur id (header X-Investisseur-Id)' });
|
||||
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
|
||||
if (!row) return res.status(403).json({ error: 'Investisseur not found or not owned by user' });
|
||||
invWhere = 'investisseur_id = ?';
|
||||
invParams = [invId];
|
||||
}
|
||||
|
||||
/* ── Intérêts réels groupés par année ────────────────────────── */
|
||||
let rembourses;
|
||||
if (scopeAll) {
|
||||
rembourses = db.prepare(`
|
||||
SELECT
|
||||
CAST(strftime('%Y', r.date_remb) AS INTEGER) AS annee,
|
||||
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
|
||||
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
|
||||
COALESCE(SUM(r.capital), 0) AS capital,
|
||||
COALESCE(SUM(r.cashback), 0) AS cashback
|
||||
FROM remboursements r
|
||||
LEFT JOIN investissements inv ON inv.id = r.investissement_id
|
||||
LEFT JOIN investisseurs own ON own.id = inv.investisseur_id
|
||||
LEFT JOIN investisseurs bonus_own ON bonus_own.id = r.bonus_investisseur_id
|
||||
WHERE (
|
||||
(r.investissement_id IS NOT NULL AND own.user_id = ?)
|
||||
OR (r.bonus_investisseur_id IS NOT NULL AND bonus_own.user_id = ?)
|
||||
)
|
||||
GROUP BY annee
|
||||
ORDER BY annee
|
||||
`).all(userId, userId);
|
||||
} else {
|
||||
rembourses = db.prepare(`
|
||||
SELECT
|
||||
CAST(strftime('%Y', r.date_remb) AS INTEGER) AS annee,
|
||||
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
|
||||
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
|
||||
COALESCE(SUM(r.capital), 0) AS capital,
|
||||
COALESCE(SUM(r.cashback), 0) AS cashback
|
||||
FROM remboursements r
|
||||
LEFT JOIN investissements inv ON inv.id = r.investissement_id
|
||||
LEFT JOIN investisseurs bonus_own ON bonus_own.id = r.bonus_investisseur_id
|
||||
WHERE (
|
||||
(r.investissement_id IS NOT NULL AND inv.investisseur_id = ?)
|
||||
OR (r.bonus_investisseur_id IS NOT NULL AND r.bonus_investisseur_id = ?)
|
||||
)
|
||||
GROUP BY annee
|
||||
ORDER BY annee
|
||||
`).all(invId, invId);
|
||||
}
|
||||
|
||||
/* ── Projections groupées par année (uniquement futures/en cours) */
|
||||
const projections = db.prepare(`
|
||||
SELECT
|
||||
CAST(strftime('%Y', s.date_prevue) AS INTEGER) AS annee,
|
||||
COALESCE(SUM(s.interets_prevus), 0) AS interets_prevus,
|
||||
COALESCE(SUM(s.capital_prevu), 0) AS capital_prevu
|
||||
FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND s.date_prevue >= date('now', 'start of month')
|
||||
GROUP BY annee
|
||||
ORDER BY annee
|
||||
`).all(...invParams);
|
||||
|
||||
/* ── Années disponibles (remboursements + projections) ───────── */
|
||||
const annees = db.prepare(`
|
||||
SELECT DISTINCT CAST(strftime('%Y', r.date_remb) AS INTEGER) AS annee
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
UNION
|
||||
SELECT DISTINCT CAST(strftime('%Y', s.date_prevue) AS INTEGER) AS annee
|
||||
FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND CAST(strftime('%Y', s.date_prevue) AS INTEGER) <= CAST(strftime('%Y', 'now') AS INTEGER) + 10
|
||||
ORDER BY annee
|
||||
`).all(...invParams, ...invParams).map(r => r.annee);
|
||||
|
||||
/* ── Capital souscrit par année (pour KPI "Capital investi" filtré) ── */
|
||||
const capitalParAnnee = db.prepare(`
|
||||
SELECT
|
||||
CAST(strftime('%Y', i.date_souscription) AS INTEGER) AS annee,
|
||||
COALESCE(SUM(
|
||||
i.montant_investi
|
||||
+ COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
|
||||
), 0) AS capital_souscrit
|
||||
FROM investissements i
|
||||
WHERE ${invWhere}
|
||||
GROUP BY annee
|
||||
ORDER BY annee
|
||||
`).all(...invParams);
|
||||
|
||||
res.json({ rembourses, projections, annees, capitalParAnnee });
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/dashboard/interets-par-plateforme
|
||||
*
|
||||
* Retourne les intérêts réels + projetés groupés par plateforme et par mois
|
||||
* pour l'année demandée, ainsi que le capital investi estimé chaque mois.
|
||||
*
|
||||
* Query params : annee (int, défaut = année courante), scope=all (optionnel)
|
||||
*/
|
||||
router.get('/interets-par-plateforme', (req, res) => {
|
||||
const annee = parseInt(req.query.annee) || new Date().getFullYear();
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
const userId = req.user.id;
|
||||
const anneeStr = String(annee);
|
||||
|
||||
let invWhere, invParams, invId;
|
||||
|
||||
if (scopeAll) {
|
||||
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
|
||||
invParams = [userId];
|
||||
} else {
|
||||
const raw = req.header('X-Investisseur-Id');
|
||||
invId = Number(raw);
|
||||
if (!invId) return res.status(400).json({ error: 'Missing investisseur id (header X-Investisseur-Id)' });
|
||||
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
|
||||
if (!row) return res.status(403).json({ error: 'Investisseur not found or not owned by user' });
|
||||
invWhere = 'investisseur_id = ?';
|
||||
invParams = [invId];
|
||||
}
|
||||
|
||||
/* ── Intérêts réels par plateforme et par mois ───────────────── */
|
||||
const remboursesByPlat = db.prepare(`
|
||||
SELECT
|
||||
p.id AS plateforme_id,
|
||||
p.nom AS plateforme_nom,
|
||||
strftime('%Y-%m', r.date_remb) AS mois,
|
||||
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
|
||||
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
|
||||
COALESCE(SUM(r.cashback), 0) AS cashback,
|
||||
COALESCE(SUM(r.capital), 0) AS capital
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND strftime('%Y', r.date_remb) = ?
|
||||
AND r.type = 'normal'
|
||||
GROUP BY p.id, p.nom, mois
|
||||
ORDER BY p.nom, mois
|
||||
`).all(...invParams, anneeStr);
|
||||
|
||||
/* ── Cashback bonus (parrainage / plateforme) par plateforme et par mois ── */
|
||||
const bonusCashbackByPlat = db.prepare(`
|
||||
SELECT
|
||||
r.bonus_plateforme_id AS plateforme_id,
|
||||
p.nom AS plateforme_nom,
|
||||
strftime('%Y-%m', r.date_remb) AS mois,
|
||||
COALESCE(SUM(r.cashback), 0) AS cashback
|
||||
FROM remboursements r
|
||||
JOIN plateformes p ON p.id = r.bonus_plateforme_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'bonus_investisseur_id')}
|
||||
AND r.type IN ('bonus_parrainage', 'bonus_plateforme')
|
||||
AND strftime('%Y', r.date_remb) = ?
|
||||
GROUP BY r.bonus_plateforme_id, p.nom, mois
|
||||
ORDER BY p.nom, mois
|
||||
`).all(...invParams, anneeStr);
|
||||
|
||||
/* ── Projections par plateforme et par mois ──────────────────── */
|
||||
// Exclure les investissements qui ont déjà un remboursement réel dans le même mois
|
||||
const projectionsByPlat = db.prepare(`
|
||||
SELECT
|
||||
p.id AS plateforme_id,
|
||||
p.nom AS plateforme_nom,
|
||||
strftime('%Y-%m', s.date_prevue) AS mois,
|
||||
COALESCE(SUM(s.interets_prevus), 0) AS interets_prevus,
|
||||
COALESCE(SUM(s.capital_prevu), 0) AS capital_prevu
|
||||
FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
AND strftime('%Y', s.date_prevue) = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM remboursements r
|
||||
WHERE r.investissement_id = s.investissement_id
|
||||
AND strftime('%Y-%m', r.date_remb) = strftime('%Y-%m', s.date_prevue)
|
||||
)
|
||||
GROUP BY p.id, p.nom, mois
|
||||
ORDER BY p.nom, mois
|
||||
`).all(...invParams, anneeStr);
|
||||
|
||||
/* ── Capital investi mensuel ─────────────────────────────────── */
|
||||
// Récupérer tous les investissements avec leurs dates pour calculer
|
||||
// le capital restant dû chaque mois (montant_investi + reinvests − capital_remboursé_cumulé)
|
||||
const invs = db.prepare(`
|
||||
SELECT
|
||||
i.id,
|
||||
i.montant_investi,
|
||||
i.date_souscription,
|
||||
i.date_cible,
|
||||
i.statut,
|
||||
(SELECT MAX(r.date_remb) FROM remboursements r WHERE r.investissement_id = i.id) AS last_remb_date
|
||||
FROM investissements i
|
||||
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
|
||||
`).all(...invParams);
|
||||
|
||||
const invIds = invs.map(i => i.id);
|
||||
const placeholder = invIds.map(() => '?').join(',');
|
||||
|
||||
// Remboursements de capital par investissement avec leur date (tous types normaux)
|
||||
const capitalRembRows = invIds.length ? db.prepare(`
|
||||
SELECT investissement_id, date_remb, SUM(capital) AS capital_remb
|
||||
FROM remboursements
|
||||
WHERE investissement_id IN (${placeholder})
|
||||
AND type = 'normal'
|
||||
GROUP BY investissement_id, date_remb
|
||||
`).all(...invIds) : [];
|
||||
|
||||
// Réinvestissements par investissement avec leur date
|
||||
const reinvestRows = invIds.length ? db.prepare(`
|
||||
SELECT investissement_id, date_reinvestissement, SUM(montant) AS montant
|
||||
FROM reinvestissements
|
||||
WHERE investissement_id IN (${placeholder})
|
||||
GROUP BY investissement_id, date_reinvestissement
|
||||
`).all(...invIds) : [];
|
||||
|
||||
// Capital projeté par investissement (simul_remboursements) — pour les mois futurs
|
||||
const capitalSimulRows = invIds.length ? db.prepare(`
|
||||
SELECT investissement_id, date_prevue, SUM(capital_prevu) AS capital_prevu
|
||||
FROM simul_remboursements
|
||||
WHERE investissement_id IN (${placeholder})
|
||||
GROUP BY investissement_id, date_prevue
|
||||
`).all(...invIds) : [];
|
||||
|
||||
// Map : inv_id → [{date, capital}]
|
||||
const capitalRembMap = {};
|
||||
for (const r of capitalRembRows) {
|
||||
if (!capitalRembMap[r.investissement_id]) capitalRembMap[r.investissement_id] = [];
|
||||
capitalRembMap[r.investissement_id].push({ date: r.date_remb, capital: r.capital_remb });
|
||||
}
|
||||
|
||||
// Map : inv_id → [{date, montant}]
|
||||
const reinvestMap = {};
|
||||
for (const r of reinvestRows) {
|
||||
if (!reinvestMap[r.investissement_id]) reinvestMap[r.investissement_id] = [];
|
||||
reinvestMap[r.investissement_id].push({ date: r.date_reinvestissement, montant: r.montant });
|
||||
}
|
||||
|
||||
// Map : inv_id → [{date, capital}]
|
||||
const capitalSimulMap = {};
|
||||
for (const r of capitalSimulRows) {
|
||||
if (!capitalSimulMap[r.investissement_id]) capitalSimulMap[r.investissement_id] = [];
|
||||
capitalSimulMap[r.investissement_id].push({ date: r.date_prevue, capital: r.capital_prevu });
|
||||
}
|
||||
|
||||
// Date du jour au format YYYY-MM-DD (locale) pour distinguer passé/futur
|
||||
const _today = new Date();
|
||||
const todayStr = `${_today.getFullYear()}-${String(_today.getMonth() + 1).padStart(2, '0')}-${String(_today.getDate()).padStart(2, '0')}`;
|
||||
|
||||
const capitalMensuel = [];
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
const moisStr = `${anneeStr}-${String(m).padStart(2, '0')}`;
|
||||
const firstDay = `${moisStr}-01`;
|
||||
// Dernier jour du mois (calcul local pour éviter le décalage UTC)
|
||||
const _d = new Date(annee, m, 0);
|
||||
const lastDay = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, '0')}-${String(_d.getDate()).padStart(2, '0')}`;
|
||||
|
||||
let capital = 0;
|
||||
let enDefaut = 0;
|
||||
for (const inv of invs) {
|
||||
if (inv.date_souscription > lastDay) continue; // pas encore débuté
|
||||
|
||||
// Capital réellement remboursé jusqu'à la fin de ce mois
|
||||
const rembs = capitalRembMap[inv.id] ?? [];
|
||||
const capitalRembCumul = rembs
|
||||
.filter(r => r.date <= lastDay)
|
||||
.reduce((s, r) => s + r.capital, 0);
|
||||
|
||||
// Capital projeté pour les échéances futures (après le dernier remb réel et après aujourd'hui)
|
||||
const lastRembDate = inv.last_remb_date ?? '0000-00-00';
|
||||
const capitalSimulCumul = (capitalSimulMap[inv.id] ?? [])
|
||||
.filter(r => r.date > todayStr && r.date > lastRembDate && r.date <= lastDay)
|
||||
.reduce((s, r) => s + r.capital, 0);
|
||||
|
||||
const reinvestsCumul = (reinvestMap[inv.id] ?? [])
|
||||
.filter(r => r.date <= lastDay)
|
||||
.reduce((s, r) => s + r.montant, 0);
|
||||
const capitalRestant = Math.max(0, inv.montant_investi + reinvestsCumul - capitalRembCumul - capitalSimulCumul);
|
||||
|
||||
const actif = ['en_cours', 'en_retard', 'procedure'].includes(inv.statut);
|
||||
if (actif) {
|
||||
capital += capitalRestant;
|
||||
if (['en_retard', 'procedure'].includes(inv.statut)) enDefaut += capitalRestant;
|
||||
} else {
|
||||
// Remboursé/clôturé : était-il encore actif ce mois-ci ?
|
||||
// Priorité : date_cible → dernier remboursement réel → exclure
|
||||
const fin = inv.date_cible ?? inv.last_remb_date ?? null;
|
||||
if (fin && fin >= firstDay) {
|
||||
capital += capitalRestant;
|
||||
}
|
||||
}
|
||||
}
|
||||
capitalMensuel.push({ mois: moisStr, capital: Math.round(capital * 100) / 100, en_defaut: Math.round(enDefaut * 100) / 100 });
|
||||
}
|
||||
|
||||
/* ── Noms des détenteurs par plateforme ─────────────────────── */
|
||||
const detenteurRows = db.prepare(`
|
||||
SELECT p.id AS plateforme_id, inv.nom AS detenteur_nom
|
||||
FROM plateformes p
|
||||
LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id
|
||||
WHERE p.user_id = ?
|
||||
`).all(userId);
|
||||
const detenteurMap = {};
|
||||
for (const d of detenteurRows) detenteurMap[d.plateforme_id] = d.detenteur_nom;
|
||||
|
||||
/* ── Construire la réponse par plateforme ────────────────────── */
|
||||
// Indexer les rembs et projections par plateforme_id puis mois
|
||||
const rembMap = {};
|
||||
for (const r of remboursesByPlat) {
|
||||
if (!rembMap[r.plateforme_id]) rembMap[r.plateforme_id] = { id: r.plateforme_id, nom: r.plateforme_nom, detenteur_nom: detenteurMap[r.plateforme_id] ?? null, rembourses: {}, projections: {} };
|
||||
rembMap[r.plateforme_id].rembourses[r.mois] = { interets_bruts: r.interets_bruts, interets_nets: r.interets_nets, cashback: r.cashback, capital: r.capital };
|
||||
}
|
||||
// Ajouter le cashback bonus (parrainage/plateforme) aux remboursements par mois
|
||||
for (const b of bonusCashbackByPlat) {
|
||||
if (!rembMap[b.plateforme_id]) rembMap[b.plateforme_id] = { id: b.plateforme_id, nom: b.plateforme_nom, detenteur_nom: detenteurMap[b.plateforme_id] ?? null, rembourses: {}, projections: {} };
|
||||
const entry = rembMap[b.plateforme_id].rembourses[b.mois];
|
||||
if (entry) {
|
||||
entry.cashback = (entry.cashback ?? 0) + b.cashback;
|
||||
} else {
|
||||
rembMap[b.plateforme_id].rembourses[b.mois] = { interets_bruts: 0, interets_nets: 0, cashback: b.cashback, capital: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of projectionsByPlat) {
|
||||
if (!rembMap[p.plateforme_id]) rembMap[p.plateforme_id] = { id: p.plateforme_id, nom: p.plateforme_nom, detenteur_nom: detenteurMap[p.plateforme_id] ?? null, rembourses: {}, projections: {} };
|
||||
rembMap[p.plateforme_id].projections[p.mois] = { interets_prevus: p.interets_prevus, capital_prevu: p.capital_prevu };
|
||||
}
|
||||
|
||||
const plateformes = Object.values(rembMap).sort((a, b) => a.nom.localeCompare(b.nom));
|
||||
|
||||
res.json({ annee, plateformes, capitalMensuel });
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/dashboard/detail-cellule
|
||||
*
|
||||
* Retourne le détail ligne par ligne d'une cellule du tableau mensuel :
|
||||
* - recus : remboursements réels pour plateforme_id + YYYY-MM
|
||||
* - projetes : simul_remboursements pour plateforme_id + YYYY-MM
|
||||
* (uniquement les projections sans remboursement réel dans ce mois)
|
||||
*
|
||||
* Query params : plateforme_id (int), annee (4 chiffres), mois (01-12), scope=all (opt)
|
||||
*/
|
||||
router.get('/detail-cellule', (req, res) => {
|
||||
const { plateforme_id, annee, mois } = req.query;
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
const userId = req.user.id;
|
||||
|
||||
if (!annee || !mois)
|
||||
return res.status(400).json({ error: 'annee et mois sont requis' });
|
||||
|
||||
const moisStr = `${annee}-${String(mois).padStart(2, '0')}`;
|
||||
const platId = plateforme_id ? Number(plateforme_id) : null;
|
||||
const platFilter = platId ? 'AND i.plateforme_id = ?' : '';
|
||||
|
||||
let invWhere, invParams;
|
||||
if (scopeAll) {
|
||||
invWhere = 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
|
||||
invParams = [userId];
|
||||
} else {
|
||||
const raw = req.header('X-Investisseur-Id');
|
||||
const invId = Number(raw);
|
||||
if (!invId) return res.status(400).json({ error: 'Missing X-Investisseur-Id' });
|
||||
const row = db.prepare('SELECT id FROM investisseurs WHERE id=? AND user_id=?').get(invId, userId);
|
||||
if (!row) return res.status(403).json({ error: 'Investisseur not owned by user' });
|
||||
invWhere = 'i.investisseur_id = ?';
|
||||
invParams = [invId];
|
||||
}
|
||||
|
||||
const recusParams = platId ? [...invParams, platId, moisStr] : [...invParams, moisStr];
|
||||
const projetesParams = platId ? [...invParams, platId, moisStr, moisStr] : [...invParams, moisStr, moisStr];
|
||||
|
||||
/* ── Remboursements reçus ─────────────────────────────────── */
|
||||
const recus = db.prepare(`
|
||||
SELECT
|
||||
r.id, r.investissement_id, r.date_remb,
|
||||
r.capital, r.cashback,
|
||||
r.interets_bruts, r.prelev_sociaux, r.prelev_forfaitaire,
|
||||
r.interets_nets, r.net_recu,
|
||||
i.nom_projet, i.plateforme_id,
|
||||
p.nom AS plateforme_nom,
|
||||
inv.nom AS detenteur_nom
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
JOIN investisseurs inv ON inv.id = i.investisseur_id
|
||||
LEFT JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE ${invWhere}
|
||||
${platFilter}
|
||||
AND strftime('%Y-%m', r.date_remb) = ?
|
||||
AND r.type = 'normal'
|
||||
ORDER BY p.nom, r.date_remb, i.nom_projet
|
||||
`).all(...recusParams);
|
||||
|
||||
/* ── Projections (sans remb réel dans le même mois) ─────── */
|
||||
const projetes = db.prepare(`
|
||||
SELECT
|
||||
s.id, s.investissement_id, s.date_prevue,
|
||||
s.capital_prevu, s.interets_prevus, s.total_prevu,
|
||||
s.numero_echeance,
|
||||
i.nom_projet, i.plateforme_id,
|
||||
p.nom AS plateforme_nom,
|
||||
inv.nom AS detenteur_nom
|
||||
FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
JOIN investisseurs inv ON inv.id = i.investisseur_id
|
||||
LEFT JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE ${invWhere}
|
||||
${platFilter}
|
||||
AND strftime('%Y-%m', s.date_prevue) = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM remboursements r
|
||||
WHERE r.investissement_id = s.investissement_id
|
||||
AND strftime('%Y-%m', r.date_remb) = ?
|
||||
)
|
||||
ORDER BY p.nom, s.date_prevue, i.nom_projet
|
||||
`).all(...projetesParams);
|
||||
|
||||
res.json({ recus, projetes });
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in new issue
Block a user