// ECO-PANNEAU.FR - _react/admin/_admin_logistique.jsx // 1. - COMPOSANT CARTE ISOLÉ ET RÉUTILISABLE (100% ENCAPSULÉ / ZÉRO-DETTE) window.pano_AdminLogisticsCard = ({ delivery: c, clients, isPinned, toggleDashboardPin, refreshData, isMini = false, limitShippingForce = 90, // PROPS DE ROUTAGE INJECTÉES PAR LE PARENT openModal, openDialog, closeCurrentLayer, activeModal, activeDialog, targetId, dialogId }) => { const { useState } = React; // ZÉRO-DETTE : Utilisation de notre Hook abstrait ! const { isMounted, safeFetch } = window.pano_useSafeFetch(); // CORRECTION ZÉRO-DETTE : Fallbacks de navigation robustes const handleOpenModal = (n, id, s) => { if (openModal) openModal(n, id, s); }; const handleOpenDialog = (n, id, s) => { if (openDialog) openDialog(n, id, s); }; const handleCloseLayer = () => { if (closeCurrentLayer) closeCurrentLayer(); }; const [isSaving, setIsSaving] = useState(false); const [rejectReason, setRejectReason] = useState(''); const [adminPwd, setAdminPwd] = useState(''); const [trackingData, setTrackingData] = useState({ number: c.tracking_number || '', link: c.tracking_link || '' }); const [manualUploadData, setManualUploadData] = useState(null); const [confirmConfig, setConfirmConfig] = useState(null); const [hasBeenControlled, setHasBeenControlled] = useState(false); const { PackageIcon, CheckCircleIcon, ExternalLinkIcon, LoaderIcon, AlertTriangleIcon, EyeIcon, XIcon, PinIcon, UploadIcon, Trash2Icon, DownloadIcon, EditIcon } = window.pano_getIcons(); const { StatusBadge, IconBadge, Button, DataCard, Modal, ConfirmModal, FormTextarea, FormInput, UniversalViewer } = window.pano_getComponents(); const computeFileHash = async (blobOrFile) => { try { const buffer = await blobOrFile.arrayBuffer(); const digest = await crypto.subtle.digest('SHA-256', buffer); const array = Array.from(new Uint8Array(digest)); return array.map(b => b.toString(16).padStart(2, '0')).join('').substring(0, 32); } catch (e) { return Date.now().toString(16); } }; const owner = clients.find(cl => cl.id === c.clientName); const currentStatus = c.shipping_status || 'En attente de validation'; const canModifyMaquette = ['En attente de validation', 'Attente validation client', 'Maquette refusée'].includes(currentStatus); let isForceDeliveryAvailable = false; if (currentStatus === 'Expédié' && c.updated_at) { const updateTime = new Date(c.updated_at.replace(' ', 'T')).getTime(); const daysSince = (Date.now() - updateTime) / (1000 * 3600 * 24); if (daysSince > limitShippingForce) isForceDeliveryAvailable = true; } let variant = 'default'; if (currentStatus === 'Maquette refusée') variant = 'danger'; else if (['En attente de validation', 'En attente de commande au fournisseur', "En attente d'impression"].includes(currentStatus) || isForceDeliveryAvailable) variant = 'warning'; else if (currentStatus === 'Annulé') variant = 'slate'; const pdfFileName = `A1_${c.id}_${c.a1PdfHash || 'gen'}.pdf`; const handleA1Delete = () => { setConfirmConfig({ title: "Retirer la maquette", message: "Voulez-vous retirer le fichier PDF A1 de ce panneau ?", confirmText: "Retirer", isDestructive: true, onConfirm: async () => { handleCloseLayer(); const d = await safeFetch('panneaux', { body: { id: c.id, status: c.status, offerType: c.offerType, currentRate: c.currentRate, physicalPanels: c.physicalPanels, details: { ...c, a1PdfId: '', a1PdfHash: '' }, a1PdfId: '' }, setLoading: setIsSaving, successMessage: "Maquette retirée avec succès." }); if (!isMounted.current) return; // SÉCURITÉ if (d && refreshData) refreshData(); setConfirmConfig(null); }, onCancel: () => { setConfirmConfig(null); handleCloseLayer(); } }); handleOpenDialog('confirm', c.id, false); }; const updateShippingStatus = async (newStatus) => { const d = await safeFetch('panneaux/shipping', { body: { id: c.id, shipping_status: newStatus }, setLoading: setIsSaving, successMessage: "Statut logistique mis à jour." }); if (!isMounted.current) return; // SÉCURITÉ if (d && refreshData) refreshData(); }; const handleConfirmDelivery = async () => { const d = await safeFetch('panneaux/shipping', { body: { id: c.id, shipping_status: 'Réception confirmée' }, setLoading: setIsSaving, successMessage: "Réception confirmée !" }); if (!isMounted.current) return; // SÉCURITÉ if (d) { if (refreshData) refreshData(); handleCloseLayer(); } }; // ------------------------------------------------------------------------ // Rendu : Mode Mini (Historique compact) // ------------------------------------------------------------------------ if (isMini) { return ( <>

{c.name || 'Projet sans nom'}

{owner?.name || c.clientName} • Qté: {c.physicalPanels} • {currentStatus}

{c.a1PdfId && (
{activeDialog === 'confirm' && dialogId === c.id && confirmConfig && ConfirmModal && } {activeModal === 'view_a1_pdf' && targetId === c.id && UniversalViewer && ( )} ); } // ------------------------------------------------------------------------ // Rendu : Mode Standard (Cartes complètes pour l'action requise / en cours) // ------------------------------------------------------------------------ return ( <>
{ e.stopPropagation(); toggleDashboardPin && toggleDashboardPin('deliveries', c.id, e); }} className={`absolute top-2 right-2 z-20 p-1 cursor-pointer transition-all duration-200 ${isPinned ? 'text-amber-500 opacity-100 hover:scale-110' : 'text-slate-300 opacity-0 group-hover:opacity-100 hover:text-slate-500 hover:scale-110'}`} style={{ transform: 'rotate(30deg)' }} title={isPinned ? "Désépingler" : "Épingler au tableau de bord"} >

{c.name || 'Projet sans nom'}

{(owner?.name || c.clientName) && (

{owner ? owner.name : c.clientName}

)} {(c.shippingAddress || c.location) && (

{c.shippingAddress || c.location}

)}
{c.physicalPanels} Qté
{/* BLOC ACTIONS / CONTENU */}
{/* Gestion PDF (seulement si la commande n'est pas clôturée) */} {!(currentStatus.startsWith('Réception confirmée') || currentStatus === 'Annulé') && (
Maquette A1 pour impression {c.a1PdfId ? (
{canModifyMaquette && (
{canModifyMaquette && ( )}
) : (
{canModifyMaquette ? ( <>
) : (
{AlertTriangleIcon && } Maquette indisponible (Anomalie)
)}
)}
Nom du fichier {c.a1PdfId ? pdfFileName : Aucun fichier joint}
)}
{currentStatus === 'Annulé' && (

Commande Annulée

)} {currentStatus === 'Attente validation client' && (

En attente de la validation du client

)} {currentStatus === 'Maquette refusée' && (
{AlertTriangleIcon && }

Veuillez proposer une nouvelle maquette

)} {currentStatus === 'En attente de validation' && c.a1PdfId && ( )} {currentStatus === 'En attente de commande au fournisseur' && ( )} {currentStatus === "En attente d'impression" && ( )} {currentStatus === 'Expédié' && (
{c.tracking_number ? (

{c.tracking_number}

) : (

En cours de livraison

)} {c.tracking_link && (
{isForceDeliveryAvailable && ( )}
)} {currentStatus.startsWith('Réception confirmée') && (

Colis Livré

)} {!(currentStatus.startsWith('Réception confirmée') || currentStatus === 'Annulé') && (
)}
{/* Modales Encapsulées Zéro-Dette */} {activeModal === 'delivery_confirm' && targetId === c.id && Modal && ( ( <> )}>

Confirmez-vous la bonne réception de la commande logistique associée à ce panneau ?

)} {activeModal === 'reject_a1' && targetId === c.id && Modal && ( ( <> )}>
{ e.preventDefault(); const d = await safeFetch('panneaux/client_validate_a1', { body: { id: c.id, action: 'reject', reason: rejectReason }, setLoading: setIsSaving, successMessage: "Refus transmis au support technique." }); if (!isMounted.current) return; // SÉCURITÉ if (d) { if (refreshData) refreshData(); handleCloseLayer(); } }} className="space-y-4 min-w-0"> {FormTextarea && setRejectReason(e.target.value)} placeholder="Ex: Le numéro de permis est incorrect, la couleur de la bordure ne correspond pas..." autoFocus className="min-w-0 w-full" />}
)} {activeDialog === 'manual_upload_message' && dialogId === c.id && manualUploadData && Modal && ( { setManualUploadData(null); handleCloseLayer(); }} preventClose={isSaving} actions={(close) => ( <> )}>
{ e.preventDefault(); setIsSaving(true); try { const { file, message } = manualUploadData; const hash_str = await computeFileHash(file); if (!isMounted.current) return; // SÉCURITÉ const idObj = await window.pano_uploadFile(file, 'pdf', null, false, false, c.clientName, c.id); if (!isMounted.current) return; // SÉCURITÉ const actualId = idObj.id || idObj; if (actualId) { const d1 = await safeFetch('panneaux', { body: { id: c.id, status: c.status, offerType: c.offerType, currentRate: c.currentRate, physicalPanels: c.physicalPanels, details: { ...c, a1PdfHash: hash_str } } }); if (!isMounted.current) return; // SÉCURITÉ const d2 = await safeFetch('panneaux/manual_a1', { body: { id: c.id, a1PdfId: actualId, message: message }, successMessage: "Fichier envoyé." }); if (!isMounted.current) return; // SÉCURITÉ if (d2) { if (refreshData) refreshData(); setManualUploadData(null); handleCloseLayer(); } } } catch (err) { if (!isMounted.current) return; // SÉCURITÉ if (window.pano_showToast) window.pano_showToast(err.message || "Erreur d'envoi", "error"); } finally { if (isMounted.current) setIsSaving(false); } }} className="space-y-4 min-w-0">

Vous allez soumettre un PDF modifié au client.

Le client devra valider formellement ce fichier depuis son espace avant de passer commande.

{FormTextarea && setManualUploadData({...manualUploadData, message: e.target.value})} placeholder="Ex: Bonjour, nous avons ajusté..." className="min-w-0 w-full" />}
)} {activeDialog === 'cancel_order' && dialogId === c.id && Modal && ( ( <> )}>
{ e.preventDefault(); const d = await safeFetch('panneaux/cancel_order', { body: { id: c.id, password: adminPwd }, setLoading: setIsSaving, successMessage: "Commande annulée." }); if (!isMounted.current) return; // SÉCURITÉ if (d) { if (refreshData) refreshData(); handleCloseLayer(); } }} className="space-y-4 min-w-0">

Vous allez annuler cette commande logistique.

Le montant sera automatiquement recrédité (Avoir) au client.

{FormInput && setAdminPwd(e.target.value)} className="min-w-0 w-full" />}
)} {activeDialog === 'shipping' && dialogId === c.id && Modal && ( ( <> )}>
{ e.preventDefault(); let link = trackingData.link.trim(); if (link && !/^https?:\/\//i.test(link)) link = 'https://' + link; const d = await safeFetch('panneaux/shipping', { body: { id: c.id, shipping_status: 'Expédié', tracking_number: trackingData.number, tracking_link: link }, setLoading: setIsSaving, successMessage: "Suivi mis à jour !" }); if (!isMounted.current) return; // SÉCURITÉ if(d) { if(refreshData) refreshData(); handleCloseLayer(); } }} className="space-y-4 min-w-0"> {FormInput && setTrackingData({...trackingData, number: e.target.value})} placeholder="Ex: 8J009088..." className="min-w-0 w-full" />} {FormInput && setTrackingData({...trackingData, link: e.target.value})} placeholder="www.transporteur.com/suivi/..." className="min-w-0 w-full" />}
)} {activeDialog === 'confirm' && dialogId === c.id && confirmConfig && ConfirmModal && } {activeModal === 'view_a1_pdf' && targetId === c.id && UniversalViewer && ( )} ); }; // ============================================================================ // 2. ONGLET PRINCIPAL LOGISTIQUE ADMIN // ============================================================================ window.pano_AdminLogisticsTab = ({ data, refreshData, adminOpts, toggleDashboardPin }) => { // ZÉRO-DETTE : Utilisation de notre Hook abstrait ! const { isMounted, safeFetch } = window.pano_useSafeFetch(); // CORRECTION ZÉRO-DETTE PERFORMANCE : Abonnement unique au routeur const urlModal = window.pano_useUrlModal ? window.pano_useUrlModal() : {}; const { openModal, openDialog, closeCurrentLayer, activeModal, activeDialog, targetId, dialogId } = urlModal; // CORRECTION ZÉRO-DETTE : Retrait du et du setPreviewConfig parent const { SearchBar, EmptySearch, PaginationFooter, CardGrid, AdminLogisticsCard } = window.pano_getComponents(); const { PackageIcon, AlertTriangleIcon, PinIcon, LoaderIcon, CheckCircleIcon, TruckIcon } = window.pano_getIcons(); const deliveries = (data.panneaux || []).filter(p => p.physicalPanels > 0 && p.status !== 'Brouillon' && p.id !== 'demo-panneau' && p.shipping_status !== 'Archivé'); const clients = data.clients || []; const { visibleCount, setVisibleCount, searchQuery, setSearchQuery, filteredData: filteredDeliveries } = window.pano_useSearchAndPagination(deliveries, (p, q) => { const n = window.pano_normalizeString; const owner = clients.find(cl => cl.id === p.clientName); const ownerName = n(owner ? owner.name : p.clientName); return n(p.name).includes(q) || ownerName.includes(q) || n(p.tracking_number).includes(q) || n(p.shipping_status).includes(q) || n(p.location).includes(q) || n(p.shippingAddress).includes(q); }); const pinnedIds = adminOpts?.deliveries || []; const attentionDeliveries = []; const pinnedDeliveries = []; const activeDeliveries = []; const otherDeliveries = []; // Dynamisation des règles métier const limitShippingForce = parseInt(data.settings?.limit_shipping_force || 90, 10); const MAX_HISTORY = parseInt(data.settings?.limit_history_max || 500, 10); // Répartition des commandes en 4 catégories pour l'admin filteredDeliveries.forEach(c => { const currentStatus = c.shipping_status || 'En attente de validation'; let isForceDeliveryAvailable = false; if (currentStatus === 'Expédié' && c.updated_at) { const updateTime = new Date(c.updated_at.replace(' ', 'T')).getTime(); const daysSince = (Date.now() - updateTime) / (1000 * 3600 * 24); if (daysSince > limitShippingForce) isForceDeliveryAvailable = true; } const needsAttention = ['En attente de validation', 'Maquette refusée', 'En attente de commande au fournisseur', "En attente d'impression"].includes(currentStatus) || isForceDeliveryAvailable; const isPinned = pinnedIds.includes(c.id); const isActive = !needsAttention && !(currentStatus.startsWith('Réception confirmée') || currentStatus === 'Annulé'); // Distribution des props du routeur const cardProps = { delivery: c, clients, isPinned, toggleDashboardPin, refreshData, limitShippingForce, openModal, openDialog, closeCurrentLayer, activeModal, activeDialog, targetId, dialogId }; if (needsAttention) { attentionDeliveries.push(cardProps); } else if (isPinned) { pinnedDeliveries.push(cardProps); } else if (isActive) { activeDeliveries.push(cardProps); } else { otherDeliveries.push(cardProps); } }); const hasMoreHistory = otherDeliveries.length > MAX_HISTORY; const historyToDisplay = hasMoreHistory ? otherDeliveries.slice(0, MAX_HISTORY) : otherDeliveries; const displayedOtherDeliveries = historyToDisplay.slice(0, visibleCount); return (

Logistique

{deliveries.length > 0 && SearchBar && (
)}
{deliveries.length === 0 ? (

Aucune livraison

) : filteredDeliveries.length === 0 ? ( EmptySearch && ) : ( <> {/* 10.1 - Priorités */} {attentionDeliveries.length > 0 && (

Action requise

{attentionDeliveries.map(props => )}
)} {/* 10.2 - Favoris */} {pinnedDeliveries.length > 0 && (

Favoris (Épinglés)

{pinnedDeliveries.map(props => )}
)} {/* 10.3 - Commandes en cours */} {activeDeliveries.length > 0 && (

Commandes en cours

{activeDeliveries.map(props => )}
)} {/* 10.4 - Historique (Compacté) */} {otherDeliveries.length > 0 && (

Historique des commandes

{displayedOtherDeliveries.map(props => )}
{hasMoreHistory && visibleCount >= MAX_HISTORY && (

L'affichage est limité aux {MAX_HISTORY} résultats les plus récents pour garantir les performances.

Veuillez utiliser la barre de recherche ci-dessus pour affiner les résultats et trouver des commandes plus anciennes.

)} {PaginationFooter && !hasMoreHistory && } {PaginationFooter && hasMoreHistory && visibleCount < MAX_HISTORY && }
)} )}
); }; /* EOF ========== [_react/admin/_admin_logistique.jsx] */