Correction de solde

This commit is contained in:
ocroguennec committed 2026-08-16 17:48:40 +02:00
1 parent 1bab97b5b3
commit 849e16de33
3 files changed
+188 -6

No files matched your search

+98 -2
View File
@@ -134,6 +134,99 @@ export function ExportDropdown({ disabled, onCSV, onXLS, onJSON }) {
);
}
/* ── 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 (
@@ -371,8 +464,11 @@ export function CorrectionModal({ open, onClose, onConfirm,
fontSize: 'var(--fs-sm)', color: 'var(--text-muted)',
marginBottom: 18, lineHeight: 1.5,
}}>
Après ce {isRetrait ? 'retrait' : 'dépôt'} sur <strong>{plateforme_nom}</strong>,
quel est le solde indiqué sur la plateforme ?
{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>
+44 -2
View File
@@ -16,7 +16,7 @@ import SuiviObjectifs from '../components/SuiviObjectifs.jsx';
import * as XLSX from 'xlsx';
import {
DEBIT_TYPES, signedMontant, MOUV_TYPE_LABELS, mouvToCSV, mouvToXLS, mouvToJSON,
ExportDropdown, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
ExportDropdown, AddDropdown, CorrectionPickerModal, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
timestampSuffix, dlBlob,
} from '../components/MouvementsShared.jsx';
@@ -156,6 +156,8 @@ export default function DepotsRetraits() {
open: false, plateforme_nom: '', type_operation: 'retrait',
computedBalance: 0, date: '', platId: null, investisseurId: null,
});
// État de la modale de sélection plateforme/date (correction de solde autonome, bouton "+")
const [corrPickerOpen, setCorrPickerOpen] = useState(false);
const load = async () => {
if (!activeId && activeView !== 'all') return;
@@ -705,6 +707,32 @@ export default function DepotsRetraits() {
});
};
/* Correction de solde autonome (bouton "+" du tableau Mouvements) — pas de
mouvement dépôt/retrait déclencheur, l'utilisateur choisit plateforme + date. */
const openCorrectionManual = async ({ plateforme_id, date }) => {
const plat = plats.find(p => p.id === plateforme_id);
const investId = investisseurForPlat(plateforme_id);
let computedBalance = 0;
try {
const result = await api.get('/dashboard/solde-historique', { plateforme_id, date });
computedBalance = result.solde ?? 0;
} catch (e) {
console.error('solde-historique error:', e);
}
setCorrPickerOpen(false);
setCorrModal({
open: true,
plateforme_nom: plat?.nom || '',
type_operation: null,
computedBalance,
date,
platId: plateforme_id,
investisseurId: investId ? Number(investId) : null,
});
};
const close = () => { setModalOpen(false); setEditingId(null); setForm(emptyMouvementForm); setErr(null); };
const submit = async (e) => {
@@ -766,7 +794,9 @@ export default function DepotsRetraits() {
plateforme_id: corrModal.platId,
date: corrModal.date,
montant: diff,
notes: `Correction solde porte-monnaie — ${corrModal.type_operation === 'retrait' ? 'retrait' : 'dépôt'}`,
notes: corrModal.type_operation
? `Correction solde porte-monnaie — ${corrModal.type_operation === 'retrait' ? 'retrait' : 'dépôt'}`
: 'Correction solde porte-monnaie — saisie manuelle',
});
} catch (e) {
console.error('Correction creation failed:', e);
@@ -1116,6 +1146,10 @@ export default function DepotsRetraits() {
onXLS={() => dlBlob(mouvToXLS(tableRows, multiDetenteur), `Mouvements Dépôts Retraits ${timestampSuffix()}.xlsx`, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
onJSON={() => dlBlob(mouvToJSON(tableRows), `Mouvements Dépôts Retraits ${timestampSuffix()}.json`, 'application/json')}
/>
<AddDropdown
disabled={plats.length === 0}
onNewCorrection={() => setCorrPickerOpen(true)}
/>
<button
type="button"
className="icon-btn"
@@ -1557,6 +1591,14 @@ export default function DepotsRetraits() {
onConfirm={handleCorrectionConfirm}
/>
<CorrectionPickerModal
open={corrPickerOpen}
onClose={() => setCorrPickerOpen(false)}
onContinue={openCorrectionManual}
plateformes={plats}
multiDetenteur={multiDetenteur}
/>
{deleteConfirm && (
<ConfirmModal
open={true}
+46 -2
View File
@@ -19,7 +19,7 @@ import Modal from '../components/Modal.jsx';
import ConfirmModal from '../components/ConfirmModal.jsx';
import {
mouvToCSV, mouvToXLS, mouvToJSON,
ExportDropdown, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
ExportDropdown, AddDropdown, CorrectionPickerModal, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
timestampSuffix, dlBlob,
} from '../components/MouvementsShared.jsx';
@@ -897,6 +897,8 @@ export default function Plateformes() {
open: false, plateforme_nom: '', type_operation: 'retrait',
computedBalance: 0, date: '', platId: null, investisseurId: null,
});
// État de la modale de sélection plateforme/date (correction de solde autonome, bouton "+")
const [mvCorrPickerOpen, setMvCorrPickerOpen] = useState(false);
// Le périmètre plateforme de cet onglet est strictement verrouillé sur la sélection
// "Plateforme" de la page (filteredPlatIds) — aucun contrôle interne ne permet de
@@ -1187,6 +1189,33 @@ export default function Plateformes() {
});
};
/* Correction de solde autonome (bouton "+" du tableau Mouvements Porte-monnaie) —
pas de mouvement dépôt/retrait déclencheur, l'utilisateur choisit plateforme + date
parmi les plateformes du périmètre de la page (filteredPlatIds). */
const mvOpenCorrectionManual = async ({ plateforme_id, date }) => {
const plat = plats.find(p => p.id === plateforme_id);
const investId = mvInvestisseurForPlat(plateforme_id);
let computedBalance = 0;
try {
const result = await api.get('/dashboard/solde-historique', { plateforme_id, date });
computedBalance = result.solde ?? 0;
} catch (e) {
console.error('solde-historique error:', e);
}
setMvCorrPickerOpen(false);
setMvCorrModal({
open: true,
plateforme_nom: plat?.nom || '',
type_operation: null,
computedBalance,
date,
platId: plateforme_id,
investisseurId: investId ? Number(investId) : null,
});
};
const mvSubmit = async (e) => {
e?.preventDefault?.(); setMvErr(null);
try {
@@ -1220,7 +1249,9 @@ export default function Plateformes() {
plateforme_id: mvCorrModal.platId,
date: mvCorrModal.date,
montant: diff,
notes: `Correction solde porte-monnaie — ${mvCorrModal.type_operation === 'retrait' ? 'retrait' : 'dépôt'}`,
notes: mvCorrModal.type_operation
? `Correction solde porte-monnaie — ${mvCorrModal.type_operation === 'retrait' ? 'retrait' : 'dépôt'}`
: 'Correction solde porte-monnaie — saisie manuelle',
});
} catch (e) {
console.error('Correction creation failed:', e);
@@ -2394,6 +2425,10 @@ export default function Plateformes() {
onXLS={() => dlBlob(mouvToXLS(mvTableRows, multiDetenteur), `Mouvements Porte-monnaie ${timestampSuffix()}.xlsx`, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
onJSON={() => dlBlob(mouvToJSON(mvTableRows), `Mouvements Porte-monnaie ${timestampSuffix()}.json`, 'application/json')}
/>
<AddDropdown
disabled={filteredPlatIds.size === 0}
onNewCorrection={() => setMvCorrPickerOpen(true)}
/>
</div>
</div>
@@ -2801,6 +2836,15 @@ export default function Plateformes() {
onConfirm={mvHandleCorrectionConfirm}
/>
<CorrectionPickerModal
open={mvCorrPickerOpen}
onClose={() => setMvCorrPickerOpen(false)}
onContinue={mvOpenCorrectionManual}
plateformes={plats.filter(p => filteredPlatIds.has(p.id))}
multiDetenteur={multiDetenteur}
defaultPlateformeId={filteredPlatIds.size === 1 ? [...filteredPlatIds][0] : undefined}
/>
{mvDeleteConfirm && (
<ConfirmModal
open={true}