This commit is contained in:
2026-04-06 02:32:47 +03:00
parent 5f3d208158
commit 24a44481a0
152 changed files with 85913 additions and 44338 deletions
@@ -0,0 +1,29 @@
import { DashboardDetailsPage } from "@/base/components/layout/dashboard"
import { AppointmentActions } from "@/modules/appointments/appointment-actions"
import { AppointmentProvider } from "@/modules/appointments/appointment-context"
import { CalendarCheck2 } from "lucide-react"
import React from "react"
export default async function layout(props: {
params: Promise<{ id: string }>
children: React.ReactNode
}) {
const { id } = await props.params
return (
<AppointmentProvider appointment={{ id, label: `Appointment #${id}` }}>
<DashboardDetailsPage
className="p-0 lg:p-0"
icon={<CalendarCheck2 className="size-5" />}
title={`Appointment #${id}`}
backHref="/calendar/appointment/list"
actions={<AppointmentActions appointmentId={id} />}
tabs={[
{ href: `/calendar/appointment/${id}`, label: "Details" },
]}
>
{props.children}
</DashboardDetailsPage>
</AppointmentProvider>
)
}
@@ -0,0 +1,49 @@
"use client"
import { use } from "react"
import { useQuery } from "@tanstack/react-query"
import { useAuthApi } from "@/shared/useApi"
import { APPOINTMENT_ROUTES } from "@garage/api"
import { AppointmentGeneralInfo } from "@/modules/appointments/appointment-general-info"
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"
import { Skeleton } from "@/shared/components/ui/skeleton"
export default function AppointmentDetailsPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
const api = useAuthApi()
const { data, isLoading } = useQuery({
queryKey: [APPOINTMENT_ROUTES.INDEX, "detail", id],
queryFn: async () => {
const response = await api.appointments.list()
const items = (response as any)?.data ?? []
return items.find((item: any) => String(item.id) === id) ?? null
},
})
if (isLoading) {
return (
<DashboardPage header={null}>
<div className="grid gap-6 md:grid-cols-2 p-6">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-48 rounded-xl" />
))}
</div>
</DashboardPage>
)
}
if (!data) {
return (
<DashboardPage header={null}>
<p className="text-muted-foreground p-6">Appointment not found.</p>
</DashboardPage>
)
}
return (
<DashboardPage header={null}>
<AppointmentGeneralInfo appointment={data} />
</DashboardPage>
)
}
@@ -0,0 +1,112 @@
"use client"
import { useRouter } from "next/navigation"
import { ResourcePage } from "@/shared/data-view/resource-page"
import { ColumnHeader } from "@/shared/data-view/table-view"
import FormDialog from "@/shared/components/form-dialog"
import { AppointmentForm } from "@/modules/appointments/appointment-form"
import { APPOINTMENT_ROUTES } from "@garage/api"
import type { AppointmentsClient } from "@garage/api"
import { CalendarCheck2Icon, ClipboardListIcon, ClockIcon, ExternalLinkIcon } from "lucide-react"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
const STATUS_COLORS: Record<string, string> = {
requested: "bg-yellow-100 text-yellow-800",
confirmed: "bg-blue-100 text-blue-800",
in_progress: "bg-purple-100 text-purple-800",
completed: "bg-green-100 text-green-800",
cancelled: "bg-red-100 text-red-800",
}
export default function AppointmentsPage() {
const router = useRouter()
return (
<ResourcePage<AppointmentsClient>
pageTitle="Appointments"
routeKey={APPOINTMENT_ROUTES.INDEX}
getClient={(api) => api.appointments}
onRowClick={(row) => router.push(`/calendar/appointment/${(row as any).id}`)}
headerProps={({ selectedItem, invalidateQuery }) => ({
actions: (
<FormDialog title="Appointment">
{(resourceId) => (
<AppointmentForm
resourceId={resourceId}
initialData={selectedItem}
onSuccess={invalidateQuery}
/>
)}
</FormDialog>
),
})}
columns={({ actionsColumn }) => [
{
accessorKey: "title",
header: ({ column }) => <ColumnHeader column={column} title="Title" />,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<CalendarCheck2Icon className="size-4 text-muted-foreground" />
<span>{(row.original as any).title}</span>
</div>
),
},
{
accessorKey: "date",
header: ({ column }) => <ColumnHeader column={column} title="Date" />,
},
{
accessorKey: "from_time",
header: ({ column }) => <ColumnHeader column={column} title="Time" />,
cell: ({ row }) => {
const r = row.original as any
return (
<div className="flex items-center gap-1">
<ClockIcon className="size-3 text-muted-foreground" />
<span>{r.from_time} – {r.to_time}</span>
</div>
)
},
},
{
accessorKey: "status",
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
cell: ({ row }) => {
const status = (row.original as any).status
const colorClass = STATUS_COLORS[status] ?? "bg-gray-100 text-gray-800"
return (
<Badge className={colorClass}>
{status?.replace("_", " ") ?? "—"}
</Badge>
)
},
},
{
id: "job_card",
header: ({ column }) => <ColumnHeader column={column} title="Job Card" />,
cell: ({ row }) => {
const jobCardId = (row.original as any).job_card_id
if (!jobCardId) return <span className="text-muted-foreground">—</span>
return (
<Button
variant="outline"
size="sm"
className="h-7 gap-1.5"
onClick={(e) => {
e.stopPropagation()
router.push(`/sales/job-cards/${jobCardId}`)
}}
>
<ClipboardListIcon className="size-3" />
#{jobCardId}
<ExternalLinkIcon className="size-3" />
</Button>
)
},
},
actionsColumn(),
]}
/>
)
}