chore: initial project import
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
This commit is contained in:
413
lib/demo-data.ts
Normal file
413
lib/demo-data.ts
Normal file
@ -0,0 +1,413 @@
|
||||
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[];
|
||||
}
|
||||
Reference in New Issue
Block a user