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