Correction de solde
This commit is contained in:
1 parent
1bab97b5b3
commit
849e16de33
3 files changed
+188
-6
No files matched your search
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user