feat: pagina Conversazioni — inbox unificata messaggi clienti
Nuova pagina admin /admin/conversazioni: vista WhatsApp-style (lista conversazioni a sinistra, thread + risposta a destra) che aggrega i messaggi di tutti i clienti dalla tabella comments, cross-cliente. - Migration additiva 0014: clients.admin_last_read_at per tracciare letto/non-letto (pallini + badge in sidebar). Applicata a prod. - conversations-queries.ts: aggregazione entity_id → cliente (general/phase/task/deliverable), getConversations / getConversationThread / getUnreadConversationsCount. - Risposte admin salvate come commento "general" (visibili anche nella chat cliente e nel CommentsTab della scheda). - Voce di menu + badge non-letti (AdminSidebar/AdminShell/layout). - Token semantici per dual light/dark; refresh manuale (no polling). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Conversazioni — Luxury Admin CRM</title>
|
||||
<!-- Google Fonts: Plus Jakarta Sans -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Tailwind CSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
brand: {
|
||||
dark: '#1A463C', /* Verde scuro lusso */
|
||||
darkHover: '#13342D',
|
||||
active: 'rgba(255, 255, 255, 0.08)',
|
||||
bg: '#F8F9FA',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.sidebar-transition {
|
||||
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-brand-bg font-sans text-slate-800 antialiased min-h-screen flex overflow-x-hidden">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<aside id="sidebar" class="w-64 bg-brand-dark text-white flex flex-col justify-between p-6 border-r border-emerald-950/20 shrink-0 sidebar-transition relative z-10">
|
||||
<div class="overflow-hidden">
|
||||
<!-- Logo e Intestazione -->
|
||||
<div class="mb-10 px-2 flex items-center justify-between">
|
||||
<span id="sidebar-logo-text" class="text-lg font-bold tracking-wider text-emerald-50 whitespace-nowrap transition-opacity duration-300">iamcavalli</span>
|
||||
</div>
|
||||
|
||||
<!-- Navigazione Principale -->
|
||||
<nav class="space-y-1">
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2v-4zM14 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2v-4z"/></svg>
|
||||
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Dashboard</span>
|
||||
</a>
|
||||
|
||||
<!-- NUOVA VOCE: Conversazioni (Con Badge Notifica) -->
|
||||
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg text-sm text-white bg-brand-active font-medium transition-all duration-200 shadow-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a.598.598 0 01-.73-.578 5.672 5.672 0 011.02-3.155A8.72 8.72 0 013 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"/></svg>
|
||||
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Conversazioni</span>
|
||||
</div>
|
||||
<span class="bg-emerald-500 text-white text-[10px] font-bold px-2 py-0.5 rounded-full sidebar-text">2</span>
|
||||
</a>
|
||||
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Lead</span>
|
||||
</a>
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Clienti</span>
|
||||
</a>
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Progetti</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Area Inferiore Sidebar -->
|
||||
<div class="border-t border-white/10 pt-4 space-y-3 overflow-hidden">
|
||||
<div class="flex items-center justify-between px-3 py-1 text-xs text-emerald-200/50 whitespace-nowrap">
|
||||
<span class="sidebar-text transition-opacity duration-300">Tema</span>
|
||||
<button class="p-1 rounded-full bg-white/5 hover:bg-white/10 text-emerald-200 hover:text-white transition-all duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M14 12a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-red-300/80 hover:text-red-200 hover:bg-red-950/20 transition-all duration-200 whitespace-nowrap">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||
<span class="sidebar-text transition-opacity duration-300">Esci</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- SEZIONE PRINCIPALE -->
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
|
||||
<!-- BARRA SUPERIORE -->
|
||||
<header class="h-16 border-b border-slate-100 bg-white px-8 flex items-center justify-between shrink-0">
|
||||
<div class="flex items-center gap-4">
|
||||
<button id="sidebar-toggle" class="p-2 -ml-2 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-800 transition-colors focus:outline-none" title="Espandi/Comprimi Sidebar">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 6h16M4 12h12M4 18h16"/></svg>
|
||||
</button>
|
||||
<span class="text-xs text-slate-400 font-medium">Centro Messaggi Clienti</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-xs font-semibold text-slate-700 bg-slate-100 px-2.5 py-1 rounded-full">Admin Panel</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- AREA CHAT INBOX (LAYOUT A 2 COLONNE STILE WHATSAPP / INTERCOM) -->
|
||||
<main class="flex-1 p-8 max-w-[1400px] w-full mx-auto flex gap-6 h-[calc(100vh-4rem)] overflow-hidden">
|
||||
|
||||
<!-- COLONNA SINISTRA (1/3): Lista Progetti / Chat Clienti -->
|
||||
<div class="w-80 lg:w-96 bg-white border border-slate-100 rounded-xl flex flex-col shadow-[0_4px_20px_rgba(0,0,0,0.01)] shrink-0">
|
||||
|
||||
<!-- Header Filtro Lista -->
|
||||
<div class="p-4 border-b border-slate-100">
|
||||
<h2 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-3">Conversazioni Attive</h2>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cerca cliente o progetto..."
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-lg pl-8 pr-3 py-2 text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-brand-dark transition-all"
|
||||
/>
|
||||
<svg class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-2.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista Thread Clienti -->
|
||||
<div class="flex-1 overflow-y-auto divide-y divide-slate-50">
|
||||
|
||||
<!-- Thread 1 (Selezionato / Non letto) -->
|
||||
<div onclick="selectThread('Rossi Inc')" class="p-4 bg-emerald-50/30 border-l-4 border-brand-dark cursor-pointer transition-colors flex justify-between items-start gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<h3 class="text-xs font-bold text-slate-900 truncate">Rossi Inc</h3>
|
||||
<span class="text-[10px] text-slate-400 font-mono">10:42</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-600 font-medium truncate mb-1">In merito all'analisi competitor della Fase 2...</p>
|
||||
<span class="text-[9px] font-semibold text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-100">Fase 2: Analisi</span>
|
||||
</div>
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500 shrink-0 mt-1"></span>
|
||||
</div>
|
||||
|
||||
<!-- Thread 2 (Non letto) -->
|
||||
<div onclick="selectThread('Azienda Srl')" class="p-4 hover:bg-slate-50 cursor-pointer transition-colors flex justify-between items-start gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<h3 class="text-xs font-bold text-slate-800 truncate">Azienda Srl</h3>
|
||||
<span class="text-[10px] text-slate-400 font-mono">Ieri</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-500 truncate mb-1">Abbiamo caricato i file di onboarding!</p>
|
||||
<span class="text-[9px] font-semibold text-emerald-700 bg-emerald-50 px-1.5 py-0.5 rounded border border-emerald-100">Fase 1: Onboarding</span>
|
||||
</div>
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500 shrink-0 mt-1"></span>
|
||||
</div>
|
||||
|
||||
<!-- Thread 3 (Letto) -->
|
||||
<div onclick="selectThread('Caruso Speaker')" class="p-4 hover:bg-slate-50 cursor-pointer transition-colors flex justify-between items-start gap-3 opacity-60">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<h3 class="text-xs font-semibold text-slate-800 truncate">Caruso Speaker</h3>
|
||||
<span class="text-[10px] text-slate-400 font-mono">12 Lug</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-500 truncate mb-1">Perfetto, attendo aggiornamenti.</p>
|
||||
<span class="text-[9px] font-semibold text-slate-500 bg-slate-100 px-1.5 py-0.5 rounded">Generale</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COLONNA DESTRA (2/3): Area Chat con Thread Attivo -->
|
||||
<div class="flex-1 bg-white border border-slate-100 rounded-xl flex flex-col shadow-[0_4px_20px_rgba(0,0,0,0.01)] min-w-0">
|
||||
|
||||
<!-- Header Chat Selezionata -->
|
||||
<div class="p-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 id="active-chat-title" class="text-sm font-bold text-slate-900">Rossi Inc</h2>
|
||||
<p class="text-[10px] text-slate-400 mt-0.5">Progetto: Web Domination — Pagina Pubblica Client Portal</p>
|
||||
</div>
|
||||
<a href="#" class="text-xs font-medium text-brand-dark hover:text-brand-darkHover inline-flex items-center gap-1">
|
||||
Vedi Portal Cliente ➔
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Thread Messaggi -->
|
||||
<div id="admin-chat-messages" class="flex-1 p-6 overflow-y-auto bg-slate-50/30 space-y-4">
|
||||
|
||||
<!-- Messaggio Cliente -->
|
||||
<div class="max-w-[75%] bg-white border border-slate-200/80 p-3.5 rounded-2xl rounded-tl-sm shadow-sm space-y-1">
|
||||
<div class="flex justify-between items-center gap-4 text-[9px] font-bold text-slate-400">
|
||||
<span>Cliente (Rossi Inc)</span>
|
||||
<span class="text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded">Riferito a: Fase 2 - Analisi</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-700 leading-relaxed">
|
||||
Buongiorno, abbiamo revisionato l'analisi dei competitor. Vorremmo aggiungere un paio di riferimenti al report prima della meeting di martedì.
|
||||
</p>
|
||||
<span class="text-[9px] text-slate-300 font-mono block text-right">10:42</span>
|
||||
</div>
|
||||
|
||||
<!-- Risposta Admin -->
|
||||
<div class="max-w-[75%] ml-auto bg-brand-dark text-white p-3.5 rounded-2xl rounded-tr-sm shadow-sm space-y-1">
|
||||
<div class="flex justify-between items-center gap-4 text-[9px] font-bold text-emerald-200/80">
|
||||
<span>Tu (Admin)</span>
|
||||
<span>10:45</span>
|
||||
</div>
|
||||
<p class="text-xs leading-relaxed">
|
||||
Ricevuto! Potete caricare i documenti direttamente nella sezione Documenti della vostra area riservata, così li integriamo nella Fase 2.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Box Risposta Admin -->
|
||||
<div class="p-4 border-t border-slate-100 bg-white">
|
||||
<div class="flex gap-2">
|
||||
<textarea
|
||||
id="admin-chat-input"
|
||||
rows="2"
|
||||
placeholder="Scrivi una risposta per il cliente..."
|
||||
class="flex-1 bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-brand-dark focus:bg-white transition-all resize-none"
|
||||
></textarea>
|
||||
<button
|
||||
onclick="sendAdminReply()"
|
||||
class="bg-brand-dark hover:bg-brand-darkHover text-white text-xs font-semibold px-5 py-2 rounded-lg transition-colors shrink-0 self-end h-10"
|
||||
>
|
||||
Invia Risposta
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- SCRIPT INTERATTIVO CHAT -->
|
||||
<script>
|
||||
// Toggle Sidebar
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const toggleButton = document.getElementById('sidebar-toggle');
|
||||
const logoText = document.getElementById('sidebar-logo-text');
|
||||
const sidebarTexts = document.querySelectorAll('.sidebar-text');
|
||||
|
||||
toggleButton.addEventListener('click', () => {
|
||||
const isCollapsed = sidebar.classList.contains('w-20');
|
||||
|
||||
if (isCollapsed) {
|
||||
sidebar.classList.remove('w-20');
|
||||
sidebar.classList.add('w-64');
|
||||
setTimeout(() => {
|
||||
logoText.classList.remove('opacity-0');
|
||||
sidebarTexts.forEach(text => text.classList.remove('opacity-0'));
|
||||
}, 150);
|
||||
} else {
|
||||
logoText.classList.add('opacity-0');
|
||||
sidebarTexts.forEach(text => text.classList.add('opacity-0'));
|
||||
sidebar.classList.remove('w-64');
|
||||
sidebar.classList.add('w-20');
|
||||
}
|
||||
});
|
||||
|
||||
// Cambio Thread Cliente
|
||||
function selectThread(clientName) {
|
||||
document.getElementById('active-chat-title').innerText = clientName;
|
||||
}
|
||||
|
||||
// Invio Risposta
|
||||
function sendAdminReply() {
|
||||
const input = document.getElementById('admin-chat-input');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
const container = document.getElementById('admin-chat-messages');
|
||||
const replyBubble = document.createElement('div');
|
||||
replyBubble.className = "max-w-[75%] ml-auto bg-brand-dark text-white p-3.5 rounded-2xl rounded-tr-sm shadow-sm space-y-1";
|
||||
replyBubble.innerHTML = `
|
||||
<div class="flex justify-between items-center gap-4 text-[9px] font-bold text-emerald-200/80">
|
||||
<span>Tu (Admin)</span>
|
||||
<span>Adesso</span>
|
||||
</div>
|
||||
<p class="text-xs leading-relaxed">${text}</p>
|
||||
`;
|
||||
|
||||
container.appendChild(replyBubble);
|
||||
input.value = '';
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user