Amélioration mouvements
This commit is contained in:
1 parent
c5dc63cfe5
commit
dc1863369c
2 files changed
+148
-36
No files matched your search
@@ -56,12 +56,14 @@ function TrendBadge({ current, prev, invert = false }) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const DEBIT_TYPES = new Set(['retrait', 'souscription', 'prelevement_ir', 'prelevement_csg', 'prelevement_local']);
|
||||||
|
|
||||||
// Montant signé, identique à ce qui est affiché à l'écran (+ crédit / − débit),
|
// Montant signé, identique à ce qui est affiché à l'écran (+ crédit / − débit),
|
||||||
// pour permettre de sommer directement l'extraction et contrôler le solde.
|
// pour permettre de sommer directement l'extraction et contrôler le solde.
|
||||||
// depot/remboursement/bonus → crédit (+) ; retrait/souscription → débit (−) ;
|
// depot/remboursement/bonus → crédit (+) ; retrait/souscription/prélèvements → débit (−) ;
|
||||||
// correction → déjà signé en base (montant peut être positif ou négatif).
|
// correction → déjà signé en base (montant peut être positif ou négatif).
|
||||||
function signedMontant(r) {
|
function signedMontant(r) {
|
||||||
if (r.type === 'retrait' || r.type === 'souscription') return -Math.abs(r.montant);
|
if (DEBIT_TYPES.has(r.type)) return -Math.abs(r.montant);
|
||||||
if (r.type === 'correction') return r.montant;
|
if (r.type === 'correction') return r.montant;
|
||||||
return Math.abs(r.montant);
|
return Math.abs(r.montant);
|
||||||
}
|
}
|
||||||
@@ -80,6 +82,8 @@ function mouvToCSV(rows) {
|
|||||||
const MOUV_TYPE_LABELS = {
|
const MOUV_TYPE_LABELS = {
|
||||||
depot: 'Dépôt', retrait: 'Retrait', correction: 'Correction de solde',
|
depot: 'Dépôt', retrait: 'Retrait', correction: 'Correction de solde',
|
||||||
remboursement: 'Remboursement', bonus: 'Bonus / Cashback', souscription: 'Souscription',
|
remboursement: 'Remboursement', bonus: 'Bonus / Cashback', souscription: 'Souscription',
|
||||||
|
prelevement_ir: 'Prélèvement IR', prelevement_csg: 'Prélèvement CSG',
|
||||||
|
prelevement_local: "Prélèvement impôts à l'étranger",
|
||||||
};
|
};
|
||||||
|
|
||||||
function mouvToXLS(rows, multiDetenteur) {
|
function mouvToXLS(rows, multiDetenteur) {
|
||||||
@@ -215,20 +219,24 @@ function DetailPanel({ row, onEdit, onDeleteCorrection, onViewRemb, onViewInvest
|
|||||||
const isRemb = row.type === 'remboursement' || row.type === 'bonus';
|
const isRemb = row.type === 'remboursement' || row.type === 'bonus';
|
||||||
const isBonus = row.type === 'bonus';
|
const isBonus = row.type === 'bonus';
|
||||||
const isSouscription = row.type === 'souscription';
|
const isSouscription = row.type === 'souscription';
|
||||||
|
const isPrelevement = row.type === 'prelevement_ir' || row.type === 'prelevement_csg' || row.type === 'prelevement_local';
|
||||||
const isAuto = row.source === 'auto_remboursement';
|
const isAuto = row.source === 'auto_remboursement';
|
||||||
|
|
||||||
const typeLabel = isCorrection ? 'Correction de solde'
|
const typeLabel = isCorrection ? 'Correction de solde'
|
||||||
: isBonus ? 'Bonus / Cashback'
|
: isBonus ? 'Bonus / Cashback'
|
||||||
: row.type === 'remboursement' ? 'Remboursement (porte-monnaie)'
|
: row.type === 'remboursement' ? 'Remboursement (porte-monnaie, brut)'
|
||||||
|
: row.type === 'prelevement_ir' ? 'Prélèvement impôt sur le revenu'
|
||||||
|
: row.type === 'prelevement_csg' ? 'Prélèvement CSG'
|
||||||
|
: row.type === 'prelevement_local' ? "Prélèvement impôts à l'étranger"
|
||||||
: isSouscription ? (row._isReinvest ? 'Réinvestissement' : 'Souscription à un investissement')
|
: isSouscription ? (row._isReinvest ? 'Réinvestissement' : 'Souscription à un investissement')
|
||||||
: row.type === 'depot' ? 'Dépôt' : 'Retrait';
|
: row.type === 'depot' ? 'Dépôt' : 'Retrait';
|
||||||
|
|
||||||
const editLabel = isCorrection ? 'Modifier la correction de solde'
|
const editLabel = isCorrection ? 'Modifier la correction de solde'
|
||||||
: row.type === 'depot' ? 'Modifier le dépôt' : 'Modifier le retrait';
|
: row.type === 'depot' ? 'Modifier le dépôt' : 'Modifier le retrait';
|
||||||
|
|
||||||
// Débit (−) pour retrait/souscription, crédit (+) pour dépôt/remboursement/bonus ;
|
// Débit (−) pour retrait/souscription/prélèvements, crédit (+) pour dépôt/remboursement/bonus ;
|
||||||
// la correction est déjà signée en base.
|
// la correction est déjà signée en base.
|
||||||
const isDebitType = row.type === 'retrait' || isSouscription;
|
const isDebitType = row.type === 'retrait' || isSouscription || isPrelevement;
|
||||||
const isPos = isCorrection ? row.montant >= 0 : !isDebitType;
|
const isPos = isCorrection ? row.montant >= 0 : !isDebitType;
|
||||||
const sign = isPos ? '+' : '−';
|
const sign = isPos ? '+' : '−';
|
||||||
const montantFmt = `${sign} ${fmtEUR(Math.abs(row.montant))}`;
|
const montantFmt = `${sign} ${fmtEUR(Math.abs(row.montant))}`;
|
||||||
@@ -243,9 +251,14 @@ function DetailPanel({ row, onEdit, onDeleteCorrection, onViewRemb, onViewInvest
|
|||||||
isRemb && !isBonus && { label: 'Capital', value: fmtEUR(row._capital) },
|
isRemb && !isBonus && { label: 'Capital', value: fmtEUR(row._capital) },
|
||||||
isRemb && !isBonus && row._cashback > 0 && { label: 'Cashback', value: fmtEUR(row._cashback) },
|
isRemb && !isBonus && row._cashback > 0 && { label: 'Cashback', value: fmtEUR(row._cashback) },
|
||||||
isRemb && !isBonus && { label: 'Intérêts bruts', value: fmtEUR(row._interets_bruts) },
|
isRemb && !isBonus && { label: 'Intérêts bruts', value: fmtEUR(row._interets_bruts) },
|
||||||
isRemb && !isBonus && { label: 'Traitement fiscal', value: row._isFlatTax ? 'Flat tax France (PFU prélevé à la source)' : 'Étranger — indicatif, aucun prélèvement à la source ici' },
|
isRemb && !isBonus && { label: 'Traitement fiscal', value: row._isFlatTax
|
||||||
|
? 'Flat tax France (PFU prélevé à la source — voir prélèvements associés)'
|
||||||
|
: row._taxeLocale > 0
|
||||||
|
? "Étranger avec retenue fiscale locale — voir prélèvement associé"
|
||||||
|
: 'Étranger — indicatif, aucun prélèvement à la source ici' },
|
||||||
|
isPrelevement && row.libelle && { label: 'Projet', value: row.libelle },
|
||||||
isSouscription && row.libelle && { label: 'Projet', value: row.libelle },
|
isSouscription && row.libelle && { label: 'Projet', value: row.libelle },
|
||||||
!isCorrection && !isRemb && !isSouscription && row.libelle && { label: 'Libellé', value: row.libelle },
|
!isCorrection && !isRemb && !isSouscription && !isPrelevement && row.libelle && { label: 'Libellé', value: row.libelle },
|
||||||
!isCorrection && !isRemb && row.reference && { label: 'Référence', value: row.reference },
|
!isCorrection && !isRemb && row.reference && { label: 'Référence', value: row.reference },
|
||||||
row.notes && { label: 'Notes', value: row.notes },
|
row.notes && { label: 'Notes', value: row.notes },
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
@@ -295,8 +308,31 @@ function DetailPanel({ row, onEdit, onDeleteCorrection, onViewRemb, onViewInvest
|
|||||||
{isBonus
|
{isBonus
|
||||||
? 'Bonus crédité directement au porte-monnaie de la plateforme.'
|
? 'Bonus crédité directement au porte-monnaie de la plateforme.'
|
||||||
: row._isAutoSweep
|
: row._isAutoSweep
|
||||||
? "Remboursement crédité sur le porte-monnaie puis reversé aussitôt sur le compte courant (retrait automatique associé, même montant). Effet net nul sur le solde. Modifiable depuis la page Remboursements."
|
? "Remboursement (montant brut) crédité sur le porte-monnaie puis reversé aussitôt sur le compte courant (retrait automatique associé). Les prélèvements éventuels apparaissent en lignes séparées. Modifiable depuis la page Remboursements."
|
||||||
: "Remboursement versé sur le porte-monnaie de la plateforme (méthode « portefeuille »). Modifiable depuis la page Remboursements."}
|
: "Montant brut du remboursement versé sur le porte-monnaie de la plateforme (méthode « portefeuille ») — les prélèvements éventuels apparaissent en lignes séparées. Modifiable depuis la page Remboursements."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isPrelevement && (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'flex-start', gap: 8,
|
||||||
|
margin: '5px 5px 12px',
|
||||||
|
padding: '9px 12px', borderRadius: 6,
|
||||||
|
background: `color-mix(in srgb, ${row.type === 'prelevement_local' ? '#f97316' : 'var(--danger)'} 10%, transparent)`,
|
||||||
|
border: `1px solid color-mix(in srgb, ${row.type === 'prelevement_local' ? '#f97316' : 'var(--danger)'} 25%, transparent)`,
|
||||||
|
fontSize: 'var(--fs-sm)', color: 'var(--text-muted)',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
}}>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
||||||
|
style={{ marginTop: 1, flexShrink: 0, color: row.type === 'prelevement_local' ? '#f97316' : 'var(--danger)' }}>
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
{row.type === 'prelevement_local'
|
||||||
|
? "Retenue fiscale prélevée localement à l'étranger, avant que le remboursement n'atteigne le porte-monnaie de la plateforme."
|
||||||
|
: "Prélèvement forfaitaire unique (PFU France) retenu à la source sur ce remboursement. Modifiable depuis la page Remboursements."}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -334,7 +370,7 @@ function DetailPanel({ row, onEdit, onDeleteCorrection, onViewRemb, onViewInvest
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dr-detail-footer">
|
<div className="dr-detail-footer">
|
||||||
{isAuto ? null : isRemb ? (
|
{isAuto ? null : (isRemb || isPrelevement) ? (
|
||||||
<button className="dr-detail-edit-btn" onClick={() => onViewRemb(row)}>
|
<button className="dr-detail-edit-btn" onClick={() => onViewRemb(row)}>
|
||||||
Voir / modifier le remboursement
|
Voir / modifier le remboursement
|
||||||
</button>
|
</button>
|
||||||
@@ -610,40 +646,71 @@ export default function DepotsRetraits() {
|
|||||||
return true;
|
return true;
|
||||||
}), [normalizedCorrections, filter.plateforme_ids, filter.year, filter.month]);
|
}), [normalizedCorrections, filter.plateforme_ids, filter.year, filter.month]);
|
||||||
|
|
||||||
// Remboursements affichés comme mouvements du porte-monnaie de la plateforme —
|
// Investissements par id (déjà scopés) — sert à retrouver fiscalite_override et à
|
||||||
// règle fiscale : flat_tax (France) → net_recu (PFU déjà prélevé) ;
|
// rattacher les réinvestissements (qui ne portent que investissement_id) à leur
|
||||||
// hors France → capital + cashback + intérêts bruts (pas de PFU prélevé à la source ici).
|
// plateforme/projet/détenteur.
|
||||||
// Les bonus (parrainage/plateforme) créditent le cashback, indépendamment de la fiscalité du prêt.
|
const invById = useMemo(() => new Map(allInv.map(i => [i.id, i])), [allInv]);
|
||||||
|
|
||||||
|
// Remboursements affichés comme mouvements du porte-monnaie de la plateforme, décomposés
|
||||||
|
// en plusieurs lignes pour rendre les prélèvements visibles :
|
||||||
|
// 1. "remboursement" (crédit, montant BRUT = capital + cashback + intérêts avant tout
|
||||||
|
// prélèvement, y compris avant retenue locale étrangère le cas échéant)
|
||||||
|
// 2. "prelevement_local" (débit = taxe_locale) si la plateforme retient une fiscalité
|
||||||
|
// locale à l'étranger — toujours réellement retenue, indépendamment du flat_tax.
|
||||||
|
// 3. "prelevement_ir" et "prelevement_csg" (débits) uniquement si le PFU est réellement
|
||||||
|
// retenu à la source ici (plateforme flat_tax France, non exonérée) — sinon les
|
||||||
|
// prélèvements ne sont qu'indicatifs (déclaratif) et ne doivent pas apparaître comme
|
||||||
|
// un mouvement réel du porte-monnaie.
|
||||||
|
// La somme de ces lignes reste égale à l'ancien montant unique (net_recu / brut selon le
|
||||||
|
// cas), donc le solde porte-monnaie n'est pas affecté — seule la présentation change.
|
||||||
|
// Les bonus (parrainage/plateforme) créditent le cashback, sans prélèvement.
|
||||||
// Note : les remboursements methode_remboursement='compte_courant' transitent bien par le
|
// Note : les remboursements methode_remboursement='compte_courant' transitent bien par le
|
||||||
// porte-monnaie (crédit) avant d'en être aussitôt sortis par le retrait automatique
|
// porte-monnaie (crédit) avant d'en être aussitôt sortis par le retrait automatique
|
||||||
// (source='auto_remboursement', même montant) — on affiche donc les deux mouvements pour
|
// (source='auto_remboursement', même montant net) — on affiche donc ce mouvement aussi,
|
||||||
// que l'historique soit lisible, même si l'effet net sur le solde est nul (cf. calcul KPI
|
// même si l'effet net sur le solde est nul (cf. calcul KPI porte-monnaie ci-dessous, qui
|
||||||
// porte-monnaie ci-dessous, qui lui continue d'ignorer ces remboursements).
|
// lui continue d'ignorer ces remboursements).
|
||||||
const normalizedRembMouvements = useMemo(() => {
|
const normalizedRembMouvements = useMemo(() => {
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const r of allRemb) {
|
for (const r of allRemb) {
|
||||||
const isBonus = r.type === 'bonus_parrainage' || r.type === 'bonus_plateforme';
|
const isBonus = r.type === 'bonus_parrainage' || r.type === 'bonus_plateforme';
|
||||||
const plat = plats.find(p => p.id === r.plateforme_id);
|
const plat = plats.find(p => p.id === r.plateforme_id);
|
||||||
const montant = isBonus
|
const base = {
|
||||||
? (r.cashback || 0)
|
|
||||||
: (plat?.fiscalite === 'flat_tax'
|
|
||||||
? (r.net_recu || 0)
|
|
||||||
: (r.capital || 0) + (r.cashback || 0) + (r.interets_bruts || 0));
|
|
||||||
if (!montant) continue;
|
|
||||||
out.push({
|
|
||||||
id: `remb_${r.id}`,
|
|
||||||
_rembId: r.id,
|
_rembId: r.id,
|
||||||
plateforme_id: r.plateforme_id,
|
plateforme_id: r.plateforme_id,
|
||||||
plateforme_nom: r.plateforme_nom,
|
plateforme_nom: r.plateforme_nom,
|
||||||
plateforme_detenteur_nom: r.plateforme_detenteur_nom,
|
plateforme_detenteur_nom: r.plateforme_detenteur_nom,
|
||||||
date_operation: r.date_remb,
|
date_operation: r.date_remb,
|
||||||
type: isBonus ? 'bonus' : 'remboursement',
|
|
||||||
montant,
|
|
||||||
libelle: r.nom_projet || '',
|
libelle: r.nom_projet || '',
|
||||||
source: 'remboursement',
|
source: 'remboursement',
|
||||||
investisseur_id: r.resolved_investisseur_id ?? null,
|
investisseur_id: r.resolved_investisseur_id ?? null,
|
||||||
_isFlatTax: !isBonus && plat?.fiscalite === 'flat_tax',
|
};
|
||||||
_isAutoSweep: !isBonus && r.methode_remboursement === 'compte_courant',
|
|
||||||
|
if (isBonus) {
|
||||||
|
const montant = r.cashback || 0;
|
||||||
|
if (!montant) continue;
|
||||||
|
out.push({ ...base, id: `remb_${r.id}`, type: 'bonus', montant });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inv = invById.get(r.investissement_id);
|
||||||
|
const isFlatTax = plat?.fiscalite === 'flat_tax';
|
||||||
|
const isExonere = inv?.fiscalite_override === 'exonere';
|
||||||
|
// PFU réellement retenu à la source uniquement si flat_tax France non exonérée ;
|
||||||
|
// sinon prélèvements indicatifs seulement (déclaratif), pas un vrai mouvement de cash.
|
||||||
|
const pfuReellementRetenu = isFlatTax && !isExonere;
|
||||||
|
const taxeLocale = r.taxe_locale || 0;
|
||||||
|
const grossInterets = r.interets_bruts_avant_local > 0 ? r.interets_bruts_avant_local : (r.interets_bruts || 0);
|
||||||
|
const grossMontant = (r.capital || 0) + (r.cashback || 0) + grossInterets;
|
||||||
|
|
||||||
|
if (grossMontant) {
|
||||||
|
out.push({
|
||||||
|
...base,
|
||||||
|
id: `remb_${r.id}`,
|
||||||
|
type: 'remboursement',
|
||||||
|
montant: grossMontant,
|
||||||
|
_isFlatTax: isFlatTax,
|
||||||
|
_isAutoSweep: r.methode_remboursement === 'compte_courant',
|
||||||
|
_taxeLocale: taxeLocale,
|
||||||
_capital: r.capital || 0,
|
_capital: r.capital || 0,
|
||||||
_cashback: r.cashback || 0,
|
_cashback: r.cashback || 0,
|
||||||
_interets_bruts: r.interets_bruts || 0,
|
_interets_bruts: r.interets_bruts || 0,
|
||||||
@@ -651,8 +718,19 @@ export default function DepotsRetraits() {
|
|||||||
_net_recu: r.net_recu || 0,
|
_net_recu: r.net_recu || 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (taxeLocale > 0) {
|
||||||
|
out.push({ ...base, id: `remb_${r.id}_local`, type: 'prelevement_local', montant: taxeLocale });
|
||||||
|
}
|
||||||
|
if (pfuReellementRetenu && r.prelev_forfaitaire > 0) {
|
||||||
|
out.push({ ...base, id: `remb_${r.id}_ir`, type: 'prelevement_ir', montant: r.prelev_forfaitaire });
|
||||||
|
}
|
||||||
|
if (pfuReellementRetenu && r.prelev_sociaux > 0) {
|
||||||
|
out.push({ ...base, id: `remb_${r.id}_csg`, type: 'prelevement_csg', montant: r.prelev_sociaux });
|
||||||
|
}
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}, [allRemb, plats]);
|
}, [allRemb, plats, invById]);
|
||||||
|
|
||||||
// Filtrés (plateforme + année + mois, sans filtre type — appliqué au merge dans tableRows)
|
// Filtrés (plateforme + année + mois, sans filtre type — appliqué au merge dans tableRows)
|
||||||
const filteredRembMouvements = useMemo(() => normalizedRembMouvements.filter(r => {
|
const filteredRembMouvements = useMemo(() => normalizedRembMouvements.filter(r => {
|
||||||
@@ -667,10 +745,6 @@ export default function DepotsRetraits() {
|
|||||||
// sur les retraits AUTO, qui ne portent que remboursement_id)
|
// sur les retraits AUTO, qui ne portent que remboursement_id)
|
||||||
const rembProjetById = useMemo(() => new Map(allRemb.map(r => [r.id, r.nom_projet || ''])), [allRemb]);
|
const rembProjetById = useMemo(() => new Map(allRemb.map(r => [r.id, r.nom_projet || ''])), [allRemb]);
|
||||||
|
|
||||||
// Investissements par id (déjà scopés) — sert à rattacher les réinvestissements
|
|
||||||
// (qui ne portent que investissement_id) à leur plateforme/projet/détenteur.
|
|
||||||
const invById = useMemo(() => new Map(allInv.map(i => [i.id, i])), [allInv]);
|
|
||||||
|
|
||||||
// Souscriptions à un investissement (débit du porte-monnaie) + réinvestissements —
|
// Souscriptions à un investissement (débit du porte-monnaie) + réinvestissements —
|
||||||
// les réinvestissements automatiques de capital d'intérêts sont aussi traités comme une
|
// les réinvestissements automatiques de capital d'intérêts sont aussi traités comme une
|
||||||
// souscription (débit) : cohérent avec la formule du solde porte-monnaie, qui les
|
// souscription (débit) : cohérent avec la formule du solde porte-monnaie, qui les
|
||||||
@@ -1463,6 +1537,9 @@ export default function DepotsRetraits() {
|
|||||||
<option value="remboursement">Remboursements</option>
|
<option value="remboursement">Remboursements</option>
|
||||||
<option value="bonus">Bonus / Cashback</option>
|
<option value="bonus">Bonus / Cashback</option>
|
||||||
<option value="souscription">Souscriptions</option>
|
<option value="souscription">Souscriptions</option>
|
||||||
|
<option value="prelevement_ir">Prélèvements IR</option>
|
||||||
|
<option value="prelevement_csg">Prélèvements CSG</option>
|
||||||
|
<option value="prelevement_local">Prélèvements étranger</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 2, minWidth: 160, position: 'relative' }} ref={platFilterRef}>
|
<div style={{ flex: 2, minWidth: 160, position: 'relative' }} ref={platFilterRef}>
|
||||||
@@ -1599,6 +1676,35 @@ export default function DepotsRetraits() {
|
|||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (r.type === 'prelevement_ir' || r.type === 'prelevement_csg' || r.type === 'prelevement_local') {
|
||||||
|
const isLocal = r.type === 'prelevement_local';
|
||||||
|
const label = isLocal ? "prélèvement étranger" : r.type === 'prelevement_ir' ? 'prélèvement IR' : 'prélèvement CSG';
|
||||||
|
const color = isLocal ? '#f97316' : 'var(--danger)';
|
||||||
|
return (
|
||||||
|
<tr key={r.id}
|
||||||
|
className={`dr-row${selectedRow?.id === r.id ? ' dr-row-selected' : ''}`}
|
||||||
|
onClick={() => setSelectedRow(selectedRow?.id === r.id ? null : r)}
|
||||||
|
style={{ cursor: 'pointer' }}>
|
||||||
|
<td>{fmtDate(r.date_operation)}</td>
|
||||||
|
<td>{r.plateforme_nom}</td>
|
||||||
|
{multiDetenteur && <td className="text-muted" style={{ fontSize: 'var(--fs-sm)' }}>{r.plateforme_detenteur_nom || '—'}</td>}
|
||||||
|
<td>
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
fontSize: 11, fontWeight: 600, letterSpacing: '0.03em',
|
||||||
|
padding: '2px 7px', borderRadius: 4,
|
||||||
|
background: `color-mix(in srgb, ${color} 15%, transparent)`,
|
||||||
|
color,
|
||||||
|
}}>{label}</span>
|
||||||
|
</td>
|
||||||
|
<td className="text-muted">{r._projet || '—'}</td>
|
||||||
|
<td className="num danger">
|
||||||
|
− {fmtEUR(r.montant)}
|
||||||
|
</td>
|
||||||
|
<td style={{ width: 36, textAlign: 'center' }} />
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (r.type === 'souscription') {
|
if (r.type === 'souscription') {
|
||||||
return (
|
return (
|
||||||
<tr key={r.id}
|
<tr key={r.id}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
export const fmtEUR = (n) =>
|
export const fmtEUR = (n) => {
|
||||||
(n == null ? '' : Number(n).toLocaleString('fr-FR', { style: 'currency', currency: 'EUR' }));
|
if (n == null) return '';
|
||||||
|
// Évite l'affichage "-0,00 €" causé par un zéro négatif JS ou une imprécision
|
||||||
|
// flottante résiduelle (ex. somme de dépôts/retraits qui s'annulent presque).
|
||||||
|
const rounded = Math.round(Number(n) * 100) / 100;
|
||||||
|
const v = rounded === 0 ? 0 : rounded;
|
||||||
|
return v.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR' });
|
||||||
|
};
|
||||||
|
|
||||||
export const fmtNum = (n, digits = 2) =>
|
export const fmtNum = (n, digits = 2) =>
|
||||||
(n == null ? '' : Number(n).toLocaleString('fr-FR', { minimumFractionDigits: digits, maximumFractionDigits: digits }));
|
(n == null ? '' : Number(n).toLocaleString('fr-FR', { minimumFractionDigits: digits, maximumFractionDigits: digits }));
|
||||||
|
|||||||
Reference in new issue
Block a user