Files
crowdlending-app/frontend/src/components/InvestissementSelect.jsx
T
2026-09-18 08:01:50 +02:00

146 lines
6.4 KiB
React

import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { fmtEUR } from '../utils/format.js';
/**
* InvestissementSelect — combobox pour le champ "Investissement" de RembFormModal.
*
* Remplace le <select> natif : le navigateur ne permet pas de limiter la hauteur du popup
* d'un <select> natif via CSS, ce qui produisait une liste déroulante démesurément haute
* quand beaucoup d'investissements existent.
*
* Positionnement en `position: fixed` (calculé via getBoundingClientRect, comme InvSelect.jsx) —
* pas en `position: absolute` — car ce champ est utilisé dans RembFormModal dont le conteneur
* `.card` a `overflow: auto` (voir Modal.jsx) : un dropdown en absolute y serait rogné quand la
* fenêtre est basse. Le dropdown bascule aussi au-dessus du champ s'il n'y a pas assez de place
* en dessous, et se ferme au scroll/resize (repositionnement non géré en direct, comme InvSelect).
*
* Props :
* value : '' | 'BONUS_PARRAINAGE' | 'BONUS_PLATEFORME' | 'INTERETS_PLATEFORME' | id (string/number)
* onChange : (value) => void
* options : investissementOptions — { id, plateforme_nom, nom_projet, montant_investi }[]
*/
export default function InvestissementSelect({
value, onChange, options = [],
// Libellé du sentinel INTERETS_PLATEFORME (17/09/26, cf. RemboursementsSimple.jsx —
// une distribution PE n'est pas un "intérêt" au sens crowdlending du terme ; la valeur
// technique INTERETS_PLATEFORME/interets_plateforme reste inchangée, seul l'intitulé
// affiché change). Par défaut, le libellé crowdlending d'origine.
interetsPlateformeLabel = '🏦 Intérêts plateforme',
}) {
const [open, setOpen] = useState(false);
const [dropPos, setDropPos] = useState({ left: 0, width: 0, maxHeight: 260, direction: 'down', top: 0, bottom: 0 });
const wrapRef = useRef(null);
const triggerRef = useRef(null);
const selectedOption = options.find(i => String(i.id) === String(value));
const label = value === 'BONUS_PARRAINAGE' ? '🎁 Bonus Parrainage'
: value === 'BONUS_PLATEFORME' ? '🏆 Bonus Plateforme'
: value === 'INTERETS_PLATEFORME' ? interetsPlateformeLabel
: selectedOption ? `${selectedOption.plateforme_nom} — ${selectedOption.nom_projet} (${fmtEUR(selectedOption.montant_investi)})`
: null;
useLayoutEffect(() => {
if (!open || !triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
const margin = 8;
const spaceBelow = window.innerHeight - rect.bottom - margin;
const spaceAbove = rect.top - margin;
const desired = 260; // ~ moitié de la hauteur qu'affichait le <select> natif sans contrainte
if (spaceBelow >= 140 || spaceBelow >= spaceAbove) {
setDropPos({
direction: 'down', left: rect.left, width: rect.width,
top: rect.bottom + 4, bottom: 0,
maxHeight: Math.max(120, Math.min(desired, spaceBelow)),
});
} else {
setDropPos({
direction: 'up', left: rect.left, width: rect.width,
top: 0, bottom: window.innerHeight - rect.top + 4,
maxHeight: Math.max(120, Math.min(desired, spaceAbove)),
});
}
}, [open]);
useEffect(() => {
if (!open) return;
const close = (e) => {
if (wrapRef.current?.contains(e.target)) return;
const drop = document.getElementById('investissement-select-dropdown-portal');
if (drop?.contains(e.target)) return;
setOpen(false);
};
const closeOnScroll = (e) => {
const drop = document.getElementById('investissement-select-dropdown-portal');
if (drop?.contains(e.target)) return; // scroll dans le dropdown — on garde ouvert
setOpen(false);
};
document.addEventListener('mousedown', close);
window.addEventListener('scroll', closeOnScroll, true);
window.addEventListener('resize', closeOnScroll);
return () => {
document.removeEventListener('mousedown', close);
window.removeEventListener('scroll', closeOnScroll, true);
window.removeEventListener('resize', closeOnScroll);
};
}, [open]);
const select = (v) => {
onChange(v);
setOpen(false);
};
const rowStyle = (active) => ({
background: active ? 'var(--surface-2)' : undefined,
fontWeight: active ? 600 : 400,
color: active ? 'var(--primary)' : undefined,
});
return (
<div ref={wrapRef} className="cat-select-wrap">
<button
ref={triggerRef}
type="button"
className={`cat-select-trigger${open ? ' open' : ''}`}
onClick={() => setOpen(o => !o)}
aria-haspopup="listbox"
aria-expanded={open}
>
<span className="cat-select-label" style={!label ? { color: 'var(--text-muted)' } : undefined}>
{label ?? '—'}
</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
style={{ flexShrink: 0, transition: 'transform .15s', transform: open ? 'rotate(0deg)' : 'rotate(180deg)' }}
aria-hidden="true">
<path d="M18 15l-6-6-6 6"/>
</svg>
</button>
{open && (
<div
id="investissement-select-dropdown-portal"
className="cat-select-dropdown"
role="listbox"
style={{
position: 'fixed', left: dropPos.left, width: dropPos.width,
maxHeight: dropPos.maxHeight,
...(dropPos.direction === 'down' ? { top: dropPos.top } : { bottom: dropPos.bottom }),
}}
>
<button type="button" style={rowStyle(value === '')} onClick={() => select('')}>—</button>
<button type="button" style={rowStyle(value === 'BONUS_PARRAINAGE')} onClick={() => select('BONUS_PARRAINAGE')}>🎁 Bonus Parrainage</button>
<button type="button" style={rowStyle(value === 'BONUS_PLATEFORME')} onClick={() => select('BONUS_PLATEFORME')}>🏆 Bonus Plateforme</button>
<button type="button" style={rowStyle(value === 'INTERETS_PLATEFORME')} onClick={() => select('INTERETS_PLATEFORME')}>{interetsPlateformeLabel}</button>
<div className="cat-select-sep" />
{options.map(i => (
<button key={i.id} type="button" style={rowStyle(String(value) === String(i.id))} onClick={() => select(String(i.id))}>
{i.plateforme_nom} — {i.nom_projet} ({fmtEUR(i.montant_investi)})
</button>
))}
</div>
)}
</div>
);
}