180 lines
4.8 KiB
TypeScript
180 lines
4.8 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
|
|
|
export type AdminSearchHit =
|
|
| {
|
|
type: "user";
|
|
id: string;
|
|
title: string;
|
|
subtitle: string;
|
|
href: string;
|
|
}
|
|
| {
|
|
type: "trip";
|
|
id: string;
|
|
title: string;
|
|
subtitle: string;
|
|
href: string;
|
|
}
|
|
| {
|
|
type: "booking";
|
|
id: string;
|
|
title: string;
|
|
subtitle: string;
|
|
href: string;
|
|
};
|
|
|
|
/**
|
|
* Resolve query admin search ke entity detail page yang paling relevan.
|
|
* Dispatch berdasarkan pola input:
|
|
* - email (`@`) → user lookup exact
|
|
* - `midtrans-<bookingId>-<n>` / `manual-<bookingId>` → booking detail
|
|
* - cuid (~25 char alphanumeric, mulai `cm`) → coba trip → booking → user
|
|
* - sisanya → fuzzy search trip title/destination + user name
|
|
*/
|
|
export const adminSearchService = {
|
|
async resolve(query: string, limit = 10): Promise<AdminSearchHit[]> {
|
|
const q = query.trim();
|
|
if (q.length < 2) return [];
|
|
|
|
// Email exact
|
|
if (q.includes("@")) {
|
|
const user = await prisma.user.findUnique({
|
|
where: { email: q.toLowerCase() },
|
|
select: { id: true, name: true, email: true },
|
|
});
|
|
if (user) {
|
|
return [
|
|
{
|
|
type: "user",
|
|
id: user.id,
|
|
title: user.name,
|
|
subtitle: user.email,
|
|
href: `/admin/users/${user.id}`,
|
|
},
|
|
];
|
|
}
|
|
}
|
|
|
|
// Midtrans / manual order_id pattern
|
|
if (q.startsWith("midtrans-") || q.startsWith("manual-")) {
|
|
const payment = await prisma.payment.findUnique({
|
|
where: { externalOrderId: q },
|
|
select: {
|
|
bookingId: true,
|
|
booking: {
|
|
select: {
|
|
trip: { select: { title: true } },
|
|
user: { select: { name: true, email: true } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (payment) {
|
|
return [
|
|
{
|
|
type: "booking",
|
|
id: payment.bookingId,
|
|
title: `Booking: ${payment.booking.trip.title}`,
|
|
subtitle: `${payment.booking.user.name} · ${payment.booking.user.email}`,
|
|
href: `/admin/bookings/${payment.bookingId}`,
|
|
},
|
|
];
|
|
}
|
|
}
|
|
|
|
// CUID pattern — try trip → booking → user
|
|
if (/^cm[a-z0-9]{10,}$/i.test(q)) {
|
|
const [trip, booking, user] = await Promise.all([
|
|
prisma.trip.findUnique({
|
|
where: { id: q },
|
|
select: { id: true, title: true, destination: true },
|
|
}),
|
|
prisma.booking.findUnique({
|
|
where: { id: q },
|
|
select: {
|
|
id: true,
|
|
trip: { select: { title: true } },
|
|
user: { select: { name: true, email: true } },
|
|
},
|
|
}),
|
|
prisma.user.findUnique({
|
|
where: { id: q },
|
|
select: { id: true, name: true, email: true },
|
|
}),
|
|
]);
|
|
const hits: AdminSearchHit[] = [];
|
|
if (trip) {
|
|
hits.push({
|
|
type: "trip",
|
|
id: trip.id,
|
|
title: trip.title,
|
|
subtitle: trip.destination,
|
|
href: `/admin/trips/${trip.id}`,
|
|
});
|
|
}
|
|
if (booking) {
|
|
hits.push({
|
|
type: "booking",
|
|
id: booking.id,
|
|
title: `Booking: ${booking.trip.title}`,
|
|
subtitle: `${booking.user.name} · ${booking.user.email}`,
|
|
href: `/admin/bookings/${booking.id}`,
|
|
});
|
|
}
|
|
if (user) {
|
|
hits.push({
|
|
type: "user",
|
|
id: user.id,
|
|
title: user.name,
|
|
subtitle: user.email,
|
|
href: `/admin/users/${user.id}`,
|
|
});
|
|
}
|
|
if (hits.length > 0) return hits;
|
|
}
|
|
|
|
// Fuzzy: trip + user
|
|
const [trips, users] = await Promise.all([
|
|
prisma.trip.findMany({
|
|
where: {
|
|
OR: [
|
|
{ title: { contains: q, mode: "insensitive" } },
|
|
{ destination: { contains: q, mode: "insensitive" } },
|
|
],
|
|
},
|
|
select: { id: true, title: true, destination: true, location: true },
|
|
take: limit,
|
|
orderBy: { date: "desc" },
|
|
}),
|
|
prisma.user.findMany({
|
|
where: {
|
|
OR: [
|
|
{ name: { contains: q, mode: "insensitive" } },
|
|
{ email: { contains: q, mode: "insensitive" } },
|
|
],
|
|
},
|
|
select: { id: true, name: true, email: true },
|
|
take: limit,
|
|
orderBy: { createdAt: "desc" },
|
|
}),
|
|
]);
|
|
|
|
return [
|
|
...users.map<AdminSearchHit>((u) => ({
|
|
type: "user",
|
|
id: u.id,
|
|
title: u.name,
|
|
subtitle: u.email,
|
|
href: `/admin/users/${u.id}`,
|
|
})),
|
|
...trips.map<AdminSearchHit>((t) => ({
|
|
type: "trip",
|
|
id: t.id,
|
|
title: t.title,
|
|
subtitle: `${t.destination} · ${t.location}`,
|
|
href: `/admin/trips/${t.id}`,
|
|
})),
|
|
].slice(0, limit);
|
|
},
|
|
};
|