/**
* =========================================================================
* PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0
* Socle UI (4/4) - Modales, Coffre-fort et Paiement (Stripe)
* =========================================================================
*/
// =========================================================================
// 1. WRAPPER STRIPE ELEMENTS
// =========================================================================
window.StripeElements = ({ clientSecret, onSuccess }) => {
const stripePromise = React.useMemo(() => window.Stripe ? window.Stripe(window.ECO_CONFIG.stripePubKey) : null, []);
if (!window.ReactStripe || !stripePromise) {
return
Erreur : Le module de paiement sécurisé est indisponible.
;
}
const { Elements, PaymentElement, useStripe, useElements } = window.ReactStripe;
const CheckoutForm = () => {
const stripe = useStripe();
const elements = useElements();
const [error, ReactSetError] = React.useState(null);
const [processing, setProcessing] = React.useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) return;
setProcessing(true);
const result = await stripe.confirmPayment({
elements,
redirect: 'if_required'
});
if (result.error) {
ReactSetError(result.error.message);
setProcessing(false);
} else if (result.paymentIntent && result.paymentIntent.status === 'succeeded') {
onSuccess(result.paymentIntent.id);
}
};
return (
);
};
return (
);
};
// =========================================================================
// 2. MODALES GÉNÉRIQUES (SPA URL LAYER 1 ET LAYER 2)
// =========================================================================
window.ModalOverlay = ({ children, zIndex = "z-[100]", onClose, preventClose = false, className = "flex items-center justify-center p-4" }) => {
return (
{
if (e.target === e.currentTarget && !preventClose && onClose) {
onClose();
}
}}
>
{children}
);
};
window.Modal = ({ title, type = 'info', onClose, children, preventClose = false, requireConfirm = false, zIndex = "z-[100]" }) => {
// FACTORISATION : Utilisation des utilitaires globaux
const { XIcon } = window.getIcons();
const { alertParam, setAlert } = window.useUrlModal();
// Le parent envoie le close, mais s'il y a un requireConfirm, on doit l'intercepter via l'URL (Layer 2)
const handleCloseAttempt = React.useCallback(() => {
if (requireConfirm) {
setAlert('unsaved');
} else {
if (onClose) onClose();
}
}, [requireConfirm, onClose, setAlert]);
const preventRef = React.useRef(preventClose);
const closeRef = React.useRef(handleCloseAttempt);
React.useEffect(() => {
preventRef.current = preventClose;
closeRef.current = handleCloseAttempt;
}, [preventClose, handleCloseAttempt]);
// Fermeture propre via la touche Échap (Accessibilité)
React.useEffect(() => {
const handleEsc = (e) => { if (e.key === 'Escape' && !preventRef.current) closeRef.current(); };
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, []);
// Lecture de l'URL via le Hook pour afficher l'alerte d'annulation (Couche 2 / Dialog)
const showConfirm = alertParam === 'unsaved';
let headerBgClass = 'bg-slate-50/50';
let headerTextClass = 'text-slate-800';
let closeIconClass = 'text-slate-400 hover:text-blue-500 hover:bg-blue-50';
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';
} else {
headerBgClass = 'bg-slate-50/50 border-b border-slate-100';
}
return (
<>
e.stopPropagation()}>
{title}
{!preventClose && (
)}
{children}
{showConfirm && window.ConfirmModal && (
{
window.history.back(); // Enlève le paramètre d'alerte de l'URL
setTimeout(() => { if (onClose) onClose(); }, 50); // Laisse le temps au navigateur de nettoyer l'URL avant de fermer la modale principale
}}
onCancel={() => window.history.back()}
zIndex="z-[9999]"
/>
)}
>
);
};
window.ConfirmModal = ({ title, message, confirmText = "Confirmer", cancelText = "Annuler", isDestructive = false, type = 'confirm', onConfirm, onCancel, zIndex = "z-[200]" }) => {
const { LoaderIcon } = window.getIcons();
const [isProcessing, ReactSetIsProcessing] = React.useState(false);
const modalType = isDestructive ? 'error' : type;
return (
{message}
{cancelText}
{ ReactSetIsProcessing(true); await onConfirm(); }}
disabled={isProcessing}
icon={isProcessing ? LoaderIcon : null}
className="flex-1 py-3 uppercase tracking-widest justify-center"
>
{confirmText}
);
};
window.PromptModal = ({ title, message, placeholder = "", confirmText = "Valider", cancelText = "Annuler", type = 'confirm', onConfirm, onCancel, zIndex = "z-[200]" }) => {
const { LoaderIcon } = window.getIcons();
const [val, setVal] = React.useState('');
const [isProcessing, setIsProcessing] = React.useState(false);
return (
);
};
window.PasswordPromptModal = ({ title = "Confirmation de sécurité", desc, onConfirm, onCancel, isSaving }) => {
const { LoaderIcon } = window.getIcons();
const [pwd, setPwd] = React.useState('');
return (
{desc}
);
};
// =========================================================================
// 3. COMPOSANTS GLOBAUX DU COFFRE-FORT
// =========================================================================
window.VaultDocumentThumbnail = ({ doc }) => {
const { FileIcon, LoaderIcon } = window.getIcons();
const [status, setStatus] = React.useState('loading');
const [retryCount, setRetryCount] = React.useState(0);
const handleError = () => {
if (retryCount < 5) {
setTimeout(() => {
setRetryCount(prev => prev + 1);
setStatus('loading');
}, 1000 + (retryCount * 500));
} else {
setStatus('error');
}
};
const typeLabel = doc.type === 'image' ? 'IMG' : 'PDF';
const srcUrl = doc.type === 'image'
? `?api=file/download&type=image&id=${doc.id}&private=1&retry=${retryCount}`
: `?api=file/download&type=pdf&id=${doc.id}&preview=1&page=0&private=1&retry=${retryCount}`;
return (
{(status === 'loading' || status === 'error') && (
{typeLabel}
{status === 'loading' && }
)}

setStatus('loaded')}
onError={handleError}
/>
);
};
window.VaultDocumentViewer = ({ viewingDoc, setViewingDoc }) => {
const { ArrowLeftIcon, DownloadIcon, FileIcon } = window.getIcons();
if (!viewingDoc) return null;
const numPages = viewingDoc.numPages || 1;
const pages = Array.from({ length: numPages }, (_, i) => i + 1);
const VaultPageImage = ({ src, alt }) => {
const [error, setError] = React.useState(false);
if (error) {
return (
e.stopPropagation()}>
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)}
/>
);
};
return (
setViewingDoc(null)}>
e.stopPropagation()}>
setViewingDoc(null)} className="flex items-center gap-2 px-2 sm:px-4 py-2 hover:bg-white/10 rounded-xl transition text-sm font-bold cursor-pointer shrink-0">
Retour
{viewingDoc.name}
Original
setViewingDoc(null)}>
{viewingDoc.type === 'image' ? (
) : (
<>
{pages.map(p => (
))}
{numPages === 50 && (
e.stopPropagation()}>
L'aperçu en ligne est limité aux 50 premières pages.
Veuillez télécharger le document original pour le consulter en intégralité.
)}
>
)}
);
};
window.UploadErrorsModal = ({ errors, onClose, zIndex = "z-[350]" }) => {
const Modal = window.Modal || (() => null);
const Button = window.Button || (() => null);
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}
))}
);
};
/* EOF ========== [_react/_ui_modals.jsx] */