admin roadmap csv export, adminactionlog, global search
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user