Amélioration des frais

This commit is contained in:
ocroguennec committed 2026-09-04 08:38:32 +02:00
1 parent 981ddf5830
commit 3e8508d48b
12 files changed
+1083 -206

No files matched your search

-127
View File
@@ -10,8 +10,6 @@ import { useUi } from '../context/UiContext.jsx';
import ConfirmModal from '../components/ConfirmModal.jsx';
import InteretsChart from '../components/InteretsChart.jsx';
import InteretsDistributionChart from '../components/InteretsDistributionChart.jsx';
import FraisDistributionChart from '../components/FraisDistributionChart.jsx';
import { FRAIS_CATEGORIE_LABELS } from '../components/MouvementsShared.jsx';
import TableauInteretsPlateforme from '../components/TableauInteretsPlateforme.jsx';
import DrillCellPanel from '../components/DrillCellPanel.jsx';
import RembFormModal from '../components/RembFormModal.jsx';
@@ -420,39 +418,6 @@ export default function Remboursements() {
[byPlatform]
);
/* ── Étape 5 : reporting "Frais payés" — filtré comme le reste de la page par
filterPlatId (plateforme) et rembPlatYear (année), sur date_operation cette fois. ── */
const fraisFiltered = useMemo(() =>
allFraisOps.filter(f => {
if (filterPlatId && String(f.plateforme_id) !== filterPlatId) return false;
if (rembPlatYear && (f.date_operation || '').slice(0, 4) !== rembPlatYear) return false;
return true;
}),
[allFraisOps, filterPlatId, rembPlatYear]
);
const fraisPrevFiltered = useMemo(() =>
allFraisOps.filter(f => {
if (filterPlatId && String(f.plateforme_id) !== filterPlatId) return false;
return (f.date_operation || '').slice(0, 4) === rembPrevYear;
}),
[allFraisOps, filterPlatId, rembPrevYear]
);
const fraisTotal = useMemo(() => fraisFiltered.reduce((s, f) => s + (f.montant || 0), 0), [fraisFiltered]);
const fraisPrevTotal = useMemo(() => fraisPrevFiltered.reduce((s, f) => s + (f.montant || 0), 0), [fraisPrevFiltered]);
const byCategorie = useMemo(() => {
const map = {};
for (const f of fraisFiltered) {
const key = f.categorie || 'autre';
if (!map[key]) map[key] = { categorie: key, montant: 0, count: 0 };
map[key].montant += f.montant || 0;
map[key].count += 1;
}
return Object.values(map).sort((a, b) => b.montant - a.montant);
}, [fraisFiltered]);
/* ── Tableau remboursements : filtrage client-side ── */
const tableRows = useMemo(() => {
// Remboursements filtrés
@@ -941,8 +906,6 @@ export default function Remboursements() {
onClick={() => setActiveTab('remboursements')}>Remboursements</button>
<button className={`dr-tab${activeTab === 'projections' ? ' active' : ''}`}
onClick={() => setActiveTab('projections')}>Projections Échéances</button>
<button className={`dr-tab${activeTab === 'frais' ? ' active' : ''}`}
onClick={() => setActiveTab('frais')}>Frais</button>
</div>}
{/* ══ ONGLET PLATEFORMES ══ */}
@@ -1561,96 +1524,6 @@ export default function Remboursements() {
</div>
)}
{/* ══ ONGLET FRAIS (Étape 5 — reporting consolidé) ══ */}
{activeTab === 'frais' && (
<div style={{ padding: '0 24px' }}>
{/* ── KPI ── */}
<div className="dr-kpi-row" style={{ marginBottom: 16 }}>
<div className="kpi">
<div className="label">
Frais payés{rembPlatYear && ` en ${rembPlatYear}`}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(fraisTotal)}</span>
{rembPlatYear && <TrendBadge current={fraisTotal} prev={fraisPrevTotal} invert />}
</div>
{rembPlatYear && (
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(fraisPrevTotal)} en {rembPrevYear}
</div>
)}
</div>
</div>
{/* ── Graphique ── */}
<div className="charts-row" style={{ marginBottom: 16 }}>
<FraisDistributionChart rows={fraisFiltered} />
</div>
{/* ── Tableau par catégorie ── */}
<div className="card" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
<h3 style={{ margin: 0 }}>
Répartition par catégorie
{rembPlatYear && <span style={{ marginLeft: 8 }}>pour l'année {rembPlatYear}</span>}
</h3>
{filterPlatId && (
<span style={{ fontSize: 12, color: 'var(--text-muted)', fontStyle: 'italic' }}>
Filtré sur cette plateforme
</span>
)}
</div>
<table>
<thead>
<tr>
<th>Catégorie</th>
<th className="num">Montant</th>
<th className="num">Nombre</th>
<th className="num" style={{ minWidth: 120 }}>Poids</th>
</tr>
</thead>
<tbody>
{byCategorie.length === 0 && (
<tr><td colSpan={4} className="text-muted" style={{ textAlign: 'center', padding: 24 }}>
{loading ? 'Chargement…' : 'Aucun frais enregistré pour ces critères'}
</td></tr>
)}
{byCategorie.map(c => {
const poids = fraisTotal !== 0 ? (c.montant / fraisTotal) * 100 : 0;
return (
<tr key={c.categorie}>
<td>{FRAIS_CATEGORIE_LABELS[c.categorie] || c.categorie}</td>
<td className="num">{fmtEUR(c.montant)}</td>
<td className="num" style={{ color: 'var(--text-muted)', fontSize: 'var(--fs-sm)' }}>{c.count}</td>
<td className="num">
<div style={{ display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'flex-end' }}>
<div style={{ width: 48, height: 5, borderRadius: 3, background: 'var(--surface-2)', overflow: 'hidden' }}>
<div style={{ width: `${Math.max(0, poids)}%`, height: '100%', background: 'var(--primary)', borderRadius: 3 }} />
</div>
<span style={{ minWidth: 36, textAlign: 'right', fontSize: 'var(--fs-sm)' }}>
{poids.toFixed(1)} %
</span>
</div>
</td>
</tr>
);
})}
</tbody>
{byCategorie.length > 1 && (
<tfoot>
<tr style={{ borderTop: '2px solid var(--border)', fontWeight: 700, background: 'var(--surface-2)' }}>
<td>Total — {byCategorie.length} catégories</td>
<td className="num">{fmtEUR(fraisTotal)}</td>
<td className="num" style={{ color: 'var(--text-muted)', fontSize: 'var(--fs-sm)' }}>{fraisFiltered.length}</td>
<td className="num"><span style={{ fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>100 %</span></td>
</tr>
</tfoot>
)}
</table>
</div>
</div>
)}
{/* ══ MODAL ══ */}
{(() => {