From 1bab97b5b39ade07e6cb31e46ea605b7fee560b4 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 16 Aug 2026 16:49:20 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20du=20type=20Int=C3=A9r=C3=AAt=20platefo?= =?UTF-8?q?rme=20pour=20les=20remboursements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/db/index.js | 39 +++++++++++ backend/src/routes/dashboard.js | 53 ++++++++++++++- backend/src/routes/remboursements.js | 50 ++++++++++++-- .../src/components/InvestissementSelect.jsx | 4 +- frontend/src/components/MouvementsShared.jsx | 17 +++-- frontend/src/components/RembFormModal.jsx | 68 +++++++++++-------- frontend/src/pages/DepotsRetraits.jsx | 15 ++-- frontend/src/pages/Plateformes.jsx | 17 +++-- frontend/src/pages/Remboursements.jsx | 51 ++++++++++---- 9 files changed, 248 insertions(+), 66 deletions(-) diff --git a/backend/src/db/index.js b/backend/src/db/index.js index 6025f37..59d9bde 100644 --- a/backend/src/db/index.js +++ b/backend/src/db/index.js @@ -2301,4 +2301,43 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)'); } } +// ── Migration : v_interets_annuels — inclure les "Intérêts plateforme" ── +// Nouveau type de remboursement (intérêts versés par une plateforme, hors +// investissement précis, rattachés via bonus_plateforme_id/bonus_investisseur_id +// comme les bonus). Contrairement aux bonus (cashback non imposable), ce sont +// de vrais intérêts fiscalisés : ils doivent compter dans le récap annuel au +// même titre que les intérêts d'investissement classiques. +{ + const viewRow = db.prepare( + "SELECT sql FROM sqlite_master WHERE type='view' AND name='v_interets_annuels'" + ).get(); + if (viewRow && !viewRow.sql.includes('interets_plateforme')) { + db.exec('DROP VIEW IF EXISTS v_interets_annuels'); + db.exec(` + CREATE VIEW v_interets_annuels AS + SELECT investisseur_id, 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 ( + SELECT i.investisseur_id AS investisseur_id, + substr(r.date_remb,1,4) AS annee, + r.interets_bruts, r.prelev_sociaux, r.prelev_forfaitaire, r.net_recu + FROM remboursements r + JOIN investissements i ON i.id = r.investissement_id + WHERE r.statut IN ('paye','partiel') AND r.type = 'normal' + UNION ALL + SELECT r.bonus_investisseur_id AS investisseur_id, + substr(r.date_remb,1,4) AS annee, + r.interets_bruts, r.prelev_sociaux, r.prelev_forfaitaire, r.net_recu + FROM remboursements r + WHERE r.statut IN ('paye','partiel') AND r.type = 'interets_plateforme' + ) + GROUP BY investisseur_id, annee + `); + console.log('[DB] v_interets_annuels: vue recréée (inclut interets_plateforme)'); + } +} + export default db; diff --git a/backend/src/routes/dashboard.js b/backend/src/routes/dashboard.js index 097367a..349966a 100644 --- a/backend/src/routes/dashboard.js +++ b/backend/src/routes/dashboard.js @@ -92,6 +92,17 @@ router.get('/', (req, res) => { GROUP BY bonus_plateforme_id `).all(...invParams); + // Intérêts plateforme : comme les bonus, ils créditent directement le + // porte-monnaie de la plateforme (net_recu tient déjà compte des prélèvements). + const interetsPlateformeWalletPerPlat = db.prepare(` + SELECT bonus_plateforme_id AS plateforme_id, + COALESCE(SUM(net_recu), 0) AS interets_plateforme_wallet + FROM remboursements + WHERE ${invWhere.replace('investisseur_id', 'bonus_investisseur_id')} + AND type = 'interets_plateforme' + GROUP BY bonus_plateforme_id + `).all(...invParams); + const capitalInvestiPerPlat = db.prepare(` SELECT i.plateforme_id, COALESCE(SUM( @@ -117,6 +128,7 @@ router.get('/', (req, res) => { 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 interetsPlateformeWalletPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) + r.interets_plateforme_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; @@ -149,6 +161,10 @@ router.get('/', (req, res) => { `).get(...invParams); /* ── Interets ────────────────────────────────────────────────── */ + // Inclut les remboursements normaux (liés à un investissement) et les + // "Intérêts plateforme" (type=interets_plateforme, rattachés via + // bonus_plateforme_id/bonus_investisseur_id comme les bonus, mais ce sont + // de vrais intérêts fiscalisés — contrairement aux bonus qui restent exclus). const interets = db.prepare(` SELECT COALESCE(SUM(r.interets_bruts),0) AS interets_bruts, @@ -156,9 +172,12 @@ router.get('/', (req, res) => { 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); + LEFT JOIN investissements i ON i.id = r.investissement_id + WHERE ( + (r.investissement_id IS NOT NULL AND ${invWhere.replace('investisseur_id', 'i.investisseur_id')}) + OR (r.investissement_id IS NULL AND r.type = 'interets_plateforme' AND ${invWhere.replace('investisseur_id', 'r.bonus_investisseur_id')}) + ) + `).get(...invParams, ...invParams); /* ── Interets par annee ──────────────────────────────────────── */ const interetsParAnnee = scopeAll @@ -618,6 +637,23 @@ router.get('/interets-par-plateforme', (req, res) => { ORDER BY p.nom, mois `).all(...invParams, anneeStr); + /* ── Intérêts plateforme (hors investissement) par plateforme et par mois ── */ + const interetsPlateformeByPlat = 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.interets_bruts), 0) AS interets_bruts, + COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets + FROM remboursements r + JOIN plateformes p ON p.id = r.bonus_plateforme_id + WHERE ${invWhere.replace('investisseur_id', 'bonus_investisseur_id')} + AND r.type = 'interets_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(` @@ -782,6 +818,17 @@ router.get('/interets-par-plateforme', (req, res) => { rembMap[b.plateforme_id].rembourses[b.mois] = { interets_bruts: 0, interets_nets: 0, cashback: b.cashback, capital: 0 }; } } + // Ajouter les intérêts plateforme (hors investissement) aux remboursements par mois + for (const ip of interetsPlateformeByPlat) { + if (!rembMap[ip.plateforme_id]) rembMap[ip.plateforme_id] = { id: ip.plateforme_id, nom: ip.plateforme_nom, detenteur_nom: detenteurMap[ip.plateforme_id] ?? null, rembourses: {}, projections: {} }; + const entry = rembMap[ip.plateforme_id].rembourses[ip.mois]; + if (entry) { + entry.interets_bruts = (entry.interets_bruts ?? 0) + ip.interets_bruts; + entry.interets_nets = (entry.interets_nets ?? 0) + ip.interets_nets; + } else { + rembMap[ip.plateforme_id].rembourses[ip.mois] = { interets_bruts: ip.interets_bruts, interets_nets: ip.interets_nets, cashback: 0, 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: {} }; diff --git a/backend/src/routes/remboursements.js b/backend/src/routes/remboursements.js index 6e61bd8..0ac376a 100644 --- a/backend/src/routes/remboursements.js +++ b/backend/src/routes/remboursements.js @@ -8,7 +8,7 @@ import { adjustSimulForActuals, generateSimulWithReinvestissements } from '../ut const router = Router(); const Schema = z.object({ - type: z.enum(['normal', 'bonus_parrainage', 'bonus_plateforme']).default('normal'), + type: z.enum(['normal', 'bonus_parrainage', 'bonus_plateforme', 'interets_plateforme']).default('normal'), investissement_id: z.number().int().positive().nullable().optional(), bonus_plateforme_id: z.number().int().positive().nullable().optional(), bonus_investisseur_id: z.number().int().positive().nullable().optional(), @@ -44,13 +44,20 @@ function computeChamps(body, isTaxIndicatif = false) { return { interets_nets, net_recu }; } -function getTaxIndicatif(investissementId) { - if (!investissementId) return false; +function getTaxIndicatif(body) { + // Intérêts plateforme : pas d'investissement, la fiscalité applicable est celle + // de la plateforme associée (bonus_plateforme_id) — pas de fiscalite_override possible. + if (body.type === 'interets_plateforme') { + if (!body.bonus_plateforme_id) return false; + const p = db.prepare('SELECT fiscalite FROM plateformes WHERE id = ?').get(body.bonus_plateforme_id); + return p ? p.fiscalite !== 'flat_tax' : false; + } + if (!body.investissement_id) return false; const row = db.prepare(` SELECT p.fiscalite, i.fiscalite_override FROM investissements i JOIN plateformes p ON p.id = i.plateforme_id WHERE i.id = ? - `).get(investissementId); + `).get(body.investissement_id); if (!row) return false; return row.fiscalite !== 'flat_tax' || row.fiscalite_override === 'exonere'; } @@ -167,6 +174,7 @@ router.get('/', (req, res) => { WHEN 'normal' THEN b.inv_nom_projet WHEN 'bonus_parrainage' THEN '— Bonus Parrainage' || CASE WHEN NULLIF(b.notes, '') IS NOT NULL THEN ' — ' || b.notes ELSE '' END WHEN 'bonus_plateforme' THEN '— Bonus Plateforme' || CASE WHEN NULLIF(b.notes, '') IS NOT NULL THEN ' — ' || b.notes ELSE '' END + WHEN 'interets_plateforme' THEN '— Intérêts plateforme' || CASE WHEN NULLIF(b.notes, '') IS NOT NULL THEN ' — ' || b.notes ELSE '' END ELSE b.inv_nom_projet END AS nom_projet, p.nom AS plateforme_nom, @@ -188,7 +196,7 @@ router.get('/', (req, res) => { router.post('/', (req, res, next) => { try { const body = Schema.parse(req.body); - const { interets_nets, net_recu } = computeChamps(body, getTaxIndicatif(body.investissement_id)); + const { interets_nets, net_recu } = computeChamps(body, getTaxIndicatif(body)); if (body.type === 'normal') { assertOwnedInvestissement(body.investissement_id, req.user.id); @@ -229,9 +237,26 @@ router.post('/', (req, res, next) => { return res.status(201).json({ id: r.lastInsertRowid, ...body, interets_nets, net_recu }); } - // Bonus (parrainage ou plateforme) + // Bonus (parrainage / plateforme) ou Intérêts plateforme const bonusInvestisseurId = body.bonus_investisseur_id ?? req.investisseur.id; assertOwnedInvestisseur(bonusInvestisseurId, req.user.id); + + if (body.type === 'interets_plateforme') { + const r = db.prepare(` + INSERT INTO remboursements + (type, bonus_plateforme_id, bonus_investisseur_id, date_remb, + capital, cashback, interets_bruts_avant_local, taxe_locale, interets_bruts, + prelev_sociaux, prelev_forfaitaire, interets_nets, net_recu, statut, source, notes) + VALUES ('interets_plateforme',?,?,?, 0,0,?,?,?,?,?, ?,?, ?, 'manuel', ?) + `).run( + body.bonus_plateforme_id, bonusInvestisseurId, body.date_remb, + body.interets_bruts_avant_local, body.taxe_locale, body.interets_bruts, + body.prelev_sociaux, body.prelev_forfaitaire, + interets_nets, net_recu, body.statut, body.notes || null, + ); + return res.status(201).json({ id: r.lastInsertRowid, ...body, interets_nets, net_recu }); + } + const r = db.prepare(` INSERT INTO remboursements (type, bonus_plateforme_id, bonus_investisseur_id, date_remb, @@ -348,7 +373,7 @@ router.put('/:id', (req, res, next) => { if (!existing) throw new HttpError(404, 'Not found'); const body = Schema.parse(req.body); - const { interets_nets, net_recu } = computeChamps(body, getTaxIndicatif(body.investissement_id)); + const { interets_nets, net_recu } = computeChamps(body, getTaxIndicatif(body)); if (body.type === 'normal') { db.prepare(` @@ -369,6 +394,17 @@ router.put('/:id', (req, res, next) => { syncInvestissementStatut(body.investissement_id); adjustSimulForActuals(db, body.investissement_id); syncAutoRetrait(id, body.methode_remboursement, net_recu, body.investissement_id, body.date_remb); + } else if (body.type === 'interets_plateforme') { + db.prepare(` + UPDATE remboursements + SET date_remb=?, interets_bruts_avant_local=?, taxe_locale=?, interets_bruts=?, + prelev_sociaux=?, prelev_forfaitaire=?, interets_nets=?, net_recu=?, statut=?, notes=? + WHERE id=? + `).run( + body.date_remb, body.interets_bruts_avant_local, body.taxe_locale, body.interets_bruts, + body.prelev_sociaux, body.prelev_forfaitaire, + interets_nets, net_recu, body.statut, body.notes || null, id, + ); } else { const bonusInvestisseurId = body.bonus_investisseur_id ?? existing.bonus_investisseur_id; db.prepare(` diff --git a/frontend/src/components/InvestissementSelect.jsx b/frontend/src/components/InvestissementSelect.jsx index ad84f97..9a29405 100644 --- a/frontend/src/components/InvestissementSelect.jsx +++ b/frontend/src/components/InvestissementSelect.jsx @@ -15,7 +15,7 @@ import { fmtEUR } from '../utils/format.js'; * en dessous, et se ferme au scroll/resize (repositionnement non géré en direct, comme InvSelect). * * Props : - * value : '' | 'BONUS_PARRAINAGE' | 'BONUS_PLATEFORME' | id (string/number) + * value : '' | 'BONUS_PARRAINAGE' | 'BONUS_PLATEFORME' | 'INTERETS_PLATEFORME' | id (string/number) * onChange : (value) => void * options : investissementOptions — { id, plateforme_nom, nom_projet, montant_investi }[] */ @@ -29,6 +29,7 @@ export default function InvestissementSelect({ value, onChange, options = [] }) const selectedOption = options.find(i => String(i.id) === String(value)); const label = value === 'BONUS_PARRAINAGE' ? '🎁 Bonus Parrainage' : value === 'BONUS_PLATEFORME' ? '🏆 Bonus Plateforme' + : value === 'INTERETS_PLATEFORME' ? '🏦 Intérêts plateforme' : selectedOption ? `${selectedOption.plateforme_nom} — ${selectedOption.nom_projet} (${fmtEUR(selectedOption.montant_investi)})` : null; @@ -123,6 +124,7 @@ export default function InvestissementSelect({ value, onChange, options = [] }) +
{options.map(i => (
)} diff --git a/frontend/src/components/RembFormModal.jsx b/frontend/src/components/RembFormModal.jsx index b8d8fb2..5d33d1f 100644 --- a/frontend/src/components/RembFormModal.jsx +++ b/frontend/src/components/RembFormModal.jsx @@ -20,6 +20,7 @@ const getRatesForYear = (dateStr, pfuRates) => { }; const BONUS_VALUES = ['BONUS_PARRAINAGE', 'BONUS_PLATEFORME']; +const INTERETS_PLATEFORME_VALUE = 'INTERETS_PLATEFORME'; /** * RembFormModal — formulaire partagé de saisie/édition d'un remboursement. @@ -67,6 +68,9 @@ export default function RembFormModal({ confirmingDelete, setConfirmingDelete, }) { const isBonus = !locked && BONUS_VALUES.includes(form.investissement_id); + const isInteretsPlateforme = !locked && form.investissement_id === INTERETS_PLATEFORME_VALUE; + // Modes "plateforme" (pas d'investissement précis) : bonus classique ou intérêts plateforme + const isPlateformeMode = isBonus || isInteretsPlateforme; const rates = getRatesForYear(form.date_remb, pfuRates); const labelPS = `Prélèvements sociaux${rates ? ` (${fmtRate(rates.prelev_sociaux)})` : ''} (€)`; @@ -75,8 +79,10 @@ export default function RembFormModal({ const currentInv = locked ? investissements[0] - : (!isBonus ? investissements.find(i => i.id === Number(form.investissement_id)) : null); - const currentPlat = currentInv ? plateformes.find(p => p.id === currentInv.plateforme_id) : null; + : (!isPlateformeMode ? investissements.find(i => i.id === Number(form.investissement_id)) : null); + const currentPlat = isInteretsPlateforme + ? plateformes.find(p => String(p.id) === String(form.bonus_plateforme_id)) + : (currentInv ? plateformes.find(p => p.id === currentInv.plateforme_id) : null); const hasLocalTax = !isBonus && currentPlat?.fiscalite === 'avec_fiscalite_locale' && currentPlat?.taux_fiscalite_locale; const isExonere = !isBonus && currentInv?.fiscalite_override === 'exonere'; const isIndicatif = Boolean(isExonere || (!isBonus && currentPlat && currentPlat.fiscalite !== 'flat_tax')); @@ -127,7 +133,7 @@ export default function RembFormModal({ {/* ── Sélecteur d'investissement (mode non verrouillé uniquement) ── */} {!locked && ( -
+
)} - {!locked && isBonus && ( + {!locked && isPlateformeMode && (
onFieldChange('capital', e.target.value)} /> -
+ {!isInteretsPlateforme && ( +
+ + onFieldChange('capital', e.target.value)} /> +
+ )}
!hasLocalTax && onFieldChange('interets_bruts', e.target.value)} />
-
- - onFieldChange('cashback', e.target.value)} /> -
+ {!isInteretsPlateforme && ( +
+ + onFieldChange('cashback', e.target.value)} /> +
+ )}
Imposition{isIndicatif ? ' — indicatif' : ''}
@@ -228,16 +238,18 @@ export default function RembFormModal({ onFieldChange('date_remb', e.target.value)} />
+ {!isInteretsPlateforme && ( +
+ + +
+ )}
- - -
-
- {form.methode_remboursement === 'compte_courant' && ( + {!isInteretsPlateforme && form.methode_remboursement === 'compte_courant' && ( <> {comptesInvestisseur?.length > 0 ? ( @@ -276,10 +288,12 @@ export default function RembFormModal({ onChange={e => onFieldChange('cashback', e.target.value)} />
)} - {!locked && isBonus && ( -
+ {!locked && isPlateformeMode && ( +
- onFieldChange('notes', e.target.value)} />
)} diff --git a/frontend/src/pages/DepotsRetraits.jsx b/frontend/src/pages/DepotsRetraits.jsx index 43072a8..68d5f39 100644 --- a/frontend/src/pages/DepotsRetraits.jsx +++ b/frontend/src/pages/DepotsRetraits.jsx @@ -268,6 +268,7 @@ export default function DepotsRetraits() { const out = []; for (const r of allRemb) { const isBonus = r.type === 'bonus_parrainage' || r.type === 'bonus_plateforme'; + const isInteretsPlateforme = r.type === 'interets_plateforme'; const plat = plats.find(p => p.id === r.plateforme_id); const base = { _rembId: r.id, @@ -305,7 +306,7 @@ export default function DepotsRetraits() { out.push({ ...base, id: `remb_${r.id}`, - type: 'remboursement', + type: isInteretsPlateforme ? 'interets_plateforme' : 'remboursement', montant: grossMontant, _isFlatTax: isFlatTax, _isAutoSweep: r.methode_remboursement === 'compte_courant', @@ -1140,6 +1141,7 @@ export default function DepotsRetraits() { + @@ -1251,9 +1253,10 @@ export default function DepotsRetraits() { ); } - if (r.type === 'remboursement' || r.type === 'bonus' || r.type === 'cashback') { + if (r.type === 'remboursement' || r.type === 'bonus' || r.type === 'cashback' || r.type === 'interets_plateforme') { const isBonus = r.type === 'bonus'; const isCashback = r.type === 'cashback'; + const isInteretsPlateforme = r.type === 'interets_plateforme'; return ( {isBonus ? 'bonus' : isCashback ? 'cashback' : 'remboursement'} + : isInteretsPlateforme + ? 'color-mix(in srgb, var(--primary) 18%, transparent)' + : 'color-mix(in srgb, var(--success) 15%, transparent)', + color: (isBonus || isCashback) ? 'var(--warning)' : isInteretsPlateforme ? 'var(--primary)' : 'var(--success)', + }}>{isBonus ? 'bonus' : isCashback ? 'cashback' : isInteretsPlateforme ? 'intérêts plateforme' : 'remboursement'} {r._projet || '—'} diff --git a/frontend/src/pages/Plateformes.jsx b/frontend/src/pages/Plateformes.jsx index 91f69d6..bead4e7 100644 --- a/frontend/src/pages/Plateformes.jsx +++ b/frontend/src/pages/Plateformes.jsx @@ -850,6 +850,8 @@ export default function Plateformes() { : (r.capital || 0) + (r.cashback || 0) + (r.interets_bruts || 0); } else if (r.type === 'bonus_parrainage' || r.type === 'bonus_plateforme') { balance += r.cashback || 0; + } else if (r.type === 'interets_plateforme') { + balance += r.net_recu || 0; } } for (const inv of allRows) { @@ -964,6 +966,7 @@ export default function Plateformes() { const out = []; for (const r of allRembs) { const isBonus = r.type === 'bonus_parrainage' || r.type === 'bonus_plateforme'; + const isInteretsPlateforme = r.type === 'interets_plateforme'; const plat = plats.find(p => p.id === r.plateforme_id); const base = { _rembId: r.id, @@ -999,7 +1002,7 @@ export default function Plateformes() { out.push({ ...base, id: `remb_${r.id}`, - type: 'remboursement', + type: isInteretsPlateforme ? 'interets_plateforme' : 'remboursement', montant: grossMontant, _isFlatTax: isFlatTax, _isAutoSweep: r.methode_remboursement === 'compte_courant', @@ -2406,6 +2409,7 @@ export default function Plateformes() { + @@ -2493,9 +2497,10 @@ export default function Plateformes() { ); } - if (r.type === 'remboursement' || r.type === 'bonus' || r.type === 'cashback') { + if (r.type === 'remboursement' || r.type === 'bonus' || r.type === 'cashback' || r.type === 'interets_plateforme') { const isBonus = r.type === 'bonus'; const isCashback = r.type === 'cashback'; + const isInteretsPlateforme = r.type === 'interets_plateforme'; return ( {isBonus ? 'bonus' : isCashback ? 'cashback' : 'remboursement'} + : isInteretsPlateforme + ? 'color-mix(in srgb, var(--primary) 18%, transparent)' + : 'color-mix(in srgb, var(--success) 15%, transparent)', + color: (isBonus || isCashback) ? 'var(--warning)' : isInteretsPlateforme ? 'var(--primary)' : 'var(--success)', + }}>{isBonus ? 'bonus' : isCashback ? 'cashback' : isInteretsPlateforme ? 'intérêts plateforme' : 'remboursement'} {r._projet || '—'} diff --git a/frontend/src/pages/Remboursements.jsx b/frontend/src/pages/Remboursements.jsx index 0bbd4a4..4452246 100644 --- a/frontend/src/pages/Remboursements.jsx +++ b/frontend/src/pages/Remboursements.jsx @@ -48,6 +48,7 @@ function TrendBadge({ current, prev, invert = false }) { const BONUS_VALUES = ['BONUS_PARRAINAGE', 'BONUS_PLATEFORME']; +const INTERETS_PLATEFORME_VALUE = 'INTERETS_PLATEFORME'; const ICONS_BASE = '/api/icons-files/'; const BONUS_TYPE_MAP = { BONUS_PARRAINAGE: 'bonus_parrainage', BONUS_PLATEFORME: 'bonus_plateforme' }; @@ -424,9 +425,9 @@ export default function Remboursements() { if (tableFilter.type && tableFilter.type !== 'correction_solde' && r.type !== tableFilter.type) return false; if (tableFilter.type === 'correction_solde') return false; // géré séparément // Filtre projet - const isBonusTypeFilter = tableFilter.type === 'bonus_parrainage' || tableFilter.type === 'bonus_plateforme'; + const isBonusTypeFilter = tableFilter.type === 'bonus_parrainage' || tableFilter.type === 'bonus_plateforme' || tableFilter.type === 'interets_plateforme'; if (!isBonusTypeFilter && tableFilter.investissement_id) { - const isBonus = r.type === 'bonus_parrainage' || r.type === 'bonus_plateforme'; + const isBonus = r.type === 'bonus_parrainage' || r.type === 'bonus_plateforme' || r.type === 'interets_plateforme'; if (isBonus) return false; if (String(r.investissement_id) !== String(tableFilter.investissement_id)) return false; } @@ -535,9 +536,13 @@ export default function Remboursements() { const openEdit = (r) => { setEditingId(r.id); const isBonus = r.type === 'bonus_parrainage' || r.type === 'bonus_plateforme'; - const sentinelle = r.type === 'bonus_parrainage' ? 'BONUS_PARRAINAGE' : r.type === 'bonus_plateforme' ? 'BONUS_PLATEFORME' : ''; + const isInteretsPlateforme = r.type === 'interets_plateforme'; + const sentinelle = r.type === 'bonus_parrainage' ? 'BONUS_PARRAINAGE' + : r.type === 'bonus_plateforme' ? 'BONUS_PLATEFORME' + : isInteretsPlateforme ? INTERETS_PLATEFORME_VALUE + : ''; setForm({ - investissement_id: isBonus ? sentinelle : r.investissement_id, + investissement_id: (isBonus || isInteretsPlateforme) ? sentinelle : r.investissement_id, date_remb: r.date_remb, capital: r.capital, cashback: r.cashback || 0, interets_bruts_avant_local: r.interets_bruts_avant_local || 0, @@ -552,7 +557,7 @@ export default function Remboursements() { }); setErr(null); setModalOpen(true); // Charge les comptes du détenteur pour le dropdown Compte de réception - if (!isBonus) { + if (!isBonus && !isInteretsPlateforme) { const invForEdit = investissements.find(i => i.id === r.investissement_id); if (invForEdit?.investisseur_id) { api.get(`/investissements/comptes-par-investisseur/${invForEdit.investisseur_id}`) @@ -654,8 +659,12 @@ export default function Remboursements() { // Fiscalité locale : calcule taxe_locale et interets_bruts à partir du brut avant retenue if (k === 'interets_bruts_avant_local') { - const inv = investissements.find(i => i.id === Number(next.investissement_id)); - const plat = plateformes.find(p => p.id === inv?.plateforme_id); + const plat = next.investissement_id === INTERETS_PLATEFORME_VALUE + ? plateformes.find(p => String(p.id) === String(next.bonus_plateforme_id)) + : (() => { + const inv = investissements.find(i => i.id === Number(next.investissement_id)); + return plateformes.find(p => p.id === inv?.plateforme_id); + })(); const taux = plat?.fiscalite === 'avec_fiscalite_locale' ? (plat?.taux_fiscalite_locale || 0) : 0; next.taxe_locale = round2(Number(v) * taux / 100); next.interets_bruts = round2(Number(v) - next.taxe_locale); @@ -676,7 +685,7 @@ export default function Remboursements() { } // Quand l'investissement change, on hérite silencieusement de la méthode de la plateforme // (si fermée) ou on remet le défaut (si ouverte, l'utilisateur choisira dans le formulaire) - if (k === 'investissement_id' && !BONUS_VALUES.includes(v)) { + if (k === 'investissement_id' && !BONUS_VALUES.includes(v) && v !== INTERETS_PLATEFORME_VALUE) { const inv = investissements.find(i => i.id === Number(v)); const plat = plateformes.find(p => p.id === inv?.plateforme_id); if (plat && plat.methode_remboursement !== 'choix_investisseur') { @@ -708,6 +717,7 @@ export default function Remboursements() { }; const isBonus = BONUS_VALUES.includes(form.investissement_id); + const isInteretsPlateforme = form.investissement_id === INTERETS_PLATEFORME_VALUE; const submit = async (e) => { e?.preventDefault?.(); @@ -724,6 +734,20 @@ export default function Remboursements() { statut: form.statut, notes: form.notes || undefined, }; + } else if (isInteretsPlateforme) { + payload = { + type: 'interets_plateforme', + bonus_plateforme_id: Number(form.bonus_plateforme_id), + bonus_investisseur_id: form.bonus_investisseur_id ? Number(form.bonus_investisseur_id) : undefined, + date_remb: form.date_remb, + interets_bruts_avant_local: Number(form.interets_bruts_avant_local || 0), + taxe_locale: Number(form.taxe_locale || 0), + interets_bruts: Number(form.interets_bruts || 0), + prelev_sociaux: Number(form.prelev_sociaux || 0), + prelev_forfaitaire: Number(form.prelev_forfaitaire || 0), + statut: form.statut, + notes: form.notes || undefined, + }; } else { payload = { type: 'normal', @@ -1136,6 +1160,7 @@ export default function Remboursements() { +
@@ -1186,6 +1211,8 @@ export default function Remboursements() { )} {pagedTableRows.map(r => { const isBonus = r.type === 'bonus_parrainage' || r.type === 'bonus_plateforme'; + const isInteretsPlateforme = r.type === 'interets_plateforme'; + const isBonusLike = isBonus || isInteretsPlateforme; const isCorrection = r._is_correction === true; const rowKey = isCorrection ? `corr-${r.id}` : `remb-${r.id}`; return ( @@ -1214,7 +1241,7 @@ export default function Remboursements() { {isCorrection ? {r.nom_projet} - : isBonus + : isBonusLike ? {r.nom_projet} : r.nom_projet} @@ -1233,9 +1260,9 @@ export default function Remboursements() { Porte-monnaie )} - {(isBonus || isCorrection) ? '—' : fmtEUR(r.capital)} - - {(isBonus || isCorrection) ? '—' : fmtEUR(Math.max(0, r.capital_restant_du ?? 0))} + {(isBonusLike || isCorrection) ? '—' : fmtEUR(r.capital)} + + {(isBonusLike || isCorrection) ? '—' : fmtEUR(Math.max(0, r.capital_restant_du ?? 0))} {r.cashback ? fmtEUR(r.cashback) : '—'} {(isBonus || isCorrection) ? '—' : fmtEUR(r.interets_bruts)}