/** * ========================================================================= * PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0 * Composants : Modales complexes B2B (Stripe, Coffre-fort, Équipe, Partage) * ========================================================================= */ const { useState, useMemo } = React; // ========================================================================= // 1. COMPOSANT STRIPE : FORMULAIRE DE PAIEMENT // ========================================================================= const StripePaymentForm = ({ clientSecret, onSuccess, onCancel, amountCents }) => { const { AlertTriangle, Loader, CreditCard } = window; const stripePromise = useMemo(() => window.Stripe(window.ECO_CONFIG.stripePubKey), []); const options = useMemo(() => ({ clientSecret, appearance: { theme: 'stripe', variables: { colorPrimary: '#10b981', borderRadius: '12px' } } }), [clientSecret]); const CheckoutForm = () => { const stripeInstance = window.ReactStripe.useStripe(); const elements = window.ReactStripe.useElements(); const [isProcessing, setIsProcessing] = useState(false); const [error, setError] = useState(null); const handleSubmit = async (e) => { e.preventDefault(); if (!stripeInstance || !elements) return; setIsProcessing(true); setError(null); const { error: submitError } = await elements.submit(); if (submitError) { setError(submitError.message); setIsProcessing(false); return; } const { error: confirmError, paymentIntent } = await stripeInstance.confirmPayment({ elements, confirmParams: { return_url: window.location.href }, redirect: "if_required" }); if (confirmError) { setError(confirmError.message); setIsProcessing(false); } else if (paymentIntent && paymentIntent.status === "succeeded") { onSuccess(paymentIntent.id); } else { setError("Statut de paiement inattendu."); setIsProcessing(false); } }; return (

Montant à régler : {amountCents ? (amountCents / 100).toFixed(2) : "0.00"} € TTC

{error &&
{error}
}
); }; return ( ); }; // ========================================================================= // 2. MODALE DE CHECKOUT (CHOIX D'OFFRE + PAIEMENT) // ========================================================================= window.ClientCheckoutModal = ({ managingPanneau, setManagingPanneau, data, myClientData, profileData, onClose, onBackToEditor, refreshData, showToast }) => { const { X, AlertTriangle, Package, Loader, CreditCard } = window; const [step, setStep] = useState('select'); // 'select' ou 'payment' const [paymentData, setPaymentData] = useState(null); const [waiverAccepted, setWaiverAccepted] = useState(false); const [isSaving, setIsSaving] = useState(false); const isSuspended = myClientData.paymentStatus === 'suspended'; const purchasesAllowed = data.settings?.allow_new_purchases !== '0'; const pRental = data.prices?.rentalMo ?? 180; const pPurchase = data.prices?.purchase ?? 850; const pHosting = data.prices?.hostingYr ?? 1; const pBoardFirst = data.prices?.boardFirst ?? 250; const pBoardAdd = data.prices?.boardAdd ?? 150; const pNoAds = data.prices?.noAds ?? 150; const originalPanneau = data.panneaux?.find(p => p.id === managingPanneau?.id); const isActif = originalPanneau?.status === 'Actif'; const originalOfferType = originalPanneau?.offerType || 'rental'; const originalPanels = originalPanneau?.physicalPanels || 0; const originalHasNoAds = originalPanneau?.hasNoAds || false; const currentOfferType = managingPanneau?.offerType || 'rental'; const currentPanels = managingPanneau?.physicalPanels || 0; const currentHasNoAds = managingPanneau?.hasNoAds || false; let addedPanelsCost = 0; if (currentPanels > originalPanels) { const costNew = currentPanels > 0 ? pBoardFirst + (currentPanels - 1) * pBoardAdd : 0; const costOld = originalPanels > 0 ? pBoardFirst + (originalPanels - 1) * pBoardAdd : 0; addedPanelsCost = Math.max(0, costNew - costOld); } let upfrontHT = 0; if (!isActif) { upfrontHT = (currentPanels > 0 ? pBoardFirst + (currentPanels - 1) * pBoardAdd : 0) + (currentHasNoAds ? pNoAds : 0) + (currentOfferType === 'rental' ? pRental : pPurchase + pHosting); } else { if (originalOfferType === 'rental' && currentOfferType === 'purchase') upfrontHT += pPurchase + pHosting; if (currentHasNoAds && !originalHasNoAds) upfrontHT += pNoAds; upfrontHT += addedPanelsCost; } const discountPct = myClientData.discount || 0; const priceMultiplier = Math.max(0, (100 - discountPct) / 100); const platformHasTva = data.settings?.billing_has_tva !== '0'; const tvaMult = platformHasTva ? 1.20 : 1.00; const finalUpfrontTTC = upfrontHT * priceMultiplier * tvaMult; const finalMonthlyTTC = pRental * priceMultiplier * tvaMult; const remainingToPay = Math.max(0, finalUpfrontTTC - (myClientData.wallet_balance || 0)); const handleActivationRequest = async () => { if(isSuspended) return showToast("Votre compte est suspendu. Veuillez régulariser vos factures.", "error"); if(remainingToPay > 0 && (!waiverAccepted || !purchasesAllowed)) { if (!purchasesAllowed) return showToast("Les nouvelles commandes sont suspendues.", "error"); return showToast("Veuillez accepter les conditions de rétractation.", "error"); } setIsSaving(true); try { const payload = { panneau_id: managingPanneau.id, offer_type: managingPanneau.offerType, physical_panels: managingPanneau.physicalPanels, hasNoAds: managingPanneau.hasNoAds }; const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'stripe/intent', { method: 'POST', body: JSON.stringify(payload) }); const d = await res.json(); if(d.status === 'success') { if(d.data.free_activation_required) { const freeRes = await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux/activate_free', { method: 'POST', body: JSON.stringify({ ...payload, id: managingPanneau.id, offerType: managingPanneau.offerType, physicalPanels: managingPanneau.physicalPanels, details: managingPanneau, name: managingPanneau.name }) }); if((await freeRes.json()).status === 'success') { showToast("Mise à jour et activation réussie !"); onClose(); refreshData(); } else showToast("Erreur d'activation", "error"); } else { setPaymentData({ secret: d.data.clientSecret, subId: d.data.subscription_id, amountCents: d.data.amountCents }); setStep('payment'); } } else showToast(d.message, 'error'); } catch(e) { showToast("Erreur de communication", "error"); } setIsSaving(false); }; const handlePaymentSuccess = async (pi_id) => { setIsSaving(true); try { const payload = { payment_intent_id: pi_id, subscription_id: paymentData?.subId, id: managingPanneau.id, offerType: managingPanneau.offerType, currentRate: managingPanneau.currentRate, physicalPanels: managingPanneau.physicalPanels, details: managingPanneau, name: managingPanneau.name }; const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux/activate_paid', { method: 'POST', body: JSON.stringify(payload) }); const d = await res.json(); if(d.status === 'success') { showToast("Paiement validé !"); onClose(); refreshData(); } else showToast(d.message, 'error'); } catch(e) { showToast("Erreur", "error"); } setIsSaving(false); }; if (step === 'payment' && paymentData) { return ( ); } return (

Choix de l'offre

Sélectionnez le type d'abonnement et les options matérielles.

setManagingPanneau({...managingPanneau, offerType: 'rental'})} className={`p-6 rounded-2xl border-2 cursor-pointer transition flex flex-col justify-between ${currentOfferType === 'rental' ? 'border-emerald-500 bg-emerald-50 shadow-md' : 'border-slate-200 hover:border-emerald-200'} ${(isActif && originalOfferType === 'purchase') ? 'opacity-50 pointer-events-none' : ''}`} >

Abonnement mensuel

{pRental} € HT / mois

Paiement mensuel automatisé. Résiliable à tout moment, en un clic, à la fin de vos travaux. Engagement mensuel, tout mois entamé est dû.

setManagingPanneau({...managingPanneau, offerType: 'purchase'})} className={`p-6 rounded-2xl border-2 cursor-pointer transition flex flex-col justify-between ${currentOfferType === 'purchase' ? 'border-emerald-500 bg-emerald-50 shadow-md' : 'border-slate-200 hover:border-emerald-200'}`} >

Achat définitif

{pPurchase} € HT

Paiement unique sans abonnement. Le panneau numérique reste actif indéfiniment.

Commander des panneaux physiques

Recevez votre panneau pré-imprimé (Format A1) sur un support rigide Akilux résistant aux intempéries, livré directement au cabinet.

Quantité souhaitée

{pBoardFirst}€ HT le premier, {pBoardAdd}€ HT l'unité supplémentaire.

{currentPanels}
{currentPanels > 0 && (

Laissez vide pour utiliser l'adresse de votre société.

)}

Résumé de votre commande

{!isActif && ( <>
{currentOfferType === 'rental' ? 'Abonnement mensuel (1er mois inclus)' : 'Achat définitif'}{(currentOfferType === 'rental' ? pRental : pPurchase).toFixed(2)} € HT
{currentOfferType === 'purchase' &&
Hébergement annuel (1ère année offerte)0.00 € HT {pHosting.toFixed(2)} €
} {currentPanels > 0 &&
Panneaux physiques ({currentPanels}x){(currentPanels > 0 ? pBoardFirst + (currentPanels - 1) * pBoardAdd : 0).toFixed(2)} € HT
} {currentHasNoAds &&
Option marque blanche (Sans mentions de eco-panneau.fr){pNoAds.toFixed(2)} € HT
} )} {isActif && ( <> {originalOfferType === 'rental' && currentOfferType === 'purchase' &&
Passage à l'achat définitif{pPurchase.toFixed(2)} € HT
} {currentPanels > originalPanels &&
Panneaux physiques supp. ({currentPanels - originalPanels}x){addedPanelsCost.toFixed(2)} € HT
} {currentHasNoAds && !originalHasNoAds &&
Option marque blanche (Sans mentions de eco-panneau.fr){pNoAds.toFixed(2)} € HT
} )} {discountPct > 0 && upfrontHT > 0 && (
Remise client ({discountPct}%) -{(upfrontHT * (discountPct / 100)).toFixed(2)} € HT
)} {myClientData.wallet_balance > 0 && upfrontHT > 0 && (
Payé avec votre solde (Avoir) -{Math.min(finalUpfrontTTC, myClientData.wallet_balance).toFixed(2)} € TTC
)}

Reste à payer aujourd'hui

{platformHasTva ? 'TVA 20% incluse' : 'TVA non applicable'}

{Math.max(0, finalUpfrontTTC - (myClientData.wallet_balance || 0)).toFixed(2)} €

{currentOfferType === 'rental' &&

Puis {finalMonthlyTTC.toFixed(2)} € TTC / mois

} {currentOfferType === 'purchase' &&

Puis { (pHosting * priceMultiplier * tvaMult).toFixed(2) } € TTC / an

}
{(!purchasesAllowed && remainingToPay > 0) && (

L'administration a temporairement suspendu la validation de nouvelles commandes payantes sur la plateforme. Veuillez réessayer ultérieurement.

)} {remainingToPay > 0 && purchasesAllowed && ( )}
); }; // ========================================================================= // 3. MODALE DU COFFRE-FORT LÉGAL // ========================================================================= window.ClientLegalVaultModal = ({ managingPanneau, setManagingPanneau, onClose, refreshData, showToast }) => { const { Lock, ArrowUp, Trash2, Loader, ArrowLeft, Download } = window; const [isSaving, setIsSaving] = useState(false); const [vaultDragging, setVaultDragging] = useState(false); const [viewingDoc, setViewingDoc] = useState(null); const handleVaultUpload = async (files) => { if (!files || files.length === 0) return; let currentTotalSize = managingPanneau.privateDocs?.reduce((acc, doc) => acc + (doc.size || 0), 0) || 0; setIsSaving(true); let newDocs = [...(managingPanneau.privateDocs || [])]; for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.size > 100 * 1024 * 1024) { showToast(`Le fichier ${file.name} dépasse 100 Mo.`, "error"); continue; } if (currentTotalSize + file.size > 500 * 1024 * 1024) { showToast("La limite totale de 500 Mo du coffre-fort serait dépassée.", "error"); break; } try { const type = file.type.includes('pdf') ? 'pdf' : 'image'; const fd = new FormData(); fd.append('file', file); fd.append('type', type); fd.append('is_private', 'true'); const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'upload/file', { method: 'POST', body: fd }); const d = await res.json(); if (d.status === 'success') { newDocs.push({ id: d.data.fileId, type, name: file.name, date: new Date().toLocaleDateString('fr-FR'), size: d.data.size || file.size }); currentTotalSize += (d.data.size || file.size); } else showToast(d.message, "error"); } catch(err) { showToast("Erreur upload", "error"); } } const updatedPanneau = {...managingPanneau, privateDocs: newDocs}; setManagingPanneau(updatedPanneau); await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux', { method: 'POST', body: JSON.stringify({ id: updatedPanneau.id, status: updatedPanneau.status, offerType: updatedPanneau.offerType, physicalPanels: updatedPanneau.physicalPanels, details: updatedPanneau }) }); refreshData(); setIsSaving(false); }; return ( <>

Preuves et constats d'huissier

Ces documents sont chiffrés et inaccessibles au public. Ils seront inclus dans l'archive D.O.E lors de la clôture du panneau.

{ e.preventDefault(); setVaultDragging(true); }} onDragLeave={(e) => { e.preventDefault(); setVaultDragging(false); }} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); setVaultDragging(false); handleVaultUpload(e.dataTransfer.files); }} className={`w-full min-h-[150px] border-2 border-dashed rounded-xl flex flex-col items-center justify-center p-6 transition relative ${vaultDragging ? 'border-emerald-500 bg-emerald-50/50' : 'border-slate-300 bg-slate-50 hover:bg-slate-100'}`} title="Vous pouvez également coller vos fichiers (Ctrl+V) directement sur cette fenêtre." > {isSaving ? (
Chiffrement en cours...
) : ( <>
Glissez-déposez vos preuves ici (ou Ctrl+V) Images ou PDF (Max 100 Mo/fichier, 500 Mo au total) )}
{managingPanneau.privateDocs && managingPanneau.privateDocs.length > 0 ? (
{managingPanneau.privateDocs.map((doc, idx) => (
setViewingDoc(doc)}>
{doc.type === 'image' ? ( aperçu ) : ( aperçu pdf { e.target.style.display = 'none'; e.target.parentElement.innerHTML = '
PDF
'; }} /> )}

{doc.name}

{doc.date}
))}
) : (

Aucun document privé stocké.

)}
Total utilisé : {((managingPanneau.privateDocs?.reduce((acc, doc) => acc + (doc.size || 0), 0) || 0) / (1024*1024)).toFixed(2)} Mo / 500 Mo
{/* Visualiseur Plein Écran Coffre-Fort */} {viewingDoc && (
setViewingDoc(null)}>
e.stopPropagation()}>
{viewingDoc.name} Télécharger
{viewingDoc.type === 'image' ? ( Aperçu e.stopPropagation()} /> ) : ( Aperçu PDF e.stopPropagation()} /> )}
)} ); }; // ========================================================================= // 4. MODALE DE GESTION DES LIENS DÉLÉGUÉS // ========================================================================= window.ClientDelegateShareModal = ({ managingPanneau, setManagingPanneau, onClose, refreshData, showToast }) => { const { Plus, Lock, Copy, Trash2 } = window; const [newLink, setNewLink] = useState({ name: '', expDays: 7, lockedFields: [] }); const [isSaving, setIsSaving] = useState(false); return (
Créez des liens sécurisés pour confier la saisie d'informations ou le dépôt de documents (PDF) à d'autres membres de l'équipe du chantier sans leur donner accès à votre compte ou à votre facturation.

Créer un nouveau lien

setNewLink({...newLink, name: e.target.value})} placeholder="Ex: Architecte Dupont" className="w-full border border-slate-200 rounded-lg p-2 text-sm outline-none focus:border-blue-500" />
setNewLink({...newLink, expDays: parseInt(e.target.value)||7})} className="w-full border border-slate-200 rounded-lg p-2 text-sm outline-none focus:border-blue-500" />
{[ { id: 'name', label: 'Nom du chantier' }, { id: 'location', label: 'Lieu' }, { id: 'maitreOuvrage', label: "Maître d'ouvrage" }, { id: 'permitNumber', label: 'N° de permis' }, { id: 'description', label: 'Description' }, { id: 'promoterLink', label: 'Lien promoteur' }, { id: 'emergencyPhone', label: 'N° d\'urgence' }, { id: 'noiseSchedule', label: 'Horaires' }, { id: 'intervenants', label: 'Intervenants' }, { id: 'lots', label: 'Lots' }, { id: 'pdfId', label: 'Arrêté (PDF)' } ].map(f => ( ))}

Liens générés pour ce panneau

{(!managingPanneau.delegate_links || managingPanneau.delegate_links.length === 0) && (

Aucun lien n'a encore été créé pour ce panneau.

)} {managingPanneau.delegate_links?.map((link, idx) => { const isExpired = link.exp < Date.now() / 1000; return (

{link.name}

Exp. : {new Date(link.exp * 1000).toLocaleDateString('fr-FR')} {isExpired && (Expiré)}

{link.lockedFields && link.lockedFields.length > 0 && (

Bloqué : {link.lockedFields.join(', ')}

)}
); })}
); }; /* EOF ===== [_clients_modals.jsx] =============== */