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 {