medusajs-repairshop-plugin 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.medusa/server/src/admin/index.js +1464 -0
  2. package/.medusa/server/src/admin/index.mjs +1465 -0
  3. package/.medusa/server/src/api/admin/parts/route.js +34 -0
  4. package/.medusa/server/src/api/admin/plugin/route.js +7 -0
  5. package/.medusa/server/src/api/admin/repairs/[id]/approve/route.js +17 -0
  6. package/.medusa/server/src/api/admin/repairs/[id]/costs/route.js +26 -0
  7. package/.medusa/server/src/api/admin/repairs/[id]/custom-parts/route.js +42 -0
  8. package/.medusa/server/src/api/admin/repairs/[id]/details/route.js +57 -0
  9. package/.medusa/server/src/api/admin/repairs/[id]/document/route.js +53 -0
  10. package/.medusa/server/src/api/admin/repairs/[id]/media/route.js +28 -0
  11. package/.medusa/server/src/api/admin/repairs/[id]/messages/route.js +26 -0
  12. package/.medusa/server/src/api/admin/repairs/[id]/notes/route.js +27 -0
  13. package/.medusa/server/src/api/admin/repairs/[id]/parts/[variant_id]/route.js +15 -0
  14. package/.medusa/server/src/api/admin/repairs/[id]/parts/route.js +16 -0
  15. package/.medusa/server/src/api/admin/repairs/[id]/remind/route.js +16 -0
  16. package/.medusa/server/src/api/admin/repairs/[id]/route.js +49 -0
  17. package/.medusa/server/src/api/admin/repairs/[id]/status/route.js +22 -0
  18. package/.medusa/server/src/api/admin/repairs/analytics/route.js +86 -0
  19. package/.medusa/server/src/api/admin/repairs/middleware.js +121 -0
  20. package/.medusa/server/src/api/admin/repairs/route.js +39 -0
  21. package/.medusa/server/src/api/middlewares.js +17 -0
  22. package/.medusa/server/src/api/store/customers/me/repairs/route.js +37 -0
  23. package/.medusa/server/src/api/store/plugin/route.js +7 -0
  24. package/.medusa/server/src/api/store/repairs/[id]/approve/route.js +55 -0
  25. package/.medusa/server/src/api/store/repairs/[id]/compliance/route.js +37 -0
  26. package/.medusa/server/src/api/store/repairs/[id]/document/route.js +48 -0
  27. package/.medusa/server/src/api/store/repairs/[id]/messages/route.js +83 -0
  28. package/.medusa/server/src/api/store/repairs/[id]/reject/route.js +23 -0
  29. package/.medusa/server/src/api/store/repairs/[serial_number]/route.js +125 -0
  30. package/.medusa/server/src/api/store/repairs/[ticket_number]/route.js +55 -0
  31. package/.medusa/server/src/api/store/repairs/approve/route.js +45 -0
  32. package/.medusa/server/src/api/store/repairs/compliance/route.js +32 -0
  33. package/.medusa/server/src/api/store/repairs/middleware.js +28 -0
  34. package/.medusa/server/src/api/store/repairs/route.js +42 -0
  35. package/.medusa/server/src/api/store/repairs/token/[token]/document/route.js +46 -0
  36. package/.medusa/server/src/api/store/repairs/token/[token]/route.js +17 -0
  37. package/.medusa/server/src/email-templates/order-confirmation.js +41 -0
  38. package/.medusa/server/src/email-templates/styles.js +85 -0
  39. package/.medusa/server/src/index.js +8 -0
  40. package/.medusa/server/src/links/repair-product.js +15 -0
  41. package/.medusa/server/src/migration-script/25022026-initial-seed.js +264 -0
  42. package/.medusa/server/src/modules/repair/index.js +13 -0
  43. package/.medusa/server/src/modules/repair/migrations/Migration20260427135428.js +43 -0
  44. package/.medusa/server/src/modules/repair/migrations/Migration20260506000000.js +16 -0
  45. package/.medusa/server/src/modules/repair/migrations/Migration20260506000001.js +14 -0
  46. package/.medusa/server/src/modules/repair/migrations/Migration20260506000002.js +14 -0
  47. package/.medusa/server/src/modules/repair/migrations/Migration20260506000003.js +16 -0
  48. package/.medusa/server/src/modules/repair/migrations/Migration20260526000000.js +14 -0
  49. package/.medusa/server/src/modules/repair/models/device.js +22 -0
  50. package/.medusa/server/src/modules/repair/models/repair-media.js +26 -0
  51. package/.medusa/server/src/modules/repair/models/repair-note.js +25 -0
  52. package/.medusa/server/src/modules/repair/models/repair-ticket.js +77 -0
  53. package/.medusa/server/src/modules/repair/models/repair-update.js +25 -0
  54. package/.medusa/server/src/modules/repair/service.js +21 -0
  55. package/.medusa/server/src/subscribers/notifications.js +187 -0
  56. package/.medusa/server/src/subscribers/order-canceled.js +31 -0
  57. package/.medusa/server/src/subscribers/order-paid.js +63 -0
  58. package/.medusa/server/src/subscribers/order-refunded.js +56 -0
  59. package/.medusa/server/src/subscribers/repair-customer-reminder.js +87 -0
  60. package/.medusa/server/src/subscribers/repair-status-changed.js +88 -0
  61. package/.medusa/server/src/utils/generate-repair-document.js +237 -0
  62. package/.medusa/server/src/utils/templates/repair.js +115 -0
  63. package/.medusa/server/src/workflows/add-repair-parts-workflow.js +10 -0
  64. package/.medusa/server/src/workflows/approve-repair-cost-workflow.js +25 -0
  65. package/.medusa/server/src/workflows/create-repair-ticket-workflow.js +28 -0
  66. package/.medusa/server/src/workflows/reject-repair-cost-workflow.js +55 -0
  67. package/.medusa/server/src/workflows/remove-repair-part-workflow.js +10 -0
  68. package/.medusa/server/src/workflows/steps/add-repair-media.js +16 -0
  69. package/.medusa/server/src/workflows/steps/add-repair-note.js +16 -0
  70. package/.medusa/server/src/workflows/steps/add-repair-parts.js +104 -0
  71. package/.medusa/server/src/workflows/steps/add-repair-update.js +16 -0
  72. package/.medusa/server/src/workflows/steps/approve-repair-cost.js +30 -0
  73. package/.medusa/server/src/workflows/steps/create-device.js +16 -0
  74. package/.medusa/server/src/workflows/steps/create-repair-order.js +108 -0
  75. package/.medusa/server/src/workflows/steps/create-repair-ticket.js +26 -0
  76. package/.medusa/server/src/workflows/steps/emit-compliance-requested-event.js +17 -0
  77. package/.medusa/server/src/workflows/steps/emit-repair-status-changed-event.js +14 -0
  78. package/.medusa/server/src/workflows/steps/remove-repair-part.js +42 -0
  79. package/.medusa/server/src/workflows/steps/update-repair-costs.js +60 -0
  80. package/.medusa/server/src/workflows/steps/update-repair-ticket-status.js +47 -0
  81. package/.medusa/server/src/workflows/update-repair-status-workflow.js +23 -0
  82. package/README.md +95 -0
  83. package/package.json +81 -0
@@ -0,0 +1,1464 @@
1
+ "use strict";
2
+ const jsxRuntime = require("react/jsx-runtime");
3
+ const adminSdk = require("@medusajs/admin-sdk");
4
+ const ui = require("@medusajs/ui");
5
+ const react = require("react");
6
+ const icons = require("@medusajs/icons");
7
+ const reactRouterDom = require("react-router-dom");
8
+ const recharts = require("recharts");
9
+ const useStoreCurrency = () => {
10
+ const [currencyCode, setCurrencyCode] = react.useState("KES");
11
+ react.useEffect(() => {
12
+ fetch("/admin/stores", { credentials: "include" }).then((res) => res.json()).then((data) => {
13
+ var _a;
14
+ const store = data.store || data.stores && data.stores[0];
15
+ if (store) {
16
+ if (store.default_currency_code) {
17
+ setCurrencyCode(store.default_currency_code.toUpperCase());
18
+ } else if (((_a = store.supported_currencies) == null ? void 0 : _a.length) > 0) {
19
+ const defaultCurrency = store.supported_currencies.find((c) => c.is_default) || store.supported_currencies[0];
20
+ if (defaultCurrency == null ? void 0 : defaultCurrency.currency_code) {
21
+ setCurrencyCode(defaultCurrency.currency_code.toUpperCase());
22
+ }
23
+ }
24
+ }
25
+ }).catch((err) => {
26
+ console.error("Failed to load store currency", err);
27
+ });
28
+ }, []);
29
+ const formatCurrency = (amount) => {
30
+ return new Intl.NumberFormat(window.navigator.language || "en-US", {
31
+ style: "currency",
32
+ currency: currencyCode
33
+ }).format(amount / 100);
34
+ };
35
+ return { currencyCode, formatCurrency };
36
+ };
37
+ const RepairTicketWidget = ({ data }) => {
38
+ const [tickets, setTickets] = react.useState([]);
39
+ const [loading, setLoading] = react.useState(true);
40
+ const { formatCurrency } = useStoreCurrency();
41
+ react.useEffect(() => {
42
+ fetch(`/admin/repairs?customer_id=${data.id}`, {
43
+ credentials: "include"
44
+ }).then((res) => res.json()).then((data2) => {
45
+ setTickets(data2.repair_tickets || []);
46
+ setLoading(false);
47
+ }).catch((err) => {
48
+ console.error("Failed to load repair tickets:", err);
49
+ setLoading(false);
50
+ });
51
+ }, []);
52
+ const getStatusColor = (status) => {
53
+ const colors = {
54
+ received: "grey",
55
+ diagnosing: "blue",
56
+ awaiting_approval: "orange",
57
+ repairing: "blue",
58
+ ready: "green",
59
+ completed: "green",
60
+ cancelled: "red",
61
+ refunded: "red"
62
+ };
63
+ return colors[status] || "grey";
64
+ };
65
+ if (loading) {
66
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading repairs..." }) });
67
+ }
68
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
69
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Active Repair Tickets" }) }),
70
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-ui-fg-subtle px-6 py-4", children: tickets.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No active repair tickets" }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: tickets.slice(0, 5).map((ticket) => /* @__PURE__ */ jsxRuntime.jsxs(
71
+ "div",
72
+ {
73
+ className: "flex items-start justify-between border-b pb-3 last:border-b-0",
74
+ children: [
75
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
76
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
77
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: ticket.ticket_number }),
78
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: getStatusColor(ticket.status), size: "small", children: ticket.status.replace("_", " ") }),
79
+ ticket.technician_name && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "purple", size: "small", children: ticket.technician_name })
80
+ ] }),
81
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle mt-1", children: [
82
+ ticket.issue_description.substring(0, 60),
83
+ ticket.issue_description.length > 60 ? "..." : ""
84
+ ] }),
85
+ ticket.estimated_completion && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted mt-1", children: [
86
+ "ETC:",
87
+ " ",
88
+ new Date(
89
+ ticket.estimated_completion
90
+ ).toLocaleDateString()
91
+ ] })
92
+ ] }),
93
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-right", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: formatCurrency(ticket.total_estimate) }) })
94
+ ]
95
+ },
96
+ ticket.id
97
+ )) }) })
98
+ ] });
99
+ };
100
+ adminSdk.defineWidgetConfig({
101
+ zone: "customer.details.before"
102
+ });
103
+ const RepairsPage = () => {
104
+ const navigate = reactRouterDom.useNavigate();
105
+ const [tickets, setTickets] = react.useState([]);
106
+ const [loading, setLoading] = react.useState(true);
107
+ const [searchTerm, setSearchTerm] = react.useState("");
108
+ const [statusFilter, setStatusFilter] = react.useState("all");
109
+ const { formatCurrency } = useStoreCurrency();
110
+ const [createModalOpen, setCreateModalOpen] = react.useState(false);
111
+ const [isCreating, setIsCreating] = react.useState(false);
112
+ const [newDevice, setNewDevice] = react.useState({
113
+ serial_number: "",
114
+ model_name: "",
115
+ brand: "",
116
+ customer_id: "",
117
+ imei: "",
118
+ condition: ""
119
+ });
120
+ const [newTicket, setNewTicket] = react.useState({
121
+ customer_id: "",
122
+ issue_description: "",
123
+ terms_accepted: false,
124
+ data_wiped_consent: false
125
+ });
126
+ const [accessoryInput, setAccessoryInput] = react.useState("");
127
+ const [accessories, setAccessories] = react.useState([]);
128
+ const [customers, setCustomers] = react.useState([]);
129
+ const [loadingCustomers, setLoadingCustomers] = react.useState(false);
130
+ const loadCustomers = () => {
131
+ setLoadingCustomers(true);
132
+ fetch(`/admin/customers`, {
133
+ credentials: "include"
134
+ }).then((res) => res.json()).then((data) => {
135
+ setCustomers(data.customers || []);
136
+ setLoadingCustomers(false);
137
+ }).catch((err) => {
138
+ console.error("Failed to load customers:", err);
139
+ setLoadingCustomers(false);
140
+ });
141
+ };
142
+ const loadTickets = () => {
143
+ setLoading(true);
144
+ fetch(`/admin/repairs`, {
145
+ credentials: "include"
146
+ }).then((res) => res.json()).then((data) => {
147
+ setTickets(data.repair_tickets || []);
148
+ setLoading(false);
149
+ }).catch((err) => {
150
+ console.error("Failed to load repair tickets:", err);
151
+ setLoading(false);
152
+ });
153
+ };
154
+ react.useEffect(() => {
155
+ loadTickets();
156
+ loadCustomers();
157
+ }, []);
158
+ const handleAccessoryKeyDown = (e) => {
159
+ if (e.key === "," || e.key === "Enter") {
160
+ e.preventDefault();
161
+ const val = accessoryInput.trim().replace(",", "");
162
+ if (val && !accessories.includes(val)) {
163
+ setAccessories([...accessories, val]);
164
+ }
165
+ setAccessoryInput("");
166
+ }
167
+ };
168
+ const removeAccessory = (acc) => setAccessories(accessories.filter((a) => a !== acc));
169
+ const handleCreateTicket = async () => {
170
+ if (!newTicket.terms_accepted) {
171
+ alert("Customer must accept the Repair Terms & Conditions to proceed.");
172
+ return;
173
+ }
174
+ try {
175
+ setIsCreating(true);
176
+ const res = await fetch(`/admin/repairs`, {
177
+ method: "POST",
178
+ credentials: "include",
179
+ headers: { "Content-Type": "application/json" },
180
+ body: JSON.stringify({
181
+ device: {
182
+ serial_number: newDevice.serial_number,
183
+ model_name: newDevice.model_name,
184
+ brand: newDevice.brand,
185
+ customer_id: newDevice.customer_id || void 0,
186
+ imei: newDevice.imei || void 0,
187
+ condition: newDevice.condition || void 0
188
+ },
189
+ ticket: {
190
+ customer_id: newTicket.customer_id || void 0,
191
+ issue_description: newTicket.issue_description,
192
+ accessories: accessories.length > 0 ? accessories.join(", ") : void 0,
193
+ terms_accepted: newTicket.terms_accepted,
194
+ data_wiped_consent: newTicket.data_wiped_consent
195
+ }
196
+ })
197
+ });
198
+ const data = await res.json();
199
+ if (!res.ok) {
200
+ throw new Error(data.message || "Failed to create repair ticket");
201
+ }
202
+ setCreateModalOpen(false);
203
+ setNewDevice({
204
+ serial_number: "",
205
+ model_name: "",
206
+ brand: "",
207
+ customer_id: "",
208
+ imei: "",
209
+ condition: ""
210
+ });
211
+ setNewTicket({
212
+ customer_id: "",
213
+ issue_description: "",
214
+ terms_accepted: false,
215
+ data_wiped_consent: false
216
+ });
217
+ setAccessories([]);
218
+ setAccessoryInput("");
219
+ loadTickets();
220
+ } catch (err) {
221
+ console.error(err);
222
+ alert("Error: " + (err instanceof Error ? err.message : String(err)));
223
+ } finally {
224
+ setIsCreating(false);
225
+ }
226
+ };
227
+ const getStatusColor = (status) => {
228
+ const colors = {
229
+ received: "grey",
230
+ diagnosing: "blue",
231
+ awaiting_approval: "orange",
232
+ repairing: "blue",
233
+ ready: "green",
234
+ completed: "green",
235
+ cancelled: "red"
236
+ };
237
+ return colors[status] || "grey";
238
+ };
239
+ const getCustomerName = (customerId) => {
240
+ if (!customerId) return "No Customer";
241
+ const customer = customers.find((c) => c.id === customerId);
242
+ if (!customer) return "Unknown";
243
+ return customer.first_name || customer.last_name ? `${customer.first_name || ""} ${customer.last_name || ""}`.trim() : customer.email || "Unknown";
244
+ };
245
+ const filteredTickets = tickets.map((ticket) => {
246
+ const parseNum = (val) => {
247
+ if (typeof val === "object" && val !== null && "value" in val)
248
+ return Number(val.value);
249
+ if (val !== void 0) return Number(val);
250
+ return 0;
251
+ };
252
+ return {
253
+ ...ticket,
254
+ total_estimate: parseNum(ticket.total_estimate)
255
+ };
256
+ }).filter((ticket) => {
257
+ var _a;
258
+ const matchesSearch = ticket.ticket_number.toLowerCase().includes(searchTerm.toLowerCase()) || ticket.issue_description.toLowerCase().includes(searchTerm.toLowerCase()) || ((_a = ticket.technician_name) == null ? void 0 : _a.toLowerCase().includes(searchTerm.toLowerCase()));
259
+ const matchesStatus = statusFilter === "all" || ticket.status === statusFilter;
260
+ return matchesSearch && matchesStatus;
261
+ });
262
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { children: [
263
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-6", children: [
264
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Repair Tickets" }),
265
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.FocusModal, { open: createModalOpen, onOpenChange: setCreateModalOpen, children: [
266
+ /* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "primary", children: "Create Repair Ticket" }) }),
267
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.FocusModal.Content, { children: [
268
+ /* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(
269
+ ui.Button,
270
+ {
271
+ variant: "primary",
272
+ onClick: handleCreateTicket,
273
+ isLoading: isCreating,
274
+ children: "Save Ticket"
275
+ }
276
+ ) }),
277
+ /* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Body, { className: "flex flex-col items-center py-16 overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full max-w-lg flex-col gap-y-8", children: [
278
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
279
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Create Repair Ticket" }),
280
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-fg-subtle text-sm", children: "Add a new device and repair ticket." })
281
+ ] }),
282
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
283
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "text-lg", children: "Device Details" }),
284
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
285
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
286
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "brand", children: "Brand *" }),
287
+ /* @__PURE__ */ jsxRuntime.jsx(
288
+ ui.Input,
289
+ {
290
+ id: "brand",
291
+ value: newDevice.brand,
292
+ onChange: (e) => setNewDevice({ ...newDevice, brand: e.target.value }),
293
+ placeholder: "Apple"
294
+ }
295
+ )
296
+ ] }),
297
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
298
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "model_name", children: "Model Name *" }),
299
+ /* @__PURE__ */ jsxRuntime.jsx(
300
+ ui.Input,
301
+ {
302
+ id: "model_name",
303
+ value: newDevice.model_name,
304
+ onChange: (e) => setNewDevice({
305
+ ...newDevice,
306
+ model_name: e.target.value
307
+ }),
308
+ placeholder: "iPhone 13 Pro"
309
+ }
310
+ )
311
+ ] }),
312
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
313
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "serial_number", children: "Serial Number *" }),
314
+ /* @__PURE__ */ jsxRuntime.jsx(
315
+ ui.Input,
316
+ {
317
+ id: "serial_number",
318
+ value: newDevice.serial_number,
319
+ onChange: (e) => setNewDevice({
320
+ ...newDevice,
321
+ serial_number: e.target.value
322
+ }),
323
+ placeholder: "SN12345678"
324
+ }
325
+ )
326
+ ] }),
327
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
328
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "imei", children: "IMEI" }),
329
+ /* @__PURE__ */ jsxRuntime.jsx(
330
+ ui.Input,
331
+ {
332
+ id: "imei",
333
+ value: newDevice.imei,
334
+ onChange: (e) => setNewDevice({ ...newDevice, imei: e.target.value }),
335
+ placeholder: "Optional"
336
+ }
337
+ )
338
+ ] }),
339
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 col-span-2", children: [
340
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "condition", children: "Device Condition" }),
341
+ /* @__PURE__ */ jsxRuntime.jsx(
342
+ ui.Textarea,
343
+ {
344
+ id: "condition",
345
+ value: newDevice.condition,
346
+ onChange: (e) => setNewDevice({
347
+ ...newDevice,
348
+ condition: e.target.value
349
+ }),
350
+ placeholder: "Scratches on screen, etc.",
351
+ rows: 3
352
+ }
353
+ )
354
+ ] })
355
+ ] })
356
+ ] }),
357
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
358
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "text-lg", children: "Ticket Details" }),
359
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4", children: [
360
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
361
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "customer_id", children: "Customer" }),
362
+ /* @__PURE__ */ jsxRuntime.jsxs(
363
+ ui.Select,
364
+ {
365
+ value: newTicket.customer_id,
366
+ onValueChange: (val) => {
367
+ setNewTicket({ ...newTicket, customer_id: val });
368
+ setNewDevice({ ...newDevice, customer_id: val });
369
+ },
370
+ children: [
371
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(
372
+ ui.Select.Value,
373
+ {
374
+ placeholder: loadingCustomers ? "Loading..." : "Select a customer"
375
+ }
376
+ ) }),
377
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: customers.map((customer) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Item, { value: customer.id, children: [
378
+ customer.first_name || customer.last_name ? `${customer.first_name || ""} ${customer.last_name || ""}`.trim() : "No Name",
379
+ " ",
380
+ "(",
381
+ customer.email,
382
+ ")"
383
+ ] }, customer.id)) })
384
+ ]
385
+ }
386
+ )
387
+ ] }),
388
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
389
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "issue_description", children: "Issue Description *" }),
390
+ /* @__PURE__ */ jsxRuntime.jsx(
391
+ ui.Textarea,
392
+ {
393
+ id: "issue_description",
394
+ value: newTicket.issue_description,
395
+ onChange: (e) => setNewTicket({
396
+ ...newTicket,
397
+ issue_description: e.target.value
398
+ }),
399
+ placeholder: "Screen is cracked and battery draining fast."
400
+ }
401
+ )
402
+ ] }),
403
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
404
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "accessories", children: "Accessories Included (press comma to add)" }),
405
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2 mb-2", children: accessories.map((acc) => /* @__PURE__ */ jsxRuntime.jsxs(
406
+ ui.Badge,
407
+ {
408
+ color: "grey",
409
+ size: "small",
410
+ className: "flex items-center gap-1",
411
+ children: [
412
+ acc,
413
+ /* @__PURE__ */ jsxRuntime.jsx(
414
+ "button",
415
+ {
416
+ type: "button",
417
+ onClick: () => removeAccessory(acc),
418
+ className: "text-ui-fg-muted hover:text-ui-fg-base",
419
+ children: "×"
420
+ }
421
+ )
422
+ ]
423
+ },
424
+ acc
425
+ )) }),
426
+ /* @__PURE__ */ jsxRuntime.jsx(
427
+ ui.Input,
428
+ {
429
+ id: "accessories",
430
+ value: accessoryInput,
431
+ onChange: (e) => setAccessoryInput(e.target.value),
432
+ onKeyDown: handleAccessoryKeyDown,
433
+ placeholder: "Type and press comma..."
434
+ }
435
+ )
436
+ ] })
437
+ ] })
438
+ ] }),
439
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
440
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "text-lg", children: "Legal & Compliance" }),
441
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
442
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
443
+ /* @__PURE__ */ jsxRuntime.jsx(
444
+ ui.Checkbox,
445
+ {
446
+ id: "terms_accepted",
447
+ checked: newTicket.terms_accepted,
448
+ onCheckedChange: (c) => setNewTicket({
449
+ ...newTicket,
450
+ terms_accepted: c === true
451
+ })
452
+ }
453
+ ),
454
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "terms_accepted", children: "Customer accepts Repair Terms & Conditions *" })
455
+ ] }),
456
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
457
+ /* @__PURE__ */ jsxRuntime.jsx(
458
+ ui.Checkbox,
459
+ {
460
+ id: "data_wiped_consent",
461
+ checked: newTicket.data_wiped_consent,
462
+ onCheckedChange: (c) => setNewTicket({
463
+ ...newTicket,
464
+ data_wiped_consent: c === true
465
+ })
466
+ }
467
+ ),
468
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "data_wiped_consent", children: "Customer consents to device data wipe (if required)" })
469
+ ] })
470
+ ] })
471
+ ] })
472
+ ] }) })
473
+ ] })
474
+ ] })
475
+ ] }),
476
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-4 mb-6", children: [
477
+ /* @__PURE__ */ jsxRuntime.jsx(
478
+ ui.Input,
479
+ {
480
+ placeholder: "Search tickets...",
481
+ value: searchTerm,
482
+ onChange: (e) => setSearchTerm(e.target.value),
483
+ className: "flex-1"
484
+ }
485
+ ),
486
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: statusFilter, onValueChange: setStatusFilter, children: [
487
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Filter by status" }) }),
488
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
489
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: "All Statuses" }),
490
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "received", children: "Received" }),
491
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "diagnosing", children: "Diagnosing" }),
492
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "awaiting_approval", children: "Awaiting Approval" }),
493
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "repairing", children: "Repairing" }),
494
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "ready", children: "Ready" }),
495
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "completed", children: "Completed" }),
496
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "cancelled", children: "Cancelled" })
497
+ ] })
498
+ ] })
499
+ ] }),
500
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: "Loading repair tickets..." }) : filteredTickets.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-12 text-ui-fg-subtle", children: [
501
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Wrench, { className: "mx-auto mb-4", size: 48 }),
502
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No repair tickets found" })
503
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
504
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
505
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Ticket #" }),
506
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
507
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Technician" }),
508
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Issue" }),
509
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Estimate" }),
510
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Created" }),
511
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
512
+ ] }) }),
513
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: filteredTickets.map((ticket) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
514
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
515
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: ticket.ticket_number }),
516
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs", children: getCustomerName(ticket.customer_id) })
517
+ ] }),
518
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: getStatusColor(ticket.status), size: "small", children: ticket.status.replace("_", " ") }) }),
519
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: ticket.technician_name ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "purple", size: "small", children: ticket.technician_name }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-muted", children: "Unassigned" }) }),
520
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "max-w-xs truncate", children: ticket.issue_description }),
521
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: formatCurrency(ticket.total_estimate) }),
522
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: new Date(ticket.created_at).toLocaleDateString() }),
523
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => navigate(`/repairs/${ticket.id}`), children: "View" }) })
524
+ ] }, ticket.id)) })
525
+ ] })
526
+ ] });
527
+ };
528
+ const config$1 = adminSdk.defineRouteConfig({
529
+ label: "Repair",
530
+ icon: icons.Wrench
531
+ });
532
+ const useParams = () => {
533
+ const path = window.location.pathname;
534
+ const parts = path.split("/");
535
+ const id = parts[parts.length - 1];
536
+ return { id };
537
+ };
538
+ const RepairDetailPage = () => {
539
+ const { id } = useParams();
540
+ const navigate = reactRouterDom.useNavigate();
541
+ const [ticket, setTicket] = react.useState(null);
542
+ const [loading, setLoading] = react.useState(true);
543
+ const { formatCurrency } = useStoreCurrency();
544
+ const [newStatus, setNewStatus] = react.useState("");
545
+ const [unifiedMessage, setUnifiedMessage] = react.useState("");
546
+ const [messageType, setMessageType] = react.useState("chat");
547
+ const [technicianName, setTechnicianName] = react.useState("");
548
+ const [technicianId, setTechnicianId] = react.useState("");
549
+ const [laborCost, setLaborCost] = react.useState("");
550
+ const [etc, setEtc] = react.useState("");
551
+ const [inventoryParts, setInventoryParts] = react.useState([]);
552
+ const [selectedInventoryPart, setSelectedInventoryPart] = react.useState("");
553
+ const [isAddingPart, setIsAddingPart] = react.useState(false);
554
+ const [customPartName, setCustomPartName] = react.useState("");
555
+ const [customPartPrice, setCustomPartPrice] = react.useState("");
556
+ const [technicianSearch, setTechnicianSearch] = react.useState("");
557
+ const [technicianOptions, setTechnicianOptions] = react.useState([]);
558
+ const [showTechnicianDropdown, setShowTechnicianDropdown] = react.useState(false);
559
+ const [isSendingReminder, setIsSendingReminder] = react.useState(false);
560
+ react.useEffect(() => {
561
+ const delayDebounceFn = setTimeout(() => {
562
+ if (!technicianSearch) {
563
+ setTechnicianOptions([]);
564
+ return;
565
+ }
566
+ Promise.all([
567
+ fetch(`/admin/users?q=${technicianSearch}`, { credentials: "include" }).then((res) => res.json()),
568
+ fetch(`/admin/customers?q=${technicianSearch}`, { credentials: "include" }).then((res) => res.json())
569
+ ]).then(([usersData, customersData]) => {
570
+ const admins = (usersData.users || []).map((u) => ({ ...u, type: "Admin" }));
571
+ const customers = (customersData.customers || []).map((c) => ({ ...c, type: "Customer" }));
572
+ setTechnicianOptions([...admins, ...customers]);
573
+ }).catch((err) => console.error("Search failed", err));
574
+ }, 300);
575
+ return () => clearTimeout(delayDebounceFn);
576
+ }, [technicianSearch]);
577
+ const loadTicket = async () => {
578
+ try {
579
+ setLoading(true);
580
+ const res = await fetch(`/admin/repairs/${id}`, {
581
+ credentials: "include"
582
+ });
583
+ const data = await res.json();
584
+ if (!res.ok) {
585
+ throw new Error(data.message || "Failed to fetch ticket");
586
+ }
587
+ const t = data.repair_ticket;
588
+ const parseNum = (val) => {
589
+ if (typeof val === "object" && val !== null && "value" in val)
590
+ return Number(val.value);
591
+ if (val !== void 0) return Number(val);
592
+ return 0;
593
+ };
594
+ t.labor_estimate = parseNum(t.labor_estimate);
595
+ t.parts_estimate = parseNum(t.parts_estimate);
596
+ t.total_estimate = parseNum(t.total_estimate);
597
+ t.labor_actual = parseNum(t.labor_actual);
598
+ t.parts_actual = parseNum(t.parts_actual);
599
+ t.total_actual = parseNum(t.total_actual);
600
+ t.custom_parts = t.custom_parts || [];
601
+ setTicket(t);
602
+ setNewStatus(t.status);
603
+ setTechnicianName(t.technician_name || "");
604
+ setTechnicianId(t.technician_id || "");
605
+ setLaborCost((t.labor_estimate / 100).toFixed(2));
606
+ setEtc(
607
+ t.estimated_completion ? t.estimated_completion.split("T")[0] : ""
608
+ );
609
+ loadInventoryParts();
610
+ } catch (err) {
611
+ console.error("Failed to load repair ticket:", err);
612
+ } finally {
613
+ setLoading(false);
614
+ }
615
+ };
616
+ const loadInventoryParts = async () => {
617
+ try {
618
+ const res = await fetch(`/admin/parts`, {
619
+ credentials: "include"
620
+ });
621
+ const data = await res.json();
622
+ if (res.ok) {
623
+ setInventoryParts(data.parts || []);
624
+ }
625
+ } catch (err) {
626
+ }
627
+ };
628
+ react.useEffect(() => {
629
+ if (id) {
630
+ loadTicket();
631
+ }
632
+ }, [id]);
633
+ const handleAddInventoryPart = async () => {
634
+ if (!selectedInventoryPart) return;
635
+ try {
636
+ setIsAddingPart(true);
637
+ await fetch(`/admin/repairs/${id}/parts`, {
638
+ method: "POST",
639
+ credentials: "include",
640
+ headers: { "Content-Type": "application/json" },
641
+ body: JSON.stringify({ variant_ids: [selectedInventoryPart] })
642
+ });
643
+ ui.toast.success("Inventory part added");
644
+ setSelectedInventoryPart("");
645
+ loadTicket();
646
+ } catch (err) {
647
+ ui.toast.error("Failed to add part");
648
+ } finally {
649
+ setIsAddingPart(false);
650
+ }
651
+ };
652
+ const handleRemoveInventoryPart = async (variantId) => {
653
+ try {
654
+ setIsAddingPart(true);
655
+ await fetch(`/admin/repairs/${id}/parts/${variantId}`, {
656
+ method: "DELETE",
657
+ credentials: "include"
658
+ });
659
+ ui.toast.success("Inventory part removed");
660
+ loadTicket();
661
+ } catch (err) {
662
+ ui.toast.error("Failed to remove part");
663
+ } finally {
664
+ setIsAddingPart(false);
665
+ }
666
+ };
667
+ const handleAddCustomPart = async () => {
668
+ if (!customPartName || customPartPrice === "") return;
669
+ try {
670
+ setIsAddingPart(true);
671
+ await fetch(`/admin/repairs/${id}/custom-parts`, {
672
+ method: "POST",
673
+ credentials: "include",
674
+ headers: { "Content-Type": "application/json" },
675
+ body: JSON.stringify({
676
+ name: customPartName,
677
+ price: Number(customPartPrice)
678
+ })
679
+ });
680
+ ui.toast.success("Custom part added");
681
+ setCustomPartName("");
682
+ setCustomPartPrice("");
683
+ loadTicket();
684
+ } catch (err) {
685
+ ui.toast.error("Failed to add custom part");
686
+ } finally {
687
+ setIsAddingPart(false);
688
+ }
689
+ };
690
+ const handleSendUnified = async () => {
691
+ if (!unifiedMessage.trim()) return;
692
+ try {
693
+ if (messageType === "chat") {
694
+ await fetch(`/admin/repairs/${id}/messages`, {
695
+ method: "POST",
696
+ credentials: "include",
697
+ headers: { "Content-Type": "application/json" },
698
+ body: JSON.stringify({ message: unifiedMessage })
699
+ });
700
+ ui.toast.success("Message sent");
701
+ } else {
702
+ await fetch(`/admin/repairs/${id}/notes`, {
703
+ method: "POST",
704
+ credentials: "include",
705
+ headers: { "Content-Type": "application/json" },
706
+ body: JSON.stringify({
707
+ content: unifiedMessage,
708
+ is_internal: messageType === "internal_note"
709
+ })
710
+ });
711
+ ui.toast.success("Note added");
712
+ }
713
+ setUnifiedMessage("");
714
+ loadTicket();
715
+ } catch (err) {
716
+ ui.toast.error("Failed to add entry");
717
+ }
718
+ };
719
+ const handleSendReminder = async () => {
720
+ try {
721
+ setIsSendingReminder(true);
722
+ await fetch(`/admin/repairs/${id}/remind`, {
723
+ method: "POST",
724
+ credentials: "include"
725
+ });
726
+ ui.toast.success("Reminder sent successfully");
727
+ } catch (err) {
728
+ ui.toast.error("Failed to send reminder");
729
+ } finally {
730
+ setIsSendingReminder(false);
731
+ }
732
+ };
733
+ const handleUpdateCosts = async () => {
734
+ try {
735
+ const promises = [];
736
+ if (laborCost !== "") {
737
+ const laborAmount = Math.round(parseFloat(laborCost) * 100);
738
+ if (!isNaN(laborAmount)) {
739
+ promises.push(
740
+ fetch(`/admin/repairs/${id}/costs`, {
741
+ method: "POST",
742
+ credentials: "include",
743
+ headers: { "Content-Type": "application/json" },
744
+ body: JSON.stringify({
745
+ labor_estimate: laborAmount
746
+ })
747
+ })
748
+ );
749
+ }
750
+ }
751
+ promises.push(
752
+ fetch(`/admin/repairs/${id}/details`, {
753
+ method: "POST",
754
+ credentials: "include",
755
+ headers: { "Content-Type": "application/json" },
756
+ body: JSON.stringify({
757
+ estimated_completion: etc ? new Date(etc).toISOString() : null,
758
+ technician_name: technicianName || null,
759
+ technician_id: technicianId || null
760
+ })
761
+ })
762
+ );
763
+ if (ticket && newStatus !== ticket.status) {
764
+ promises.push(
765
+ fetch(`/admin/repairs/${id}/status`, {
766
+ method: "POST",
767
+ credentials: "include",
768
+ headers: { "Content-Type": "application/json" },
769
+ body: JSON.stringify({ status: newStatus })
770
+ })
771
+ );
772
+ }
773
+ await Promise.all(promises);
774
+ ui.toast.success("Details updated");
775
+ loadTicket();
776
+ } catch (err) {
777
+ ui.toast.error("Failed to update details");
778
+ }
779
+ };
780
+ const handleManualApprove = async () => {
781
+ try {
782
+ setLoading(true);
783
+ const res = await fetch(`/admin/repairs/${id}/approve`, {
784
+ method: "POST",
785
+ headers: {
786
+ "Content-Type": "application/json"
787
+ }
788
+ });
789
+ if (!res.ok) {
790
+ throw new Error("Failed to approve ticket");
791
+ }
792
+ await loadTicket();
793
+ } catch (err) {
794
+ console.error(err);
795
+ } finally {
796
+ setLoading(false);
797
+ }
798
+ };
799
+ const getStatusColor = (status) => {
800
+ const colors = {
801
+ received: "grey",
802
+ diagnosing: "blue",
803
+ awaiting_approval: "orange",
804
+ repairing: "blue",
805
+ ready: "green",
806
+ completed: "green",
807
+ cancelled: "red"
808
+ };
809
+ return colors[status] || "grey";
810
+ };
811
+ if (loading) {
812
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading repair ticket..." }) });
813
+ }
814
+ if (!ticket) {
815
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Repair ticket not found" }) });
816
+ }
817
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
818
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Toaster, {}),
819
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between", children: [
820
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
821
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
822
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: ticket.ticket_number }),
823
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: getStatusColor(ticket.status), children: ticket.status.replace("_", " ") }),
824
+ ticket.technician_name && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "purple", children: ticket.technician_name })
825
+ ] }),
826
+ ticket.device && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-ui-fg-subtle mt-2", children: [
827
+ ticket.device.brand,
828
+ " ",
829
+ ticket.device.model_name,
830
+ " - S/N:",
831
+ " ",
832
+ ticket.device.serial_number
833
+ ] })
834
+ ] }),
835
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
836
+ /* @__PURE__ */ jsxRuntime.jsx(
837
+ "a",
838
+ {
839
+ href: `/admin/repairs/${id}/document?type=quote`,
840
+ target: "_blank",
841
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", children: "Quote PDF" })
842
+ }
843
+ ),
844
+ /* @__PURE__ */ jsxRuntime.jsx(
845
+ "a",
846
+ {
847
+ href: `/admin/repairs/${id}/document?type=invoice`,
848
+ target: "_blank",
849
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", children: "Invoice PDF" })
850
+ }
851
+ ),
852
+ /* @__PURE__ */ jsxRuntime.jsx(
853
+ "a",
854
+ {
855
+ href: `/admin/repairs/${id}/document?type=job_card`,
856
+ target: "_blank",
857
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", children: "Job Card" })
858
+ }
859
+ ),
860
+ /* @__PURE__ */ jsxRuntime.jsxs(
861
+ ui.Button,
862
+ {
863
+ variant: "secondary",
864
+ size: "small",
865
+ onClick: handleSendReminder,
866
+ disabled: isSendingReminder,
867
+ children: [
868
+ /* @__PURE__ */ jsxRuntime.jsx(icons.BellAlert, { className: "mr-1" }),
869
+ " Reminder"
870
+ ]
871
+ }
872
+ ),
873
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => navigate("/repairs"), children: "Back" })
874
+ ] })
875
+ ] }) }),
876
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-6", children: [
877
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
878
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { children: [
879
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Issue Details" }),
880
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
881
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
882
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Description" }),
883
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: ticket.issue_description })
884
+ ] }),
885
+ ticket.accessories && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
886
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Accessories" }),
887
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: ticket.accessories })
888
+ ] }),
889
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
890
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Created" }),
891
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: new Date(ticket.created_at).toLocaleString() })
892
+ ] }),
893
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-3 border-t", children: [
894
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Legal & Compliance" }),
895
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 mt-2", children: [
896
+ /* @__PURE__ */ jsxRuntime.jsxs(
897
+ ui.Badge,
898
+ {
899
+ color: ticket.terms_accepted ? "green" : "red",
900
+ size: "small",
901
+ children: [
902
+ "Terms ",
903
+ ticket.terms_accepted ? "Accepted" : "Not Accepted"
904
+ ]
905
+ }
906
+ ),
907
+ /* @__PURE__ */ jsxRuntime.jsxs(
908
+ ui.Badge,
909
+ {
910
+ color: ticket.data_wiped_consent ? "green" : "grey",
911
+ size: "small",
912
+ children: [
913
+ "Data Wipe",
914
+ " ",
915
+ ticket.data_wiped_consent ? "Consented" : "Not Consented"
916
+ ]
917
+ }
918
+ )
919
+ ] })
920
+ ] })
921
+ ] })
922
+ ] }),
923
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { children: [
924
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Cost Breakdown" }),
925
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
926
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
927
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Parts Estimate:" }),
928
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: formatCurrency(ticket.parts_estimate) })
929
+ ] }),
930
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
931
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Labor Estimate:" }),
932
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: formatCurrency(ticket.labor_estimate) })
933
+ ] }),
934
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between text-lg font-semibold border-t pt-2", children: [
935
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Total Estimate:" }),
936
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: formatCurrency(ticket.total_estimate) })
937
+ ] }),
938
+ ticket.is_approved ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "green", size: "small", className: "mt-2", children: [
939
+ "Approved on",
940
+ " ",
941
+ new Date(ticket.approved_at).toLocaleDateString()
942
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 pt-4 border-t", children: /* @__PURE__ */ jsxRuntime.jsx(
943
+ ui.Button,
944
+ {
945
+ variant: "primary",
946
+ size: "small",
947
+ onClick: handleManualApprove,
948
+ disabled: loading,
949
+ className: "w-full",
950
+ children: "Manually Approve & Create Order"
951
+ }
952
+ ) })
953
+ ] })
954
+ ] }),
955
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { children: [
956
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Parts & Inventory" }),
957
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 mb-6", children: [
958
+ ticket.parts && ticket.parts.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
959
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Inventory Parts Used" }),
960
+ ticket.parts.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(
961
+ "div",
962
+ {
963
+ className: "flex items-center justify-between p-2 bg-ui-bg-subtle rounded border text-sm",
964
+ children: [
965
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
966
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: p.title }),
967
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs", children: p.sku || "-" })
968
+ ] }),
969
+ /* @__PURE__ */ jsxRuntime.jsx(
970
+ ui.Button,
971
+ {
972
+ variant: "transparent",
973
+ className: "text-ui-fg-muted hover:text-ui-fg-base",
974
+ onClick: () => handleRemoveInventoryPart(p.id),
975
+ disabled: isAddingPart,
976
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
977
+ }
978
+ )
979
+ ]
980
+ },
981
+ p.id
982
+ ))
983
+ ] }),
984
+ ticket.custom_parts && ticket.custom_parts.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
985
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Custom Parts" }),
986
+ ticket.custom_parts.map((cp, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
987
+ "div",
988
+ {
989
+ className: "flex justify-between p-2 bg-ui-bg-subtle rounded border text-sm",
990
+ children: [
991
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: cp.name }),
992
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: formatCurrency(cp.price) })
993
+ ]
994
+ },
995
+ idx
996
+ ))
997
+ ] })
998
+ ] }),
999
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 pt-4 border-t", children: [
1000
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "text-sm", children: "Add Inventory Part" }),
1001
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
1002
+ /* @__PURE__ */ jsxRuntime.jsxs(
1003
+ ui.Select,
1004
+ {
1005
+ value: selectedInventoryPart,
1006
+ onValueChange: setSelectedInventoryPart,
1007
+ children: [
1008
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Select variant..." }) }),
1009
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: inventoryParts.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Item, { value: p.id, children: [
1010
+ p.title,
1011
+ " ",
1012
+ p.sku ? `(${p.sku})` : ""
1013
+ ] }, p.id)) })
1014
+ ]
1015
+ }
1016
+ ),
1017
+ /* @__PURE__ */ jsxRuntime.jsx(
1018
+ ui.Button,
1019
+ {
1020
+ variant: "secondary",
1021
+ onClick: handleAddInventoryPart,
1022
+ disabled: isAddingPart || !selectedInventoryPart,
1023
+ children: "Add"
1024
+ }
1025
+ )
1026
+ ] }),
1027
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "text-sm mt-4", children: "Add Custom Part" }),
1028
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
1029
+ /* @__PURE__ */ jsxRuntime.jsx(
1030
+ ui.Input,
1031
+ {
1032
+ className: "flex-1",
1033
+ placeholder: "Part description",
1034
+ value: customPartName,
1035
+ onChange: (e) => setCustomPartName(e.target.value)
1036
+ }
1037
+ ),
1038
+ /* @__PURE__ */ jsxRuntime.jsx(
1039
+ ui.Input,
1040
+ {
1041
+ type: "number",
1042
+ step: "0.01",
1043
+ className: "w-24",
1044
+ placeholder: "Cost",
1045
+ value: customPartPrice,
1046
+ onChange: (e) => setCustomPartPrice(e.target.value)
1047
+ }
1048
+ ),
1049
+ /* @__PURE__ */ jsxRuntime.jsx(
1050
+ ui.Button,
1051
+ {
1052
+ variant: "secondary",
1053
+ onClick: handleAddCustomPart,
1054
+ disabled: isAddingPart || !customPartName || !customPartPrice,
1055
+ children: "Add"
1056
+ }
1057
+ )
1058
+ ] })
1059
+ ] })
1060
+ ] }),
1061
+ ticket.media && ticket.media.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { children: [
1062
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Media" }),
1063
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-3", children: ticket.media.map((media) => /* @__PURE__ */ jsxRuntime.jsxs(
1064
+ "a",
1065
+ {
1066
+ href: media.file_url,
1067
+ target: "_blank",
1068
+ rel: "noopener noreferrer",
1069
+ className: "relative aspect-square rounded border overflow-hidden hover:opacity-80",
1070
+ children: [
1071
+ /* @__PURE__ */ jsxRuntime.jsx(
1072
+ "img",
1073
+ {
1074
+ src: media.file_url,
1075
+ alt: "Repair media",
1076
+ className: "w-full h-full object-cover"
1077
+ }
1078
+ ),
1079
+ /* @__PURE__ */ jsxRuntime.jsx(
1080
+ icons.ArrowUpRightOnBox,
1081
+ {
1082
+ className: "absolute top-2 right-2 text-white",
1083
+ size: 16
1084
+ }
1085
+ )
1086
+ ]
1087
+ },
1088
+ media.id
1089
+ )) })
1090
+ ] })
1091
+ ] }),
1092
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
1093
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { children: [
1094
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Update Details" }),
1095
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
1096
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
1097
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Technician" }),
1098
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 mt-1", children: [
1099
+ technicianName && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between p-2 bg-ui-bg-subtle border rounded-md", children: [
1100
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: technicianName }),
1101
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "transparent", size: "small", onClick: () => {
1102
+ setTechnicianName("");
1103
+ setTechnicianId("");
1104
+ }, children: "Clear" })
1105
+ ] }),
1106
+ !technicianName && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1107
+ /* @__PURE__ */ jsxRuntime.jsx(
1108
+ ui.Input,
1109
+ {
1110
+ placeholder: "Search by name or email...",
1111
+ value: technicianSearch,
1112
+ onChange: (e) => {
1113
+ setTechnicianSearch(e.target.value);
1114
+ setShowTechnicianDropdown(true);
1115
+ },
1116
+ onFocus: () => setShowTechnicianDropdown(true),
1117
+ onBlur: () => setTimeout(() => setShowTechnicianDropdown(false), 200)
1118
+ }
1119
+ ),
1120
+ showTechnicianDropdown && technicianOptions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute z-50 w-full mt-1 bg-ui-bg-base border rounded-md shadow-lg max-h-60 overflow-y-auto", children: technicianOptions.map((opt) => /* @__PURE__ */ jsxRuntime.jsxs(
1121
+ "div",
1122
+ {
1123
+ className: "p-2 cursor-pointer hover:bg-ui-bg-subtle-hover flex justify-between items-center",
1124
+ onClick: () => {
1125
+ setTechnicianName(`${opt.first_name} ${opt.last_name}`);
1126
+ setTechnicianId(opt.id);
1127
+ setTechnicianSearch("");
1128
+ setShowTechnicianDropdown(false);
1129
+ },
1130
+ children: [
1131
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { children: [
1132
+ opt.first_name,
1133
+ " ",
1134
+ opt.last_name
1135
+ ] }),
1136
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1137
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: opt.email }),
1138
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "small", color: opt.type === "Admin" ? "purple" : "blue", children: opt.type })
1139
+ ] })
1140
+ ]
1141
+ },
1142
+ opt.id
1143
+ )) })
1144
+ ] })
1145
+ ] })
1146
+ ] }),
1147
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1148
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Status" }),
1149
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: newStatus, onValueChange: setNewStatus, children: [
1150
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
1151
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
1152
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "received", children: "Received" }),
1153
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "diagnosing", children: "Diagnosing" }),
1154
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "awaiting_approval", children: "Awaiting Approval" }),
1155
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Item, { value: "repairing", disabled: !(ticket == null ? void 0 : ticket.is_approved), children: [
1156
+ "Repairing ",
1157
+ !(ticket == null ? void 0 : ticket.is_approved) && "(Requires Approval)"
1158
+ ] }),
1159
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Item, { value: "ready", disabled: !(ticket == null ? void 0 : ticket.is_approved), children: [
1160
+ "Ready ",
1161
+ !(ticket == null ? void 0 : ticket.is_approved) && "(Requires Approval)"
1162
+ ] }),
1163
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Item, { value: "completed", disabled: !(ticket == null ? void 0 : ticket.is_approved), children: [
1164
+ "Completed ",
1165
+ !(ticket == null ? void 0 : ticket.is_approved) && "(Requires Approval)"
1166
+ ] }),
1167
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "cancelled", children: "Cancelled" }),
1168
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "refunded", children: "Refunded" })
1169
+ ] })
1170
+ ] }),
1171
+ !(ticket == null ? void 0 : ticket.is_approved) && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-error mt-1", children: "Customer must approve estimate before starting work." })
1172
+ ] }),
1173
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1174
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Labor Cost" }),
1175
+ /* @__PURE__ */ jsxRuntime.jsx(
1176
+ ui.Input,
1177
+ {
1178
+ type: "number",
1179
+ step: "0.01",
1180
+ value: laborCost,
1181
+ onChange: (e) => setLaborCost(e.target.value),
1182
+ placeholder: "0.00"
1183
+ }
1184
+ )
1185
+ ] }),
1186
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1187
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Estimated Completion" }),
1188
+ /* @__PURE__ */ jsxRuntime.jsx(
1189
+ ui.Input,
1190
+ {
1191
+ type: "date",
1192
+ value: etc,
1193
+ onChange: (e) => setEtc(e.target.value)
1194
+ }
1195
+ )
1196
+ ] }),
1197
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2 mt-4", children: /* @__PURE__ */ jsxRuntime.jsx(
1198
+ ui.Button,
1199
+ {
1200
+ onClick: handleUpdateCosts,
1201
+ variant: "primary",
1202
+ className: "w-full",
1203
+ children: "Save Details"
1204
+ }
1205
+ ) })
1206
+ ] })
1207
+ ] }),
1208
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { children: [
1209
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-4", children: [
1210
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ChatBubbleLeftRight, { size: 20 }),
1211
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Timeline & Communication" })
1212
+ ] }),
1213
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
1214
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2 max-h-80 overflow-y-auto mb-4 border rounded bg-ui-bg-subtle/50 p-2", children: (() => {
1215
+ const items = [
1216
+ ...(ticket.notes || []).map((n) => ({
1217
+ ...n,
1218
+ entryType: "note"
1219
+ })),
1220
+ ...(ticket.updates || []).map((u) => ({
1221
+ ...u,
1222
+ entryType: "update"
1223
+ }))
1224
+ ].sort(
1225
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
1226
+ );
1227
+ if (items.length === 0) {
1228
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted p-2 text-center text-sm", children: "No activity yet." });
1229
+ }
1230
+ return items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
1231
+ "div",
1232
+ {
1233
+ className: `p-3 rounded border bg-ui-bg-base ${item.entryType === "update" && item.author_type !== "customer" ? "ml-8" : item.entryType === "update" && item.author_type === "customer" ? "mr-8" : ""}`,
1234
+ children: [
1235
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
1236
+ item.entryType === "note" ? /* @__PURE__ */ jsxRuntime.jsx(
1237
+ ui.Badge,
1238
+ {
1239
+ color: item.is_internal ? "orange" : "blue",
1240
+ size: "small",
1241
+ children: item.is_internal ? "Internal Note" : "Public Note"
1242
+ }
1243
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1244
+ ui.Badge,
1245
+ {
1246
+ color: item.author_type === "customer" ? "green" : "purple",
1247
+ size: "small",
1248
+ children: item.author_type === "customer" ? "Customer Msg" : "Technician Msg"
1249
+ }
1250
+ ),
1251
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: new Date(item.created_at).toLocaleString() })
1252
+ ] }),
1253
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "whitespace-pre-wrap", children: item.entryType === "note" ? item.content : item.message })
1254
+ ]
1255
+ },
1256
+ `${item.entryType}_${item.id}`
1257
+ ));
1258
+ })() }),
1259
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 p-3 bg-ui-bg-subtle rounded border", children: [
1260
+ /* @__PURE__ */ jsxRuntime.jsx(
1261
+ ui.Textarea,
1262
+ {
1263
+ value: unifiedMessage,
1264
+ onChange: (e) => setUnifiedMessage(e.target.value),
1265
+ placeholder: "Type a message or note...",
1266
+ rows: 3,
1267
+ className: "bg-ui-bg-base"
1268
+ }
1269
+ ),
1270
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
1271
+ /* @__PURE__ */ jsxRuntime.jsxs(
1272
+ ui.Select,
1273
+ {
1274
+ value: messageType,
1275
+ onValueChange: (val) => setMessageType(val),
1276
+ children: [
1277
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { className: "w-[180px] bg-ui-bg-base", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
1278
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
1279
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "chat", children: "Message Customer" }),
1280
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "internal_note", children: "Internal Note" }),
1281
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "public_note", children: "Public Note" })
1282
+ ] })
1283
+ ]
1284
+ }
1285
+ ),
1286
+ /* @__PURE__ */ jsxRuntime.jsx(
1287
+ ui.Button,
1288
+ {
1289
+ onClick: handleSendUnified,
1290
+ variant: "primary",
1291
+ className: "flex-1",
1292
+ children: "Send / Add"
1293
+ }
1294
+ )
1295
+ ] })
1296
+ ] })
1297
+ ] })
1298
+ ] })
1299
+ ] })
1300
+ ] })
1301
+ ] });
1302
+ };
1303
+ const ReportsPage = () => {
1304
+ const [data, setData] = react.useState(null);
1305
+ const [loading, setLoading] = react.useState(true);
1306
+ const { formatCurrency } = useStoreCurrency();
1307
+ react.useEffect(() => {
1308
+ fetch(`/admin/repairs/analytics`, {
1309
+ credentials: "include"
1310
+ }).then((res) => res.json()).then((resData) => {
1311
+ setData(resData.analytics);
1312
+ setLoading(false);
1313
+ }).catch((err) => {
1314
+ console.error("Failed to load analytics:", err);
1315
+ setLoading(false);
1316
+ });
1317
+ }, []);
1318
+ if (loading) {
1319
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-8", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading reports..." }) });
1320
+ }
1321
+ if (!data) {
1322
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-8", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No analytics data available." }) });
1323
+ }
1324
+ const chartData = Object.keys(data.status_counts).map((key) => ({
1325
+ name: key.charAt(0).toUpperCase() + key.slice(1).replace("_", " "),
1326
+ count: data.status_counts[key]
1327
+ }));
1328
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-8", children: [
1329
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-8", children: [
1330
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ChartBar, { className: "text-ui-fg-subtle" }),
1331
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Repair Analytics & Reports" })
1332
+ ] }),
1333
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12", children: [
1334
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 border rounded-lg bg-ui-bg-base border-ui-border-base shadow-sm", children: [
1335
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-1", children: "Total Repairs" }),
1336
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "text-3xl", children: data.total_repairs })
1337
+ ] }),
1338
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 border rounded-lg bg-ui-bg-base border-ui-border-base shadow-sm", children: [
1339
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-1", children: "Total Revenue (Estimated)" }),
1340
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "text-3xl", children: formatCurrency(data.total_expected_revenue) })
1341
+ ] }),
1342
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 border rounded-lg bg-ui-bg-base border-ui-border-base shadow-sm", children: [
1343
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-1", children: "Completed Repairs" }),
1344
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "text-3xl", children: data.completed_count })
1345
+ ] }),
1346
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 border rounded-lg bg-ui-bg-base border-ui-border-base shadow-sm", children: [
1347
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-1", children: "Avg Repair Time (Days)" }),
1348
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "text-3xl", children: data.avg_repair_time_days.toFixed(1) })
1349
+ ] })
1350
+ ] }),
1351
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8", children: [
1352
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-ui-bg-base border border-ui-border-base rounded-lg p-6 shadow-sm h-96", children: [
1353
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-6", children: "Repairs by Status" }),
1354
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.BarChart, { data: chartData, children: [
1355
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3" }),
1356
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.XAxis, { dataKey: "name" }),
1357
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.YAxis, {}),
1358
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, {}),
1359
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Legend, {}),
1360
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Bar, { dataKey: "count", fill: "#8884d8", name: "Tickets" })
1361
+ ] }) })
1362
+ ] }),
1363
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-ui-bg-base border border-ui-border-base rounded-lg p-6 shadow-sm h-96", children: [
1364
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-6", children: "Revenue by Month (Parts vs Labor)" }),
1365
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(
1366
+ recharts.BarChart,
1367
+ {
1368
+ data: data.monthly_revenue,
1369
+ margin: { top: 20, right: 30, left: 20, bottom: 5 },
1370
+ children: [
1371
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3" }),
1372
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.XAxis, { dataKey: "month" }),
1373
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.YAxis, {}),
1374
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, { formatter: (value) => `$${Number(value).toFixed(2)}` }),
1375
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Legend, {}),
1376
+ /* @__PURE__ */ jsxRuntime.jsx(
1377
+ recharts.Bar,
1378
+ {
1379
+ dataKey: "partsRevenue",
1380
+ stackId: "a",
1381
+ fill: "#82ca9d",
1382
+ name: "Parts Revenue"
1383
+ }
1384
+ ),
1385
+ /* @__PURE__ */ jsxRuntime.jsx(
1386
+ recharts.Bar,
1387
+ {
1388
+ dataKey: "laborRevenue",
1389
+ stackId: "a",
1390
+ fill: "#ffc658",
1391
+ name: "Labor Revenue"
1392
+ }
1393
+ )
1394
+ ]
1395
+ }
1396
+ ) })
1397
+ ] })
1398
+ ] })
1399
+ ] });
1400
+ };
1401
+ const config = adminSdk.defineRouteConfig({
1402
+ label: "Repair Reports",
1403
+ icon: icons.ChartBar
1404
+ });
1405
+ const i18nTranslations0 = {};
1406
+ const widgetModule = { widgets: [
1407
+ {
1408
+ Component: RepairTicketWidget,
1409
+ zone: ["customer.details.before"],
1410
+ widgetId: "Widget-8fda"
1411
+ }
1412
+ ] };
1413
+ const routeModule = {
1414
+ routes: [
1415
+ {
1416
+ Component: RepairsPage,
1417
+ path: "/repairs"
1418
+ },
1419
+ {
1420
+ Component: RepairDetailPage,
1421
+ path: "/repairs/:id"
1422
+ },
1423
+ {
1424
+ Component: ReportsPage,
1425
+ path: "/repairs/reports"
1426
+ }
1427
+ ]
1428
+ };
1429
+ const menuItemModule = {
1430
+ menuItems: [
1431
+ {
1432
+ label: config$1.label,
1433
+ icon: config$1.icon,
1434
+ path: "/repairs",
1435
+ nested: void 0,
1436
+ rank: void 0,
1437
+ translationNs: void 0
1438
+ },
1439
+ {
1440
+ label: config.label,
1441
+ icon: config.icon,
1442
+ path: "/repairs/reports",
1443
+ nested: void 0,
1444
+ rank: void 0,
1445
+ translationNs: void 0
1446
+ }
1447
+ ]
1448
+ };
1449
+ const formModule = { customFields: {} };
1450
+ const displayModule = {
1451
+ displays: {}
1452
+ };
1453
+ const i18nModule = { resources: i18nTranslations0 };
1454
+ const layoutModule = { layouts: [] };
1455
+ const plugin = {
1456
+ widgetModule,
1457
+ routeModule,
1458
+ menuItemModule,
1459
+ formModule,
1460
+ displayModule,
1461
+ i18nModule,
1462
+ layoutModule
1463
+ };
1464
+ module.exports = plugin;