From 8879453dcd1b9fdb86f993d5d02c9be12a9e18e6 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:52:40 +0100 Subject: [PATCH 01/30] feat(export): extend ExportType union to include kyc, fleet, users --- app/api/admin/reports/export/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index 797cd797..8a090460 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -12,7 +12,7 @@ import User from "@/models/User" import Vehicle from "@/models/Vehicle" import InvestmentPool from "@/models/InvestmentPool" -type ExportType = "deposits" | "investments" | "repayments" +type ExportType = "deposits" | "investments" | "repayments" | "kyc" | "fleet" | "users" type RangeType = "7d" | "30d" | "90d" | "all" | "custom" function csvEscape(value: unknown): string { From ae147a0158931f51a2cd58fa258e82d3da8eaecd Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:52:44 +0100 Subject: [PATCH 02/30] feat(export): update type-guard validation to accept kyc, fleet, users --- app/api/admin/reports/export/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index 8a090460..98adc8b1 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -97,7 +97,7 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url) const type = (searchParams.get("type") || "deposits") as ExportType - if (!["deposits", "investments", "repayments"].includes(type)) { + if (!["deposits", "investments", "repayments", "kyc", "fleet", "users"].includes(type)) { return NextResponse.json({ message: "Invalid export type" }, { status: 400 }) } From 01ab8431825e785da12c3af903b07896fa360951 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:52:48 +0100 Subject: [PATCH 03/30] feat(export): add kyc handler placeholder --- app/api/admin/reports/export/route.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index 98adc8b1..dff32463 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -228,6 +228,12 @@ export async function GET(request: Request) { return shouldRefreshSession ? withSessionRefresh(response, user) : response } + // ── kyc ────────────────────────────────────────────────────────────── + if (type === "kyc") { + return NextResponse.json({ message: "KYC export coming soon." }, { status: 501 }) + } + + const repayments = await DriverPayment.find({ status: "CONFIRMED", ...dateMatch("createdAt", startDate, endDate), From 0c4115d2285a0dd7afa7cb0468b82fb281627585 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:10 +0100 Subject: [PATCH 04/30] feat(export): extract kycStatusFilter from searchParams --- app/api/admin/reports/export/route.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index dff32463..13dc36c1 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -230,6 +230,7 @@ export async function GET(request: Request) { // ── kyc ────────────────────────────────────────────────────────────── if (type === "kyc") { + const kycStatusFilter = searchParams.get("status") || "" return NextResponse.json({ message: "KYC export coming soon." }, { status: 501 }) } From 7bd73b4d48ec9039f75059cb1f9a02805de25b31 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:13 +0100 Subject: [PATCH 05/30] feat(export): build kycQuery with status and date filters --- app/api/admin/reports/export/route.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index 13dc36c1..987b3253 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -231,6 +231,8 @@ export async function GET(request: Request) { // ── kyc ────────────────────────────────────────────────────────────── if (type === "kyc") { const kycStatusFilter = searchParams.get("status") || "" + const kycQuery: Record = { kycStatus: { $nin: ["none", null] }, ...dateMatch("createdAt", startDate, endDate) } + if (["pending", "approved", "rejected"].includes(kycStatusFilter)) kycQuery.kycStatus = kycStatusFilter return NextResponse.json({ message: "KYC export coming soon." }, { status: 501 }) } From 2f5683f14c5096f29f2ed0928a7d25e0b4b3026a Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:14 +0100 Subject: [PATCH 06/30] feat(export): add kycUsers DB query --- app/api/admin/reports/export/route.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index 987b3253..2ddedee1 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -233,6 +233,7 @@ export async function GET(request: Request) { const kycStatusFilter = searchParams.get("status") || "" const kycQuery: Record = { kycStatus: { $nin: ["none", null] }, ...dateMatch("createdAt", startDate, endDate) } if (["pending", "approved", "rejected"].includes(kycStatusFilter)) kycQuery.kycStatus = kycStatusFilter + const kycUsers = await User.find(kycQuery).select("name fullName email role kycStatus kycVerified createdAt").sort({ createdAt: -1 }).lean() return NextResponse.json({ message: "KYC export coming soon." }, { status: 501 }) } From 9efae40bec931562780fd7c16ee69749c289fdb7 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:16 +0100 Subject: [PATCH 07/30] feat(export): generate KYC CSV and return response --- app/api/admin/reports/export/route.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index 2ddedee1..d0224449 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -234,7 +234,15 @@ export async function GET(request: Request) { const kycQuery: Record = { kycStatus: { $nin: ["none", null] }, ...dateMatch("createdAt", startDate, endDate) } if (["pending", "approved", "rejected"].includes(kycStatusFilter)) kycQuery.kycStatus = kycStatusFilter const kycUsers = await User.find(kycQuery).select("name fullName email role kycStatus kycVerified createdAt").sort({ createdAt: -1 }).lean() - return NextResponse.json({ message: "KYC export coming soon." }, { status: 501 }) + const kycCsv = toCsv( + ["Date Joined", "Name", "Email", "Role", "KYC Status", "KYC Verified"], + kycUsers.map((u: any) => [ + u.createdAt ? new Date(u.createdAt).toISOString() : "", + getUserName(u), u.email || "", u.role || "", u.kycStatus || "none", u.kycVerified ? "Yes" : "No", + ]), + ) + const kycResponse = new NextResponse(kycCsv, { status: 200, headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="kyc-report-${range}.csv"` } }) + return shouldRefreshSession ? withSessionRefresh(kycResponse, user) : kycResponse } From 997302d6a4fc2888f8d0a52037218a864e44ef6e Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:18 +0100 Subject: [PATCH 08/30] feat(export): add fleet handler placeholder --- app/api/admin/reports/export/route.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index d0224449..9fd7c4e4 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -246,6 +246,11 @@ export async function GET(request: Request) { } + // ── fleet ───────────────────────────────────────────────────────────────── + if (type === "fleet") { + return NextResponse.json({ message: "Fleet export coming soon." }, { status: 501 }) + } + const repayments = await DriverPayment.find({ status: "CONFIRMED", ...dateMatch("createdAt", startDate, endDate), From 5c0582a8b331970c4c7dd62837b2f0cdd23a8b2a Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:20 +0100 Subject: [PATCH 09/30] =?UTF-8?q?feat(export):=20implement=20fleet=20handl?= =?UTF-8?q?er=20=E2=80=94=20query,=20CSV,=20and=20response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/admin/reports/export/route.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index 9fd7c4e4..47feb5fd 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -248,7 +248,16 @@ export async function GET(request: Request) { // ── fleet ───────────────────────────────────────────────────────────────── if (type === "fleet") { - return NextResponse.json({ message: "Fleet export coming soon." }, { status: 501 }) + const vStatusFilter = searchParams.get("vstatus") || "" + const vQuery: Record = {} + if (["Available", "Financed", "Reserved", "Maintenance", "Retired"].includes(vStatusFilter)) vQuery.status = vStatusFilter + const vehicles = await Vehicle.find(vQuery).select("name identifier type year price status fundingStatus totalFundedAmount addedDate").sort({ addedDate: -1 }).lean() + const fleetCsv = toCsv( + ["Date Added", "Name", "Identifier", "Type", "Year", "Price (NGN)", "Status", "Funding Status", "Total Funded (NGN)"], + vehicles.map((v: any) => [v.addedDate ? new Date(v.addedDate).toISOString() : "", v.name || "", v.identifier || "", v.type || "", v.year || "", Number(v.price || 0), v.status || "", v.fundingStatus || "", Number(v.totalFundedAmount || 0)]), + ) + const fleetResponse = new NextResponse(fleetCsv, { status: 200, headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="fleet-report-${range}.csv"` } }) + return shouldRefreshSession ? withSessionRefresh(fleetResponse, user) : fleetResponse } const repayments = await DriverPayment.find({ From 38795e8227d27c538f886ffddce26ca831f5512f Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:22 +0100 Subject: [PATCH 10/30] feat(export): add users handler placeholder --- app/api/admin/reports/export/route.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index 47feb5fd..a70f80c2 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -260,6 +260,11 @@ export async function GET(request: Request) { return shouldRefreshSession ? withSessionRefresh(fleetResponse, user) : fleetResponse } + // ── users ───────────────────────────────────────────────────────────────── + if (type === "users") { + return NextResponse.json({ message: "Users export coming soon." }, { status: 501 }) + } + const repayments = await DriverPayment.find({ status: "CONFIRMED", ...dateMatch("createdAt", startDate, endDate), From 8afcffa40a7081dfab579f34db676e1b1a044bb2 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:24 +0100 Subject: [PATCH 11/30] feat(export): add users query with role and date filters --- app/api/admin/reports/export/route.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index a70f80c2..52cc26f4 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -262,6 +262,10 @@ export async function GET(request: Request) { // ── users ───────────────────────────────────────────────────────────────── if (type === "users") { + const roleFilter = searchParams.get("role") || "" + const uQuery: Record = { ...dateMatch("createdAt", startDate, endDate) } + if (["driver", "investor", "admin"].includes(roleFilter)) uQuery.role = roleFilter + const platformUsers = await User.find(uQuery).select("name fullName email role kycVerified createdAt").sort({ createdAt: -1 }).lean() return NextResponse.json({ message: "Users export coming soon." }, { status: 501 }) } From f07bf4d5e2394a7f137043d0d1b03587df3c85ba Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:25 +0100 Subject: [PATCH 12/30] feat(export): generate users CSV and return response --- app/api/admin/reports/export/route.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index 52cc26f4..e0f1b66a 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -266,7 +266,12 @@ export async function GET(request: Request) { const uQuery: Record = { ...dateMatch("createdAt", startDate, endDate) } if (["driver", "investor", "admin"].includes(roleFilter)) uQuery.role = roleFilter const platformUsers = await User.find(uQuery).select("name fullName email role kycVerified createdAt").sort({ createdAt: -1 }).lean() - return NextResponse.json({ message: "Users export coming soon." }, { status: 501 }) + const usersCsv = toCsv( + ["Date Joined", "Name", "Email", "Role", "KYC Verified"], + platformUsers.map((u: any) => [u.createdAt ? new Date(u.createdAt).toISOString() : "", getUserName(u), u.email || "", u.role || "", u.kycVerified ? "Yes" : "No"]), + ) + const usersResponse = new NextResponse(usersCsv, { status: 200, headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="users-report-${range}.csv"` } }) + return shouldRefreshSession ? withSessionRefresh(usersResponse, user) : usersResponse } const repayments = await DriverPayment.find({ From b4b3e44c337eeb0284be86c5c105e2e5d6f535cf Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:27 +0100 Subject: [PATCH 13/30] style(export): normalise trailing newline --- app/api/admin/reports/export/route.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index e0f1b66a..a3975e8e 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -327,4 +327,3 @@ export async function GET(request: Request) { return NextResponse.json({ message: "Failed to export report." }, { status: 500 }) } } - From 8eb49a6f831b01617386c7f94f9704379e644431 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:28 +0100 Subject: [PATCH 14/30] feat(export): final export route with all 6 report types complete --- app/api/admin/reports/export/route.ts | 195 +++++++++++++++++--------- 1 file changed, 132 insertions(+), 63 deletions(-) diff --git a/app/api/admin/reports/export/route.ts b/app/api/admin/reports/export/route.ts index a3975e8e..f4f3911d 100644 --- a/app/api/admin/reports/export/route.ts +++ b/app/api/admin/reports/export/route.ts @@ -104,6 +104,7 @@ export async function GET(request: Request) { const range = parseRange(searchParams.get("range")) const { startDate, endDate } = buildWindow(range, searchParams.get("from"), searchParams.get("to")) + // ── deposits ──────────────────────────────────────────────────────────── if (type === "deposits") { const deposits = await Transaction.find({ type: { $in: ["deposit", "wallet_funding"] }, @@ -146,6 +147,7 @@ export async function GET(request: Request) { return shouldRefreshSession ? withSessionRefresh(response, user) : response } + // ── investments ────────────────────────────────────────────────────────── if (type === "investments") { const [poolInvestments, legacyInvestments] = await Promise.all([ PoolInvestment.find({ @@ -228,97 +230,164 @@ export async function GET(request: Request) { return shouldRefreshSession ? withSessionRefresh(response, user) : response } - // ── kyc ────────────────────────────────────────────────────────────── + // ── repayments ─────────────────────────────────────────────────────────── + if (type === "repayments") { + const repayments = await DriverPayment.find({ + status: "CONFIRMED", + ...dateMatch("createdAt", startDate, endDate), + }) + .select("driverUserId contractId amountNgn appliedAmountNgn method paystackRef status createdAt") + .sort({ createdAt: -1 }) + .lean() + + const userIds = collectObjectIds(repayments.map((item: any) => item.driverUserId)) + const contractIds = collectObjectIds(repayments.map((item: any) => item.contractId)) + + const [users, contracts] = await Promise.all([ + userIds.length ? User.find({ _id: { $in: userIds } }).select("name fullName email").lean() : Promise.resolve([]), + contractIds.length + ? HirePurchaseContract.find({ _id: { $in: contractIds } }).select("vehicleDisplayName").lean() + : Promise.resolve([]), + ]) + + const userById = new Map(users.map((entry: any) => [entry._id.toString(), entry])) + const contractById = new Map(contracts.map((entry: any) => [entry._id.toString(), entry])) + + const csv = toCsv( + ["Date", "Driver", "Email", "Vehicle/Contract", "Amount (NGN)", "Applied Amount (NGN)", "Method", "Reference", "Status"], + repayments.map((item: any) => { + const userEntry = userById.get(item.driverUserId?.toString?.() || "") + const contract = contractById.get(item.contractId?.toString?.() || "") + return [ + item.createdAt ? new Date(item.createdAt).toISOString() : "", + getUserName(userEntry), + userEntry?.email || "", + contract?.vehicleDisplayName || "Contract", + Number(item.amountNgn || 0), + Number(item.appliedAmountNgn || 0), + item.method || "PAYSTACK", + item.paystackRef || "", + item.status || "unknown", + ] + }), + ) + + const response = new NextResponse(csv, { + status: 200, + headers: { + "Content-Type": "text/csv; charset=utf-8", + "Content-Disposition": `attachment; filename="repayments-${range}.csv"`, + }, + }) + return shouldRefreshSession ? withSessionRefresh(response, user) : response + } + + // ── kyc ────────────────────────────────────────────────────────────────── if (type === "kyc") { const kycStatusFilter = searchParams.get("status") || "" - const kycQuery: Record = { kycStatus: { $nin: ["none", null] }, ...dateMatch("createdAt", startDate, endDate) } - if (["pending", "approved", "rejected"].includes(kycStatusFilter)) kycQuery.kycStatus = kycStatusFilter - const kycUsers = await User.find(kycQuery).select("name fullName email role kycStatus kycVerified createdAt").sort({ createdAt: -1 }).lean() - const kycCsv = toCsv( + const kycQuery: Record = { + kycStatus: { $ne: "none" }, + ...dateMatch("createdAt", startDate, endDate), + } + if (["pending", "approved", "rejected"].includes(kycStatusFilter)) { + kycQuery.kycStatus = kycStatusFilter + } + + const kycUsers = await User.find(kycQuery) + .select("name fullName email role kycStatus kycVerified createdAt") + .sort({ createdAt: -1 }) + .lean() + + const csv = toCsv( ["Date Joined", "Name", "Email", "Role", "KYC Status", "KYC Verified"], kycUsers.map((u: any) => [ u.createdAt ? new Date(u.createdAt).toISOString() : "", - getUserName(u), u.email || "", u.role || "", u.kycStatus || "none", u.kycVerified ? "Yes" : "No", + getUserName(u), + u.email || "", + u.role || "", + u.kycStatus || "none", + u.kycVerified ? "Yes" : "No", ]), ) - const kycResponse = new NextResponse(kycCsv, { status: 200, headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="kyc-report-${range}.csv"` } }) - return shouldRefreshSession ? withSessionRefresh(kycResponse, user) : kycResponse - } + const response = new NextResponse(csv, { + status: 200, + headers: { + "Content-Type": "text/csv; charset=utf-8", + "Content-Disposition": `attachment; filename="kyc-report-${range}.csv"`, + }, + }) + return shouldRefreshSession ? withSessionRefresh(response, user) : response + } // ── fleet ───────────────────────────────────────────────────────────────── if (type === "fleet") { - const vStatusFilter = searchParams.get("vstatus") || "" - const vQuery: Record = {} - if (["Available", "Financed", "Reserved", "Maintenance", "Retired"].includes(vStatusFilter)) vQuery.status = vStatusFilter - const vehicles = await Vehicle.find(vQuery).select("name identifier type year price status fundingStatus totalFundedAmount addedDate").sort({ addedDate: -1 }).lean() - const fleetCsv = toCsv( + const vehicleStatusFilter = searchParams.get("vstatus") || "" + const vehicleQuery: Record = {} + if (["Available", "Financed", "Reserved", "Maintenance", "Retired"].includes(vehicleStatusFilter)) { + vehicleQuery.status = vehicleStatusFilter + } + + const vehicles = await Vehicle.find(vehicleQuery) + .select("name identifier type year price status fundingStatus totalFundedAmount addedDate") + .sort({ addedDate: -1 }) + .lean() + + const csv = toCsv( ["Date Added", "Name", "Identifier", "Type", "Year", "Price (NGN)", "Status", "Funding Status", "Total Funded (NGN)"], - vehicles.map((v: any) => [v.addedDate ? new Date(v.addedDate).toISOString() : "", v.name || "", v.identifier || "", v.type || "", v.year || "", Number(v.price || 0), v.status || "", v.fundingStatus || "", Number(v.totalFundedAmount || 0)]), + vehicles.map((v: any) => [ + v.addedDate ? new Date(v.addedDate).toISOString() : "", + v.name || "", + v.identifier || "", + v.type || "", + v.year || "", + Number(v.price || 0), + v.status || "", + v.fundingStatus || "", + Number(v.totalFundedAmount || 0), + ]), ) - const fleetResponse = new NextResponse(fleetCsv, { status: 200, headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="fleet-report-${range}.csv"` } }) - return shouldRefreshSession ? withSessionRefresh(fleetResponse, user) : fleetResponse + + const response = new NextResponse(csv, { + status: 200, + headers: { + "Content-Type": "text/csv; charset=utf-8", + "Content-Disposition": `attachment; filename="fleet-report-${range}.csv"`, + }, + }) + return shouldRefreshSession ? withSessionRefresh(response, user) : response } // ── users ───────────────────────────────────────────────────────────────── - if (type === "users") { - const roleFilter = searchParams.get("role") || "" - const uQuery: Record = { ...dateMatch("createdAt", startDate, endDate) } - if (["driver", "investor", "admin"].includes(roleFilter)) uQuery.role = roleFilter - const platformUsers = await User.find(uQuery).select("name fullName email role kycVerified createdAt").sort({ createdAt: -1 }).lean() - const usersCsv = toCsv( - ["Date Joined", "Name", "Email", "Role", "KYC Verified"], - platformUsers.map((u: any) => [u.createdAt ? new Date(u.createdAt).toISOString() : "", getUserName(u), u.email || "", u.role || "", u.kycVerified ? "Yes" : "No"]), - ) - const usersResponse = new NextResponse(usersCsv, { status: 200, headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="users-report-${range}.csv"` } }) - return shouldRefreshSession ? withSessionRefresh(usersResponse, user) : usersResponse + const roleFilter = searchParams.get("role") || "" + const userQuery: Record = { + ...dateMatch("createdAt", startDate, endDate), + } + if (["driver", "investor", "admin"].includes(roleFilter)) { + userQuery.role = roleFilter } - const repayments = await DriverPayment.find({ - status: "CONFIRMED", - ...dateMatch("createdAt", startDate, endDate), - }) - .select("driverUserId contractId amountNgn appliedAmountNgn method paystackRef status createdAt") + const platformUsers = await User.find(userQuery) + .select("name fullName email role kycVerified createdAt") .sort({ createdAt: -1 }) .lean() - const userIds = collectObjectIds(repayments.map((item: any) => item.driverUserId)) - const contractIds = collectObjectIds(repayments.map((item: any) => item.contractId)) - - const [users, contracts] = await Promise.all([ - userIds.length ? User.find({ _id: { $in: userIds } }).select("name fullName email").lean() : Promise.resolve([]), - contractIds.length - ? HirePurchaseContract.find({ _id: { $in: contractIds } }).select("vehicleDisplayName").lean() - : Promise.resolve([]), - ]) - - const userById = new Map(users.map((entry: any) => [entry._id.toString(), entry])) - const contractById = new Map(contracts.map((entry: any) => [entry._id.toString(), entry])) - const csv = toCsv( - ["Date", "Driver", "Email", "Vehicle/Contract", "Amount (NGN)", "Applied Amount (NGN)", "Method", "Reference", "Status"], - repayments.map((item: any) => { - const userEntry = userById.get(item.driverUserId?.toString?.() || "") - const contract = contractById.get(item.contractId?.toString?.() || "") - return [ - item.createdAt ? new Date(item.createdAt).toISOString() : "", - getUserName(userEntry), - userEntry?.email || "", - contract?.vehicleDisplayName || "Contract", - Number(item.amountNgn || 0), - Number(item.appliedAmountNgn || 0), - item.method || "PAYSTACK", - item.paystackRef || "", - item.status || "unknown", - ] - }), + ["Date Joined", "Name", "Email", "Role", "KYC Verified"], + platformUsers.map((u: any) => [ + u.createdAt ? new Date(u.createdAt).toISOString() : "", + getUserName(u), + u.email || "", + u.role || "", + u.kycVerified ? "Yes" : "No", + ]), ) const response = new NextResponse(csv, { status: 200, headers: { "Content-Type": "text/csv; charset=utf-8", - "Content-Disposition": `attachment; filename="repayments-${range}.csv"`, + "Content-Disposition": `attachment; filename="users-report-${range}.csv"`, }, }) return shouldRefreshSession ? withSessionRefresh(response, user) : response From ab59d3a0a9737c29ffe8ba9030d4ab3376b071fb Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:30 +0100 Subject: [PATCH 15/30] feat(reports): add Printer icon import and ReportTab type --- app/dashboard/admin/reports/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index df9e11dd..55410aff 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -1,5 +1,5 @@ import Link from "next/link" -import { Download } from "lucide-react" +import { Download, Printer } from "lucide-react" import { PageHeader } from "@/components/dashboard/admin/page-header" import { Button } from "@/components/ui/button" @@ -21,6 +21,7 @@ interface ReportsPageProps { } type ReportRange = "7d" | "30d" | "90d" | "all" | "custom" +type ReportTab = "overview" | "kyc" | "fleet" | "users" function getParam(value: string | string[] | undefined, fallback = "") { if (Array.isArray(value)) return value[0] ?? fallback From 2f88845baf4b85efcdd71a9a07535efb031867e8 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:32 +0100 Subject: [PATCH 16/30] feat(reports): add hrefForTab URL helper --- app/dashboard/admin/reports/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 55410aff..ec686c5e 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -65,6 +65,17 @@ function hrefForRange(range: ReportRange, from: string, to: string) { return `/dashboard/admin/reports?${params.toString()}` } +function hrefForTab(tab: ReportTab, range: ReportRange, from: string, to: string) { + const params = new URLSearchParams() + params.set("tab", tab) + params.set("range", range) + if (range === "custom") { + if (from) params.set("from", from) + if (to) params.set("to", to) + } + return `/dashboard/admin/reports?${params.toString()}` +} + export default async function AdminReportsPage({ searchParams }: ReportsPageProps) { await requireAdminAccess() await dbConnect() From e00d5608fdb8e1bd109c3c97c9021e70a89a37f7 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:34 +0100 Subject: [PATCH 17/30] feat(reports): extract tab URL param in page component --- app/dashboard/admin/reports/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index ec686c5e..197154d1 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -85,6 +85,8 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp const range: ReportRange = ["7d", "30d", "90d", "all", "custom"].includes(rawRange) ? (rawRange as ReportRange) : "30d" const from = getParam(resolved.from) const to = getParam(resolved.to) + const rawTab = getParam(resolved.tab, "overview") + const tab: ReportTab = ["overview", "kyc", "fleet", "users"].includes(rawTab) ? (rawTab as ReportTab) : "overview" const { startDate, endDate } = buildWindow(range, from, to) const txDateMatch = buildDateMatch("timestamp", startDate, endDate) From f64d0023e847a53abb2b913db591aaa140c405ed Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:35 +0100 Subject: [PATCH 18/30] feat(reports): extract per-tab filter params (status, vstatus, role) --- app/dashboard/admin/reports/page.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 197154d1..ae63daf9 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -87,6 +87,9 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp const to = getParam(resolved.to) const rawTab = getParam(resolved.tab, "overview") const tab: ReportTab = ["overview", "kyc", "fleet", "users"].includes(rawTab) ? (rawTab as ReportTab) : "overview" + const kycStatusFilter = getParam(resolved.status) + const vehicleStatusFilter = getParam(resolved.vstatus) + const roleFilter = getParam(resolved.role) const { startDate, endDate } = buildWindow(range, from, to) const txDateMatch = buildDateMatch("timestamp", startDate, endDate) From abdd61fe1d2a9a05a16c2515e0c7318be4e3db30 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:37 +0100 Subject: [PATCH 19/30] feat(reports): add userDateMatch for user-scoped date range queries --- app/dashboard/admin/reports/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index ae63daf9..33d6f4ab 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -97,6 +97,7 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp const legacyInvestDateMatch = buildDateMatch("date", startDate, endDate) const paymentDateMatch = buildDateMatch("createdAt", startDate, endDate) const poolsDateMatch = buildDateMatch("createdAt", startDate, endDate) + const userDateMatch = buildDateMatch("createdAt", startDate, endDate) const [depositsAgg, poolInvestAgg, legacyInvestAgg, repaymentsAgg, creditsAgg, poolSummary] = await Promise.all([ Transaction.aggregate([ From 3ecc0e018583983e6a94cef4c0e03aa5feaf2c22 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:38 +0100 Subject: [PATCH 20/30] feat(reports): add User and Vehicle model imports --- app/dashboard/admin/reports/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 33d6f4ab..c478a834 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -12,6 +12,8 @@ import InvestmentPool from "@/models/InvestmentPool" import InvestorCredit from "@/models/InvestorCredit" import PoolInvestment from "@/models/PoolInvestment" import Transaction from "@/models/Transaction" +import User from "@/models/User" +import Vehicle from "@/models/Vehicle" import { requireAdminAccess } from "@/src/server/admin/require-admin" export const dynamic = "force-dynamic" From 2c7286bca388035f41c9e9213b74054dad85cbc3 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:41 +0100 Subject: [PATCH 21/30] feat(reports): declare tab-specific data variables with empty defaults --- app/dashboard/admin/reports/page.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index c478a834..5326b212 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -101,6 +101,14 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp const poolsDateMatch = buildDateMatch("createdAt", startDate, endDate) const userDateMatch = buildDateMatch("createdAt", startDate, endDate) + // Tab-specific data + let kycStatusCounts: Array<{ _id: string; count: number }> = [] + let recentKyc: any[] = [] + let vehicleStatusCounts: Array<{ _id: string; count: number }> = [] + let recentVehicles: any[] = [] + let userRoleCounts: Array<{ _id: string; count: number }> = [] + let recentUsers: any[] = [] + const [depositsAgg, poolInvestAgg, legacyInvestAgg, repaymentsAgg, creditsAgg, poolSummary] = await Promise.all([ Transaction.aggregate([ { From c95a33db3f3d5ccf3d941414ee449e5f116479cb Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:43 +0100 Subject: [PATCH 22/30] feat(reports): fetch KYC status counts and recent KYC records when tab=kyc --- app/dashboard/admin/reports/page.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 5326b212..76c364ec 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -164,6 +164,15 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp const fundedPools = Number(poolSummary.find((entry: any) => String(entry._id).toUpperCase() === "FUNDED")?.count || 0) const activePools = openPools + fundedPools + if (tab === "kyc") { + const kycQuery: Record = { kycStatus: { $nin: ["none", null] } } + if (["pending", "approved", "rejected"].includes(kycStatusFilter)) kycQuery.kycStatus = kycStatusFilter + ;[kycStatusCounts, recentKyc] = await Promise.all([ + User.aggregate([{ $match: { kycStatus: { $nin: ["none", null] } } }, { $group: { _id: "$kycStatus", count: { $sum: 1 } } }]), + User.find(kycQuery).select("name fullName email role kycStatus kycVerified createdAt").sort({ createdAt: -1 }).limit(15).lean(), + ]) + } + const exportQuery = new URLSearchParams() exportQuery.set("range", range) if (range === "custom") { From 36ede46a5f98e2a210a2860ab7ca5fabe6cb389d Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:45 +0100 Subject: [PATCH 23/30] feat(reports): fetch fleet status counts and vehicles when tab=fleet --- app/dashboard/admin/reports/page.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 76c364ec..0d34e2b0 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -164,6 +164,15 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp const fundedPools = Number(poolSummary.find((entry: any) => String(entry._id).toUpperCase() === "FUNDED")?.count || 0) const activePools = openPools + fundedPools + if (tab === "fleet") { + const vQuery: Record = {} + if (["Available", "Financed", "Reserved", "Maintenance", "Retired"].includes(vehicleStatusFilter)) vQuery.status = vehicleStatusFilter + ;[vehicleStatusCounts, recentVehicles] = await Promise.all([ + Vehicle.aggregate([{ $group: { _id: "$status", count: { $sum: 1 } } }]), + Vehicle.find(vQuery).select("name identifier type year price status fundingStatus totalFundedAmount addedDate").sort({ addedDate: -1 }).limit(15).lean(), + ]) + } + if (tab === "kyc") { const kycQuery: Record = { kycStatus: { $nin: ["none", null] } } if (["pending", "approved", "rejected"].includes(kycStatusFilter)) kycQuery.kycStatus = kycStatusFilter From 8f7737ff454bfb294b2d38db0c71074d398c3c0e Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:48 +0100 Subject: [PATCH 24/30] feat(reports): fetch user role counts and recent users when tab=users --- app/dashboard/admin/reports/page.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 0d34e2b0..52e7e27a 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -164,6 +164,15 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp const fundedPools = Number(poolSummary.find((entry: any) => String(entry._id).toUpperCase() === "FUNDED")?.count || 0) const activePools = openPools + fundedPools + if (tab === "users") { + const uQuery: Record = { ...userDateMatch } + if (["driver", "investor", "admin"].includes(roleFilter)) uQuery.role = roleFilter + ;[userRoleCounts, recentUsers] = await Promise.all([ + User.aggregate([{ $group: { _id: "$role", count: { $sum: 1 } } }]), + User.find(uQuery).select("name fullName email role kycVerified createdAt").sort({ createdAt: -1 }).limit(15).lean(), + ]) + } + if (tab === "fleet") { const vQuery: Record = {} if (["Available", "Financed", "Reserved", "Maintenance", "Retired"].includes(vehicleStatusFilter)) vQuery.status = vehicleStatusFilter From 6c2f20db2c08168471a348e34461c11c5054a742 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:50 +0100 Subject: [PATCH 25/30] feat(reports): add Badge and Table component imports --- app/dashboard/admin/reports/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 52e7e27a..01751c23 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -2,7 +2,9 @@ import Link from "next/link" import { Download, Printer } from "lucide-react" import { PageHeader } from "@/components/dashboard/admin/page-header" +import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { formatNaira } from "@/lib/currency" import dbConnect from "@/lib/dbConnect" From ca5cb3c05f444bcf3012ecabba949d739f45d02c Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:51 +0100 Subject: [PATCH 26/30] feat(reports): add kycBadgeVariant and vehicleStatusVariant helpers --- app/dashboard/admin/reports/page.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 01751c23..4e102b0a 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -80,6 +80,20 @@ function hrefForTab(tab: ReportTab, range: ReportRange, from: string, to: string return `/dashboard/admin/reports?${params.toString()}` } +function kycBadgeVariant(status: string) { + if (status === "approved") return "default" + if (status === "rejected") return "destructive" + if (status === "pending") return "secondary" + return "outline" +} + +function vehicleStatusVariant(status: string) { + if (status === "Available") return "default" + if (status === "Financed" || status === "Reserved") return "secondary" + if (status === "Maintenance") return "destructive" + return "outline" +} + export default async function AdminReportsPage({ searchParams }: ReportsPageProps) { await requireAdminAccess() await dbConnect() From 70b157b66683f41db7115b50fff368eeb5ad1980 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:53 +0100 Subject: [PATCH 27/30] feat(reports): update PageHeader actions with tab-aware form and Print button --- app/dashboard/admin/reports/page.tsx | 43 ++++++++++------------------ 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 4e102b0a..42bd87f1 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -220,34 +220,21 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp title="Reports" subtitle="Platform reporting hub and exports." actions={ -
- - - - -
+
+
+ + + + + +
+
} /> From 39edcb2ad9546450d05276722fec9b7000756b09 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:55 +0100 Subject: [PATCH 28/30] feat(reports): add tab navigation links to page --- app/dashboard/admin/reports/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index 42bd87f1..fbe33ea7 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -253,6 +253,17 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp +
+ {(["overview", "kyc", "fleet", "users"] as const).map((t) => ( + + {t === "overview" ? "Overview" : t === "kyc" ? "KYC" : t === "fleet" ? "Fleet" : "Users"} + + ))} +
+
From a46af7589c701d6a8158c2a35f622386958a52eb Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:57 +0100 Subject: [PATCH 29/30] feat(reports): wire tab param through range quick-links --- app/dashboard/admin/reports/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/dashboard/admin/reports/page.tsx b/app/dashboard/admin/reports/page.tsx index fbe33ea7..8b545d7f 100644 --- a/app/dashboard/admin/reports/page.tsx +++ b/app/dashboard/admin/reports/page.tsx @@ -240,16 +240,16 @@ export default async function AdminReportsPage({ searchParams }: ReportsPageProp
From bedb80875099a0c58d0cdff093b1a898ec5893c9 Mon Sep 17 00:00:00 2001 From: Jaydbrown Date: Sat, 20 Jun 2026 18:54:58 +0100 Subject: [PATCH 30/30] chore(reports): update loading page description to reflect expanded report scope --- app/dashboard/admin/reports/loading.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/dashboard/admin/reports/loading.tsx b/app/dashboard/admin/reports/loading.tsx index 49884574..efa9b58b 100644 --- a/app/dashboard/admin/reports/loading.tsx +++ b/app/dashboard/admin/reports/loading.tsx @@ -1,5 +1,5 @@ import { DashboardRouteLoading } from "@/components/dashboard/dashboard-route-loading" export default function Loading() { - return -} + return +}