1import { as as toDate, u as useDealership, r as reactExports, H as addDays, t as trpc, e as toast, j as jsxRuntimeExports, B as Button, S as Select, a as SelectTrigger, b as SelectValue, c as SelectContent, d as SelectItem, D as Dialog, ac as DialogTrigger, y as DialogContent, z as DialogHeader, E as DialogTitle, I as Calendar, f as Badge, m as Clock, l as Car } from "./index-DZUVA1tZ.js"; 2import { P as PageHeader } from "./PageHeader-BeAccEfP.js"; 3import { C as Card, c as CardContent, a as CardHeader, b as CardTitle } from "./card-C2_jI6O8.js"; 4import { I as Input } from "./input-BsBhHtex.js"; 5import { T as Textarea } from "./textarea-B194Jwaz.js"; 6import { e as endOfDay } from "./endOfDay-8cIaVQRx.js"; 7import { P as Plus } from "./plus-BbqdyZv7.js"; 8import { U as User } from "./user-C-t3XOxU.js"; 9import { C as CircleCheck } from "./circle-check-RQpKy75X.js"; 10import { C as CircleAlert } from "./circle-alert-B4dbcbDi.js"; 11import { C as CircleX } from "./circle-x-Bl7mXUyr.js"; 12import { f as format, s as startOfDay } from "./format-B8OmckKX.js"; 13import "./calendar-B8_9t0D4.js"; 14import "./endOfMonth-BBZWhvnk.js"; 15import "./chevron-left-BUMmw3WR.js"; 16import "./calendar-days-kCkFTmM5.js"; 17import "./download-2Lv-z89u.js"; 18import "./useComposition-Bo7q6aoH.js"; 19function isPast(date) { 20 return +toDate(date) < Date.now(); 21} 22const TYPE_LABELS = { 23 test_drive: "Test Drive", 24 delivery: "Delivery", 25 follow_up: "Follow Up", 26 service: "Service", 27 other: "Other" 28}; 29const TYPE_COLORS = { 30 test_drive: "bg-blue-500/10 text-blue-500 border-blue-500/30", 31 delivery: "bg-emerald-500/10 text-emerald-500 border-emerald-500/30", 32 follow_up: "bg-amber-500/10 text-amber-500 border-amber-500/30", 33 service: "bg-purple-500/10 text-purple-500 border-purple-500/30", 34 other: "bg-slate-500/10 text-slate-400 border-slate-500/30" 35}; 36const STATUS_COLORS = { 37 scheduled: "bg-blue-500/10 text-blue-500 border-blue-500/30", 38 completed: "bg-emerald-500/10 text-emerald-500 border-emerald-500/30", 39 cancelled: "bg-red-500/10 text-red-500 border-red-500/30", 40 no_show: "bg-amber-500/10 text-amber-500 border-amber-500/30" 41}; 42function AppointmentScheduler() { 43 const { selectedDealerId } = useDealership(); 44 const [statusFilter, setStatusFilter] = reactExports.useState("scheduled"); 45 const [dialogOpen, setDialogOpen] = reactExports.useState(false); 46 const [viewMode, setViewMode] = reactExports.useState("today"); 47 const [formType, setFormType] = reactExports.useState("test_drive"); 48 const [formCustomer, setFormCustomer] = reactExports.useState(""); 49 const [formPhone, setFormPhone] = reactExports.useState(""); 50 const [formAssigned, setFormAssigned] = reactExports.useState(""); 51 const [formVehicle, setFormVehicle] = reactExports.useState(""); 52 const [formDate, setFormDate] = reactExports.useState(format(/* @__PURE__ */ new Date(), "yyyy-MM-dd")); 53 const [formTime, setFormTime] = reactExports.useState("10:00"); 54 const [formDuration, setFormDuration] = reactExports.useState("30"); 55 const [formNotes, setFormNotes] = reactExports.useState(""); 56 const timeRange = reactExports.useMemo(() => { 57 const now = /* @__PURE__ */ new Date(); 58 if (viewMode === "today") { 59 return { startTime: startOfDay(now).getTime(), endTime: endOfDay(now).getTime() }; 60 } else if (viewMode === "week") { 61 return { startTime: startOfDay(now).getTime(), endTime: endOfDay(addDays(now, 7)).getTime() }; 62 } 63 return { startTime: void 0, endTime: void 0 }; 64 }, [viewMode]); 65 const { data: appts, isLoading } = trpc.appointments.list.useQuery( 66 { 67 dealerId: selectedDealerId, 68 startTime: timeRange.startTime, 69 endTime: timeRange.endTime, 70 status: statusFilter === "all" ? void 0 : statusFilter 71 }, 72 { enabled: !!selectedDealerId, refetchInterval: 3e4 } 73 ); 74 const utils = trpc.useUtils(); 75 const createMutation = trpc.appointments.create.useMutation({ 76 onSuccess: () => { 77 toast.success("Appointment created"); 78 setDialogOpen(false); 79 resetForm(); 80 utils.appointments.list.invalidate(); 81 }, 82 onError: (e) => toast.error(e.message) 83 }); 84 const updateMutation = trpc.appointments.update.useMutation({ 85 onSuccess: () => { 86 toast.success("Appointment updated");
87 utils.appointments.list.invalidate(); 88 }, 89 onError: (e) => toast.error(e.message) 90 }); 91 trpc.appointments.delete.useMutation({ 92 onSuccess: () => { 93 toast.success("Appointment deleted"); 94 utils.appointments.list.invalidate(); 95 } 96 }); 97 function resetForm() { 98 setFormType("test_drive"); 99 setFormCustomer(""); 100 setFormPhone(""); 101 setFormAssigned(""); 102 setFormVehicle(""); 103 setFormDate(format(/* @__PURE__ */ new Date(), "yyyy-MM-dd")); 104 setFormTime("10:00"); 105 setFormDuration("30"); 106 setFormNotes(""); 107 } 108 function handleCreate() { 109 const [y, m, d] = formDate.split("-").map(Number); 110 const [h, min] = formTime.split(":").map(Number); 111 const scheduledAt = new Date(y, m - 1, d, h, min).getTime(); 112 createMutation.mutate({ 113 dealerId: selectedDealerId, 114 type: formType, 115 customerName: formCustomer || void 0, 116 customerPhone: formPhone || void 0, 117 assignedTo: formAssigned || void 0, 118 vehicleInfo: formVehicle || void 0, 119 scheduledAt, 120 durationMinutes: parseInt(formDuration) || 30, 121 notes: formNotes || void 0 122 }); 123 } 124 const stats = reactExports.useMemo(() => { 125 if (!appts) return { total: 0, scheduled: 0, completed: 0, noShow: 0 }; 126 return { 127 total: appts.length, 128 scheduled: appts.filter((a) => a.status === "scheduled").length, 129 completed: appts.filter((a) => a.status === "completed").length, 130 noShow: appts.filter((a) => a.status === "no_show").length 131 }; 132 }, [appts]); 133 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6 p-6", children: [ 134 /* @__PURE__ */ jsxRuntimeExports.jsxs( 135 PageHeader, 136 { 137 title: "Appointment Scheduler", 138 description: "Manage test drives, deliveries, follow-ups, and service appointments", 139 children: [
140 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2", children: ["today", "week", "all"].map((mode) => /* @__PURE__ */ jsxRuntimeExports.jsx( 141 Button, 142 { 143 variant: viewMode === mode ? "default" : "outline", 144 size: "sm", 145 className: "h-9", 146 onClick: () => setViewMode(mode), 147 children: mode === "today" ? "Today" : mode === "week" ? "This Week" : "All" 148 }, 149 mode 150 )) }), 151 /* @__PURE__ */ jsxRuntimeExports.jsxs(Select, { value: statusFilter, onValueChange: setStatusFilter, children: [ 152 /* @__PURE__ */ jsxRuntimeExports.jsx(SelectTrigger, { className: "w-[140px] h-9", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SelectValue, {}) }), 153 /* @__PURE__ */ jsxRuntimeExports.jsxs(SelectContent, { children: [ 154 /* @__PURE__ */ jsxRuntimeExports.jsx(SelectItem, { value: "all", children: "All Status" }), 155 /* @__PURE__ */ jsxRuntimeExports.jsx(SelectItem, { value: "scheduled", children: "Scheduled" }),
156 /* @__PURE__ */ jsxRuntimeExports.jsx(SelectItem, { value: "completed", children: "Completed" }), 157 /* @__PURE__ */ jsxRuntimeExports.jsx(SelectItem, { value: "cancelled", children: "Cancelled" }), 158 /* @__PURE__ */ jsxRuntimeExports.jsx(SelectItem, { value: "no_show", children: "No Show" }) 159 ] }) 160 ] }), 161 /* @__PURE__ */ jsxRuntimeExports.jsxs(Dialog, { open: dialogOpen, onOpenChange: setDialogOpen, children: [ 162 /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { size: "sm", className: "gap-1.5 h-9", children: [ 163 /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "size-3.5" }), 164 " New Appointment" 165 ] }) }), 166 /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogContent, { className: "max-w-md", children: [ 167 /* @__PURE__ */ jsxRuntimeExports.jsx(DialogHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { children: "Schedule Appointment" }) }),
168 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3 pt-2", children: [ 169 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [ 170 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 171 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Type" }), 172 /* @__PURE__ */ jsxRuntimeExports.jsxs(Select, { value: formType, onValueChange: setFormType, children: [ 173 /* @__PURE__ */ jsxRuntimeExports.jsx(SelectTrigger, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(SelectValue, {}) }), 174 /* @__PURE__ */ jsxRuntimeExports.jsx(SelectContent, { children: Object.entries(TYPE_LABELS).map(([k, v]) => /* @__PURE__ */ jsxRuntimeExports.jsx(SelectItem, { value: k, children: v }, k)) }) 175 ] }) 176 ] }), 177 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
178 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Duration (min)" }), 179 /* @__PURE__ */ jsxRuntimeExports.jsx(Input, { value: formDuration, onChange: (e) => setFormDuration(e.target.value), type: "number", min: "5", max: "480" }) 180 ] }) 181 ] }), 182 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 183 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Customer Name" }), 184 /* @__PURE__ */ jsxRuntimeExports.jsx(Input, { value: formCustomer, onChange: (e) => setFormCustomer(e.target.value), placeholder: "John Smith" }) 185 ] }), 186 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 187 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Phone" }), 188 /* @__P
188URE__ */ jsxRuntimeExports.jsx(Input, { value: formPhone, onChange: (e) => setFormPhone(e.target.value), placeholder: "(555) 123-4567" }) 189 ] }), 190 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 191 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Assigned To" }), 192 /* @__PURE__ */ jsxRuntimeExports.jsx(Input, { value: formAssigned, onChange: (e) => setFormAssigned(e.target.value), placeholder: "Rep name" }) 193 ] }), 194 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 195 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Vehicle" }), 196 /* @__PURE__ */ jsxRuntimeExports.jsx(Input, { value: formVehicle, onChange: (e) => setFormVehicle(e.target.value), placeholder: "2024 Toyota Camry" }) 197 ] }), 198 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
199 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 200 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Date" }), 201 /* @__PURE__ */ jsxRuntimeExports.jsx(Input, { type: "date", value: formDate, onChange: (e) => setFormDate(e.target.value) }) 202 ] }), 203 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 204 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Time" }), 205 /* @__PURE__ */ jsxRuntimeExports.jsx(Input, { type: "time", value: formTime, onChange: (e) => setFormTime(e.target.value) }) 206 ] }) 207 ] }), 208 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 209 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Notes" }),
210 /* @__PURE__ */ jsxRuntimeExports.jsx(Textarea, { value: formNotes, onChange: (e) => setFormNotes(e.target.value), rows: 2, placeholder: "Additional details..." }) 211 ] }), 212 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { className: "w-full", onClick: handleCreate, disabled: createMutation.isPending, children: createMutation.isPending ? "Creating..." : "Create Appointment" }) 213 ] }) 214 ] }) 215 ] }) 216 ] 217 } 218 ), 219 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-4", children: [ 220 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "border-border/40 shadow-depth-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "pt-4 pb-3", children: [ 221 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wider", children: "Total" }), 222 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-2xl font-bold mt-1", children: stats.total }) 223 ] }) }),
224 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "border-border/40 shadow-depth-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "pt-4 pb-3", children: [ 225 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wider", children: "Scheduled" }), 226 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-2xl font-bold mt-1 text-blue-500", children: stats.scheduled }) 227 ] }) }), 228 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "border-border/40 shadow-depth-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "pt-4 pb-3", children: [ 229 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wider", children: "Completed" }), 230 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-2xl font-bold mt-1 text-emerald-500", children: stats.completed }) 231 ] }) }), 232 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "border-border/40 shadow-depth-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "pt-4 pb-3", children: [
233 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wider", children: "No-Shows" }), 234 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-2xl font-bold mt-1 text-amber-500", children: stats.noShow }) 235 ] }) }) 236 ] }), 237 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { className: "border-border/40 shadow-depth-1", children: [ 238 /* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, { className: "pb-2", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "text-base font-semibold", children: [ 239 viewMode === "today" ? "Today's" : viewMode === "week" ? "This Week's" : "All", 240 " Appointments" 241 ] }) }), 242 /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { children: isLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "py-12 text-center text-muted-foreground", children: "Loading appointments..." }) : !appts || appts.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-12 text-center text-muted-foreground", children: [
243 /* @__PURE__ */ jsxRuntimeExports.jsx(Calendar, { className: "size-8 mx-auto mb-2 opacity-40" }), 244 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "No appointments found" }) 245 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-3", children: appts.map((appt) => { 246 const time = new Date(appt.scheduledAt); 247 const overdue = appt.status === "scheduled" && isPast(time); 248 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 249 "div", 250 { 251 className: `flex items-center gap-4 p-3 rounded-lg border transition-colors ${overdue ? "border-red-500/30 bg-red-500/5" : "border-border/40 hover:bg-muted/30"}`, 252 children: [ 253 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [ 254 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [ 255 /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", className: TYPE_COLORS[appt.type] || TYPE_COLORS.other, children: TYPE_LABELS[appt.type] || appt.type }),
256 /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", className: STATUS_COLORS[appt.status] || "", children: appt.status.replace("_", " ") }), 257 overdue && /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", className: "bg-red-500/10 text-red-500 border-red-500/30", children: "Overdue" }) 258 ] }), 259 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 mt-1.5 text-sm", children: [ 260 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-muted-foreground", children: [ 261 /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { className: "size-3.5" }), 262 format(time, "MMM d, h:mm a"), 263 appt.durationMinutes && ` (${appt.durationMinutes}m)` 264 ] }), 265 appt.customerName && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1", children: [ 266 /* @__P
266URE__ */ jsxRuntimeExports.jsx(User, { className: "size-3.5 text-muted-foreground" }), 267 appt.customerName 268 ] }), 269 appt.vehicleInfo && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-muted-foreground", children: [ 270 /* @__PURE__ */ jsxRuntimeExports.jsx(Car, { className: "size-3.5" }), 271 appt.vehicleInfo 272 ] }) 273 ] }), 274 appt.assignedTo && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: [ 275 "Assigned: ", 276 appt.assignedTo 277 ] }), 278 appt.notes && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground mt-0.5 italic", children: appt.notes }) 279 ] }), 280 appt.status === "scheduled" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 shrink-0", children: [
281 /* @__PURE__ */ jsxRuntimeExports.jsx( 282 Button, 283 { 284 variant: "ghost", 285 size: "sm", 286 className: "h-7 px-2 text-emerald-500 hover:text-emerald-400 hover:bg-emerald-500/10", 287 onClick: () => updateMutation.mutate({ id: appt.id, status: "completed" }), 288 title: "Mark Completed", 289 children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { className: "size-4" }) 290 } 291 ), 292 /* @__PURE__ */ jsxRuntimeExports.jsx( 293 Button, 294 { 295 variant: "ghost", 296 size: "sm", 297 className: "h-7 px-2 text-amber-500 hover:text-amber-400 hover:bg-amber-500/10", 298 onClick: () => updateMutation.mutate({ id: appt.id, status: "no_show" }), 299 title: "No Show", 300 children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "size-4" }) 301 } 302 ),
303 /* @__PURE__ */ jsxRuntimeExports.jsx( 304 Button, 305 { 306 variant: "ghost", 307 size: "sm", 308 className: "h-7 px-2 text-red-500 hover:text-red-400 hover:bg-red-500/10", 309 onClick: () => updateMutation.mutate({ id: appt.id, status: "cancelled" }), 310 title: "Cancel", 311 children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleX, { className: "size-4" }) 312 } 313 ) 314 ] }) 315 ] 316 }, 317 appt.id 318 ); 319 }) }) }) 320 ] }) 321 ] }); 322} 323export { 324 AppointmentScheduler as default 325};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.