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 ────────────────────────── */
|
/* ── Panneau de détail d'un mouvement ────────────────────────── */
|
||||||
export function DetailPanel({ row, onEdit, onDeleteCorrection, onViewRemb, onViewInvestissement }) {
|
export function DetailPanel({ row, onEdit, onDeleteCorrection, onViewRemb, onViewInvestissement }) {
|
||||||
if (!row) return (
|
if (!row) return (
|
||||||
@@ -371,8 +464,11 @@ export function CorrectionModal({ open, onClose, onConfirm,
|
|||||||
fontSize: 'var(--fs-sm)', color: 'var(--text-muted)',
|
fontSize: 'var(--fs-sm)', color: 'var(--text-muted)',
|
||||||
marginBottom: 18, lineHeight: 1.5,
|
marginBottom: 18, lineHeight: 1.5,
|
||||||
}}>
|
}}>
|
||||||
Après ce {isRetrait ? 'retrait' : 'dépôt'} sur <strong>{plateforme_nom}</strong>,
|
{type_operation ? (
|
||||||
quel est le solde indiqué sur la plateforme ?
|
<>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.
|
{' '}Si différent du solde calculé, une correction sera créée automatiquement.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import SuiviObjectifs from '../components/SuiviObjectifs.jsx';
|
|||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import {
|
import {
|
||||||
DEBIT_TYPES, signedMontant, MOUV_TYPE_LABELS, mouvToCSV, mouvToXLS, mouvToJSON,
|
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,
|
timestampSuffix, dlBlob,
|
||||||
} from '../components/MouvementsShared.jsx';
|
} from '../components/MouvementsShared.jsx';
|
||||||
|
|
||||||
@@ -156,6 +156,8 @@ export default function DepotsRetraits() {
|
|||||||
open: false, plateforme_nom: '', type_operation: 'retrait',
|
open: false, plateforme_nom: '', type_operation: 'retrait',
|
||||||
computedBalance: 0, date: '', platId: null, investisseurId: null,
|
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 () => {
|
const load = async () => {
|
||||||
if (!activeId && activeView !== 'all') return;
|
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 close = () => { setModalOpen(false); setEditingId(null); setForm(emptyMouvementForm); setErr(null); };
|
||||||
|
|
||||||
const submit = async (e) => {
|
const submit = async (e) => {
|
||||||
@@ -766,7 +794,9 @@ export default function DepotsRetraits() {
|
|||||||
plateforme_id: corrModal.platId,
|
plateforme_id: corrModal.platId,
|
||||||
date: corrModal.date,
|
date: corrModal.date,
|
||||||
montant: diff,
|
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) {
|
} catch (e) {
|
||||||
console.error('Correction creation failed:', 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')}
|
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')}
|
onJSON={() => dlBlob(mouvToJSON(tableRows), `Mouvements Dépôts Retraits ${timestampSuffix()}.json`, 'application/json')}
|
||||||
/>
|
/>
|
||||||
|
<AddDropdown
|
||||||
|
disabled={plats.length === 0}
|
||||||
|
onNewCorrection={() => setCorrPickerOpen(true)}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-btn"
|
className="icon-btn"
|
||||||
@@ -1557,6 +1591,14 @@ export default function DepotsRetraits() {
|
|||||||
onConfirm={handleCorrectionConfirm}
|
onConfirm={handleCorrectionConfirm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CorrectionPickerModal
|
||||||
|
open={corrPickerOpen}
|
||||||
|
onClose={() => setCorrPickerOpen(false)}
|
||||||
|
onContinue={openCorrectionManual}
|
||||||
|
plateformes={plats}
|
||||||
|
multiDetenteur={multiDetenteur}
|
||||||
|
/>
|
||||||
|
|
||||||
{deleteConfirm && (
|
{deleteConfirm && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
open={true}
|
open={true}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import Modal from '../components/Modal.jsx';
|
|||||||
import ConfirmModal from '../components/ConfirmModal.jsx';
|
import ConfirmModal from '../components/ConfirmModal.jsx';
|
||||||
import {
|
import {
|
||||||
mouvToCSV, mouvToXLS, mouvToJSON,
|
mouvToCSV, mouvToXLS, mouvToJSON,
|
||||||
ExportDropdown, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
|
ExportDropdown, AddDropdown, CorrectionPickerModal, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
|
||||||
timestampSuffix, dlBlob,
|
timestampSuffix, dlBlob,
|
||||||
} from '../components/MouvementsShared.jsx';
|
} from '../components/MouvementsShared.jsx';
|
||||||
|
|
||||||
@@ -897,6 +897,8 @@ export default function Plateformes() {
|
|||||||
open: false, plateforme_nom: '', type_operation: 'retrait',
|
open: false, plateforme_nom: '', type_operation: 'retrait',
|
||||||
computedBalance: 0, date: '', platId: null, investisseurId: null,
|
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
|
// 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
|
// "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) => {
|
const mvSubmit = async (e) => {
|
||||||
e?.preventDefault?.(); setMvErr(null);
|
e?.preventDefault?.(); setMvErr(null);
|
||||||
try {
|
try {
|
||||||
@@ -1220,7 +1249,9 @@ export default function Plateformes() {
|
|||||||
plateforme_id: mvCorrModal.platId,
|
plateforme_id: mvCorrModal.platId,
|
||||||
date: mvCorrModal.date,
|
date: mvCorrModal.date,
|
||||||
montant: diff,
|
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) {
|
} catch (e) {
|
||||||
console.error('Correction creation failed:', 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')}
|
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')}
|
onJSON={() => dlBlob(mouvToJSON(mvTableRows), `Mouvements Porte-monnaie ${timestampSuffix()}.json`, 'application/json')}
|
||||||
/>
|
/>
|
||||||
|
<AddDropdown
|
||||||
|
disabled={filteredPlatIds.size === 0}
|
||||||
|
onNewCorrection={() => setMvCorrPickerOpen(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2801,6 +2836,15 @@ export default function Plateformes() {
|
|||||||
onConfirm={mvHandleCorrectionConfirm}
|
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 && (
|
{mvDeleteConfirm && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
open={true}
|
open={true}
|
||||||
|
|||||||
Reference in new issue
Block a user