Files
whatsapp-inbox-platform/lib/demo-data.ts
Wira Basalamah adde003fba
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
chore: initial project import
2026-04-21 09:29:29 +07:00

414 lines
11 KiB
TypeScript

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[];
}