Améliorations

This commit is contained in:
ocroguennec committed 2026-09-05 17:05:54 +02:00
1 parent 8d104f2058
commit bab7a79b63
4 files changed
+147 -5

No files matched your search

+31 -5
View File
@@ -48,6 +48,17 @@ export default function InvChart({ rows, remboursements, reinvestissements, plat
const currentYear = String(new Date().getFullYear());
const cutoffStr = platYear && platYear !== currentYear ? `${platYear}-12-31` : todayStr;
// Coupure appliquée aux MONTANTS cumulés (formule "Capital investi" unifiée,
// cf. project_invchart_capital_encours) : bornée au 31/12/{platYear} pour une
// année passée révolue, ILLIMITÉE sinon — exactement comme les totaux KPI
// (Plateformes.jsx / Investissements.jsx, cutoff = selectedYear ? ... : null).
// Sans ça, un remboursement déjà saisi avec une date de règlement future
// (ex. Olivier enregistre le remboursement final dès que la plateforme
// l'annonce, avec la date de règlement communiquée, avant qu'elle soit
// atteinte) est compté par le KPI mais pas encore par le graphique, qui
// affiche alors un capital investi supérieur au KPI de exactement ce montant.
const dataCutoffStr = platYear && platYear !== currentYear ? `${platYear}-12-31` : null;
/* ── 1. Courbe capital en cours = investissements − capital remboursé ── */
const allPoints = useMemo(() => {
if (!rows?.length) return [];
@@ -61,7 +72,7 @@ export default function InvChart({ rows, remboursements, reinvestissements, plat
// +montant_investi à chaque date de souscription
for (const r of rows) {
const d = r.date_souscription?.slice(0, 10);
if (!d || d > cutoffStr) continue;
if (!d || (dataCutoffStr && d > dataCutoffStr)) continue;
deltas[d] = (deltas[d] || 0) + (r.montant_investi ?? 0);
}
@@ -70,7 +81,7 @@ export default function InvChart({ rows, remboursements, reinvestissements, plat
for (const rv of reinvestissements) {
if (!rowIds.has(rv.investissement_id)) continue;
const d = rv.date_reinvestissement?.slice(0, 10);
if (!d || !rv.montant || d > cutoffStr) continue;
if (!d || !rv.montant || (dataCutoffStr && d > dataCutoffStr)) continue;
deltas[d] = (deltas[d] || 0) + rv.montant;
}
}
@@ -80,18 +91,33 @@ export default function InvChart({ rows, remboursements, reinvestissements, plat
for (const rb of remboursements) {
if (!rowIds.has(rb.investissement_id)) continue;
const d = rb.date_remb?.slice(0, 10);
if (!d || !rb.capital || d > cutoffStr) continue;
if (!d || !rb.capital || (dataCutoffStr && d > dataCutoffStr)) continue;
deltas[d] = (deltas[d] || 0) - rb.capital;
}
}
// Tri chronologique + cumul
let cum = 0;
return Object.keys(deltas).sort().map(date => {
const cumulated = Object.keys(deltas).sort().map(date => {
cum += deltas[date];
return { date, value: Math.max(0, cum) };
});
}, [rows, remboursements, reinvestissements, cutoffStr]);
// Un point dont la date dépasse aujourd'hui (remboursement/réinvestissement
// saisi par avance, cf. dataCutoffStr) est regroupé sur la date du jour pour
// l'affichage : la courbe ne va jamais visuellement au-delà d'aujourd'hui,
// mais la valeur cumulée (déjà correcte, alignée sur le KPI) est conservée.
const clamped = [];
for (const p of cumulated) {
const d = p.date > todayStr ? todayStr : p.date;
if (clamped.length && clamped[clamped.length - 1].date === d) {
clamped[clamped.length - 1] = { date: d, value: p.value };
} else {
clamped.push({ date: d, value: p.value });
}
}
return clamped;
}, [rows, remboursements, reinvestissements, dataCutoffStr, todayStr]);
const currentValue = allPoints.length ? allPoints[allPoints.length - 1].value : 0;
@@ -35,6 +35,7 @@ function SectionHeader({ title, description }) {
const DEFAULT = {
appName: 'Crowdlending Tracker',
appUrl: '',
environnement: 'developpement',
mcpUrl: '',
allowRegistration: true,
minPasswordLength: 8,
@@ -42,6 +43,12 @@ const DEFAULT = {
documentsQuotaCount: 500,
};
const ENVIRONNEMENTS = [
{ value: 'production', label: 'Production' },
{ value: 'qualification', label: 'Qualification' },
{ value: 'developpement', label: 'Développement' },
];
export default function GeneralSection() {
const [form, setForm] = useState(DEFAULT);
const [saved, setSaved] = useState(null); // 'ok' | 'err' | null
@@ -58,6 +65,7 @@ export default function GeneralSection() {
setForm({
appName: dd.appName || 'Crowdlending Tracker',
appUrl: dd.appUrl || '',
environnement: dd.environnement || 'developpement',
mcpUrl: dd.mcpUrl || '',
allowRegistration: dd.allowRegistration !== false,
minPasswordLength: dd.minPasswordLength || 8,
@@ -77,6 +85,7 @@ export default function GeneralSection() {
await api.patch('/admin/general', {
appName: form.appName.trim(),
appUrl: form.appUrl.trim(),
environnement: form.environnement,
mcpUrl: form.mcpUrl.trim(),
allowRegistration: form.allowRegistration,
minPasswordLength: form.minPasswordLength,
@@ -131,6 +140,18 @@ export default function GeneralSection() {
/>
</SettingRow>
<SettingRow label="Environnement" description="Identifie cette instance (affiché dans l'interface). Choisi manuellement par un administrateur : conservé tel quel lors d'une restauration de base copiée depuis un autre environnement.">
<select
value={form.environnement}
onChange={e => set('environnement', e.target.value)}
style={{ width: '100%' }}
>
{ENVIRONNEMENTS.map(({ value, label }) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</SettingRow>
<SettingRow label="URL du serveur MCP" description={`Point d'entrée public du serveur MCP (endpoint /mcp inclus). Inclure le protocole (https://).${import.meta.env.DEV ? ' Valeur forcée en développement local (ignore celle de la base, potentiellement une copie de la prod).' : ''}`}>
<input
className="form-input"