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