// ECO-PANNEAU.FR - _react/ui/_ui_modals.jsx // 1. - GESTIONNAIRE GLOBAL DE Z-INDEX window.pano_getNextModalZIndex = () => { if (typeof window.pano_modalZIndexCounter === 'undefined') { window.pano_modalZIndexCounter = 99000; } window.pano_modalZIndexCounter += 10; return window.pano_modalZIndexCounter; }; // 2. - WRAPPER STRIPE ELEMENTS (PAIEMENT) const StripeCheckoutForm = ({ onSuccess }) => { const { PaymentElement, useStripe, useElements } = window.ReactStripe; const stripe = useStripe(); const elements = useElements(); const [error, ReactSetError] = React.useState(null); const [processing, setProcessing] = React.useState(false); const [isValidating, setIsValidating] = React.useState(false); // SÉCURITÉ ANTI-FUITE DE MÉMOIRE (Zéro-Dette) const isMounted = React.useRef(true); React.useEffect(() => { return () => { isMounted.current = false; }; }, []); const { Button } = window.pano_getComponents(); const handleSubmit = async (event) => { event.preventDefault(); if (!stripe || !elements) return; setProcessing(true); const result = await stripe.confirmPayment({ elements, confirmParams: { return_url: window.location.origin + window.location.pathname + '?tab=panneaux&payment_success=true' }, redirect: 'if_required' }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit (Le client a fermé la modale pendant le traitement Stripe) if (result.error) { ReactSetError(result.error.message); setProcessing(false); setIsValidating(false); } else if (result.paymentIntent && result.paymentIntent.status === 'succeeded') { setIsValidating(true); onSuccess(result.paymentIntent.id); } }; return (
{error &&
{error}
} ); }; window.pano_StripeElements = ({ clientSecret, onSuccess }) => { const stripePromise = React.useMemo(() => window.Stripe ? window.Stripe(window.pano_CONFIG.stripePubKey) : null, []); if (!window.ReactStripe || !stripePromise) { if (window.pano_logFallback) window.pano_logFallback("Échec du chargement du module de paiement Stripe (Erreur réseau ou bloqueur)."); return
Erreur : Le module de paiement sécurisé est indisponible.
; } const { Elements } = window.ReactStripe; return ( ); }; // 3. - MODALES GÉNÉRIQUES (SMART MODAL) window.pano_ModalOverlay = ({ children, onClose, preventClose = false, className = "flex items-center justify-center p-4", positionClass = "fixed" }) => { const [zIdx] = React.useState(() => window.pano_getNextModalZIndex()); const overlayContent = (
{ if (e.target === e.currentTarget && !preventClose && onClose) { onClose(e); } }} > {children}
); return (positionClass === 'absolute' || !window.ReactDOM) ? overlayContent : window.ReactDOM.createPortal(overlayContent, document.body); }; window.pano_Modal = ({ title, subTitle, type = 'default', onClose, children, preventClose = false, requireConfirm = false, maxWidth = "max-w-lg", positionClass = "fixed", tabs = [], activeTab, onTabChange, actions, onSave, formId, isDirty = false, isSaving = false, saveText = "Enregistrer", savedText = "À jour", cancelText = "Annuler" }) => { const [zIdx] = React.useState(() => window.pano_getNextModalZIndex()); const { XIcon, SaveIcon, LoaderIcon, CheckCircleIcon } = window.pano_getIcons(); const { Button } = window.pano_getComponents(); const { alertParam, setAlert, closeCurrentLayer } = window.pano_useUrlModal ? window.pano_useUrlModal() : {}; const scrollRef = React.useRef(null); const effectiveRequireConfirm = requireConfirm || isDirty; const effectivePreventClose = preventClose || isSaving; React.useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = 0; } }, [activeTab]); const handleCloseAttempt = React.useCallback((e) => { if (effectiveRequireConfirm) { if (setAlert) setAlert('unsaved', false); } else { if (onClose) onClose(e); } }, [effectiveRequireConfirm, onClose, setAlert]); const preventRef = React.useRef(effectivePreventClose); const closeRef = React.useRef(handleCloseAttempt); React.useEffect(() => { preventRef.current = effectivePreventClose; closeRef.current = handleCloseAttempt; }, [effectivePreventClose, handleCloseAttempt]); React.useEffect(() => { const handleEsc = (e) => { if (e.key === 'Escape' && !preventRef.current) closeRef.current(e); }; window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); }, []); const showConfirm = effectiveRequireConfirm && alertParam === 'unsaved'; let headerBgClass = 'bg-slate-50 border-b border-slate-200'; let headerTextClass = 'text-slate-800'; let closeIconClass = 'text-slate-400 hover:text-slate-600 hover:bg-slate-200'; if (type === 'success') { headerBgClass = 'bg-emerald-50 border-b border-emerald-100'; headerTextClass = 'text-emerald-900'; closeIconClass = 'text-emerald-500 hover:text-emerald-700 hover:bg-emerald-100'; } else if (type === 'info') { headerBgClass = 'bg-blue-50 border-b border-blue-100'; headerTextClass = 'text-blue-900'; closeIconClass = 'text-blue-500 hover:text-blue-700 hover:bg-blue-100'; } else if (type === 'error') { headerBgClass = 'bg-red-50 border-b border-red-100'; headerTextClass = 'text-red-900'; closeIconClass = 'text-red-500 hover:text-red-700 hover:bg-red-100'; } else if (type === 'warning') { headerBgClass = 'bg-orange-50 border-b border-orange-100'; headerTextClass = 'text-orange-900'; closeIconClass = 'text-orange-500 hover:text-orange-700 hover:bg-orange-100'; } else if (type === 'confirm' || type === 'black') { headerBgClass = 'bg-slate-900 border-b border-slate-900'; headerTextClass = 'text-white'; closeIconClass = 'text-slate-400 hover:text-white hover:bg-slate-800'; } if (showConfirm) { const confirmContent = (

Modifications non enregistrées

Vous avez des modifications en cours. Êtes-vous sûr de vouloir fermer sans sauvegarder ?

); return (positionClass === 'absolute' || !window.ReactDOM) ? confirmContent : window.ReactDOM.createPortal(confirmContent, document.body); } const modalContent = (
{ if (e.target === e.currentTarget && !preventRef.current) closeRef.current(e); }} >
e.stopPropagation()}>

{title}

{subTitle &&

{subTitle}

}
{!effectivePreventClose && (
{tabs && tabs.length > 0 && (
{tabs.map(t => (
onTabChange && onTabChange(t.id)} className={`px-4 py-3 text-sm font-bold whitespace-nowrap border-b-2 transition cursor-pointer select-none ${activeTab === t.id ? 'border-emerald-600 text-emerald-700' : 'border-transparent text-slate-500 hover:text-slate-800'}`} > {t.label}
))}
)}
{children}
{(actions || onSave || formId) && (
{actions ? ( typeof actions === 'function' ? actions(handleCloseAttempt) : actions ) : ( <> )}
)}
); return (positionClass === 'absolute' || !window.ReactDOM) ? modalContent : window.ReactDOM.createPortal(modalContent, document.body); }; window.pano_ConfirmModal = ({ title, message, confirmText = "Confirmer", cancelText = "Annuler", isDestructive = false, type = 'confirm', onConfirm, onCancel, positionClass = "fixed" }) => { const { LoaderIcon } = window.pano_getIcons(); const { Button, Modal } = window.pano_getComponents(); const [isProcessing, ReactSetIsProcessing] = React.useState(false); const modalType = isDestructive ? 'error' : type; // SÉCURITÉ ANTI-GEL INFNI const isMounted = React.useRef(true); React.useEffect(() => { return () => { isMounted.current = false; }; }, []); return ( ( <> )} >

{message}

); }; window.pano_PromptModal = ({ title, message, placeholder = "", confirmText = "Valider", cancelText = "Annuler", type = 'confirm', onConfirm, onCancel, positionClass = "fixed" }) => { const { LoaderIcon } = window.pano_getIcons(); const { Button, Modal } = window.pano_getComponents(); const [val, setVal] = React.useState(''); const [isProcessing, setIsProcessing] = React.useState(false); // SÉCURITÉ ANTI-GEL INFNI const isMounted = React.useRef(true); React.useEffect(() => { return () => { isMounted.current = false; }; }, []); return ( ( <> )} >
{ e.preventDefault(); setIsProcessing(true); try { await onConfirm(val, e); } finally { if (isMounted.current) setIsProcessing(false); } }} className="space-y-6">
* Champs obligatoires

{message}

setVal(e.target.value)} placeholder={placeholder} className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition font-bold" autoFocus required />
); }; window.pano_PasswordPromptModal = ({ title = "Confirmation de sécurité", desc, onConfirm, onCancel, isSaving, positionClass = "fixed" }) => { const { LoaderIcon } = window.pano_getIcons(); const { Button, Modal, FormInput } = window.pano_getComponents(); const [pwd, setPwd] = React.useState(''); // CORRECTION UX/SÉCURITÉ : Surlignement en rouge de la clé AES si elle est demandée const formatDesc = (text) => { if (!text) return null; if (text.includes('AES_KEY_CONFIRM')) { const parts = text.split('AES_KEY_CONFIRM'); return ( <> {parts[0]} AES_KEY_CONFIRM {parts[1]} ); } return text; }; return ( ( )} >
{formatDesc(desc)}
{ e.preventDefault(); if(pwd) onConfirm(pwd, e); }}> setPwd(e.target.value)} />
); }; window.pano_UploadErrorsModal = ({ errors, onClose }) => { const { Button, Modal } = window.pano_getComponents(); if (!errors || errors.length === 0) return null; return ( ( )} >

Certains fichiers ont été ignorés lors de la sélection ou du transfert :

{errors.map((e, i) => (
{e.file} {e.reason}
))}
); }; // 4. - LECTEURS ET VISIONNEUSES DE DOCUMENTS window.pano_VaultDocumentThumbnail = ({ doc }) => { const { FileIcon, LoaderIcon } = window.pano_getIcons(); const [status, setStatus] = React.useState('loading'); const [retryCount, setRetryCount] = React.useState(0); // SÉCURITÉ ANTI-FUITE DE MÉMOIRE const isMounted = React.useRef(true); React.useEffect(() => { return () => { isMounted.current = false; }; }, []); const handleError = () => { if (retryCount < 5) { setTimeout(() => { if (isMounted.current) { setRetryCount(prev => prev + 1); setStatus('loading'); } }, 1000 + (retryCount * 500)); } else { if (isMounted.current) setStatus('error'); } }; const typeLabel = doc.type === 'image' ? 'IMG' : 'PDF'; const srcUrl = doc.type === 'image' ? `${window.pano_CONFIG.apiBaseUrl}file/download&type=image&id=${doc.id}&preview=1&private=1&retry=${retryCount}` : `${window.pano_CONFIG.apiBaseUrl}file/download&type=pdf&id=${doc.id}&preview=1&page=0&private=1&retry=${retryCount}`; return (
{(status === 'loading' || status === 'error') && (
{FileIcon && } {typeLabel} {status === 'loading' && LoaderIcon && }
)} {doc.name { if(isMounted.current) setStatus('loaded'); }} onError={handleError} />
); }; const VaultPageImage = ({ src, alt }) => { const { FileIcon } = window.pano_getIcons(); const [error, setError] = React.useState(false); if (error) { return (
e.stopPropagation()}> {FileIcon && }

Aperçu web indisponible

Le document a été sauvegardé brut. Veuillez télécharger le fichier original pour le visualiser.

); } return ( {alt} e.stopPropagation()} onError={() => setError(true)} /> ); }; window.pano_UniversalViewer = ({ fileId, fileUrl, fileType, fileName, numPages = 1, isPrivate = false, useSafePdfMode = false, positionClass = "fixed", onClose }) => { const [zIdx] = React.useState(() => window.pano_getNextModalZIndex()); const { ArrowLeftIcon, DownloadIcon } = window.pano_getIcons(); const { Button, PdfFullViewer } = window.pano_getComponents(); const privateParam = isPrivate ? '&private=1' : ''; const downloadUrl = fileUrl || `${window.pano_CONFIG.apiBaseUrl}file/download&type=${fileType}&id=${fileId}${privateParam}`; const imageUrl = fileUrl || `${window.pano_CONFIG.apiBaseUrl}file/download&type=image&id=${fileId}${privateParam}`; const handleBackgroundClick = (e) => { if (e.target === e.currentTarget) onClose(e); }; const safeNumPages = Math.min(numPages, 20); const pages = Array.from({ length: safeNumPages }, (_, i) => i + 1); const viewerContent = (
e.stopPropagation()}>
{fileType === 'image' && ( {fileName e.stopPropagation()} /> )} {fileType === 'pdf' && !useSafePdfMode && PdfFullViewer && ( )} {fileType === 'pdf' && !useSafePdfMode && !PdfFullViewer && (
Le lecteur PDF est introuvable.
)} {fileType === 'pdf' && useSafePdfMode && ( <> {pages.map(p => ( ))} {numPages > 20 && (
e.stopPropagation()}> L'aperçu en ligne est limité aux 20 premières pages pour des raisons de performance.
Veuillez télécharger le document original pour le consulter en intégralité.
)} )}
); return (positionClass === 'absolute' || !window.ReactDOM) ? viewerContent : window.ReactDOM.createPortal(viewerContent, document.body); }; /* EOF ========== [_react/ui/_ui_modals.jsx] */