PageSourceSearch

https://lottrax.com/assets/AppointmentScheduler-D4VykpF9.js

js lottrax.com collected 2026-10-09 22:54:43 UTC 20,978 bytes, 325 lines download raw bytes

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.