medusajs-documents-plugin 1.1.61 → 1.1.64
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.
- package/.medusa/server/src/admin/index.js +565 -142
- package/.medusa/server/src/admin/index.mjs +566 -143
- package/.medusa/server/src/api/admin/documents/manual/route.js +3 -3
- package/.medusa/server/src/api/admin/documents/sync/route.js +8 -5
- package/.medusa/server/src/modules/documents/service.js +198 -16
- package/.medusa/server/src/services/zoho-books.js +6 -1
- package/.medusa/server/src/subscribers/repair-documents.js +18 -7
- package/CHANGELOG.md +32 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { jsxs, jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
3
|
import { Container, Heading, Text, Button, Input, Select, Tooltip, IconButton, Switch, Label, Table, Badge, DropdownMenu, Textarea, FocusModal } from "@medusajs/ui";
|
|
4
4
|
import { useState, useRef, useEffect, useMemo, useCallback, Fragment } from "react";
|
|
5
|
-
import { DocumentText, ArrowPath, Plus, Sparkles, ChevronDown, ChevronRight, ArrowDownTray, PencilSquare, Envelope, Trash } from "@medusajs/icons";
|
|
5
|
+
import { DocumentText, ArrowPath, Plus, Sparkles, ChevronDown, ChevronRight, ArrowDownTray, PencilSquare, Envelope, Trash, MagnifyingGlass, XMark } from "@medusajs/icons";
|
|
6
6
|
function extractBaseReference(displayId) {
|
|
7
7
|
if (!displayId) return "";
|
|
8
8
|
const cleaned = String(displayId).trim();
|
|
@@ -329,31 +329,6 @@ The recipient confirms that the device has been inspected, tested in their prese
|
|
|
329
329
|
},
|
|
330
330
|
notes: "By accepting handover, the client acknowledges satisfactory return of all custody items and confirms device operability."
|
|
331
331
|
},
|
|
332
|
-
{
|
|
333
|
-
id: "proforma",
|
|
334
|
-
name: "Commercial Quotation / Proforma",
|
|
335
|
-
icon: "💼",
|
|
336
|
-
badge: "Cost Estimate",
|
|
337
|
-
title: "Commercial Quotation (Proforma)",
|
|
338
|
-
subject: "Commercial Quotation & Scope of Work - Urban Device Care",
|
|
339
|
-
body_text: `Thank you for choosing Urban Device Care. Below is an itemized commercial quotation detailing required OEM components, workshop labor, precision calibration, and expected turnaround schedule for your equipment overhaul.
|
|
340
|
-
|
|
341
|
-
This quotation is valid for 14 calendar days from the date of issue. Upon formal approval, component procurement and bench allocation will commence immediately.`,
|
|
342
|
-
items: [
|
|
343
|
-
{ title: "OEM Liquid Retina XDR Display Assembly", description: "Brand new original factory panel with factory color calibration", quantity: 1, unit_price: 42e3 },
|
|
344
|
-
{ title: "Clean-Room Workshop Installation & Sensor Alignment", description: "Certified clean-room installation, gasket replacement & TrueTone calibration", quantity: 1, unit_price: 6500 },
|
|
345
|
-
{ title: "Hydrophobic Dust & Water Ingress Seal Restoration", description: "OEM adhesive barrier pressure cured to IP68 rating", quantity: 1, unit_price: 2500 }
|
|
346
|
-
],
|
|
347
|
-
custom_fields: {
|
|
348
|
-
"Device Model": "Apple iPad Pro 12.9-inch (6th Gen - Wi-Fi + Cellular)",
|
|
349
|
-
"Serial / IMEI": "DLXW9201M93K",
|
|
350
|
-
"Ticket Reference": "RT-2024-4190",
|
|
351
|
-
"Estimator / Technician": "Bonifencia A.",
|
|
352
|
-
"Validity Period": "14 Calendar Days",
|
|
353
|
-
"Estimated Turnaround": "24 - 48 Hours"
|
|
354
|
-
},
|
|
355
|
-
notes: "All replaced hardware carries our standard 90-day warranty coverage. Payment accepted via M-Pesa, Bank Transfer, or Credit/Debit Card."
|
|
356
|
-
},
|
|
357
332
|
{
|
|
358
333
|
id: "trade_in",
|
|
359
334
|
name: "Device Trade-In Agreement",
|
|
@@ -377,27 +352,358 @@ The seller warrants that they are the sole lawful owner of the equipment, that i
|
|
|
377
352
|
"Evaluation Specialist": "Bonifencia A."
|
|
378
353
|
},
|
|
379
354
|
notes: "Ownership transfers immediately upon document execution. Valuation is binding and irrevocable once trade-in payout or store credit is issued."
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
id: "blank",
|
|
383
|
-
name: "Blank Custom Document",
|
|
384
|
-
icon: "📝",
|
|
385
|
-
badge: "Custom",
|
|
386
|
-
title: "Official Document",
|
|
387
|
-
subject: "Official Document from Urban Device Care",
|
|
388
|
-
body_text: "This official technical document confirms the specifications, service terms, and verified conditions documented by Urban Device Care for the specified customer asset.",
|
|
389
|
-
items: [
|
|
390
|
-
{ title: "Professional Hardware Inspection & Maintenance Service", description: "Certified technical evaluation and service clearance", quantity: 1, unit_price: 0 }
|
|
391
|
-
],
|
|
392
|
-
custom_fields: {
|
|
393
|
-
"Device Model": "Computing Asset / Hardware Module",
|
|
394
|
-
"Serial / IMEI": "SN-XXXX-XXXX",
|
|
395
|
-
"Ticket Reference": "RT-2024-0001",
|
|
396
|
-
"Technician": "Bonifencia A."
|
|
397
|
-
},
|
|
398
|
-
notes: "Standard terms and conditions apply. For inquiries, contact info@urbandevicecare.co.uk."
|
|
399
355
|
}
|
|
400
356
|
];
|
|
357
|
+
const CustomerAutofillCombobox = ({
|
|
358
|
+
candidates,
|
|
359
|
+
loading = false,
|
|
360
|
+
onSelectCustomer,
|
|
361
|
+
label = "Quick Autofill Customer Details"
|
|
362
|
+
}) => {
|
|
363
|
+
const [query, setQuery] = useState("");
|
|
364
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
365
|
+
const [autofilledNotice, setAutofilledNotice] = useState(null);
|
|
366
|
+
const [remoteCandidates, setRemoteCandidates] = useState([]);
|
|
367
|
+
const [isSearchingRemote, setIsSearchingRemote] = useState(false);
|
|
368
|
+
const containerRef = useRef(null);
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
const handleClickOutside = (e) => {
|
|
371
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
372
|
+
setIsOpen(false);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
376
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
377
|
+
}, []);
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
const q = query.trim();
|
|
380
|
+
if (q.length < 2) {
|
|
381
|
+
setRemoteCandidates([]);
|
|
382
|
+
setIsSearchingRemote(false);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const timer = setTimeout(async () => {
|
|
386
|
+
setIsSearchingRemote(true);
|
|
387
|
+
try {
|
|
388
|
+
const res = await fetch(`/admin/customers?q=${encodeURIComponent(q)}&fields=*addresses&limit=25`, {
|
|
389
|
+
credentials: "include"
|
|
390
|
+
});
|
|
391
|
+
if (res.ok) {
|
|
392
|
+
const data = await res.json().catch(() => ({}));
|
|
393
|
+
if (data.customers && Array.isArray(data.customers)) {
|
|
394
|
+
const mapped = data.customers.map((c) => {
|
|
395
|
+
var _a, _b, _c;
|
|
396
|
+
const fullName = [c.first_name, c.last_name].filter(Boolean).join(" ").trim();
|
|
397
|
+
const addr = ((_a = c.addresses) == null ? void 0 : _a[0]) || c.default_billing_address || c.billing_address;
|
|
398
|
+
const formattedAddr = addr ? [addr.address_1, addr.address_2, addr.city, addr.postal_code, addr.province, (_b = addr.country_code) == null ? void 0 : _b.toUpperCase()].filter(Boolean).join(", ") : "";
|
|
399
|
+
const company = c.company_name || (addr == null ? void 0 : addr.company) || ((_c = c.metadata) == null ? void 0 : _c.company_name) || "";
|
|
400
|
+
const name = fullName || company || c.email || "Customer";
|
|
401
|
+
return {
|
|
402
|
+
id: `remote-${c.id}`,
|
|
403
|
+
name,
|
|
404
|
+
company_name: company,
|
|
405
|
+
email: c.email || "",
|
|
406
|
+
phone: c.phone || (addr == null ? void 0 : addr.phone) || "",
|
|
407
|
+
address: formattedAddr,
|
|
408
|
+
source: "customer",
|
|
409
|
+
source_label: "Medusa Customer (Live)"
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
setRemoteCandidates(mapped);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
} finally {
|
|
417
|
+
setIsSearchingRemote(false);
|
|
418
|
+
}
|
|
419
|
+
}, 250);
|
|
420
|
+
return () => clearTimeout(timer);
|
|
421
|
+
}, [query]);
|
|
422
|
+
const allCandidates = useMemo(() => {
|
|
423
|
+
var _a, _b, _c, _d;
|
|
424
|
+
if (remoteCandidates.length === 0) return candidates;
|
|
425
|
+
const map = /* @__PURE__ */ new Map();
|
|
426
|
+
for (const c of candidates) {
|
|
427
|
+
const key = ((_a = c.email) == null ? void 0 : _a.toLowerCase()) || ((_b = c.phone) == null ? void 0 : _b.replace(/\s+/g, "")) || `${c.name.toLowerCase()}::${c.company_name.toLowerCase()}`;
|
|
428
|
+
if (key) map.set(key, c);
|
|
429
|
+
}
|
|
430
|
+
for (const c of remoteCandidates) {
|
|
431
|
+
const key = ((_c = c.email) == null ? void 0 : _c.toLowerCase()) || ((_d = c.phone) == null ? void 0 : _d.replace(/\s+/g, "")) || `${c.name.toLowerCase()}::${c.company_name.toLowerCase()}`;
|
|
432
|
+
if (key) {
|
|
433
|
+
if (!map.has(key)) {
|
|
434
|
+
map.set(key, c);
|
|
435
|
+
} else {
|
|
436
|
+
const existing = map.get(key);
|
|
437
|
+
if (!existing.address && c.address) existing.address = c.address;
|
|
438
|
+
if (!existing.company_name && c.company_name) existing.company_name = c.company_name;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return Array.from(map.values());
|
|
443
|
+
}, [candidates, remoteCandidates]);
|
|
444
|
+
const filteredCandidates = useMemo(() => {
|
|
445
|
+
const q = query.trim().toLowerCase();
|
|
446
|
+
if (!q) {
|
|
447
|
+
return allCandidates.slice(0, 10);
|
|
448
|
+
}
|
|
449
|
+
const cleanQ = q.replace(/\s+/g, "");
|
|
450
|
+
const digitsOnlyQ = cleanQ.replace(/\D/g, "");
|
|
451
|
+
return allCandidates.filter((c) => {
|
|
452
|
+
const name = c.name.toLowerCase();
|
|
453
|
+
const company = c.company_name.toLowerCase();
|
|
454
|
+
const email = c.email.toLowerCase();
|
|
455
|
+
const phone = c.phone.replace(/\s+/g, "").toLowerCase();
|
|
456
|
+
const digitsOnlyPhone = phone.replace(/\D/g, "");
|
|
457
|
+
const addr = c.address.toLowerCase();
|
|
458
|
+
const src = c.source_label.toLowerCase();
|
|
459
|
+
const phoneMatches = phone.includes(cleanQ) || digitsOnlyQ.length >= 4 && (digitsOnlyPhone.includes(digitsOnlyQ) || digitsOnlyQ.includes(digitsOnlyPhone));
|
|
460
|
+
return name.includes(q) || company.includes(q) || email.includes(q) || phoneMatches || addr.includes(q) || src.includes(q);
|
|
461
|
+
}).slice(0, 20);
|
|
462
|
+
}, [allCandidates, query]);
|
|
463
|
+
const handleSelect = (candidate) => {
|
|
464
|
+
onSelectCustomer(candidate);
|
|
465
|
+
const displayName = candidate.company_name ? `${candidate.name} (${candidate.company_name})` : candidate.name;
|
|
466
|
+
setAutofilledNotice(`✓ Autofilled: ${displayName}`);
|
|
467
|
+
setQuery("");
|
|
468
|
+
setIsOpen(false);
|
|
469
|
+
setTimeout(() => {
|
|
470
|
+
setAutofilledNotice(null);
|
|
471
|
+
}, 4e3);
|
|
472
|
+
};
|
|
473
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative flex flex-col gap-1.5 p-3 rounded-lg bg-ui-bg-subtle border border-ui-border-base", children: [
|
|
474
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
475
|
+
/* @__PURE__ */ jsxs(Label, { className: "text-xs font-semibold flex items-center gap-1.5 text-ui-fg-base", children: [
|
|
476
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "w-3.5 h-3.5 text-ui-fg-interactive" }),
|
|
477
|
+
label
|
|
478
|
+
] }),
|
|
479
|
+
autofilledNotice && /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-emerald-600 bg-emerald-50 dark:bg-emerald-950/40 px-2 py-0.5 rounded border border-emerald-200", children: autofilledNotice })
|
|
480
|
+
] }),
|
|
481
|
+
/* @__PURE__ */ jsx(Text, { className: "text-[11px] text-ui-fg-subtle", children: "Search existing customer records, repair tickets, or recent documents in Medusa to automatically populate customer fields." }),
|
|
482
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
483
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none text-ui-fg-muted", children: /* @__PURE__ */ jsx(MagnifyingGlass, { className: "w-4 h-4" }) }),
|
|
484
|
+
/* @__PURE__ */ jsx(
|
|
485
|
+
"input",
|
|
486
|
+
{
|
|
487
|
+
type: "text",
|
|
488
|
+
value: query,
|
|
489
|
+
onChange: (e) => {
|
|
490
|
+
setQuery(e.target.value);
|
|
491
|
+
setIsOpen(true);
|
|
492
|
+
},
|
|
493
|
+
onFocus: (e) => {
|
|
494
|
+
setIsOpen(true);
|
|
495
|
+
e.target.select();
|
|
496
|
+
},
|
|
497
|
+
placeholder: "Type name, company, email, phone, or ticket # to search & autofill...",
|
|
498
|
+
className: "w-full pl-8.5 pr-8 py-1.5 text-xs bg-ui-bg-base border border-ui-border-base rounded-md text-ui-fg-base placeholder:text-ui-fg-muted focus:outline-none focus:ring-1 focus:ring-ui-border-interactive focus:border-ui-border-interactive"
|
|
499
|
+
}
|
|
500
|
+
),
|
|
501
|
+
query && /* @__PURE__ */ jsx(
|
|
502
|
+
"button",
|
|
503
|
+
{
|
|
504
|
+
type: "button",
|
|
505
|
+
onClick: () => setQuery(""),
|
|
506
|
+
className: "absolute inset-y-0 right-0 pr-2.5 flex items-center text-ui-fg-muted hover:text-ui-fg-base",
|
|
507
|
+
children: /* @__PURE__ */ jsx(XMark, { className: "w-3.5 h-3.5" })
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
] }),
|
|
511
|
+
isOpen && /* @__PURE__ */ jsx("div", { className: "absolute top-full left-0 right-0 mt-1 z-50 bg-ui-bg-base border border-ui-border-base rounded-md shadow-lg max-h-64 overflow-y-auto divide-y divide-ui-border-base", children: loading || isSearchingRemote ? /* @__PURE__ */ jsxs("div", { className: "p-3 text-xs text-ui-fg-muted text-center flex items-center justify-center gap-2", children: [
|
|
512
|
+
/* @__PURE__ */ jsx(ArrowPath, { className: "w-3 h-3 animate-spin" }),
|
|
513
|
+
"Searching customer records..."
|
|
514
|
+
] }) : filteredCandidates.length === 0 ? /* @__PURE__ */ jsx("div", { className: "p-3 text-xs text-ui-fg-muted text-center", children: "No matching customer records found" }) : filteredCandidates.map((c) => {
|
|
515
|
+
const badgeColor = c.source === "customer" ? "blue" : c.source === "repair" ? "orange" : "green";
|
|
516
|
+
return /* @__PURE__ */ jsxs(
|
|
517
|
+
"div",
|
|
518
|
+
{
|
|
519
|
+
onClick: () => handleSelect(c),
|
|
520
|
+
className: "p-2.5 hover:bg-ui-bg-subtle-hover cursor-pointer transition-colors",
|
|
521
|
+
children: [
|
|
522
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
|
|
523
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 truncate", children: [
|
|
524
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-xs text-ui-fg-base truncate", children: c.name }),
|
|
525
|
+
c.company_name && c.company_name !== c.name && /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-ui-fg-muted truncate", children: [
|
|
526
|
+
"(",
|
|
527
|
+
c.company_name,
|
|
528
|
+
")"
|
|
529
|
+
] })
|
|
530
|
+
] }),
|
|
531
|
+
/* @__PURE__ */ jsx(Badge, { size: "small", color: badgeColor, className: "shrink-0 text-[10px]", children: c.source_label })
|
|
532
|
+
] }),
|
|
533
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-[11px] text-ui-fg-subtle truncate", children: [
|
|
534
|
+
c.email && /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
535
|
+
"📧 ",
|
|
536
|
+
c.email
|
|
537
|
+
] }),
|
|
538
|
+
c.phone && /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
539
|
+
"📞 ",
|
|
540
|
+
c.phone
|
|
541
|
+
] }),
|
|
542
|
+
c.address && /* @__PURE__ */ jsxs("span", { className: "truncate max-w-[200px]", children: [
|
|
543
|
+
"📍 ",
|
|
544
|
+
c.address
|
|
545
|
+
] })
|
|
546
|
+
] })
|
|
547
|
+
]
|
|
548
|
+
},
|
|
549
|
+
c.id
|
|
550
|
+
);
|
|
551
|
+
}) })
|
|
552
|
+
] });
|
|
553
|
+
};
|
|
554
|
+
const RepairTicketCombobox = ({
|
|
555
|
+
tickets,
|
|
556
|
+
selectedTicketId,
|
|
557
|
+
onSelectTicket,
|
|
558
|
+
onClear,
|
|
559
|
+
loading = false
|
|
560
|
+
}) => {
|
|
561
|
+
const [query, setQuery] = useState("");
|
|
562
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
563
|
+
const containerRef = useRef(null);
|
|
564
|
+
const getTicketLabel = (t) => {
|
|
565
|
+
var _a, _b, _c, _d, _e;
|
|
566
|
+
const cleanNum = String(t.ticket_number || t.id).replace(/^(RT-|JOB-|CERT-)+/i, "");
|
|
567
|
+
const displayRef = `RT-${cleanNum}`;
|
|
568
|
+
const custName = ((_a = t.customer) == null ? void 0 : _a.company_name) || t.company_name || t.customer_name || (((_b = t.customer) == null ? void 0 : _b.first_name) ? `${t.customer.first_name} ${t.customer.last_name || ""}`.trim() : "") || "Customer";
|
|
569
|
+
const brandModel = [(_c = t.device) == null ? void 0 : _c.brand, (_d = t.device) == null ? void 0 : _d.model_name].filter(Boolean).join(" ").trim() || ((_e = t.custom_fields) == null ? void 0 : _e["Device Model"]) || "Device";
|
|
570
|
+
return `${displayRef} — ${custName} (${brandModel})`;
|
|
571
|
+
};
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (!selectedTicketId) {
|
|
574
|
+
setQuery("");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const t = tickets.find((item) => item.id === selectedTicketId || item.ticket_number === selectedTicketId);
|
|
578
|
+
if (t) {
|
|
579
|
+
setQuery(getTicketLabel(t));
|
|
580
|
+
}
|
|
581
|
+
}, [selectedTicketId, tickets]);
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
const handleClickOutside = (e) => {
|
|
584
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
585
|
+
setIsOpen(false);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
589
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
590
|
+
}, []);
|
|
591
|
+
const filteredTickets = useMemo(() => {
|
|
592
|
+
const q = query.trim().toLowerCase();
|
|
593
|
+
const cleanQ = q.replace(/^(?:(?:rt|job|cert)[-–—\s#]*|[#])/i, "").trim().toLowerCase();
|
|
594
|
+
const digitsOnlyQ = cleanQ.replace(/\D/g, "");
|
|
595
|
+
const selectedTicket = tickets.find((item) => item.id === selectedTicketId || item.ticket_number === selectedTicketId);
|
|
596
|
+
const selectedLabel = selectedTicket ? getTicketLabel(selectedTicket).toLowerCase() : "";
|
|
597
|
+
const isUnmodifiedSelection = Boolean(selectedTicket && q === selectedLabel);
|
|
598
|
+
if (!q || isUnmodifiedSelection) {
|
|
599
|
+
return tickets.slice(0, 30);
|
|
600
|
+
}
|
|
601
|
+
return tickets.filter((t) => {
|
|
602
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
603
|
+
const cleanNum = String(t.ticket_number || t.id).replace(/^(RT-|JOB-|CERT-)+/i, "").toLowerCase();
|
|
604
|
+
const rawNum = String(t.ticket_number || "").toLowerCase();
|
|
605
|
+
const rawId = String(t.id || "").toLowerCase();
|
|
606
|
+
const displayRef = `rt-${cleanNum}`;
|
|
607
|
+
const custName = (((_a = t.customer) == null ? void 0 : _a.name) || t.customer_name || (((_b = t.customer) == null ? void 0 : _b.first_name) ? `${t.customer.first_name} ${t.customer.last_name || ""}`.trim() : "")).toLowerCase();
|
|
608
|
+
const compName = (((_c = t.customer) == null ? void 0 : _c.company_name) || t.company_name || "").toLowerCase();
|
|
609
|
+
const email = (((_d = t.customer) == null ? void 0 : _d.email) || t.customer_email || "").toLowerCase();
|
|
610
|
+
const phone = (((_e = t.customer) == null ? void 0 : _e.phone) || t.customer_phone || "").replace(/\s+/g, "").toLowerCase();
|
|
611
|
+
const digitsOnlyPhone = phone.replace(/\D/g, "");
|
|
612
|
+
const brandModel = ([(_f = t.device) == null ? void 0 : _f.brand, (_g = t.device) == null ? void 0 : _g.model_name].filter(Boolean).join(" ") || ((_h = t.custom_fields) == null ? void 0 : _h["Device Model"]) || "").toLowerCase();
|
|
613
|
+
const phoneMatches = phone.includes(q.replace(/\s+/g, "")) || digitsOnlyQ.length >= 4 && (digitsOnlyPhone.includes(digitsOnlyQ) || digitsOnlyQ.includes(digitsOnlyPhone));
|
|
614
|
+
return cleanNum.includes(q) || cleanQ && cleanNum.includes(cleanQ) || displayRef.includes(q) || rawNum.includes(q) || cleanQ && rawNum.includes(cleanQ) || rawId.includes(q) || custName.includes(q) || compName.includes(q) || email.includes(q) || phoneMatches || brandModel.includes(q);
|
|
615
|
+
}).slice(0, 30);
|
|
616
|
+
}, [tickets, query, selectedTicketId]);
|
|
617
|
+
const handleSelect = (ticket) => {
|
|
618
|
+
onSelectTicket(ticket.id);
|
|
619
|
+
setQuery(getTicketLabel(ticket));
|
|
620
|
+
setIsOpen(false);
|
|
621
|
+
};
|
|
622
|
+
const handleClear = () => {
|
|
623
|
+
onClear();
|
|
624
|
+
setQuery("");
|
|
625
|
+
};
|
|
626
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative w-full", children: [
|
|
627
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
628
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none text-ui-fg-muted", children: /* @__PURE__ */ jsx(MagnifyingGlass, { className: "w-4 h-4" }) }),
|
|
629
|
+
/* @__PURE__ */ jsx(
|
|
630
|
+
"input",
|
|
631
|
+
{
|
|
632
|
+
type: "text",
|
|
633
|
+
value: query,
|
|
634
|
+
onChange: (e) => {
|
|
635
|
+
setQuery(e.target.value);
|
|
636
|
+
setIsOpen(true);
|
|
637
|
+
},
|
|
638
|
+
onFocus: (e) => {
|
|
639
|
+
setIsOpen(true);
|
|
640
|
+
e.target.select();
|
|
641
|
+
},
|
|
642
|
+
placeholder: "Search by customer name, company, or ticket number (e.g. 5390 or RT-5390)...",
|
|
643
|
+
className: "w-full pl-8.5 pr-8 py-1.5 text-xs bg-ui-bg-base border border-ui-border-base rounded-md text-ui-fg-base placeholder:text-ui-fg-muted focus:outline-none focus:ring-1 focus:ring-ui-border-interactive focus:border-ui-border-interactive"
|
|
644
|
+
}
|
|
645
|
+
),
|
|
646
|
+
(query || selectedTicketId) && /* @__PURE__ */ jsx(
|
|
647
|
+
"button",
|
|
648
|
+
{
|
|
649
|
+
type: "button",
|
|
650
|
+
onClick: handleClear,
|
|
651
|
+
className: "absolute inset-y-0 right-0 pr-2.5 flex items-center text-ui-fg-muted hover:text-ui-fg-base",
|
|
652
|
+
title: "Clear ticket link",
|
|
653
|
+
children: /* @__PURE__ */ jsx(XMark, { className: "w-3.5 h-3.5" })
|
|
654
|
+
}
|
|
655
|
+
)
|
|
656
|
+
] }),
|
|
657
|
+
isOpen && /* @__PURE__ */ jsx("div", { className: "absolute top-full left-0 right-0 mt-1 z-50 bg-ui-bg-base border border-ui-border-base rounded-md shadow-lg max-h-64 overflow-y-auto divide-y divide-ui-border-base", children: loading ? /* @__PURE__ */ jsxs("div", { className: "p-3 text-xs text-ui-fg-muted text-center flex items-center justify-center gap-2", children: [
|
|
658
|
+
/* @__PURE__ */ jsx(ArrowPath, { className: "w-3 h-3 animate-spin" }),
|
|
659
|
+
"Loading repair tickets..."
|
|
660
|
+
] }) : filteredTickets.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "p-3 text-xs text-ui-fg-muted text-center", children: [
|
|
661
|
+
'No repair tickets found matching "',
|
|
662
|
+
query,
|
|
663
|
+
'"'
|
|
664
|
+
] }) : filteredTickets.map((t) => {
|
|
665
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
666
|
+
const cleanNum = String(t.ticket_number || t.id).replace(/^(RT-|JOB-|CERT-)+/i, "");
|
|
667
|
+
const displayRef = `RT-${cleanNum}`;
|
|
668
|
+
const custName = ((_a = t.customer) == null ? void 0 : _a.company_name) || t.company_name || t.customer_name || (((_b = t.customer) == null ? void 0 : _b.first_name) ? `${t.customer.first_name} ${t.customer.last_name || ""}`.trim() : "") || "Customer";
|
|
669
|
+
const compName = ((_c = t.customer) == null ? void 0 : _c.company_name) || t.company_name;
|
|
670
|
+
const brandModel = [(_d = t.device) == null ? void 0 : _d.brand, (_e = t.device) == null ? void 0 : _e.model_name].filter(Boolean).join(" ").trim() || ((_f = t.custom_fields) == null ? void 0 : _f["Device Model"]) || "Device";
|
|
671
|
+
const isSelected = selectedTicketId === t.id;
|
|
672
|
+
return /* @__PURE__ */ jsxs(
|
|
673
|
+
"div",
|
|
674
|
+
{
|
|
675
|
+
onClick: () => handleSelect(t),
|
|
676
|
+
className: `p-2.5 hover:bg-ui-bg-subtle-hover cursor-pointer transition-colors ${isSelected ? "bg-ui-bg-interactive-subtle" : ""}`,
|
|
677
|
+
children: [
|
|
678
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
|
|
679
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 truncate", children: [
|
|
680
|
+
/* @__PURE__ */ jsx(Badge, { size: "small", color: isSelected ? "blue" : "grey", className: "shrink-0 font-mono", children: displayRef }),
|
|
681
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-xs text-ui-fg-base truncate", children: custName }),
|
|
682
|
+
compName && compName !== custName && /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-ui-fg-muted truncate", children: [
|
|
683
|
+
"(",
|
|
684
|
+
compName,
|
|
685
|
+
")"
|
|
686
|
+
] })
|
|
687
|
+
] }),
|
|
688
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-ui-fg-subtle truncate shrink-0 font-medium", children: brandModel })
|
|
689
|
+
] }),
|
|
690
|
+
(((_g = t.customer) == null ? void 0 : _g.email) || t.customer_email || ((_h = t.customer) == null ? void 0 : _h.phone) || t.customer_phone) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-[11px] text-ui-fg-muted truncate", children: [
|
|
691
|
+
(((_i = t.customer) == null ? void 0 : _i.email) || t.customer_email) && /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
692
|
+
"📧 ",
|
|
693
|
+
((_j = t.customer) == null ? void 0 : _j.email) || t.customer_email
|
|
694
|
+
] }),
|
|
695
|
+
(((_k = t.customer) == null ? void 0 : _k.phone) || t.customer_phone) && /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
696
|
+
"📞 ",
|
|
697
|
+
((_l = t.customer) == null ? void 0 : _l.phone) || t.customer_phone
|
|
698
|
+
] })
|
|
699
|
+
] })
|
|
700
|
+
]
|
|
701
|
+
},
|
|
702
|
+
t.id
|
|
703
|
+
);
|
|
704
|
+
}) })
|
|
705
|
+
] });
|
|
706
|
+
};
|
|
401
707
|
const DocumentsPage = () => {
|
|
402
708
|
var _a, _b;
|
|
403
709
|
const [activeTab, setActiveTab] = useState("hub");
|
|
@@ -511,7 +817,7 @@ const DocumentsPage = () => {
|
|
|
511
817
|
items: [{ title: "", description: "", quantity: 1, unit_price: 0 }],
|
|
512
818
|
currency_code: "kes",
|
|
513
819
|
notes: "",
|
|
514
|
-
force_source: "
|
|
820
|
+
force_source: "zoho",
|
|
515
821
|
auto_send: false
|
|
516
822
|
});
|
|
517
823
|
const [customDocForm, setCustomDocForm] = useState({
|
|
@@ -521,6 +827,7 @@ const DocumentsPage = () => {
|
|
|
521
827
|
customer_name: "Jane Doe",
|
|
522
828
|
customer_email: "jane.doe@example.com",
|
|
523
829
|
customer_phone: "+254 712 345 678",
|
|
830
|
+
customer_address: "",
|
|
524
831
|
body_text: CUSTOM_DOC_PRESETS[0].body_text,
|
|
525
832
|
items: CUSTOM_DOC_PRESETS[0].items.map((it) => ({ ...it })),
|
|
526
833
|
custom_fields: { ...CUSTOM_DOC_PRESETS[0].custom_fields || {} },
|
|
@@ -534,6 +841,8 @@ const DocumentsPage = () => {
|
|
|
534
841
|
const [selectedTicketId, setSelectedTicketId] = useState("");
|
|
535
842
|
const [newCustomFieldKey, setNewCustomFieldKey] = useState("");
|
|
536
843
|
const [newCustomFieldValue, setNewCustomFieldValue] = useState("");
|
|
844
|
+
const [customersList, setCustomersList] = useState([]);
|
|
845
|
+
const [loadingCustomers, setLoadingCustomers] = useState(false);
|
|
537
846
|
const loadRepairTickets = async () => {
|
|
538
847
|
setLoadingRepairs(true);
|
|
539
848
|
try {
|
|
@@ -551,16 +860,60 @@ const DocumentsPage = () => {
|
|
|
551
860
|
setLoadingRepairs(false);
|
|
552
861
|
}
|
|
553
862
|
};
|
|
863
|
+
const loadCustomers = async () => {
|
|
864
|
+
setLoadingCustomers(true);
|
|
865
|
+
try {
|
|
866
|
+
const res = await fetch("/admin/customers?limit=100&fields=*addresses", { credentials: "include" });
|
|
867
|
+
if (res.ok) {
|
|
868
|
+
const data = await res.json().catch(() => ({}));
|
|
869
|
+
if (data.customers && Array.isArray(data.customers)) {
|
|
870
|
+
setCustomersList(data.customers);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
} catch (e) {
|
|
875
|
+
console.warn("Could not fetch /admin/customers:", e);
|
|
876
|
+
} finally {
|
|
877
|
+
setLoadingCustomers(false);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
554
880
|
useEffect(() => {
|
|
555
|
-
if (customDocModalOpen) {
|
|
881
|
+
if (customDocModalOpen || manualModalOpen) {
|
|
556
882
|
loadRepairTickets();
|
|
883
|
+
loadCustomers();
|
|
557
884
|
}
|
|
558
|
-
}, [customDocModalOpen]);
|
|
885
|
+
}, [customDocModalOpen, manualModalOpen]);
|
|
559
886
|
const availableTickets = useMemo(() => {
|
|
560
|
-
var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
887
|
+
var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
|
|
561
888
|
const map = /* @__PURE__ */ new Map();
|
|
562
889
|
for (const t of repairTickets) {
|
|
563
|
-
if (t.id)
|
|
890
|
+
if (t.id) {
|
|
891
|
+
const linkedCust = t.customer_id ? customersList.find((c) => c.id === t.customer_id) : null;
|
|
892
|
+
const custName = ((_a2 = t.customer) == null ? void 0 : _a2.name) || (((_b2 = t.customer) == null ? void 0 : _b2.first_name) ? `${t.customer.first_name} ${t.customer.last_name || ""}`.trim() : "") || t.customer_name || ((_c = t.metadata) == null ? void 0 : _c.customer_name) || (linkedCust ? [linkedCust.first_name, linkedCust.last_name].filter(Boolean).join(" ").trim() : "") || "";
|
|
893
|
+
const compName = ((_d = t.customer) == null ? void 0 : _d.company_name) || t.company_name || ((_e = t.metadata) == null ? void 0 : _e.company_name) || (linkedCust == null ? void 0 : linkedCust.company_name) || "";
|
|
894
|
+
const email = ((_f = t.customer) == null ? void 0 : _f.email) || t.customer_email || ((_g = t.metadata) == null ? void 0 : _g.customer_email) || (linkedCust == null ? void 0 : linkedCust.email) || "";
|
|
895
|
+
const phone = ((_h = t.customer) == null ? void 0 : _h.phone) || t.customer_phone || ((_i = t.metadata) == null ? void 0 : _i.customer_phone) || (linkedCust == null ? void 0 : linkedCust.phone) || "";
|
|
896
|
+
const linkedAddr = (_j = linkedCust == null ? void 0 : linkedCust.addresses) == null ? void 0 : _j[0];
|
|
897
|
+
const formattedCustAddr = linkedAddr ? [linkedAddr.address_1, linkedAddr.address_2, linkedAddr.city, linkedAddr.postal_code, linkedAddr.province, (_k = linkedAddr.country_code) == null ? void 0 : _k.toUpperCase()].filter(Boolean).join(", ") : "";
|
|
898
|
+
const address = ((_l = t.customer) == null ? void 0 : _l.address) || t.customer_address || ((_m = t.metadata) == null ? void 0 : _m.address) || ((_n = t.custom_fields) == null ? void 0 : _n["Address"]) || formattedCustAddr || "";
|
|
899
|
+
map.set(t.id, {
|
|
900
|
+
...t,
|
|
901
|
+
customer_name: custName || t.customer_name,
|
|
902
|
+
company_name: compName || t.company_name,
|
|
903
|
+
customer_email: email || t.customer_email,
|
|
904
|
+
customer_phone: phone || t.customer_phone,
|
|
905
|
+
customer_address: address || t.customer_address,
|
|
906
|
+
customer: {
|
|
907
|
+
name: custName,
|
|
908
|
+
first_name: (linkedCust == null ? void 0 : linkedCust.first_name) || ((_o = t.customer) == null ? void 0 : _o.first_name),
|
|
909
|
+
last_name: (linkedCust == null ? void 0 : linkedCust.last_name) || ((_p = t.customer) == null ? void 0 : _p.last_name),
|
|
910
|
+
company_name: compName,
|
|
911
|
+
email,
|
|
912
|
+
phone,
|
|
913
|
+
address
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
}
|
|
564
917
|
}
|
|
565
918
|
for (const d of documents) {
|
|
566
919
|
const ticketId = d.repair_ticket_id;
|
|
@@ -569,14 +922,14 @@ const DocumentsPage = () => {
|
|
|
569
922
|
const idKey = ticketId || displayId;
|
|
570
923
|
if (isRepairDoc && idKey) {
|
|
571
924
|
if (!map.has(idKey)) {
|
|
572
|
-
const customFields = d.custom_fields || ((
|
|
925
|
+
const customFields = d.custom_fields || ((_q = d.metadata) == null ? void 0 : _q.custom_fields) || {};
|
|
573
926
|
map.set(idKey, {
|
|
574
927
|
id: idKey,
|
|
575
928
|
ticket_number: String(displayId).replace(/^(RT-|JOB-|CERT-)+/i, "") || idKey,
|
|
576
929
|
customer_name: d.customer_name,
|
|
577
930
|
customer_email: d.customer_email,
|
|
578
931
|
customer_phone: d.customer_phone,
|
|
579
|
-
company_name: ((
|
|
932
|
+
company_name: ((_r = d.customer) == null ? void 0 : _r.company_name) || ((_s = d.metadata) == null ? void 0 : _s.company_name),
|
|
580
933
|
device: {
|
|
581
934
|
brand: customFields["Brand"] || "",
|
|
582
935
|
model_name: customFields["Device Model"] || customFields["Model"] || "Device",
|
|
@@ -587,16 +940,16 @@ const DocumentsPage = () => {
|
|
|
587
940
|
technician_name: customFields["Technician"] || "Bonifencia A.",
|
|
588
941
|
issue_description: customFields["Reported Issue"] || d.notes || "Standard Service",
|
|
589
942
|
custom_fields: customFields,
|
|
590
|
-
items: ((
|
|
943
|
+
items: ((_t = d.metadata) == null ? void 0 : _t.items) || []
|
|
591
944
|
});
|
|
592
945
|
} else {
|
|
593
946
|
const existing = map.get(idKey);
|
|
594
947
|
if (!existing.customer_name && d.customer_name) existing.customer_name = d.customer_name;
|
|
595
948
|
if (!existing.customer_email && d.customer_email) existing.customer_email = d.customer_email;
|
|
596
949
|
if (!existing.customer_phone && d.customer_phone) existing.customer_phone = d.customer_phone;
|
|
597
|
-
const compName = ((
|
|
950
|
+
const compName = ((_u = d.customer) == null ? void 0 : _u.company_name) || ((_v = d.metadata) == null ? void 0 : _v.company_name);
|
|
598
951
|
if (!existing.company_name && compName) existing.company_name = compName;
|
|
599
|
-
if ((!((
|
|
952
|
+
if ((!((_w = existing.device) == null ? void 0 : _w.model_name) || ((_x = existing.device) == null ? void 0 : _x.model_name) === "Device") && ((_z = (_y = d.metadata) == null ? void 0 : _y.custom_fields) == null ? void 0 : _z["Device Model"])) {
|
|
600
953
|
existing.device = existing.device || {};
|
|
601
954
|
existing.device.model_name = d.metadata.custom_fields["Device Model"];
|
|
602
955
|
}
|
|
@@ -604,9 +957,87 @@ const DocumentsPage = () => {
|
|
|
604
957
|
}
|
|
605
958
|
}
|
|
606
959
|
return Array.from(map.values());
|
|
607
|
-
}, [repairTickets, documents]);
|
|
960
|
+
}, [repairTickets, documents, customersList]);
|
|
961
|
+
const customerAutofillCandidates = useMemo(() => {
|
|
962
|
+
var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w;
|
|
963
|
+
const map = /* @__PURE__ */ new Map();
|
|
964
|
+
const addCandidate = (c) => {
|
|
965
|
+
var _a3, _b3;
|
|
966
|
+
const key = ((_a3 = c.email) == null ? void 0 : _a3.trim().toLowerCase()) || ((_b3 = c.phone) == null ? void 0 : _b3.trim().replace(/\s+/g, "")) || `${c.name.trim().toLowerCase()}::${c.company_name.trim().toLowerCase()}`;
|
|
967
|
+
if (!key || key === "::" || key === "customer::") return;
|
|
968
|
+
if (!map.has(key)) {
|
|
969
|
+
map.set(key, c);
|
|
970
|
+
} else {
|
|
971
|
+
const existing = map.get(key);
|
|
972
|
+
if (!existing.name || existing.name === "Customer") existing.name = c.name;
|
|
973
|
+
if (!existing.company_name && c.company_name) existing.company_name = c.company_name;
|
|
974
|
+
if (!existing.email && c.email) existing.email = c.email;
|
|
975
|
+
if (!existing.phone && c.phone) existing.phone = c.phone;
|
|
976
|
+
if (!existing.address && c.address) existing.address = c.address;
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
for (const c of customersList) {
|
|
980
|
+
const fullName = [c.first_name, c.last_name].filter(Boolean).join(" ").trim();
|
|
981
|
+
const addr = ((_a2 = c.addresses) == null ? void 0 : _a2[0]) || c.default_billing_address || c.billing_address;
|
|
982
|
+
const formattedAddr = addr ? [addr.address_1, addr.address_2, addr.city, addr.postal_code, addr.province, (_b2 = addr.country_code) == null ? void 0 : _b2.toUpperCase()].filter(Boolean).join(", ") : "";
|
|
983
|
+
const company = c.company_name || (addr == null ? void 0 : addr.company) || ((_c = c.metadata) == null ? void 0 : _c.company_name) || "";
|
|
984
|
+
const name = fullName || company || c.email || "Customer";
|
|
985
|
+
addCandidate({
|
|
986
|
+
id: c.id || `cust-${Math.random()}`,
|
|
987
|
+
name,
|
|
988
|
+
company_name: company,
|
|
989
|
+
email: c.email || "",
|
|
990
|
+
phone: c.phone || (addr == null ? void 0 : addr.phone) || "",
|
|
991
|
+
address: formattedAddr,
|
|
992
|
+
source: "customer",
|
|
993
|
+
source_label: "Medusa Customer"
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
for (const t of availableTickets) {
|
|
997
|
+
const fullName = (((_d = t.customer) == null ? void 0 : _d.first_name) ? `${t.customer.first_name} ${t.customer.last_name || ""}`.trim() : "") || ((_e = t.customer) == null ? void 0 : _e.name) || t.customer_name || "";
|
|
998
|
+
const company = ((_f = t.customer) == null ? void 0 : _f.company_name) || t.company_name || ((_g = t.metadata) == null ? void 0 : _g.company_name) || "";
|
|
999
|
+
const email = ((_h = t.customer) == null ? void 0 : _h.email) || t.customer_email || ((_i = t.metadata) == null ? void 0 : _i.customer_email) || "";
|
|
1000
|
+
const phone = ((_j = t.customer) == null ? void 0 : _j.phone) || t.customer_phone || ((_k = t.metadata) == null ? void 0 : _k.customer_phone) || "";
|
|
1001
|
+
const address = ((_l = t.customer) == null ? void 0 : _l.address) || t.customer_address || ((_m = t.metadata) == null ? void 0 : _m.address) || ((_n = t.custom_fields) == null ? void 0 : _n["Address"]) || "";
|
|
1002
|
+
const cleanNum = String(t.ticket_number || t.id).replace(/^(RT-|JOB-|CERT-)+/i, "");
|
|
1003
|
+
const name = fullName || company || "Customer";
|
|
1004
|
+
{
|
|
1005
|
+
addCandidate({
|
|
1006
|
+
id: `ticket-${t.id || cleanNum}`,
|
|
1007
|
+
name,
|
|
1008
|
+
company_name: company,
|
|
1009
|
+
email,
|
|
1010
|
+
phone,
|
|
1011
|
+
address,
|
|
1012
|
+
source: "repair",
|
|
1013
|
+
source_label: `Ticket RT-${cleanNum}`
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
for (const d of documents) {
|
|
1018
|
+
const name = d.customer_name || "";
|
|
1019
|
+
const company = ((_o = d.customer) == null ? void 0 : _o.company_name) || ((_p = d.metadata) == null ? void 0 : _p.company_name) || d.company_name || "";
|
|
1020
|
+
const email = d.customer_email || ((_q = d.customer) == null ? void 0 : _q.email) || "";
|
|
1021
|
+
const phone = d.customer_phone || ((_r = d.customer) == null ? void 0 : _r.phone) || "";
|
|
1022
|
+
const address = ((_s = d.customer) == null ? void 0 : _s.address) || d.customer_address || ((_t = d.metadata) == null ? void 0 : _t.customer_address) || ((_v = (_u = d.metadata) == null ? void 0 : _u.customer) == null ? void 0 : _v.address) || "";
|
|
1023
|
+
const docRef = d.display_id || ((_w = d.id) == null ? void 0 : _w.slice(0, 8));
|
|
1024
|
+
if (name || company || email || phone) {
|
|
1025
|
+
addCandidate({
|
|
1026
|
+
id: `doc-${d.id}`,
|
|
1027
|
+
name: name || company || "Customer",
|
|
1028
|
+
company_name: company,
|
|
1029
|
+
email,
|
|
1030
|
+
phone,
|
|
1031
|
+
address,
|
|
1032
|
+
source: "document",
|
|
1033
|
+
source_label: `Doc ${docRef}`
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return Array.from(map.values());
|
|
1038
|
+
}, [customersList, availableTickets, documents]);
|
|
608
1039
|
const handleSelectRepairTicket = (ticketId) => {
|
|
609
|
-
var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
|
|
1040
|
+
var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C;
|
|
610
1041
|
setSelectedTicketId(ticketId);
|
|
611
1042
|
if (!ticketId) return;
|
|
612
1043
|
const ticket = availableTickets.find((t) => t.id === ticketId || t.ticket_number === ticketId);
|
|
@@ -674,12 +1105,14 @@ const DocumentsPage = () => {
|
|
|
674
1105
|
});
|
|
675
1106
|
}
|
|
676
1107
|
}
|
|
1108
|
+
const customerAddress = ((_A = ticket.customer) == null ? void 0 : _A.address) || ticket.customer_address || ((_B = ticket.metadata) == null ? void 0 : _B.address) || ((_C = ticket.custom_fields) == null ? void 0 : _C["Address"]) || "";
|
|
677
1109
|
setCustomDocForm((prev) => ({
|
|
678
1110
|
...prev,
|
|
679
1111
|
customer_name: customerName,
|
|
680
1112
|
customer_email: customerEmail,
|
|
681
1113
|
customer_phone: customerPhone,
|
|
682
1114
|
company_name: companyName,
|
|
1115
|
+
customer_address: customerAddress || prev.customer_address,
|
|
683
1116
|
custom_fields: {
|
|
684
1117
|
...prev.custom_fields,
|
|
685
1118
|
"Device Model": brandModel,
|
|
@@ -825,7 +1258,7 @@ const DocumentsPage = () => {
|
|
|
825
1258
|
items: validItems,
|
|
826
1259
|
currency_code: manualForm.currency_code,
|
|
827
1260
|
notes: manualForm.notes,
|
|
828
|
-
force_source:
|
|
1261
|
+
force_source: "zoho",
|
|
829
1262
|
auto_send: manualForm.auto_send
|
|
830
1263
|
})
|
|
831
1264
|
});
|
|
@@ -844,7 +1277,7 @@ const DocumentsPage = () => {
|
|
|
844
1277
|
items: [{ title: "", description: "", quantity: 1, unit_price: 0 }],
|
|
845
1278
|
currency_code: "kes",
|
|
846
1279
|
notes: "",
|
|
847
|
-
force_source: "
|
|
1280
|
+
force_source: "zoho",
|
|
848
1281
|
auto_send: false
|
|
849
1282
|
});
|
|
850
1283
|
loadDocuments();
|
|
@@ -880,7 +1313,8 @@ const DocumentsPage = () => {
|
|
|
880
1313
|
name: customDocForm.customer_name,
|
|
881
1314
|
company_name: customDocForm.company_name || void 0,
|
|
882
1315
|
email: customDocForm.customer_email,
|
|
883
|
-
phone: customDocForm.customer_phone
|
|
1316
|
+
phone: customDocForm.customer_phone,
|
|
1317
|
+
address: customDocForm.customer_address || void 0
|
|
884
1318
|
},
|
|
885
1319
|
body_text: customDocForm.body_text,
|
|
886
1320
|
notes: customDocForm.notes,
|
|
@@ -898,6 +1332,7 @@ const DocumentsPage = () => {
|
|
|
898
1332
|
customDocForm.send_email_now ? "Custom document generated & emailed successfully!" : "Custom document generated and saved to Documents Hub!"
|
|
899
1333
|
);
|
|
900
1334
|
setCustomDocModalOpen(false);
|
|
1335
|
+
setSelectedTicketId("");
|
|
901
1336
|
loadDocuments();
|
|
902
1337
|
} else {
|
|
903
1338
|
alert(`Failed to generate custom document: ${data.message || res.statusText}`);
|
|
@@ -2362,52 +2797,6 @@ const DocumentsPage = () => {
|
|
|
2362
2797
|
)
|
|
2363
2798
|
] })
|
|
2364
2799
|
] }),
|
|
2365
|
-
/* @__PURE__ */ jsxs("div", { className: "p-5 border border-ui-border-base rounded-lg bg-ui-bg-base flex flex-col justify-between shadow-sm hover:border-ui-border-interactive transition-colors", children: [
|
|
2366
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
2367
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
2368
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2369
|
-
/* @__PURE__ */ jsx("span", { className: "text-xl", children: "💼" }),
|
|
2370
|
-
/* @__PURE__ */ jsx(Label, { className: "font-semibold text-sm", children: "Commercial Proforma Quote" })
|
|
2371
|
-
] }),
|
|
2372
|
-
/* @__PURE__ */ jsx(Badge, { color: "orange", size: "small", children: "Cost Estimate" })
|
|
2373
|
-
] }),
|
|
2374
|
-
/* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-subtle mb-4", children: "Itemized corporate repair quotation with workshop turnaround estimate, OEM part warranties, and 14-day validity period." }),
|
|
2375
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
2376
|
-
/* @__PURE__ */ jsx(Label, { className: "text-xs font-medium", children: "Template Style" }),
|
|
2377
|
-
/* @__PURE__ */ jsxs(
|
|
2378
|
-
Select,
|
|
2379
|
-
{
|
|
2380
|
-
value: settings.custom_doc_template || "modern",
|
|
2381
|
-
onValueChange: (v) => updateSettingField("custom_doc_template", v),
|
|
2382
|
-
children: [
|
|
2383
|
-
/* @__PURE__ */ jsx(Select.Trigger, { size: "small", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
|
|
2384
|
-
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
2385
|
-
/* @__PURE__ */ jsx(Select.Item, { value: "modern", children: "Modern Minimalist" }),
|
|
2386
|
-
/* @__PURE__ */ jsx(Select.Item, { value: "classic", children: "Classic Corporate" }),
|
|
2387
|
-
/* @__PURE__ */ jsx(Select.Item, { value: "minimal", children: "Clean Tech Monospace" })
|
|
2388
|
-
] })
|
|
2389
|
-
]
|
|
2390
|
-
}
|
|
2391
|
-
)
|
|
2392
|
-
] })
|
|
2393
|
-
] }),
|
|
2394
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-5 pt-3 border-t border-ui-border-base flex items-center justify-between", children: [
|
|
2395
|
-
/* @__PURE__ */ jsx(Text, { className: "text-[11px] text-ui-fg-muted font-mono", children: "Preset: proforma" }),
|
|
2396
|
-
/* @__PURE__ */ jsxs(
|
|
2397
|
-
Button,
|
|
2398
|
-
{
|
|
2399
|
-
variant: "secondary",
|
|
2400
|
-
size: "small",
|
|
2401
|
-
onClick: () => handlePreviewTemplate(settings.custom_doc_template || "modern", "custom", "proforma"),
|
|
2402
|
-
isLoading: previewLoading === `${settings.custom_doc_template || "modern"}-custom-proforma`,
|
|
2403
|
-
children: [
|
|
2404
|
-
/* @__PURE__ */ jsx(ArrowDownTray, { className: "w-3.5 h-3.5 mr-1" }),
|
|
2405
|
-
" Preview Proforma"
|
|
2406
|
-
]
|
|
2407
|
-
}
|
|
2408
|
-
)
|
|
2409
|
-
] })
|
|
2410
|
-
] }),
|
|
2411
2800
|
/* @__PURE__ */ jsxs("div", { className: "p-5 border border-ui-border-base rounded-lg bg-ui-bg-base flex flex-col justify-between shadow-sm hover:border-ui-border-interactive transition-colors", children: [
|
|
2412
2801
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
2413
2802
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
@@ -3295,6 +3684,23 @@ Your *{document_title} #{display_id}* has been generated.
|
|
|
3295
3684
|
}
|
|
3296
3685
|
)
|
|
3297
3686
|
] }),
|
|
3687
|
+
/* @__PURE__ */ jsx("div", { className: "col-span-2", children: /* @__PURE__ */ jsx(
|
|
3688
|
+
CustomerAutofillCombobox,
|
|
3689
|
+
{
|
|
3690
|
+
candidates: customerAutofillCandidates,
|
|
3691
|
+
loading: loadingCustomers || loadingRepairs,
|
|
3692
|
+
onSelectCustomer: (candidate) => {
|
|
3693
|
+
setManualForm((prev) => ({
|
|
3694
|
+
...prev,
|
|
3695
|
+
customer_name: candidate.name,
|
|
3696
|
+
company_name: candidate.company_name,
|
|
3697
|
+
customer_email: candidate.email,
|
|
3698
|
+
customer_phone: candidate.phone,
|
|
3699
|
+
customer_address: candidate.address || prev.customer_address
|
|
3700
|
+
}));
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
) }),
|
|
3298
3704
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
3299
3705
|
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "Company Name (Optional)" }),
|
|
3300
3706
|
/* @__PURE__ */ jsx(
|
|
@@ -3308,7 +3714,7 @@ Your *{document_title} #{display_id}* has been generated.
|
|
|
3308
3714
|
)
|
|
3309
3715
|
] }),
|
|
3310
3716
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
3311
|
-
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "Customer Name" }),
|
|
3717
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "Customer Name *" }),
|
|
3312
3718
|
/* @__PURE__ */ jsx(
|
|
3313
3719
|
Input,
|
|
3314
3720
|
{
|
|
@@ -3343,23 +3749,26 @@ Your *{document_title} #{display_id}* has been generated.
|
|
|
3343
3749
|
}
|
|
3344
3750
|
)
|
|
3345
3751
|
] }),
|
|
3346
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
3347
|
-
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "
|
|
3348
|
-
/* @__PURE__ */
|
|
3349
|
-
|
|
3752
|
+
/* @__PURE__ */ jsxs("div", { className: "col-span-2", children: [
|
|
3753
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "Customer Address (Optional)" }),
|
|
3754
|
+
/* @__PURE__ */ jsx(
|
|
3755
|
+
Input,
|
|
3350
3756
|
{
|
|
3351
|
-
value: manualForm.
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
3356
|
-
/* @__PURE__ */ jsx(Select.Item, { value: "auto", children: "Auto (Zoho if enabled, else PDFKit)" }),
|
|
3357
|
-
/* @__PURE__ */ jsx(Select.Item, { value: "zoho", children: "Force Zoho Books" }),
|
|
3358
|
-
/* @__PURE__ */ jsx(Select.Item, { value: "pdfkit", children: "Force Local PDFKit" })
|
|
3359
|
-
] })
|
|
3360
|
-
]
|
|
3757
|
+
value: manualForm.customer_address,
|
|
3758
|
+
onChange: (e) => setManualForm({ ...manualForm, customer_address: e.target.value }),
|
|
3759
|
+
placeholder: "Street Address, City, Postal Code",
|
|
3760
|
+
size: "small"
|
|
3361
3761
|
}
|
|
3362
3762
|
)
|
|
3763
|
+
] }),
|
|
3764
|
+
/* @__PURE__ */ jsxs("div", { className: "col-span-2", children: [
|
|
3765
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "Generation Source" }),
|
|
3766
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-1 px-3 py-2 rounded-md border border-ui-border-base bg-ui-bg-subtle text-xs text-ui-fg-base", children: [
|
|
3767
|
+
/* @__PURE__ */ jsx("span", { className: "inline-block w-2 h-2 rounded-full bg-emerald-500 animate-pulse" }),
|
|
3768
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-ui-fg-base", children: "Zoho Books (Cloud Invoicing)" }),
|
|
3769
|
+
/* @__PURE__ */ jsx(Badge, { size: "small", color: "blue", className: "ml-auto font-mono", children: "Enforced" })
|
|
3770
|
+
] }),
|
|
3771
|
+
/* @__PURE__ */ jsx(Text, { className: "text-[11px] text-ui-fg-subtle mt-1", children: "All manual invoices and quotes are generated and synced directly via Zoho Books. Local PDFKit fallback is disabled." })
|
|
3363
3772
|
] })
|
|
3364
3773
|
] }),
|
|
3365
3774
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
@@ -3595,29 +4004,14 @@ Your *{document_title} #{display_id}* has been generated.
|
|
|
3595
4004
|
] })
|
|
3596
4005
|
] }),
|
|
3597
4006
|
/* @__PURE__ */ jsx(Text, { className: "text-[11px] text-ui-fg-subtle", children: "Automatically autofills customer details, device specifications (model, serial/IMEI, accessories), technician notes, and services from active repairs." }),
|
|
3598
|
-
/* @__PURE__ */
|
|
3599
|
-
|
|
4007
|
+
/* @__PURE__ */ jsx(
|
|
4008
|
+
RepairTicketCombobox,
|
|
3600
4009
|
{
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
var _a2, _b2, _c, _d, _e;
|
|
3607
|
-
const brandModel = [(_a2 = t.device) == null ? void 0 : _a2.brand, (_b2 = t.device) == null ? void 0 : _b2.model_name].filter(Boolean).join(" ").trim() || ((_c = t.custom_fields) == null ? void 0 : _c["Device Model"]) || "Device";
|
|
3608
|
-
const cleanNum = String(t.ticket_number || t.id).replace(/^(RT-|JOB-)+/i, "");
|
|
3609
|
-
const displayRef = `RT-${cleanNum}`;
|
|
3610
|
-
const custName = ((_d = t.customer) == null ? void 0 : _d.company_name) || t.company_name || t.customer_name || (((_e = t.customer) == null ? void 0 : _e.first_name) ? `${t.customer.first_name} ${t.customer.last_name || ""}`.trim() : "") || "Customer";
|
|
3611
|
-
return /* @__PURE__ */ jsxs(Select.Item, { value: t.id, children: [
|
|
3612
|
-
displayRef,
|
|
3613
|
-
" — ",
|
|
3614
|
-
custName,
|
|
3615
|
-
" (",
|
|
3616
|
-
brandModel,
|
|
3617
|
-
")"
|
|
3618
|
-
] }, t.id);
|
|
3619
|
-
}) })
|
|
3620
|
-
]
|
|
4010
|
+
tickets: availableTickets,
|
|
4011
|
+
selectedTicketId,
|
|
4012
|
+
onSelectTicket: handleSelectRepairTicket,
|
|
4013
|
+
onClear: handleClearRepairLink,
|
|
4014
|
+
loading: loadingRepairs
|
|
3621
4015
|
}
|
|
3622
4016
|
),
|
|
3623
4017
|
selectedTicketId && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-2.5 py-1.5 rounded bg-ui-bg-interactive-subtle border border-ui-border-interactive text-[11px] text-ui-fg-interactive font-medium", children: [
|
|
@@ -3665,6 +4059,23 @@ Your *{document_title} #{display_id}* has been generated.
|
|
|
3665
4059
|
}
|
|
3666
4060
|
)
|
|
3667
4061
|
] }),
|
|
4062
|
+
/* @__PURE__ */ jsx("div", { className: "col-span-2", children: /* @__PURE__ */ jsx(
|
|
4063
|
+
CustomerAutofillCombobox,
|
|
4064
|
+
{
|
|
4065
|
+
candidates: customerAutofillCandidates,
|
|
4066
|
+
loading: loadingCustomers || loadingRepairs,
|
|
4067
|
+
onSelectCustomer: (candidate) => {
|
|
4068
|
+
setCustomDocForm((prev) => ({
|
|
4069
|
+
...prev,
|
|
4070
|
+
customer_name: candidate.name,
|
|
4071
|
+
company_name: candidate.company_name,
|
|
4072
|
+
customer_email: candidate.email,
|
|
4073
|
+
customer_phone: candidate.phone,
|
|
4074
|
+
customer_address: candidate.address || prev.customer_address
|
|
4075
|
+
}));
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
) }),
|
|
3668
4079
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
3669
4080
|
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "Company Name (Optional)" }),
|
|
3670
4081
|
/* @__PURE__ */ jsx(
|
|
@@ -3712,6 +4123,18 @@ Your *{document_title} #{display_id}* has been generated.
|
|
|
3712
4123
|
size: "small"
|
|
3713
4124
|
}
|
|
3714
4125
|
)
|
|
4126
|
+
] }),
|
|
4127
|
+
/* @__PURE__ */ jsxs("div", { className: "col-span-2", children: [
|
|
4128
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "Customer Address (Optional)" }),
|
|
4129
|
+
/* @__PURE__ */ jsx(
|
|
4130
|
+
Input,
|
|
4131
|
+
{
|
|
4132
|
+
value: customDocForm.customer_address,
|
|
4133
|
+
onChange: (e) => setCustomDocForm({ ...customDocForm, customer_address: e.target.value }),
|
|
4134
|
+
placeholder: "Street Address, City, Postal Code",
|
|
4135
|
+
size: "small"
|
|
4136
|
+
}
|
|
4137
|
+
)
|
|
3715
4138
|
] })
|
|
3716
4139
|
] }),
|
|
3717
4140
|
/* @__PURE__ */ jsxs("div", { children: [
|