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
48 changes: 45 additions & 3 deletions app/dashboard/driver/repayment/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import Link from "next/link"
import { redirect } from "next/navigation"
import { ArrowRight, Wallet } from "lucide-react"
import { AlertTriangle, ArrowRight, CheckCircle2, Wallet } from "lucide-react"

import { DashboardShell } from "@/components/dashboard/dashboard-shell"
import { DashboardHeader } from "@/components/dashboard/investor-overview/dashboard-header"
import { ContractSummaryCard } from "@/components/dashboard/driver-hire-purchase/contract-summary-card"
import { DriverPaymentForm } from "@/components/dashboard/driver-hire-purchase/driver-payment-form"
import { DriverPaymentsTable } from "@/components/dashboard/driver-hire-purchase/driver-payments-table"
import { DriverVirtualAccountCard } from "@/components/dashboard/driver-hire-purchase/driver-virtual-account-card"
import { RepaymentScheduleTable } from "@/components/dashboard/driver-hire-purchase/repayment-schedule-table"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import dbConnect from "@/lib/dbConnect"
import { formatNaira } from "@/lib/currency"
import { getSessionFromCookies } from "@/lib/auth/session"
import { getDriverContract, getDriverPayments } from "@/lib/services/driver-contracts.service"
import { getOrProvisionDriverVirtualAccount } from "@/lib/services/paystack-dva.service"
Expand Down Expand Up @@ -61,7 +64,7 @@ export default async function DriverRepaymentPage() {
<CardHeader>
<CardTitle>No Active Contract</CardTitle>
<CardDescription>
A repayment contract must be assigned before you can make payments.
No active hire-purchase contract is assigned to your driver account yet. Once a contract is assigned, this repayment center will show your vehicle, schedule, arrears, and ownership progress.
</CardDescription>
</CardHeader>
<CardContent>
Expand Down Expand Up @@ -132,7 +135,7 @@ export default async function DriverRepaymentPage() {
<div>
<h2 className="text-xl font-semibold leading-tight text-foreground md:text-2xl">Repayment Center</h2>
<p className="mt-2 text-sm text-muted-foreground">
Make weekly fiat NGN repayments for your hire-purchase contract.
Track your active contract, repayment schedule, arrears, overpayments, and ownership progress.
</p>
</div>
<Button asChild variant="outline" className="h-10 w-full sm:w-auto">
Expand All @@ -144,6 +147,37 @@ export default async function DriverRepaymentPage() {
</div>
</section>


{contract.arrears.status === "LATE" ? (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Late repayment status</AlertTitle>
<AlertDescription>
{contract.arrears.overdueInstallments} installment(s) are overdue with {formatNaira(contract.arrears.arrearsAmountNgn)} in arrears.
</AlertDescription>
</Alert>
) : (
<Alert className="border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-900/50 dark:bg-emerald-950/20 dark:text-emerald-200">
<CheckCircle2 className="h-4 w-4" />
<AlertTitle>{contract.arrears.status === "COMPLETED" ? "Ownership completed" : "Repayments current"}</AlertTitle>
<AlertDescription>
{contract.arrears.status === "COMPLETED"
? "Your contract has been fully repaid."
: "No missed or late installments are currently due for this contract."}
</AlertDescription>
</Alert>
)}

{contract.overpaymentNgn > 0 ? (
<Alert>
<Wallet className="h-4 w-4" />
<AlertTitle>Overpayment recorded</AlertTitle>
<AlertDescription>
{formatNaira(contract.overpaymentNgn)} has been received above the contract amount and is displayed safely as unapplied value.
</AlertDescription>
</Alert>
) : null}

<section className="grid grid-cols-1 gap-4 xl:grid-cols-[1.1fr_0.9fr]">
<ContractSummaryCard contract={contract} />
<DriverVirtualAccountCard
Expand All @@ -154,6 +188,14 @@ export default async function DriverRepaymentPage() {
/>
</section>

<section className="rounded-[10px] border border-border/70 bg-card p-4 md:p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold text-foreground">Repayment Schedule</h3>
<p className="mt-1 text-sm text-muted-foreground">Expected weekly installments, applied payments, remaining amounts, and late status.</p>
</div>
<RepaymentScheduleTable schedule={contract.schedule} />
</section>

<section className="rounded-[10px] border border-dashed border-border/70 bg-card/70 p-4 md:p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold text-foreground">Paystack Checkout Fallback</h3>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AlertTriangle, Calendar, CheckCircle, Receipt } from "lucide-react"
import { AlertTriangle, Calendar, CheckCircle, Receipt, TrendingUp } from "lucide-react"

import { formatNaira } from "@/lib/currency"
import { Progress } from "@/components/ui/progress"
import { cn } from "@/lib/utils"
import type { DriverContractSnapshot } from "@/lib/services/driver-contracts.service"

Expand Down Expand Up @@ -69,9 +70,7 @@ export function ContractSummaryCard({ contract, className }: ContractSummaryCard
<p className="text-xs text-muted-foreground">Repayment Progress</p>
<p className="text-xs font-semibold text-foreground">{progressPercent}%</p>
</div>
<div className="h-2 rounded-full bg-muted">
<div className="h-2 rounded-full bg-emerald-600 dark:bg-emerald-500" style={{ width: `${progressPercent}%` }} />
</div>
<Progress value={progressPercent} className="h-2" />
<div className="mt-2 grid grid-cols-1 gap-1 text-xs text-muted-foreground sm:grid-cols-2">
<p className="inline-flex items-center">
<Receipt className="mr-1.5 h-3.5 w-3.5" />
Expand All @@ -83,6 +82,24 @@ export function ContractSummaryCard({ contract, className }: ContractSummaryCard
</p>
</div>
</div>

<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-3">
<article className="rounded-[10px] border border-border/70 px-3 py-3">
<p className="text-xs text-muted-foreground">Total contract</p>
<p className="mt-1 text-sm font-semibold text-foreground">{formatNaira(contract.totalPayableNgn)}</p>
</article>
<article className="rounded-[10px] border border-border/70 px-3 py-3">
<p className="text-xs text-muted-foreground">Outstanding</p>
<p className="mt-1 text-sm font-semibold text-foreground">{formatNaira(contract.remainingBalanceNgn)}</p>
</article>
<article className="rounded-[10px] border border-border/70 px-3 py-3">
<p className="inline-flex items-center text-xs text-muted-foreground">
<TrendingUp className="mr-1 h-3.5 w-3.5" />
Ownership
</p>
<p className="mt-1 text-sm font-semibold text-foreground">{progressPercent}% complete</p>
</article>
</div>
</section>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ export function DriverPaymentsTable({ payments, emptyLabel = "No payment records
<div>
<p className="text-xs text-muted-foreground">{formatDateLabel(payment.createdAt)}</p>
<p className="mt-1 text-sm font-semibold">{formatNaira(payment.amountNgn)}</p>
<p className="mt-1 text-xs text-muted-foreground">Applied: {formatNaira(payment.appliedAmountNgn)}</p>
</div>
{renderStatusBadge(payment.status)}
</div>
<div className="mt-2 grid gap-1 text-xs text-muted-foreground">
<p>Method: {payment.method}</p>
{payment.overpaymentNgn > 0 ? <p>Overpayment credited: {formatNaira(payment.overpaymentNgn)}</p> : null}
<p className="truncate">Reference: {payment.paystackRef}</p>
</div>
</article>
Expand All @@ -91,6 +93,8 @@ export function DriverPaymentsTable({ payments, emptyLabel = "No payment records
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Applied</TableHead>
<TableHead>Overpayment</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead>Reference</TableHead>
Expand All @@ -101,6 +105,8 @@ export function DriverPaymentsTable({ payments, emptyLabel = "No payment records
<TableRow key={payment.id}>
<TableCell className="text-xs">{formatDateLabel(payment.createdAt)}</TableCell>
<TableCell className="font-medium">{formatNaira(payment.amountNgn)}</TableCell>
<TableCell>{formatNaira(payment.appliedAmountNgn)}</TableCell>
<TableCell>{payment.overpaymentNgn > 0 ? formatNaira(payment.overpaymentNgn) : "—"}</TableCell>
<TableCell>{renderStatusBadge(payment.status)}</TableCell>
<TableCell className="text-xs">{payment.method}</TableCell>
<TableCell className="max-w-[160px] truncate text-xs text-muted-foreground">{payment.paystackRef}</TableCell>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { AlertTriangle, CheckCircle2, Clock3 } from "lucide-react"

import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { formatNaira } from "@/lib/currency"
import type { DriverRepaymentScheduleItem } from "@/lib/services/driver-contracts.service"

interface RepaymentScheduleTableProps {
schedule: DriverRepaymentScheduleItem[]
}

function formatDateLabel(value: string) {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return "N/A"
return date.toLocaleDateString("en-NG", {
month: "short",
day: "numeric",
year: "numeric",
})
}

function renderStatusBadge(status: DriverRepaymentScheduleItem["status"]) {
if (status === "PAID") {
return (
<Badge className="bg-emerald-600 text-white hover:bg-emerald-600">
<CheckCircle2 className="mr-1 h-3.5 w-3.5" />
Paid
</Badge>
)
}

if (status === "LATE") {
return (
<Badge className="bg-red-600 text-white hover:bg-red-600">
<AlertTriangle className="mr-1 h-3.5 w-3.5" />
Late
</Badge>
)
}

if (status === "PARTIAL") {
return (
<Badge className="bg-amber-500 text-white hover:bg-amber-500">
<Clock3 className="mr-1 h-3.5 w-3.5" />
Partial
</Badge>
)
}

return <Badge variant="secondary">Upcoming</Badge>
}

export function RepaymentScheduleTable({ schedule }: RepaymentScheduleTableProps) {
if (schedule.length === 0) {
return (
<div className="rounded-[10px] border border-dashed border-border px-4 py-8 text-center text-sm text-muted-foreground">
No repayment schedule is available for this contract.
</div>
)
}

return (
<div className="overflow-x-auto rounded-[10px] border border-border/70">
<Table>
<TableHeader className="sticky top-0 z-10 bg-background">
<TableRow>
<TableHead>Week</TableHead>
<TableHead>Due date</TableHead>
<TableHead>Expected</TableHead>
<TableHead>Applied</TableHead>
<TableHead>Remaining</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{schedule.map((item) => (
<TableRow key={item.installmentNumber}>
<TableCell className="font-medium">#{item.installmentNumber}</TableCell>
<TableCell>{formatDateLabel(item.dueDate)}</TableCell>
<TableCell>{formatNaira(item.expectedAmountNgn)}</TableCell>
<TableCell>{formatNaira(item.paidAmountNgn)}</TableCell>
<TableCell>{formatNaira(item.remainingAmountNgn)}</TableCell>
<TableCell>{renderStatusBadge(item.status)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
Loading
Loading