chore: initial project import
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled

This commit is contained in:
Wira Basalamah
2026-04-21 09:29:29 +07:00
commit adde003fba
222 changed files with 37657 additions and 0 deletions

View File

@ -0,0 +1,13 @@
import { ShellPage } from "@/components/page-templates";
import { SectionCard } from "@/components/ui";
export default function AgentContactDetailPage() {
return (
<ShellPage shell="agent" title="Contact Detail" description="Identity, tags, dan previous chats untuk agent.">
<div className="grid gap-6 xl:grid-cols-2">
<SectionCard title="Identity">Nama, nomor WhatsApp, tags.</SectionCard>
<SectionCard title="Previous chats">Riwayat ringkas conversation.</SectionCard>
</div>
</ShellPage>
);
}

View File

@ -0,0 +1,48 @@
import { redirect } from "next/navigation";
import Link from "next/link";
import { getSession } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { ShellPage } from "@/components/page-templates";
import { TablePlaceholder } from "@/components/placeholders";
export default async function AgentContactsPage() {
const session = await getSession();
if (!session) {
redirect("/login");
}
if (session.role !== "agent") {
redirect("/unauthorized");
}
const contacts = await prisma.contact.findMany({
where: { tenantId: session.tenantId },
include: {
contactTags: {
include: { tag: true }
}
},
orderBy: { lastInteractionAt: "desc" }
});
const rows = contacts.map((contact) => [
contact.fullName,
contact.phoneNumber,
contact.lastInteractionAt ? new Intl.DateTimeFormat("id-ID", { hour: "2-digit", minute: "2-digit" }).format(contact.lastInteractionAt) : "-"
,
<Link key={contact.id} href={`/contacts/${contact.id}`} className="text-brand hover:underline">
View
</Link>
]);
return (
<ShellPage shell="agent" title="Contacts" description="View contact terbatas untuk kebutuhan handling conversation.">
<TablePlaceholder
title="Contacts"
columns={["Name", "Phone", "Last interaction", "Action"]}
rows={rows}
/>
</ShellPage>
);
}

View File

@ -0,0 +1,25 @@
import Link from "next/link";
import { ShellPage } from "@/components/page-templates";
import { TablePlaceholder } from "@/components/placeholders";
import { getAgentMentionedConversations } from "@/lib/inbox-ops";
export default async function AgentMentionedPage() {
const rows = await getAgentMentionedConversations();
return (
<ShellPage shell="agent" title="Mentioned Conversations" description="Conversation yang melibatkan agent secara khusus.">
<TablePlaceholder
title="Mentioned"
columns={["Conversation", "Mentioned by", "Time"]}
rows={rows.map((item) => [
<Link key={`${item.id}-conv`} className="text-primary underline" href={`/agent/inbox?conversationId=${item.id}`}>
{item.contactName}
</Link>,
item.mentionedBy,
item.time
])}
/>
</ShellPage>
);
}

48
app/agent/inbox/page.tsx Normal file
View File

@ -0,0 +1,48 @@
import { InboxPlaceholder } from "@/components/placeholders";
import { ShellPage } from "@/components/page-templates";
import {
addConversationNote,
assignConversation,
getInboxWorkspace,
replyToConversation,
setConversationTags,
updateConversationStatus
} from "@/lib/inbox-ops";
const allowedFilters = ["all", "open", "pending", "resolved", "unassigned"] as const;
export default async function AgentInboxPage({
searchParams
}: {
searchParams: Promise<{ conversationId?: string; filter?: string }>;
}) {
const params = await searchParams;
const filter =
params?.filter && allowedFilters.includes(params.filter as (typeof allowedFilters)[number])
? (params.filter as (typeof allowedFilters)[number])
: "all";
const data = await getInboxWorkspace({
scope: "agent",
conversationId: params?.conversationId,
filter
});
return (
<ShellPage shell="agent" title="My Inbox" description="Assigned conversations, notes, tags, dan reply composer versi agent.">
<InboxPlaceholder
conversations={data.conversations}
selectedConversation={data.selectedConversation}
defaultPath={data.defaultPath}
role={data.role}
filter={data.filter}
canSelfAssign={data.canSelfAssign}
assignConversation={assignConversation}
updateConversationStatus={updateConversationStatus}
replyToConversation={replyToConversation}
addConversationNote={addConversationNote}
setConversationTags={setConversationTags}
/>
</ShellPage>
);
}

View File

@ -0,0 +1,17 @@
import { ShellPage } from "@/components/page-templates";
import { TablePlaceholder } from "@/components/placeholders";
import { getAgentResolvedHistory } from "@/lib/inbox-ops";
export default async function AgentResolvedPage() {
const rows = await getAgentResolvedHistory();
return (
<ShellPage shell="agent" title="Resolved History" description="Riwayat conversation yang sudah selesai ditangani.">
<TablePlaceholder
title="Resolved"
columns={["Conversation", "Resolved at", "Last action"]}
rows={rows.map((item) => [item.contactName, item.resolvedAt, item.lastAction])}
/>
</ShellPage>
);
}

View File

@ -0,0 +1,31 @@
import { ShellPage } from "@/components/page-templates";
import { TablePlaceholder } from "@/components/placeholders";
import { assignConversation, getInboxWorkspace } from "@/lib/inbox-ops";
export default async function AgentUnassignedPage() {
const data = await getInboxWorkspace({ scope: "agent", filter: "unassigned" });
return (
<ShellPage shell="agent" title="Unassigned Queue" description="Queue yang bisa diambil agent jika diizinkan.">
<TablePlaceholder
title="Queue"
columns={["Contact", "Last message", "Waiting time", "Action"]}
rows={data.conversations.map((item) => [
<>
<p>{item.name}</p>
<p className="text-xs text-outline">{item.phone}</p>
</>,
item.snippet,
item.time,
<form key={item.id} action={assignConversation} className="inline">
<input type="hidden" name="conversationId" value={item.id} />
<input type="hidden" name="nextPath" value="/agent/inbox/unassigned" />
<button className="rounded-full bg-surface-container-low px-3 py-2 text-xs" type="submit">
Take assignment
</button>
</form>
])}
/>
</ShellPage>
);
}

18
app/agent/page.tsx Normal file
View File

@ -0,0 +1,18 @@
import { DashboardPlaceholder } from "@/components/placeholders";
import { PlaceholderActions, ShellPage } from "@/components/page-templates";
import { getDashboardData } from "@/lib/platform-data";
export default async function AgentDashboardPage() {
const data = await getDashboardData();
return (
<ShellPage
shell="agent"
title="Agent Dashboard"
description="Assigned conversations, unread queue, due follow-up, dan personal stats."
actions={<PlaceholderActions primaryHref="/agent/inbox" primaryLabel="Open my inbox" />}
>
<DashboardPlaceholder stats={data.stats} priorityQueue={data.priorityQueue} />
</ShellPage>
);
}

View File

@ -0,0 +1,13 @@
import { DashboardPlaceholder } from "@/components/placeholders";
import { ShellPage } from "@/components/page-templates";
import { getDashboardData } from "@/lib/platform-data";
export default async function AgentPerformancePage() {
const data = await getDashboardData();
return (
<ShellPage shell="agent" title="My Performance" description="Response stats dan resolved chats milik agent.">
<DashboardPlaceholder stats={data.stats} priorityQueue={data.priorityQueue} />
</ShellPage>
);
}

View File

@ -0,0 +1,52 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { ShellPage } from "@/components/page-templates";
import { TablePlaceholder } from "@/components/placeholders";
function truncate(value: string, limit: number) {
return value.length <= limit ? value : `${value.slice(0, limit - 1)}`;
}
export default async function AgentQuickToolsPage() {
const session = await getSession();
if (!session) {
redirect("/login");
}
if (session.role !== "agent") {
redirect("/unauthorized");
}
const tenantId = session.tenantId;
const [templateCount, activeTemplates, followUpNotes] = await Promise.all([
prisma.messageTemplate.count({ where: { tenantId } }),
prisma.messageTemplate.count({ where: { tenantId, approvalStatus: "APPROVED" } }),
prisma.conversationNote.count({ where: { tenantId, userId: session.userId } })
]);
const totalMessages = await prisma.conversationActivity.count({ where: { tenantId, actorUserId: session.userId } });
return (
<ShellPage shell="agent" title="Quick Tools" description="Canned responses, template picker, dan follow-up reminders.">
<TablePlaceholder
title="Quick tools"
columns={["Tool", "Purpose", "Usage", "Action"]}
rows={[
[
"Canned Responses",
"Shortcuts berbasis template yang paling sering dipakai.",
truncate(`Total ${totalMessages} log agent activity`, 80),
<Link key="quick-canned" href="/templates" className="text-brand hover:underline">
Open templates
</Link>
],
["Template Picker", "Pilih template yang sudah disetujui.", `${activeTemplates}/${templateCount} approved`, <Link key="quick-picker" href="/templates" className="text-brand hover:underline">Open templates</Link>],
["Follow-up Notes", "Catatan follow-up dari conversation sendiri.", String(followUpNotes), <Link key="quick-followup" href="/agent/inbox" className="text-brand hover:underline">Open inbox</Link>]
]}
/>
</ShellPage>
);
}