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