Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/kyc-documents/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export async function GET(request: Request) {
body = Buffer.from(await upstreamResponse.arrayBuffer())
}

const response = new NextResponse(body, {
const response = new NextResponse(body as any, {
status: 200,
headers: {
"Cache-Control": "private, no-store, max-age=0",
Expand Down
4 changes: 2 additions & 2 deletions app/api/users/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export async function PUT(request: Request, { params }: RouteContext) {
return NextResponse.json({ message: "No user changes were provided." }, { status: 400 })
}

if (params.id === auth.user._id.toString() && hasRole && role !== "admin") {
if (params.id === auth.user!._id.toString() && hasRole && role !== "admin") {
return NextResponse.json({ message: "You cannot remove your own admin access." }, { status: 403 })
}

Expand Down Expand Up @@ -294,7 +294,7 @@ export async function DELETE(request: Request, { params }: RouteContext) {

await dbConnect()

if (params.id === auth.user._id.toString()) {
if (params.id === auth.user!._id.toString()) {
return NextResponse.json({ message: "You cannot delete your own account." }, { status: 403 })
}

Expand Down
3 changes: 1 addition & 2 deletions app/dashboard/admin/admincomponents/MetricsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@

import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { LucideIcon } from "lucide-react";

interface MetricsCardProps {
title: string;
value: string;
description: string;
icon: LucideIcon;
icon: React.ComponentType<{ className?: string }>;
gradient: string;
}

Expand Down
2 changes: 1 addition & 1 deletion app/dashboard/admin/adminfunctions/useAdminDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export const useAdminDashboard = () => {
isTermSet: false, // New field
};

await addVehicle(vehicleData as Omit<Vehicle, '_id'>);
await addVehicle(vehicleData as unknown as Omit<Vehicle, '_id'>);

toast({
title: "Vehicle Added",
Expand Down
252 changes: 252 additions & 0 deletions app/dashboard/admin/fleet-operations/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import Link from "next/link"
import { notFound } from "next/navigation"
import mongoose from "mongoose"
import { ArrowLeft } from "lucide-react"

import { MetricCard } from "@/components/dashboard/admin/metric-card"
import { PageHeader } from "@/components/dashboard/admin/page-header"
import { RepaymentBar } from "@/components/dashboard/admin/repayment-bar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { cn } from "@/lib/utils"
import { formatNaira, formatPercent } from "@/lib/currency"
import dbConnect from "@/lib/dbConnect"
import DriverPayment from "@/models/DriverPayment"
import HirePurchaseContract from "@/models/HirePurchaseContract"
import Vehicle from "@/models/Vehicle"
import { requireAdminAccess } from "@/src/server/admin/require-admin"
import {
contractStatusBadgeClass,
normalizeVehicleStatus,
pickOperationalContract,
repaymentPercent,
vehicleStatusBadgeClass,
} from "@/src/server/admin/fleet"

export const dynamic = "force-dynamic"

interface VehicleDetailPageProps {
params: Promise<{ id: string }>
}

function formatDate(value?: Date | string | null) {
if (!value) return "—"
const date = new Date(value)
if (Number.isNaN(date.getTime())) return "—"
return date.toLocaleDateString("en-NG", { day: "2-digit", month: "short", year: "numeric" })
}

function driverLabel(driver: any) {
if (!driver) return "Unassigned"
return driver.fullName || driver.name || driver.email || "Unnamed driver"
}

function paymentStatusBadgeClass(status?: string | null) {
const value = (status || "").toUpperCase()
if (value === "CONFIRMED") return "bg-green-600 text-white hover:bg-green-600"
if (value === "FAILED") return "bg-red-600 text-white hover:bg-red-600"
return "bg-amber-600 text-white hover:bg-amber-600"
}

const SPEC_FIELDS: Array<{ key: string; label: string }> = [
{ key: "engine", label: "Engine" },
{ key: "fuelType", label: "Fuel Type" },
{ key: "transmission", label: "Transmission" },
{ key: "mileage", label: "Mileage" },
{ key: "color", label: "Color" },
{ key: "vin", label: "VIN" },
]

export default async function AdminVehicleDetailPage({ params }: VehicleDetailPageProps) {
await requireAdminAccess()

const { id } = await params
if (!mongoose.Types.ObjectId.isValid(id)) {
notFound()
}

await dbConnect()

const vehicle = await Vehicle.findById(id).populate("driverId", "name fullName email").lean<any>()
if (!vehicle) {
notFound()
}

const contractList: any[] = await HirePurchaseContract.find({ vehicleDisplayName: vehicle.name })
.sort({ createdAt: -1 })
.lean()
const contract = pickOperationalContract(contractList)

const payments = contract
? await DriverPayment.find({ contractId: contract._id })
.select("amountNgn appliedAmountNgn method paystackRef status confirmedAt createdAt")
.sort({ createdAt: -1 })
.limit(10)
.lean()
: []

const statusLabel = normalizeVehicleStatus(vehicle.status)
const percent = contract ? repaymentPercent(contract.totalPaidNgn, contract.totalPayableNgn) : null
const specifications = (vehicle.specifications || {}) as Record<string, string | undefined>

return (
<div className="space-y-5">
<PageHeader
title={vehicle.name}
subtitle={`${vehicle.type || "Vehicle"} · ${vehicle.identifier || specifications.vin || "No identifier"}`}
actions={
<Button asChild variant="outline" size="sm" className="h-9">
<Link href="/dashboard/admin/fleet-operations">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to fleet
</Link>
</Button>
}
/>

<section className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<MetricCard
label="Status"
value={
<Badge variant="default" className={cn("text-sm", vehicleStatusBadgeClass(statusLabel))}>
{statusLabel}
</Badge>
}
/>
<MetricCard label="Assigned Driver" value={<span className="text-lg">{driverLabel(vehicle.driverId)}</span>} />
<MetricCard
label="Repayment"
value={percent !== null ? formatPercent(percent, 0) : "—"}
hint={contract ? `Contract ${contract.status}` : "No active contract"}
/>
<MetricCard label="Vehicle Value" value={formatNaira(Number(vehicle.price || 0))} hint={`Year ${vehicle.year || "—"}`} />
</section>

<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
{/* Specifications */}
<Card className="border-border/70">
<CardHeader>
<CardTitle className="text-base">Specifications</CardTitle>
</CardHeader>
<CardContent>
<dl className="grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2">
{SPEC_FIELDS.map((field) => (
<div key={field.key} className="flex flex-col">
<dt className="text-xs uppercase tracking-wide text-muted-foreground">{field.label}</dt>
<dd className="text-sm text-foreground">{specifications[field.key] || "Not provided"}</dd>
</div>
))}
</dl>
</CardContent>
</Card>

{/* Contract summary */}
<Card className="border-border/70">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0">
<CardTitle className="text-base">Contract Summary</CardTitle>
{contract ? (
<Badge variant="default" className={cn(contractStatusBadgeClass(contract.status))}>
{contract.status}
</Badge>
) : null}
</CardHeader>
<CardContent>
{contract ? (
<div className="space-y-4">
{percent !== null ? <RepaymentBar percent={percent} status={contract.status} /> : null}
<dl className="grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2">
<div className="flex flex-col">
<dt className="text-xs uppercase tracking-wide text-muted-foreground">Principal</dt>
<dd className="text-sm text-foreground">{formatNaira(Number(contract.principalNgn || 0))}</dd>
</div>
<div className="flex flex-col">
<dt className="text-xs uppercase tracking-wide text-muted-foreground">Deposit</dt>
<dd className="text-sm text-foreground">{formatNaira(Number(contract.depositNgn || 0))}</dd>
</div>
<div className="flex flex-col">
<dt className="text-xs uppercase tracking-wide text-muted-foreground">Total Payable</dt>
<dd className="text-sm text-foreground">{formatNaira(Number(contract.totalPayableNgn || 0))}</dd>
</div>
<div className="flex flex-col">
<dt className="text-xs uppercase tracking-wide text-muted-foreground">Total Paid</dt>
<dd className="text-sm text-foreground">{formatNaira(Number(contract.totalPaidNgn || 0))}</dd>
</div>
<div className="flex flex-col">
<dt className="text-xs uppercase tracking-wide text-muted-foreground">Weekly Payment</dt>
<dd className="text-sm text-foreground">{formatNaira(Number(contract.weeklyPaymentNgn || 0))}</dd>
</div>
<div className="flex flex-col">
<dt className="text-xs uppercase tracking-wide text-muted-foreground">Duration</dt>
<dd className="text-sm text-foreground">{contract.durationWeeks || 0} weeks</dd>
</div>
<div className="flex flex-col">
<dt className="text-xs uppercase tracking-wide text-muted-foreground">Start Date</dt>
<dd className="text-sm text-foreground">{formatDate(contract.startDate)}</dd>
</div>
<div className="flex flex-col">
<dt className="text-xs uppercase tracking-wide text-muted-foreground">Next Due</dt>
<dd className="text-sm text-foreground">{formatDate(contract.nextDueDate)}</dd>
</div>
</dl>
</div>
) : (
<p className="py-8 text-center text-sm text-muted-foreground">
No hire-purchase contract is linked to this vehicle yet.
</p>
)}
</CardContent>
</Card>
</div>

{/* Payment history */}
<Card className="border-border/70">
<CardHeader>
<CardTitle className="text-base">Recent Repayments</CardTitle>
</CardHeader>
<CardContent className="px-0">
<div className="overflow-auto">
<table className="w-full min-w-[720px] border-collapse text-sm">
<thead>
<tr className="border-b border-border/70 text-left">
<th className="px-6 py-3 font-medium text-muted-foreground">Date</th>
<th className="px-6 py-3 font-medium text-muted-foreground">Amount</th>
<th className="px-6 py-3 font-medium text-muted-foreground">Applied</th>
<th className="px-6 py-3 font-medium text-muted-foreground">Method</th>
<th className="px-6 py-3 font-medium text-muted-foreground">Reference</th>
<th className="px-6 py-3 font-medium text-muted-foreground">Status</th>
</tr>
</thead>
<tbody>
{payments.length === 0 ? (
<tr>
<td colSpan={6} className="px-6 py-10 text-center text-muted-foreground">
No repayments recorded yet.
</td>
</tr>
) : (
payments.map((payment: any) => (
<tr key={payment._id.toString()} className="border-b border-border/60">
<td className="px-6 py-3 text-muted-foreground">{formatDate(payment.confirmedAt || payment.createdAt)}</td>
<td className="px-6 py-3 font-medium text-foreground">{formatNaira(Number(payment.amountNgn || 0))}</td>
<td className="px-6 py-3 text-muted-foreground">{formatNaira(Number(payment.appliedAmountNgn || 0))}</td>
<td className="px-6 py-3 text-muted-foreground">{payment.method || "—"}</td>
<td className="px-6 py-3">
<span className="block max-w-[200px] truncate text-muted-foreground">{payment.paystackRef || "—"}</span>
</td>
<td className="px-6 py-3">
<Badge variant="default" className={cn(paymentStatusBadgeClass(payment.status))}>
{payment.status}
</Badge>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
)
}
35 changes: 35 additions & 0 deletions app/dashboard/admin/fleet-operations/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client"

import { useEffect } from "react"

import { PageHeader } from "@/components/dashboard/admin/page-header"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"

export default function FleetOperationsError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error("Fleet operations dashboard failed to load:", error)
}, [error])

return (
<div className="space-y-5">
<PageHeader title="Fleet Operations" subtitle="Something went wrong while loading the dashboard." />
<Card className="border-border/70">
<CardContent className="flex flex-col items-center gap-4 py-12 text-center">
<p className="text-sm text-muted-foreground">
We couldn&apos;t load fleet data right now. Please try again.
</p>
<Button onClick={reset} variant="outline">
Retry
</Button>
</CardContent>
</Card>
</div>
)
}
40 changes: 40 additions & 0 deletions app/dashboard/admin/fleet-operations/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { PageHeader } from "@/components/dashboard/admin/page-header"
import { Card, CardContent, CardHeader } from "@/components/ui/card"

export default function FleetOperationsLoading() {
return (
<div className="space-y-5">
<PageHeader title="Fleet Operations" subtitle="Loading fleet data…" />

<section className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
{Array.from({ length: 8 }).map((_, index) => (
<Card key={index} className="border-border/70">
<CardHeader>
<div className="h-4 w-28 animate-pulse rounded bg-muted" />
</CardHeader>
<CardContent>
<div className="h-7 w-24 animate-pulse rounded bg-muted" />
</CardContent>
</Card>
))}
</section>

<section className="rounded-xl border border-border/70 bg-card shadow-sm">
<div className="border-b border-border/60 px-4 py-3">
<div className="h-4 w-48 animate-pulse rounded bg-muted" />
</div>
<div className="divide-y divide-border/60">
{Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="flex items-center justify-between gap-4 px-4 py-4">
<div className="space-y-2">
<div className="h-4 w-40 animate-pulse rounded bg-muted" />
<div className="h-3 w-24 animate-pulse rounded bg-muted" />
</div>
<div className="h-6 w-20 animate-pulse rounded-full bg-muted" />
</div>
))}
</div>
</section>
</div>
)
}
Loading
Loading