add car reservation feature

This commit is contained in:
root
2026-05-09 21:10:38 -04:00
parent 09b0e3b55f
commit 6322b7d2a1
37 changed files with 1169 additions and 119 deletions
@@ -3,9 +3,10 @@
import { useEffect, useRef, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import Image from 'next/image'
import { ArrowLeft, Edit, X, Check, Upload, Trash2 } from 'lucide-react'
import { ArrowLeft, Edit, X, Check, Upload, Trash2, CalendarDays, Info } from 'lucide-react'
import { formatCurrency } from '@rentaldrivego/types'
import { apiFetch } from '@/lib/api'
import VehicleCalendar from '@/components/VehicleCalendar'
interface VehicleDetail {
id: string
@@ -65,11 +66,14 @@ function initColorSelect(color: string) {
return PRESET_COLORS.includes(color) ? color : (color ? 'custom' : '')
}
type Tab = 'details' | 'calendar'
export default function FleetDetailPage() {
const params = useParams<{ id: string }>()
const router = useRouter()
const [vehicle, setVehicle] = useState<VehicleDetail | null>(null)
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<Tab>('details')
const [editing, setEditing] = useState(false)
const [saving, setSaving] = useState(false)
const [saveError, setSaveError] = useState<string | null>(null)
@@ -253,7 +257,34 @@ export default function FleetDetailPage() {
<div className="px-4 py-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{saveError}</div>
)}
<div className="grid gap-6 lg:grid-cols-[1.3fr_0.7fr]">
{/* Tab bar */}
<div className="flex gap-1 border-b border-slate-200 dark:border-slate-800">
<button
onClick={() => setActiveTab('details')}
className={[
'flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
activeTab === 'details'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300',
].join(' ')}
>
<Info className="w-4 h-4" /> Details
</button>
<button
onClick={() => setActiveTab('calendar')}
className={[
'flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
activeTab === 'calendar'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300',
].join(' ')}
>
<CalendarDays className="w-4 h-4" /> Calendar
</button>
</div>
{activeTab === 'details' && (
<div className="grid gap-6 lg:grid-cols-[1.3fr_0.7fr]">
{/* Photos */}
<div className="card p-6">
<h3 className="text-base font-semibold text-slate-900 mb-4">Photos</h3>
@@ -493,7 +524,12 @@ export default function FleetDetailPage() {
</div>
)}
</div>
</div>
</div>
)}
{activeTab === 'calendar' && (
<VehicleCalendar vehicleId={params.id} />
)}
</div>
)
}
@@ -3,7 +3,7 @@
import { useEffect, useState, useCallback } from 'react'
import dayjs from 'dayjs'
import Link from 'next/link'
import { formatCurrency, type ApiPaginated } from '@rentaldrivego/types'
import { formatCurrency } from '@rentaldrivego/types'
import { apiFetch } from '@/lib/api'
import { Globe, CheckCircle, XCircle, Clock, ChevronRight, User, Car, CalendarDays, MessageSquare } from 'lucide-react'
@@ -81,9 +81,9 @@ export default function OnlineReservationsPage() {
const load = useCallback(async () => {
try {
setLoading(true)
const result = await apiFetch<ApiPaginated<OnlineReservation>>('/reservations?source=MARKETPLACE&pageSize=100')
const result = await apiFetch<OnlineReservation[]>('/reservations?source=MARKETPLACE&pageSize=100')
setRows(
(result.data ?? []).sort((a, b) => {
(result ?? []).sort((a, b) => {
// DRAFT first, then by createdAt desc
if (a.status === 'DRAFT' && b.status !== 'DRAFT') return -1
if (a.status !== 'DRAFT' && b.status === 'DRAFT') return 1
@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'
import { formatCurrency } from '@rentaldrivego/types'
import { apiFetch, EMPLOYEE_TOKEN_KEY } from '@/lib/api'
import { API_BASE, apiFetch, EMPLOYEE_TOKEN_KEY } from '@/lib/api'
interface ReportRow {
reservationId: string
@@ -37,8 +37,6 @@ const periodLabels: Record<ReportPeriod, string> = {
ANNUAL: 'Annual',
}
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
export default function ReportsPage() {
const [period, setPeriod] = useState<ReportPeriod>('MONTHLY')
const [report, setReport] = useState<ReportData | null>(null)
@@ -3,10 +3,9 @@
import Link from 'next/link'
import { useState } from 'react'
import { useDashboardI18n } from '@/components/I18nProvider'
import { API_BASE } from '@/lib/api'
import { marketplaceUrl } from '@/lib/urls'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
export default function ForgotPasswordPage() {
const { language } = useDashboardI18n()
const dict = {
@@ -4,8 +4,7 @@ import Link from 'next/link'
import { useState, Suspense } from 'react'
import { useSearchParams } from 'next/navigation'
import { useDashboardI18n } from '@/components/I18nProvider'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
import { API_BASE } from '@/lib/api'
export default function ResetPasswordPage() {
return (
@@ -4,11 +4,8 @@ import Link from 'next/link'
import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useDashboardI18n } from '@/components/I18nProvider'
import { marketplaceUrl } from '@/lib/urls'
import { EMPLOYEE_TOKEN_KEY } from '@/lib/api'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
const ADMIN_URL = process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3002'
import { adminUrl, marketplaceUrl } from '@/lib/urls'
import { API_BASE, EMPLOYEE_TOKEN_KEY } from '@/lib/api'
export default function SignInPage() {
const { language } = useDashboardI18n()
@@ -141,11 +138,11 @@ function LocalSignInForm({ dict, portal }: {
const [error, setError] = useState<string | null>(null)
const adminNext = searchParams.get('next') || '/dashboard'
const employeeRedirect = searchParams.get('redirect') || '/dashboard'
const forgotPasswordHref = portal === 'admin' ? `${ADMIN_URL}/forgot-password` : '/forgot-password'
const forgotPasswordHref = portal === 'admin' ? `${adminUrl}/forgot-password` : '/forgot-password'
function redirectAdmin(token: string) {
const hash = new URLSearchParams({ token, next: adminNext }).toString()
window.location.href = `${ADMIN_URL}/auth-redirect#${hash}`
window.location.href = `${adminUrl}/auth-redirect#${hash}`
}
async function handleCredentials(e: React.FormEvent) {