// 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 (
);
};
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 ?
{
if (closeCurrentLayer) closeCurrentLayer();
}} className="flex-1 py-3 justify-center text-xs">Annuler
{
if (closeCurrentLayer) closeCurrentLayer();
setTimeout(() => { if (onClose) onClose(e); }, 50);
}} className="flex-[1.5] py-3 justify-center text-xs shadow-md">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
) : (
<>
{cancelText}
{effectivePreventClose ? "Enregistrement..." : (isDirty ? saveText : savedText)}
>
)}
)}
);
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 (
(
<>
close(e)} disabled={isProcessing} className="flex-1 py-3 justify-center">
{cancelText}
{
ReactSetIsProcessing(true);
try {
await onConfirm(e);
} finally {
if (isMounted.current) ReactSetIsProcessing(false);
}
}}
disabled={isProcessing}
icon={isProcessing ? LoaderIcon : null}
className="flex-[2] py-3 uppercase tracking-widest justify-center shadow-md text-xs"
>
{confirmText}
>
)}
>
);
};
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 (
(
<>
close(e)} disabled={isProcessing} className="flex-1 py-3 justify-center">
{cancelText}
{confirmText}
>
)}
>
);
};
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 (
(
Confirmer l'action
)}
>
{formatDesc(desc)}
);
};
window.pano_UploadErrorsModal = ({ errors, onClose }) => {
const { Button, Modal } = window.pano_getComponents();
if (!errors || errors.length === 0) return null;
return (
(
Fermer
)}
>
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 && }
)}
{ 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 (
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 = (
{fileType === 'image' && (
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] */