@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.
@@ -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, Et as CardContent, 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, dn as Breadcrumb, fn as BreadcrumbItem, gn as BreadcrumbSeparator, hn as BreadcrumbPage, j as SelectContent, kn as useZodForm, mn as BreadcrumbList, pn as BreadcrumbLink, q as DropdownMenu, rt as DropdownMenuTrigger, vn as AlertDialog, w as Sheet, wn as AlertDialogHeader, wt as Card, xn as AlertDialogContent, y as Spinner, yn as AlertDialogAction } from "./src-B3W228vu.mjs";
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, useImperativeHandle, useMemo, useRef, useState } from "react";
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, ArrowLeft, Calendar, ChevronLeft, ChevronRight, CircleCheck, ClipboardList, EllipsisVertical, List, ListOrdered, Mail, MapPin, MoreHorizontal, Paperclip, Pen, Phone, Plus, Search, SquareCheckBig, StickyNote, Trash2, Users, X } from "lucide-react";
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 status = contact.status?.trim() || null;
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: "bg-muted text-muted-foreground relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-xl text-base font-semibold shadow-sm",
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__ */ jsxs("div", {
329
- className: "flex items-center gap-2",
330
- children: [/* @__PURE__ */ jsx("span", {
331
- className: "text-foreground truncate text-sm font-bold tracking-tight",
332
- children: name
333
- }), status && /* @__PURE__ */ jsx(StatusBadge, {
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 group-hover:text-foreground group-hover:translate-x-0.5"),
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: "px-7 pt-8 pb-5",
414
+ className: "border-border/50 border-b px-6 py-3",
464
415
  children: /* @__PURE__ */ jsxs("div", {
465
- className: "flex items-start justify-between gap-3",
416
+ className: "flex items-center gap-3",
466
417
  children: [/* @__PURE__ */ jsxs("div", {
467
- className: "min-w-0",
468
- children: [/* @__PURE__ */ jsx("h1", {
469
- className: "text-foreground text-2xl font-extrabold tracking-tight",
470
- children: t("breadcrumb")
471
- }), /* @__PURE__ */ jsx("p", {
472
- className: "text-muted-foreground mt-1.5 text-sm",
473
- children: isLoading ? t("loading") : hasNextPage && filter.kind === "status" ? t("searching") : subtitleCount === 1 ? t("person_count", { count: subtitleCount }) : t("people_count", { count: subtitleCount })
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-foreground text-background hover:bg-foreground/90 inline-flex shrink-0 items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-bold transition-colors",
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: "px-6 pb-4",
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 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
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-foreground text-background" : "border-border bg-muted text-foreground hover:bg-muted/70 border"),
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/hooks/useContactDetail.ts
595
- function useContactDetail(contactId, queryKeyPrefix = "contacts") {
596
- const api = useContactsCrud();
597
- return useQuery({
598
- queryKey: CONTACTS_QUERY_KEYS.detail(queryKeyPrefix, contactId),
599
- queryFn: () => api.getContact(contactId),
600
- enabled: !!contactId
601
- });
602
- }
603
- //#endregion
604
- //#region ../../contacts/ui/src/shared/hooks/useUpdateContactMutation.ts
605
- function useUpdateContactMutation(contactId, queryKeyPrefix = "contacts", options) {
606
- const queryClient = useQueryClient();
607
- const api = useContactsCrud();
608
- return useMutation({
609
- mutationFn: ({ id, data }) => api.updateContact(id, data),
610
- onSuccess: () => {
611
- fluidToast({
612
- title: "Contact updated successfully",
613
- type: "success"
614
- });
615
- queryClient.invalidateQueries({ queryKey: CONTACTS_QUERY_KEYS.all(queryKeyPrefix) });
616
- options?.onSuccess?.();
617
- },
618
- onError: (error) => {
619
- fluidToast({
620
- title: "Failed to save contact",
621
- type: "error",
622
- description: parseApiErrors(error)
623
- });
624
- options?.onError?.(error);
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
- //#endregion
768
- //#region ../../contacts/ui/src/portal/hooks/notes/use-contact-notes.ts
769
- function useContactNotes(contactId) {
770
- const api = useNotesApi();
771
- return useQuery({
772
- queryKey: contactsKeys.notes(contactId),
773
- queryFn: () => api.listNotes(contactId),
774
- enabled: !!contactId,
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: "flex flex-col gap-5 md:flex-row md:items-start md:justify-between md:gap-8",
787
- children: [/* @__PURE__ */ jsxs("div", {
788
- className: "flex flex-col gap-4 md:flex-row md:items-start md:gap-5",
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: "bg-muted text-muted-foreground relative flex size-20 shrink-0 items-center justify-center overflow-hidden rounded-xl text-2xl font-semibold shadow-sm md:size-24 md:text-2xl",
791
- children: contact.avatar_url ? /* @__PURE__ */ jsx("img", {
792
- alt: name,
793
- src: contact.avatar_url,
794
- className: "size-full object-cover"
795
- }) : /* @__PURE__ */ jsx("span", {
796
- "aria-hidden": "true",
797
- children: getInitials(name)
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
- }), /* @__PURE__ */ jsxs("div", {
800
- className: "min-w-0 flex-1",
801
- children: [/* @__PURE__ */ jsx("h2", {
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: "bg-muted flex shrink-0 gap-6 rounded-xl px-5 py-4",
594
+ className: "grid grid-cols-1 gap-6 lg:grid-cols-2",
814
595
  children: [
815
- /* @__PURE__ */ jsx(Stat, {
816
- label: t("tab_tasks"),
817
- value: tasksCount
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("div", {
820
- className: "bg-border w-px",
821
- "aria-hidden": "true"
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(Stat, {
824
- label: t("tab_notes"),
825
- value: notesCount
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/portal/components/contacts/rep-layout/ContactInfoRow.tsx
842
- function buildAddressLine(contact) {
843
- const parts = [
844
- contact.address,
845
- contact.city,
846
- contact.state,
847
- contact.postal_code
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/portal/hooks/contacts/use-delete-contact-task.ts
927
- function useDeleteContactTask(contactId, options) {
803
+ //#region ../../contacts/ui/src/shared/hooks/useUpdateContactMutation.ts
804
+ function useUpdateContactMutation(contactId, queryKeyPrefix = "contacts", options) {
928
805
  const queryClient = useQueryClient();
929
- const api = useTasksApi();
806
+ const api = useContactsCrud();
930
807
  return useMutation({
931
- mutationFn: (taskId) => api.deleteTask(taskId, contactId),
808
+ mutationFn: ({ id, data }) => api.updateContact(id, data),
932
809
  onSuccess: () => {
933
810
  fluidToast({
934
- title: "Task deleted",
811
+ title: "Contact updated successfully",
935
812
  type: "success"
936
813
  });
937
- queryClient.invalidateQueries({ queryKey: contactsKeys.tasks(contactId) });
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 delete task",
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/portal/hooks/contacts/use-create-contact-task.ts
951
- function useCreateContactTask(contactId, options) {
828
+ //#region ../../contacts/ui/src/shared/hooks/useDeleteContactMutation.ts
829
+ function useDeleteContactMutation(queryKeyPrefix = "contacts", options) {
952
830
  const queryClient = useQueryClient();
953
- const api = useTasksApi();
831
+ const api = useContactsCrud();
954
832
  return useMutation({
955
- mutationFn: (input) => api.createTask(contactId, input),
833
+ mutationFn: (contactId) => api.deleteContact(contactId),
956
834
  onSuccess: () => {
957
835
  fluidToast({
958
- title: "Task created",
836
+ title: "Contact deleted successfully",
959
837
  type: "success"
960
838
  });
961
- queryClient.invalidateQueries({ queryKey: contactsKeys.tasks(contactId) });
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 create task",
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/portal/hooks/contacts/use-update-contact-task.ts
975
- function useUpdateContactTask(contactId, options) {
976
- const queryClient = useQueryClient();
977
- const api = useTasksApi();
978
- return useMutation({
979
- mutationFn: ({ taskId, input }) => api.updateTask(taskId, contactId, input),
980
- onSuccess: () => {
981
- fluidToast({
982
- title: "Task updated",
983
- type: "success"
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
- queryClient.invalidateQueries({ queryKey: contactsKeys.tasks(contactId) });
986
- options?.onSuccess?.();
987
- },
988
- onError: (error) => {
926
+ }, (errors) => {
989
927
  fluidToast({
990
- title: "Failed to update task",
991
- type: "error",
992
- description: parseApiErrors(error)
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/utils/format-date.ts
999
- function formatDateForDisplay(dateStr) {
1000
- return new Date(dateStr).toLocaleDateString("en-US", {
1001
- month: "short",
1002
- day: "numeric",
1003
- year: "numeric"
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/components/editor/note-task-editor.tsx
1008
- function extractTitleAndBody(editor) {
1009
- if (!editor) return {
1010
- title: "",
1011
- body: ""
1012
- };
1013
- const doc = editor.state.doc;
1014
- let title = "";
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
- return parts.join("\n");
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
- else if (diffDays === 1) return "Yesterday";
1403
- else return `${diffDays} days ago`;
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 EmptyState$1({ onCreateTask }) {
1406
- return /* @__PURE__ */ jsxs("div", {
1407
- className: "flex flex-col items-center justify-center px-4 py-12 text-center",
1408
- children: [
1409
- /* @__PURE__ */ jsxs("div", {
1410
- className: "relative mb-6",
1411
- children: [/* @__PURE__ */ jsx("div", {
1412
- className: "flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-teal-100 to-emerald-100",
1413
- children: /* @__PURE__ */ jsx(ClipboardList, { className: "h-9 w-9 text-teal-500" })
1414
- }), /* @__PURE__ */ jsx("div", {
1415
- 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-blue-100 to-indigo-100 shadow-sm",
1416
- children: /* @__PURE__ */ jsx(SquareCheckBig, { className: "h-4 w-4 text-blue-500" })
1417
- })]
1418
- }),
1419
- /* @__PURE__ */ jsx("h4", {
1420
- className: "text-foreground text-base font-semibold",
1421
- children: "No tasks yet"
1422
- }),
1423
- /* @__PURE__ */ jsx("p", {
1424
- className: "text-muted-foreground mt-1.5 max-w-[260px] text-sm leading-relaxed",
1425
- children: "Stay organized by creating tasks to track follow-ups for this contact"
1426
- }),
1427
- /* @__PURE__ */ jsxs("button", {
1428
- type: "button",
1429
- onClick: onCreateTask,
1430
- 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",
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
- return /* @__PURE__ */ jsx("div", {
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 bg-card min-w-[250px] cursor-pointer rounded-xl border p-4 transition-colors",
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-opacity hover:opacity-70 disabled:cursor-not-allowed disabled:opacity-50",
1464
- "aria-label": isCompleted ? "Mark as open" : "Mark as completed",
1465
- children: isCompleted ? /* @__PURE__ */ jsx(CircleCheck, { className: "text-primary h-5 w-5" }) : /* @__PURE__ */ jsx("div", { className: "border-muted-foreground h-5 w-5 rounded-full border-2" })
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__ */ jsxs("div", {
1470
- className: "flex items-start justify-between gap-2",
1471
- children: [/* @__PURE__ */ jsx("h4", {
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-3 text-sm",
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-row items-center gap-2",
1501
- children: [/* @__PURE__ */ jsx("span", {
1502
- className: "text-muted-foreground text-xs",
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, hideHeader = false, ref }) {
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
- if (editingTask) updateTask.mutate({
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__ */ jsxs("div", {
1553
- className: "space-y-4",
1554
- children: [!hideHeader && /* @__PURE__ */ jsxs("div", {
1555
- className: "flex items-center justify-between",
1556
- 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" })]
1557
- }), /* @__PURE__ */ jsx("div", {
1558
- className: "grid grid-cols-1 gap-5 sm:grid-cols-2",
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
- !hideHeader && /* @__PURE__ */ jsxs("div", {
1570
- className: "flex items-start justify-between",
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: isModalOpen,
1912
+ open: editingTask !== null,
1599
1913
  onOpenChange: handleModalOpenChange,
1600
- mode: editingTask ? "edit" : "create",
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: createTask.isPending || updateTask.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 ?? "create"),
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: "Delete Task" }), /* @__PURE__ */ jsx(AlertDialogDescription, { children: "Are you sure you want to delete this task? This action cannot be undone." })] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, { children: "Cancel" }), /* @__PURE__ */ jsx(AlertDialogAction, {
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: "Delete"
1939
+ children: t("delete")
1626
1940
  })] })] })
1627
1941
  })
1628
1942
  ]
1629
1943
  });
1630
1944
  }
1631
1945
  //#endregion
1632
- //#region ../../contacts/ui/src/portal/hooks/notes/use-delete-contact-note.ts
1633
- function useDeleteContactNote(contactId, options) {
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 = useNotesApi();
1949
+ const api = useTasksApi();
1684
1950
  return useMutation({
1685
- mutationFn: ({ noteId, input }) => api.updateNote(noteId, contactId, input),
1951
+ mutationFn: (input) => api.createTask(contactId, input),
1686
1952
  onSuccess: () => {
1687
1953
  fluidToast({
1688
- title: "Note updated",
1954
+ title: "Task created",
1689
1955
  type: "success"
1690
1956
  });
1691
- queryClient.invalidateQueries({ queryKey: contactsKeys.notes(contactId) });
1957
+ queryClient.invalidateQueries({ queryKey: contactsKeys.tasks(contactId) });
1692
1958
  options?.onSuccess?.();
1693
1959
  },
1694
1960
  onError: (error) => {
1695
1961
  fluidToast({
1696
- title: "Failed to update note",
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/shared/components/contacts/contactDetailsForm.tsx
1927
- const DEFAULT_COUNTRIES = [];
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
- name: "Cold",
1943
- value: "cold"
1973
+ key: "today",
1974
+ offsetDays: 0
1944
1975
  },
1945
1976
  {
1946
- name: "Lead",
1947
- value: "lead"
1977
+ key: "tomorrow",
1978
+ offsetDays: 1
1948
1979
  },
1949
1980
  {
1950
- name: "Customer",
1951
- value: "customer"
1981
+ key: "next_week",
1982
+ offsetDays: 7
1952
1983
  }
1953
1984
  ];
1954
- const ContactDetailsForm = ({ className, countries = DEFAULT_COUNTRIES, renderAvatarPicker }) => {
1955
- const { control, watch, setValue } = useFormContext();
1956
- const currentStatus = useWatch({
1957
- control,
1958
- name: "status"
1959
- });
1960
- const avatarUrl = watch("avatar_url") ?? null;
1961
- const effectiveStatusOptions = useMemo(() => {
1962
- if (currentStatus && typeof currentStatus === "string" && !statusOptions.some((o) => o.value === currentStatus)) return [{
1963
- name: currentStatus.charAt(0).toUpperCase() + currentStatus.slice(1).replace(/_/g, " "),
1964
- value: currentStatus
1965
- }, ...statusOptions];
1966
- return statusOptions;
1967
- }, [currentStatus]);
1968
- const initials = [watch("first_name")?.[0] ?? "", watch("last_name")?.[0] ?? ""].filter(Boolean).join("").toUpperCase() || "?";
1969
- return /* @__PURE__ */ jsxs("div", {
1970
- className: cn("space-y-6", className),
1971
- children: [renderAvatarPicker && /* @__PURE__ */ jsxs("div", {
1972
- className: "flex flex-col items-center gap-3",
1973
- children: [/* @__PURE__ */ jsx("div", {
1974
- className: "border-border bg-background relative h-20 w-20 shrink-0 overflow-hidden rounded-full border-2",
1975
- children: avatarUrl ? /* @__PURE__ */ jsx("img", {
1976
- src: avatarUrl,
1977
- alt: "",
1978
- className: "h-full w-full object-cover"
1979
- }) : /* @__PURE__ */ jsx("div", {
1980
- className: "text-muted-foreground flex h-full w-full items-center justify-center text-lg font-semibold",
1981
- children: initials
1982
- })
1983
- }), renderAvatarPicker({
1984
- value: avatarUrl,
1985
- onChange: (url) => setValue("avatar_url", url ?? "", { shouldDirty: true })
1986
- })]
1987
- }), /* @__PURE__ */ jsxs("div", {
1988
- className: "grid grid-cols-1 gap-6 lg:grid-cols-2",
1989
- children: [
1990
- /* @__PURE__ */ jsx(FormField, {
1991
- control,
1992
- name: "first_name",
1993
- render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
1994
- /* @__PURE__ */ jsx(FormLabel, {
1995
- className: "font-inter text-foreground font-medium",
1996
- children: "First Name"
1997
- }),
1998
- /* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(Input, {
1999
- placeholder: "Enter first name",
2000
- ...field,
2001
- value: field.value ?? "",
2002
- className: "ring-input"
2003
- }) }),
2004
- /* @__PURE__ */ jsx(FormMessage, {})
2005
- ] })
2006
- }),
2007
- /* @__PURE__ */ jsx(FormField, {
2008
- control,
2009
- name: "last_name",
2010
- render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
2011
- /* @__PURE__ */ jsx(FormLabel, {
2012
- className: "font-inter text-foreground font-medium",
2013
- children: "Last Name"
2014
- }),
2015
- /* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(Input, {
2016
- placeholder: "Enter last name",
2017
- ...field,
2018
- value: field.value ?? "",
2019
- className: "ring-input"
2020
- }) }),
2021
- /* @__PURE__ */ jsx(FormMessage, {})
2022
- ] })
2023
- }),
2024
- /* @__PURE__ */ jsx(FormField, {
2025
- control,
2026
- name: "email",
2027
- render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
2028
- /* @__PURE__ */ jsx(FormLabel, {
2029
- className: "font-inter text-foreground font-medium",
2030
- children: "Email"
2031
- }),
2032
- /* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(Input, {
2033
- placeholder: "Enter email address",
2034
- type: "email",
2035
- ...field,
2036
- value: field.value ?? "",
2037
- className: "ring-input"
2038
- }) }),
2039
- /* @__PURE__ */ jsx(FormMessage, {})
2040
- ] })
2041
- }),
2042
- /* @__PURE__ */ jsx(FormField, {
2043
- control,
2044
- name: "phone",
2045
- render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
2046
- /* @__PURE__ */ jsx(FormLabel, {
2047
- className: "font-inter text-foreground font-medium",
2048
- children: "Phone"
2049
- }),
2050
- /* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(Input, {
2051
- placeholder: "Enter phone number",
2052
- ...field,
2053
- value: field.value ?? "",
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 NON_PROFILE_TABS = [
2189
- "Tasks",
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: [activeTab === "Profile" ? /* @__PURE__ */ jsxs("div", {
2205
- className: "border-border flex flex-col gap-3 border-b sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:gap-x-4 sm:gap-y-2",
2206
- children: [/* @__PURE__ */ jsxs("div", {
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: () => setActiveTab(tab),
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-foreground text-background" : "bg-muted text-muted-foreground"),
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-foreground absolute inset-x-0 -bottom-px h-0.5",
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
- }), /* @__PURE__ */ jsx("button", {
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
- activeTab === "Tasks" && /* @__PURE__ */ jsxs("div", {
2267
- className: "space-y-4",
2268
- children: [/* @__PURE__ */ jsxs("button", {
2269
- type: "button",
2270
- onClick: () => openTaskModalRef.current?.(),
2271
- className: "border-border 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",
2272
- children: [/* @__PURE__ */ jsx(Plus, {
2273
- className: "size-4",
2274
- "aria-hidden": "true"
2275
- }), t("new_task")]
2276
- }), /* @__PURE__ */ jsx(TaskList, {
2277
- tasks,
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, { contact }),
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-BFiClnMI.mjs.map
3401
+ //# sourceMappingURL=ContactsScreen-Cg_PgwAi.mjs.map