137 lines
5.7 KiB
React
137 lines
5.7 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' | id (string/number)
|
|
* onChange : (value) => void
|
|
* options : investissementOptions — { id, plateforme_nom, nom_projet, montant_investi }[]
|
|
*/
|
|
export default function InvestissementSelect({ value, onChange, options = [] }) {
|
|
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'
|
|
: 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>
|
|
<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>
|
|
);
|
|
}
|