admin roadmap csv export, adminactionlog, global search

This commit is contained in:
2026-05-18 20:09:22 +07:00
parent 244a6da9bb
commit ea63f56e97
25 changed files with 1330 additions and 158 deletions
+96
View File
@@ -0,0 +1,96 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { isAdminEmail } from "@/lib/admin";
import { payoutRepo } from "@/server/repositories/payout.repo";
import { buildCsv, csvDateJakarta, csvResponse } from "@/lib/csv";
import type { PayoutStatus } from "@/app/generated/prisma/enums";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const VALID_STATUS = new Set<PayoutStatus>([
"HELD",
"RELEASED",
"PAID",
"CANCELLED",
]);
function parseDate(value: string | null): Date | undefined {
if (!value) return undefined;
const d = new Date(value);
return Number.isNaN(d.getTime()) ? undefined : d;
}
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user || !isAdminEmail(session.user.email)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const params = req.nextUrl.searchParams;
const statusParam = params.get("status");
if (!statusParam || !VALID_STATUS.has(statusParam as PayoutStatus)) {
return NextResponse.json(
{ error: "status param wajib (HELD/RELEASED/PAID/CANCELLED)" },
{ status: 400 }
);
}
const status = statusParam as PayoutStatus;
const rows = await payoutRepo.listByStatus(status, {
dateFrom: parseDate(params.get("dateFrom")),
dateTo: parseDate(params.get("dateTo")),
processorEmail: params.get("reviewer") || undefined,
});
const csv = buildCsv(
[
"Payout ID",
"Status",
"Nominal (IDR)",
"Currency",
"Held until",
"Released at",
"Paid at",
"Cancelled at",
"Processor email",
"Admin note",
"Dibuat",
"Organizer nama",
"Organizer email",
"Bank nama",
"Bank rekening",
"Bank atas nama",
"Trip ID",
"Trip judul",
"Booking ID",
"Peserta nama",
],
rows.map((p) => [
p.id,
p.status,
p.amount,
p.currency,
csvDateJakarta(p.heldUntil),
csvDateJakarta(p.releasedAt),
csvDateJakarta(p.paidAt),
csvDateJakarta(p.cancelledAt),
p.processedBy?.email ?? "",
p.adminNote ?? "",
csvDateJakarta(p.createdAt),
p.organizer.name,
p.organizer.email,
p.bankName ?? "",
p.bankAccountNumber ?? "",
p.bankAccountName ?? "",
p.trip.id,
p.trip.title,
p.booking.id,
p.booking.user.name,
])
);
const today = new Date().toISOString().slice(0, 10);
return csvResponse(`payouts-${status}-${today}.csv`, csv);
}
+114
View File
@@ -0,0 +1,114 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { isAdminEmail } from "@/lib/admin";
import { refundRepo } from "@/server/repositories/refund.repo";
import { buildCsv, csvDateJakarta, csvResponse } from "@/lib/csv";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const VALID_STATUS = new Set([
"PENDING",
"APPROVED",
"REJECTED",
"PROCESSING",
"SUCCEEDED",
"FAILED",
]);
const VALID_REASON = new Set([
"USER_CANCELLATION",
"ORGANIZER_CANCELLED",
"TRIP_ISSUE",
"ADMIN_ADJUSTMENT",
"DISPUTE_RESOLVED",
]);
function parseDate(value: string | null): Date | undefined {
if (!value) return undefined;
const d = new Date(value);
return Number.isNaN(d.getTime()) ? undefined : d;
}
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user || !isAdminEmail(session.user.email)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const params = req.nextUrl.searchParams;
const statusParam = params.get("status");
const status =
statusParam && VALID_STATUS.has(statusParam)
? (statusParam as
| "PENDING"
| "APPROVED"
| "REJECTED"
| "PROCESSING"
| "SUCCEEDED"
| "FAILED")
: undefined;
const reasonParam = params.get("reason");
const reason =
reasonParam && VALID_REASON.has(reasonParam)
? (reasonParam as
| "USER_CANCELLATION"
| "ORGANIZER_CANCELLED"
| "TRIP_ISSUE"
| "ADMIN_ADJUSTMENT"
| "DISPUTE_RESOLVED")
: undefined;
const rows = await refundRepo.listByStatus(status, {
dateFrom: parseDate(params.get("dateFrom")),
dateTo: parseDate(params.get("dateTo")),
reviewerEmail: params.get("reviewer") || undefined,
reason,
});
const csv = buildCsv(
[
"Refund ID",
"Status",
"Reason",
"Nominal (IDR)",
"Dilaporkan oleh",
"Catatan laporan",
"Catatan admin",
"Dibuat",
"Reviewed at",
"Succeeded at",
"Failed at",
"Reviewer email",
"Booking ID",
"Peserta nama",
"Peserta email",
"Trip ID",
"Trip judul",
"Trip tanggal",
],
rows.map((r) => [
r.id,
r.status,
r.reason,
r.amount,
r.reportedBy,
r.reportNote,
r.adminNote ?? "",
csvDateJakarta(r.createdAt),
csvDateJakarta(r.reviewedAt),
csvDateJakarta(r.succeededAt),
csvDateJakarta(r.failedAt),
r.reviewedBy?.email ?? "",
r.booking.id,
r.booking.user.name,
r.booking.user.email,
r.booking.trip.id,
r.booking.trip.title,
csvDateJakarta(r.booking.trip.date),
])
);
const today = new Date().toISOString().slice(0, 10);
return csvResponse(`refunds-${status ?? "all"}-${today}.csv`, csv);
}
@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { isAdminEmail } from "@/lib/admin";
import { organizerRepo } from "@/server/repositories/organizer.repo";
import { buildCsv, csvDateJakarta, csvResponse } from "@/lib/csv";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const VALID_STATUS = new Set(["PENDING", "APPROVED", "REJECTED"]);
function parseDate(value: string | null): Date | undefined {
if (!value) return undefined;
const d = new Date(value);
return Number.isNaN(d.getTime()) ? undefined : d;
}
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user || !isAdminEmail(session.user.email)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const params = req.nextUrl.searchParams;
const statusParam = params.get("status");
const status =
statusParam && VALID_STATUS.has(statusParam)
? (statusParam as "PENDING" | "APPROVED" | "REJECTED")
: undefined;
const rows = await organizerRepo.listByStatus(status, {
dateFrom: parseDate(params.get("dateFrom")),
dateTo: parseDate(params.get("dateTo")),
reviewerEmail: params.get("reviewer") || undefined,
});
// SENGAJA tidak ekspor: NIK plaintext (encrypted), ktpImageKey, livenessKey,
// bankAccountNumber. Export ini hanya untuk metadata audit — KYC sensitive
// info tetap di DB & cuma diakses lewat admin UI dengan auth gate.
const csv = buildCsv(
[
"Verification ID",
"Status",
"Nama (KTP)",
"User nama",
"User email",
"Bank nama",
"Bank atas nama",
"Dibuat",
"Reviewed at",
"Verified at",
"Rejection reason",
"Reviewer email",
],
rows.map((v) => [
v.id,
v.status,
v.fullName,
v.user.name,
v.user.email,
v.bankName,
v.bankAccountName,
csvDateJakarta(v.createdAt),
csvDateJakarta(v.reviewedAt),
csvDateJakarta(v.verifiedAt),
v.rejectionReason ?? "",
v.reviewedBy?.email ?? "",
])
);
const today = new Date().toISOString().slice(0, 10);
return csvResponse(`verifications-${status ?? "all"}-${today}.csv`, csv);
}
+19
View File
@@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { isAdminEmail } from "@/lib/admin";
import { adminSearchService } from "@/server/services/admin-search.service";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user || !isAdminEmail(session.user.email)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const q = req.nextUrl.searchParams.get("q") ?? "";
const hits = await adminSearchService.resolve(q, 10);
return NextResponse.json({ hits });
}