From 03b7b875384a24db2d3e94b7ce3dfe312f4fa43f Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 18 Jun 2026 08:25:05 +0200 Subject: [PATCH] =?UTF-8?q?Am=C3=A9liorations=20diverses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/dashboard.js | 19 ++-- frontend/src/components/DrillCellPanel.jsx | 12 ++- frontend/src/components/InvMensuelTable.jsx | 29 +----- .../components/TableauInteretsPlateforme.jsx | 7 +- frontend/src/pages/Dashboard.jsx | 4 +- frontend/src/pages/Plateformes.jsx | 92 ++++++++++++++----- frontend/src/styles.css | 11 ++- 7 files changed, 110 insertions(+), 64 deletions(-) diff --git a/backend/src/routes/dashboard.js b/backend/src/routes/dashboard.js index 2960472..097367a 100644 --- a/backend/src/routes/dashboard.js +++ b/backend/src/routes/dashboard.js @@ -805,16 +805,23 @@ router.get('/interets-par-plateforme', (req, res) => { * 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 { plateforme_id, plateforme_ids, 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 = ?' : ''; + const moisStr = `${annee}-${String(mois).padStart(2, '0')}`; + + /* Support filtre simple (platId) OU multiple (platIds, vue consolidée) */ + const platIds = plateforme_ids + ? plateforme_ids.split(',').map(Number).filter(Boolean) + : plateforme_id ? [Number(plateforme_id)].filter(Boolean) : []; + + const platFilter = platIds.length + ? `AND i.plateforme_id IN (${platIds.map(() => '?').join(',')})` + : ''; let invWhere, invParams; if (scopeAll) { @@ -830,8 +837,8 @@ router.get('/detail-cellule', (req, res) => { invParams = [invId]; } - const recusParams = platId ? [...invParams, platId, moisStr] : [...invParams, moisStr]; - const projetesParams = platId ? [...invParams, platId, moisStr, moisStr] : [...invParams, moisStr, moisStr]; + const recusParams = [...invParams, ...platIds, moisStr]; + const projetesParams = [...invParams, ...platIds, moisStr, moisStr]; /* ── Remboursements reçus ─────────────────────────────────── */ const recus = db.prepare(` diff --git a/frontend/src/components/DrillCellPanel.jsx b/frontend/src/components/DrillCellPanel.jsx index 82946ed..e04435e 100644 --- a/frontend/src/components/DrillCellPanel.jsx +++ b/frontend/src/components/DrillCellPanel.jsx @@ -72,10 +72,20 @@ export default function DrillCellPanel({ if (!cell) { setData(null); return; } setLoading(true); setData(null); + + /* En mode consolidé, cell.platIds contient plusieurs IDs numériques ; + si l'utilisateur a choisi un filtre manuel dans le select, on l'utilise tel quel */ + const isInitialFilter = cell.platId ? filterPlatId === String(cell.platId) : !filterPlatId; + const consolidatedIds = isInitialFilter && cell.platIds?.length > 1 + ? cell.platIds.join(',') + : null; + const params = { annee: cell.annee, mois: cell.mois, - ...(filterPlatId ? { plateforme_id: filterPlatId } : {}), + ...(consolidatedIds + ? { plateforme_ids: consolidatedIds } + : filterPlatId ? { plateforme_id: filterPlatId } : {}), ...(activeView === 'all' ? { scope: 'all' } : {}), }; api.get('/dashboard/detail-cellule', params) diff --git a/frontend/src/components/InvMensuelTable.jsx b/frontend/src/components/InvMensuelTable.jsx index c1a50c7..6ac7f59 100644 --- a/frontend/src/components/InvMensuelTable.jsx +++ b/frontend/src/components/InvMensuelTable.jsx @@ -12,20 +12,6 @@ function startOfMonth(Y, M) { return `${Y}-${String(M).padStart(2,'0')}-01`; } -const STATUT_BG = { - en_cours: 'var(--b-en_cours-bg)', - rembourse: 'var(--b-rembourse-bg)', - en_retard: 'var(--b-en_retard-bg)', - procedure: 'var(--b-procedure-bg)', - cloture: 'var(--surface-2)', -}; -const STATUT_FG = { - en_cours: 'var(--b-en_cours-fg)', - rembourse: 'var(--b-rembourse-fg)', - en_retard: 'var(--b-en_retard-fg)', - procedure: 'var(--b-procedure-fg)', - cloture: 'var(--text-muted)', -}; export default function InvMensuelTable({ rows, allRembs, allReinvests, year, originFrom }) { const navigate = useNavigate(); @@ -125,11 +111,7 @@ export default function InvMensuelTable({ rows, allRembs, allReinvests, year, or Investissement - Statut + Statut {MOIS_LONG.map((m, i) => ( @@ -148,14 +130,7 @@ export default function InvMensuelTable({ rows, allRembs, allReinvests, year, or {inv.nom_projet || '—'} - - {fmtStatut(inv.statut)} - + {fmtStatut(inv.statut)} {months.map((v, mi) => { const curClass = displayYear === currentYear && mi === currentMonth - 1 ? ' tip-col-current' : ''; diff --git a/frontend/src/components/TableauInteretsPlateforme.jsx b/frontend/src/components/TableauInteretsPlateforme.jsx index 9c4ee4b..2fd0d43 100644 --- a/frontend/src/components/TableauInteretsPlateforme.jsx +++ b/frontend/src/components/TableauInteretsPlateforme.jsx @@ -136,12 +136,14 @@ export default function TableauInteretsPlateforme({ activeView, activeId, pfuRat if (!byNom[plat.nom]) { byNom[plat.nom] = { ...plat, - id: plat.nom, + id: plat.nom, + _ids: [plat.id], // ← tous les IDs numériques fusionnés detenteur_nom: null, rembourses: { ...plat.rembourses }, projections: { ...plat.projections }, }; } else { + byNom[plat.nom]._ids.push(plat.id); byNom[plat.nom].rembourses = mergeMaps(byNom[plat.nom].rembourses, plat.rembourses); byNom[plat.nom].projections = mergeMaps(byNom[plat.nom].projections, plat.projections); } @@ -397,7 +399,8 @@ export default function TableauInteretsPlateforme({ activeView, activeId, pfuRat className={`tip-td-num${v?.projected ? ' tip-projected' : ''}${isCurrent ? ' tip-col-current' : ''}${isActive ? ' tip-td-active' : ''}${clickable ? ' tip-td-clickable' : ''}`} onClick={() => clickable && onCellClick && onCellClick({ key: cellKey, - platId: plat.id, + platId: plat._ids ? plat._ids[0] : plat.id, // toujours numérique + platIds: plat._ids ?? [plat.id], // tous les IDs (consolidé) platNom: plat.nom, annee, mois: String(mi + 1).padStart(2, '0'), diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index d34b6f0..19c959d 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -518,8 +518,8 @@ export default function Dashboard() { activeId={activeId} pfuRates={pfuRates} onCapitalMensuel={setCapitalMensuelData} - onCellClick={({ platId, platNom, annee, mois, moisLabel }) => - setDrillCell({ platId, platNom, annee, mois, moisLabel }) + onCellClick={({ platId, platIds, platNom, annee, mois, moisLabel }) => + setDrillCell({ platId, platIds, platNom, annee, mois, moisLabel }) } activeCell={drillCell} /> diff --git a/frontend/src/pages/Plateformes.jsx b/frontend/src/pages/Plateformes.jsx index bab1098..83fea1d 100644 --- a/frontend/src/pages/Plateformes.jsx +++ b/frontend/src/pages/Plateformes.jsx @@ -8,6 +8,8 @@ import EmptyState from '../components/EmptyState.jsx'; import InvChart from '../components/InvChart.jsx'; import InvMensuelTable from '../components/InvMensuelTable.jsx'; import { fmtEUR, fmtDate, fmtStatut, memberLabel } from '../utils/format.js'; +import { usePagination } from '../hooks/usePagination.js'; +import Pagination from '../components/Pagination.jsx'; const LOGOS_BASE = '/api/logos/'; @@ -324,7 +326,7 @@ export default function Plateformes() { const [pfuRates, setPfuRates] = useState([]); /* ── État UI ── */ - const activeTab = searchParams.get('tab') || 'depots-retraits'; + const activeTab = searchParams.get('tab') || 'remboursements'; const setActiveTab = (tab) => setSearchParams(p => { const n = new URLSearchParams(p); n.set('tab', tab); return n; }, { replace: true }); const [listFocused, setListFocused] = useState(false); @@ -526,6 +528,17 @@ export default function Plateformes() { }, { investi: 0, cap_remb: 0, int_perc: 0, int_perc_net: 0, encours: 0, defaut: 0 }), [chartRows, capRembParInv, rembParInv, reinvestCumulParInv]); + /* ── Pagination onglet Investissements ── */ + const sortedChartRows = useMemo(() => + chartRows.slice().sort((a, b) => (a.date_souscription || '') < (b.date_souscription || '') ? -1 : 1), + [chartRows] + ); + const { + pagedItems: pagedChartRows, page: platInvPage, setPage: setPlatInvPage, + pageSize: platInvPageSize, setPageSize: setPlatInvPageSize, + totalPages: platInvTotalPages, totalItems: platInvTotalItems, PAGE_SIZES: platInvPageSizes, + } = usePagination(sortedChartRows, 'cl_pagesize_plat_inv', [selectedPlatName, selectedYear]); + /* ── KPI N-1 (pour TrendBadge) ── */ const prevTotals = useMemo(() => { const effectiveYear = selectedYear || String(new Date().getFullYear()); @@ -1004,7 +1017,7 @@ export default function Plateformes() { )} - Statut + Statut {MOIS_LONG.map((m, i) => ( {m} ))} @@ -1030,14 +1043,7 @@ export default function Plateformes() { {inv.nom_projet || '—'} - - {fmtStatut(inv.statut)} - + {fmtStatut(inv.statut)} {(() => { const firstNonNull = months.findIndex(v => v !== null); @@ -1163,9 +1169,10 @@ export default function Plateformes() {
-
- Mouvements de trésorerie · {selectedYear || 'Toutes les années'} -
+

+ Mouvements de trésorerie + — {selectedYear || 'Toutes les années'} +

@@ -1187,6 +1194,22 @@ export default function Plateformes() { onClick={() => setSelectedYear('')}> TOUT +
@@ -1380,9 +1403,10 @@ export default function Plateformes() {
-
- Capital investi par investissement · {selectedYear || 'Toutes les années'} -
+

+ Capital investi par investissement + — {selectedYear || 'Toutes les années'} +

@@ -1444,9 +1468,26 @@ export default function Plateformes() { Investissements {selectedYear && — {selectedYear}} - - {chartRows.length} investissement{chartRows.length !== 1 ? 's' : ''} - +
+ + {chartRows.length} investissement{chartRows.length !== 1 ? 's' : ''} + + +
{!chartRows.length ? ( @@ -1454,6 +1495,7 @@ export default function Plateformes() { {loading ? 'Chargement…' : 'Aucun investissement'}
) : ( + <> @@ -1467,10 +1509,7 @@ export default function Plateformes() { - {chartRows - .slice() - .sort((a, b) => (a.date_souscription || '') < (b.date_souscription || '') ? -1 : 1) - .map(r => { + {pagedChartRows.map(r => { const capInv = r.montant_investi + (reinvestCumulParInv[r.id] || 0); const capRemb = capRembParInv[r.id] || 0; const capRestant = Math.max(0, capInv - capRemb); @@ -1492,6 +1531,13 @@ export default function Plateformes() { })}
+ + )}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 13724c0..6a11d22 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1716,17 +1716,18 @@ tr:hover td { background: var(--surface-2); } padding: 6px 10px; } .tip-th-name { - background: var(--surface-2); + background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%); + color: #fff; text-align: left; padding: 6px 12px; - font-weight: 600; + font-weight: 700; font-size: var(--fs-sm); white-space: nowrap; min-width: 160px; position: sticky; left: 0; z-index: 2; - border-right: 1px solid var(--border); + border-right: 1px solid rgba(255,255,255,.2); } .tip-th-month { background: var(--surface-2); @@ -1777,6 +1778,8 @@ tr:hover td { background: var(--surface-2); } white-space: nowrap; color: var(--text-muted); font-size: var(--fs-xs); + background: rgba(109,40,217,.04); + border-left: 1px solid rgba(109,40,217,.1); } /* Valeurs projetées */ @@ -1854,6 +1857,8 @@ tr:hover td { background: var(--surface-2); } /* Dark mode ajustements */ [data-theme="dark"] .tip-footer-total { background: rgba(217,119,6,.12); } [data-theme="dark"] .tip-td-total { background: rgba(217,119,6,.08); } +[data-theme="dark"] .tip-td-avg { background: rgba(217,119,6,.04); border-left-color: rgba(217,119,6,.15); } +[data-theme="dark"] .tip-th-name { background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%); } /* Colonne mois courant */ .tip-th-month-current {