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:
13
app/agent/contacts/[contactId]/page.tsx
Normal file
13
app/agent/contacts/[contactId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
48
app/agent/contacts/page.tsx
Normal file
48
app/agent/contacts/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
app/agent/inbox/mentioned/page.tsx
Normal file
25
app/agent/inbox/mentioned/page.tsx
Normal 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
48
app/agent/inbox/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
app/agent/inbox/resolved/page.tsx
Normal file
17
app/agent/inbox/resolved/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
app/agent/inbox/unassigned/page.tsx
Normal file
31
app/agent/inbox/unassigned/page.tsx
Normal 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
18
app/agent/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
app/agent/performance/page.tsx
Normal file
13
app/agent/performance/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
52
app/agent/quick-tools/page.tsx
Normal file
52
app/agent/quick-tools/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user