// ECO-PANNEAU.FR - _react/clients/_clients_modals_checkout.jsx const { useState, useEffect, useRef } = React; window.pano_ClientCheckoutModal = ({ managingPanneau, setManagingPanneau, data, myClientData, onClose, onBackToEditor, refreshData, showToast, onGoToBilling }) => { // ZÉRO-DETTE : Utilisation de notre Hook abstrait ! const { isMounted, safeFetch } = window.pano_useSafeFetch(); // 1. - Composants et Icônes const { XIcon, AlertTriangleIcon, PackageIcon, LoaderIcon, CreditCardIcon, MinusIcon, PlusIcon, CheckCircleIcon, DownloadIcon, FileTextIcon, ShieldAlertIcon } = window.pano_getIcons(); const { StripeElements, ModalOverlay, AlertBox, Button, CardGrid } = window.pano_getComponents(); // 2. - États locaux const [step, setStep] = useState('select'); const [paymentData, setPaymentData] = useState(null); const [waiverAccepted, setWaiverAccepted] = useState(false); const [isSaving, setIsSaving] = useState(false); // États pour le polling de la facture const [latestInvoiceRef, setLatestInvoiceRef] = useState(null); const [isPollingInvoice, setIsPollingInvoice] = useState(false); const pollCountRef = useRef(0); const pollIntervalRef = useRef(null); // 3. - Variables de tarification et d'état sécurisées const pCible = managingPanneau || {}; const isSuspended = myClientData?.paymentStatus === 'suspended'; // VÉRIFICATIONS DES RÈGLES DE BLOCAGE const rawAllow = data.settings?.allow_new_purchases ?? window.pano_CONFIG?.allow_new_purchases ?? window.pano_CONFIG?.settings?.allow_new_purchases; const purchasesAllowed = rawAllow === '1'; const isAdminView = window.pano_CONFIG?.role === 'admin' || data.role === 'admin'; const pRental = data.prices?.rentalMo ?? 180; const pPurchase = data.prices?.purchase ?? 850; 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 === pCible.id); const isActif = originalPanneau?.status === 'Actif'; const originalOfferType = originalPanneau?.offerType || 'rental'; const originalPanels = originalPanneau?.physicalPanels || 0; const originalHasNoAds = originalPanneau?.hasNoAds || false; const currentOfferType = pCible.offerType || 'rental'; const currentPanels = pCible.physicalPanels || 0; const currentHasNoAds = pCible.hasNoAds || false; // SÉCURITÉ : Cristallisation du coût unitaire au moment de la commande const totalPhysicalPanelsCost = currentPanels > 0 ? pBoardFirst + (currentPanels - 1) * pBoardAdd : 0; let addedPanelsCost = 0; if (currentPanels > originalPanels) { const costNew = totalPhysicalPanelsCost; const costOld = originalPanels > 0 ? pBoardFirst + (originalPanels - 1) * pBoardAdd : 0; addedPanelsCost = Math.max(0, costNew - costOld); } let discountableHT = 0; let nonDiscountableHT = 0; if (!isActif) { nonDiscountableHT = totalPhysicalPanelsCost; discountableHT = (currentHasNoAds ? pNoAds : 0) + (currentOfferType === 'rental' ? pRental : pPurchase); } else { if (originalOfferType === 'rental' && currentOfferType === 'purchase') discountableHT += pPurchase; if (currentHasNoAds && !originalHasNoAds) discountableHT += pNoAds; nonDiscountableHT = addedPanelsCost; } const upfrontHT = discountableHT + nonDiscountableHT; const discountPct = myClientData?.discount || 0; const priceMultiplier = Math.max(0, (100 - discountPct) / 100); const discountAmount = discountableHT * (discountPct / 100); const discountedHT = upfrontHT - discountAmount; const platformHasTva = data.settings?.billing_has_tva !== '0'; const tvaMult = platformHasTva ? 1.20 : 1.00; const finalUpfrontTTC = discountedHT * tvaMult; const finalMonthlyTTC = pRental * priceMultiplier * tvaMult; const remainingToPay = Math.max(0, finalUpfrontTTC - (myClientData?.wallet_balance || 0)); // CORRECTION SÉCURITÉ 3D SECURE : Vérification au montage si l'on revient d'un paiement validé useEffect(() => { const successPiId = sessionStorage.getItem('pano_payment_success'); if (successPiId) { sessionStorage.removeItem('pano_payment_success'); setStep('success'); startInvoicePolling(successPiId); } return () => { if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Utilitaires de Garbage Collection const handleSafeClose = () => { sessionStorage.removeItem('pano_checkout_context'); if (onClose) onClose(); }; const handleSafeBack = () => { sessionStorage.removeItem('pano_checkout_context'); if (onBackToEditor) onBackToEditor(); }; // 4. - Actions métier const startInvoicePolling = (pi_id) => { if (!isMounted.current) return; setIsPollingInvoice(true); pollCountRef.current = 0; pollIntervalRef.current = setInterval(async () => { if (!isMounted.current) { clearInterval(pollIntervalRef.current); return; } pollCountRef.current += 1; const res = await safeFetch('invoices/check_by_pi', { method: 'POST', body: { pi_id: pi_id }, silent: true }); if (!isMounted.current) { clearInterval(pollIntervalRef.current); return; } if (res && res.status === 'success' && res.data?.invoice_ref) { setLatestInvoiceRef(res.data.invoice_ref); setIsPollingInvoice(false); clearInterval(pollIntervalRef.current); return; } if (pollCountRef.current >= 8) { setIsPollingInvoice(false); clearInterval(pollIntervalRef.current); } }, 2000); }; const handleActivationRequest = async () => { if (isSuspended) return showToast("Votre compte est suspendu. Veuillez régulariser vos factures.", "error"); if (!purchasesAllowed) return showToast("Les commandes sont temporairement suspendues.", "error"); if (isAdminView) return showToast("En tant qu'administration, vous ne pouvez pas passer de commande.", "error"); if (!waiverAccepted) return showToast("Veuillez accepter la renonciation au délai de rétractation.", "error"); const detailsWithCost = { ...pCible, physicalPanelsCostHT: totalPhysicalPanelsCost }; const payloadForIntent = { panneau_id: pCible.id, offer_type: currentOfferType, price_type: currentOfferType === 'rental' ? 'rentalMo' : 'purchase', physicalPanels: currentPanels, hasNoAds: currentHasNoAds }; const payloadForPaid = { id: pCible.id, offerType: currentOfferType, currentRate: pCible.currentRate, physicalPanels: currentPanels, details: detailsWithCost, name: pCible.name }; // CORRECTION SÉCURITÉ 3D SECURE : Sérialisation du contexte dans la session locale sessionStorage.setItem('pano_checkout_context', JSON.stringify(payloadForPaid)); const d = await safeFetch('stripe/intent', { body: payloadForIntent, setLoading: setIsSaving }); if (!isMounted.current) return; if (d) { if (d.data && d.data.free_activation_required) { const freeRes = await safeFetch('panneaux/activate_free', { body: { ...payloadForIntent, ...payloadForPaid }, setLoading: setIsSaving }); if (!isMounted.current) return; if (freeRes) { // GARBAGE COLLECTION : Nettoyage immédiat après activation gratuite sessionStorage.removeItem('pano_checkout_context'); setStep('success'); setIsPollingInvoice(false); refreshData(); } } else if (d.data && d.data.clientSecret) { const pi_id_extracted = d.data.clientSecret.split('_secret')[0]; setPaymentData({ secret: d.data.clientSecret, pi_id: pi_id_extracted, subId: d.data.subscription_id, amountCents: d.data.amountCents }); setStep('payment'); } else { showToast("Erreur lors de l'initialisation du paiement.", "error"); } } }; const handlePaymentSuccess = async (pi_id) => { // GARBAGE COLLECTION : Nettoyage immédiat après paiement réussi direct sessionStorage.removeItem('pano_checkout_context'); const active_pi = pi_id || paymentData?.pi_id; const detailsWithCost = { ...pCible, physicalPanelsCostHT: totalPhysicalPanelsCost }; const payload = { payment_intent_id: active_pi, subscription_id: paymentData?.subId, id: pCible.id, offerType: currentOfferType, currentRate: pCible.currentRate, physicalPanels: currentPanels, details: detailsWithCost, name: pCible.name }; const d = await safeFetch('panneaux/activate_paid', { body: payload, setLoading: setIsSaving }); if (!isMounted.current) return; if (d) { setStep('success'); if (active_pi) { startInvoicePolling(active_pi); } refreshData(); } else { showToast("Erreur inattendue après validation du paiement.", "error"); } }; // 5. - ÉCRANS DE BLOCAGE STRICTS if (!purchasesAllowed) { return (
e.stopPropagation()}>
{AlertTriangleIcon && }

Commandes suspendues

Pour des raisons de maintenance ou de surcharge logistique, les commandes sont temporairement désactivées sur l'ensemble de la plateforme. Veuillez nous excuser pour ce désagrément et réessayer ultérieurement.

); } if (isAdminView) { return (
e.stopPropagation()}>
{ShieldAlertIcon ? : }

Mode Administrateur

En tant que membre de l'équipe, vous avez la possibilité de préparer et configurer des brouillons, mais seul le client final est autorisé à valider le paiement et passer la commande depuis son espace personnel.

); } // 6. - Rendu UI : Tunnel de paiement (Étape 3) - MODALE DE SUCCÈS DÉDIÉE if (step === 'success') { return (
e.stopPropagation()}>
{CheckCircleIcon && }

Paiement validé !

Votre panneau est désormais activé. {currentPanels > 0 ? (currentPanels > 1 ? " Les impressions ont été lancées." : " L'impression a été lancée.") : ""}

Votre facture a été générée.

Vous pouvez la télécharger immédiatement pour votre comptabilité.

{isPollingInvoice ? ( ) : latestInvoiceRef ? ( ) : ( )}
); } // 7. - Rendu UI : Tunnel de paiement (Étape 2) - FORMULAIRE CB if (step === 'payment' && paymentData && StripeElements) { return ( { setStep('select'); setPaymentData(null); }} preventClose={isSaving}>
e.stopPropagation()}>

Paiement sécurisé

Montant à régler : {(paymentData.amountCents / 100).toFixed(2)} €
); } // 8. - Rendu UI : Configuration de l'offre (Étape 1) return (
e.stopPropagation()}>

{isActif ? "Commandes additionnelles" : "Détails de facturation"}

{isActif ? "Commandez de nouveaux panneaux physiques pour ce chantier." : "Configurez votre abonnement et commandez vos supports physiques."}

* Champs obligatoires

1. Formule d'abonnement

2. Panneaux physiques A1

Impression A1 (Akilux)

{pBoardFirst}€ / 1er panneau

Puis {pBoardAdd}€ l'unité supp.

Commandez vos panneaux physiques imprimés par nos soins et livrés sur votre chantier. (Tarif unique, impression et livraison incluses).

{originalPanels > 0 &&

Vous avez déjà commandé {originalPanels} panneaux physiques.

}
{currentPanels > originalPanels && (

Modifiez cette adresse si le lieu de livraison diffère de l'adresse de votre entreprise.

)}

Résumé de la commande (HT)

{!isActif && currentOfferType === 'rental' && (
Abonnement (Location 1er mois){pRental.toFixed(2)} €
)} {!isActif && currentOfferType === 'purchase' && ( <>
Achat définitif du panneau{pPurchase.toFixed(2)} €
)} {isActif && originalOfferType === 'rental' && currentOfferType === 'purchase' && ( <>
Upgrade : Achat définitif{pPurchase.toFixed(2)} €
)} {currentHasNoAds && !originalHasNoAds && (
Option marque blanche{pNoAds.toFixed(2)} €
)} {addedPanelsCost > 0 && (
Impression et livraison de {currentPanels - originalPanels} panneaux A1{addedPanelsCost.toFixed(2)} €
)}
{discountPct > 0 && discountableHT > 0 && (

Remise client applicable ({discountPct}%)

Sur les abonnements et options logicielles

-{discountAmount.toFixed(2)} €
)} {myClientData?.wallet_balance > 0 && (

Solde disponible (Avoir)

Sera déduit automatiquement de ce paiement

-{myClientData.wallet_balance.toFixed(2)} € TTC
)}

Total à régler {platformHasTva ? 'TTC' : 'HT'}

{remainingToPay.toFixed(2)} €
{platformHasTva &&

TVA (20%) incluse. Total initial HT : {discountedHT.toFixed(2)} €

}
{currentOfferType === 'rental' && (

Puis abonnement

{finalMonthlyTTC.toFixed(2)} € / mois {platformHasTva ? 'TTC' : 'HT'}

)}
{/* FIXED FOOTER */}
); }; /* EOF ========== [_react/clients/_clients_modals_checkout.jsx] */