Files
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

133 lines
4.0 KiB
TypeScript

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";
import type { ReactNode } from "react";
type SearchRow = [string, string, string, ReactNode];
function toLocale(date: Date | null) {
if (!date) {
return "-";
}
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 60_000) {
return "just now";
}
if (diff < 3_600_000) {
return `${Math.floor(diff / 60_000)}m ago`;
}
if (diff < 86_400_000) {
return `${Math.floor(diff / 3_600_000)}h ago`;
}
return new Intl.DateTimeFormat("id-ID", {
day: "2-digit",
month: "short",
year: "numeric"
}).format(date);
}
export default async function SearchPage({
searchParams
}: {
searchParams?: Promise<{ q?: string }>;
}) {
const params = await (searchParams ?? Promise.resolve({ q: "" }));
const q = params.q?.trim() ?? "";
const session = await getSession();
if (!session) {
redirect("/login");
}
const tenantFilter = session.role === "super_admin" ? {} : { tenantId: session.tenantId };
const contactScope = q
? {
OR: [{ fullName: { contains: q } }, { phoneNumber: { contains: q } }]
}
: {};
const userScope = q
? { OR: [{ fullName: { contains: q } }, { email: { contains: q } }] }
: {};
const convoScope = q
? { OR: [{ contact: { fullName: { contains: q } } }, { subject: { contains: q } }] }
: {};
const [contacts, users, conversations] = await Promise.all([
prisma.contact.findMany({
where: q ? { ...tenantFilter, ...contactScope } : tenantFilter,
orderBy: { lastInteractionAt: "desc" },
take: q ? 5 : 0
}),
prisma.user.findMany({
where: q ? { ...tenantFilter, ...userScope } : tenantFilter,
orderBy: { fullName: "asc" },
take: q ? 5 : 0
}),
prisma.conversation.findMany({
where: q ? { ...tenantFilter, ...convoScope } : tenantFilter,
include: { contact: true },
orderBy: { lastMessageAt: "desc" },
take: q ? 5 : 0
})
]);
const rows: SearchRow[] = q
? [
...contacts.map(
(contact) =>
[
"Contact",
contact.fullName,
`Last seen: ${toLocale(contact.lastInteractionAt)}`,
<Link key={`contact-${contact.id}`} href={`/contacts/${contact.id}`} className="text-brand hover:underline">
View
</Link>
] as SearchRow
),
...users.map(
(user) =>
[
"User",
user.fullName,
`Email: ${user.email}`,
<Link key={`user-${user.id}`} href={`/team/${user.id}`} className="text-brand hover:underline">
View
</Link>
] as SearchRow
),
...conversations.map(
(conversation) =>
[
"Conversation",
conversation.contact.fullName,
`Last message: ${toLocale(conversation.lastMessageAt)}`,
<Link
key={`conversation-${conversation.id}`}
href={session.role === "agent" ? `/agent/inbox?conversationId=${conversation.id}` : `/inbox?conversationId=${conversation.id}`}
className="text-brand hover:underline"
>
Open
</Link>
] as SearchRow
)
]
: [];
const infoText = q
? `${rows.length} hasil untuk "${q}"`
: "Masukkan keyword di query ?q=... untuk mencari conversation, contact, atau user.";
return (
<ShellPage shell="admin" title="Global Search" description="Entry point untuk search conversation, contact, dan user.">
<p className="rounded-xl border border-line bg-surface-container p-3 text-sm text-on-surface-variant">{infoText}</p>
<TablePlaceholder title="Search results" columns={["Type", "Name", "Context", "Action"]} rows={rows} />
</ShellPage>
);
}