@fluid-app/portal-sdk 0.1.258 → 0.1.259
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/dist/{ContactsScreen-BFiClnMI.mjs → ContactsScreen-Cg_PgwAi.mjs} +1458 -1245
- package/dist/ContactsScreen-Cg_PgwAi.mjs.map +1 -0
- package/dist/{ContactsScreen-BbQ8HDzt.cjs → ContactsScreen-D9GTzEO7.cjs} +1 -1
- package/dist/{ContactsScreen-Bok_ZdN5.cjs → ContactsScreen-DrMOpvio.cjs} +1455 -1242
- package/dist/ContactsScreen-DrMOpvio.cjs.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.mjs +3 -3
- package/package.json +18 -18
- package/styles/globals.css +18 -0
- package/dist/ContactsScreen-BFiClnMI.mjs.map +0 -1
- package/dist/ContactsScreen-Bok_ZdN5.cjs.map +0 -1
|
@@ -236,32 +236,6 @@ function useGroups() {
|
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
238
|
//#endregion
|
|
239
|
-
//#region ../../contacts/ui/src/shared/components/contacts/statusBadge.tsx
|
|
240
|
-
const statusStyles = {
|
|
241
|
-
new: "border-[var(--status-new-border)] bg-[var(--status-new)] text-[var(--status-new-foreground)]",
|
|
242
|
-
active: "border-[var(--status-active-border)] bg-[var(--status-active)] text-[var(--status-active-foreground)]",
|
|
243
|
-
inactive: "border-border bg-muted text-muted-foreground",
|
|
244
|
-
lead: "border-[var(--status-lead-border)] bg-[var(--status-lead)] text-[var(--status-lead-foreground)]",
|
|
245
|
-
customer: "border-[var(--status-customer-border)] bg-[var(--status-customer)] text-[var(--status-customer-foreground)]",
|
|
246
|
-
success: "border-[var(--badge-success-border)] bg-[var(--badge-success)] text-[var(--badge-success-foreground)]",
|
|
247
|
-
warning: "border-[var(--badge-warning-border)] bg-[var(--badge-warning)] text-[var(--badge-warning-foreground)]",
|
|
248
|
-
danger: "border-[var(--badge-danger-border)] bg-[var(--badge-danger)] text-[var(--badge-danger-foreground)]",
|
|
249
|
-
info: "border-[var(--badge-info-border)] bg-[var(--badge-info)] text-[var(--badge-info-foreground)]",
|
|
250
|
-
neutral: "border-border bg-muted text-muted-foreground",
|
|
251
|
-
notice: "border-[var(--badge-notice-border)] bg-[var(--badge-notice)] text-[var(--badge-notice-foreground)]",
|
|
252
|
-
accent: "border-[var(--badge-accent-border)] bg-[var(--badge-accent)] text-[var(--badge-accent-foreground)]"
|
|
253
|
-
};
|
|
254
|
-
const defaultStyle = "border-border bg-muted text-muted-foreground";
|
|
255
|
-
function getStatusStyle(status) {
|
|
256
|
-
return statusStyles[status] ?? defaultStyle;
|
|
257
|
-
}
|
|
258
|
-
function StatusBadge({ status, label, className }) {
|
|
259
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
260
|
-
className: require_src.cn("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold capitalize", getStatusStyle(status), className),
|
|
261
|
-
children: label ?? status
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
//#endregion
|
|
265
239
|
//#region ../../contacts/ui/src/portal/components/contacts/rep-layout/utils.ts
|
|
266
240
|
function getDisplayName(contact) {
|
|
267
241
|
if (contact.full_name && contact.full_name.trim().length > 0) return contact.full_name;
|
|
@@ -304,9 +278,7 @@ function getRelativeUpdated(contact) {
|
|
|
304
278
|
function ContactsSidebarRow({ contact, isSelected, onSelect }) {
|
|
305
279
|
const name = getDisplayName(contact);
|
|
306
280
|
const id = String(contact.id);
|
|
307
|
-
const
|
|
308
|
-
const relativeLabel = getRelativeUpdated(contact);
|
|
309
|
-
const secondary = contact.email ?? status ?? null;
|
|
281
|
+
const secondary = contact.email ?? contact.status?.trim() ?? null;
|
|
310
282
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
311
283
|
type: "button",
|
|
312
284
|
onClick: () => onSelect(id),
|
|
@@ -314,7 +286,7 @@ function ContactsSidebarRow({ contact, isSelected, onSelect }) {
|
|
|
314
286
|
className: require_src.cn("group flex w-full items-center gap-3.5 rounded-xl px-3 py-3 text-left transition-colors", isSelected ? "bg-muted" : "hover:bg-muted/60"),
|
|
315
287
|
children: [
|
|
316
288
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
317
|
-
className: "
|
|
289
|
+
className: require_src.cn("text-muted-foreground relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-xl text-base font-semibold transition-colors", isSelected ? "bg-background" : "bg-muted"),
|
|
318
290
|
children: contact.avatar_url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
319
291
|
alt: name,
|
|
320
292
|
src: contact.avatar_url,
|
|
@@ -326,36 +298,16 @@ function ContactsSidebarRow({ contact, isSelected, onSelect }) {
|
|
|
326
298
|
}),
|
|
327
299
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
328
300
|
className: "min-w-0 flex-1",
|
|
329
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
330
|
-
className: "
|
|
331
|
-
children:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
status,
|
|
336
|
-
className: "shrink-0"
|
|
337
|
-
})]
|
|
338
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
339
|
-
className: "text-muted-foreground mt-0.5 flex items-center gap-2 text-sm",
|
|
340
|
-
children: [
|
|
341
|
-
secondary && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
342
|
-
className: "truncate",
|
|
343
|
-
children: secondary
|
|
344
|
-
}),
|
|
345
|
-
secondary && relativeLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
346
|
-
className: "text-muted-foreground/60 shrink-0 text-xs",
|
|
347
|
-
"aria-hidden": "true",
|
|
348
|
-
children: "·"
|
|
349
|
-
}),
|
|
350
|
-
relativeLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
351
|
-
className: "text-muted-foreground/60 shrink-0 text-xs",
|
|
352
|
-
children: relativeLabel
|
|
353
|
-
})
|
|
354
|
-
]
|
|
301
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
302
|
+
className: "text-foreground block truncate text-sm font-semibold tracking-tight",
|
|
303
|
+
children: name
|
|
304
|
+
}), secondary && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
305
|
+
className: "text-muted-foreground mt-0.5 block truncate text-sm",
|
|
306
|
+
children: secondary
|
|
355
307
|
})]
|
|
356
308
|
}),
|
|
357
309
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
|
|
358
|
-
className: require_src.cn("size-4 shrink-0 transition-all", isSelected ? "text-foreground" : "text-muted-foreground/60
|
|
310
|
+
className: require_src.cn("size-4 shrink-0 transition-all", isSelected ? "text-foreground" : "text-muted-foreground/60 opacity-0 group-hover:translate-x-0.5 group-hover:opacity-100"),
|
|
359
311
|
"aria-hidden": "true"
|
|
360
312
|
})
|
|
361
313
|
]
|
|
@@ -366,7 +318,7 @@ function ContactsSidebarRow({ contact, isSelected, onSelect }) {
|
|
|
366
318
|
const DEBOUNCE_MS = 200;
|
|
367
319
|
function useBuiltInFilters() {
|
|
368
320
|
const { t } = useContactsTranslation();
|
|
369
|
-
return [
|
|
321
|
+
return (0, react.useMemo)(() => [
|
|
370
322
|
{
|
|
371
323
|
id: "all",
|
|
372
324
|
label: t("filter_all"),
|
|
@@ -388,7 +340,7 @@ function useBuiltInFilters() {
|
|
|
388
340
|
status: "customer"
|
|
389
341
|
}
|
|
390
342
|
}
|
|
391
|
-
];
|
|
343
|
+
], [t]);
|
|
392
344
|
}
|
|
393
345
|
function filterId(value) {
|
|
394
346
|
if (value.kind === "all") return "all";
|
|
@@ -397,6 +349,7 @@ function filterId(value) {
|
|
|
397
349
|
}
|
|
398
350
|
function ContactsSidebar({ selectedContactId, onSelect, onAdd }) {
|
|
399
351
|
const { t } = useContactsTranslation();
|
|
352
|
+
const builtInFilters = useBuiltInFilters();
|
|
400
353
|
const [searchInput, setSearchInput] = (0, react.useState)("");
|
|
401
354
|
const [debouncedSearch, setDebouncedSearch] = (0, react.useState)("");
|
|
402
355
|
const [filter, setFilter] = (0, react.useState)({ kind: "all" });
|
|
@@ -419,8 +372,6 @@ function ContactsSidebar({ selectedContactId, onSelect, onAdd }) {
|
|
|
419
372
|
if (filter.kind === "status") return contacts.filter((c) => c.status === filter.status);
|
|
420
373
|
return contacts;
|
|
421
374
|
}, [contacts, filter]);
|
|
422
|
-
const serverTotalCount = data?.pages[0]?.meta.total_count ?? contacts.length;
|
|
423
|
-
const subtitleCount = filter.kind === "status" ? visibleContacts.length : serverTotalCount;
|
|
424
375
|
const sentinelRef = (0, react.useRef)(null);
|
|
425
376
|
const hasAutoSelectedRef = (0, react.useRef)(false);
|
|
426
377
|
(0, react.useEffect)(() => {
|
|
@@ -461,22 +412,26 @@ function ContactsSidebar({ selectedContactId, onSelect, onAdd }) {
|
|
|
461
412
|
const activeFilterId = filterId(filter);
|
|
462
413
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
463
414
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
464
|
-
className: "
|
|
415
|
+
className: "border-border/50 border-b px-6 py-3",
|
|
465
416
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
466
|
-
className: "flex items-
|
|
417
|
+
className: "flex items-center gap-3",
|
|
467
418
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
468
|
-
className: "min-w-0",
|
|
469
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
470
|
-
className: "text-foreground
|
|
471
|
-
|
|
472
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
473
|
-
|
|
474
|
-
|
|
419
|
+
className: "bg-muted focus-within:ring-foreground/20 flex min-w-0 flex-1 items-center gap-2 rounded-lg px-3 py-1.5 transition focus-within:ring-2",
|
|
420
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Search, {
|
|
421
|
+
className: "text-muted-foreground size-3.5 shrink-0",
|
|
422
|
+
"aria-hidden": "true"
|
|
423
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
424
|
+
value: searchInput,
|
|
425
|
+
onChange: (e) => setSearchInput(e.target.value),
|
|
426
|
+
placeholder: t("search_placeholder"),
|
|
427
|
+
type: "search",
|
|
428
|
+
className: "placeholder:text-muted-foreground/80 h-auto flex-1 border-0 bg-transparent p-0 text-xs font-medium shadow-none focus-visible:ring-0",
|
|
429
|
+
"aria-label": t("search_placeholder")
|
|
475
430
|
})]
|
|
476
431
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
477
432
|
type: "button",
|
|
478
433
|
onClick: onAdd,
|
|
479
|
-
className: "bg-
|
|
434
|
+
className: "bg-primary text-primary-foreground hover:bg-primary/90 inline-flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold transition-colors",
|
|
480
435
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, {
|
|
481
436
|
className: "size-3.5",
|
|
482
437
|
"aria-hidden": "true"
|
|
@@ -484,25 +439,9 @@ function ContactsSidebar({ selectedContactId, onSelect, onAdd }) {
|
|
|
484
439
|
})]
|
|
485
440
|
})
|
|
486
441
|
}),
|
|
487
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
488
|
-
className: "px-6 pb-3",
|
|
489
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
490
|
-
className: "bg-muted focus-within:ring-foreground/20 flex items-center gap-2.5 rounded-xl px-3.5 py-2.5 transition focus-within:ring-2",
|
|
491
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Search, {
|
|
492
|
-
className: "text-muted-foreground size-4 shrink-0",
|
|
493
|
-
"aria-hidden": "true"
|
|
494
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
495
|
-
value: searchInput,
|
|
496
|
-
onChange: (e) => setSearchInput(e.target.value),
|
|
497
|
-
placeholder: t("search_placeholder"),
|
|
498
|
-
type: "search",
|
|
499
|
-
className: "placeholder:text-muted-foreground/80 h-auto flex-1 border-0 bg-transparent p-0 text-sm font-medium shadow-none focus-visible:ring-0",
|
|
500
|
-
"aria-label": t("search_placeholder")
|
|
501
|
-
})]
|
|
502
|
-
})
|
|
503
|
-
}),
|
|
504
442
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilterPills, {
|
|
505
443
|
activeId: activeFilterId,
|
|
444
|
+
builtInFilters,
|
|
506
445
|
groups: groups.map((g) => g.name),
|
|
507
446
|
onChange: setFilter
|
|
508
447
|
}),
|
|
@@ -546,12 +485,11 @@ function ContactsSidebar({ selectedContactId, onSelect, onAdd }) {
|
|
|
546
485
|
})
|
|
547
486
|
] });
|
|
548
487
|
}
|
|
549
|
-
function FilterPills({ activeId, groups, onChange }) {
|
|
550
|
-
const builtInFilters = useBuiltInFilters();
|
|
488
|
+
function FilterPills({ activeId, builtInFilters, groups, onChange }) {
|
|
551
489
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
552
|
-
className: "
|
|
490
|
+
className: "overflow-x-auto pt-4 pb-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
|
553
491
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
554
|
-
className: "flex gap-1.5
|
|
492
|
+
className: "flex gap-1.5 px-6",
|
|
555
493
|
children: (0, react.useMemo)(() => [...builtInFilters, ...groups.map((tag) => ({
|
|
556
494
|
id: `group:${tag}`,
|
|
557
495
|
label: tag,
|
|
@@ -565,7 +503,7 @@ function FilterPills({ activeId, groups, onChange }) {
|
|
|
565
503
|
type: "button",
|
|
566
504
|
onClick: () => onChange(value),
|
|
567
505
|
"aria-pressed": isActive,
|
|
568
|
-
className: require_src.cn("shrink-0 rounded-full px-4 py-1.5 text-sm font-semibold capitalize transition-colors", isActive ? "bg-
|
|
506
|
+
className: require_src.cn("shrink-0 rounded-full px-4 py-1.5 text-sm font-semibold capitalize transition-colors", isActive ? "bg-primary text-primary-foreground" : "border-border/50 bg-muted text-foreground hover:bg-muted/70 border"),
|
|
569
507
|
children: label
|
|
570
508
|
}, id);
|
|
571
509
|
})
|
|
@@ -592,448 +530,853 @@ function SidebarSkeleton() {
|
|
|
592
530
|
});
|
|
593
531
|
}
|
|
594
532
|
//#endregion
|
|
595
|
-
//#region ../../contacts/ui/src/shared/
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
//#endregion
|
|
630
|
-
//#region ../../contacts/ui/src/shared/hooks/useDeleteContactMutation.ts
|
|
631
|
-
function useDeleteContactMutation(queryKeyPrefix = "contacts", options) {
|
|
632
|
-
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
633
|
-
const api = useContactsCrud();
|
|
634
|
-
return (0, _tanstack_react_query.useMutation)({
|
|
635
|
-
mutationFn: (contactId) => api.deleteContact(contactId),
|
|
636
|
-
onSuccess: () => {
|
|
637
|
-
require_src.fluidToast({
|
|
638
|
-
title: "Contact deleted successfully",
|
|
639
|
-
type: "success"
|
|
640
|
-
});
|
|
641
|
-
queryClient.invalidateQueries({ queryKey: CONTACTS_QUERY_KEYS.all(queryKeyPrefix) });
|
|
642
|
-
options?.onSuccess?.();
|
|
643
|
-
},
|
|
644
|
-
onError: (error) => {
|
|
645
|
-
require_src.fluidToast({
|
|
646
|
-
title: "Failed to delete contact",
|
|
647
|
-
type: "error",
|
|
648
|
-
description: parseApiErrors(error)
|
|
649
|
-
});
|
|
650
|
-
options?.onError?.(error);
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
//#endregion
|
|
655
|
-
//#region ../../contacts/ui/src/shared/schemas/contactFormSchema.ts
|
|
656
|
-
/**
|
|
657
|
-
* Form schema for creating a contact.
|
|
658
|
-
* Matches CompanyContactCreate / ContactCreate from the OpenAPI spec.
|
|
659
|
-
*
|
|
660
|
-
* @see CompanyContactCreate in company_contacts.d.ts
|
|
661
|
-
* @see ContactCreate in users_contacts.d.ts
|
|
662
|
-
*/
|
|
663
|
-
const createContactFormSchema = zod.z.object({
|
|
664
|
-
first_name: zod.z.string().min(1, { message: "First name is required" }),
|
|
665
|
-
last_name: zod.z.string().min(1, { message: "Last name is required" }),
|
|
666
|
-
status: zod.z.string().nullable().optional(),
|
|
667
|
-
email: zod.z.string().email().or(zod.z.literal("")).nullable().optional(),
|
|
668
|
-
phone: zod.z.string().nullable().optional(),
|
|
669
|
-
address: zod.z.string().nullable().optional(),
|
|
670
|
-
city: zod.z.string().nullable().optional(),
|
|
671
|
-
state: zod.z.string().nullable().optional(),
|
|
672
|
-
postal_code: zod.z.string().nullable().optional(),
|
|
673
|
-
country_code: zod.z.coerce.string().nullable().optional(),
|
|
674
|
-
language_code: zod.z.coerce.string().nullable().optional(),
|
|
675
|
-
affiliate: zod.z.record(zod.z.string(), zod.z.unknown()).nullable().optional(),
|
|
676
|
-
metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
677
|
-
});
|
|
678
|
-
/**
|
|
679
|
-
* Form schema for editing a contact.
|
|
680
|
-
* Same fields as create plus id. Uses .passthrough() so extra fields
|
|
681
|
-
* from the Contact read model (full_name, avatar_url, etc.) don't
|
|
682
|
-
* cause validation failures.
|
|
683
|
-
*
|
|
684
|
-
* @see CompanyContactUpdate in company_contacts.d.ts
|
|
685
|
-
*/
|
|
686
|
-
const editContactFormSchema = createContactFormSchema.passthrough();
|
|
687
|
-
//#endregion
|
|
688
|
-
//#region ../../contacts/ui/src/shared/hooks/useContactDetailPage.ts
|
|
689
|
-
const mutableKeys = Object.keys(createContactFormSchema.shape);
|
|
690
|
-
function useContactDetailPage(contactId, options) {
|
|
691
|
-
const queryKeyPrefix = options?.queryKeyPrefix ?? "contacts";
|
|
692
|
-
const { data, isLoading } = useContactDetail(contactId, queryKeyPrefix);
|
|
693
|
-
const { data: countries } = (0, _tanstack_react_query.useQuery)({
|
|
694
|
-
queryKey: ["countries"],
|
|
695
|
-
queryFn: options?.getCountries ?? (() => Promise.resolve([])),
|
|
696
|
-
enabled: !!options?.getCountries
|
|
697
|
-
});
|
|
698
|
-
const countryOptions = (0, react.useMemo)(() => [...countries?.map((c) => ({
|
|
699
|
-
name: c.name,
|
|
700
|
-
value: c.iso ?? c.id.toString()
|
|
701
|
-
})) ?? []].sort((a, b) => a.name.localeCompare(b.name)), [countries]);
|
|
702
|
-
const contact = data?.contact;
|
|
703
|
-
const methods = require_src.useZodForm(editContactFormSchema, {
|
|
704
|
-
values: (0, react.useMemo)(() => {
|
|
705
|
-
if (!contact) return void 0;
|
|
706
|
-
return {
|
|
707
|
-
...contact,
|
|
708
|
-
country_code: contact.country?.iso ?? contact.country_id?.toString() ?? null
|
|
709
|
-
};
|
|
710
|
-
}, [contact]),
|
|
711
|
-
mode: "onBlur"
|
|
712
|
-
});
|
|
713
|
-
const updateMutation = useUpdateContactMutation(contactId, queryKeyPrefix, { onSuccess: () => {
|
|
714
|
-
methods.reset(methods.getValues());
|
|
715
|
-
} });
|
|
716
|
-
const deleteMutation = useDeleteContactMutation(queryKeyPrefix, { onSuccess: () => {
|
|
717
|
-
options?.onDeleteSuccess?.();
|
|
718
|
-
} });
|
|
719
|
-
const onSave = (0, react.useCallback)(() => {
|
|
720
|
-
methods.handleSubmit((formData) => {
|
|
721
|
-
const payload = {};
|
|
722
|
-
for (const key of mutableKeys) if (key in formData) payload[key] = formData[key];
|
|
723
|
-
updateMutation.mutate({
|
|
724
|
-
id: contactId,
|
|
725
|
-
data: payload
|
|
726
|
-
});
|
|
727
|
-
}, (errors) => {
|
|
728
|
-
require_src.fluidToast({
|
|
729
|
-
title: "Please fix the form errors before saving",
|
|
730
|
-
description: Object.entries(errors).map(([field, err]) => {
|
|
731
|
-
const msg = typeof err?.message === "string" ? err.message : "invalid";
|
|
732
|
-
return `${field.replace(/_/g, " ")}: ${msg}`;
|
|
733
|
-
}).join(", ") || void 0,
|
|
734
|
-
type: "error"
|
|
735
|
-
});
|
|
736
|
-
})();
|
|
737
|
-
}, [
|
|
738
|
-
methods,
|
|
739
|
-
updateMutation,
|
|
740
|
-
contactId
|
|
741
|
-
]);
|
|
742
|
-
const onDelete = (0, react.useCallback)(() => {
|
|
743
|
-
deleteMutation.mutate(contactId);
|
|
744
|
-
}, [deleteMutation, contactId]);
|
|
745
|
-
return {
|
|
746
|
-
contact,
|
|
747
|
-
isLoading,
|
|
748
|
-
methods,
|
|
749
|
-
countryOptions,
|
|
750
|
-
isDirty: methods.formState.isDirty,
|
|
751
|
-
isSubmitting: updateMutation.isPending,
|
|
752
|
-
isDeleting: deleteMutation.isPending,
|
|
753
|
-
onSave,
|
|
754
|
-
onDelete
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
//#endregion
|
|
758
|
-
//#region ../../contacts/ui/src/portal/hooks/contacts/use-contact-tasks.ts
|
|
759
|
-
function useContactTasks(contactId) {
|
|
760
|
-
const api = useTasksApi();
|
|
761
|
-
return (0, _tanstack_react_query.useQuery)({
|
|
762
|
-
queryKey: contactsKeys.tasks(contactId),
|
|
763
|
-
queryFn: () => api.listTasks(contactId),
|
|
764
|
-
enabled: !!contactId,
|
|
765
|
-
select: (data) => data.tasks
|
|
533
|
+
//#region ../../contacts/ui/src/shared/components/contacts/contactDetailsForm.tsx
|
|
534
|
+
const DEFAULT_COUNTRIES = [];
|
|
535
|
+
const statusOptions = [
|
|
536
|
+
{
|
|
537
|
+
name: "New",
|
|
538
|
+
value: "new"
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "Active",
|
|
542
|
+
value: "active"
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "Inactive",
|
|
546
|
+
value: "inactive"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "Cold",
|
|
550
|
+
value: "cold"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: "Lead",
|
|
554
|
+
value: "lead"
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "Customer",
|
|
558
|
+
value: "customer"
|
|
559
|
+
}
|
|
560
|
+
];
|
|
561
|
+
const ContactDetailsForm = ({ className, countries = DEFAULT_COUNTRIES, renderAvatarPicker }) => {
|
|
562
|
+
const { control, watch, setValue } = (0, react_hook_form.useFormContext)();
|
|
563
|
+
const currentStatus = (0, react_hook_form.useWatch)({
|
|
564
|
+
control,
|
|
565
|
+
name: "status"
|
|
766
566
|
});
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
select: (data) => data.notes
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
//#endregion
|
|
780
|
-
//#region ../../contacts/ui/src/portal/components/contacts/rep-layout/ContactDetailHero.tsx
|
|
781
|
-
function ContactDetailHero({ contact, tasksCount, notesCount }) {
|
|
782
|
-
const { t } = useContactsTranslation();
|
|
783
|
-
const name = getDisplayName(contact);
|
|
784
|
-
const status = contact.status?.trim();
|
|
785
|
-
const lastUpdated = getRelativeUpdated(contact);
|
|
567
|
+
const avatarUrl = watch("avatar_url") ?? null;
|
|
568
|
+
const effectiveStatusOptions = (0, react.useMemo)(() => {
|
|
569
|
+
if (currentStatus && typeof currentStatus === "string" && !statusOptions.some((o) => o.value === currentStatus)) return [{
|
|
570
|
+
name: currentStatus.charAt(0).toUpperCase() + currentStatus.slice(1).replace(/_/g, " "),
|
|
571
|
+
value: currentStatus
|
|
572
|
+
}, ...statusOptions];
|
|
573
|
+
return statusOptions;
|
|
574
|
+
}, [currentStatus]);
|
|
575
|
+
const initials = [watch("first_name")?.[0] ?? "", watch("last_name")?.[0] ?? ""].filter(Boolean).join("").toUpperCase() || "?";
|
|
786
576
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
787
|
-
className: "
|
|
788
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
789
|
-
className: "flex flex-col
|
|
577
|
+
className: require_src.cn("space-y-6", className),
|
|
578
|
+
children: [renderAvatarPicker && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
579
|
+
className: "flex flex-col items-center gap-3",
|
|
790
580
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
791
|
-
className: "
|
|
792
|
-
children:
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
className: "
|
|
796
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
797
|
-
"
|
|
798
|
-
children:
|
|
581
|
+
className: "border-border bg-background relative h-20 w-20 shrink-0 overflow-hidden rounded-full border-2",
|
|
582
|
+
children: avatarUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
583
|
+
src: avatarUrl,
|
|
584
|
+
alt: "",
|
|
585
|
+
className: "h-full w-full object-cover"
|
|
586
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
587
|
+
className: "text-muted-foreground flex h-full w-full items-center justify-center text-lg font-semibold",
|
|
588
|
+
children: initials
|
|
799
589
|
})
|
|
800
|
-
}),
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
className: "text-foreground text-2xl leading-tight font-extrabold tracking-tight",
|
|
804
|
-
children: name
|
|
805
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
806
|
-
className: "mt-3 flex flex-wrap items-center gap-2.5",
|
|
807
|
-
children: [status && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusBadge, { status }), lastUpdated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
808
|
-
className: "text-muted-foreground text-sm",
|
|
809
|
-
children: t("last_updated", { date: lastUpdated })
|
|
810
|
-
})]
|
|
811
|
-
})]
|
|
590
|
+
}), renderAvatarPicker({
|
|
591
|
+
value: avatarUrl,
|
|
592
|
+
onChange: (url) => setValue("avatar_url", url ?? "", { shouldDirty: true })
|
|
812
593
|
})]
|
|
813
594
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
814
|
-
className: "
|
|
595
|
+
className: "grid grid-cols-1 gap-6 lg:grid-cols-2",
|
|
815
596
|
children: [
|
|
816
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
817
|
-
|
|
818
|
-
|
|
597
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
598
|
+
control,
|
|
599
|
+
name: "first_name",
|
|
600
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
601
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
602
|
+
className: "font-inter text-foreground font-medium",
|
|
603
|
+
children: "First Name"
|
|
604
|
+
}),
|
|
605
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
606
|
+
placeholder: "Enter first name",
|
|
607
|
+
...field,
|
|
608
|
+
value: field.value ?? "",
|
|
609
|
+
className: "ring-input"
|
|
610
|
+
}) }),
|
|
611
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
612
|
+
] })
|
|
819
613
|
}),
|
|
820
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
821
|
-
|
|
822
|
-
|
|
614
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
615
|
+
control,
|
|
616
|
+
name: "last_name",
|
|
617
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
618
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
619
|
+
className: "font-inter text-foreground font-medium",
|
|
620
|
+
children: "Last Name"
|
|
621
|
+
}),
|
|
622
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
623
|
+
placeholder: "Enter last name",
|
|
624
|
+
...field,
|
|
625
|
+
value: field.value ?? "",
|
|
626
|
+
className: "ring-input"
|
|
627
|
+
}) }),
|
|
628
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
629
|
+
] })
|
|
823
630
|
}),
|
|
824
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
825
|
-
|
|
826
|
-
|
|
631
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
632
|
+
control,
|
|
633
|
+
name: "email",
|
|
634
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
635
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
636
|
+
className: "font-inter text-foreground font-medium",
|
|
637
|
+
children: "Email"
|
|
638
|
+
}),
|
|
639
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
640
|
+
placeholder: "Enter email address",
|
|
641
|
+
type: "email",
|
|
642
|
+
...field,
|
|
643
|
+
value: field.value ?? "",
|
|
644
|
+
className: "ring-input"
|
|
645
|
+
}) }),
|
|
646
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
647
|
+
] })
|
|
648
|
+
}),
|
|
649
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
650
|
+
control,
|
|
651
|
+
name: "phone",
|
|
652
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
653
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
654
|
+
className: "font-inter text-foreground font-medium",
|
|
655
|
+
children: "Phone"
|
|
656
|
+
}),
|
|
657
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
658
|
+
placeholder: "Enter phone number",
|
|
659
|
+
...field,
|
|
660
|
+
value: field.value ?? "",
|
|
661
|
+
className: "ring-input"
|
|
662
|
+
}) }),
|
|
663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
664
|
+
] })
|
|
665
|
+
}),
|
|
666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
667
|
+
control,
|
|
668
|
+
name: "status",
|
|
669
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
670
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
671
|
+
className: "font-inter text-foreground font-medium",
|
|
672
|
+
children: "Status"
|
|
673
|
+
}),
|
|
674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.Select, {
|
|
675
|
+
value: field.value ?? "",
|
|
676
|
+
onValueChange: field.onChange,
|
|
677
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectTrigger, {
|
|
678
|
+
className: "w-full",
|
|
679
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectValue, { placeholder: "Select status" })
|
|
680
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectContent, {
|
|
681
|
+
position: "popper",
|
|
682
|
+
sideOffset: 4,
|
|
683
|
+
children: effectiveStatusOptions.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectItem, {
|
|
684
|
+
value: opt.value,
|
|
685
|
+
children: opt.name
|
|
686
|
+
}, opt.value))
|
|
687
|
+
})]
|
|
688
|
+
}),
|
|
689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
690
|
+
] })
|
|
691
|
+
}),
|
|
692
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
693
|
+
control,
|
|
694
|
+
name: "address",
|
|
695
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, {
|
|
696
|
+
className: "lg:col-span-2",
|
|
697
|
+
children: [
|
|
698
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
699
|
+
className: "font-inter text-foreground font-medium",
|
|
700
|
+
children: "Full Address"
|
|
701
|
+
}),
|
|
702
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
703
|
+
placeholder: "Enter street address",
|
|
704
|
+
...field,
|
|
705
|
+
value: field.value ?? "",
|
|
706
|
+
className: "ring-input"
|
|
707
|
+
}) }),
|
|
708
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
709
|
+
]
|
|
710
|
+
})
|
|
711
|
+
}),
|
|
712
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
713
|
+
control,
|
|
714
|
+
name: "city",
|
|
715
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
716
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
717
|
+
className: "font-inter text-foreground font-medium",
|
|
718
|
+
children: "City"
|
|
719
|
+
}),
|
|
720
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
721
|
+
placeholder: "Enter city",
|
|
722
|
+
...field,
|
|
723
|
+
value: field.value ?? "",
|
|
724
|
+
className: "ring-input"
|
|
725
|
+
}) }),
|
|
726
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
727
|
+
] })
|
|
728
|
+
}),
|
|
729
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
730
|
+
control,
|
|
731
|
+
name: "state",
|
|
732
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
733
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
734
|
+
className: "font-inter text-foreground font-medium",
|
|
735
|
+
children: "State/Province"
|
|
736
|
+
}),
|
|
737
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
738
|
+
placeholder: "Enter state or province",
|
|
739
|
+
...field,
|
|
740
|
+
value: field.value ?? "",
|
|
741
|
+
className: "ring-input"
|
|
742
|
+
}) }),
|
|
743
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
744
|
+
] })
|
|
745
|
+
}),
|
|
746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
747
|
+
control,
|
|
748
|
+
name: "postal_code",
|
|
749
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
751
|
+
className: "font-inter text-foreground font-medium",
|
|
752
|
+
children: "Postal Code"
|
|
753
|
+
}),
|
|
754
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
755
|
+
placeholder: "Enter postal code",
|
|
756
|
+
...field,
|
|
757
|
+
value: field.value ?? "",
|
|
758
|
+
className: "ring-input"
|
|
759
|
+
}) }),
|
|
760
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
761
|
+
] })
|
|
762
|
+
}),
|
|
763
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
764
|
+
control,
|
|
765
|
+
name: "country_code",
|
|
766
|
+
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
767
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
768
|
+
className: "font-inter text-foreground font-medium",
|
|
769
|
+
children: "Country"
|
|
770
|
+
}),
|
|
771
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.Select, {
|
|
772
|
+
value: field.value ?? "",
|
|
773
|
+
onValueChange: field.onChange,
|
|
774
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectTrigger, {
|
|
775
|
+
className: "w-full",
|
|
776
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectValue, { placeholder: "Select country" })
|
|
777
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectContent, {
|
|
778
|
+
position: "popper",
|
|
779
|
+
sideOffset: 4,
|
|
780
|
+
children: countries.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectItem, {
|
|
781
|
+
value: opt.value,
|
|
782
|
+
children: opt.name
|
|
783
|
+
}, opt.value))
|
|
784
|
+
})]
|
|
785
|
+
}),
|
|
786
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
787
|
+
] })
|
|
827
788
|
})
|
|
828
789
|
]
|
|
829
790
|
})]
|
|
830
791
|
});
|
|
831
|
-
}
|
|
832
|
-
function Stat({ label, value }) {
|
|
833
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
834
|
-
className: "text-muted-foreground text-xs font-bold tracking-wider uppercase",
|
|
835
|
-
children: label
|
|
836
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
837
|
-
className: "text-foreground mt-1 text-lg font-bold tracking-tight",
|
|
838
|
-
children: value
|
|
839
|
-
})] });
|
|
840
|
-
}
|
|
792
|
+
};
|
|
841
793
|
//#endregion
|
|
842
|
-
//#region ../../contacts/ui/src/
|
|
843
|
-
function
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
].filter((part) => Boolean(part?.trim()));
|
|
850
|
-
if (parts.length === 0) return null;
|
|
851
|
-
return parts.join(", ");
|
|
852
|
-
}
|
|
853
|
-
function ContactInfoRow({ contact }) {
|
|
854
|
-
const { t } = useContactsTranslation();
|
|
855
|
-
const address = buildAddressLine(contact);
|
|
856
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
857
|
-
className: "border-border bg-card mt-6 grid grid-cols-1 gap-x-6 gap-y-3 rounded-xl border p-4 md:grid-cols-3 md:p-5",
|
|
858
|
-
children: [
|
|
859
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoCell, {
|
|
860
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Mail, {
|
|
861
|
-
className: "size-3.5",
|
|
862
|
-
"aria-hidden": "true"
|
|
863
|
-
}),
|
|
864
|
-
label: t("label_email"),
|
|
865
|
-
value: contact.email,
|
|
866
|
-
notSetLabel: t("not_set")
|
|
867
|
-
}),
|
|
868
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoCell, {
|
|
869
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Phone, {
|
|
870
|
-
className: "size-3.5",
|
|
871
|
-
"aria-hidden": "true"
|
|
872
|
-
}),
|
|
873
|
-
label: t("label_phone"),
|
|
874
|
-
value: contact.phone,
|
|
875
|
-
notSetLabel: t("not_set")
|
|
876
|
-
}),
|
|
877
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoCell, {
|
|
878
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.MapPin, {
|
|
879
|
-
className: "size-3.5",
|
|
880
|
-
"aria-hidden": "true"
|
|
881
|
-
}),
|
|
882
|
-
label: t("label_address"),
|
|
883
|
-
value: address,
|
|
884
|
-
notSetLabel: t("not_set")
|
|
885
|
-
})
|
|
886
|
-
]
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
function InfoCell({ icon, label, value, notSetLabel }) {
|
|
890
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
891
|
-
className: "min-w-0",
|
|
892
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
893
|
-
className: "text-muted-foreground flex items-center gap-1.5 text-xs font-bold tracking-wider uppercase",
|
|
894
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
895
|
-
className: "text-muted-foreground",
|
|
896
|
-
children: icon
|
|
897
|
-
}), label]
|
|
898
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
899
|
-
className: "text-foreground mt-1 truncate text-sm font-medium",
|
|
900
|
-
children: value && value.trim().length > 0 ? value : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
901
|
-
className: "text-muted-foreground/60 font-normal italic",
|
|
902
|
-
children: notSetLabel
|
|
903
|
-
})
|
|
904
|
-
})]
|
|
905
|
-
});
|
|
906
|
-
}
|
|
907
|
-
//#endregion
|
|
908
|
-
//#region ../../contacts/ui/src/portal/hooks/contacts/use-toggle-task-completion.ts
|
|
909
|
-
function useToggleTaskCompletion(contactId) {
|
|
910
|
-
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
911
|
-
const api = useTasksApi();
|
|
912
|
-
return (0, _tanstack_react_query.useMutation)({
|
|
913
|
-
mutationFn: ({ taskId, isCompleted }) => api.updateTask(taskId, contactId, { completed_at: isCompleted ? null : (/* @__PURE__ */ new Date()).toISOString() }),
|
|
914
|
-
onSuccess: () => {
|
|
915
|
-
queryClient.invalidateQueries({ queryKey: contactsKeys.tasks(contactId) });
|
|
916
|
-
},
|
|
917
|
-
onError: (error) => {
|
|
918
|
-
require_src.fluidToast({
|
|
919
|
-
title: "Failed to update task",
|
|
920
|
-
type: "error",
|
|
921
|
-
description: parseApiErrors(error)
|
|
922
|
-
});
|
|
923
|
-
}
|
|
794
|
+
//#region ../../contacts/ui/src/shared/hooks/useContactDetail.ts
|
|
795
|
+
function useContactDetail(contactId, queryKeyPrefix = "contacts") {
|
|
796
|
+
const api = useContactsCrud();
|
|
797
|
+
return (0, _tanstack_react_query.useQuery)({
|
|
798
|
+
queryKey: CONTACTS_QUERY_KEYS.detail(queryKeyPrefix, contactId),
|
|
799
|
+
queryFn: () => api.getContact(contactId),
|
|
800
|
+
enabled: !!contactId
|
|
924
801
|
});
|
|
925
802
|
}
|
|
926
803
|
//#endregion
|
|
927
|
-
//#region ../../contacts/ui/src/
|
|
928
|
-
function
|
|
804
|
+
//#region ../../contacts/ui/src/shared/hooks/useUpdateContactMutation.ts
|
|
805
|
+
function useUpdateContactMutation(contactId, queryKeyPrefix = "contacts", options) {
|
|
929
806
|
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
930
|
-
const api =
|
|
807
|
+
const api = useContactsCrud();
|
|
931
808
|
return (0, _tanstack_react_query.useMutation)({
|
|
932
|
-
mutationFn: (
|
|
809
|
+
mutationFn: ({ id, data }) => api.updateContact(id, data),
|
|
933
810
|
onSuccess: () => {
|
|
934
811
|
require_src.fluidToast({
|
|
935
|
-
title: "
|
|
812
|
+
title: "Contact updated successfully",
|
|
936
813
|
type: "success"
|
|
937
814
|
});
|
|
938
|
-
queryClient.invalidateQueries({ queryKey:
|
|
815
|
+
queryClient.invalidateQueries({ queryKey: CONTACTS_QUERY_KEYS.all(queryKeyPrefix) });
|
|
939
816
|
options?.onSuccess?.();
|
|
940
817
|
},
|
|
941
818
|
onError: (error) => {
|
|
942
819
|
require_src.fluidToast({
|
|
943
|
-
title: "Failed to
|
|
820
|
+
title: "Failed to save contact",
|
|
944
821
|
type: "error",
|
|
945
822
|
description: parseApiErrors(error)
|
|
946
823
|
});
|
|
824
|
+
options?.onError?.(error);
|
|
947
825
|
}
|
|
948
826
|
});
|
|
949
827
|
}
|
|
950
828
|
//#endregion
|
|
951
|
-
//#region ../../contacts/ui/src/
|
|
952
|
-
function
|
|
829
|
+
//#region ../../contacts/ui/src/shared/hooks/useDeleteContactMutation.ts
|
|
830
|
+
function useDeleteContactMutation(queryKeyPrefix = "contacts", options) {
|
|
953
831
|
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
954
|
-
const api =
|
|
832
|
+
const api = useContactsCrud();
|
|
955
833
|
return (0, _tanstack_react_query.useMutation)({
|
|
956
|
-
mutationFn: (
|
|
834
|
+
mutationFn: (contactId) => api.deleteContact(contactId),
|
|
957
835
|
onSuccess: () => {
|
|
958
836
|
require_src.fluidToast({
|
|
959
|
-
title: "
|
|
837
|
+
title: "Contact deleted successfully",
|
|
960
838
|
type: "success"
|
|
961
839
|
});
|
|
962
|
-
queryClient.invalidateQueries({ queryKey:
|
|
840
|
+
queryClient.invalidateQueries({ queryKey: CONTACTS_QUERY_KEYS.all(queryKeyPrefix) });
|
|
963
841
|
options?.onSuccess?.();
|
|
964
842
|
},
|
|
965
843
|
onError: (error) => {
|
|
966
844
|
require_src.fluidToast({
|
|
967
|
-
title: "Failed to
|
|
845
|
+
title: "Failed to delete contact",
|
|
968
846
|
type: "error",
|
|
969
847
|
description: parseApiErrors(error)
|
|
970
848
|
});
|
|
849
|
+
options?.onError?.(error);
|
|
971
850
|
}
|
|
972
851
|
});
|
|
973
852
|
}
|
|
974
853
|
//#endregion
|
|
975
|
-
//#region ../../contacts/ui/src/
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
854
|
+
//#region ../../contacts/ui/src/shared/schemas/contactFormSchema.ts
|
|
855
|
+
/**
|
|
856
|
+
* Form schema for creating a contact.
|
|
857
|
+
* Matches CompanyContactCreate / ContactCreate from the OpenAPI spec.
|
|
858
|
+
*
|
|
859
|
+
* @see CompanyContactCreate in company_contacts.d.ts
|
|
860
|
+
* @see ContactCreate in users_contacts.d.ts
|
|
861
|
+
*/
|
|
862
|
+
const createContactFormSchema = zod.z.object({
|
|
863
|
+
first_name: zod.z.string().min(1, { message: "First name is required" }),
|
|
864
|
+
last_name: zod.z.string().min(1, { message: "Last name is required" }),
|
|
865
|
+
status: zod.z.string().nullable().optional(),
|
|
866
|
+
email: zod.z.string().email().or(zod.z.literal("")).nullable().optional(),
|
|
867
|
+
phone: zod.z.string().nullable().optional(),
|
|
868
|
+
address: zod.z.string().nullable().optional(),
|
|
869
|
+
city: zod.z.string().nullable().optional(),
|
|
870
|
+
state: zod.z.string().nullable().optional(),
|
|
871
|
+
postal_code: zod.z.string().nullable().optional(),
|
|
872
|
+
country_code: zod.z.coerce.string().nullable().optional(),
|
|
873
|
+
language_code: zod.z.coerce.string().nullable().optional(),
|
|
874
|
+
affiliate: zod.z.record(zod.z.string(), zod.z.unknown()).nullable().optional(),
|
|
875
|
+
metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
876
|
+
});
|
|
877
|
+
/**
|
|
878
|
+
* Form schema for editing a contact.
|
|
879
|
+
* Same fields as create plus id. Uses .passthrough() so extra fields
|
|
880
|
+
* from the Contact read model (full_name, avatar_url, etc.) don't
|
|
881
|
+
* cause validation failures.
|
|
882
|
+
*
|
|
883
|
+
* @see CompanyContactUpdate in company_contacts.d.ts
|
|
884
|
+
*/
|
|
885
|
+
const editContactFormSchema = createContactFormSchema.passthrough();
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region ../../contacts/ui/src/shared/hooks/useContactDetailPage.ts
|
|
888
|
+
const mutableKeys = Object.keys(createContactFormSchema.shape);
|
|
889
|
+
function useContactDetailPage(contactId, options) {
|
|
890
|
+
const queryKeyPrefix = options?.queryKeyPrefix ?? "contacts";
|
|
891
|
+
const { data, isLoading } = useContactDetail(contactId, queryKeyPrefix);
|
|
892
|
+
const { data: countries } = (0, _tanstack_react_query.useQuery)({
|
|
893
|
+
queryKey: ["countries"],
|
|
894
|
+
queryFn: options?.getCountries ?? (() => Promise.resolve([])),
|
|
895
|
+
enabled: !!options?.getCountries
|
|
896
|
+
});
|
|
897
|
+
const countryOptions = (0, react.useMemo)(() => [...countries?.map((c) => ({
|
|
898
|
+
name: c.name,
|
|
899
|
+
value: c.iso ?? c.id.toString()
|
|
900
|
+
})) ?? []].sort((a, b) => a.name.localeCompare(b.name)), [countries]);
|
|
901
|
+
const contact = data?.contact;
|
|
902
|
+
const methods = require_src.useZodForm(editContactFormSchema, {
|
|
903
|
+
values: (0, react.useMemo)(() => {
|
|
904
|
+
if (!contact) return void 0;
|
|
905
|
+
return {
|
|
906
|
+
...contact,
|
|
907
|
+
country_code: contact.country?.iso ?? contact.country_id?.toString() ?? null
|
|
908
|
+
};
|
|
909
|
+
}, [contact]),
|
|
910
|
+
mode: "onBlur"
|
|
911
|
+
});
|
|
912
|
+
const updateMutation = useUpdateContactMutation(contactId, queryKeyPrefix, { onSuccess: () => {
|
|
913
|
+
methods.reset(methods.getValues());
|
|
914
|
+
options?.onSaveSuccess?.();
|
|
915
|
+
} });
|
|
916
|
+
const deleteMutation = useDeleteContactMutation(queryKeyPrefix, { onSuccess: () => {
|
|
917
|
+
options?.onDeleteSuccess?.();
|
|
918
|
+
} });
|
|
919
|
+
const onSave = (0, react.useCallback)(() => {
|
|
920
|
+
methods.handleSubmit((formData) => {
|
|
921
|
+
const payload = {};
|
|
922
|
+
for (const key of mutableKeys) if (key in formData) payload[key] = formData[key];
|
|
923
|
+
updateMutation.mutate({
|
|
924
|
+
id: contactId,
|
|
925
|
+
data: payload
|
|
985
926
|
});
|
|
986
|
-
|
|
987
|
-
options?.onSuccess?.();
|
|
988
|
-
},
|
|
989
|
-
onError: (error) => {
|
|
927
|
+
}, (errors) => {
|
|
990
928
|
require_src.fluidToast({
|
|
991
|
-
title: "
|
|
992
|
-
|
|
993
|
-
|
|
929
|
+
title: "Please fix the form errors before saving",
|
|
930
|
+
description: Object.entries(errors).map(([field, err]) => {
|
|
931
|
+
const msg = typeof err?.message === "string" ? err.message : "invalid";
|
|
932
|
+
return `${field.replace(/_/g, " ")}: ${msg}`;
|
|
933
|
+
}).join(", ") || void 0,
|
|
934
|
+
type: "error"
|
|
994
935
|
});
|
|
995
|
-
}
|
|
996
|
-
}
|
|
936
|
+
})();
|
|
937
|
+
}, [
|
|
938
|
+
methods,
|
|
939
|
+
updateMutation,
|
|
940
|
+
contactId
|
|
941
|
+
]);
|
|
942
|
+
const onDelete = (0, react.useCallback)(() => {
|
|
943
|
+
deleteMutation.mutate(contactId);
|
|
944
|
+
}, [deleteMutation, contactId]);
|
|
945
|
+
return {
|
|
946
|
+
contact,
|
|
947
|
+
isLoading,
|
|
948
|
+
methods,
|
|
949
|
+
countryOptions,
|
|
950
|
+
isDirty: methods.formState.isDirty,
|
|
951
|
+
isSubmitting: updateMutation.isPending,
|
|
952
|
+
isDeleting: deleteMutation.isPending,
|
|
953
|
+
onSave,
|
|
954
|
+
onDelete
|
|
955
|
+
};
|
|
997
956
|
}
|
|
998
957
|
//#endregion
|
|
999
|
-
//#region ../../contacts/ui/src/portal/
|
|
1000
|
-
function
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
958
|
+
//#region ../../contacts/ui/src/portal/hooks/contacts/use-contact-tasks.ts
|
|
959
|
+
function useContactTasks(contactId) {
|
|
960
|
+
const api = useTasksApi();
|
|
961
|
+
return (0, _tanstack_react_query.useQuery)({
|
|
962
|
+
queryKey: contactsKeys.tasks(contactId),
|
|
963
|
+
queryFn: () => api.listTasks(contactId),
|
|
964
|
+
enabled: !!contactId,
|
|
965
|
+
select: (data) => data.tasks
|
|
1005
966
|
});
|
|
1006
967
|
}
|
|
1007
968
|
//#endregion
|
|
1008
|
-
//#region ../../contacts/ui/src/portal/
|
|
1009
|
-
function
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const bodyParts = [];
|
|
1017
|
-
doc.forEach((node) => {
|
|
1018
|
-
if (node.type.name === "heading" && !title) title = node.textContent.trim();
|
|
1019
|
-
else {
|
|
1020
|
-
const text = node.textContent.trim();
|
|
1021
|
-
if (text) bodyParts.push(text);
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
return {
|
|
1025
|
-
title,
|
|
1026
|
-
body: bodyParts.join("\n")
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
function extractBodyOnly(editor) {
|
|
1030
|
-
if (!editor) return "";
|
|
1031
|
-
const parts = [];
|
|
1032
|
-
editor.state.doc.forEach((node) => {
|
|
1033
|
-
const text = node.textContent.trim();
|
|
1034
|
-
if (text) parts.push(text);
|
|
969
|
+
//#region ../../contacts/ui/src/portal/hooks/notes/use-contact-notes.ts
|
|
970
|
+
function useContactNotes(contactId) {
|
|
971
|
+
const api = useNotesApi();
|
|
972
|
+
return (0, _tanstack_react_query.useQuery)({
|
|
973
|
+
queryKey: contactsKeys.notes(contactId),
|
|
974
|
+
queryFn: () => api.listNotes(contactId),
|
|
975
|
+
enabled: !!contactId,
|
|
976
|
+
select: (data) => data.notes
|
|
1035
977
|
});
|
|
1036
|
-
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region ../../contacts/ui/src/shared/components/contacts/statusBadge.tsx
|
|
981
|
+
const statusStyles = {
|
|
982
|
+
new: "border-[var(--status-new-border)] bg-[var(--status-new)] text-[var(--status-new-foreground)]",
|
|
983
|
+
active: "border-[var(--status-active-border)] bg-[var(--status-active)] text-[var(--status-active-foreground)]",
|
|
984
|
+
inactive: "border-border bg-muted text-muted-foreground",
|
|
985
|
+
lead: "border-[var(--status-lead-border)] bg-[var(--status-lead)] text-[var(--status-lead-foreground)]",
|
|
986
|
+
customer: "border-[var(--status-customer-border)] bg-[var(--status-customer)] text-[var(--status-customer-foreground)]",
|
|
987
|
+
success: "border-[var(--badge-success-border)] bg-[var(--badge-success)] text-[var(--badge-success-foreground)]",
|
|
988
|
+
warning: "border-[var(--badge-warning-border)] bg-[var(--badge-warning)] text-[var(--badge-warning-foreground)]",
|
|
989
|
+
danger: "border-[var(--badge-danger-border)] bg-[var(--badge-danger)] text-[var(--badge-danger-foreground)]",
|
|
990
|
+
info: "border-[var(--badge-info-border)] bg-[var(--badge-info)] text-[var(--badge-info-foreground)]",
|
|
991
|
+
neutral: "border-border bg-muted text-muted-foreground",
|
|
992
|
+
notice: "border-[var(--badge-notice-border)] bg-[var(--badge-notice)] text-[var(--badge-notice-foreground)]",
|
|
993
|
+
accent: "border-[var(--badge-accent-border)] bg-[var(--badge-accent)] text-[var(--badge-accent-foreground)]"
|
|
994
|
+
};
|
|
995
|
+
const defaultStyle = "border-border bg-muted text-muted-foreground";
|
|
996
|
+
function getStatusStyle(status) {
|
|
997
|
+
return statusStyles[status] ?? defaultStyle;
|
|
998
|
+
}
|
|
999
|
+
function StatusBadge({ status, label, className }) {
|
|
1000
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1001
|
+
className: require_src.cn("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold capitalize", getStatusStyle(status), className),
|
|
1002
|
+
children: label ?? status
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
//#endregion
|
|
1006
|
+
//#region ../../contacts/ui/src/portal/components/contacts/rep-layout/ContactDetailHero.tsx
|
|
1007
|
+
function ContactDetailHero({ contact, tasksCount, notesCount }) {
|
|
1008
|
+
const { t } = useContactsTranslation();
|
|
1009
|
+
const name = getDisplayName(contact);
|
|
1010
|
+
const status = contact.status?.trim();
|
|
1011
|
+
const lastUpdated = getRelativeUpdated(contact);
|
|
1012
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1013
|
+
className: "flex flex-col gap-6 md:flex-row md:items-center md:justify-between md:gap-8",
|
|
1014
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1015
|
+
className: "flex flex-col items-start gap-4 md:flex-row md:items-center md:gap-5",
|
|
1016
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1017
|
+
className: "bg-muted relative flex size-20 shrink-0 items-center justify-center overflow-hidden rounded-2xl md:size-24",
|
|
1018
|
+
children: contact.avatar_url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1019
|
+
alt: name,
|
|
1020
|
+
src: contact.avatar_url,
|
|
1021
|
+
className: "size-full object-cover"
|
|
1022
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1023
|
+
"aria-hidden": "true",
|
|
1024
|
+
className: "text-muted-foreground text-2xl font-bold tracking-tight md:text-3xl",
|
|
1025
|
+
children: getInitials(name)
|
|
1026
|
+
})
|
|
1027
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1028
|
+
className: "min-w-0 flex-1",
|
|
1029
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1030
|
+
className: "text-foreground text-2xl leading-tight font-bold tracking-tight md:text-3xl",
|
|
1031
|
+
children: name
|
|
1032
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1033
|
+
className: "mt-2.5 flex flex-wrap items-center gap-2",
|
|
1034
|
+
children: [
|
|
1035
|
+
status && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusBadge, { status }),
|
|
1036
|
+
status && lastUpdated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1037
|
+
"aria-hidden": "true",
|
|
1038
|
+
className: "text-muted-foreground/40 text-sm",
|
|
1039
|
+
children: "·"
|
|
1040
|
+
}),
|
|
1041
|
+
lastUpdated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1042
|
+
className: "text-muted-foreground text-sm",
|
|
1043
|
+
children: t("last_updated", { date: lastUpdated })
|
|
1044
|
+
})
|
|
1045
|
+
]
|
|
1046
|
+
})]
|
|
1047
|
+
})]
|
|
1048
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1049
|
+
className: "flex shrink-0 items-center gap-6 md:gap-8",
|
|
1050
|
+
children: [
|
|
1051
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
1052
|
+
label: t("tab_tasks"),
|
|
1053
|
+
value: tasksCount,
|
|
1054
|
+
icon: lucide_react.ListTodo
|
|
1055
|
+
}),
|
|
1056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1057
|
+
className: "bg-border h-10 w-px",
|
|
1058
|
+
"aria-hidden": "true"
|
|
1059
|
+
}),
|
|
1060
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
1061
|
+
label: t("tab_notes"),
|
|
1062
|
+
value: notesCount,
|
|
1063
|
+
icon: lucide_react.StickyNote
|
|
1064
|
+
})
|
|
1065
|
+
]
|
|
1066
|
+
})]
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function Stat({ label, value, icon: Icon }) {
|
|
1070
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1071
|
+
className: "flex flex-col items-start gap-1",
|
|
1072
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1073
|
+
className: "text-muted-foreground flex items-center gap-1.5",
|
|
1074
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1075
|
+
className: "size-3.5",
|
|
1076
|
+
"aria-hidden": true
|
|
1077
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1078
|
+
className: "text-xs font-medium",
|
|
1079
|
+
children: label
|
|
1080
|
+
})]
|
|
1081
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1082
|
+
className: "text-foreground text-2xl font-bold tracking-tight tabular-nums",
|
|
1083
|
+
children: value
|
|
1084
|
+
})]
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region ../../contacts/ui/src/portal/components/contacts/rep-layout/ContactInfoRow.tsx
|
|
1089
|
+
function buildAddressLine(contact) {
|
|
1090
|
+
const parts = [
|
|
1091
|
+
contact.address,
|
|
1092
|
+
contact.city,
|
|
1093
|
+
contact.state,
|
|
1094
|
+
contact.postal_code
|
|
1095
|
+
].filter((part) => Boolean(part?.trim()));
|
|
1096
|
+
if (parts.length === 0) return null;
|
|
1097
|
+
return parts.join(", ");
|
|
1098
|
+
}
|
|
1099
|
+
function ContactInfoRow({ contact, onEditEmpty }) {
|
|
1100
|
+
const { t } = useContactsTranslation();
|
|
1101
|
+
const address = buildAddressLine(contact);
|
|
1102
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1103
|
+
className: "border-border/50 divide-border/50 mt-6 divide-y overflow-hidden rounded-2xl border",
|
|
1104
|
+
children: [
|
|
1105
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoRow, {
|
|
1106
|
+
icon: lucide_react.Mail,
|
|
1107
|
+
label: t("label_email"),
|
|
1108
|
+
value: contact.email,
|
|
1109
|
+
href: contact.email ? `mailto:${contact.email}` : void 0,
|
|
1110
|
+
onEditEmpty
|
|
1111
|
+
}),
|
|
1112
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoRow, {
|
|
1113
|
+
icon: lucide_react.Phone,
|
|
1114
|
+
label: t("label_phone"),
|
|
1115
|
+
value: contact.phone,
|
|
1116
|
+
href: contact.phone ? `tel:${contact.phone}` : void 0,
|
|
1117
|
+
onEditEmpty
|
|
1118
|
+
}),
|
|
1119
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoRow, {
|
|
1120
|
+
icon: lucide_react.MapPin,
|
|
1121
|
+
label: t("label_address"),
|
|
1122
|
+
value: address,
|
|
1123
|
+
href: address ? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}` : void 0,
|
|
1124
|
+
external: true,
|
|
1125
|
+
onEditEmpty
|
|
1126
|
+
})
|
|
1127
|
+
]
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
function InfoRow({ icon: Icon, label, value, href, external = false, onEditEmpty }) {
|
|
1131
|
+
const { t } = useContactsTranslation();
|
|
1132
|
+
const trimmed = value?.trim();
|
|
1133
|
+
const isEmpty = !trimmed;
|
|
1134
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1135
|
+
const handleCopy = () => {
|
|
1136
|
+
if (!trimmed) return;
|
|
1137
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) return;
|
|
1138
|
+
navigator.clipboard.writeText(trimmed).then(() => {
|
|
1139
|
+
setCopied(true);
|
|
1140
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
1141
|
+
}).catch(() => {});
|
|
1142
|
+
};
|
|
1143
|
+
if (isEmpty) {
|
|
1144
|
+
const addText = t("add_field", { field: label });
|
|
1145
|
+
if (!onEditEmpty) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1146
|
+
className: "flex items-center gap-3 px-4 py-2.5 md:px-5",
|
|
1147
|
+
children: [
|
|
1148
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1149
|
+
className: "border-border/50 text-muted-foreground/50 flex size-9 shrink-0 items-center justify-center rounded-full border-2 border-dashed",
|
|
1150
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1151
|
+
className: "size-4",
|
|
1152
|
+
"aria-hidden": true
|
|
1153
|
+
})
|
|
1154
|
+
}),
|
|
1155
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1156
|
+
className: "min-w-0 flex-1",
|
|
1157
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1158
|
+
className: "text-muted-foreground text-xs font-medium",
|
|
1159
|
+
children: label
|
|
1160
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1161
|
+
className: "text-muted-foreground/70 truncate text-sm italic",
|
|
1162
|
+
children: addText
|
|
1163
|
+
})]
|
|
1164
|
+
}),
|
|
1165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, {
|
|
1166
|
+
className: "text-muted-foreground/40 size-4 shrink-0",
|
|
1167
|
+
"aria-hidden": true
|
|
1168
|
+
})
|
|
1169
|
+
]
|
|
1170
|
+
});
|
|
1171
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1172
|
+
type: "button",
|
|
1173
|
+
onClick: onEditEmpty,
|
|
1174
|
+
"aria-label": addText,
|
|
1175
|
+
className: "hover:bg-muted/60 group flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors md:px-5",
|
|
1176
|
+
children: [
|
|
1177
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1178
|
+
className: "border-border/50 text-muted-foreground/50 group-hover:border-foreground/30 group-hover:text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded-full border-2 border-dashed transition-colors",
|
|
1179
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1180
|
+
className: "size-4",
|
|
1181
|
+
"aria-hidden": true
|
|
1182
|
+
})
|
|
1183
|
+
}),
|
|
1184
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1185
|
+
className: "min-w-0 flex-1",
|
|
1186
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1187
|
+
className: "text-muted-foreground text-xs font-medium",
|
|
1188
|
+
children: label
|
|
1189
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1190
|
+
className: "text-muted-foreground/70 group-hover:text-muted-foreground truncate text-sm italic transition-colors",
|
|
1191
|
+
children: addText
|
|
1192
|
+
})]
|
|
1193
|
+
}),
|
|
1194
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, {
|
|
1195
|
+
className: "text-muted-foreground/40 group-hover:text-foreground size-4 shrink-0 transition-colors",
|
|
1196
|
+
"aria-hidden": true
|
|
1197
|
+
})
|
|
1198
|
+
]
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1202
|
+
className: "hover:bg-muted/60 group flex items-center transition-colors",
|
|
1203
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
1204
|
+
href,
|
|
1205
|
+
...external ? {
|
|
1206
|
+
target: "_blank",
|
|
1207
|
+
rel: "noopener noreferrer"
|
|
1208
|
+
} : {},
|
|
1209
|
+
className: "flex min-w-0 flex-1 items-center gap-3 px-4 py-2.5 md:px-5",
|
|
1210
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1211
|
+
className: "bg-muted text-muted-foreground group-hover:bg-muted/70 group-hover:text-foreground flex size-9 shrink-0 items-center justify-center rounded-full transition-colors",
|
|
1212
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1213
|
+
className: "size-4",
|
|
1214
|
+
"aria-hidden": true
|
|
1215
|
+
})
|
|
1216
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1217
|
+
className: "min-w-0 flex-1",
|
|
1218
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1219
|
+
className: "text-muted-foreground text-xs font-medium",
|
|
1220
|
+
children: label
|
|
1221
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1222
|
+
className: "text-foreground truncate text-sm font-semibold",
|
|
1223
|
+
children: trimmed
|
|
1224
|
+
})]
|
|
1225
|
+
})]
|
|
1226
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1227
|
+
type: "button",
|
|
1228
|
+
onClick: handleCopy,
|
|
1229
|
+
"aria-label": copied ? t("copied_field", { field: label }) : t("copy_field", { field: label }),
|
|
1230
|
+
className: "text-muted-foreground hover:bg-muted hover:text-foreground mr-3 flex size-8 shrink-0 items-center justify-center rounded-md transition-colors",
|
|
1231
|
+
children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Check, {
|
|
1232
|
+
className: "text-primary size-3.5",
|
|
1233
|
+
"aria-hidden": true
|
|
1234
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Copy, {
|
|
1235
|
+
className: "size-3.5",
|
|
1236
|
+
"aria-hidden": true
|
|
1237
|
+
})
|
|
1238
|
+
})]
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region ../../contacts/core/src/iso-date.ts
|
|
1243
|
+
/**
|
|
1244
|
+
* Format a date as YYYY-MM-DD in the renderer's local timezone, optionally
|
|
1245
|
+
* shifted by `offsetDays`. Use this for due-date inputs where "today" must
|
|
1246
|
+
* resolve to the user's local calendar date — never `toISOString().slice(0, 10)`,
|
|
1247
|
+
* which returns the UTC date and silently rolls over a day for users east/west
|
|
1248
|
+
* of UTC at the wrong hours.
|
|
1249
|
+
*/
|
|
1250
|
+
function isoDate(offsetDays, now = /* @__PURE__ */ new Date()) {
|
|
1251
|
+
const d = new Date(now);
|
|
1252
|
+
d.setDate(d.getDate() + offsetDays);
|
|
1253
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Return a Date pinned to local midnight of the calendar day represented by
|
|
1257
|
+
* `input`. Use this whenever you need to compare two due dates by calendar day
|
|
1258
|
+
* (overdue / today / tomorrow / future).
|
|
1259
|
+
*
|
|
1260
|
+
* Why not just `new Date(input)`? `new Date("YYYY-MM-DD")` parses the string
|
|
1261
|
+
* as UTC midnight, which lands on the *previous* calendar day in any UTC−
|
|
1262
|
+
* timezone — a task due "May 1" then reads as April 30 for a user in the
|
|
1263
|
+
* Americas, classifying it as "Overdue" all day. Parse the date components
|
|
1264
|
+
* directly so the calendar-day intent of the string is preserved.
|
|
1265
|
+
*/
|
|
1266
|
+
function startOfLocalDay(input) {
|
|
1267
|
+
if (typeof input === "string") {
|
|
1268
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(input);
|
|
1269
|
+
if (match?.[1] && match[2] && match[3]) return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
1270
|
+
}
|
|
1271
|
+
const d = typeof input === "string" ? new Date(input) : input;
|
|
1272
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
1273
|
+
}
|
|
1274
|
+
//#endregion
|
|
1275
|
+
//#region ../../contacts/ui/src/portal/hooks/contacts/use-toggle-task-completion.ts
|
|
1276
|
+
function useToggleTaskCompletion(contactId) {
|
|
1277
|
+
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
1278
|
+
const api = useTasksApi();
|
|
1279
|
+
return (0, _tanstack_react_query.useMutation)({
|
|
1280
|
+
mutationFn: ({ taskId, isCompleted }) => api.updateTask(taskId, contactId, { completed_at: isCompleted ? null : (/* @__PURE__ */ new Date()).toISOString() }),
|
|
1281
|
+
onSuccess: () => {
|
|
1282
|
+
queryClient.invalidateQueries({ queryKey: contactsKeys.tasks(contactId) });
|
|
1283
|
+
},
|
|
1284
|
+
onError: (error) => {
|
|
1285
|
+
require_src.fluidToast({
|
|
1286
|
+
title: "Failed to update task",
|
|
1287
|
+
type: "error",
|
|
1288
|
+
description: parseApiErrors(error)
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
//#endregion
|
|
1294
|
+
//#region ../../contacts/ui/src/portal/hooks/contacts/use-delete-contact-task.ts
|
|
1295
|
+
function useDeleteContactTask(contactId, options) {
|
|
1296
|
+
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
1297
|
+
const api = useTasksApi();
|
|
1298
|
+
return (0, _tanstack_react_query.useMutation)({
|
|
1299
|
+
mutationFn: (taskId) => api.deleteTask(taskId, contactId),
|
|
1300
|
+
onSuccess: () => {
|
|
1301
|
+
require_src.fluidToast({
|
|
1302
|
+
title: "Task deleted",
|
|
1303
|
+
type: "success"
|
|
1304
|
+
});
|
|
1305
|
+
queryClient.invalidateQueries({ queryKey: contactsKeys.tasks(contactId) });
|
|
1306
|
+
options?.onSuccess?.();
|
|
1307
|
+
},
|
|
1308
|
+
onError: (error) => {
|
|
1309
|
+
require_src.fluidToast({
|
|
1310
|
+
title: "Failed to delete task",
|
|
1311
|
+
type: "error",
|
|
1312
|
+
description: parseApiErrors(error)
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
//#endregion
|
|
1318
|
+
//#region ../../contacts/ui/src/portal/hooks/contacts/use-update-contact-task.ts
|
|
1319
|
+
function useUpdateContactTask(contactId, options) {
|
|
1320
|
+
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
1321
|
+
const api = useTasksApi();
|
|
1322
|
+
return (0, _tanstack_react_query.useMutation)({
|
|
1323
|
+
mutationFn: ({ taskId, input }) => api.updateTask(taskId, contactId, input),
|
|
1324
|
+
onSuccess: () => {
|
|
1325
|
+
require_src.fluidToast({
|
|
1326
|
+
title: "Task updated",
|
|
1327
|
+
type: "success"
|
|
1328
|
+
});
|
|
1329
|
+
queryClient.invalidateQueries({ queryKey: contactsKeys.tasks(contactId) });
|
|
1330
|
+
options?.onSuccess?.();
|
|
1331
|
+
},
|
|
1332
|
+
onError: (error) => {
|
|
1333
|
+
require_src.fluidToast({
|
|
1334
|
+
title: "Failed to update task",
|
|
1335
|
+
type: "error",
|
|
1336
|
+
description: parseApiErrors(error)
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
//#endregion
|
|
1342
|
+
//#region ../../contacts/ui/src/portal/utils/format-date.ts
|
|
1343
|
+
function formatDateForDisplay(dateStr) {
|
|
1344
|
+
return new Date(dateStr).toLocaleDateString("en-US", {
|
|
1345
|
+
month: "short",
|
|
1346
|
+
day: "numeric",
|
|
1347
|
+
year: "numeric"
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
//#endregion
|
|
1351
|
+
//#region ../../contacts/ui/src/portal/components/editor/note-task-editor.tsx
|
|
1352
|
+
function extractTitleAndBody(editor) {
|
|
1353
|
+
if (!editor) return {
|
|
1354
|
+
title: "",
|
|
1355
|
+
body: ""
|
|
1356
|
+
};
|
|
1357
|
+
const doc = editor.state.doc;
|
|
1358
|
+
let title = "";
|
|
1359
|
+
const bodyParts = [];
|
|
1360
|
+
doc.forEach((node) => {
|
|
1361
|
+
if (node.type.name === "heading" && !title) title = node.textContent.trim();
|
|
1362
|
+
else {
|
|
1363
|
+
const text = node.textContent.trim();
|
|
1364
|
+
if (text) bodyParts.push(text);
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
return {
|
|
1368
|
+
title,
|
|
1369
|
+
body: bodyParts.join("\n")
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function extractBodyOnly(editor) {
|
|
1373
|
+
if (!editor) return "";
|
|
1374
|
+
const parts = [];
|
|
1375
|
+
editor.state.doc.forEach((node) => {
|
|
1376
|
+
const text = node.textContent.trim();
|
|
1377
|
+
if (text) parts.push(text);
|
|
1378
|
+
});
|
|
1379
|
+
return parts.join("\n");
|
|
1037
1380
|
}
|
|
1038
1381
|
function NoteTaskEditor({ titlePlaceholder = "New Note", bodyPlaceholder = "Start writing...", onChange, className, initialTitle, initialBody, showDueDate = false, initialDueDate, showTitle = true, editorClassName }) {
|
|
1039
1382
|
const [dueDate, setDueDate] = (0, react.useState)(initialDueDate);
|
|
@@ -1123,10 +1466,10 @@ function NoteTaskEditor({ titlePlaceholder = "New Note", bodyPlaceholder = "Star
|
|
|
1123
1466
|
const buttonInactive = "text-muted-foreground hover:bg-muted/50";
|
|
1124
1467
|
const toolbarSeparator = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "bg-border mx-1 h-5 w-px" });
|
|
1125
1468
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1126
|
-
className: require_src.cn("border-border flex flex-col overflow-hidden rounded-lg border", className),
|
|
1469
|
+
className: require_src.cn("border-border/50 flex flex-col overflow-hidden rounded-lg border", className),
|
|
1127
1470
|
children: [
|
|
1128
1471
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1129
|
-
className: "border-border bg-muted/50 flex items-center gap-0.5 border-b px-2 py-1.5",
|
|
1472
|
+
className: "border-border/50 bg-muted/50 flex items-center gap-0.5 border-b px-2 py-1.5",
|
|
1130
1473
|
children: [
|
|
1131
1474
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1132
1475
|
type: "button",
|
|
@@ -1392,7 +1735,7 @@ function formatDueDate$1(dateStr) {
|
|
|
1392
1735
|
year: "numeric"
|
|
1393
1736
|
});
|
|
1394
1737
|
}
|
|
1395
|
-
function formatRelativeTime(dateStr) {
|
|
1738
|
+
function formatRelativeTime(dateStr, t) {
|
|
1396
1739
|
const date = new Date(dateStr);
|
|
1397
1740
|
const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
|
|
1398
1741
|
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
@@ -1400,45 +1743,50 @@ function formatRelativeTime(dateStr) {
|
|
|
1400
1743
|
hour: "numeric",
|
|
1401
1744
|
minute: "2-digit"
|
|
1402
1745
|
});
|
|
1403
|
-
|
|
1404
|
-
|
|
1746
|
+
if (diffDays === 1) return t("task_yesterday");
|
|
1747
|
+
return diffDays === 1 ? t("task_days_ago_one") : t("task_days_ago_other", { count: diffDays });
|
|
1405
1748
|
}
|
|
1406
|
-
function
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "h-3.5 w-3.5" }), "New Task"]
|
|
1433
|
-
})
|
|
1434
|
-
]
|
|
1435
|
-
});
|
|
1749
|
+
function classifyDue(dueAt, isCompleted, t) {
|
|
1750
|
+
if (isCompleted) return {
|
|
1751
|
+
label: t("task_due", { date: formatDueDate$1(dueAt) }),
|
|
1752
|
+
tone: "done"
|
|
1753
|
+
};
|
|
1754
|
+
const due = startOfLocalDay(dueAt);
|
|
1755
|
+
const today = startOfLocalDay(/* @__PURE__ */ new Date());
|
|
1756
|
+
const tomorrow = new Date(today);
|
|
1757
|
+
tomorrow.setDate(today.getDate() + 1);
|
|
1758
|
+
const dueTime = due.getTime();
|
|
1759
|
+
if (dueTime < today.getTime()) return {
|
|
1760
|
+
label: t("task_overdue_due", { date: formatDueDate$1(dueAt) }),
|
|
1761
|
+
tone: "overdue"
|
|
1762
|
+
};
|
|
1763
|
+
if (dueTime === today.getTime()) return {
|
|
1764
|
+
label: t("quick_today"),
|
|
1765
|
+
tone: "today"
|
|
1766
|
+
};
|
|
1767
|
+
if (dueTime === tomorrow.getTime()) return {
|
|
1768
|
+
label: t("quick_tomorrow"),
|
|
1769
|
+
tone: "tomorrow"
|
|
1770
|
+
};
|
|
1771
|
+
return {
|
|
1772
|
+
label: formatDueDate$1(dueAt),
|
|
1773
|
+
tone: "future"
|
|
1774
|
+
};
|
|
1436
1775
|
}
|
|
1776
|
+
const DUE_TONE_CLASS = {
|
|
1777
|
+
overdue: "bg-destructive/10 text-destructive",
|
|
1778
|
+
today: "bg-primary/10 text-primary",
|
|
1779
|
+
tomorrow: "bg-primary/10 text-primary",
|
|
1780
|
+
future: "bg-muted text-muted-foreground",
|
|
1781
|
+
done: "bg-muted text-muted-foreground"
|
|
1782
|
+
};
|
|
1437
1783
|
function TaskCard({ task, toggleCompletion, onEdit, onDeleteClick }) {
|
|
1784
|
+
const { t } = useContactsTranslation();
|
|
1438
1785
|
const isCompleted = !!task.completed_at;
|
|
1439
1786
|
const { title, body: bodyText } = require_parse_task_body.parseTaskBody(task.body ?? "");
|
|
1440
|
-
const timestamp = task.created_at ? formatRelativeTime(task.created_at) : "";
|
|
1441
|
-
|
|
1787
|
+
const timestamp = task.created_at ? formatRelativeTime(task.created_at, t) : "";
|
|
1788
|
+
const due = task.due_at ? classifyDue(task.due_at, isCompleted, t) : null;
|
|
1789
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1442
1790
|
role: "button",
|
|
1443
1791
|
tabIndex: 0,
|
|
1444
1792
|
onClick: () => onEdit(task),
|
|
@@ -1448,8 +1796,8 @@ function TaskCard({ task, toggleCompletion, onEdit, onDeleteClick }) {
|
|
|
1448
1796
|
onEdit(task);
|
|
1449
1797
|
}
|
|
1450
1798
|
},
|
|
1451
|
-
className: "border-border
|
|
1452
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1799
|
+
className: "group border-border/50 hover:border-foreground/20 hover:bg-muted/40 relative cursor-pointer rounded-xl border p-4 transition-all hover:shadow-sm",
|
|
1800
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1453
1801
|
className: "flex items-start gap-3",
|
|
1454
1802
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1455
1803
|
type: "button",
|
|
@@ -1461,133 +1809,99 @@ function TaskCard({ task, toggleCompletion, onEdit, onDeleteClick }) {
|
|
|
1461
1809
|
});
|
|
1462
1810
|
},
|
|
1463
1811
|
disabled: toggleCompletion.isPending,
|
|
1464
|
-
className: "mt-0.5 shrink-0 transition-
|
|
1465
|
-
"aria-label": isCompleted ? "
|
|
1466
|
-
children: isCompleted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CircleCheck, {
|
|
1812
|
+
className: "mt-0.5 shrink-0 transition-transform hover:scale-110 disabled:cursor-not-allowed disabled:opacity-50",
|
|
1813
|
+
"aria-label": isCompleted ? t("mark_as_open") : t("mark_as_completed"),
|
|
1814
|
+
children: isCompleted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CircleCheck, {
|
|
1815
|
+
className: "text-primary size-5",
|
|
1816
|
+
"aria-hidden": "true"
|
|
1817
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-muted-foreground/40 group-hover:border-primary size-5 rounded-full border-2 transition-colors" })
|
|
1467
1818
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1468
1819
|
className: "min-w-0 flex-1",
|
|
1469
1820
|
children: [
|
|
1470
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1471
|
-
className: "
|
|
1472
|
-
children:
|
|
1473
|
-
className: require_src.cn("text-foreground line-clamp-2 font-semibold", isCompleted && "text-muted-foreground line-through"),
|
|
1474
|
-
children: title
|
|
1475
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuTrigger, {
|
|
1476
|
-
asChild: true,
|
|
1477
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Button, {
|
|
1478
|
-
variant: "ghost",
|
|
1479
|
-
size: "icon-xs",
|
|
1480
|
-
onClick: (e) => e.stopPropagation(),
|
|
1481
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.EllipsisVertical, { className: "h-4 w-4" })
|
|
1482
|
-
})
|
|
1483
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuContent, {
|
|
1484
|
-
align: "end",
|
|
1485
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuItem, {
|
|
1486
|
-
className: "text-destructive",
|
|
1487
|
-
onClick: (e) => {
|
|
1488
|
-
e.stopPropagation();
|
|
1489
|
-
onDeleteClick(task);
|
|
1490
|
-
},
|
|
1491
|
-
children: "Delete"
|
|
1492
|
-
})
|
|
1493
|
-
})] })]
|
|
1821
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
1822
|
+
className: require_src.cn("text-foreground line-clamp-2 text-sm leading-snug font-semibold", isCompleted && "text-muted-foreground line-through"),
|
|
1823
|
+
children: title
|
|
1494
1824
|
}),
|
|
1495
1825
|
bodyText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1496
|
-
className: "text-muted-foreground mt-1 line-clamp-
|
|
1497
|
-
style: isCompleted ? { textDecoration: "line-through" } : void 0,
|
|
1826
|
+
className: require_src.cn("text-muted-foreground mt-1 line-clamp-2 text-xs leading-relaxed", isCompleted && "line-through"),
|
|
1498
1827
|
children: bodyText
|
|
1499
1828
|
}),
|
|
1500
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1501
|
-
className: "mt-2 flex flex-
|
|
1502
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1503
|
-
className: "
|
|
1829
|
+
(due || timestamp) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1830
|
+
className: "mt-2.5 flex flex-wrap items-center gap-1.5",
|
|
1831
|
+
children: [due && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1832
|
+
className: require_src.cn("inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium", DUE_TONE_CLASS[due.tone]),
|
|
1833
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Calendar, {
|
|
1834
|
+
className: "size-3",
|
|
1835
|
+
"aria-hidden": "true"
|
|
1836
|
+
}), due.label]
|
|
1837
|
+
}), timestamp && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1838
|
+
className: "text-muted-foreground/60 text-xs",
|
|
1504
1839
|
children: timestamp
|
|
1505
|
-
}), task.due_at && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1506
|
-
className: "flex items-center gap-1",
|
|
1507
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Calendar, { className: require_src.cn("h-3.5 w-3.5", isCompleted ? "text-muted-foreground" : "text-primary") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1508
|
-
className: require_src.cn("text-xs font-medium", isCompleted ? "text-muted-foreground" : "text-primary"),
|
|
1509
|
-
children: ["Due ", formatDueDate$1(task.due_at)]
|
|
1510
|
-
})]
|
|
1511
1840
|
})]
|
|
1512
1841
|
})
|
|
1513
1842
|
]
|
|
1514
1843
|
})]
|
|
1515
|
-
})
|
|
1844
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1845
|
+
className: "absolute top-3 right-3 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100",
|
|
1846
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuTrigger, {
|
|
1847
|
+
asChild: true,
|
|
1848
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Button, {
|
|
1849
|
+
variant: "ghost",
|
|
1850
|
+
size: "icon-xs",
|
|
1851
|
+
onClick: (e) => e.stopPropagation(),
|
|
1852
|
+
"aria-label": t("task_actions"),
|
|
1853
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.EllipsisVertical, { className: "size-4" })
|
|
1854
|
+
})
|
|
1855
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuContent, {
|
|
1856
|
+
align: "end",
|
|
1857
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuItem, {
|
|
1858
|
+
className: "text-destructive",
|
|
1859
|
+
onClick: (e) => {
|
|
1860
|
+
e.stopPropagation();
|
|
1861
|
+
onDeleteClick(task);
|
|
1862
|
+
},
|
|
1863
|
+
children: t("delete")
|
|
1864
|
+
})
|
|
1865
|
+
})] })
|
|
1866
|
+
})]
|
|
1516
1867
|
});
|
|
1517
1868
|
}
|
|
1518
|
-
function TaskList({ tasks, isLoading, contactId
|
|
1869
|
+
function TaskList({ tasks, isLoading, contactId }) {
|
|
1870
|
+
const { t } = useContactsTranslation();
|
|
1519
1871
|
const [taskToDelete, setTaskToDelete] = (0, react.useState)(null);
|
|
1520
|
-
const [modalOpen, setModalOpen] = (0, react.useState)(false);
|
|
1521
1872
|
const [editingTask, setEditingTask] = (0, react.useState)(null);
|
|
1522
1873
|
const toggleCompletion = useToggleTaskCompletion(contactId);
|
|
1523
1874
|
const deleteTask = useDeleteContactTask(contactId, { onSuccess: () => setTaskToDelete(null) });
|
|
1524
|
-
const createTask = useCreateContactTask(contactId, { onSuccess: () => setModalOpen(false) });
|
|
1525
1875
|
const updateTask = useUpdateContactTask(contactId, { onSuccess: () => setEditingTask(null) });
|
|
1526
|
-
const openCreateModal = () => {
|
|
1527
|
-
setEditingTask(null);
|
|
1528
|
-
setModalOpen(true);
|
|
1529
|
-
};
|
|
1530
|
-
(0, react.useImperativeHandle)(ref, () => openCreateModal);
|
|
1531
1876
|
const handleSave = (data) => {
|
|
1877
|
+
if (!editingTask) return;
|
|
1532
1878
|
const taskBody = data.body ? `${data.title}\n\n${data.body}` : data.title;
|
|
1533
|
-
|
|
1879
|
+
updateTask.mutate({
|
|
1534
1880
|
taskId: editingTask.id,
|
|
1535
1881
|
input: {
|
|
1536
1882
|
body: taskBody,
|
|
1537
1883
|
due_at: data.dueDate ?? null
|
|
1538
1884
|
}
|
|
1539
1885
|
});
|
|
1540
|
-
else createTask.mutate({
|
|
1541
|
-
body: taskBody,
|
|
1542
|
-
due_at: data.dueDate ?? null
|
|
1543
|
-
});
|
|
1544
1886
|
};
|
|
1545
1887
|
const editingParsed = editingTask ? require_parse_task_body.parseTaskBody(editingTask.body ?? "") : null;
|
|
1546
|
-
const isModalOpen = modalOpen || editingTask !== null;
|
|
1547
1888
|
const handleModalOpenChange = (open) => {
|
|
1548
|
-
if (!open)
|
|
1549
|
-
setModalOpen(false);
|
|
1550
|
-
setEditingTask(null);
|
|
1551
|
-
}
|
|
1889
|
+
if (!open) setEditingTask(null);
|
|
1552
1890
|
};
|
|
1553
|
-
if (isLoading) return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1554
|
-
className: "
|
|
1555
|
-
children: [
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
children: [
|
|
1561
|
-
1,
|
|
1562
|
-
2,
|
|
1563
|
-
3
|
|
1564
|
-
].map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-border bg-muted/50 h-32 animate-pulse rounded-xl border" }, i))
|
|
1565
|
-
})]
|
|
1891
|
+
if (isLoading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1892
|
+
className: "flex flex-col gap-3",
|
|
1893
|
+
children: [
|
|
1894
|
+
1,
|
|
1895
|
+
2,
|
|
1896
|
+
3
|
|
1897
|
+
].map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-border/50 bg-muted/50 h-32 animate-pulse rounded-xl border" }, i))
|
|
1566
1898
|
});
|
|
1899
|
+
if (tasks.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
|
|
1567
1900
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1568
1901
|
className: "space-y-4",
|
|
1569
1902
|
children: [
|
|
1570
|
-
|
|
1571
|
-
className: "flex
|
|
1572
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
1573
|
-
className: "text-foreground text-base font-semibold",
|
|
1574
|
-
children: [
|
|
1575
|
-
"Tasks (",
|
|
1576
|
-
tasks.length,
|
|
1577
|
-
")"
|
|
1578
|
-
]
|
|
1579
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1580
|
-
className: "text-muted-foreground mt-0.5 text-sm",
|
|
1581
|
-
children: "Manage tasks and follow-ups for this contact"
|
|
1582
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1583
|
-
type: "button",
|
|
1584
|
-
onClick: openCreateModal,
|
|
1585
|
-
className: "bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
|
|
1586
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "h-3.5 w-3.5" }), "Add Task"]
|
|
1587
|
-
})]
|
|
1588
|
-
}),
|
|
1589
|
-
tasks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState$1, { onCreateTask: openCreateModal }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1590
|
-
className: "grid grid-cols-1 gap-5 sm:grid-cols-2",
|
|
1903
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1904
|
+
className: "flex flex-col gap-3",
|
|
1591
1905
|
children: tasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
|
|
1592
1906
|
task,
|
|
1593
1907
|
toggleCompletion,
|
|
@@ -1596,717 +1910,544 @@ function TaskList({ tasks, isLoading, contactId, hideHeader = false, ref }) {
|
|
|
1596
1910
|
}, task.id))
|
|
1597
1911
|
}),
|
|
1598
1912
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoteTaskModal, {
|
|
1599
|
-
open:
|
|
1913
|
+
open: editingTask !== null,
|
|
1600
1914
|
onOpenChange: handleModalOpenChange,
|
|
1601
|
-
mode:
|
|
1915
|
+
mode: "edit",
|
|
1602
1916
|
type: "task",
|
|
1603
1917
|
initialTitle: editingParsed?.title ?? "",
|
|
1604
1918
|
initialBody: editingParsed?.body ?? "",
|
|
1605
1919
|
initialDueDate: editingTask?.due_at ?? void 0,
|
|
1606
1920
|
showDueDate: true,
|
|
1607
1921
|
onSave: handleSave,
|
|
1608
|
-
isPending:
|
|
1922
|
+
isPending: updateTask.isPending,
|
|
1609
1923
|
isCompleted: editingTask ? !!editingTask.completed_at : void 0,
|
|
1610
1924
|
onToggleComplete: editingTask ? () => toggleCompletion.mutate({
|
|
1611
1925
|
taskId: editingTask.id,
|
|
1612
1926
|
isCompleted: !!editingTask.completed_at
|
|
1613
1927
|
}, { onSuccess: () => setEditingTask(null) }) : void 0,
|
|
1614
1928
|
isTogglePending: toggleCompletion.isPending
|
|
1615
|
-
}, editingTask?.id ?? "
|
|
1929
|
+
}, editingTask?.id ?? "edit"),
|
|
1616
1930
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialog, {
|
|
1617
1931
|
open: taskToDelete !== null,
|
|
1618
1932
|
onOpenChange: (open) => {
|
|
1619
1933
|
if (!open) setTaskToDelete(null);
|
|
1620
1934
|
},
|
|
1621
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogContent, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogTitle, { children: "
|
|
1935
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogContent, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogTitle, { children: t("delete_task_title") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogDescription, { children: t("delete_task_confirm") })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogCancel, { children: t("cancel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogAction, {
|
|
1622
1936
|
variant: "destructive",
|
|
1623
1937
|
onClick: () => {
|
|
1624
1938
|
if (taskToDelete) deleteTask.mutate(taskToDelete.id);
|
|
1625
1939
|
},
|
|
1626
|
-
children: "
|
|
1940
|
+
children: t("delete")
|
|
1627
1941
|
})] })] })
|
|
1628
1942
|
})
|
|
1629
1943
|
]
|
|
1630
1944
|
});
|
|
1631
1945
|
}
|
|
1632
1946
|
//#endregion
|
|
1633
|
-
//#region ../../contacts/ui/src/portal/hooks/
|
|
1634
|
-
function
|
|
1635
|
-
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
1636
|
-
const api = useNotesApi();
|
|
1637
|
-
return (0, _tanstack_react_query.useMutation)({
|
|
1638
|
-
mutationFn: (noteId) => api.deleteNote(noteId, contactId),
|
|
1639
|
-
onSuccess: () => {
|
|
1640
|
-
require_src.fluidToast({
|
|
1641
|
-
title: "Note deleted",
|
|
1642
|
-
type: "success"
|
|
1643
|
-
});
|
|
1644
|
-
queryClient.invalidateQueries({ queryKey: contactsKeys.notes(contactId) });
|
|
1645
|
-
options?.onSuccess?.();
|
|
1646
|
-
},
|
|
1647
|
-
onError: (error) => {
|
|
1648
|
-
require_src.fluidToast({
|
|
1649
|
-
title: "Failed to delete note",
|
|
1650
|
-
type: "error",
|
|
1651
|
-
description: parseApiErrors(error)
|
|
1652
|
-
});
|
|
1653
|
-
}
|
|
1654
|
-
});
|
|
1655
|
-
}
|
|
1656
|
-
//#endregion
|
|
1657
|
-
//#region ../../contacts/ui/src/portal/hooks/notes/use-create-contact-note.ts
|
|
1658
|
-
function useCreateContactNote(contactId, options) {
|
|
1659
|
-
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
1660
|
-
const api = useNotesApi();
|
|
1661
|
-
return (0, _tanstack_react_query.useMutation)({
|
|
1662
|
-
mutationFn: (input) => api.createNote(contactId, input),
|
|
1663
|
-
onSuccess: () => {
|
|
1664
|
-
require_src.fluidToast({
|
|
1665
|
-
title: "Note created",
|
|
1666
|
-
type: "success"
|
|
1667
|
-
});
|
|
1668
|
-
queryClient.invalidateQueries({ queryKey: contactsKeys.notes(contactId) });
|
|
1669
|
-
options?.onSuccess?.();
|
|
1670
|
-
},
|
|
1671
|
-
onError: (error) => {
|
|
1672
|
-
require_src.fluidToast({
|
|
1673
|
-
title: "Failed to create note",
|
|
1674
|
-
type: "error",
|
|
1675
|
-
description: parseApiErrors(error)
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
//#endregion
|
|
1681
|
-
//#region ../../contacts/ui/src/portal/hooks/notes/use-update-contact-note.ts
|
|
1682
|
-
function useUpdateContactNote(contactId, options) {
|
|
1947
|
+
//#region ../../contacts/ui/src/portal/hooks/contacts/use-create-contact-task.ts
|
|
1948
|
+
function useCreateContactTask(contactId, options) {
|
|
1683
1949
|
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
1684
|
-
const api =
|
|
1950
|
+
const api = useTasksApi();
|
|
1685
1951
|
return (0, _tanstack_react_query.useMutation)({
|
|
1686
|
-
mutationFn: (
|
|
1952
|
+
mutationFn: (input) => api.createTask(contactId, input),
|
|
1687
1953
|
onSuccess: () => {
|
|
1688
1954
|
require_src.fluidToast({
|
|
1689
|
-
title: "
|
|
1955
|
+
title: "Task created",
|
|
1690
1956
|
type: "success"
|
|
1691
1957
|
});
|
|
1692
|
-
queryClient.invalidateQueries({ queryKey: contactsKeys.
|
|
1958
|
+
queryClient.invalidateQueries({ queryKey: contactsKeys.tasks(contactId) });
|
|
1693
1959
|
options?.onSuccess?.();
|
|
1694
1960
|
},
|
|
1695
1961
|
onError: (error) => {
|
|
1696
1962
|
require_src.fluidToast({
|
|
1697
|
-
title: "Failed to
|
|
1698
|
-
type: "error",
|
|
1699
|
-
description: parseApiErrors(error)
|
|
1700
|
-
});
|
|
1701
|
-
}
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
//#endregion
|
|
1705
|
-
//#region ../../contacts/ui/src/portal/components/notes/notes-list.tsx
|
|
1706
|
-
function formatDueDate(dateStr) {
|
|
1707
|
-
return new Date(dateStr).toLocaleDateString("en-US", {
|
|
1708
|
-
month: "short",
|
|
1709
|
-
day: "numeric",
|
|
1710
|
-
year: "numeric"
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
function EmptyState({ onCreateNote }) {
|
|
1714
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1715
|
-
className: "flex flex-col items-center justify-center px-4 py-12 text-center",
|
|
1716
|
-
children: [
|
|
1717
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1718
|
-
className: "relative mb-6",
|
|
1719
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1720
|
-
className: "flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-amber-100 to-yellow-100",
|
|
1721
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.StickyNote, { className: "h-9 w-9 text-amber-500" })
|
|
1722
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1723
|
-
className: "border-background absolute -right-2 -bottom-1 flex h-9 w-9 items-center justify-center rounded-full border-2 bg-gradient-to-br from-orange-100 to-red-100 shadow-sm",
|
|
1724
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Pen, { className: "h-4 w-4 text-orange-500" })
|
|
1725
|
-
})]
|
|
1726
|
-
}),
|
|
1727
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
1728
|
-
className: "text-foreground text-base font-semibold",
|
|
1729
|
-
children: "No notes yet"
|
|
1730
|
-
}),
|
|
1731
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1732
|
-
className: "text-muted-foreground mt-1.5 max-w-[260px] text-sm leading-relaxed",
|
|
1733
|
-
children: "Capture key details and insights about this contact to stay on top of every conversation"
|
|
1734
|
-
}),
|
|
1735
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1736
|
-
type: "button",
|
|
1737
|
-
onClick: onCreateNote,
|
|
1738
|
-
className: "bg-primary text-primary-foreground hover:bg-primary/90 mt-4 inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-medium transition-colors",
|
|
1739
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "h-3.5 w-3.5" }), "New Note"]
|
|
1740
|
-
})
|
|
1741
|
-
]
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1744
|
-
function NoteCard({ note, onEdit, onDelete }) {
|
|
1745
|
-
const assetCount = note.assets?.length ?? 0;
|
|
1746
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1747
|
-
role: "button",
|
|
1748
|
-
tabIndex: 0,
|
|
1749
|
-
onClick: () => onEdit(note),
|
|
1750
|
-
onKeyDown: (e) => {
|
|
1751
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
1752
|
-
e.preventDefault();
|
|
1753
|
-
onEdit(note);
|
|
1754
|
-
}
|
|
1755
|
-
},
|
|
1756
|
-
className: "border-border bg-card hover:border-border/80 cursor-pointer rounded-xl border p-5 transition-colors",
|
|
1757
|
-
children: [
|
|
1758
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1759
|
-
className: "flex items-start justify-between gap-2",
|
|
1760
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
1761
|
-
className: "text-foreground line-clamp-1 text-sm font-semibold",
|
|
1762
|
-
children: note.title
|
|
1763
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuTrigger, {
|
|
1764
|
-
asChild: true,
|
|
1765
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1766
|
-
type: "button",
|
|
1767
|
-
onClick: (e) => e.stopPropagation(),
|
|
1768
|
-
className: "text-muted-foreground inline-flex h-6 w-6 items-center justify-center rounded-md hover:opacity-70",
|
|
1769
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.EllipsisVertical, { className: "h-4 w-4" })
|
|
1770
|
-
})
|
|
1771
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuContent, {
|
|
1772
|
-
align: "end",
|
|
1773
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuItem, {
|
|
1774
|
-
className: "text-destructive",
|
|
1775
|
-
onClick: (e) => {
|
|
1776
|
-
e.stopPropagation();
|
|
1777
|
-
onDelete(note);
|
|
1778
|
-
},
|
|
1779
|
-
children: "Delete"
|
|
1780
|
-
})
|
|
1781
|
-
})] })]
|
|
1782
|
-
}),
|
|
1783
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1784
|
-
className: "text-muted-foreground mt-1.5 line-clamp-4 text-sm leading-relaxed",
|
|
1785
|
-
children: note.body
|
|
1786
|
-
}),
|
|
1787
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1788
|
-
className: "mt-3 flex flex-wrap items-center gap-4",
|
|
1789
|
-
children: [
|
|
1790
|
-
note.due_at && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1791
|
-
className: "flex items-center gap-1.5",
|
|
1792
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Calendar, { className: "text-primary h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1793
|
-
className: "text-primary text-xs font-medium",
|
|
1794
|
-
children: ["Due ", formatDueDate(note.due_at)]
|
|
1795
|
-
})]
|
|
1796
|
-
}),
|
|
1797
|
-
!note.due_at && note.due_date && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1798
|
-
className: "flex items-center gap-1.5",
|
|
1799
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Calendar, { className: "text-primary h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1800
|
-
className: "text-primary text-xs font-medium",
|
|
1801
|
-
children: ["Due ", formatDueDate(note.due_date)]
|
|
1802
|
-
})]
|
|
1803
|
-
}),
|
|
1804
|
-
assetCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1805
|
-
className: "flex items-center gap-1.5",
|
|
1806
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Paperclip, { className: "text-muted-foreground h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1807
|
-
className: "text-muted-foreground text-xs",
|
|
1808
|
-
children: [
|
|
1809
|
-
assetCount,
|
|
1810
|
-
" ",
|
|
1811
|
-
assetCount === 1 ? "Attachment" : "Attachments"
|
|
1812
|
-
]
|
|
1813
|
-
})]
|
|
1814
|
-
})
|
|
1815
|
-
]
|
|
1816
|
-
})
|
|
1817
|
-
]
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1820
|
-
function NotesList({ notes, isLoading, contactId, ref }) {
|
|
1821
|
-
const [noteToDelete, setNoteToDelete] = (0, react.useState)(null);
|
|
1822
|
-
const [modalOpen, setModalOpen] = (0, react.useState)(false);
|
|
1823
|
-
const [editingNote, setEditingNote] = (0, react.useState)(null);
|
|
1824
|
-
const deleteNote = useDeleteContactNote(contactId);
|
|
1825
|
-
const createNote = useCreateContactNote(contactId, { onSuccess: () => setModalOpen(false) });
|
|
1826
|
-
const updateNote = useUpdateContactNote(contactId, { onSuccess: () => setEditingNote(null) });
|
|
1827
|
-
const openCreateModal = () => {
|
|
1828
|
-
setEditingNote(null);
|
|
1829
|
-
setModalOpen(true);
|
|
1830
|
-
};
|
|
1831
|
-
(0, react.useImperativeHandle)(ref, () => openCreateModal);
|
|
1832
|
-
const handleSave = (data) => {
|
|
1833
|
-
if (editingNote) updateNote.mutate({
|
|
1834
|
-
noteId: editingNote.id,
|
|
1835
|
-
input: {
|
|
1836
|
-
title: data.title,
|
|
1837
|
-
body: data.body
|
|
1838
|
-
}
|
|
1839
|
-
});
|
|
1840
|
-
else createNote.mutate({
|
|
1841
|
-
title: data.title,
|
|
1842
|
-
body: data.body
|
|
1843
|
-
});
|
|
1844
|
-
};
|
|
1845
|
-
const isModalOpen = modalOpen || editingNote !== null;
|
|
1846
|
-
const handleModalOpenChange = (open) => {
|
|
1847
|
-
if (!open) {
|
|
1848
|
-
setModalOpen(false);
|
|
1849
|
-
setEditingNote(null);
|
|
1850
|
-
}
|
|
1851
|
-
};
|
|
1852
|
-
if (isLoading) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1853
|
-
className: "space-y-4",
|
|
1854
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1855
|
-
className: "flex items-center justify-between",
|
|
1856
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "bg-muted h-5 w-24 animate-pulse rounded" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "bg-muted mt-1 h-4 w-48 animate-pulse rounded" })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "bg-muted h-8 w-24 animate-pulse rounded-lg" })]
|
|
1857
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1858
|
-
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
1859
|
-
children: [
|
|
1860
|
-
1,
|
|
1861
|
-
2,
|
|
1862
|
-
3
|
|
1863
|
-
].map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-border bg-muted/50 h-40 animate-pulse rounded-xl border" }, i))
|
|
1864
|
-
})]
|
|
1865
|
-
});
|
|
1866
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1867
|
-
className: "space-y-4",
|
|
1868
|
-
children: [
|
|
1869
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1870
|
-
className: "flex items-start justify-between",
|
|
1871
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
1872
|
-
className: "text-foreground text-base font-semibold",
|
|
1873
|
-
children: [
|
|
1874
|
-
"Notes (",
|
|
1875
|
-
notes.length,
|
|
1876
|
-
")"
|
|
1877
|
-
]
|
|
1878
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1879
|
-
className: "text-muted-foreground mt-0.5 text-sm",
|
|
1880
|
-
children: "Capture key details and insights about this contact"
|
|
1881
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1882
|
-
type: "button",
|
|
1883
|
-
onClick: openCreateModal,
|
|
1884
|
-
className: "bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
|
|
1885
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "h-3.5 w-3.5" }), "Add Note"]
|
|
1886
|
-
})]
|
|
1887
|
-
}),
|
|
1888
|
-
notes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, { onCreateNote: openCreateModal }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1889
|
-
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
1890
|
-
children: notes.map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoteCard, {
|
|
1891
|
-
note,
|
|
1892
|
-
onEdit: setEditingNote,
|
|
1893
|
-
onDelete: setNoteToDelete
|
|
1894
|
-
}, note.id))
|
|
1895
|
-
}),
|
|
1896
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoteTaskModal, {
|
|
1897
|
-
open: isModalOpen,
|
|
1898
|
-
onOpenChange: handleModalOpenChange,
|
|
1899
|
-
mode: editingNote ? "edit" : "create",
|
|
1900
|
-
type: "note",
|
|
1901
|
-
initialTitle: editingNote?.title ?? "",
|
|
1902
|
-
initialBody: editingNote?.body ?? "",
|
|
1903
|
-
onSave: handleSave,
|
|
1904
|
-
isPending: createNote.isPending || updateNote.isPending
|
|
1905
|
-
}, editingNote?.id ?? "create"),
|
|
1906
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialog, {
|
|
1907
|
-
open: !!noteToDelete,
|
|
1908
|
-
onOpenChange: (open) => {
|
|
1909
|
-
if (!open) setNoteToDelete(null);
|
|
1910
|
-
},
|
|
1911
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogContent, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogTitle, { children: "Delete Note" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogDescription, { children: [
|
|
1912
|
-
"Are you sure you want to delete \"",
|
|
1913
|
-
noteToDelete?.title,
|
|
1914
|
-
"\"? This action cannot be undone."
|
|
1915
|
-
] })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogCancel, { children: "Cancel" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogAction, {
|
|
1916
|
-
variant: "destructive",
|
|
1917
|
-
onClick: () => {
|
|
1918
|
-
if (noteToDelete) deleteNote.mutate(noteToDelete.id);
|
|
1919
|
-
},
|
|
1920
|
-
children: "Delete"
|
|
1921
|
-
})] })] })
|
|
1922
|
-
})
|
|
1923
|
-
]
|
|
1963
|
+
title: "Failed to create task",
|
|
1964
|
+
type: "error",
|
|
1965
|
+
description: parseApiErrors(error)
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1924
1968
|
});
|
|
1925
1969
|
}
|
|
1926
1970
|
//#endregion
|
|
1927
|
-
//#region ../../contacts/ui/src/
|
|
1928
|
-
const
|
|
1929
|
-
const statusOptions = [
|
|
1930
|
-
{
|
|
1931
|
-
name: "New",
|
|
1932
|
-
value: "new"
|
|
1933
|
-
},
|
|
1934
|
-
{
|
|
1935
|
-
name: "Active",
|
|
1936
|
-
value: "active"
|
|
1937
|
-
},
|
|
1938
|
-
{
|
|
1939
|
-
name: "Inactive",
|
|
1940
|
-
value: "inactive"
|
|
1941
|
-
},
|
|
1971
|
+
//#region ../../contacts/ui/src/portal/components/tasks/inline-task-composer.tsx
|
|
1972
|
+
const QUICK_DATES = [
|
|
1942
1973
|
{
|
|
1943
|
-
|
|
1944
|
-
|
|
1974
|
+
key: "today",
|
|
1975
|
+
offsetDays: 0
|
|
1945
1976
|
},
|
|
1946
1977
|
{
|
|
1947
|
-
|
|
1948
|
-
|
|
1978
|
+
key: "tomorrow",
|
|
1979
|
+
offsetDays: 1
|
|
1949
1980
|
},
|
|
1950
1981
|
{
|
|
1951
|
-
|
|
1952
|
-
|
|
1982
|
+
key: "next_week",
|
|
1983
|
+
offsetDays: 7
|
|
1953
1984
|
}
|
|
1954
1985
|
];
|
|
1955
|
-
|
|
1956
|
-
const {
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
const
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
}, [
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
className: "ring-input"
|
|
2056
|
-
}) }),
|
|
2057
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
2058
|
-
] })
|
|
2059
|
-
}),
|
|
2060
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
2061
|
-
control,
|
|
2062
|
-
name: "status",
|
|
2063
|
-
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
2064
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
2065
|
-
className: "font-inter text-foreground font-medium",
|
|
2066
|
-
children: "Status"
|
|
2067
|
-
}),
|
|
2068
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.Select, {
|
|
2069
|
-
value: field.value ?? "",
|
|
2070
|
-
onValueChange: field.onChange,
|
|
2071
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectTrigger, {
|
|
2072
|
-
className: "w-full",
|
|
2073
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectValue, { placeholder: "Select status" })
|
|
2074
|
-
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectContent, {
|
|
2075
|
-
position: "popper",
|
|
2076
|
-
sideOffset: 4,
|
|
2077
|
-
children: effectiveStatusOptions.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectItem, {
|
|
2078
|
-
value: opt.value,
|
|
2079
|
-
children: opt.name
|
|
2080
|
-
}, opt.value))
|
|
2081
|
-
})]
|
|
2082
|
-
}),
|
|
2083
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
2084
|
-
] })
|
|
2085
|
-
}),
|
|
2086
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
2087
|
-
control,
|
|
2088
|
-
name: "address",
|
|
2089
|
-
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, {
|
|
2090
|
-
className: "lg:col-span-2",
|
|
2091
|
-
children: [
|
|
2092
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
2093
|
-
className: "font-inter text-foreground font-medium",
|
|
2094
|
-
children: "Full Address"
|
|
2095
|
-
}),
|
|
2096
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
2097
|
-
placeholder: "Enter street address",
|
|
2098
|
-
...field,
|
|
2099
|
-
value: field.value ?? "",
|
|
2100
|
-
className: "ring-input"
|
|
2101
|
-
}) }),
|
|
2102
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
2103
|
-
]
|
|
2104
|
-
})
|
|
2105
|
-
}),
|
|
2106
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
2107
|
-
control,
|
|
2108
|
-
name: "city",
|
|
2109
|
-
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
2110
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
2111
|
-
className: "font-inter text-foreground font-medium",
|
|
2112
|
-
children: "City"
|
|
2113
|
-
}),
|
|
2114
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
2115
|
-
placeholder: "Enter city",
|
|
2116
|
-
...field,
|
|
2117
|
-
value: field.value ?? "",
|
|
2118
|
-
className: "ring-input"
|
|
2119
|
-
}) }),
|
|
2120
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
2121
|
-
] })
|
|
2122
|
-
}),
|
|
2123
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
2124
|
-
control,
|
|
2125
|
-
name: "state",
|
|
2126
|
-
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
2127
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
2128
|
-
className: "font-inter text-foreground font-medium",
|
|
2129
|
-
children: "State/Province"
|
|
2130
|
-
}),
|
|
2131
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
2132
|
-
placeholder: "Enter state or province",
|
|
2133
|
-
...field,
|
|
2134
|
-
value: field.value ?? "",
|
|
2135
|
-
className: "ring-input"
|
|
2136
|
-
}) }),
|
|
2137
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
2138
|
-
] })
|
|
2139
|
-
}),
|
|
2140
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
2141
|
-
control,
|
|
2142
|
-
name: "postal_code",
|
|
2143
|
-
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
2144
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
2145
|
-
className: "font-inter text-foreground font-medium",
|
|
2146
|
-
children: "Postal Code"
|
|
2147
|
-
}),
|
|
2148
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Input, {
|
|
2149
|
-
placeholder: "Enter postal code",
|
|
2150
|
-
...field,
|
|
2151
|
-
value: field.value ?? "",
|
|
2152
|
-
className: "ring-input"
|
|
2153
|
-
}) }),
|
|
2154
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
2155
|
-
] })
|
|
2156
|
-
}),
|
|
2157
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormField, {
|
|
2158
|
-
control,
|
|
2159
|
-
name: "country_code",
|
|
2160
|
-
render: ({ field }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.FormItem, { children: [
|
|
2161
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormLabel, {
|
|
2162
|
-
className: "font-inter text-foreground font-medium",
|
|
2163
|
-
children: "Country"
|
|
2164
|
-
}),
|
|
2165
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.Select, {
|
|
2166
|
-
value: field.value ?? "",
|
|
2167
|
-
onValueChange: field.onChange,
|
|
2168
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormControl, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectTrigger, {
|
|
2169
|
-
className: "w-full",
|
|
2170
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectValue, { placeholder: "Select country" })
|
|
2171
|
-
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectContent, {
|
|
2172
|
-
position: "popper",
|
|
2173
|
-
sideOffset: 4,
|
|
2174
|
-
children: countries.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.SelectItem, {
|
|
2175
|
-
value: opt.value,
|
|
2176
|
-
children: opt.name
|
|
2177
|
-
}, opt.value))
|
|
2178
|
-
})]
|
|
2179
|
-
}),
|
|
2180
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.FormMessage, {})
|
|
2181
|
-
] })
|
|
1986
|
+
function InlineTaskComposer({ contactId }) {
|
|
1987
|
+
const { t } = useContactsTranslation();
|
|
1988
|
+
const [isOpen, setIsOpen] = (0, react.useState)(false);
|
|
1989
|
+
const [body, setBody] = (0, react.useState)("");
|
|
1990
|
+
const [dueDate, setDueDate] = (0, react.useState)(null);
|
|
1991
|
+
const inputRef = (0, react.useRef)(null);
|
|
1992
|
+
const createTask = useCreateContactTask(contactId, { onSuccess: () => {
|
|
1993
|
+
setBody("");
|
|
1994
|
+
setDueDate(null);
|
|
1995
|
+
setIsOpen(false);
|
|
1996
|
+
} });
|
|
1997
|
+
(0, react.useEffect)(() => {
|
|
1998
|
+
if (isOpen) inputRef.current?.focus();
|
|
1999
|
+
}, [isOpen]);
|
|
2000
|
+
const quickDates = (0, react.useMemo)(() => QUICK_DATES.map((q) => ({
|
|
2001
|
+
key: q.key,
|
|
2002
|
+
iso: isoDate(q.offsetDays)
|
|
2003
|
+
})), []);
|
|
2004
|
+
const quickLabels = {
|
|
2005
|
+
today: t("quick_today"),
|
|
2006
|
+
tomorrow: t("quick_tomorrow"),
|
|
2007
|
+
next_week: t("quick_next_week")
|
|
2008
|
+
};
|
|
2009
|
+
const close = () => {
|
|
2010
|
+
setIsOpen(false);
|
|
2011
|
+
setBody("");
|
|
2012
|
+
setDueDate(null);
|
|
2013
|
+
};
|
|
2014
|
+
const submit = () => {
|
|
2015
|
+
const trimmed = body.trim();
|
|
2016
|
+
if (!trimmed || createTask.isPending) return;
|
|
2017
|
+
createTask.mutate({
|
|
2018
|
+
body: trimmed,
|
|
2019
|
+
due_at: dueDate
|
|
2020
|
+
});
|
|
2021
|
+
};
|
|
2022
|
+
const canSubmit = body.trim().length > 0 && !createTask.isPending;
|
|
2023
|
+
if (!isOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2024
|
+
type: "button",
|
|
2025
|
+
onClick: () => setIsOpen(true),
|
|
2026
|
+
className: "border-border/50 text-muted-foreground hover:border-foreground/30 hover:bg-muted/40 hover:text-foreground flex w-full items-center gap-3 rounded-xl border border-dashed px-5 py-4 text-sm font-medium transition-colors",
|
|
2027
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, {
|
|
2028
|
+
className: "size-4",
|
|
2029
|
+
"aria-hidden": "true"
|
|
2030
|
+
}), t("new_task")]
|
|
2031
|
+
});
|
|
2032
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2033
|
+
className: "border-border/50 focus-within:border-foreground/30 rounded-xl border p-3 transition-colors",
|
|
2034
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2035
|
+
className: "flex items-start gap-3",
|
|
2036
|
+
children: [
|
|
2037
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2038
|
+
className: "border-muted-foreground/50 mt-1.5 size-5 shrink-0 rounded-full border-2",
|
|
2039
|
+
"aria-hidden": "true"
|
|
2040
|
+
}),
|
|
2041
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2042
|
+
ref: inputRef,
|
|
2043
|
+
value: body,
|
|
2044
|
+
onChange: (e) => setBody(e.target.value),
|
|
2045
|
+
onKeyDown: (e) => {
|
|
2046
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
2047
|
+
e.preventDefault();
|
|
2048
|
+
submit();
|
|
2049
|
+
} else if (e.key === "Escape") {
|
|
2050
|
+
e.preventDefault();
|
|
2051
|
+
close();
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
placeholder: t("task_describe_placeholder"),
|
|
2055
|
+
"aria-label": t("task_description_aria"),
|
|
2056
|
+
className: "placeholder:text-muted-foreground/80 text-foreground flex-1 border-0 bg-transparent text-sm font-medium outline-none"
|
|
2057
|
+
}),
|
|
2058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2059
|
+
type: "button",
|
|
2060
|
+
onClick: close,
|
|
2061
|
+
"aria-label": t("task_discard_aria"),
|
|
2062
|
+
className: "text-muted-foreground hover:bg-muted hover:text-foreground -mt-0.5 -mr-0.5 flex size-7 shrink-0 items-center justify-center rounded-md transition-colors",
|
|
2063
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "size-4" })
|
|
2064
|
+
})
|
|
2065
|
+
]
|
|
2066
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2067
|
+
className: "mt-3 flex items-center gap-1.5 pl-8",
|
|
2068
|
+
children: [
|
|
2069
|
+
quickDates.map((q) => {
|
|
2070
|
+
const isActive = dueDate === q.iso;
|
|
2071
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2072
|
+
type: "button",
|
|
2073
|
+
onClick: () => setDueDate(isActive ? null : q.iso),
|
|
2074
|
+
"aria-pressed": isActive,
|
|
2075
|
+
className: require_src.cn("shrink-0 rounded-full px-3 py-1 text-xs font-medium transition-colors", isActive ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-muted/70"),
|
|
2076
|
+
children: quickLabels[q.key]
|
|
2077
|
+
}, q.key);
|
|
2078
|
+
}),
|
|
2079
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "ml-auto" }),
|
|
2080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2081
|
+
type: "button",
|
|
2082
|
+
onClick: submit,
|
|
2083
|
+
disabled: !canSubmit,
|
|
2084
|
+
className: "bg-primary text-primary-foreground hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground inline-flex shrink-0 items-center gap-1.5 rounded-full px-4 py-1.5 text-xs font-semibold transition-colors disabled:cursor-not-allowed",
|
|
2085
|
+
children: createTask.isPending ? t("adding") : t("add_task")
|
|
2182
2086
|
})
|
|
2183
2087
|
]
|
|
2184
2088
|
})]
|
|
2185
2089
|
});
|
|
2186
|
-
}
|
|
2090
|
+
}
|
|
2091
|
+
//#endregion
|
|
2092
|
+
//#region ../../contacts/ui/src/portal/hooks/notes/use-delete-contact-note.ts
|
|
2093
|
+
function useDeleteContactNote(contactId, options) {
|
|
2094
|
+
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
2095
|
+
const api = useNotesApi();
|
|
2096
|
+
return (0, _tanstack_react_query.useMutation)({
|
|
2097
|
+
mutationFn: (noteId) => api.deleteNote(noteId, contactId),
|
|
2098
|
+
onSuccess: () => {
|
|
2099
|
+
require_src.fluidToast({
|
|
2100
|
+
title: "Note deleted",
|
|
2101
|
+
type: "success"
|
|
2102
|
+
});
|
|
2103
|
+
queryClient.invalidateQueries({ queryKey: contactsKeys.notes(contactId) });
|
|
2104
|
+
options?.onSuccess?.();
|
|
2105
|
+
},
|
|
2106
|
+
onError: (error) => {
|
|
2107
|
+
require_src.fluidToast({
|
|
2108
|
+
title: "Failed to delete note",
|
|
2109
|
+
type: "error",
|
|
2110
|
+
description: parseApiErrors(error)
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
//#endregion
|
|
2116
|
+
//#region ../../contacts/ui/src/portal/hooks/notes/use-update-contact-note.ts
|
|
2117
|
+
function useUpdateContactNote(contactId, options) {
|
|
2118
|
+
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
2119
|
+
const api = useNotesApi();
|
|
2120
|
+
return (0, _tanstack_react_query.useMutation)({
|
|
2121
|
+
mutationFn: ({ noteId, input }) => api.updateNote(noteId, contactId, input),
|
|
2122
|
+
onSuccess: () => {
|
|
2123
|
+
require_src.fluidToast({
|
|
2124
|
+
title: "Note updated",
|
|
2125
|
+
type: "success"
|
|
2126
|
+
});
|
|
2127
|
+
queryClient.invalidateQueries({ queryKey: contactsKeys.notes(contactId) });
|
|
2128
|
+
options?.onSuccess?.();
|
|
2129
|
+
},
|
|
2130
|
+
onError: (error) => {
|
|
2131
|
+
require_src.fluidToast({
|
|
2132
|
+
title: "Failed to update note",
|
|
2133
|
+
type: "error",
|
|
2134
|
+
description: parseApiErrors(error)
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
//#endregion
|
|
2140
|
+
//#region ../../contacts/ui/src/portal/hooks/notes/use-create-contact-note.ts
|
|
2141
|
+
function useCreateContactNote(contactId, options) {
|
|
2142
|
+
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
2143
|
+
const api = useNotesApi();
|
|
2144
|
+
return (0, _tanstack_react_query.useMutation)({
|
|
2145
|
+
mutationFn: (input) => api.createNote(contactId, input),
|
|
2146
|
+
onSuccess: () => {
|
|
2147
|
+
require_src.fluidToast({
|
|
2148
|
+
title: "Note created",
|
|
2149
|
+
type: "success"
|
|
2150
|
+
});
|
|
2151
|
+
queryClient.invalidateQueries({ queryKey: contactsKeys.notes(contactId) });
|
|
2152
|
+
options?.onSuccess?.();
|
|
2153
|
+
},
|
|
2154
|
+
onError: (error) => {
|
|
2155
|
+
require_src.fluidToast({
|
|
2156
|
+
title: "Failed to create note",
|
|
2157
|
+
type: "error",
|
|
2158
|
+
description: parseApiErrors(error)
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
//#endregion
|
|
2164
|
+
//#region ../../contacts/ui/src/portal/components/notes/inline-note-composer.tsx
|
|
2165
|
+
function detectMacPlatform() {
|
|
2166
|
+
if (typeof navigator === "undefined") return false;
|
|
2167
|
+
const platform = navigator.userAgentData?.platform ?? navigator.userAgent ?? "";
|
|
2168
|
+
return /Mac|iPhone|iPad/i.test(platform);
|
|
2169
|
+
}
|
|
2170
|
+
function InlineNoteComposer({ contactId }) {
|
|
2171
|
+
const { t } = useContactsTranslation();
|
|
2172
|
+
const [body, setBody] = (0, react.useState)("");
|
|
2173
|
+
const [isFocused, setIsFocused] = (0, react.useState)(false);
|
|
2174
|
+
const [isMac, setIsMac] = (0, react.useState)(false);
|
|
2175
|
+
const textareaRef = (0, react.useRef)(null);
|
|
2176
|
+
(0, react.useEffect)(() => {
|
|
2177
|
+
setIsMac(detectMacPlatform());
|
|
2178
|
+
}, []);
|
|
2179
|
+
const createNote = useCreateContactNote(contactId, { onSuccess: () => {
|
|
2180
|
+
setBody("");
|
|
2181
|
+
textareaRef.current?.blur();
|
|
2182
|
+
} });
|
|
2183
|
+
const submit = () => {
|
|
2184
|
+
const trimmed = body.trim();
|
|
2185
|
+
if (!trimmed || createNote.isPending) return;
|
|
2186
|
+
const firstLine = trimmed.split("\n")[0]?.slice(0, 120) ?? "Note";
|
|
2187
|
+
createNote.mutate({
|
|
2188
|
+
title: firstLine,
|
|
2189
|
+
body: trimmed
|
|
2190
|
+
});
|
|
2191
|
+
};
|
|
2192
|
+
const canSubmit = body.trim().length > 0 && !createNote.isPending;
|
|
2193
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2194
|
+
className: require_src.cn("border-border/50 focus-within:border-foreground/30 rounded-2xl border p-4 transition-colors", isFocused && "border-foreground/30"),
|
|
2195
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
2196
|
+
ref: textareaRef,
|
|
2197
|
+
value: body,
|
|
2198
|
+
onChange: (e) => setBody(e.target.value),
|
|
2199
|
+
onFocus: () => setIsFocused(true),
|
|
2200
|
+
onBlur: () => setIsFocused(false),
|
|
2201
|
+
onKeyDown: (e) => {
|
|
2202
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
2203
|
+
e.preventDefault();
|
|
2204
|
+
submit();
|
|
2205
|
+
}
|
|
2206
|
+
},
|
|
2207
|
+
placeholder: t("note_placeholder"),
|
|
2208
|
+
"aria-label": t("note_body_aria"),
|
|
2209
|
+
rows: 3,
|
|
2210
|
+
className: "placeholder:text-muted-foreground text-foreground min-h-[80px] w-full resize-none border-0 bg-transparent text-sm leading-relaxed outline-none"
|
|
2211
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2212
|
+
className: "mt-3 flex items-center justify-between gap-3",
|
|
2213
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2214
|
+
className: "text-muted-foreground text-xs",
|
|
2215
|
+
children: isMac ? t("tip_save_shortcut_mac") : t("tip_save_shortcut_other")
|
|
2216
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2217
|
+
type: "button",
|
|
2218
|
+
onClick: submit,
|
|
2219
|
+
disabled: !canSubmit,
|
|
2220
|
+
className: "bg-primary text-primary-foreground hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground inline-flex shrink-0 items-center gap-1.5 rounded-full px-4 py-1.5 text-xs font-semibold transition-colors disabled:cursor-not-allowed",
|
|
2221
|
+
children: createNote.isPending ? t("adding") : t("add_note")
|
|
2222
|
+
})]
|
|
2223
|
+
})]
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
//#endregion
|
|
2227
|
+
//#region ../../contacts/ui/src/portal/components/notes/notes-list.tsx
|
|
2228
|
+
function formatDueDate(dateStr) {
|
|
2229
|
+
return new Date(dateStr).toLocaleDateString("en-US", {
|
|
2230
|
+
month: "short",
|
|
2231
|
+
day: "numeric",
|
|
2232
|
+
year: "numeric"
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
function formatRelativeNote(dateStr, t) {
|
|
2236
|
+
const date = new Date(dateStr);
|
|
2237
|
+
const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
|
|
2238
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
2239
|
+
if (diffDays === 0) return date.toLocaleTimeString("en-US", {
|
|
2240
|
+
hour: "numeric",
|
|
2241
|
+
minute: "2-digit"
|
|
2242
|
+
});
|
|
2243
|
+
if (diffDays === 1) return t("task_yesterday");
|
|
2244
|
+
if (diffDays < 7) return t("task_days_ago_other", { count: diffDays });
|
|
2245
|
+
return formatDueDate(dateStr);
|
|
2246
|
+
}
|
|
2247
|
+
function NoteCard({ note, onEdit, onDelete }) {
|
|
2248
|
+
const { t } = useContactsTranslation();
|
|
2249
|
+
const assetCount = note.assets?.length ?? 0;
|
|
2250
|
+
const showTitle = !!note.title?.trim() && note.title.trim() !== note.body?.trim();
|
|
2251
|
+
const dueDateValue = note.due_at ?? note.due_date ?? null;
|
|
2252
|
+
const createdLabel = note.created_at ? formatRelativeNote(note.created_at, t) : null;
|
|
2253
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2254
|
+
role: "button",
|
|
2255
|
+
tabIndex: 0,
|
|
2256
|
+
onClick: () => onEdit(note),
|
|
2257
|
+
onKeyDown: (e) => {
|
|
2258
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2259
|
+
e.preventDefault();
|
|
2260
|
+
onEdit(note);
|
|
2261
|
+
}
|
|
2262
|
+
},
|
|
2263
|
+
className: "group border-border/50 hover:border-foreground/20 hover:bg-muted/40 relative cursor-pointer rounded-xl border p-4 transition-all hover:shadow-sm",
|
|
2264
|
+
children: [
|
|
2265
|
+
showTitle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
2266
|
+
className: "text-foreground line-clamp-1 text-sm font-semibold",
|
|
2267
|
+
children: note.title
|
|
2268
|
+
}),
|
|
2269
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2270
|
+
className: require_src.cn("text-foreground line-clamp-4 text-sm leading-relaxed whitespace-pre-wrap", showTitle && "text-muted-foreground mt-1"),
|
|
2271
|
+
children: note.body
|
|
2272
|
+
}),
|
|
2273
|
+
(createdLabel || dueDateValue || assetCount > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2274
|
+
className: "mt-2.5 flex flex-wrap items-center gap-3",
|
|
2275
|
+
children: [
|
|
2276
|
+
createdLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2277
|
+
className: "text-muted-foreground/70 text-xs",
|
|
2278
|
+
children: createdLabel
|
|
2279
|
+
}),
|
|
2280
|
+
dueDateValue && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2281
|
+
className: "bg-primary/10 text-primary inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
|
|
2282
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Calendar, {
|
|
2283
|
+
className: "size-3",
|
|
2284
|
+
"aria-hidden": "true"
|
|
2285
|
+
}), t("task_due", { date: formatDueDate(dueDateValue) })]
|
|
2286
|
+
}),
|
|
2287
|
+
assetCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2288
|
+
className: "text-muted-foreground inline-flex items-center gap-1 text-xs",
|
|
2289
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Paperclip, {
|
|
2290
|
+
className: "size-3",
|
|
2291
|
+
"aria-hidden": "true"
|
|
2292
|
+
}), assetCount === 1 ? t("attachment_one") : t("attachment_other", { count: assetCount })]
|
|
2293
|
+
})
|
|
2294
|
+
]
|
|
2295
|
+
}),
|
|
2296
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2297
|
+
className: "absolute top-3 right-3 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100",
|
|
2298
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuTrigger, {
|
|
2299
|
+
asChild: true,
|
|
2300
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Button, {
|
|
2301
|
+
variant: "ghost",
|
|
2302
|
+
size: "icon-xs",
|
|
2303
|
+
onClick: (e) => e.stopPropagation(),
|
|
2304
|
+
"aria-label": t("note_actions"),
|
|
2305
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.EllipsisVertical, { className: "size-4" })
|
|
2306
|
+
})
|
|
2307
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuContent, {
|
|
2308
|
+
align: "end",
|
|
2309
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DropdownMenuItem, {
|
|
2310
|
+
className: "text-destructive",
|
|
2311
|
+
onClick: (e) => {
|
|
2312
|
+
e.stopPropagation();
|
|
2313
|
+
onDelete(note);
|
|
2314
|
+
},
|
|
2315
|
+
children: t("delete")
|
|
2316
|
+
})
|
|
2317
|
+
})] })
|
|
2318
|
+
})
|
|
2319
|
+
]
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
function NotesList({ notes, isLoading, contactId }) {
|
|
2323
|
+
const { t } = useContactsTranslation();
|
|
2324
|
+
const [noteToDelete, setNoteToDelete] = (0, react.useState)(null);
|
|
2325
|
+
const [editingNote, setEditingNote] = (0, react.useState)(null);
|
|
2326
|
+
const deleteNote = useDeleteContactNote(contactId, { onSuccess: () => setNoteToDelete(null) });
|
|
2327
|
+
const updateNote = useUpdateContactNote(contactId, { onSuccess: () => setEditingNote(null) });
|
|
2328
|
+
const handleEditSave = (data) => {
|
|
2329
|
+
if (!editingNote) return;
|
|
2330
|
+
updateNote.mutate({
|
|
2331
|
+
noteId: editingNote.id,
|
|
2332
|
+
input: {
|
|
2333
|
+
title: data.title,
|
|
2334
|
+
body: data.body
|
|
2335
|
+
}
|
|
2336
|
+
});
|
|
2337
|
+
};
|
|
2338
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2339
|
+
className: "space-y-4",
|
|
2340
|
+
children: [
|
|
2341
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineNoteComposer, { contactId }),
|
|
2342
|
+
isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2343
|
+
className: "flex flex-col gap-3",
|
|
2344
|
+
children: [
|
|
2345
|
+
1,
|
|
2346
|
+
2,
|
|
2347
|
+
3
|
|
2348
|
+
].map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-border/50 bg-muted/50 h-20 animate-pulse rounded-xl border" }, i))
|
|
2349
|
+
}) : notes.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2350
|
+
className: "flex flex-col gap-3",
|
|
2351
|
+
children: notes.map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoteCard, {
|
|
2352
|
+
note,
|
|
2353
|
+
onEdit: setEditingNote,
|
|
2354
|
+
onDelete: setNoteToDelete
|
|
2355
|
+
}, note.id))
|
|
2356
|
+
}),
|
|
2357
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoteTaskModal, {
|
|
2358
|
+
open: editingNote !== null,
|
|
2359
|
+
onOpenChange: (open) => {
|
|
2360
|
+
if (!open) setEditingNote(null);
|
|
2361
|
+
},
|
|
2362
|
+
mode: "edit",
|
|
2363
|
+
type: "note",
|
|
2364
|
+
initialTitle: editingNote?.title ?? "",
|
|
2365
|
+
initialBody: editingNote?.body ?? "",
|
|
2366
|
+
onSave: handleEditSave,
|
|
2367
|
+
isPending: updateNote.isPending
|
|
2368
|
+
}, editingNote?.id ?? "edit"),
|
|
2369
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialog, {
|
|
2370
|
+
open: !!noteToDelete,
|
|
2371
|
+
onOpenChange: (open) => {
|
|
2372
|
+
if (!open) setNoteToDelete(null);
|
|
2373
|
+
},
|
|
2374
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogContent, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogTitle, { children: t("delete_note_title") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogDescription, { children: t("delete_note_irreversible") })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.AlertDialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogCancel, { children: t("cancel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialogAction, {
|
|
2375
|
+
variant: "destructive",
|
|
2376
|
+
onClick: () => {
|
|
2377
|
+
if (noteToDelete) deleteNote.mutate(noteToDelete.id);
|
|
2378
|
+
},
|
|
2379
|
+
children: t("delete")
|
|
2380
|
+
})] })] })
|
|
2381
|
+
})
|
|
2382
|
+
]
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2187
2385
|
//#endregion
|
|
2188
2386
|
//#region ../../contacts/ui/src/portal/components/contacts/rep-layout/ContactDetailTabs.tsx
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2191
|
-
"Notes",
|
|
2192
|
-
"Profile"
|
|
2193
|
-
].filter((t) => t !== "Profile");
|
|
2194
|
-
function ContactDetailTabs({ contactId, contactName, countryOptions, isDirty, isSubmitting, onSave, tasks, isLoadingTasks, notes, isLoadingNotes }) {
|
|
2387
|
+
const VISIBLE_TABS = ["Tasks", "Notes"];
|
|
2388
|
+
function ContactDetailTabs({ contactId, tasks, isLoadingTasks, notes, isLoadingNotes, activeTab, onTabChange }) {
|
|
2195
2389
|
const { t } = useContactsTranslation();
|
|
2196
|
-
const [activeTab, setActiveTab] = (0, react.useState)("Tasks");
|
|
2197
2390
|
const tabLabels = {
|
|
2198
2391
|
Tasks: t("tab_tasks"),
|
|
2199
|
-
Notes: t("tab_notes")
|
|
2200
|
-
Profile: t("edit_profile")
|
|
2392
|
+
Notes: t("tab_notes")
|
|
2201
2393
|
};
|
|
2202
|
-
const openTaskModalRef = (0, react.useRef)(null);
|
|
2203
2394
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2204
2395
|
className: "mt-8",
|
|
2205
|
-
children: [
|
|
2206
|
-
className: "border-border flex
|
|
2207
|
-
children:
|
|
2208
|
-
className: "flex min-w-0 items-center gap-1",
|
|
2209
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2210
|
-
type: "button",
|
|
2211
|
-
onClick: () => setActiveTab("Tasks"),
|
|
2212
|
-
className: "text-muted-foreground hover:bg-muted hover:text-foreground hidden size-9 shrink-0 items-center justify-center rounded-md transition-colors sm:flex",
|
|
2213
|
-
"aria-label": t("back_to_tasks"),
|
|
2214
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ArrowLeft, { className: "size-4" })
|
|
2215
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2216
|
-
className: "border-foreground text-foreground -mb-px border-b-2 py-3 text-sm font-medium",
|
|
2217
|
-
children: t("editing_name", { name: contactName })
|
|
2218
|
-
})]
|
|
2219
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2220
|
-
className: "flex w-full flex-col gap-2 sm:w-auto sm:shrink-0 sm:flex-row sm:items-center sm:gap-3",
|
|
2221
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Button, {
|
|
2222
|
-
type: "button",
|
|
2223
|
-
variant: "secondary",
|
|
2224
|
-
onClick: () => setActiveTab("Tasks"),
|
|
2225
|
-
disabled: isSubmitting,
|
|
2226
|
-
className: "w-full sm:w-auto",
|
|
2227
|
-
children: t("cancel")
|
|
2228
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.Button, {
|
|
2229
|
-
type: "button",
|
|
2230
|
-
onClick: onSave,
|
|
2231
|
-
disabled: !isDirty || isSubmitting,
|
|
2232
|
-
"aria-busy": isSubmitting,
|
|
2233
|
-
className: "w-full sm:w-auto",
|
|
2234
|
-
children: [isSubmitting && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Spinner, { className: "size-4" }), isSubmitting ? t("saving") : t("update_contact")]
|
|
2235
|
-
})]
|
|
2236
|
-
})]
|
|
2237
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2238
|
-
className: "border-border flex items-center gap-1 border-b",
|
|
2239
|
-
children: [NON_PROFILE_TABS.map((tab) => {
|
|
2396
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2397
|
+
className: "border-border/50 flex items-center gap-1 border-b",
|
|
2398
|
+
children: VISIBLE_TABS.map((tab) => {
|
|
2240
2399
|
const count = tab === "Tasks" ? tasks.length : notes.length;
|
|
2241
2400
|
const isActive = activeTab === tab;
|
|
2242
2401
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2243
2402
|
type: "button",
|
|
2244
|
-
onClick: () =>
|
|
2403
|
+
onClick: () => onTabChange(tab),
|
|
2245
2404
|
className: require_src.cn("relative flex items-center gap-1.5 px-3 py-2.5 text-sm font-semibold transition-colors", isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground"),
|
|
2246
2405
|
children: [
|
|
2247
2406
|
tabLabels[tab],
|
|
2248
2407
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2249
|
-
className: require_src.cn("rounded-full px-1.5 text-xs leading-5 font-bold", isActive ? "bg-
|
|
2408
|
+
className: require_src.cn("rounded-full px-1.5 text-xs leading-5 font-bold", isActive ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"),
|
|
2250
2409
|
children: count
|
|
2251
2410
|
}),
|
|
2252
2411
|
isActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2253
|
-
className: "bg-
|
|
2412
|
+
className: "bg-primary absolute inset-x-0 -bottom-px h-0.5",
|
|
2254
2413
|
"aria-hidden": "true"
|
|
2255
2414
|
})
|
|
2256
2415
|
]
|
|
2257
2416
|
}, tab);
|
|
2258
|
-
})
|
|
2259
|
-
type: "button",
|
|
2260
|
-
onClick: () => setActiveTab("Profile"),
|
|
2261
|
-
className: "text-muted-foreground hover:text-foreground ml-auto px-3 py-2.5 text-sm font-semibold transition-colors",
|
|
2262
|
-
children: t("edit_profile")
|
|
2263
|
-
})]
|
|
2417
|
+
})
|
|
2264
2418
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2265
2419
|
className: "pt-6",
|
|
2266
|
-
children: [
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
isLoading: isLoadingTasks,
|
|
2280
|
-
contactId,
|
|
2281
|
-
hideHeader: true,
|
|
2282
|
-
ref: openTaskModalRef
|
|
2283
|
-
})]
|
|
2284
|
-
}),
|
|
2285
|
-
activeTab === "Notes" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotesList, {
|
|
2286
|
-
notes,
|
|
2287
|
-
isLoading: isLoadingNotes,
|
|
2288
|
-
contactId
|
|
2289
|
-
}),
|
|
2290
|
-
activeTab === "Profile" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Card, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.CardContent, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2291
|
-
className: "text-muted-foreground mb-6 text-sm",
|
|
2292
|
-
children: t("profile_edit_description")
|
|
2293
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContactDetailsForm, { countries: countryOptions })] }) })
|
|
2294
|
-
]
|
|
2420
|
+
children: [activeTab === "Tasks" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2421
|
+
className: "space-y-4",
|
|
2422
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineTaskComposer, { contactId }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskList, {
|
|
2423
|
+
tasks,
|
|
2424
|
+
isLoading: isLoadingTasks,
|
|
2425
|
+
contactId,
|
|
2426
|
+
hideHeader: true
|
|
2427
|
+
})]
|
|
2428
|
+
}), activeTab === "Notes" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotesList, {
|
|
2429
|
+
notes,
|
|
2430
|
+
isLoading: isLoadingNotes,
|
|
2431
|
+
contactId
|
|
2432
|
+
})]
|
|
2295
2433
|
})]
|
|
2296
2434
|
});
|
|
2297
2435
|
}
|
|
2298
2436
|
//#endregion
|
|
2299
2437
|
//#region ../../contacts/ui/src/portal/components/contacts/rep-layout/ContactDetailPane.tsx
|
|
2300
2438
|
function ContactDetailPane({ contactId, queryKeyPrefix, getCountries, onBack }) {
|
|
2439
|
+
const { t } = useContactsTranslation();
|
|
2301
2440
|
const { contact, isLoading, methods, countryOptions, isDirty, isSubmitting, isDeleting, onSave, onDelete } = useContactDetailPage(contactId, {
|
|
2302
2441
|
queryKeyPrefix,
|
|
2303
2442
|
getCountries,
|
|
2304
|
-
onDeleteSuccess: onBack
|
|
2443
|
+
onDeleteSuccess: onBack,
|
|
2444
|
+
onSaveSuccess: () => setEditProfileOpen(false)
|
|
2305
2445
|
});
|
|
2306
2446
|
const { data: tasks = [], isLoading: isLoadingTasks } = useContactTasks(contactId);
|
|
2307
2447
|
const { data: notes = [], isLoading: isLoadingNotes } = useContactNotes(contactId);
|
|
2308
|
-
const { t } = useContactsTranslation();
|
|
2309
2448
|
const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
|
|
2449
|
+
const [activeTab, setActiveTab] = (0, react.useState)("Tasks");
|
|
2450
|
+
const [editProfileOpen, setEditProfileOpen] = (0, react.useState)(false);
|
|
2310
2451
|
const handleConfirmDelete = (0, react.useCallback)(() => {
|
|
2311
2452
|
setConfirmDelete(false);
|
|
2312
2453
|
onDelete();
|
|
@@ -2318,7 +2459,8 @@ function ContactDetailPane({ contactId, queryKeyPrefix, getCountries, onBack })
|
|
|
2318
2459
|
onBack,
|
|
2319
2460
|
phone: null,
|
|
2320
2461
|
email: null,
|
|
2321
|
-
onDelete: null
|
|
2462
|
+
onDelete: null,
|
|
2463
|
+
onEditProfile: null
|
|
2322
2464
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2323
2465
|
className: "mx-auto flex max-w-md flex-col items-center px-6 py-16 text-center",
|
|
2324
2466
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
@@ -2340,7 +2482,8 @@ function ContactDetailPane({ contactId, queryKeyPrefix, getCountries, onBack })
|
|
|
2340
2482
|
onBack,
|
|
2341
2483
|
phone: contact.phone ?? null,
|
|
2342
2484
|
email: contact.email ?? null,
|
|
2343
|
-
onDelete: () => setConfirmDelete(true)
|
|
2485
|
+
onDelete: () => setConfirmDelete(true),
|
|
2486
|
+
onEditProfile: () => setEditProfileOpen(true)
|
|
2344
2487
|
}),
|
|
2345
2488
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2346
2489
|
className: "flex-1 overflow-y-auto",
|
|
@@ -2352,22 +2495,49 @@ function ContactDetailPane({ contactId, queryKeyPrefix, getCountries, onBack })
|
|
|
2352
2495
|
tasksCount: tasks.length,
|
|
2353
2496
|
notesCount: notes.length
|
|
2354
2497
|
}),
|
|
2355
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContactInfoRow, {
|
|
2498
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContactInfoRow, {
|
|
2499
|
+
contact,
|
|
2500
|
+
onEditEmpty: () => setEditProfileOpen(true)
|
|
2501
|
+
}),
|
|
2356
2502
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContactDetailTabs, {
|
|
2357
2503
|
contactId,
|
|
2358
|
-
contactName: fullName,
|
|
2359
|
-
countryOptions,
|
|
2360
|
-
isDirty,
|
|
2361
|
-
isSubmitting,
|
|
2362
|
-
onSave,
|
|
2363
2504
|
tasks,
|
|
2364
2505
|
isLoadingTasks,
|
|
2365
2506
|
notes,
|
|
2366
|
-
isLoadingNotes
|
|
2507
|
+
isLoadingNotes,
|
|
2508
|
+
activeTab,
|
|
2509
|
+
onTabChange: setActiveTab
|
|
2367
2510
|
})
|
|
2368
2511
|
]
|
|
2369
2512
|
})
|
|
2370
2513
|
}),
|
|
2514
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Dialog, {
|
|
2515
|
+
open: editProfileOpen,
|
|
2516
|
+
onOpenChange: (open) => {
|
|
2517
|
+
if (!open && isSubmitting) return;
|
|
2518
|
+
setEditProfileOpen(open);
|
|
2519
|
+
},
|
|
2520
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.DialogContent, {
|
|
2521
|
+
className: "max-h-[90vh] max-w-2xl overflow-y-auto",
|
|
2522
|
+
children: [
|
|
2523
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DialogHeader, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.DialogTitle, { children: t("edit_contact_name", { name: fullName }) }) }),
|
|
2524
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContactDetailsForm, { countries: countryOptions }),
|
|
2525
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.DialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Button, {
|
|
2526
|
+
type: "button",
|
|
2527
|
+
variant: "secondary",
|
|
2528
|
+
onClick: () => setEditProfileOpen(false),
|
|
2529
|
+
disabled: isSubmitting,
|
|
2530
|
+
children: t("cancel")
|
|
2531
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_src.Button, {
|
|
2532
|
+
type: "button",
|
|
2533
|
+
onClick: onSave,
|
|
2534
|
+
disabled: !isDirty || isSubmitting,
|
|
2535
|
+
"aria-busy": isSubmitting,
|
|
2536
|
+
children: [isSubmitting && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.Spinner, { className: "size-4" }), isSubmitting ? t("saving") : t("save_changes")]
|
|
2537
|
+
})] })
|
|
2538
|
+
]
|
|
2539
|
+
})
|
|
2540
|
+
}),
|
|
2371
2541
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.AlertDialog, {
|
|
2372
2542
|
open: confirmDelete,
|
|
2373
2543
|
onOpenChange: setConfirmDelete,
|
|
@@ -2385,10 +2555,10 @@ function ContactDetailPane({ contactId, queryKeyPrefix, getCountries, onBack })
|
|
|
2385
2555
|
})
|
|
2386
2556
|
});
|
|
2387
2557
|
}
|
|
2388
|
-
function DetailTopBar({ onBack, phone, email, onDelete }) {
|
|
2558
|
+
function DetailTopBar({ onBack, phone, email, onDelete, onEditProfile }) {
|
|
2389
2559
|
const { t } = useContactsTranslation();
|
|
2390
2560
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2391
|
-
className: "border-border bg-background/90 sticky top-0 z-10 flex items-center justify-between border-b px-4 py-3 backdrop-blur md:px-8",
|
|
2561
|
+
className: "border-border/50 bg-background/90 sticky top-0 z-10 flex items-center justify-between border-b px-4 py-3 backdrop-blur md:px-8",
|
|
2392
2562
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2393
2563
|
className: "flex items-center gap-3",
|
|
2394
2564
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -2404,6 +2574,16 @@ function DetailTopBar({ onBack, phone, email, onDelete }) {
|
|
|
2404
2574
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2405
2575
|
className: "flex items-center gap-1",
|
|
2406
2576
|
children: [
|
|
2577
|
+
onEditProfile && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2578
|
+
type: "button",
|
|
2579
|
+
onClick: onEditProfile,
|
|
2580
|
+
className: "text-muted-foreground hover:bg-muted hover:text-foreground flex size-8 items-center justify-center rounded-full transition-colors",
|
|
2581
|
+
"aria-label": t("edit_profile"),
|
|
2582
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Pencil, {
|
|
2583
|
+
className: "size-4",
|
|
2584
|
+
"aria-hidden": "true"
|
|
2585
|
+
})
|
|
2586
|
+
}),
|
|
2407
2587
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionLink, {
|
|
2408
2588
|
href: phone ? `tel:${phone}` : null,
|
|
2409
2589
|
label: t("label_call"),
|
|
@@ -2467,7 +2647,8 @@ function ContactDetailSkeleton({ onBack }) {
|
|
|
2467
2647
|
onBack,
|
|
2468
2648
|
phone: null,
|
|
2469
2649
|
email: null,
|
|
2470
|
-
onDelete: null
|
|
2650
|
+
onDelete: null,
|
|
2651
|
+
onEditProfile: null
|
|
2471
2652
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2472
2653
|
className: "mx-auto w-full max-w-[900px] flex-1 space-y-6 px-6 pt-8 pb-16 md:px-8 md:pt-10",
|
|
2473
2654
|
children: [
|
|
@@ -2515,7 +2696,7 @@ function RepContactsLayout({ selectedContactId, onSelect, onAdd, queryKeyPrefix,
|
|
|
2515
2696
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2516
2697
|
className: "bg-background flex h-full overflow-hidden",
|
|
2517
2698
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
|
|
2518
|
-
className: require_src.cn("border-border bg-background w-full shrink-0 overflow-hidden border-r md:flex md:w-[420px] md:flex-col", selectedContactId ? "hidden" : "flex flex-col"),
|
|
2699
|
+
className: require_src.cn("border-border/50 bg-background w-full shrink-0 overflow-hidden border-r md:flex md:w-[420px] md:flex-col", selectedContactId ? "hidden" : "flex flex-col"),
|
|
2519
2700
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContactsSidebar, {
|
|
2520
2701
|
selectedContactId,
|
|
2521
2702
|
onSelect,
|
|
@@ -3015,7 +3196,39 @@ const contactsDomain = require_static_dict_adapter.createDomainTranslations({
|
|
|
3015
3196
|
back_to_tasks: "Back to Tasks",
|
|
3016
3197
|
filter_all: "All",
|
|
3017
3198
|
filter_leads: "Leads",
|
|
3018
|
-
filter_customers: "Customers"
|
|
3199
|
+
filter_customers: "Customers",
|
|
3200
|
+
note_placeholder: "Leave a note about this contact…",
|
|
3201
|
+
note_body_aria: "Note body",
|
|
3202
|
+
add_note: "Add note",
|
|
3203
|
+
tip_save_shortcut_mac: "Tip: ⌘ + Enter to save",
|
|
3204
|
+
tip_save_shortcut_other: "Tip: Ctrl + Enter to save",
|
|
3205
|
+
task_describe_placeholder: "Describe the task…",
|
|
3206
|
+
task_description_aria: "Task description",
|
|
3207
|
+
task_discard_aria: "Discard task",
|
|
3208
|
+
add_task: "Add task",
|
|
3209
|
+
quick_today: "Today",
|
|
3210
|
+
quick_tomorrow: "Tomorrow",
|
|
3211
|
+
quick_next_week: "Next week",
|
|
3212
|
+
edit_contact_name: "Edit {{name}}",
|
|
3213
|
+
save_changes: "Save changes",
|
|
3214
|
+
add_field: "Add {{field}}",
|
|
3215
|
+
copy_field: "Copy {{field}}",
|
|
3216
|
+
copied_field: "Copied {{field}}",
|
|
3217
|
+
attachment_one: "1 attachment",
|
|
3218
|
+
attachment_other: "{{count}} attachments",
|
|
3219
|
+
note_actions: "Note actions",
|
|
3220
|
+
delete_note_title: "Delete note?",
|
|
3221
|
+
delete_note_irreversible: "This action cannot be undone.",
|
|
3222
|
+
task_yesterday: "Yesterday",
|
|
3223
|
+
task_days_ago_one: "1 day ago",
|
|
3224
|
+
task_days_ago_other: "{{count}} days ago",
|
|
3225
|
+
task_overdue_due: "Overdue · {{date}}",
|
|
3226
|
+
task_due: "Due {{date}}",
|
|
3227
|
+
mark_as_open: "Mark as open",
|
|
3228
|
+
mark_as_completed: "Mark as completed",
|
|
3229
|
+
task_actions: "Task actions",
|
|
3230
|
+
delete_task_title: "Delete task?",
|
|
3231
|
+
delete_task_confirm: "Are you sure you want to delete this task? This action cannot be undone."
|
|
3019
3232
|
},
|
|
3020
3233
|
loaders: {
|
|
3021
3234
|
de: () => Promise.resolve().then(() => require("./de-Bi_lh0mM.cjs")).then((m) => m.default),
|
|
@@ -3193,4 +3406,4 @@ Object.defineProperty(exports, "contactsScreenPropertySchema", {
|
|
|
3193
3406
|
}
|
|
3194
3407
|
});
|
|
3195
3408
|
|
|
3196
|
-
//# sourceMappingURL=ContactsScreen-
|
|
3409
|
+
//# sourceMappingURL=ContactsScreen-DrMOpvio.cjs.map
|