@open-mercato/core 0.5.1-develop.3045.b4b3320cc2 → 0.6.0

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.
Files changed (106) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +21 -1
  3. package/dist/modules/api_keys/api/keys/route.js +9 -0
  4. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  5. package/dist/modules/audit_logs/services/accessLogService.js +13 -0
  6. package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
  7. package/dist/modules/audit_logs/services/actionLogService.js +6 -5
  8. package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
  9. package/dist/modules/auth/api/roles/acl/route.js +27 -37
  10. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  11. package/dist/modules/auth/api/users/route.js +41 -28
  12. package/dist/modules/auth/api/users/route.js.map +3 -3
  13. package/dist/modules/auth/lib/grantChecks.js +160 -0
  14. package/dist/modules/auth/lib/grantChecks.js.map +7 -0
  15. package/dist/modules/configs/cli.js +11 -0
  16. package/dist/modules/configs/cli.js.map +2 -2
  17. package/dist/modules/configs/lib/touchGeneratedBarrels.js +46 -0
  18. package/dist/modules/configs/lib/touchGeneratedBarrels.js.map +7 -0
  19. package/dist/modules/customers/api/activities/route.js +1 -52
  20. package/dist/modules/customers/api/activities/route.js.map +2 -2
  21. package/dist/modules/customers/api/interactions/counts/route.js +2 -1
  22. package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
  23. package/dist/modules/customers/api/interactions/route.js +21 -1
  24. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  25. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -3
  26. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  27. package/dist/modules/customers/backend/customers/deals/[id]/page.js +5 -1
  28. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  29. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -3
  30. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  31. package/dist/modules/customers/components/detail/ActivitiesCard.js +62 -6
  32. package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
  33. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +21 -6
  34. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
  35. package/dist/modules/customers/components/detail/ActivitiesSection.js +37 -5
  36. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  37. package/dist/modules/customers/components/detail/ActivityCard.js +69 -17
  38. package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
  39. package/dist/modules/customers/components/detail/ActivityHistorySection.js +94 -34
  40. package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
  41. package/dist/modules/customers/components/detail/ActivityLogTab.js +3 -1
  42. package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
  43. package/dist/modules/customers/components/detail/ActivityTimeline.js +41 -8
  44. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
  45. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +19 -6
  46. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
  47. package/dist/modules/customers/components/detail/ActivityTypeSelector.js +4 -3
  48. package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +2 -2
  49. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +80 -12
  50. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  51. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +65 -10
  52. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  53. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +10 -5
  54. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  55. package/dist/modules/customers/data/validators.js +74 -2
  56. package/dist/modules/customers/data/validators.js.map +2 -2
  57. package/dist/modules/customers/lib/legacyActivityBridge.js +61 -0
  58. package/dist/modules/customers/lib/legacyActivityBridge.js.map +7 -0
  59. package/dist/modules/integrations/data/validators.js +2 -2
  60. package/dist/modules/integrations/data/validators.js.map +2 -2
  61. package/dist/modules/integrations/lib/credentials-service.js +12 -1
  62. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  63. package/dist/modules/messages/commands/actions.js +29 -14
  64. package/dist/modules/messages/commands/actions.js.map +2 -2
  65. package/dist/modules/messages/lib/actions.js +24 -4
  66. package/dist/modules/messages/lib/actions.js.map +2 -2
  67. package/dist/modules/sales/api/documents/factory.js +49 -36
  68. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  69. package/package.json +9 -10
  70. package/src/modules/api_keys/api/keys/route.ts +9 -0
  71. package/src/modules/audit_logs/services/accessLogService.ts +20 -0
  72. package/src/modules/audit_logs/services/actionLogService.ts +13 -5
  73. package/src/modules/auth/api/roles/acl/route.ts +32 -46
  74. package/src/modules/auth/api/users/route.ts +48 -33
  75. package/src/modules/auth/lib/grantChecks.ts +234 -0
  76. package/src/modules/configs/cli.ts +11 -0
  77. package/src/modules/configs/lib/touchGeneratedBarrels.ts +61 -0
  78. package/src/modules/customers/api/activities/route.ts +1 -76
  79. package/src/modules/customers/api/interactions/counts/route.ts +2 -1
  80. package/src/modules/customers/api/interactions/route.ts +28 -1
  81. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +13 -3
  82. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +14 -2
  83. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -3
  84. package/src/modules/customers/components/detail/ActivitiesCard.tsx +92 -5
  85. package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +38 -6
  86. package/src/modules/customers/components/detail/ActivitiesSection.tsx +37 -3
  87. package/src/modules/customers/components/detail/ActivityCard.tsx +79 -14
  88. package/src/modules/customers/components/detail/ActivityHistorySection.tsx +102 -33
  89. package/src/modules/customers/components/detail/ActivityLogTab.tsx +7 -1
  90. package/src/modules/customers/components/detail/ActivityTimeline.tsx +39 -5
  91. package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +29 -7
  92. package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +3 -2
  93. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +96 -13
  94. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +50 -4
  95. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +21 -5
  96. package/src/modules/customers/data/validators.ts +85 -2
  97. package/src/modules/customers/i18n/de.json +11 -0
  98. package/src/modules/customers/i18n/en.json +11 -0
  99. package/src/modules/customers/i18n/es.json +11 -0
  100. package/src/modules/customers/i18n/pl.json +11 -0
  101. package/src/modules/customers/lib/legacyActivityBridge.ts +106 -0
  102. package/src/modules/integrations/data/validators.ts +8 -6
  103. package/src/modules/integrations/lib/credentials-service.ts +15 -1
  104. package/src/modules/messages/commands/actions.ts +28 -13
  105. package/src/modules/messages/lib/actions.ts +34 -3
  106. package/src/modules/sales/api/documents/factory.ts +55 -38
@@ -1,16 +1,18 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { Phone, Mail, Users, StickyNote, User } from "lucide-react";
4
+ import { Check, ListTodo, Phone, Mail, Users, StickyNote, User } from "lucide-react";
5
5
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { Button } from "@open-mercato/ui/primitives/button";
6
7
  import { AiActionChips } from "./AiActionChips.js";
7
8
  const TYPE_ICONS = {
8
9
  call: Phone,
9
10
  email: Mail,
10
11
  meeting: Users,
11
- note: StickyNote
12
+ note: StickyNote,
13
+ task: ListTodo
12
14
  };
13
- function ActivityTimeline({ activities, onEdit }) {
15
+ function ActivityTimeline({ activities, onEdit, onMarkDone }) {
14
16
  const t = useT();
15
17
  if (activities.length === 0) {
16
18
  return /* @__PURE__ */ jsx("div", { className: "py-6 text-center text-sm text-muted-foreground", children: t("customers.timeline.empty", "No activities match the current filters.") });
@@ -33,7 +35,8 @@ function ActivityTimeline({ activities, onEdit }) {
33
35
  activity,
34
36
  t,
35
37
  withBorder: index < activities.length - 1,
36
- onEdit
38
+ onEdit,
39
+ onMarkDone
37
40
  }
38
41
  )
39
42
  ] }, activity.id);
@@ -43,12 +46,25 @@ function TimelineEntry({
43
46
  activity,
44
47
  t,
45
48
  withBorder,
46
- onEdit
49
+ onEdit,
50
+ onMarkDone
47
51
  }) {
48
52
  const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt;
49
53
  const TypeIcon = TYPE_ICONS[activity.interactionType];
50
54
  const title = activity.title ?? activity.body ?? activity.interactionType;
51
55
  const duration = activity.duration ? ` (${activity.duration} min)` : "";
56
+ const isPlanned = activity.status === "planned";
57
+ const [markingDone, setMarkingDone] = React.useState(false);
58
+ const handleMarkDone = React.useCallback(async (event) => {
59
+ event.stopPropagation();
60
+ if (!onMarkDone || markingDone) return;
61
+ setMarkingDone(true);
62
+ try {
63
+ await onMarkDone(activity.id);
64
+ } finally {
65
+ setMarkingDone(false);
66
+ }
67
+ }, [activity.id, markingDone, onMarkDone]);
52
68
  return /* @__PURE__ */ jsx(
53
69
  "div",
54
70
  {
@@ -66,9 +82,26 @@ function TimelineEntry({
66
82
  ] }),
67
83
  /* @__PURE__ */ jsx("div", { className: "flex size-8 items-center justify-center rounded-md bg-muted shrink-0", children: TypeIcon ? /* @__PURE__ */ jsx(TypeIcon, { className: "size-3.5 text-muted-foreground" }) : null }),
68
84
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1.5", children: [
69
- /* @__PURE__ */ jsxs("span", { className: "block text-[12px] font-semibold leading-tight text-foreground", children: [
70
- title,
71
- duration
85
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
86
+ /* @__PURE__ */ jsxs("span", { className: "block text-[12px] font-semibold leading-tight text-foreground", children: [
87
+ title,
88
+ duration
89
+ ] }),
90
+ isPlanned && onMarkDone ? /* @__PURE__ */ jsxs(
91
+ Button,
92
+ {
93
+ type: "button",
94
+ variant: "default",
95
+ size: "sm",
96
+ disabled: markingDone,
97
+ onClick: handleMarkDone,
98
+ className: "shrink-0",
99
+ children: [
100
+ /* @__PURE__ */ jsx(Check, { className: "size-3.5" }),
101
+ t("customers.activities.actions.markDone", "Mark done")
102
+ ]
103
+ }
104
+ ) : null
72
105
  ] }),
73
106
  activity.body && activity.title && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: activity.body }),
74
107
  activity.authorName && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-[10px] text-muted-foreground", children: [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ActivityTimeline.tsx"],
4
- "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, User } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { AiActionChips } from './AiActionChips'\nimport type { InteractionSummary } from './types'\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\ninterface ActivityTimelineProps {\n activities: InteractionSummary[]\n onEdit?: (activity: InteractionSummary) => void\n}\n\nexport function ActivityTimeline({ activities, onEdit }: ActivityTimelineProps) {\n const t = useT()\n\n if (activities.length === 0) {\n return (\n <div className=\"py-6 text-center text-sm text-muted-foreground\">\n {t('customers.timeline.empty', 'No activities match the current filters.')}\n </div>\n )\n }\n\n return (\n <div>\n {activities.map((activity, index) => {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const activityYear = dateStr ? new Date(dateStr).getFullYear() : null\n const prevDateStr = index > 0 ? (activities[index - 1].scheduledAt ?? activities[index - 1].occurredAt ?? activities[index - 1].createdAt) : null\n const prevYear = prevDateStr ? new Date(prevDateStr).getFullYear() : null\n const showYearSeparator = activityYear !== null && prevYear !== null && activityYear !== prevYear\n\n return (\n <React.Fragment key={activity.id}>\n {showYearSeparator && (\n <div className=\"flex items-center gap-3 py-3 px-5\">\n <div className=\"h-px flex-1 bg-border\" />\n <span className=\"text-xs font-semibold text-muted-foreground\">\n {t('customers.activities.yearSeparator', '{year}', { year: activityYear })}\n </span>\n <div className=\"h-px flex-1 bg-border\" />\n </div>\n )}\n <TimelineEntry\n activity={activity}\n t={t}\n withBorder={index < activities.length - 1}\n onEdit={onEdit}\n />\n </React.Fragment>\n )\n })}\n </div>\n )\n}\n\nfunction TimelineEntry({\n activity,\n t,\n withBorder,\n onEdit,\n}: {\n activity: InteractionSummary\n t: TranslateFn\n withBorder: boolean\n onEdit?: (activity: InteractionSummary) => void\n}) {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n const title = activity.title ?? activity.body ?? activity.interactionType\n const duration = activity.duration ? ` (${activity.duration} min)` : ''\n\n return (\n <div\n className={`py-2.5 ${withBorder ? 'border-b border-border/60' : ''} ${onEdit ? 'cursor-pointer hover:bg-accent/40 transition-colors' : ''}`}\n onClick={() => onEdit?.(activity)}\n role={onEdit ? 'button' : undefined}\n tabIndex={onEdit ? 0 : undefined}\n onKeyDown={onEdit ? (e) => { if (e.key === 'Enter') onEdit(activity) } : undefined}\n >\n <div className=\"grid items-start gap-3\" style={{ gridTemplateColumns: '75px 32px 1fr' }}>\n {/* Column 1: Date */}\n <div className=\"shrink-0\">\n <span className=\"block text-[11px] font-semibold leading-tight text-foreground\">\n {formatRelativeDate(dateStr, t)}\n </span>\n <span className=\"block text-[10px] leading-tight text-muted-foreground\">\n {formatTime(dateStr)}\n </span>\n </div>\n\n {/* Column 2: Type icon */}\n <div className=\"flex size-8 items-center justify-center rounded-md bg-muted shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-3.5 text-muted-foreground\" /> : null}\n </div>\n\n {/* Column 3: Content */}\n <div className=\"min-w-0 space-y-1.5\">\n <span className=\"block text-[12px] font-semibold leading-tight text-foreground\">\n {title}{duration}\n </span>\n\n {activity.body && activity.title && (\n <p className=\"text-[11px] leading-snug text-muted-foreground\">\n {activity.body}\n </p>\n )}\n\n {activity.authorName && (\n <div className=\"flex items-center gap-1 text-[10px] text-muted-foreground\">\n <User className=\"size-2.5 shrink-0\" />\n <span>{t('customers.timeline.author', 'by {{name}}', { name: activity.authorName })}</span>\n </div>\n )}\n\n <AiActionChips activityType={activity.interactionType} />\n </div>\n </div>\n </div>\n )\n}\n\nfunction formatRelativeDate(isoString: string, t: TranslateFn): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n // Compare calendar dates (not time-based diff) to correctly handle same-day future times\n const dateDay = new Date(date.getFullYear(), date.getMonth(), date.getDate())\n const nowDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const diffDays = Math.round((nowDay.getTime() - dateDay.getTime()) / (1000 * 60 * 60 * 24))\n\n if (diffDays === 0) return t('customers.timeline.date.today', 'today')\n if (diffDays === 1) return t('customers.timeline.date.yesterday', 'yesterday')\n return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n } catch {\n return ''\n }\n}\n\nfunction formatTime(isoString: string): string {\n try {\n return new Date(isoString).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n } catch {\n return ''\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Check, ListTodo, Phone, Mail, Users, StickyNote, User } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { AiActionChips } from './AiActionChips'\nimport type { InteractionSummary } from './types'\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n task: ListTodo,\n}\n\ninterface ActivityTimelineProps {\n activities: InteractionSummary[]\n onEdit?: (activity: InteractionSummary) => void\n onMarkDone?: (activityId: string) => void | Promise<void>\n}\n\nexport function ActivityTimeline({ activities, onEdit, onMarkDone }: ActivityTimelineProps) {\n const t = useT()\n\n if (activities.length === 0) {\n return (\n <div className=\"py-6 text-center text-sm text-muted-foreground\">\n {t('customers.timeline.empty', 'No activities match the current filters.')}\n </div>\n )\n }\n\n return (\n <div>\n {activities.map((activity, index) => {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const activityYear = dateStr ? new Date(dateStr).getFullYear() : null\n const prevDateStr = index > 0 ? (activities[index - 1].scheduledAt ?? activities[index - 1].occurredAt ?? activities[index - 1].createdAt) : null\n const prevYear = prevDateStr ? new Date(prevDateStr).getFullYear() : null\n const showYearSeparator = activityYear !== null && prevYear !== null && activityYear !== prevYear\n\n return (\n <React.Fragment key={activity.id}>\n {showYearSeparator && (\n <div className=\"flex items-center gap-3 py-3 px-5\">\n <div className=\"h-px flex-1 bg-border\" />\n <span className=\"text-xs font-semibold text-muted-foreground\">\n {t('customers.activities.yearSeparator', '{year}', { year: activityYear })}\n </span>\n <div className=\"h-px flex-1 bg-border\" />\n </div>\n )}\n <TimelineEntry\n activity={activity}\n t={t}\n withBorder={index < activities.length - 1}\n onEdit={onEdit}\n onMarkDone={onMarkDone}\n />\n </React.Fragment>\n )\n })}\n </div>\n )\n}\n\nfunction TimelineEntry({\n activity,\n t,\n withBorder,\n onEdit,\n onMarkDone,\n}: {\n activity: InteractionSummary\n t: TranslateFn\n withBorder: boolean\n onEdit?: (activity: InteractionSummary) => void\n onMarkDone?: (activityId: string) => void | Promise<void>\n}) {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n const title = activity.title ?? activity.body ?? activity.interactionType\n const duration = activity.duration ? ` (${activity.duration} min)` : ''\n const isPlanned = activity.status === 'planned'\n const [markingDone, setMarkingDone] = React.useState(false)\n\n const handleMarkDone = React.useCallback(async (event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation()\n if (!onMarkDone || markingDone) return\n setMarkingDone(true)\n try {\n await onMarkDone(activity.id)\n } finally {\n setMarkingDone(false)\n }\n }, [activity.id, markingDone, onMarkDone])\n\n return (\n <div\n className={`py-2.5 ${withBorder ? 'border-b border-border/60' : ''} ${onEdit ? 'cursor-pointer hover:bg-accent/40 transition-colors' : ''}`}\n onClick={() => onEdit?.(activity)}\n role={onEdit ? 'button' : undefined}\n tabIndex={onEdit ? 0 : undefined}\n onKeyDown={onEdit ? (e) => { if (e.key === 'Enter') onEdit(activity) } : undefined}\n >\n <div className=\"grid items-start gap-3\" style={{ gridTemplateColumns: '75px 32px 1fr' }}>\n {/* Column 1: Date */}\n <div className=\"shrink-0\">\n <span className=\"block text-[11px] font-semibold leading-tight text-foreground\">\n {formatRelativeDate(dateStr, t)}\n </span>\n <span className=\"block text-[10px] leading-tight text-muted-foreground\">\n {formatTime(dateStr)}\n </span>\n </div>\n\n {/* Column 2: Type icon */}\n <div className=\"flex size-8 items-center justify-center rounded-md bg-muted shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-3.5 text-muted-foreground\" /> : null}\n </div>\n\n {/* Column 3: Content */}\n <div className=\"min-w-0 space-y-1.5\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"block text-[12px] font-semibold leading-tight text-foreground\">\n {title}{duration}\n </span>\n {isPlanned && onMarkDone ? (\n <Button\n type=\"button\"\n variant=\"default\"\n size=\"sm\"\n disabled={markingDone}\n onClick={handleMarkDone}\n className=\"shrink-0\"\n >\n <Check className=\"size-3.5\" />\n {t('customers.activities.actions.markDone', 'Mark done')}\n </Button>\n ) : null}\n </div>\n\n {activity.body && activity.title && (\n <p className=\"text-[11px] leading-snug text-muted-foreground\">\n {activity.body}\n </p>\n )}\n\n {activity.authorName && (\n <div className=\"flex items-center gap-1 text-[10px] text-muted-foreground\">\n <User className=\"size-2.5 shrink-0\" />\n <span>{t('customers.timeline.author', 'by {{name}}', { name: activity.authorName })}</span>\n </div>\n )}\n\n <AiActionChips activityType={activity.interactionType} />\n </div>\n </div>\n </div>\n )\n}\n\nfunction formatRelativeDate(isoString: string, t: TranslateFn): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n // Compare calendar dates (not time-based diff) to correctly handle same-day future times\n const dateDay = new Date(date.getFullYear(), date.getMonth(), date.getDate())\n const nowDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const diffDays = Math.round((nowDay.getTime() - dateDay.getTime()) / (1000 * 60 * 60 * 24))\n\n if (diffDays === 0) return t('customers.timeline.date.today', 'today')\n if (diffDays === 1) return t('customers.timeline.date.yesterday', 'yesterday')\n return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n } catch {\n return ''\n }\n}\n\nfunction formatTime(isoString: string): string {\n try {\n return new Date(isoString).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n } catch {\n return ''\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { Phone, Mail, Users, StickyNote, SlidersHorizontal } from "lucide-react";
4
+ import { Phone, Mail, Users, StickyNote, ListTodo, SlidersHorizontal } from "lucide-react";
5
5
  import { cn } from "@open-mercato/shared/lib/utils";
6
6
  import { useT } from "@open-mercato/shared/lib/i18n/context";
7
7
  import { Button } from "@open-mercato/ui/primitives/button";
@@ -12,7 +12,8 @@ const FILTER_TYPES = [
12
12
  { type: "note", icon: StickyNote },
13
13
  { type: "call", icon: Phone },
14
14
  { type: "meeting", icon: Users },
15
- { type: "email", icon: Mail }
15
+ { type: "email", icon: Mail },
16
+ { type: "task", icon: ListTodo }
16
17
  ];
17
18
  const CHIP_BASE = "inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors";
18
19
  const CHIP_INACTIVE = "border border-border bg-card text-muted-foreground hover:bg-accent/40";
@@ -36,11 +37,19 @@ function ActivityTimelineFilters({
36
37
  const controller = new AbortController();
37
38
  void (async () => {
38
39
  try {
39
- const nextCounts = await readApiResultOrThrow(
40
+ const payload = await readApiResultOrThrow(
40
41
  `/api/customers/interactions/counts?entityId=${encodeURIComponent(entityId)}`,
41
42
  { signal: controller.signal }
42
43
  );
43
- setCounts(nextCounts);
44
+ const source = payload.result ?? payload;
45
+ setCounts({
46
+ call: source.call ?? 0,
47
+ email: source.email ?? 0,
48
+ meeting: source.meeting ?? 0,
49
+ note: source.note ?? 0,
50
+ task: source.task ?? 0,
51
+ total: source.total ?? 0
52
+ });
44
53
  } catch {
45
54
  setCounts(null);
46
55
  }
@@ -60,9 +69,11 @@ function ActivityTimelineFilters({
60
69
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
61
70
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2.5", children: [
62
71
  /* @__PURE__ */ jsx(
63
- "button",
72
+ Button,
64
73
  {
65
74
  type: "button",
75
+ variant: "ghost",
76
+ size: "sm",
66
77
  onClick: handleSelectAll,
67
78
  "aria-pressed": allActive,
68
79
  className: cn(CHIP_BASE, allActive ? CHIP_ACTIVE : CHIP_INACTIVE),
@@ -74,9 +85,11 @@ function ActivityTimelineFilters({
74
85
  const count = counts?.[type];
75
86
  const hasCount = typeof count === "number" && count > 0;
76
87
  return /* @__PURE__ */ jsxs(
77
- "button",
88
+ Button,
78
89
  {
79
90
  type: "button",
91
+ variant: "ghost",
92
+ size: "sm",
80
93
  onClick: () => handleTypeToggle(type),
81
94
  "aria-pressed": isActive,
82
95
  className: cn(CHIP_BASE, isActive ? CHIP_ACTIVE : CHIP_INACTIVE),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ActivityTimelineFilters.tsx"],
4
- "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, SlidersHorizontal } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst FILTER_TYPES = [\n { type: 'note', icon: StickyNote },\n { type: 'call', icon: Phone },\n { type: 'meeting', icon: Users },\n { type: 'email', icon: Mail },\n] as const\n\ntype InteractionCounts = {\n call: number\n email: number\n meeting: number\n note: number\n total: number\n}\n\ninterface ActivityTimelineFiltersProps {\n entityId: string | null\n activeTypes: string[]\n dateFrom: string\n dateTo: string\n onTypesChange: (types: string[]) => void\n onDateFromChange: (value: string) => void\n onDateToChange: (value: string) => void\n onReset: () => void\n}\n\nconst CHIP_BASE = 'inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors'\nconst CHIP_INACTIVE = 'border border-border bg-card text-muted-foreground hover:bg-accent/40'\nconst CHIP_ACTIVE = 'border border-status-info-border bg-status-info-bg text-status-info-text'\n\nexport function ActivityTimelineFilters({\n entityId,\n activeTypes,\n dateFrom,\n dateTo,\n onTypesChange,\n onDateFromChange,\n onDateToChange,\n onReset,\n}: ActivityTimelineFiltersProps) {\n const t = useT()\n const hasActiveFilters = activeTypes.length > 0 || dateFrom || dateTo\n const allActive = activeTypes.length === 0\n const [counts, setCounts] = React.useState<InteractionCounts | null>(null)\n\n React.useEffect(() => {\n if (!entityId) return\n const controller = new AbortController()\n void (async () => {\n try {\n const nextCounts = await readApiResultOrThrow<InteractionCounts>(\n `/api/customers/interactions/counts?entityId=${encodeURIComponent(entityId)}`,\n { signal: controller.signal },\n )\n setCounts(nextCounts)\n } catch {\n setCounts(null)\n }\n })()\n return () => controller.abort()\n }, [entityId])\n\n const handleTypeToggle = React.useCallback((type: string) => {\n if (activeTypes.includes(type)) {\n onTypesChange(activeTypes.filter((filterType) => filterType !== type))\n } else {\n onTypesChange([...activeTypes, type])\n }\n }, [activeTypes, onTypesChange])\n\n const handleSelectAll = React.useCallback(() => {\n onTypesChange([])\n }, [onTypesChange])\n\n return (\n <div className=\"flex flex-col gap-3 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex flex-wrap items-center gap-2.5\">\n <button\n type=\"button\"\n onClick={handleSelectAll}\n aria-pressed={allActive}\n className={cn(CHIP_BASE, allActive ? CHIP_ACTIVE : CHIP_INACTIVE)}\n >\n <span>{t('customers.timeline.filter.all', 'All Activities')}</span>\n </button>\n\n {FILTER_TYPES.map(({ type, icon: Icon }) => {\n const isActive = activeTypes.includes(type)\n const count = counts?.[type as keyof InteractionCounts]\n const hasCount = typeof count === 'number' && count > 0\n return (\n <button\n key={type}\n type=\"button\"\n onClick={() => handleTypeToggle(type)}\n aria-pressed={isActive}\n className={cn(CHIP_BASE, isActive ? CHIP_ACTIVE : CHIP_INACTIVE)}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span>\n {t(`customers.timeline.filter.${type}`, type)}\n {hasCount ? ` ${count}` : ''}\n </span>\n </button>\n )\n })}\n </div>\n\n <Popover>\n <PopoverTrigger asChild>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"size-7 rounded-md text-muted-foreground\"\n aria-label={t('customers.people.detail.activities.moreFilters', 'More filters')}\n >\n <SlidersHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-72 space-y-3\">\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium\">\n {t('customers.activities.filters.dateRange', 'Date range')}\n </label>\n <div className=\"flex items-center gap-2\">\n <input\n type=\"date\"\n value={dateFrom}\n onChange={(event) => onDateFromChange(event.target.value)}\n className=\"h-8 w-full rounded-md border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={t('customers.timeline.filter.from', 'From date')}\n />\n <span className=\"shrink-0 text-xs text-muted-foreground\">\u2014</span>\n <input\n type=\"date\"\n value={dateTo}\n onChange={(event) => onDateToChange(event.target.value)}\n className=\"h-8 w-full rounded-md border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={t('customers.timeline.filter.to', 'To date')}\n />\n </div>\n </div>\n\n {hasActiveFilters ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onReset}\n className=\"h-7 w-full text-xs\"\n >\n {t('customers.activities.filters.clearAll', 'Clear filters')}\n </Button>\n ) : null}\n </PopoverContent>\n </Popover>\n </div>\n )\n}\n"],
5
- "mappings": ";AA6FU,cAgBI,YAhBJ;AA5FV,YAAY,WAAW;AACvB,SAAS,OAAO,MAAM,OAAO,YAAY,yBAAyB;AAClE,SAAS,UAAU;AACnB,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,4BAA4B;AAErC,MAAM,eAAe;AAAA,EACnB,EAAE,MAAM,QAAQ,MAAM,WAAW;AAAA,EACjC,EAAE,MAAM,QAAQ,MAAM,MAAM;AAAA,EAC5B,EAAE,MAAM,WAAW,MAAM,MAAM;AAAA,EAC/B,EAAE,MAAM,SAAS,MAAM,KAAK;AAC9B;AAqBA,MAAM,YAAY;AAClB,MAAM,gBAAgB;AACtB,MAAM,cAAc;AAEb,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiC;AAC/B,QAAM,IAAI,KAAK;AACf,QAAM,mBAAmB,YAAY,SAAS,KAAK,YAAY;AAC/D,QAAM,YAAY,YAAY,WAAW;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAmC,IAAI;AAEzE,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,SAAU;AACf,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,aAAa,MAAM;AAAA,UACvB,+CAA+C,mBAAmB,QAAQ,CAAC;AAAA,UAC3E,EAAE,QAAQ,WAAW,OAAO;AAAA,QAC9B;AACA,kBAAU,UAAU;AAAA,MACtB,QAAQ;AACN,kBAAU,IAAI;AAAA,MAChB;AAAA,IACF,GAAG;AACH,WAAO,MAAM,WAAW,MAAM;AAAA,EAChC,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,mBAAmB,MAAM,YAAY,CAAC,SAAiB;AAC3D,QAAI,YAAY,SAAS,IAAI,GAAG;AAC9B,oBAAc,YAAY,OAAO,CAAC,eAAe,eAAe,IAAI,CAAC;AAAA,IACvE,OAAO;AACL,oBAAc,CAAC,GAAG,aAAa,IAAI,CAAC;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,aAAa,aAAa,CAAC;AAE/B,QAAM,kBAAkB,MAAM,YAAY,MAAM;AAC9C,kBAAc,CAAC,CAAC;AAAA,EAClB,GAAG,CAAC,aAAa,CAAC;AAElB,SACE,qBAAC,SAAI,WAAU,sEACb;AAAA,yBAAC,SAAI,WAAU,uCACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,gBAAc;AAAA,UACd,WAAW,GAAG,WAAW,YAAY,cAAc,aAAa;AAAA,UAEhE,8BAAC,UAAM,YAAE,iCAAiC,gBAAgB,GAAE;AAAA;AAAA,MAC9D;AAAA,MAEC,aAAa,IAAI,CAAC,EAAE,MAAM,MAAM,KAAK,MAAM;AAC1C,cAAM,WAAW,YAAY,SAAS,IAAI;AAC1C,cAAM,QAAQ,SAAS,IAA+B;AACtD,cAAM,WAAW,OAAO,UAAU,YAAY,QAAQ;AACtD,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAS,MAAM,iBAAiB,IAAI;AAAA,YACpC,gBAAc;AAAA,YACd,WAAW,GAAG,WAAW,WAAW,cAAc,aAAa;AAAA,YAE/D;AAAA,kCAAC,QAAK,WAAU,wBAAuB;AAAA,cACvC,qBAAC,UACE;AAAA,kBAAE,6BAA6B,IAAI,IAAI,IAAI;AAAA,gBAC3C,WAAW,IAAI,KAAK,KAAK;AAAA,iBAC5B;AAAA;AAAA;AAAA,UAVK;AAAA,QAWP;AAAA,MAEJ,CAAC;AAAA,OACH;AAAA,IAEA,qBAAC,WACC;AAAA,0BAAC,kBAAe,SAAO,MACrB;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,WAAU;AAAA,UACV,cAAY,EAAE,kDAAkD,cAAc;AAAA,UAE9E,8BAAC,qBAAkB,WAAU,YAAW;AAAA;AAAA,MAC1C,GACF;AAAA,MACA,qBAAC,kBAAe,OAAM,OAAM,WAAU,kBACpC;AAAA,6BAAC,SAAI,WAAU,eACb;AAAA,8BAAC,WAAM,WAAU,uBACd,YAAE,0CAA0C,YAAY,GAC3D;AAAA,UACA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,OAAO;AAAA,gBACP,UAAU,CAAC,UAAU,iBAAiB,MAAM,OAAO,KAAK;AAAA,gBACxD,WAAU;AAAA,gBACV,cAAY,EAAE,kCAAkC,WAAW;AAAA;AAAA,YAC7D;AAAA,YACA,oBAAC,UAAK,WAAU,0CAAyC,oBAAC;AAAA,YAC1D;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,OAAO;AAAA,gBACP,UAAU,CAAC,UAAU,eAAe,MAAM,OAAO,KAAK;AAAA,gBACtD,WAAU;AAAA,gBACV,cAAY,EAAE,gCAAgC,SAAS;AAAA;AAAA,YACzD;AAAA,aACF;AAAA,WACF;AAAA,QAEC,mBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YAET,YAAE,yCAAyC,eAAe;AAAA;AAAA,QAC7D,IACE;AAAA,SACN;AAAA,OACF;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, ListTodo, SlidersHorizontal } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst FILTER_TYPES = [\n { type: 'note', icon: StickyNote },\n { type: 'call', icon: Phone },\n { type: 'meeting', icon: Users },\n { type: 'email', icon: Mail },\n { type: 'task', icon: ListTodo },\n] as const\n\ntype InteractionCounts = {\n call: number\n email: number\n meeting: number\n note: number\n task: number\n total: number\n}\n\ntype InteractionCountsResponse = {\n ok?: boolean\n result?: InteractionCounts\n} & Partial<InteractionCounts>\n\ninterface ActivityTimelineFiltersProps {\n entityId: string | null\n activeTypes: string[]\n dateFrom: string\n dateTo: string\n onTypesChange: (types: string[]) => void\n onDateFromChange: (value: string) => void\n onDateToChange: (value: string) => void\n onReset: () => void\n}\n\nconst CHIP_BASE = 'inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors'\nconst CHIP_INACTIVE = 'border border-border bg-card text-muted-foreground hover:bg-accent/40'\nconst CHIP_ACTIVE = 'border border-status-info-border bg-status-info-bg text-status-info-text'\n\nexport function ActivityTimelineFilters({\n entityId,\n activeTypes,\n dateFrom,\n dateTo,\n onTypesChange,\n onDateFromChange,\n onDateToChange,\n onReset,\n}: ActivityTimelineFiltersProps) {\n const t = useT()\n const hasActiveFilters = activeTypes.length > 0 || dateFrom || dateTo\n const allActive = activeTypes.length === 0\n const [counts, setCounts] = React.useState<InteractionCounts | null>(null)\n\n React.useEffect(() => {\n if (!entityId) return\n const controller = new AbortController()\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<InteractionCountsResponse>(\n `/api/customers/interactions/counts?entityId=${encodeURIComponent(entityId)}`,\n { signal: controller.signal },\n )\n // Endpoint envelope is `{ ok, result: {...counts} }`. Some legacy fixtures\n // return the counts at the top level \u2014 fall back to that shape so the chip\n // badges keep working in either case.\n const source = (payload.result ?? payload) as Partial<InteractionCounts>\n setCounts({\n call: source.call ?? 0,\n email: source.email ?? 0,\n meeting: source.meeting ?? 0,\n note: source.note ?? 0,\n task: source.task ?? 0,\n total: source.total ?? 0,\n })\n } catch {\n setCounts(null)\n }\n })()\n return () => controller.abort()\n }, [entityId])\n\n const handleTypeToggle = React.useCallback((type: string) => {\n if (activeTypes.includes(type)) {\n onTypesChange(activeTypes.filter((filterType) => filterType !== type))\n } else {\n onTypesChange([...activeTypes, type])\n }\n }, [activeTypes, onTypesChange])\n\n const handleSelectAll = React.useCallback(() => {\n onTypesChange([])\n }, [onTypesChange])\n\n return (\n <div className=\"flex flex-col gap-3 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex flex-wrap items-center gap-2.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleSelectAll}\n aria-pressed={allActive}\n className={cn(CHIP_BASE, allActive ? CHIP_ACTIVE : CHIP_INACTIVE)}\n >\n <span>{t('customers.timeline.filter.all', 'All Activities')}</span>\n </Button>\n\n {FILTER_TYPES.map(({ type, icon: Icon }) => {\n const isActive = activeTypes.includes(type)\n const count = counts?.[type as keyof InteractionCounts]\n const hasCount = typeof count === 'number' && count > 0\n return (\n <Button\n key={type}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => handleTypeToggle(type)}\n aria-pressed={isActive}\n className={cn(CHIP_BASE, isActive ? CHIP_ACTIVE : CHIP_INACTIVE)}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span>\n {t(`customers.timeline.filter.${type}`, type)}\n {hasCount ? ` ${count}` : ''}\n </span>\n </Button>\n )\n })}\n </div>\n\n <Popover>\n <PopoverTrigger asChild>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"size-7 rounded-md text-muted-foreground\"\n aria-label={t('customers.people.detail.activities.moreFilters', 'More filters')}\n >\n <SlidersHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-72 space-y-3\">\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium\">\n {t('customers.activities.filters.dateRange', 'Date range')}\n </label>\n <div className=\"flex items-center gap-2\">\n <input\n type=\"date\"\n value={dateFrom}\n onChange={(event) => onDateFromChange(event.target.value)}\n className=\"h-8 w-full rounded-md border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={t('customers.timeline.filter.from', 'From date')}\n />\n <span className=\"shrink-0 text-xs text-muted-foreground\">\u2014</span>\n <input\n type=\"date\"\n value={dateTo}\n onChange={(event) => onDateToChange(event.target.value)}\n className=\"h-8 w-full rounded-md border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={t('customers.timeline.filter.to', 'To date')}\n />\n </div>\n </div>\n\n {hasActiveFilters ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onReset}\n className=\"h-7 w-full text-xs\"\n >\n {t('customers.activities.filters.clearAll', 'Clear filters')}\n </Button>\n ) : null}\n </PopoverContent>\n </Popover>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { Phone, Mail, Users, StickyNote } from "lucide-react";
3
+ import { ListTodo, Mail, Phone, StickyNote, Users } from "lucide-react";
4
4
  import { cn } from "@open-mercato/shared/lib/utils";
5
5
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
6
  import { Button } from "@open-mercato/ui/primitives/button";
@@ -8,11 +8,12 @@ const ACTIVITY_TYPES = [
8
8
  { type: "call", icon: Phone, labelKey: "customers.activityComposer.types.call", fallback: "Call" },
9
9
  { type: "email", icon: Mail, labelKey: "customers.activityComposer.types.email", fallback: "Email" },
10
10
  { type: "meeting", icon: Users, labelKey: "customers.activityComposer.types.meeting", fallback: "Meeting" },
11
- { type: "note", icon: StickyNote, labelKey: "customers.activityComposer.types.note", fallback: "Note" }
11
+ { type: "note", icon: StickyNote, labelKey: "customers.activityComposer.types.note", fallback: "Note" },
12
+ { type: "task", icon: ListTodo, labelKey: "customers.activityComposer.types.task", fallback: "Task" }
12
13
  ];
13
14
  function ActivityTypeSelector({ selectedType, onSelect }) {
14
15
  const t = useT();
15
- return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-2", children: ACTIVITY_TYPES.map(({ type, icon: Icon, labelKey, fallback }) => {
16
+ return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-5 gap-2", children: ACTIVITY_TYPES.map(({ type, icon: Icon, labelKey, fallback }) => {
16
17
  const isSelected = selectedType === type;
17
18
  return /* @__PURE__ */ jsxs(
18
19
  Button,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ActivityTypeSelector.tsx"],
4
- "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\n\nconst ACTIVITY_TYPES = [\n { type: 'call', icon: Phone, labelKey: 'customers.activityComposer.types.call', fallback: 'Call' },\n { type: 'email', icon: Mail, labelKey: 'customers.activityComposer.types.email', fallback: 'Email' },\n { type: 'meeting', icon: Users, labelKey: 'customers.activityComposer.types.meeting', fallback: 'Meeting' },\n { type: 'note', icon: StickyNote, labelKey: 'customers.activityComposer.types.note', fallback: 'Note' },\n] as const\n\nexport type ActivityType = (typeof ACTIVITY_TYPES)[number]['type']\n\ninterface ActivityTypeSelectorProps {\n selectedType: ActivityType | null\n onSelect: (type: ActivityType) => void\n}\n\nexport function ActivityTypeSelector({ selectedType, onSelect }: ActivityTypeSelectorProps) {\n const t = useT()\n\n return (\n <div className=\"grid grid-cols-4 gap-2\">\n {ACTIVITY_TYPES.map(({ type, icon: Icon, labelKey, fallback }) => {\n const isSelected = selectedType === type\n return (\n <Button\n key={type}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onSelect(type)}\n aria-pressed={isSelected}\n className={cn(\n 'h-10 gap-2 rounded-lg',\n isSelected\n ? 'border-foreground bg-background text-foreground shadow-sm'\n : 'border-border text-muted-foreground',\n )}\n >\n <Icon className=\"size-4\" />\n {t(labelKey, fallback)}\n </Button>\n )\n })}\n </div>\n )\n}\n\nexport { ACTIVITY_TYPES }\n"],
5
- "mappings": ";AA6BU,SAcE,KAdF;AA3BV,SAAS,OAAO,MAAM,OAAO,kBAAkB;AAC/C,SAAS,UAAU;AACnB,SAAS,YAAY;AACrB,SAAS,cAAc;AAEvB,MAAM,iBAAiB;AAAA,EACrB,EAAE,MAAM,QAAQ,MAAM,OAAO,UAAU,yCAAyC,UAAU,OAAO;AAAA,EACjG,EAAE,MAAM,SAAS,MAAM,MAAM,UAAU,0CAA0C,UAAU,QAAQ;AAAA,EACnG,EAAE,MAAM,WAAW,MAAM,OAAO,UAAU,4CAA4C,UAAU,UAAU;AAAA,EAC1G,EAAE,MAAM,QAAQ,MAAM,YAAY,UAAU,yCAAyC,UAAU,OAAO;AACxG;AASO,SAAS,qBAAqB,EAAE,cAAc,SAAS,GAA8B;AAC1F,QAAM,IAAI,KAAK;AAEf,SACE,oBAAC,SAAI,WAAU,0BACZ,yBAAe,IAAI,CAAC,EAAE,MAAM,MAAM,MAAM,UAAU,SAAS,MAAM;AAChE,UAAM,aAAa,iBAAiB;AACpC,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS,MAAM,SAAS,IAAI;AAAA,QAC5B,gBAAc;AAAA,QACd,WAAW;AAAA,UACT;AAAA,UACA,aACI,8DACA;AAAA,QACN;AAAA,QAEA;AAAA,8BAAC,QAAK,WAAU,UAAS;AAAA,UACxB,EAAE,UAAU,QAAQ;AAAA;AAAA;AAAA,MAdhB;AAAA,IAeP;AAAA,EAEJ,CAAC,GACH;AAEJ;",
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { ListTodo, Mail, Phone, StickyNote, Users } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\n\nconst ACTIVITY_TYPES = [\n { type: 'call', icon: Phone, labelKey: 'customers.activityComposer.types.call', fallback: 'Call' },\n { type: 'email', icon: Mail, labelKey: 'customers.activityComposer.types.email', fallback: 'Email' },\n { type: 'meeting', icon: Users, labelKey: 'customers.activityComposer.types.meeting', fallback: 'Meeting' },\n { type: 'note', icon: StickyNote, labelKey: 'customers.activityComposer.types.note', fallback: 'Note' },\n { type: 'task', icon: ListTodo, labelKey: 'customers.activityComposer.types.task', fallback: 'Task' },\n] as const\n\nexport type ActivityType = (typeof ACTIVITY_TYPES)[number]['type']\n\ninterface ActivityTypeSelectorProps {\n selectedType: ActivityType | null\n onSelect: (type: ActivityType) => void\n}\n\nexport function ActivityTypeSelector({ selectedType, onSelect }: ActivityTypeSelectorProps) {\n const t = useT()\n\n return (\n <div className=\"grid grid-cols-5 gap-2\">\n {ACTIVITY_TYPES.map(({ type, icon: Icon, labelKey, fallback }) => {\n const isSelected = selectedType === type\n return (\n <Button\n key={type}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onSelect(type)}\n aria-pressed={isSelected}\n className={cn(\n 'h-10 gap-2 rounded-lg',\n isSelected\n ? 'border-foreground bg-background text-foreground shadow-sm'\n : 'border-border text-muted-foreground',\n )}\n >\n <Icon className=\"size-4\" />\n {t(labelKey, fallback)}\n </Button>\n )\n })}\n </div>\n )\n}\n\nexport { ACTIVITY_TYPES }\n"],
5
+ "mappings": ";AA8BU,SAcE,KAdF;AA5BV,SAAS,UAAU,MAAM,OAAO,YAAY,aAAa;AACzD,SAAS,UAAU;AACnB,SAAS,YAAY;AACrB,SAAS,cAAc;AAEvB,MAAM,iBAAiB;AAAA,EACrB,EAAE,MAAM,QAAQ,MAAM,OAAO,UAAU,yCAAyC,UAAU,OAAO;AAAA,EACjG,EAAE,MAAM,SAAS,MAAM,MAAM,UAAU,0CAA0C,UAAU,QAAQ;AAAA,EACnG,EAAE,MAAM,WAAW,MAAM,OAAO,UAAU,4CAA4C,UAAU,UAAU;AAAA,EAC1G,EAAE,MAAM,QAAQ,MAAM,YAAY,UAAU,yCAAyC,UAAU,OAAO;AAAA,EACtG,EAAE,MAAM,QAAQ,MAAM,UAAU,UAAU,yCAAyC,UAAU,OAAO;AACtG;AASO,SAAS,qBAAqB,EAAE,cAAc,SAAS,GAA8B;AAC1F,QAAM,IAAI,KAAK;AAEf,SACE,oBAAC,SAAI,WAAU,0BACZ,yBAAe,IAAI,CAAC,EAAE,MAAM,MAAM,MAAM,UAAU,SAAS,MAAM;AAChE,UAAM,aAAa,iBAAiB;AACpC,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS,MAAM,SAAS,IAAI;AAAA,QAC5B,gBAAc;AAAA,QACd,WAAW;AAAA,UACT;AAAA,UACA,aACI,8DACA;AAAA,QACN;AAAA,QAEA;AAAA,8BAAC,QAAK,WAAU,UAAS;AAAA,UACxB,EAAE,UAAU,QAAQ;AAAA;AAAA;AAAA,MAdhB;AAAA,IAeP;AAAA,EAEJ,CAAC,GACH;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -4,7 +4,9 @@ import * as React from "react";
4
4
  import { Users, Phone, Check, Mail, Calendar, AlertTriangle, X } from "lucide-react";
5
5
  import { cn } from "@open-mercato/shared/lib/utils";
6
6
  import { useT } from "@open-mercato/shared/lib/i18n/context";
7
+ import { validatePhoneNumber } from "@open-mercato/shared/lib/phone";
7
8
  import { apiCallOrThrow, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
9
+ import { mapCrudServerErrorToFormErrors } from "@open-mercato/ui/backend/utils/serverErrors";
8
10
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
9
11
  import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
10
12
  import { Alert, AlertDescription, AlertTitle } from "@open-mercato/ui/primitives/alert";
@@ -12,7 +14,7 @@ import { Button } from "@open-mercato/ui/primitives/button";
12
14
  import { IconButton } from "@open-mercato/ui/primitives/icon-button";
13
15
  import { Dialog, DialogContent, DialogTitle } from "@open-mercato/ui/primitives/dialog";
14
16
  import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
15
- import { SwitchableMarkdownInput } from "@open-mercato/ui/backend/inputs";
17
+ import { PhoneNumberField, SwitchableMarkdownInput } from "@open-mercato/ui/backend/inputs";
16
18
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
17
19
  import {
18
20
  useScheduleFormState,
@@ -106,14 +108,31 @@ function ScheduleActivityDialog({
106
108
  const [callDirection, setCallDirection] = React.useState("outbound");
107
109
  const [callOutcome, setCallOutcome] = React.useState(null);
108
110
  const [callPhoneNumber, setCallPhoneNumber] = React.useState("");
111
+ const [callPhoneError, setCallPhoneError] = React.useState(null);
109
112
  const [taskPriority, setTaskPriority] = React.useState("medium");
113
+ const callPhoneInvalidMessage = React.useMemo(
114
+ () => t(
115
+ "customers.activities.errors.phoneInvalid",
116
+ "Enter a valid phone number with country code (e.g. +1 212 555 1234)"
117
+ ),
118
+ [t]
119
+ );
120
+ const translateErrorMessage = React.useCallback(
121
+ (message, fallback) => {
122
+ const key = typeof message === "string" ? message.trim() : "";
123
+ return key ? t(key, key) : fallback;
124
+ },
125
+ [t]
126
+ );
110
127
  React.useEffect(() => {
111
128
  if (!open) return;
112
129
  const raw = editData;
113
130
  const cv = raw?.customValues && typeof raw.customValues === "object" ? raw.customValues : null;
114
131
  setCallDirection(typeof cv?.callDirection === "string" && cv.callDirection === "inbound" ? "inbound" : "outbound");
115
132
  setCallOutcome(typeof cv?.callOutcome === "string" ? cv.callOutcome : null);
116
- setCallPhoneNumber(typeof cv?.callPhoneNumber === "string" ? cv.callPhoneNumber : "");
133
+ const seededPhone = typeof raw?.phoneNumber === "string" && raw.phoneNumber.trim().length > 0 ? raw.phoneNumber : typeof cv?.callPhoneNumber === "string" ? cv.callPhoneNumber : "";
134
+ setCallPhoneNumber(seededPhone);
135
+ setCallPhoneError(null);
117
136
  setTaskPriority(typeof cv?.taskPriority === "string" ? cv.taskPriority : "medium");
118
137
  }, [open, editData]);
119
138
  React.useEffect(() => {
@@ -121,8 +140,13 @@ function ScheduleActivityDialog({
121
140
  setCallDirection("outbound");
122
141
  setCallOutcome(null);
123
142
  setCallPhoneNumber("");
143
+ setCallPhoneError(null);
124
144
  setTaskPriority("medium");
125
145
  }, [state.activityType, open, isEditing]);
146
+ const handleCallPhoneChange = React.useCallback((next) => {
147
+ setCallPhoneNumber(next ?? "");
148
+ setCallPhoneError(null);
149
+ }, []);
126
150
  const formSnapshot = React.useMemo(() => JSON.stringify({
127
151
  activityType: state.activityType,
128
152
  title: state.title,
@@ -264,8 +288,36 @@ function ScheduleActivityDialog({
264
288
  }, 500);
265
289
  return () => clearTimeout(timer);
266
290
  }, [editData?.id, open, state.date, state.startTime, state.duration, state.allDay, t]);
291
+ const trimmedDate = state.date.trim();
292
+ const trimmedStartTime = state.startTime.trim();
293
+ const trimmedCallPhone = callPhoneNumber.trim();
294
+ const isDateMissing = !trimmedDate;
295
+ const isTimeMissing = !state.allDay && !trimmedStartTime;
296
+ const isSubmitDisabled = state.saving || !state.title.trim() || isDateMissing || isTimeMissing;
267
297
  const handleSave = React.useCallback(async () => {
268
298
  if (!state.title.trim()) return;
299
+ if (isDateMissing) {
300
+ flash(t("customers.activities.errors.dateRequired", "Date is required"), "error");
301
+ return;
302
+ }
303
+ if (isTimeMissing) {
304
+ flash(t("customers.activities.errors.timeRequired", "Time is required"), "error");
305
+ return;
306
+ }
307
+ let phoneNumberForPayload;
308
+ if (state.activityType === "call" && trimmedCallPhone) {
309
+ const phoneValidation = validatePhoneNumber(trimmedCallPhone);
310
+ if (!phoneValidation.valid) {
311
+ setCallPhoneError(callPhoneInvalidMessage);
312
+ flash(callPhoneInvalidMessage, "error");
313
+ return;
314
+ }
315
+ phoneNumberForPayload = phoneValidation.normalized ?? trimmedCallPhone;
316
+ setCallPhoneError(null);
317
+ if (phoneNumberForPayload !== callPhoneNumber) {
318
+ setCallPhoneNumber(phoneNumberForPayload);
319
+ }
320
+ }
269
321
  state.setSaving(true);
270
322
  try {
271
323
  const scheduledAt = state.allDay ? (/* @__PURE__ */ new Date(`${state.date}T00:00:00`)).toISOString() : (/* @__PURE__ */ new Date(`${state.date}T${state.startTime}:00`)).toISOString();
@@ -275,7 +327,7 @@ function ScheduleActivityDialog({
275
327
  if (state.activityType === "call") {
276
328
  customValues.callDirection = callDirection;
277
329
  if (callOutcome) customValues.callOutcome = callOutcome;
278
- if (callPhoneNumber.trim()) customValues.callPhoneNumber = callPhoneNumber.trim();
330
+ if (phoneNumberForPayload) customValues.callPhoneNumber = phoneNumberForPayload;
279
331
  }
280
332
  if (state.activityType === "task") {
281
333
  customValues.taskPriority = taskPriority;
@@ -288,6 +340,9 @@ function ScheduleActivityDialog({
288
340
  title: state.title.trim(),
289
341
  body: state.description.trim() || null,
290
342
  status: "planned",
343
+ date: trimmedDate,
344
+ time: state.allDay ? "00:00" : trimmedStartTime,
345
+ phoneNumber: state.activityType === "call" ? phoneNumberForPayload : void 0,
291
346
  scheduledAt,
292
347
  durationMinutes: visibleFields.has("duration") && !state.allDay ? state.duration : null,
293
348
  location: visibleFields.has("location") ? state.location.trim() || null : null,
@@ -318,12 +373,23 @@ function ScheduleActivityDialog({
318
373
  requestAnimationFrame(() => {
319
374
  onActivityCreated?.();
320
375
  });
321
- } catch {
322
- flash(t("customers.schedule.error", "Failed to schedule activity"), "error");
376
+ } catch (err) {
377
+ const { message, fieldErrors } = mapCrudServerErrorToFormErrors(err);
378
+ const phoneFieldError = fieldErrors?.phoneNumber;
379
+ if (state.activityType === "call" && phoneFieldError) {
380
+ const translatedPhoneError = translateErrorMessage(phoneFieldError, callPhoneInvalidMessage);
381
+ setCallPhoneError(translatedPhoneError);
382
+ flash(translatedPhoneError, "error");
383
+ return;
384
+ }
385
+ flash(
386
+ translateErrorMessage(message, t("customers.schedule.error", "Failed to schedule activity")),
387
+ "error"
388
+ );
323
389
  } finally {
324
390
  state.setSaving(false);
325
391
  }
326
- }, [state.activityType, state.allDay, state.date, state.description, dealId, state.duration, editData, entityId, state.guestPermissions, state.linkedEntities, state.location, onActivityCreated, onClose, state.participants, state.recurrenceCount, state.recurrenceDays, state.recurrenceEnabled, state.recurrenceEndDate, state.recurrenceEndType, state.reminderMinutes, runGuardedMutation, state.startTime, t, state.title, state.visibility, visibleFields]);
392
+ }, [callDirection, callOutcome, callPhoneInvalidMessage, callPhoneNumber, isDateMissing, isTimeMissing, state.activityType, state.allDay, state.date, state.description, dealId, state.duration, editData, entityId, state.guestPermissions, state.linkedEntities, state.location, onActivityCreated, onClose, state.participants, state.recurrenceCount, state.recurrenceDays, state.recurrenceEnabled, state.recurrenceEndDate, state.recurrenceEndType, state.reminderMinutes, runGuardedMutation, state.startTime, t, taskPriority, state.title, translateErrorMessage, trimmedCallPhone, trimmedDate, trimmedStartTime, state.visibility, visibleFields]);
327
393
  const handleKeyDown = React.useCallback((e) => {
328
394
  if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
329
395
  e.preventDefault();
@@ -498,15 +564,17 @@ function ScheduleActivityDialog({
498
564
  }
499
565
  ),
500
566
  state.activityType === "call" ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
501
- /* @__PURE__ */ jsx("label", { className: "text-overline font-semibold uppercase text-muted-foreground tracking-wider", children: t("customers.schedule.call.phoneLabel", "Phone number") }),
567
+ /* @__PURE__ */ jsx("label", { htmlFor: "schedule-call-phone", className: "text-overline font-semibold uppercase text-muted-foreground tracking-wider", children: t("customers.schedule.call.phoneLabel", "Phone number") }),
502
568
  /* @__PURE__ */ jsx(
503
- "input",
569
+ PhoneNumberField,
504
570
  {
505
- type: "tel",
571
+ id: "schedule-call-phone",
506
572
  value: callPhoneNumber,
507
- onChange: (e) => setCallPhoneNumber(e.target.value),
573
+ onValueChange: handleCallPhoneChange,
508
574
  placeholder: t("customers.schedule.call.phonePlaceholder", "+1 555 000 0000"),
509
- className: "w-full rounded-md border border-border bg-background px-3 py-2.5 text-sm text-foreground outline-none focus:border-foreground"
575
+ externalError: callPhoneError,
576
+ invalidLabel: callPhoneInvalidMessage,
577
+ minDigits: 7
510
578
  }
511
579
  )
512
580
  ] }) : /* @__PURE__ */ jsx(
@@ -556,7 +624,7 @@ function ScheduleActivityDialog({
556
624
  /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => {
557
625
  void guardedClose();
558
626
  }, className: "rounded-md border border-input bg-background px-5 py-3 text-sm font-semibold text-foreground", children: t("customers.schedule.cancel", "Cancel") }),
559
- /* @__PURE__ */ jsxs(Button, { type: "button", onClick: handleSave, disabled: state.saving || !state.title.trim(), className: "flex items-center gap-2 rounded-md bg-foreground px-5 py-3 text-sm font-semibold text-background hover:bg-foreground/90 disabled:opacity-50", children: [
627
+ /* @__PURE__ */ jsxs(Button, { type: "button", onClick: handleSave, disabled: isSubmitDisabled, className: "flex items-center gap-2 rounded-md bg-foreground px-5 py-3 text-sm font-semibold text-background hover:bg-foreground/90 disabled:opacity-50", children: [
560
628
  /* @__PURE__ */ jsx(SaveIcon, { className: "size-3.5" }),
561
629
  state.saving ? t("customers.schedule.saving", "Saving...") : isEditing ? t("customers.schedule.update", "Update activity") : t(chrome.saveKey, chrome.saveFallback)
562
630
  ] })