Files
crowdlending-app/frontend/src/components/MouvementsShared.jsx
T
2026-08-16 17:48:40 +02:00

530 lines
26 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import * as XLSX from 'xlsx';
import { fmtEUR, fmtDate, today } from '../utils/format.js';
/* ── Éléments partagés entre l'onglet "Mouvements" de DepotsRetraits.jsx et l'onglet
"Mouvements Porte-monnaie" de Plateformes.jsx. Toute évolution ici s'applique aux deux
pages — c'est le but : éviter la dérive entre les deux implémentations. ── */
/* ── Helpers export ──────────────────────────────────────────── */
/** Horodatage local AAAAMMJJ_HHmmss (jamais toISOString(), cf. décalage UTC) */
export function timestampSuffix() {
const d = new Date();
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}
export function dlBlob(content, filename, type) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
}
export 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),
// pour permettre de sommer directement l'extraction et contrôler le solde.
// 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).
export function signedMontant(r) {
if (DEBIT_TYPES.has(r.type)) return -Math.abs(r.montant);
if (r.type === 'correction') return r.montant;
return Math.abs(r.montant);
}
export const MOUV_TYPE_LABELS = {
depot: 'Dépôt', retrait: 'Retrait', correction: 'Correction de solde',
remboursement: 'Remboursement', bonus: 'Bonus', cashback: 'Cashback', souscription: 'Souscription',
prelevement_ir: 'Prélèvement IR', prelevement_csg: 'Prélèvement CSG',
prelevement_local: "Prélèvement impôts à l'étranger",
interets_plateforme: 'Intérêts plateforme',
};
export function mouvToCSV(rows) {
const BOM = ''; const sep = ';';
const q = v => `"${String(v ?? '').replace(/"/g, '""')}"`;
const headers = ['Date','Plateforme','Type','Projet','Montant (€)','Libellé','Référence','Source'];
const data = rows.map(r => [
r.date_operation, r.plateforme_nom||'', MOUV_TYPE_LABELS[r.type] || r.type, r._projet||'',
String(signedMontant(r)).replace('.',','), r.libelle||'', r.reference||'', r.source||'',
]);
return BOM + [headers,...data].map(r => r.map(q).join(sep)).join('\r\n');
}
export function mouvToXLS(rows, multiDetenteur) {
const data = rows.map(r => ({
'Date': r.date_operation,
'Plateforme': r.plateforme_nom || '',
...(multiDetenteur ? { 'Détenteur': r.plateforme_detenteur_nom || '' } : {}),
'Type': MOUV_TYPE_LABELS[r.type] || r.type,
'Projet': r._projet || '',
'Montant (€)': signedMontant(r),
}));
const ws = XLSX.utils.json_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Mouvements');
return XLSX.write(wb, { type: 'array', bookType: 'xlsx' });
}
export function mouvToJSON(rows) {
const data = rows.map(r => ({
date_operation: r.date_operation,
plateforme_nom: r.plateforme_nom || '',
type: r.type,
projet: r._projet || null,
montant: signedMontant(r),
libelle: r.libelle || null,
reference: r.reference || null,
source: r.source || null,
}));
return JSON.stringify(data, null, 2);
}
/* ── Constantes ──────────────────────────────────────────────── */
export const MOIS_FR = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
export const emptyMouvementForm = { investisseur_id: '', plateforme_id: '', date_operation: today(), type: 'depot', montant: '', libelle: '', reference: '', notes: '' };
/* ── Menu déroulant d'export CSV/XLS/JSON ────────────────────── */
export function ExportDropdown({ disabled, onCSV, onXLS, onJSON }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!open) return;
const h = e => { if (!ref.current?.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', h);
return () => document.removeEventListener('mousedown', h);
}, [open]);
const choose = fn => { setOpen(false); fn(); };
return (
<div ref={ref} style={{ position: 'relative' }}>
<button type="button" className="icon-btn" disabled={disabled}
onClick={() => setOpen(o => !o)} title="Exporter">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</button>
{open && (
<div className="export-dropdown" role="menu">
<button role="menuitem" onClick={() => choose(onCSV)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
<line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="16" y2="17"/>
</svg>
<span><strong>Format CSV</strong><small>Compatible Excel, LibreOffice</small></span>
</button>
<button role="menuitem" onClick={() => choose(onXLS)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M9 13l2 2 4-4"/>
</svg>
<span><strong>Format Excel</strong><small>Fichier .xlsx Microsoft Excel</small></span>
</button>
<button role="menuitem" onClick={() => choose(onJSON)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
<path d="M8 13h1.5a1 1 0 0 1 1 1v1a1 1 0 0 0 1 1 1 1 0 0 0-1 1v1a1 1 0 0 1-1 1H8"/>
<path d="M16 13h-1.5a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1H16"/>
</svg>
<span><strong>Format JSON</strong><small>Réimportable, structuré</small></span>
</button>
</div>
)}
</div>
);
}
/* ── Menu déroulant "+" — actions rapides sur les mouvements (extensible :
pour l'instant uniquement la création d'une correction de solde) ── */
export function AddDropdown({ disabled, onNewCorrection }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!open) return;
const h = e => { if (!ref.current?.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', h);
return () => document.removeEventListener('mousedown', h);
}, [open]);
const choose = fn => { setOpen(false); fn(); };
return (
<div ref={ref} style={{ position: 'relative' }}>
<button type="button" className="icon-btn" disabled={disabled}
onClick={() => setOpen(o => !o)} title="Ajouter">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
{open && (
<div className="export-dropdown" role="menu">
<button role="menuitem" onClick={() => choose(onNewCorrection)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/>
</svg>
<span><strong>Correction de solde</strong><small>Ajuster manuellement le solde porte-monnaie</small></span>
</button>
</div>
)}
</div>
);
}
/** Petite modale de sélection plateforme + date, avant d'ouvrir CorrectionModal
en saisie libre (déclenchée depuis AddDropdown, pas depuis un mouvement existant). */
export function CorrectionPickerModal({ open, onClose, onContinue, plateformes = [], multiDetenteur, defaultPlateformeId }) {
const [platId, setPlatId] = useState('');
const [date, setDate] = useState('');
useEffect(() => {
if (open) {
setPlatId(defaultPlateformeId ? String(defaultPlateformeId) : '');
setDate(today());
}
}, [open, defaultPlateformeId]);
if (!open) return null;
return (
<div style={{
position: 'fixed', inset: 0, zIndex: 1000,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.4)',
}}>
<div style={{
background: 'var(--surface)', borderRadius: 10,
padding: '24px 28px', width: 380, maxWidth: '90vw',
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
border: '1px solid var(--border)',
}}>
<div style={{ marginBottom: 6, fontWeight: 700, fontSize: 'var(--fs-base)' }}>
Nouvelle correction de solde
</div>
<div style={{ fontSize: 'var(--fs-sm)', color: 'var(--text-muted)', marginBottom: 18, lineHeight: 1.5 }}>
Choisissez la plateforme et la date de référence pour la correction.
</div>
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 'var(--fs-sm)', fontWeight: 500, display: 'block', marginBottom: 4 }}>Plateforme</label>
<select value={platId} onChange={e => setPlatId(e.target.value)} style={{ width: '100%' }} autoFocus>
<option value="">— Choisir —</option>
{plateformes.map(p => (
<option key={p.id} value={p.id}>{p.nom}{multiDetenteur && p.investisseur_nom ? ` — ${p.investisseur_nom}` : ''}</option>
))}
</select>
</div>
<div style={{ marginBottom: 18 }}>
<label style={{ fontSize: 'var(--fs-sm)', fontWeight: 500, display: 'block', marginBottom: 4 }}>Date</label>
<input type="date" value={date} onChange={e => setDate(e.target.value)} style={{ width: '100%' }} />
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
<button onClick={onClose}>Annuler</button>
<button className="primary" disabled={!platId || !date}
onClick={() => onContinue({ plateforme_id: Number(platId), date })}>
Continuer
</button>
</div>
</div>
</div>
);
}
/* ── Panneau de détail d'un mouvement ────────────────────────── */
export function DetailPanel({ row, onEdit, onDeleteCorrection, onViewRemb, onViewInvestissement }) {
if (!row) return (
<div className="dr-detail dr-detail-empty">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.25, marginBottom: 8 }}>
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<span>Sélectionnez un mouvement</span>
</div>
);
const isCorrection = row.type === 'correction';
const isBonus = row.type === 'bonus';
const isCashback = row.type === 'cashback';
const isInteretsPlateforme = row.type === 'interets_plateforme';
const isRemb = row.type === 'remboursement' || isBonus || isCashback || isInteretsPlateforme;
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 typeLabel = isCorrection ? 'Correction de solde'
: isBonus ? 'Bonus'
: isCashback ? 'Cashback'
: row.type === 'remboursement' ? 'Remboursement (porte-monnaie, brut)'
: isInteretsPlateforme ? 'Intérêts plateforme (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')
: row.type === 'depot' ? 'Dépôt' : 'Retrait';
const editLabel = isCorrection ? 'Modifier la correction de solde'
: row.type === 'depot' ? 'Modifier le dépôt' : 'Modifier le retrait';
// Débit (−) pour retrait/souscription/prélèvements, crédit (+) pour dépôt/remboursement/bonus/cashback ;
// la correction est déjà signée en base.
const isDebitType = row.type === 'retrait' || isSouscription || isPrelevement;
const isPos = isCorrection ? row.montant >= 0 : !isDebitType;
const sign = isPos ? '+' : '−';
const montantFmt = `${sign} ${fmtEUR(Math.abs(row.montant))}`;
const montantColor = undefined;
const isRembOnly = row.type === 'remboursement' || isInteretsPlateforme;
const fields = [
{ label: 'Plateforme', value: row.plateforme_nom },
{ label: 'Type de mouvement', value: typeLabel },
{ label: 'Montant', value: montantFmt, color: montantColor },
{ label: 'Date', value: fmtDate(row.date_operation) },
isRembOnly && row.libelle && { label: 'Projet', value: row.libelle },
(isBonus || isCashback) && row.libelle && { label: 'Projet', value: row.libelle },
isRembOnly && !isInteretsPlateforme && { label: 'Capital', value: fmtEUR(row._capital) },
isRembOnly && row._cashback > 0 && { label: 'Cashback', value: fmtEUR(row._cashback) },
isRembOnly && { label: 'Intérêts bruts', value: fmtEUR(row._interets_bruts) },
isRembOnly && { 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 },
!isCorrection && !isRemb && !isSouscription && !isPrelevement && row.libelle && { label: 'Libellé', value: row.libelle },
!isCorrection && !isRemb && row.reference && { label: 'Référence', value: row.reference },
row.notes && { label: 'Notes', value: row.notes },
].filter(Boolean);
return (
<div className="dr-detail">
<div className="dr-detail-title">Détails du mouvement</div>
{isAuto && (
<div style={{
display: 'flex', alignItems: 'flex-start', gap: 8,
margin: '5px 5px 12px',
padding: '9px 12px', borderRadius: 6,
background: 'color-mix(in srgb, var(--primary) 10%, transparent)',
border: '1px solid color-mix(in srgb, var(--primary) 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: 'var(--primary)' }}>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
<span>
Retrait automatique — généré lors d'un remboursement sur compte courant.
Modifiable via la page Remboursements ou depuis la fiche détail de l'investissement.
</span>
</div>
)}
{isRemb && (
<div style={{
display: 'flex', alignItems: 'flex-start', gap: 8,
margin: '5px 5px 12px',
padding: '9px 12px', borderRadius: 6,
background: 'color-mix(in srgb, var(--success) 10%, transparent)',
border: '1px solid color-mix(in srgb, var(--success) 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: 'var(--success)' }}>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
<span>
{isBonus
? 'Bonus crédité directement au porte-monnaie de la plateforme.'
: isCashback
? "Cashback crédité au porte-monnaie de la plateforme, saisi sur un remboursement classique plutôt qu'en tant que bonus. Modifiable depuis la page Remboursements."
: isInteretsPlateforme
? "Intérêts versés par la plateforme, hors investissement précis — crédités au porte-monnaie (montant brut). Les prélèvements éventuels apparaissent en lignes séparées. Modifiable depuis la page Remboursements."
: row._isAutoSweep
? "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."
: "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>
</div>
)}
{isSouscription && (
<div style={{
display: 'flex', alignItems: 'flex-start', gap: 8,
margin: '5px 5px 12px',
padding: '9px 12px', borderRadius: 6,
background: 'color-mix(in srgb, var(--danger) 10%, transparent)',
border: '1px solid color-mix(in srgb, 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: 'var(--danger)' }}>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
<span>
{row._isReinvest
? "Réinvestissement automatique du capital d'intérêts dans un nouvel investissement — capital sorti du porte-monnaie de la plateforme."
: "Souscription à un investissement — capital sorti du porte-monnaie de la plateforme."}
</span>
</div>
)}
<div className="dr-detail-fields">
{fields.map(f => (
<div className="dr-detail-field" key={f.label}>
<span className="dr-detail-label">{f.label}</span>
<span className="dr-detail-value" style={f.color ? { color: f.color, fontWeight: 600 } : undefined}>{f.value}</span>
</div>
))}
</div>
<div className="dr-detail-footer">
{isAuto ? null : (isRemb || isPrelevement) ? (
<button className="dr-detail-edit-btn" onClick={() => onViewRemb(row)}>
Voir / modifier le remboursement
</button>
) : isSouscription ? (
<button className="dr-detail-edit-btn" onClick={() => onViewInvestissement(row)}>
Voir l'investissement
</button>
) : isCorrection ? (
<button className="dr-detail-edit-btn" onClick={() => onDeleteCorrection(row._correctionId)}>
Supprimer la correction
</button>
) : (
<button className="dr-detail-edit-btn" onClick={() => onEdit(row)}>
{editLabel}
</button>
)}
</div>
</div>
);
}
/** Modal de correction de solde porte-monnaie (micro-écarts flat_tax) */
export function CorrectionModal({ open, onClose, onConfirm,
plateforme_nom, type_operation, computedBalance, date }) {
const isRetrait = type_operation === 'retrait';
// Défaut : 0 pour un retrait (vider le PM), solde calculé pour un dépôt
const [declared, setDeclared] = useState('');
useEffect(() => {
if (open) setDeclared(isRetrait ? '0' : String(Math.round(computedBalance * 100) / 100));
}, [open, isRetrait, computedBalance]);
const diff = Math.round((Number(declared) - computedBalance) * 100) / 100;
const hasDiff = Math.abs(diff) >= 0.005;
const handleConfirm = () => {
onConfirm(hasDiff ? diff : 0);
};
if (!open) return null;
return (
<div style={{
position: 'fixed', inset: 0, zIndex: 1000,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.4)',
}}>
<div style={{
background: 'var(--surface)', borderRadius: 10,
padding: '24px 28px', width: 420, maxWidth: '90vw',
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
border: '1px solid var(--border)',
}}>
<div style={{ marginBottom: 6, fontWeight: 700, fontSize: 'var(--fs-base)' }}>
Vérification du solde porte-monnaie
</div>
<div style={{
fontSize: 'var(--fs-sm)', color: 'var(--text-muted)',
marginBottom: 18, lineHeight: 1.5,
}}>
{type_operation ? (
<>Après ce {isRetrait ? 'retrait' : 'dépôt'} sur <strong>{plateforme_nom}</strong>, quel est le solde indiqué sur la plateforme ?</>
) : (
<>Quel est le solde indiqué sur <strong>{plateforme_nom}</strong> à la date du {date ? fmtDate(date) : ''} ?</>
)}
{' '}Si différent du solde calculé, une correction sera créée automatiquement.
</div>
<div style={{ marginBottom: 12 }}>
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '8px 12px', borderRadius: 6, marginBottom: 6,
background: 'var(--surface-2)', fontSize: 'var(--fs-sm)',
}}>
<span style={{ color: 'var(--text-muted)' }}>Solde calculé</span>
<span style={{ fontWeight: 600 }}>{Math.round(computedBalance * 100) / 100} €</span>
</div>
<label style={{ fontSize: 'var(--fs-sm)', fontWeight: 500, display: 'block', marginBottom: 4 }}>
Solde constaté sur la plateforme (€)
</label>
<input
type="number" step="0.01"
value={declared}
onChange={e => setDeclared(e.target.value)}
style={{ width: '100%', fontSize: 'var(--fs-base)' }}
autoFocus
/>
</div>
{hasDiff && (
<div style={{
padding: '8px 12px', borderRadius: 6, marginBottom: 12,
background: diff > 0
? 'color-mix(in srgb, var(--success) 12%, transparent)'
: 'color-mix(in srgb, var(--danger) 12%, transparent)',
border: `1px solid ${diff > 0 ? 'color-mix(in srgb, var(--success) 30%, transparent)' : 'color-mix(in srgb, var(--danger) 30%, transparent)'}`,
fontSize: 'var(--fs-sm)', color: 'var(--text-muted)',
}}>
Une correction de <strong style={{ color: diff > 0 ? 'var(--success)' : 'var(--danger)' }}>
{diff > 0 ? '+' : ''}{diff} €
</strong> sera créée.
</div>
)}
{!hasDiff && declared !== '' && (
<div style={{
padding: '8px 12px', borderRadius: 6, marginBottom: 12,
background: 'color-mix(in srgb, var(--primary) 10%, transparent)',
fontSize: 'var(--fs-sm)', color: 'var(--text-muted)',
}}>
Solde identique au calcul — aucune correction nécessaire.
</div>
)}
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 8 }}>
<button onClick={onClose}>Passer</button>
<button className="primary" onClick={handleConfirm}>
{hasDiff ? 'Créer la correction' : 'OK'}
</button>
</div>
</div>
</div>
);
}