import { CampaignStatus, ConversationPriority, ConversationStatus, RoleCode, TenantStatus } from "@prisma/client"; import { getSession } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; export type DashboardStats = { label: string; value: string; delta: string; }; export type ConversationRecord = { id: string; tenantId: string; name: string; phone: string; snippet: string; time: string; status: "Open" | "Pending" | "Resolved"; assignee: string; channel: string; tags: string[]; priority: "Normal" | "High"; }; export type ContactRecord = { id: string; tenantId: string; fullName: string; phone: string; email: string; tags: string[]; lastInteraction: string; optInStatus: string; }; export type UserRecord = { id: string; tenantId: string; fullName: string; email: string; role: "Admin" | "Agent"; status: "Active" | "Invited"; lastLogin: string; handled: number; avgResponse: string; }; export type CampaignRecord = { id: string; tenantId: string; name: string; channel: string; audience: string; status: "Draft" | "Processing" | "Completed"; scheduledAt: string; delivered: number; failed: number; }; export type TenantRecord = { id: string; name: string; status: "Active" | "Trial" | "Suspended" | "Inactive"; plan: string; channels: string; seats: string; }; function formatRelativeDate(date: Date | null | undefined) { if (!date) { return "-"; } const now = new Date(); const diff = now.getTime() - date.getTime(); const day = 1000 * 60 * 60 * 24; if (diff < day) { return new Intl.DateTimeFormat("id-ID", { hour: "2-digit", minute: "2-digit" }).format(date); } if (diff < day * 2) { return "Yesterday"; } return new Intl.DateTimeFormat("id-ID", { day: "2-digit", month: "short", year: "numeric" }).format(date); } function formatDate(date: Date | null | undefined) { if (!date) { return "-"; } return new Intl.DateTimeFormat("id-ID", { day: "2-digit", month: "short", year: "numeric" }).format(date); } function titleCase(value: string) { return value .toLowerCase() .split("_") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function mapConversationStatus(status: ConversationStatus): ConversationRecord["status"] { if (status === ConversationStatus.PENDING) { return "Pending"; } if (status === ConversationStatus.RESOLVED) { return "Resolved"; } return "Open"; } function mapConversationPriority(priority: ConversationPriority): ConversationRecord["priority"] { return priority === ConversationPriority.HIGH || priority === ConversationPriority.URGENT ? "High" : "Normal"; } function mapCampaignStatus(status: CampaignStatus): CampaignRecord["status"] { if (status === CampaignStatus.COMPLETED) { return "Completed"; } if (status === CampaignStatus.PROCESSING || status === CampaignStatus.PARTIAL_FAILED) { return "Processing"; } return "Draft"; } function mapTenantStatus(status: TenantStatus): TenantRecord["status"] { switch (status) { case TenantStatus.ACTIVE: return "Active"; case TenantStatus.SUSPENDED: return "Suspended"; case TenantStatus.INACTIVE: return "Inactive"; case TenantStatus.TRIAL: default: return "Trial"; } } async function getTenantScopedWhere() { const session = await getSession(); if (!session?.tenantId || session.role === "super_admin") { return {}; } return { tenantId: session.tenantId }; } export async function getDashboardData() { const where = await getTenantScopedWhere(); const [openConversations, waitingReply, resolvedToday, deliveredMessages, totalOutbound, priorityQueue] = await Promise.all([ prisma.conversation.count({ where: { ...where, status: ConversationStatus.OPEN } }), prisma.conversation.count({ where: { ...where, status: ConversationStatus.PENDING } }), prisma.conversation.count({ where: { ...where, status: ConversationStatus.RESOLVED, resolvedAt: { gte: new Date(new Date().setHours(0, 0, 0, 0)) } } }), prisma.message.count({ where: { ...where, direction: "OUTBOUND", deliveryStatus: "DELIVERED" } }), prisma.message.count({ where: { ...where, direction: "OUTBOUND" } }), prisma.conversation.findMany({ where, orderBy: [{ priority: "desc" }, { lastMessageAt: "desc" }], take: 3, include: { contact: true, channel: true, assignedUser: true, conversationTags: { include: { tag: true } }, messages: { take: 1, orderBy: { createdAt: "desc" } } } }) ]); const successRate = totalOutbound > 0 ? `${((deliveredMessages / totalOutbound) * 100).toFixed(1)}%` : "0%"; return { stats: [ { label: "Open conversations", value: String(openConversations), delta: "Live" }, { label: "Waiting reply", value: String(waitingReply), delta: "Live" }, { label: "Resolved today", value: String(resolvedToday), delta: "Today" }, { label: "Delivery success", value: successRate, delta: "Outbound" } ] satisfies DashboardStats[], priorityQueue: priorityQueue.map((conversation) => ({ id: conversation.id, tenantId: conversation.tenantId, name: conversation.contact.fullName, phone: conversation.contact.phoneNumber, snippet: conversation.messages[0]?.contentText ?? conversation.subject ?? "No recent message", time: formatRelativeDate(conversation.lastMessageAt), status: mapConversationStatus(conversation.status), assignee: conversation.assignedUser?.fullName ?? "Unassigned", channel: conversation.channel.channelName, tags: conversation.conversationTags.map((item) => item.tag.name), priority: mapConversationPriority(conversation.priority) })) }; } export async function getInboxData() { const where = await getTenantScopedWhere(); const conversations = await prisma.conversation.findMany({ where, orderBy: { lastMessageAt: "desc" }, include: { contact: true, channel: true, assignedUser: true, conversationTags: { include: { tag: true } }, messages: { take: 1, orderBy: { createdAt: "desc" } } } }); const mapped = conversations.map((conversation) => ({ id: conversation.id, tenantId: conversation.tenantId, name: conversation.contact.fullName, phone: conversation.contact.phoneNumber, snippet: conversation.messages[0]?.contentText ?? conversation.subject ?? "No recent message", time: formatRelativeDate(conversation.lastMessageAt), status: mapConversationStatus(conversation.status), assignee: conversation.assignedUser?.fullName ?? "Unassigned", channel: conversation.channel.channelName, tags: conversation.conversationTags.map((item) => item.tag.name), priority: mapConversationPriority(conversation.priority) })) satisfies ConversationRecord[]; return { conversations: mapped, selectedConversation: mapped[0] }; } export async function getContactsData() { const where = await getTenantScopedWhere(); const contacts = await prisma.contact.findMany({ where, orderBy: { lastInteractionAt: "desc" }, include: { contactTags: { include: { tag: true } } } }); return contacts.map((contact) => ({ id: contact.id, tenantId: contact.tenantId, fullName: contact.fullName, phone: contact.phoneNumber, email: contact.email ?? "-", tags: contact.contactTags.map((item) => item.tag.name), lastInteraction: formatRelativeDate(contact.lastInteractionAt), optInStatus: titleCase(contact.optInStatus) })) satisfies ContactRecord[]; } export async function getTeamData() { const where = await getTenantScopedWhere(); const users = await prisma.user.findMany({ where, include: { role: true, assignedConversations: true }, orderBy: [{ role: { code: "asc" } }, { fullName: "asc" }] }); return users.map((user) => ({ id: user.id, tenantId: user.tenantId, fullName: user.fullName, email: user.email, role: user.role.code === RoleCode.ADMIN_CLIENT ? "Admin" : "Agent", status: user.status === "ACTIVE" ? "Active" : "Invited", lastLogin: formatRelativeDate(user.lastLoginAt), handled: user.assignedConversations.length, avgResponse: user.role.code === RoleCode.AGENT ? "3m 12s" : "-" })) satisfies UserRecord[]; } export async function getCampaignsData() { const where = await getTenantScopedWhere(); const campaigns = await prisma.broadcastCampaign.findMany({ where, include: { channel: true, segment: true }, orderBy: [{ createdAt: "desc" }] }); return campaigns.map((campaign) => ({ id: campaign.id, tenantId: campaign.tenantId, name: campaign.name, channel: campaign.channel.channelName, audience: campaign.segment ? `Segment: ${campaign.segment.name}` : titleCase(campaign.audienceType), status: mapCampaignStatus(campaign.status), scheduledAt: formatDate(campaign.scheduledAt), delivered: campaign.totalDelivered, failed: campaign.totalFailed })) satisfies CampaignRecord[]; } export async function getTenantAdminSummary() { const where = await getTenantScopedWhere(); const [contactCount, campaignCount, activeAgents] = await Promise.all([ prisma.contact.count({ where }), prisma.broadcastCampaign.count({ where }), prisma.user.count({ where: { ...where, role: { code: RoleCode.AGENT } } }) ]); return { contactCount, campaignCount, activeAgents }; } export async function getPlatformSummary() { const [tenantCount, connectedChannels, failedWebhooks, tenants] = await Promise.all([ prisma.tenant.count(), prisma.channel.count({ where: { status: "CONNECTED" } }), prisma.webhookEvent.count({ where: { processStatus: "failed" } }), prisma.tenant.findMany({ include: { plan: true, channels: true, users: true }, orderBy: { createdAt: "asc" } }) ]); return { stats: [ { label: "Active tenants", value: String(tenantCount), delta: "Global" }, { label: "Connected channels", value: String(connectedChannels), delta: "Healthy" }, { label: "Webhook failures", value: String(failedWebhooks), delta: "Watch" }, { label: "Tracked seats", value: String(tenants.reduce((sum, tenant) => sum + tenant.users.length, 0)), delta: "Users" } ] satisfies DashboardStats[], tenants: tenants.map((tenant) => ({ id: tenant.id, name: tenant.name, status: mapTenantStatus(tenant.status), plan: tenant.plan.name, channels: `${tenant.channels.filter((channel) => channel.status === "CONNECTED").length} connected`, seats: `${tenant.users.length}/${tenant.plan.seatQuota}` })) satisfies TenantRecord[] }; } export async function getTenantsData() { const tenants = await prisma.tenant.findMany({ include: { plan: true, channels: true, users: true }, orderBy: { createdAt: "asc" } }); return tenants.map((tenant) => ({ id: tenant.id, name: tenant.name, status: mapTenantStatus(tenant.status), plan: tenant.plan.name, channels: `${tenant.channels.filter((channel) => channel.status === "CONNECTED").length} connected`, seats: `${tenant.users.length}/${tenant.plan.seatQuota}` })) satisfies TenantRecord[]; }