fix(dashboard): resolve tutorial QA notes (clips 2–6)

- settings/company: show success toast on save (2.1)
- inspection templates: add explicit Save button (2.6)
- parts: label price fields with AED currency (3.1)
- service groups: add parts/services picker to packages (3.3)
- vehicles: add insurance (has_insurance + insurance type) field (4.2)
- appointments: add calendar view with list/calendar toggle (5.1)
- job card check-in: require department (5.4)
- job card payments: refresh job card/invoice queries so PAID shows (5.6)
- vendors: add phone + address fields (6.1)
- bill payments: surface real vendor/employee validation errors (6.4)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 11:39:07 +04:00
co-authored by Claude Opus 4.8
parent c71b5a19ed
commit fdce301495
14 changed files with 562 additions and 39 deletions
@@ -0,0 +1,132 @@
"use client"
import { useMemo, useState } from "react"
import { useRouter } from "next/navigation"
import { useQuery } from "@tanstack/react-query"
import { CalendarCheck2Icon, ClipboardListIcon, ClockIcon } from "lucide-react"
import { Calendar } from "@/shared/components/ui/calendar"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Badge } from "@/shared/components/ui/badge"
import { useAuthApi } from "@/shared/useApi"
import { APPOINTMENT_ROUTES } from "@garage/api"
import { formatEnum } from "@/shared/utils/formatters"
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",
}
function toDateKey(d: Date): string {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, "0")
const day = String(d.getDate()).padStart(2, "0")
return `${y}-${m}-${day}`
}
export function AppointmentsCalendarView() {
const api = useAuthApi()
const router = useRouter()
const [selected, setSelected] = useState<Date | undefined>(() => new Date())
const { data, isLoading } = useQuery({
queryKey: [APPOINTMENT_ROUTES.INDEX, "calendar"],
queryFn: () => api.appointments.list({ per_page: 100 } as any),
})
const appointments: any[] = (data as any)?.data ?? []
const byDate = useMemo(() => {
const map: Record<string, any[]> = {}
for (const a of appointments) {
const key = String(a.date ?? "").slice(0, 10)
if (!key) continue
;(map[key] ??= []).push(a)
}
return map
}, [appointments])
const bookedDays = useMemo(
() => Object.keys(byDate).map((k) => new Date(`${k}T00:00:00`)),
[byDate],
)
const selectedKey = selected ? toDateKey(selected) : ""
const dayAppointments = byDate[selectedKey] ?? []
return (
<div className="grid gap-6 lg:grid-cols-[auto_1fr]">
<Card className="w-fit">
<CardContent className="p-3">
<Calendar
mode="single"
selected={selected}
onSelect={setSelected}
modifiers={{ booked: bookedDays }}
modifiersClassNames={{
booked:
"relative after:absolute after:bottom-1 after:left-1/2 after:h-1 after:w-1 after:-translate-x-1/2 after:rounded-full after:bg-primary",
}}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-semibold">
{selected
? selected.toLocaleDateString(undefined, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
: "Select a day"}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{isLoading && <p className="text-muted-foreground text-sm">Loading…</p>}
{!isLoading && dayAppointments.length === 0 && (
<p className="text-muted-foreground text-sm">No appointments on this day.</p>
)}
{dayAppointments.map((a) => {
const jobCardId = a.job_card_id ?? a.job_card?.id
return (
<button
key={a.id}
type="button"
onClick={() => router.push(`/calendar/appointment/${a.id}`)}
className="hover:bg-muted/40 w-full rounded border p-3 text-left transition"
>
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 font-medium">
<CalendarCheck2Icon className="text-muted-foreground size-4" />
{a.title}
</span>
<Badge className={STATUS_COLORS[a.status] ?? "bg-gray-100 text-gray-800"}>
{formatEnum(a.status)}
</Badge>
</div>
<div className="text-muted-foreground mt-1 flex items-center gap-3 text-sm">
<span className="flex items-center gap-1">
<ClockIcon className="size-3" />
{a.from_time} – {a.to_time}
</span>
{jobCardId && (
<span className="flex items-center gap-1">
<ClipboardListIcon className="size-3" />
{a.job_card?.title || `#${jobCardId}`}
</span>
)}
</div>
</button>
)
})}
</CardContent>
</Card>
</div>
)
}