/**
* =========================================================================
* PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0
* Interface Administrateur - Onglets Panneaux, Logistique et Messagerie
* =========================================================================
*/
const { useState, useEffect } = React;
const {
Building, Eye, AlertOctagon, CheckCircle, AlertTriangle, Power,
Package, MapPin, RefreshCw, Edit, FileText, Download, Plus,
MessageSquare, ArrowLeft, KeyRound, ShieldAlert, Shield, Mail, Trash2, X, Loader
} = window;
// =========================================================================
// 1. ONGLET : TOUS LES PANNEAUX
// =========================================================================
window.AdminPanneauxTab = ({ data, refreshData, showToast, setActiveTab }) => {
const [isSaving, setIsSaving] = useState(false);
const [previewPanneau, setPreviewPanneau] = useState(null);
const [managingPanneau, setManagingPanneau] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalView, setModalView] = useState(null);
const [editingEntity, setEditingEntity] = useState(null);
const [draggedItem, setDraggedItem] = useState(null);
const [moderationData, setModerationData] = useState(null);
const [confirmDialog, setConfirmDialog] = useState(null);
const clients = data.clients || [];
const panneaux = data.panneaux || [];
const demoPanneau = panneaux.find(p => p.id === 'demo-panneau') || {
id: 'demo-panneau',
status: 'Actif',
offerType: 'demo',
name: 'Panneau de démonstration',
location: 'Paris',
themeColor: '#059669',
hasNoAds: false
};
const regularPanneaux = panneaux.filter(p => p.id !== 'demo-panneau');
const unseenPanels = regularPanneaux.filter(p => !p.admin_seen);
const seenPanels = regularPanneaux.filter(p => p.admin_seen);
const checkPanelViolations = (p) => {
const blacklist = (data.settings?.blacklist || '')
.split(',')
.map(s => s.trim().toLowerCase())
.filter(Boolean);
const greylist = (data.settings?.greylist || '')
.split(',')
.map(s => s.trim().toLowerCase())
.filter(Boolean);
let blackCount = 0;
let greyCount = 0;
const textToSearch = [p.name, p.location, p.description, p.maitreOuvrage]
.filter(Boolean)
.join(' ')
.toLowerCase();
const detected = { black: [], grey: [] };
blacklist.forEach(word => {
if(textToSearch.includes(word)) {
blackCount++;
detected.black.push(word);
}
});
greylist.forEach(word => {
if(textToSearch.includes(word)) {
greyCount++;
detected.grey.push(word);
}
});
return { isViolation: blackCount > 0 || greyCount > 2, detected };
};
const handlePreviewAndMarkSeen = async (p) => {
setPreviewPanneau(p);
if (!p.admin_seen) {
try {
const payload = {
id: p.id,
status: p.status,
offerType: p.offerType,
currentRate: p.currentRate,
physicalPanels: p.physicalPanels,
details: { ...p, admin_seen: true }
};
await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux', {
method: 'POST',
body: JSON.stringify(payload)
});
refreshData();
} catch(e) {
console.error("Erreur de marquage vu", e);
}
}
};
const togglePanneauStatus = (id, currentStatus) => {
const newStatus = currentStatus === 'Actif' ? 'Hors ligne' : 'Actif';
setConfirmDialog({
title: "Changement de statut",
message: `Êtes-vous sûr de vouloir passer ce panneau en statut : ${newStatus} ? Si le panneau possède un abonnement Stripe, celui-ci pourrait être affecté.`,
confirmText: "Confirmer",
isDestructive: newStatus === 'Hors ligne' || newStatus === 'Désactivé',
onConfirm: async () => {
setIsSaving(true);
try {
await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux/status', {
method: 'POST',
body: JSON.stringify({ id, status: newStatus })
});
refreshData();
showToast(`Le panneau est maintenant ${newStatus}.`, "success");
} catch (e) {
showToast("Erreur lors du changement de statut.", "error");
} finally {
setIsSaving(false);
}
}
});
};
const handleSavePanneau = async () => {
setIsSaving(true);
try {
const payload = {
id: 'demo-panneau',
status: 'Actif',
offerType: 'demo',
currentRate: 0,
physicalPanels: 0,
details: managingPanneau
};
const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux', {
method: 'POST',
body: JSON.stringify(payload)
});
const d = await res.json();
if(d.status === 'success') {
showToast("Panneau de démonstration mis à jour !", "success");
setIsModalOpen(false);
refreshData();
} else {
showToast(d.message, 'error');
}
} catch(e) {
showToast("Erreur de sauvegarde", "error");
}
setIsSaving(false);
};
const saveEditedEntity = () => {
let newInter = [...(managingPanneau.intervenants || [])];
let newLots = JSON.parse(JSON.stringify(managingPanneau.lots || []));
const loc = editingEntity.location;
if (loc.type === 'intervenant') {
if (loc.index !== undefined) {
newInter[loc.index] = editingEntity.data;
} else {
newInter.push({...editingEntity.data, id: crypto.randomUUID()});
}
} else if (loc.type === 'lot') {
if (loc.index !== undefined) {
newLots[loc.index] = {...newLots[loc.index], ...editingEntity.data};
} else {
newLots.push({...editingEntity.data, id: crypto.randomUUID(), entreprises: []});
}
} else if (loc.type === 'entreprise') {
if (loc.index !== undefined) {
newLots[loc.lotIndex].entreprises[loc.index] = editingEntity.data;
} else {
newLots[loc.lotIndex].entreprises.push({...editingEntity.data, id: crypto.randomUUID()});
}
}
setManagingPanneau({ ...managingPanneau, intervenants: newInter, lots: newLots });
setEditingEntity(null);
};
const deleteEntity = (loc) => {
setConfirmDialog({
title: "Supprimer l'élément",
message: "Êtes-vous sûr de vouloir supprimer cet élément de l'équipe du projet ?",
confirmText: "Supprimer",
isDestructive: true,
onConfirm: () => {
let newInter = [...(managingPanneau.intervenants || [])];
let newLots = JSON.parse(JSON.stringify(managingPanneau.lots || []));
if (loc.type === 'intervenant') {
newInter.splice(loc.index, 1);
} else if (loc.type === 'lot') {
newLots.splice(loc.index, 1);
} else {
newLots[loc.lotIndex].entreprises.splice(loc.index, 1);
}
setManagingPanneau({ ...managingPanneau, intervenants: newInter, lots: newLots });
}
});
};
const handleDragStart = (e, location) => {
setDraggedItem(location);
e.dataTransfer.effectAllowed = 'move';
};
const handleDragOver = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const handleDrop = (e, dropLocation) => {
e.preventDefault();
if (!draggedItem || draggedItem.type !== dropLocation.type) return;
if (draggedItem.type === 'entreprise' && draggedItem.lotIndex !== dropLocation.lotIndex) return;
let newInter = [...(managingPanneau.intervenants || [])];
let newLots = JSON.parse(JSON.stringify(managingPanneau.lots || []));
if (draggedItem.type === 'intervenant') {
const item = newInter.splice(draggedItem.index, 1)[0];
newInter.splice(dropLocation.index, 0, item);
} else if (draggedItem.type === 'lot') {
const item = newLots.splice(draggedItem.index, 1)[0];
newLots.splice(dropLocation.index, 0, item);
} else if (draggedItem.type === 'entreprise') {
const arr = newLots[dropLocation.lotIndex].entreprises;
const item = arr.splice(draggedItem.index, 1)[0];
arr.splice(dropLocation.index, 0, item);
}
setManagingPanneau({ ...managingPanneau, intervenants: newInter, lots: newLots });
setDraggedItem(null);
};
const PanelCard = ({ c }) => {
const client = clients.find(cl => cl.id === c.clientName);
const { isViolation, detected } = checkPanelViolations(c);
return (
{c.name}
{c.location}
Client : {client ? client.name : c.clientName}
{isViolation && (
setModerationData({ panneau: c, detected })} className="p-2 bg-red-100 text-red-600 rounded-xl hover:bg-red-200 transition shrink-0" title="Alerte modération détectée">
)}
Abonnement
{c.offerType === 'rental' ? 'Loc.' : 'Achat'}
({c.currentRate}€)
handlePreviewAndMarkSeen(c)}
title="Contrôler le panneau public"
className="flex-1 py-2 bg-blue-50 text-blue-700 rounded-xl text-xs font-bold hover:bg-blue-100 transition flex justify-center items-center gap-2"
>
Contrôler
togglePanneauStatus(c.id, c.status)}
title={c.status === 'Actif' ? 'Désactiver le panneau (coupe abonnement)' : 'Activer le panneau'}
className={`flex-1 py-2 rounded-xl text-xs font-bold transition flex justify-center items-center gap-2 ${c.status === 'Actif' ? 'bg-red-50 text-red-600 hover:bg-red-100' : 'bg-emerald-50 text-emerald-600 hover:bg-emerald-100'}`}
>
{c.status === 'Actif' ? 'Désactiver' : 'Activer'}
);
};
return (
Tous les panneaux
Supervision des affichages légaux de l'ensemble du parc.
Panneau de démonstration
Gérez le contenu du faux panneau affiché sur la page d'accueil publique.
setPreviewPanneau({...demoPanneau})}
title="Aperçu du panneau de démonstration"
className="w-full sm:w-auto bg-purple-100 text-purple-700 px-6 py-3 rounded-xl text-sm font-bold shadow-sm hover:bg-purple-200 transition flex items-center justify-center gap-2"
>
Aperçu
{ setManagingPanneau({...demoPanneau}); setModalView('config_demo'); setIsModalOpen(true); }}
title="Éditer le panneau de démonstration"
className="w-full sm:w-auto bg-purple-600 text-white px-6 py-3 rounded-xl text-sm font-bold shadow-lg hover:bg-purple-700 transition flex items-center justify-center gap-2"
>
Éditer le panneau
{unseenPanels.length > 0 && (
À contrôler (nouveaux ou modifiés)
{unseenPanels.map(c =>
)}
)}
{seenPanels.length > 0 && (
)}
{regularPanneaux.length === 0 && (
Aucun panneau client enregistré.
)}
{/* MODALES LOCALES DES PANNEAUX */}
{isModalOpen && modalView === 'config_demo' && (
{ if(e.target === e.currentTarget) setIsModalOpen(false); }}>
e.stopPropagation()}>
Édition du panneau démo
Panneau public vitrine.
setIsModalOpen(false)} title="Fermer" className="text-slate-400 hover:bg-white hover:text-slate-700 p-2 rounded-xl border border-transparent hover:border-slate-200 transition">
setIsModalOpen(false)}
onSaveActive={handleSavePanneau}
onEditEntity={(loc, data) => setEditingEntity({location: loc, data})}
draggedItem={draggedItem}
handleDragStart={handleDragStart}
handleDragOver={handleDragOver}
handleDrop={handleDrop}
deleteEntity={deleteEntity}
isSaving={isSaving}
uiMode="professionnel"
/>
{editingEntity && (
)}
)}
{previewPanneau && (
setPreviewPanneau(null)}
showToast={showToast}
refreshData={refreshData}
/>
)}
{moderationData && (
setModerationData(null)} zIndex="z-[250]">
Mots détectés dans le panneau : {moderationData.panneau.name}
{moderationData.detected.black.length > 0 && (
Liste noire : {moderationData.detected.black.join(', ')}
)}
{moderationData.detected.grey.length > 0 && (
Liste grise : {moderationData.detected.grey.join(', ')}
)}
{
const u = new URL(window.location);
u.searchParams.set('chat_id', 'SUPPORT_' + moderationData.panneau.clientName);
window.history.replaceState({}, '', u);
setActiveTab('messages');
setModerationData(null);
}} className="w-full bg-blue-600 text-white py-3 rounded-xl font-bold hover:bg-blue-700 flex items-center justify-center gap-2 transition">
Contacter le client
{
togglePanneauStatus(moderationData.panneau.id, moderationData.panneau.status);
setModerationData(null);
}} className="w-full bg-red-600 text-white py-3 rounded-xl font-bold hover:bg-red-700 flex items-center justify-center gap-2 transition">
Suspendre ce panneau
setModerationData(null)} className="w-full bg-slate-100 text-slate-700 py-3 rounded-xl font-bold hover:bg-slate-200 transition">
Ignorer l'alerte
)}
{confirmDialog && (
setConfirmDialog(null)} />
)}
);
};
// =========================================================================
// 2. ONGLET : LOGISTIQUE
// =========================================================================
window.AdminLogistiqueTab = ({ data, refreshData, showToast }) => {
const [isSaving, setIsSaving] = useState(false);
const [shippingModal, setShippingModal] = useState(null);
const [trackingData, setTrackingData] = useState({ number: '', link: '' });
const [confirmDialog, setConfirmDialog] = useState(null);
const clients = data.clients || [];
const panneaux = data.panneaux || [];
const logiPanneaux = panneaux.filter(p => p.physicalPanels > 0 && p.id !== 'demo-panneau');
const updateShipping = async (id, status, tracking_number, tracking_link = '') => {
setIsSaving(true);
try {
const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux/shipping', {
method: 'POST',
body: JSON.stringify({ id, shipping_status: status, tracking_number, tracking_link })
});
const d = await res.json();
if (d.status === 'success') {
showToast(`Statut logistique mis à jour : ${status}`, "success");
setShippingModal(null);
refreshData();
} else {
showToast(d.message || "Erreur de mise à jour", "error");
}
} catch(e) {
showToast("Erreur réseau", "error");
} finally {
setIsSaving(false);
}
};
const handleA1Upload = async (c, e) => {
const file = e.target.files[0];
if(!file) return;
setIsSaving(true);
try {
const id = await window.uploadFile(file, 'pdf');
const payload = {
id: c.id,
status: c.status,
offerType: c.offerType,
currentRate: c.currentRate,
physicalPanels: c.physicalPanels,
details: { ...c, a1PdfId: id }
};
const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux', {
method: 'POST',
body: JSON.stringify(payload)
});
const d = await res.json();
if(d.status === 'success') {
showToast("Fichier A1 attaché avec succès !", "success");
refreshData();
} else {
showToast(d.message, 'error');
}
} catch(err) {
showToast("Erreur d'upload", "error");
}
setIsSaving(false);
e.target.value = null;
};
const handleA1Delete = (c) => {
setConfirmDialog({
title: "Retirer la maquette",
message: "Voulez-vous vraiment retirer ce fichier A1 de la commande ?",
confirmText: "Retirer",
isDestructive: true,
onConfirm: async () => {
setIsSaving(true);
try {
const payload = {
id: c.id,
status: c.status,
offerType: c.offerType,
currentRate: c.currentRate,
physicalPanels: c.physicalPanels,
details: { ...c, a1PdfId: '' }
};
const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux', {
method: 'POST',
body: JSON.stringify(payload)
});
const d = await res.json();
if (d.status === 'success') {
showToast("Fichier A1 retiré.", "success");
refreshData();
} else {
showToast(d.message, "error");
}
} catch(e) {
showToast("Erreur lors de la suppression", "error");
}
setIsSaving(false);
}
});
};
return (
Logistique
Gestion des expéditions de panneaux physiques A1.
{logiPanneaux.map(c => {
const client = clients.find(cl => cl.id === c.clientName);
const status = c.shipping_status || 'Validation en cours';
const isValidation = status === 'Validation en cours';
const isPrinting = status === "Attente d'impression";
const isShipped = status === 'Expédié';
const isDelivered = status === 'Livré';
return (
{c.name}
Client : {client?.name || 'Inconnu'}
{c.shippingAddress || client?.address || 'Adresse non renseignée'}
Quantité
{c.physicalPanels}
{isValidation && (
updateShipping(c.id, "Attente d'impression", '', '')}
disabled={isSaving || !c.a1PdfId}
className="w-full py-2 bg-amber-50 text-amber-700 font-bold rounded-xl hover:bg-amber-100 transition text-xs flex justify-center items-center gap-1 disabled:opacity-50"
title={!c.a1PdfId ? "Veuillez d'abord valider et attacher la maquette A1" : ""}
>
Passer en impression
)}
{isPrinting && (
{ setShippingModal(c); setTrackingData({number: '', link: ''}); }}
disabled={isSaving}
className="w-full py-2 bg-blue-50 text-blue-700 font-bold rounded-xl hover:bg-blue-100 transition text-xs flex justify-center items-center gap-1 disabled:opacity-50"
>
Déclarer expédié
)}
{isShipped && (
En cours d'acheminement
{c.tracking_number}
{ setShippingModal(c); setTrackingData({number: c.tracking_number || '', link: c.tracking_link || ''}); }}
disabled={isSaving}
className="mt-2 w-full py-1.5 bg-white border border-slate-200 text-slate-600 font-bold rounded-lg hover:bg-slate-50 transition text-[10px] flex justify-center items-center gap-1 shadow-sm"
>
Modifier le suivi
)}
{isDelivered && (
)}
{c.a1PdfId ? (
PDF Validé
handleA1Delete(c)} disabled={isSaving} className="text-[10px] text-red-500 font-bold hover:text-red-700 transition w-full text-center">
Retirer
) : (
{
setIsSaving(true);
try {
const blob = await window.generateA1PDF(c);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `Panneau_A1_${c.id}.pdf`;
link.click();
} catch(e) {
showToast("Erreur PDF", "error");
}
setIsSaving(false);
}}
disabled={isSaving}
className="py-1.5 px-2 bg-slate-100 text-slate-700 font-bold rounded-lg text-[10px] hover:bg-slate-200 transition w-full flex justify-center items-center gap-1 shadow-sm"
title="Télécharger le PDF généré automatiquement pour vérification"
>
Contrôler
{
setConfirmDialog({
title: "Validation de la maquette",
message: "Valider ce PDF généré automatiquement pour l'impression ?",
confirmText: "Valider",
onConfirm: async () => {
setIsSaving(true);
try {
const blob = await window.generateA1PDF(c);
const file = new File([blob], `A1_${c.id}.pdf`, { type: 'application/pdf' });
const id = await window.uploadFile(file, 'pdf');
const payload = {
id: c.id, status: c.status, offerType: c.offerType, currentRate: c.currentRate, physicalPanels: c.physicalPanels,
details: { ...c, a1PdfId: id }
};
const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux', { method: 'POST', body: JSON.stringify(payload) });
if((await res.json()).status === 'success') {
showToast("PDF généré et validé !", "success");
refreshData();
} else {
showToast("Erreur de sauvegarde", "error");
}
} catch(e) {
showToast("Erreur d'upload", "error");
}
setIsSaving(false);
}
});
}}
disabled={isSaving}
className="py-1.5 px-2 bg-emerald-50 text-emerald-700 font-bold rounded-lg text-[10px] hover:bg-emerald-100 transition w-full flex justify-center items-center gap-1 shadow-sm"
title="Valider et attacher la maquette générée automatiquement"
>
Valider
Manquant
handleA1Upload(c, e)} />
)}
);
})}
{logiPanneaux.length === 0 && (
Aucune commande
Les commandes de panneaux physiques apparaîtront ici.
)}
{shippingModal && (
setShippingModal(null)}
preventClose={isSaving}
>
)}
{confirmDialog && (
setConfirmDialog(null)} />
)}
);
};
// =========================================================================
// 3. ONGLET : MESSAGERIE ET SUPPORT GLOBAL
// =========================================================================
window.AdminMessagesTab = ({ data, refreshData, showToast, setActiveTab }) => {
const [mobileChatView, setMobileChatView] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [accessRequestModal, setAccessRequestModal] = useState(null);
const [confirmDialog, setConfirmDialog] = useState(null);
const clients = data.clients || [];
const interactions = data.interactions || [];
const threadsMap = {};
interactions.forEach(m => {
let threadId = null;
let title = '';
let targetEmail = '';
if (m.panneauId === 'CONTACT_PUBLIC') {
targetEmail = (m.target === 'Admin') ? m.author : m.target;
if (!targetEmail) return;
threadId = `CONTACT_PUBLIC_${targetEmail}`;
title = `Nous contacter : ${targetEmail}`;
} else if (m.panneauId.startsWith('SUPPORT_')) {
threadId = m.panneauId;
const cUid = m.panneauId.split('_')[1];
const cName = clients.find(c => c.id === cUid)?.name || 'Client Inconnu';
title = `Support : ${cName}`;
targetEmail = 'Client';
}
if (threadId) {
if (!threadsMap[threadId]) {
threadsMap[threadId] = { id: threadId, title, targetEmail, messages: [], unread: 0 };
}
threadsMap[threadId].messages.push(m);
if (m.target === 'Admin' && !m.resolved) {
threadsMap[threadId].unread++;
}
}
});
const requestedChatId = new URLSearchParams(window.location.search).get('chat_id');
if (requestedChatId && requestedChatId.startsWith('SUPPORT_') && !threadsMap[requestedChatId]) {
const cUid = requestedChatId.split('_')[1];
const clientObj = clients.find(c => c.id === cUid);
if (clientObj) {
threadsMap[requestedChatId] = {
id: requestedChatId,
title: `Support : ${clientObj.name || 'Client Inconnu'}`,
targetEmail: 'Client',
messages: [],
unread: 0
};
}
}
const threads = Object.values(threadsMap).sort((a,b) => {
const dateA = a.messages.length > 0 ? new Date(String(a.messages[0].created_at || '').replace(' ', 'T')).getTime() : 0;
const dateB = b.messages.length > 0 ? new Date(String(b.messages[0].created_at || '').replace(' ', 'T')).getTime() : 0;
return dateB - dateA;
});
const selectedThreadId = requestedChatId || (threads[0]?.id);
const setChatId = (id) => {
const u = new URL(window.location);
u.searchParams.set('chat_id', id);
window.history.replaceState({}, '', u);
setMobileChatView(true);
refreshData();
};
const selectedThread = threads.find(t => t.id === selectedThreadId);
const handleSendAdmin = async (text) => {
if (!selectedThread) return;
const optimisticMsg = {
id: 'temp_' + Date.now(),
panneauId: selectedThread.id.startsWith('CONTACT_PUBLIC') ? 'CONTACT_PUBLIC' : selectedThread.id,
author: 'Admin',
target: selectedThread.targetEmail,
detail: text,
isAlert: 0,
resolved: 0,
created_at: new Date().toISOString()
};
window.dispatchEvent(new CustomEvent('optimistic_message', { detail: optimisticMsg }));
const payload = {
panneauId: selectedThread.id.startsWith('CONTACT_PUBLIC') ? 'CONTACT_PUBLIC' : selectedThread.id,
detail: text,
author: 'Admin',
targetEmail: selectedThread.targetEmail
};
try {
await fetch(window.ECO_CONFIG.apiBaseUrl + 'interactions', {
method: 'POST',
body: JSON.stringify(payload)
});
} catch(e) {
console.error("Erreur lors de l'envoi", e);
}
};
const sendAccessRequest = async (uid, mode) => {
setIsSaving(true);
try {
const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'clients/request_access', {
method: 'POST',
body: JSON.stringify({ uid, mode })
});
if ((await res.json()).status === 'success') {
showToast("Demande d'accès envoyée avec succès.", "success");
setAccessRequestModal(null);
refreshData();
} else {
showToast("Erreur lors de la demande", "error");
}
} catch (e) {
showToast("Erreur réseau", "error");
} finally { setIsSaving(false); }
};
const revokeAccess = async (uid) => {
setConfirmDialog({
title: "Révocation d'accès",
message: "Êtes-vous sûr de vouloir révoquer l'autorisation d'accès technique pour ce client ?",
confirmText: "Oui, révoquer",
isDestructive: true,
onConfirm: async () => {
setIsSaving(true);
try {
const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'clients/revoke_access', {
method: 'POST',
body: JSON.stringify({ uid })
});
if ((await res.json()).status === 'success') {
showToast("Accès révoqué.", "success");
refreshData();
} else showToast("Erreur lors de la révocation", "error");
} finally { setIsSaving(false); }
}
});
};
const impersonateClient = async (uid) => {
setIsSaving(true);
try {
const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/impersonate', { method: 'POST', body: JSON.stringify({ uid }) });
if ((await res.json()).status === 'success') window.location.href = '?';
else showToast("Erreur d'accès", "error");
} finally { setIsSaving(false); }
};
const currentClient = selectedThread?.id.startsWith('SUPPORT_')
? clients.find(c => c.id === selectedThread.id.split('_')[1])
: null;
const isImpersonable = currentClient && currentClient.admin_access_until && new Date(String(currentClient.admin_access_until || '').replace(' ', 'T')) > new Date();
let chatClientName = 'Client';
if (currentClient && currentClient.name) {
chatClientName = currentClient.name;
}
const forceChatMobile = mobileChatView || !!requestedChatId || threads.length === 1;
return (
Conversations
{threads.map(t => (
setChatId(t.id)} className={`w-full text-left p-3 rounded-xl flex items-center justify-between transition ${selectedThreadId === t.id ? 'bg-emerald-50 border border-emerald-200' : 'hover:bg-white border border-transparent'}`}>
{t.title}
{t.messages.length} messages
{t.unread > 0 && {t.unread} }
))}
{threads.length === 0 &&
Aucune conversation.
}
{selectedThread ? (
<>
{threads.length > 1 && (
{
setMobileChatView(false);
const u = new URL(window.location);
u.searchParams.delete('chat_id');
window.history.replaceState({}, '', u);
}} className="md:hidden p-2 -ml-2 text-slate-500 hover:bg-slate-100 rounded-lg transition shrink-0">
)}
{selectedThread.title}
{selectedThread.id.startsWith('SUPPORT_') && (
isImpersonable ? (
impersonateClient(currentClient.id)} className="text-xs font-bold bg-emerald-600 text-white hover:bg-emerald-700 px-3 py-1.5 rounded-lg flex items-center gap-2 transition shadow-sm">
Contrôler
revokeAccess(currentClient.id)} className="text-xs font-bold bg-orange-100 text-orange-600 hover:bg-orange-200 px-3 py-1.5 rounded-lg flex items-center gap-2 transition shadow-sm border border-orange-200">
Révoquer
) : (
setAccessRequestModal(currentClient.id)} className="ml-auto text-xs font-bold bg-slate-100 text-slate-600 hover:bg-slate-200 px-3 py-1.5 rounded-lg flex items-center gap-2 transition shadow-sm border border-slate-200 shrink-0">
Demander l'accès
)
)}
>
) : (
Sélectionnez une conversation
)}
{/* MODALES LOCALES DE MESSAGERIE */}
{accessRequestModal && (
setAccessRequestModal(null)} zIndex="z-[250]">
Comment souhaitez-vous envoyer la demande d'autorisation d'accès temporaire (24h) à ce client ?
sendAccessRequest(accessRequestModal, 'chat')} disabled={isSaving} className="w-full py-3 bg-white border border-slate-200 text-slate-700 font-bold rounded-xl hover:bg-slate-50 transition text-sm flex items-center justify-center gap-2 shadow-sm">
Dans la messagerie (Recommandé)
sendAccessRequest(accessRequestModal, 'email')} disabled={isSaving} className="w-full py-3 bg-white border border-slate-200 text-slate-700 font-bold rounded-xl hover:bg-slate-50 transition text-sm flex items-center justify-center gap-2 shadow-sm">
Par e-mail
sendAccessRequest(accessRequestModal, 'both')} disabled={isSaving} className="w-full py-3 bg-emerald-600 text-white font-bold rounded-xl hover:bg-emerald-700 shadow-md transition text-sm flex items-center justify-center gap-2">
Les deux
)}
{confirmDialog && (
setConfirmDialog(null)} />
)}
);
};
/*
EOF ========== [_www/_react/_admin_panneaux_logistique.jsx]
*/