fix signin signout and apply the new style
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { formatCurrency } from '@rentaldrivego/types'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||
|
||||
type ReservationRow = {
|
||||
id: string
|
||||
contractNumber: string | null
|
||||
invoiceNumber: string | null
|
||||
status: string
|
||||
paymentStatus: string
|
||||
startDate: string
|
||||
endDate: string
|
||||
totalAmount: number
|
||||
customer: { firstName: string; lastName: string; email: string }
|
||||
vehicle: { make: string; model: string; licensePlate: string }
|
||||
}
|
||||
|
||||
export default function ContractsPage() {
|
||||
const { language } = useDashboardI18n()
|
||||
const [rows, setRows] = useState<ReservationRow[]>([])
|
||||
const [search, setSearch] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const copy = {
|
||||
en: {
|
||||
heading: 'Contracts',
|
||||
subtitle: 'Generate or reopen rental contracts from any booking.',
|
||||
search: 'Search customer, vehicle, plate, contract number…',
|
||||
booking: 'Booking',
|
||||
customer: 'Customer',
|
||||
vehicle: 'Vehicle',
|
||||
dates: 'Dates',
|
||||
contract: 'Contract',
|
||||
invoice: 'Invoice',
|
||||
total: 'Total',
|
||||
status: 'Status',
|
||||
action: 'Action',
|
||||
open: 'Open contract',
|
||||
generate: 'Generate contract',
|
||||
empty: 'No bookings found.',
|
||||
},
|
||||
fr: {
|
||||
heading: 'Contrats',
|
||||
subtitle: 'Générez ou rouvrez un contrat de location depuis n’importe quelle réservation.',
|
||||
search: 'Rechercher client, véhicule, plaque, numéro de contrat…',
|
||||
booking: 'Réservation',
|
||||
customer: 'Client',
|
||||
vehicle: 'Véhicule',
|
||||
dates: 'Dates',
|
||||
contract: 'Contrat',
|
||||
invoice: 'Facture',
|
||||
total: 'Total',
|
||||
status: 'Statut',
|
||||
action: 'Action',
|
||||
open: 'Ouvrir le contrat',
|
||||
generate: 'Générer le contrat',
|
||||
empty: 'Aucune réservation trouvée.',
|
||||
},
|
||||
ar: {
|
||||
heading: 'العقود',
|
||||
subtitle: 'أنشئ أو افتح عقد التأجير من أي حجز.',
|
||||
search: 'ابحث بالعميل أو السيارة أو اللوحة أو رقم العقد…',
|
||||
booking: 'الحجز',
|
||||
customer: 'العميل',
|
||||
vehicle: 'المركبة',
|
||||
dates: 'التواريخ',
|
||||
contract: 'العقد',
|
||||
invoice: 'الفاتورة',
|
||||
total: 'الإجمالي',
|
||||
status: 'الحالة',
|
||||
action: 'الإجراء',
|
||||
open: 'فتح العقد',
|
||||
generate: 'إنشاء العقد',
|
||||
empty: 'لم يتم العثور على حجوزات.',
|
||||
},
|
||||
}[language]
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<ReservationRow[]>('/reservations?pageSize=100')
|
||||
.then((data) => setRows(data ?? []))
|
||||
.catch((err) => setError(err.message ?? 'Failed to load contracts'))
|
||||
}, [])
|
||||
|
||||
const filteredRows = useMemo(() => {
|
||||
const q = search.trim().toLowerCase()
|
||||
if (!q) return rows
|
||||
return rows.filter((row) =>
|
||||
`${row.customer.firstName} ${row.customer.lastName}`.toLowerCase().includes(q) ||
|
||||
row.customer.email.toLowerCase().includes(q) ||
|
||||
`${row.vehicle.make} ${row.vehicle.model}`.toLowerCase().includes(q) ||
|
||||
row.vehicle.licensePlate.toLowerCase().includes(q) ||
|
||||
(row.contractNumber ?? '').toLowerCase().includes(q) ||
|
||||
(row.invoiceNumber ?? '').toLowerCase().includes(q),
|
||||
)
|
||||
}, [rows, search])
|
||||
|
||||
const localeCode = language === 'fr' ? 'fr-FR' : language === 'ar' ? 'ar-MA' : 'en-US'
|
||||
const formatDate = (iso: string) => new Date(iso).toLocaleString(localeCode, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900">{copy.heading}</h2>
|
||||
<p className="mt-1 text-sm text-slate-500">{copy.subtitle}</p>
|
||||
</div>
|
||||
<input
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder={copy.search}
|
||||
className="input-field w-full lg:max-w-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card overflow-hidden">
|
||||
{error ? (
|
||||
<div className="p-6 text-sm text-red-600">{error}</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.customer}</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.vehicle}</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.dates}</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.contract}</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.invoice}</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.status}</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wide text-slate-500">{copy.total}</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wide text-slate-500">{copy.action}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{filteredRows.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
<p className="font-semibold text-slate-900">{row.customer.firstName} {row.customer.lastName}</p>
|
||||
<p className="text-xs text-slate-500">{row.customer.email}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-700">
|
||||
<p>{row.vehicle.make} {row.vehicle.model}</p>
|
||||
<p className="text-xs text-slate-500">{row.vehicle.licensePlate}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-600">
|
||||
<p>{formatDate(row.startDate)}</p>
|
||||
<p>{formatDate(row.endDate)}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-700">{row.contractNumber ?? '—'}</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-700">{row.invoiceNumber ?? '—'}</td>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="badge-blue">{row.status}</span>
|
||||
<span className="badge-gray">{row.paymentStatus}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right text-sm font-semibold text-slate-900">{formatCurrency(row.totalAmount, 'MAD')}</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<Link href={`/contracts/${row.id}`} className="text-sm font-semibold text-blue-700 hover:underline">
|
||||
{row.contractNumber ? copy.open : copy.generate}
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredRows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} className="px-6 py-10 text-center text-sm text-slate-400">{copy.empty}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user