import { requireOwner, withCors, preflight } from "@/lib/admin"; import { query, type SqlValue } from "@/lib/db"; import { RIDE_STATUSES as LIFECYCLE_STATUSES } from "@/lib/ride-lifecycle"; const PAGE_SIZE = 25; // Lowercased for comparison against the `status` query param. const RIDE_STATUSES: readonly string[] = LIFECYCLE_STATUSES; // LEFT JOIN on drivers, deliberately. // // This was an INNER JOIN, which meant every ride without a driver was missing // from the admin list entirely — a rider cancelling before a match, or a // request that expired with nobody available, simply never appeared. Those are // exactly the rides an operator needs to see: they're the ones that went // wrong. const SELECT_RIDES = ` SELECT r.ride_id, r.origin_address, r.destination_address, r.ride_time, r.fare_price, r.payment_status, r.status, r.cancelled_by, r.cancellation_reason, r.platform_fee_cents, r.driver_payout_cents, r.commission_rate, r.platform_fee_settled_at, r.driver_payout_settled_at, r.settlement_note, r.created_at, r.completed_at, u.id AS user_id, u.email AS user_email, CASE WHEN d.id IS NULL THEN NULL ELSE json_build_object( 'driver_id', d.id, 'name', d.first_name || ' ' || d.last_name, 'rating', d.rating ) END AS driver FROM rides r LEFT JOIN drivers d ON d.id = r.driver_id INNER JOIN users u ON u.id = r.user_id `; export async function OPTIONS(request: Request) { return preflight(request); } export async function GET(request: Request) { const auth = await requireOwner(request); if ("error" in auth) return withCors(request, auth.error); try { const url = new URL(request.url); const status = url.searchParams.get("status")?.trim().toLowerCase() ?? ""; const q = url.searchParams.get("q")?.trim() ?? ""; const page = Math.max(1, Number(url.searchParams.get("page")) || 1); const conds: string[] = []; const params: SqlValue[] = []; // `status` filters the ride's own lifecycle state when it names one, and // falls back to the payment status otherwise — so the existing "paid" / // "cash" filters keep working while "cancelled" and "completed" become // filterable too, which is what an operator actually reaches for. if (status) { params.push(status); const n = params.length; conds.push( RIDE_STATUSES.includes(status) ? `LOWER(r.status) = $${n}` : `LOWER(r.payment_status) = $${n}`, ); } if (q) { params.push(`%${q}%`); const n = params.length; conds.push( `(u.email ILIKE $${n} OR (d.first_name || ' ' || d.last_name) ILIKE $${n} OR ` + `r.origin_address ILIKE $${n} OR r.destination_address ILIKE $${n})`, ); } const where = conds.length ? ` WHERE ${conds.join(" AND ")}` : ""; const [{ count }] = await query<{ count: number }>( `SELECT COUNT(*)::int AS count FROM rides r LEFT JOIN drivers d ON d.id = r.driver_id INNER JOIN users u ON u.id = r.user_id${where}`, params, ); const rows = await query( `${SELECT_RIDES}${where} ORDER BY r.created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, PAGE_SIZE, (page - 1) * PAGE_SIZE], ); return withCors(request, Response.json({ data: rows, total: count, page, pageSize: PAGE_SIZE, pages: Math.max(1, Math.ceil(count / PAGE_SIZE)), }), ); } catch (error) { console.error("[ADMIN_RIDES]: ", error); return withCors(request, Response.json({ error: "Internal Server Error" }, { status: 500 }), ); } }