diff --git a/design-reference/pagina-conversazioni b/design-reference/pagina-conversazioni new file mode 100644 index 0000000..2a99454 --- /dev/null +++ b/design-reference/pagina-conversazioni @@ -0,0 +1,292 @@ + + + + + + Conversazioni — Luxury Admin CRM + + + + + + + + + + + + + + + +
+ + +
+
+ + Centro Messaggi Clienti +
+
+ Admin Panel +
+
+ + +
+ + +
+ + +
+

Conversazioni Attive

+
+ + +
+
+ + +
+ + +
+
+
+

Rossi Inc

+ 10:42 +
+

In merito all'analisi competitor della Fase 2...

+ Fase 2: Analisi +
+ +
+ + +
+
+
+

Azienda Srl

+ Ieri +
+

Abbiamo caricato i file di onboarding!

+ Fase 1: Onboarding +
+ +
+ + +
+
+
+

Caruso Speaker

+ 12 Lug +
+

Perfetto, attendo aggiornamenti.

+ Generale +
+
+ +
+
+ + +
+ + +
+
+

Rossi Inc

+

Progetto: Web Domination — Pagina Pubblica Client Portal

+
+ + Vedi Portal Cliente ➔ + +
+ + +
+ + +
+
+ Cliente (Rossi Inc) + Riferito a: Fase 2 - Analisi +
+

+ Buongiorno, abbiamo revisionato l'analisi dei competitor. Vorremmo aggiungere un paio di riferimenti al report prima della meeting di martedì. +

+ 10:42 +
+ + +
+
+ Tu (Admin) + 10:45 +
+

+ Ricevuto! Potete caricare i documenti direttamente nella sezione Documenti della vostra area riservata, così li integriamo nella Fase 2. +

+
+ +
+ + +
+
+ + +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/src/app/admin/conversazioni/actions.ts b/src/app/admin/conversazioni/actions.ts new file mode 100644 index 0000000..1d0f1bc --- /dev/null +++ b/src/app/admin/conversazioni/actions.ts @@ -0,0 +1,59 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { getServerSession } from "next-auth"; +import { eq } from "drizzle-orm"; +import { authOptions } from "@/lib/auth"; +import { db } from "@/db"; +import { clients, comments } from "@/db/schema"; + +async function requireAdmin() { + const session = await getServerSession(authOptions); + if (!session) throw new Error("Non autorizzato"); +} + +/** + * Admin reply from the Conversazioni inbox. Per project decision, replies are + * saved as a "general" comment on the client (entity_id = clientId), so they + * surface in the client's general chat and in the client detail CommentsTab. + */ +export async function replyToConversation(clientId: string, formData: FormData) { + await requireAdmin(); + const body = (formData.get("body") as string)?.trim(); + if (!clientId || !body) throw new Error("Dati mancanti"); + + // Validate the client exists (entity_id integrity). + const rows = await db + .select({ id: clients.id }) + .from(clients) + .where(eq(clients.id, clientId)) + .limit(1); + if (rows.length === 0) throw new Error("Cliente non trovato"); + + await db.insert(comments).values({ + entity_type: "general", + entity_id: clientId, + author: "admin", + body, + }); + + // Replying implies the admin has read the incoming messages. + await db + .update(clients) + .set({ admin_last_read_at: new Date() }) + .where(eq(clients.id, clientId)); + + revalidatePath("/admin/conversazioni"); + revalidatePath(`/admin/clients/${clientId}`); +} + +/** Mark a client's conversation as read up to now (clears unread dot/badge). */ +export async function markConversationRead(clientId: string) { + await requireAdmin(); + if (!clientId) return; + await db + .update(clients) + .set({ admin_last_read_at: new Date() }) + .where(eq(clients.id, clientId)); + revalidatePath("/admin/conversazioni"); +} diff --git a/src/app/admin/conversazioni/page.tsx b/src/app/admin/conversazioni/page.tsx new file mode 100644 index 0000000..763dcbd --- /dev/null +++ b/src/app/admin/conversazioni/page.tsx @@ -0,0 +1,41 @@ +import { PageHeader } from "@/components/admin/PageHeader"; +import { ConversationsView } from "@/components/admin/conversazioni/ConversationsView"; +import { + getConversations, + getConversationThread, +} from "@/lib/conversations-queries"; + +export const revalidate = 0; + +export default async function ConversazioniPage({ + searchParams, +}: { + searchParams: Promise<{ c?: string }>; +}) { + const { c } = await searchParams; + + const conversations = await getConversations(); + // Default selection: the requested client, else the most recent conversation. + const activeClientId = + (c && conversations.some((conv) => conv.clientId === c) ? c : null) ?? + conversations[0]?.clientId ?? + null; + + const activeThread = activeClientId + ? await getConversationThread(activeClientId) + : null; + + return ( +
+ + +
+ ); +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 1c1b966..eb177b6 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,6 +1,7 @@ import { AdminShell } from "@/components/admin/AdminShell"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; +import { getUnreadConversationsCount } from "@/lib/conversations-queries"; export default async function AdminLayout({ children, @@ -11,5 +12,6 @@ export default async function AdminLayout({ if (!session) { return
{children}
; } - return {children}; + const unreadConversations = await getUnreadConversationsCount(); + return {children}; } diff --git a/src/components/admin/AdminShell.tsx b/src/components/admin/AdminShell.tsx index 809a4f4..9eb0808 100644 --- a/src/components/admin/AdminShell.tsx +++ b/src/components/admin/AdminShell.tsx @@ -12,7 +12,13 @@ const SIDEBAR_STORAGE_KEY = "iamcavalli:sidebar"; * (starts expanded on server/first paint to avoid a hydration mismatch, * then reconciles client-side — mirrors the pattern used by useTheme). */ -export function AdminShell({ children }: { children: React.ReactNode }) { +export function AdminShell({ + children, + unreadConversations = 0, +}: { + children: React.ReactNode; + unreadConversations?: number; +}) { const [collapsed, setCollapsed] = useState(false); useEffect(() => { @@ -30,7 +36,7 @@ export function AdminShell({ children }: { children: React.ReactNode }) { return (
- +
{/* Top header bar */} diff --git a/src/components/admin/AdminSidebar.tsx b/src/components/admin/AdminSidebar.tsx index fb596e7..01803cf 100644 --- a/src/components/admin/AdminSidebar.tsx +++ b/src/components/admin/AdminSidebar.tsx @@ -13,12 +13,14 @@ import { LogOut, Zap, FileText, + MessageSquare, } from "lucide-react"; import ThemeToggle from "@/components/ThemeToggle"; import { cn } from "@/lib/utils"; const NAV_ITEMS = [ { href: "/admin", label: "Dashboard", icon: LayoutDashboard, exact: true }, + { href: "/admin/conversazioni", label: "Conversazioni", icon: MessageSquare }, { href: "/admin/pipeline", label: "Pipeline", icon: Zap }, { href: "/admin/clients", label: "Clienti", icon: Users }, { href: "/admin/projects", label: "Progetti", icon: FolderOpen }, @@ -28,7 +30,13 @@ const NAV_ITEMS = [ { href: "/admin/impostazioni", label: "Impostazioni", icon: Settings }, ]; -export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) { +export function AdminSidebar({ + collapsed = false, + unreadConversations = 0, +}: { + collapsed?: boolean; + unreadConversations?: number; +}) { const pathname = usePathname(); const isActive = (href: string, exact?: boolean) => @@ -64,6 +72,10 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {