@open-mercato/core 0.6.1-develop.3081.21270ec58a → 0.6.1-develop.3090.06ab462170

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 (84) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/audit_logs/services/accessLogService.js +13 -0
  3. package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
  4. package/dist/modules/audit_logs/services/actionLogService.js +6 -5
  5. package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
  6. package/dist/modules/customers/api/activities/route.js +1 -52
  7. package/dist/modules/customers/api/activities/route.js.map +2 -2
  8. package/dist/modules/customers/api/interactions/counts/route.js +2 -1
  9. package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
  10. package/dist/modules/customers/api/interactions/route.js +21 -1
  11. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  12. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -3
  13. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  14. package/dist/modules/customers/backend/customers/deals/[id]/page.js +5 -1
  15. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  16. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -3
  17. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  18. package/dist/modules/customers/components/detail/ActivitiesCard.js +62 -6
  19. package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
  20. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +21 -6
  21. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
  22. package/dist/modules/customers/components/detail/ActivitiesSection.js +37 -5
  23. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  24. package/dist/modules/customers/components/detail/ActivityCard.js +69 -17
  25. package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
  26. package/dist/modules/customers/components/detail/ActivityHistorySection.js +94 -34
  27. package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
  28. package/dist/modules/customers/components/detail/ActivityLogTab.js +3 -1
  29. package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
  30. package/dist/modules/customers/components/detail/ActivityTimeline.js +41 -8
  31. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
  32. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +19 -6
  33. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
  34. package/dist/modules/customers/components/detail/ActivityTypeSelector.js +4 -3
  35. package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +2 -2
  36. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +80 -12
  37. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  38. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +65 -10
  39. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  40. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +10 -5
  41. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  42. package/dist/modules/customers/data/validators.js +74 -2
  43. package/dist/modules/customers/data/validators.js.map +2 -2
  44. package/dist/modules/customers/lib/legacyActivityBridge.js +61 -0
  45. package/dist/modules/customers/lib/legacyActivityBridge.js.map +7 -0
  46. package/dist/modules/integrations/lib/credentials-service.js +12 -1
  47. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  48. package/dist/modules/messages/commands/actions.js +29 -14
  49. package/dist/modules/messages/commands/actions.js.map +2 -2
  50. package/dist/modules/messages/lib/actions.js +24 -4
  51. package/dist/modules/messages/lib/actions.js.map +2 -2
  52. package/dist/modules/sales/api/documents/factory.js +49 -36
  53. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  54. package/package.json +5 -5
  55. package/src/modules/audit_logs/services/accessLogService.ts +20 -0
  56. package/src/modules/audit_logs/services/actionLogService.ts +13 -5
  57. package/src/modules/customers/api/activities/route.ts +1 -76
  58. package/src/modules/customers/api/interactions/counts/route.ts +2 -1
  59. package/src/modules/customers/api/interactions/route.ts +28 -1
  60. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +13 -3
  61. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +14 -2
  62. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -3
  63. package/src/modules/customers/components/detail/ActivitiesCard.tsx +92 -5
  64. package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +38 -6
  65. package/src/modules/customers/components/detail/ActivitiesSection.tsx +37 -3
  66. package/src/modules/customers/components/detail/ActivityCard.tsx +79 -14
  67. package/src/modules/customers/components/detail/ActivityHistorySection.tsx +102 -33
  68. package/src/modules/customers/components/detail/ActivityLogTab.tsx +7 -1
  69. package/src/modules/customers/components/detail/ActivityTimeline.tsx +39 -5
  70. package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +29 -7
  71. package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +3 -2
  72. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +96 -13
  73. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +50 -4
  74. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +21 -5
  75. package/src/modules/customers/data/validators.ts +85 -2
  76. package/src/modules/customers/i18n/de.json +11 -0
  77. package/src/modules/customers/i18n/en.json +11 -0
  78. package/src/modules/customers/i18n/es.json +11 -0
  79. package/src/modules/customers/i18n/pl.json +11 -0
  80. package/src/modules/customers/lib/legacyActivityBridge.ts +106 -0
  81. package/src/modules/integrations/lib/credentials-service.ts +15 -1
  82. package/src/modules/messages/commands/actions.ts +28 -13
  83. package/src/modules/messages/lib/actions.ts +34 -3
  84. package/src/modules/sales/api/documents/factory.ts +55 -38
@@ -5,6 +5,13 @@ import { Clock3, Search } from "lucide-react";
5
5
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
6
  import { Button } from "@open-mercato/ui/primitives/button";
7
7
  import { Input } from "@open-mercato/ui/primitives/input";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue
14
+ } from "@open-mercato/ui/primitives/select";
8
15
  import { ErrorMessage, LoadingMessage, TabEmptyState } from "@open-mercato/ui/backend/detail";
9
16
  import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
10
17
  import { ActivityCard } from "./ActivityCard.js";
@@ -12,7 +19,8 @@ const TYPE_FILTERS = [
12
19
  { value: "call", labelKey: "customers.timeline.filter.call", fallback: "Call" },
13
20
  { value: "email", labelKey: "customers.timeline.filter.email", fallback: "Email" },
14
21
  { value: "meeting", labelKey: "customers.timeline.filter.meeting", fallback: "Meeting" },
15
- { value: "note", labelKey: "customers.timeline.filter.note", fallback: "Note" }
22
+ { value: "note", labelKey: "customers.timeline.filter.note", fallback: "Note" },
23
+ { value: "task", labelKey: "customers.timeline.filter.task", fallback: "Task" }
16
24
  ];
17
25
  function computeRangeStart(range) {
18
26
  const date = /* @__PURE__ */ new Date();
@@ -92,11 +100,15 @@ function isWithinRange(activity, start) {
92
100
  if (Number.isNaN(timestamp.getTime())) return false;
93
101
  return timestamp >= start;
94
102
  }
103
+ function isAbortError(error) {
104
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
105
+ }
95
106
  function ActivityHistorySection({
96
107
  entityId,
97
108
  useCanonicalInteractions = false,
98
109
  refreshKey = 0,
99
- onEditActivity
110
+ onEditActivity,
111
+ runMutation
100
112
  }) {
101
113
  const t = useT();
102
114
  const [searchInput, setSearchInput] = React.useState("");
@@ -107,9 +119,14 @@ function ActivityHistorySection({
107
119
  const [activities, setActivities] = React.useState([]);
108
120
  const [loading, setLoading] = React.useState(true);
109
121
  const [error, setError] = React.useState(null);
110
- const [counts, setCounts] = React.useState({ call: 0, email: 0, meeting: 0, note: 0, total: 0 });
122
+ const [counts, setCounts] = React.useState({ call: 0, email: 0, meeting: 0, note: 0, task: 0, total: 0 });
111
123
  const [hasMore, setHasMore] = React.useState(false);
112
124
  const [loadedPages, setLoadedPages] = React.useState(1);
125
+ const [localRefreshKey, setLocalRefreshKey] = React.useState(0);
126
+ const historyRequestSeqRef = React.useRef(0);
127
+ const handleActivityChanged = React.useCallback(() => {
128
+ setLocalRefreshKey((current) => current + 1);
129
+ }, []);
113
130
  React.useEffect(() => {
114
131
  const timeout = window.setTimeout(() => setSearch(searchInput.trim()), 300);
115
132
  return () => window.clearTimeout(timeout);
@@ -119,7 +136,7 @@ function ActivityHistorySection({
119
136
  void (async () => {
120
137
  try {
121
138
  const payload = await readApiResultOrThrow(
122
- `/api/customers/interactions/counts?entityId=${encodeURIComponent(entityId)}&status=done`,
139
+ `/api/customers/interactions/counts?entityId=${encodeURIComponent(entityId)}`,
123
140
  { signal: controller.signal }
124
141
  );
125
142
  const result = payload.result ?? payload;
@@ -128,15 +145,18 @@ function ActivityHistorySection({
128
145
  email: result.email ?? 0,
129
146
  meeting: result.meeting ?? 0,
130
147
  note: result.note ?? 0,
148
+ task: result.task ?? 0,
131
149
  total: result.total ?? 0
132
150
  });
133
151
  } catch {
134
- setCounts({ call: 0, email: 0, meeting: 0, note: 0, total: 0 });
152
+ setCounts({ call: 0, email: 0, meeting: 0, note: 0, task: 0, total: 0 });
135
153
  }
136
154
  })();
137
155
  return () => controller.abort();
138
- }, [entityId, refreshKey]);
139
- const loadHistory = React.useCallback(async () => {
156
+ }, [entityId, refreshKey, localRefreshKey]);
157
+ const loadHistory = React.useCallback(async (options) => {
158
+ const { signal, requestSeq } = options;
159
+ const isStale = () => signal.aborted || requestSeq !== historyRequestSeqRef.current;
140
160
  setLoading(true);
141
161
  setError(null);
142
162
  try {
@@ -146,14 +166,14 @@ function ActivityHistorySection({
146
166
  let nextCursor;
147
167
  let firstPageHasMore = false;
148
168
  let pagesLoaded = 0;
169
+ const taskFilterActive = activeTypes.includes("task");
149
170
  do {
150
171
  const params = new URLSearchParams({
151
172
  entityId,
152
- status: "done",
153
- excludeInteractionType: "task",
154
173
  limit: String(pageSize),
155
174
  from: rangeStart
156
175
  });
176
+ if (!taskFilterActive) params.set("excludeInteractionType", "task");
157
177
  if (activeTypes.length > 0) params.set("type", activeTypes.join(","));
158
178
  if (search) params.set("search", search);
159
179
  if (sortMode === "recent") {
@@ -165,8 +185,10 @@ function ActivityHistorySection({
165
185
  }
166
186
  if (nextCursor) params.set("cursor", nextCursor);
167
187
  const response = await readApiResultOrThrow(
168
- `/api/customers/interactions?${params.toString()}`
188
+ `/api/customers/interactions?${params.toString()}`,
189
+ { signal }
169
190
  );
191
+ if (isStale()) return;
170
192
  const pageItems = Array.isArray(response.items) ? response.items : [];
171
193
  canonicalItems.push(...pageItems);
172
194
  nextCursor = response.nextCursor;
@@ -179,8 +201,10 @@ function ActivityHistorySection({
179
201
  let legacyTotalPages = 1;
180
202
  for (let legacyPage = 1; legacyPage <= loadedPages; legacyPage += 1) {
181
203
  const legacyPayload = await readApiResultOrThrow(
182
- `/api/customers/activities?entityId=${encodeURIComponent(entityId)}&page=${legacyPage}&pageSize=20&sortField=occurredAt&sortDir=desc`
204
+ `/api/customers/activities?entityId=${encodeURIComponent(entityId)}&page=${legacyPage}&pageSize=20&sortField=occurredAt&sortDir=desc`,
205
+ { signal }
183
206
  ).catch(() => ({ items: [], totalPages: 1 }));
207
+ if (isStale()) return;
184
208
  legacyItems.push(...Array.isArray(legacyPayload.items) ? legacyPayload.items.map(normalizeLegacyActivity) : []);
185
209
  legacyTotalPages = typeof legacyPayload.totalPages === "number" ? legacyPayload.totalPages : legacyTotalPages;
186
210
  if (legacyPage >= legacyTotalPages) break;
@@ -198,19 +222,27 @@ function ActivityHistorySection({
198
222
  combined = Array.from(deduped.values());
199
223
  firstPageHasMore = firstPageHasMore || legacyTotalPages > loadedPages;
200
224
  }
201
- setActivities(sortActivities(combined, sortMode));
202
- setHasMore(firstPageHasMore);
225
+ if (!isStale()) {
226
+ setActivities(sortActivities(combined, sortMode));
227
+ setHasMore(firstPageHasMore);
228
+ }
203
229
  } catch (loadError) {
204
- setActivities([]);
205
- setHasMore(false);
206
- setError(t("customers.activityLog.error", "Failed to load activity history"));
230
+ if (!isStale() && !isAbortError(loadError)) {
231
+ setActivities([]);
232
+ setHasMore(false);
233
+ setError(t("customers.activityLog.error", "Failed to load activity history"));
234
+ }
207
235
  } finally {
208
- setLoading(false);
236
+ if (!isStale()) setLoading(false);
209
237
  }
210
238
  }, [activeTypes, dateRange, entityId, loadedPages, search, sortMode, t, useCanonicalInteractions]);
211
239
  React.useEffect(() => {
212
- void loadHistory();
213
- }, [loadHistory, refreshKey]);
240
+ const controller = new AbortController();
241
+ const requestSeq = historyRequestSeqRef.current + 1;
242
+ historyRequestSeqRef.current = requestSeq;
243
+ void loadHistory({ signal: controller.signal, requestSeq });
244
+ return () => controller.abort();
245
+ }, [loadHistory, refreshKey, localRefreshKey]);
214
246
  React.useEffect(() => {
215
247
  setLoadedPages(1);
216
248
  }, [activeTypes, dateRange, entityId, search, sortMode, useCanonicalInteractions]);
@@ -272,32 +304,52 @@ function ActivityHistorySection({
272
304
  );
273
305
  }),
274
306
  /* @__PURE__ */ jsxs(
275
- "select",
307
+ Select,
276
308
  {
277
309
  value: dateRange,
278
- onChange: (event) => {
279
- setDateRange(event.target.value);
310
+ onValueChange: (value) => {
311
+ setDateRange(value);
280
312
  },
281
- className: "h-8 rounded-lg border bg-background px-3 text-xs outline-none ring-offset-background focus:ring-2 focus:ring-ring",
282
313
  children: [
283
- /* @__PURE__ */ jsx("option", { value: "7d", children: t("customers.changelog.last7days", "Last 7 days") }),
284
- /* @__PURE__ */ jsx("option", { value: "30d", children: t("customers.changelog.last30days", "Last 30 days") }),
285
- /* @__PURE__ */ jsx("option", { value: "90d", children: t("customers.changelog.last90days", "Last 90 days") })
314
+ /* @__PURE__ */ jsx(
315
+ SelectTrigger,
316
+ {
317
+ size: "sm",
318
+ "aria-label": t("customers.activityLog.filters.dateRangeLabel", "Date range"),
319
+ className: "w-auto",
320
+ children: /* @__PURE__ */ jsx(SelectValue, {})
321
+ }
322
+ ),
323
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
324
+ /* @__PURE__ */ jsx(SelectItem, { value: "7d", children: t("customers.changelog.last7days", "Last 7 days") }),
325
+ /* @__PURE__ */ jsx(SelectItem, { value: "30d", children: t("customers.changelog.last30days", "Last 30 days") }),
326
+ /* @__PURE__ */ jsx(SelectItem, { value: "90d", children: t("customers.changelog.last90days", "Last 90 days") })
327
+ ] })
286
328
  ]
287
329
  }
288
330
  ),
289
331
  /* @__PURE__ */ jsxs(
290
- "select",
332
+ Select,
291
333
  {
292
334
  value: sortMode,
293
- onChange: (event) => {
294
- setSortMode(event.target.value);
335
+ onValueChange: (value) => {
336
+ setSortMode(value);
295
337
  },
296
- className: "h-8 rounded-lg border bg-background px-3 text-xs outline-none ring-offset-background focus:ring-2 focus:ring-ring",
297
338
  children: [
298
- /* @__PURE__ */ jsx("option", { value: "recent", children: t("customers.activityLog.sort.recent", "Sort: newest") }),
299
- /* @__PURE__ */ jsx("option", { value: "title-asc", children: t("customers.activityLog.sort.titleAsc", "Sort: Name A-Z") }),
300
- /* @__PURE__ */ jsx("option", { value: "title-desc", children: t("customers.activityLog.sort.titleDesc", "Sort: Name Z-A") })
339
+ /* @__PURE__ */ jsx(
340
+ SelectTrigger,
341
+ {
342
+ size: "sm",
343
+ "aria-label": t("customers.activityLog.filters.sortLabel", "Sort order"),
344
+ className: "w-auto",
345
+ children: /* @__PURE__ */ jsx(SelectValue, {})
346
+ }
347
+ ),
348
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
349
+ /* @__PURE__ */ jsx(SelectItem, { value: "recent", children: t("customers.activityLog.sort.recent", "Sort: newest") }),
350
+ /* @__PURE__ */ jsx(SelectItem, { value: "title-asc", children: t("customers.activityLog.sort.titleAsc", "Sort: Name A-Z") }),
351
+ /* @__PURE__ */ jsx(SelectItem, { value: "title-desc", children: t("customers.activityLog.sort.titleDesc", "Sort: Name Z-A") })
352
+ ] })
301
353
  ]
302
354
  }
303
355
  )
@@ -324,7 +376,15 @@ function ActivityHistorySection({
324
376
  ] }),
325
377
  /* @__PURE__ */ jsx("div", { className: "h-px flex-1 bg-border" })
326
378
  ] }) : null,
327
- /* @__PURE__ */ jsx(ActivityCard, { activity, onOpen: onEditActivity })
379
+ /* @__PURE__ */ jsx(
380
+ ActivityCard,
381
+ {
382
+ activity,
383
+ onOpen: onEditActivity,
384
+ onChanged: handleActivityChanged,
385
+ runMutation
386
+ }
387
+ )
328
388
  ] }, activity.id);
329
389
  }),
330
390
  hasMore ? /* @__PURE__ */ jsx("div", { className: "pt-2 text-center", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: handleLoadMore, className: "text-sm", children: t("customers.activities.loadMore", "Load more") }) }) : null
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ActivityHistorySection.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Clock3, Search } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { ErrorMessage, LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { ActivitySummary, InteractionSummary } from './types'\nimport { ActivityCard } from './ActivityCard'\n\ntype ActivityHistorySectionProps = {\n entityId: string\n useCanonicalInteractions?: boolean\n refreshKey?: number\n onEditActivity?: (activity: InteractionSummary) => void\n}\n\ntype InteractionListResponse = {\n items?: InteractionSummary[]\n nextCursor?: string\n}\n\ntype InteractionCountsResponse = {\n ok?: boolean\n result?: {\n call: number\n email: number\n meeting: number\n note: number\n total: number\n }\n call?: number\n email?: number\n meeting?: number\n note?: number\n total?: number\n}\n\nconst TYPE_FILTERS = [\n { value: 'call', labelKey: 'customers.timeline.filter.call', fallback: 'Call' },\n { value: 'email', labelKey: 'customers.timeline.filter.email', fallback: 'Email' },\n { value: 'meeting', labelKey: 'customers.timeline.filter.meeting', fallback: 'Meeting' },\n { value: 'note', labelKey: 'customers.timeline.filter.note', fallback: 'Note' },\n] as const\n\nfunction computeRangeStart(range: '7d' | '30d' | '90d'): Date {\n const date = new Date()\n date.setHours(0, 0, 0, 0)\n const days = Number.parseInt(range.replace('d', ''), 10) || 30\n date.setDate(date.getDate() - days)\n return date\n}\n\nfunction toTimelineTimestamp(activity: InteractionSummary): string {\n return activity.occurredAt ?? activity.scheduledAt ?? activity.createdAt\n}\n\nfunction normalizeLegacyActivity(activity: ActivitySummary): InteractionSummary {\n return {\n id: activity.id,\n interactionType: activity.activityType,\n title: activity.subject ?? null,\n body: activity.body ?? null,\n status: 'done',\n scheduledAt: null,\n occurredAt: activity.occurredAt ?? null,\n priority: null,\n authorUserId: activity.authorUserId ?? null,\n ownerUserId: null,\n appearanceIcon: activity.appearanceIcon ?? null,\n appearanceColor: activity.appearanceColor ?? null,\n source: 'legacy-activity',\n entityId: activity.entityId ?? null,\n dealId: activity.dealId ?? null,\n organizationId: null,\n tenantId: null,\n authorName: activity.authorName ?? null,\n authorEmail: activity.authorEmail ?? null,\n dealTitle: activity.dealTitle ?? null,\n customValues: activity.customValues ?? null,\n createdAt: activity.createdAt,\n updatedAt: activity.createdAt,\n }\n}\n\nfunction sortTimelineActivities(items: InteractionSummary[]): InteractionSummary[] {\n const now = Date.now()\n return [...items].sort((left, right) => {\n const leftScheduled = left.scheduledAt ? new Date(left.scheduledAt).getTime() : Number.NaN\n const rightScheduled = right.scheduledAt ? new Date(right.scheduledAt).getTime() : Number.NaN\n const leftIsPlanned = left.status === 'planned' && Number.isFinite(leftScheduled)\n const rightIsPlanned = right.status === 'planned' && Number.isFinite(rightScheduled)\n const leftIsUpcoming = leftIsPlanned && leftScheduled >= now\n const rightIsUpcoming = rightIsPlanned && rightScheduled >= now\n\n if (leftIsUpcoming !== rightIsUpcoming) return leftIsUpcoming ? -1 : 1\n if (leftIsUpcoming && rightIsUpcoming) return leftScheduled - rightScheduled\n\n const compare = toTimelineTimestamp(right).localeCompare(toTimelineTimestamp(left))\n if (compare !== 0) return compare\n return right.id.localeCompare(left.id)\n })\n}\n\nfunction sortActivities(items: InteractionSummary[], sortMode: 'recent' | 'title-asc' | 'title-desc') {\n if (sortMode === 'recent') return sortTimelineActivities(items)\n return [...items].sort((left, right) => {\n const leftTitle = (left.title ?? left.body ?? left.interactionType ?? '').toLowerCase()\n const rightTitle = (right.title ?? right.body ?? right.interactionType ?? '').toLowerCase()\n return sortMode === 'title-asc'\n ? leftTitle.localeCompare(rightTitle)\n : rightTitle.localeCompare(leftTitle)\n })\n}\n\nfunction matchesSearch(activity: InteractionSummary, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized) return true\n const haystack = [\n activity.title,\n activity.body,\n activity.authorName,\n activity.authorEmail,\n activity.customer?.displayName,\n ]\n .filter(Boolean)\n .join(' ')\n .toLowerCase()\n return haystack.includes(normalized)\n}\n\nfunction isWithinRange(activity: InteractionSummary, start: Date): boolean {\n const timestamp = new Date(toTimelineTimestamp(activity))\n if (Number.isNaN(timestamp.getTime())) return false\n return timestamp >= start\n}\n\nexport function ActivityHistorySection({\n entityId,\n useCanonicalInteractions = false,\n refreshKey = 0,\n onEditActivity,\n}: ActivityHistorySectionProps) {\n const t = useT()\n const [searchInput, setSearchInput] = React.useState('')\n const [search, setSearch] = React.useState('')\n const [activeTypes, setActiveTypes] = React.useState<string[]>([])\n const [dateRange, setDateRange] = React.useState<'7d' | '30d' | '90d'>('90d')\n const [sortMode, setSortMode] = React.useState<'recent' | 'title-asc' | 'title-desc'>('recent')\n const [activities, setActivities] = React.useState<InteractionSummary[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [counts, setCounts] = React.useState<Record<string, number>>({ call: 0, email: 0, meeting: 0, note: 0, total: 0 })\n const [hasMore, setHasMore] = React.useState(false)\n const [loadedPages, setLoadedPages] = React.useState(1)\n\n React.useEffect(() => {\n const timeout = window.setTimeout(() => setSearch(searchInput.trim()), 300)\n return () => window.clearTimeout(timeout)\n }, [searchInput])\n\n React.useEffect(() => {\n const controller = new AbortController()\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<InteractionCountsResponse>(\n `/api/customers/interactions/counts?entityId=${encodeURIComponent(entityId)}&status=done`,\n { signal: controller.signal },\n )\n const result = payload.result ?? payload\n setCounts({\n call: result.call ?? 0,\n email: result.email ?? 0,\n meeting: result.meeting ?? 0,\n note: result.note ?? 0,\n total: result.total ?? 0,\n })\n } catch {\n setCounts({ call: 0, email: 0, meeting: 0, note: 0, total: 0 })\n }\n })()\n return () => controller.abort()\n }, [entityId, refreshKey])\n\n const loadHistory = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const rangeStart = computeRangeStart(dateRange).toISOString()\n const pageSize = 20\n const canonicalItems: InteractionSummary[] = []\n let nextCursor: string | undefined\n let firstPageHasMore = false\n let pagesLoaded = 0\n\n do {\n const params = new URLSearchParams({\n entityId,\n status: 'done',\n excludeInteractionType: 'task',\n limit: String(pageSize),\n from: rangeStart,\n })\n if (activeTypes.length > 0) params.set('type', activeTypes.join(','))\n if (search) params.set('search', search)\n if (sortMode === 'recent') {\n params.set('sortField', 'occurredAt')\n params.set('sortDir', 'desc')\n } else {\n params.set('sortField', 'title')\n params.set('sortDir', sortMode === 'title-asc' ? 'asc' : 'desc')\n }\n if (nextCursor) params.set('cursor', nextCursor)\n\n const response = await readApiResultOrThrow<InteractionListResponse>(\n `/api/customers/interactions?${params.toString()}`,\n )\n const pageItems = Array.isArray(response.items) ? response.items : []\n canonicalItems.push(...pageItems)\n nextCursor = response.nextCursor\n if (!firstPageHasMore) firstPageHasMore = Boolean(response.nextCursor)\n pagesLoaded += 1\n } while (nextCursor && pagesLoaded < loadedPages)\n\n let combined = canonicalItems\n\n if (!useCanonicalInteractions) {\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (let legacyPage = 1; legacyPage <= loadedPages; legacyPage += 1) {\n const legacyPayload = await readApiResultOrThrow<{ items?: ActivitySummary[]; totalPages?: number }>(\n `/api/customers/activities?entityId=${encodeURIComponent(entityId)}&page=${legacyPage}&pageSize=20&sortField=occurredAt&sortDir=desc`,\n ).catch(() => ({ items: [] as ActivitySummary[], totalPages: 1 }))\n legacyItems.push(...(Array.isArray(legacyPayload.items) ? legacyPayload.items.map(normalizeLegacyActivity) : []))\n legacyTotalPages = typeof legacyPayload.totalPages === 'number' ? legacyPayload.totalPages : legacyTotalPages\n if (legacyPage >= legacyTotalPages) break\n }\n const rangeStartDate = computeRangeStart(dateRange)\n const filteredLegacy = legacyItems.filter((item) => {\n if (activeTypes.length > 0 && !activeTypes.includes(item.interactionType)) return false\n if (!matchesSearch(item, search)) return false\n return isWithinRange(item, rangeStartDate)\n })\n const deduped = new Map<string, InteractionSummary>()\n ;[...canonicalItems, ...filteredLegacy].forEach((item) => {\n if (!deduped.has(item.id)) deduped.set(item.id, item)\n })\n combined = Array.from(deduped.values())\n firstPageHasMore = firstPageHasMore || legacyTotalPages > loadedPages\n }\n\n setActivities(sortActivities(combined, sortMode))\n setHasMore(firstPageHasMore)\n } catch (loadError) {\n setActivities([])\n setHasMore(false)\n setError(t('customers.activityLog.error', 'Failed to load activity history'))\n } finally {\n setLoading(false)\n }\n }, [activeTypes, dateRange, entityId, loadedPages, search, sortMode, t, useCanonicalInteractions])\n\n React.useEffect(() => {\n void loadHistory()\n }, [loadHistory, refreshKey])\n\n React.useEffect(() => {\n setLoadedPages(1)\n }, [activeTypes, dateRange, entityId, search, sortMode, useCanonicalInteractions])\n\n const filteredLabel = activeTypes.length > 0\n ? activeTypes.map((type) => t(`customers.timeline.filter.${type}`, type)).join(', ')\n : t('customers.timeline.filter.all', 'All')\n\n const handleTypeToggle = React.useCallback((type: string) => {\n setActiveTypes((current) => (\n current.includes(type)\n ? current.filter((entry) => entry !== type)\n : [...current, type]\n ))\n }, [])\n\n const handleLoadMore = React.useCallback(() => {\n setLoadedPages((current) => current + 1)\n }, [])\n\n return (\n <div className=\"rounded-xl border bg-card\">\n <div className=\"flex items-center gap-2 border-b px-5 py-4\">\n <Clock3 className=\"size-4 text-muted-foreground\" />\n <div className=\"min-w-0\">\n <h3 className=\"text-base font-semibold text-foreground\">\n {t('customers.activityLog.title', 'Activity history')}\n </h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.timeline.history.filtered', 'filtered: {{types}} \u00B7 {{count}} results', {\n types: filteredLabel,\n count: activities.length,\n })}\n </p>\n </div>\n </div>\n\n <div className=\"space-y-4 px-5 py-4\">\n <div className=\"flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between\">\n <div className=\"relative w-full max-w-md\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\" />\n <Input\n value={searchInput}\n onChange={(event) => {\n setSearchInput(event.target.value)\n }}\n placeholder={t('customers.activityLog.searchPlaceholder', 'Search by title, note, or author')}\n className=\"h-9 pl-9\"\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-overline font-semibold uppercase tracking-[0.12em] text-muted-foreground\">\n {t('customers.changelog.filter', 'Filter')}:\n </span>\n {TYPE_FILTERS.map((filter) => {\n const isActive = activeTypes.includes(filter.value)\n return (\n <Button\n key={filter.value}\n type=\"button\"\n variant={isActive ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => handleTypeToggle(filter.value)}\n className=\"h-auto rounded-full px-2.5 py-1 text-xs\"\n >\n {t(filter.labelKey, filter.fallback)}\n <span className={isActive ? 'ml-1 text-primary-foreground/80' : 'ml-1 text-muted-foreground'}>\n {counts[filter.value] ?? 0}\n </span>\n </Button>\n )\n })}\n\n <select\n value={dateRange}\n onChange={(event) => {\n setDateRange(event.target.value as '7d' | '30d' | '90d')\n }}\n className=\"h-8 rounded-lg border bg-background px-3 text-xs outline-none ring-offset-background focus:ring-2 focus:ring-ring\"\n >\n <option value=\"7d\">{t('customers.changelog.last7days', 'Last 7 days')}</option>\n <option value=\"30d\">{t('customers.changelog.last30days', 'Last 30 days')}</option>\n <option value=\"90d\">{t('customers.changelog.last90days', 'Last 90 days')}</option>\n </select>\n\n <select\n value={sortMode}\n onChange={(event) => {\n setSortMode(event.target.value as 'recent' | 'title-asc' | 'title-desc')\n }}\n className=\"h-8 rounded-lg border bg-background px-3 text-xs outline-none ring-offset-background focus:ring-2 focus:ring-ring\"\n >\n <option value=\"recent\">{t('customers.activityLog.sort.recent', 'Sort: newest')}</option>\n <option value=\"title-asc\">{t('customers.activityLog.sort.titleAsc', 'Sort: Name A-Z')}</option>\n <option value=\"title-desc\">{t('customers.activityLog.sort.titleDesc', 'Sort: Name Z-A')}</option>\n </select>\n </div>\n </div>\n\n {loading && activities.length === 0 ? (\n <LoadingMessage label={t('customers.people.detail.activities.loading', 'Loading activities\u2026')} className=\"min-h-[220px] justify-center\" />\n ) : error ? (\n <ErrorMessage label={error} />\n ) : activities.length === 0 ? (\n <TabEmptyState\n title={t('customers.timeline.empty', 'No activities match the current filters.')}\n description={t('customers.activityLog.emptyDescription', 'Try broadening the date range or removing some filters.')}\n />\n ) : (\n <div className=\"space-y-4\">\n {activities.map((activity, index) => {\n const currentYear = new Date(toTimelineTimestamp(activity)).getFullYear()\n const previousYear = index > 0 ? new Date(toTimelineTimestamp(activities[index - 1])).getFullYear() : null\n const showYearSeparator = previousYear !== null && currentYear !== previousYear\n return (\n <React.Fragment key={activity.id}>\n {showYearSeparator ? (\n <div className=\"flex items-center gap-3 py-1\">\n <div className=\"h-px flex-1 bg-border\" />\n <span className=\"text-xs font-semibold text-muted-foreground\">\u00B7 {currentYear} \u00B7</span>\n <div className=\"h-px flex-1 bg-border\" />\n </div>\n ) : null}\n <ActivityCard activity={activity} onOpen={onEditActivity} />\n </React.Fragment>\n )\n })}\n\n {hasMore ? (\n <div className=\"pt-2 text-center\">\n <Button type=\"button\" variant=\"link\" size=\"sm\" onClick={handleLoadMore} className=\"text-sm\">\n {t('customers.activities.loadMore', 'Load more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nexport default ActivityHistorySection\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Clock3, Search } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { ErrorMessage, LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { ActivitySummary, InteractionSummary } from './types'\nimport { ActivityCard } from './ActivityCard'\n\ntype GuardedMutationRunner = <T,>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\ntype ActivityHistorySectionProps = {\n entityId: string\n useCanonicalInteractions?: boolean\n refreshKey?: number\n onEditActivity?: (activity: InteractionSummary) => void\n /** Optional guarded-mutation runner so per-row mutations route through the parent's\n * `useGuardedMutation` and emit retry-last-mutation context. */\n runMutation?: GuardedMutationRunner\n}\n\ntype InteractionListResponse = {\n items?: InteractionSummary[]\n nextCursor?: string\n}\n\ntype InteractionCountsResponse = {\n ok?: boolean\n result?: {\n call: number\n email: number\n meeting: number\n note: number\n task: number\n total: number\n }\n call?: number\n email?: number\n meeting?: number\n note?: number\n task?: number\n total?: number\n}\n\nconst TYPE_FILTERS = [\n { value: 'call', labelKey: 'customers.timeline.filter.call', fallback: 'Call' },\n { value: 'email', labelKey: 'customers.timeline.filter.email', fallback: 'Email' },\n { value: 'meeting', labelKey: 'customers.timeline.filter.meeting', fallback: 'Meeting' },\n { value: 'note', labelKey: 'customers.timeline.filter.note', fallback: 'Note' },\n { value: 'task', labelKey: 'customers.timeline.filter.task', fallback: 'Task' },\n] as const\n\nfunction computeRangeStart(range: '7d' | '30d' | '90d'): Date {\n const date = new Date()\n date.setHours(0, 0, 0, 0)\n const days = Number.parseInt(range.replace('d', ''), 10) || 30\n date.setDate(date.getDate() - days)\n return date\n}\n\nfunction toTimelineTimestamp(activity: InteractionSummary): string {\n return activity.occurredAt ?? activity.scheduledAt ?? activity.createdAt\n}\n\nfunction normalizeLegacyActivity(activity: ActivitySummary): InteractionSummary {\n return {\n id: activity.id,\n interactionType: activity.activityType,\n title: activity.subject ?? null,\n body: activity.body ?? null,\n status: 'done',\n scheduledAt: null,\n occurredAt: activity.occurredAt ?? null,\n priority: null,\n authorUserId: activity.authorUserId ?? null,\n ownerUserId: null,\n appearanceIcon: activity.appearanceIcon ?? null,\n appearanceColor: activity.appearanceColor ?? null,\n source: 'legacy-activity',\n entityId: activity.entityId ?? null,\n dealId: activity.dealId ?? null,\n organizationId: null,\n tenantId: null,\n authorName: activity.authorName ?? null,\n authorEmail: activity.authorEmail ?? null,\n dealTitle: activity.dealTitle ?? null,\n customValues: activity.customValues ?? null,\n createdAt: activity.createdAt,\n updatedAt: activity.createdAt,\n }\n}\n\nfunction sortTimelineActivities(items: InteractionSummary[]): InteractionSummary[] {\n const now = Date.now()\n return [...items].sort((left, right) => {\n const leftScheduled = left.scheduledAt ? new Date(left.scheduledAt).getTime() : Number.NaN\n const rightScheduled = right.scheduledAt ? new Date(right.scheduledAt).getTime() : Number.NaN\n const leftIsPlanned = left.status === 'planned' && Number.isFinite(leftScheduled)\n const rightIsPlanned = right.status === 'planned' && Number.isFinite(rightScheduled)\n const leftIsUpcoming = leftIsPlanned && leftScheduled >= now\n const rightIsUpcoming = rightIsPlanned && rightScheduled >= now\n\n if (leftIsUpcoming !== rightIsUpcoming) return leftIsUpcoming ? -1 : 1\n if (leftIsUpcoming && rightIsUpcoming) return leftScheduled - rightScheduled\n\n const compare = toTimelineTimestamp(right).localeCompare(toTimelineTimestamp(left))\n if (compare !== 0) return compare\n return right.id.localeCompare(left.id)\n })\n}\n\nfunction sortActivities(items: InteractionSummary[], sortMode: 'recent' | 'title-asc' | 'title-desc') {\n if (sortMode === 'recent') return sortTimelineActivities(items)\n return [...items].sort((left, right) => {\n const leftTitle = (left.title ?? left.body ?? left.interactionType ?? '').toLowerCase()\n const rightTitle = (right.title ?? right.body ?? right.interactionType ?? '').toLowerCase()\n return sortMode === 'title-asc'\n ? leftTitle.localeCompare(rightTitle)\n : rightTitle.localeCompare(leftTitle)\n })\n}\n\nfunction matchesSearch(activity: InteractionSummary, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized) return true\n const haystack = [\n activity.title,\n activity.body,\n activity.authorName,\n activity.authorEmail,\n activity.customer?.displayName,\n ]\n .filter(Boolean)\n .join(' ')\n .toLowerCase()\n return haystack.includes(normalized)\n}\n\nfunction isWithinRange(activity: InteractionSummary, start: Date): boolean {\n const timestamp = new Date(toTimelineTimestamp(activity))\n if (Number.isNaN(timestamp.getTime())) return false\n return timestamp >= start\n}\n\nfunction isAbortError(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'name' in error &&\n (error as { name?: unknown }).name === 'AbortError'\n )\n}\n\nexport function ActivityHistorySection({\n entityId,\n useCanonicalInteractions = false,\n refreshKey = 0,\n onEditActivity,\n runMutation,\n}: ActivityHistorySectionProps) {\n const t = useT()\n const [searchInput, setSearchInput] = React.useState('')\n const [search, setSearch] = React.useState('')\n const [activeTypes, setActiveTypes] = React.useState<string[]>([])\n const [dateRange, setDateRange] = React.useState<'7d' | '30d' | '90d'>('90d')\n const [sortMode, setSortMode] = React.useState<'recent' | 'title-asc' | 'title-desc'>('recent')\n const [activities, setActivities] = React.useState<InteractionSummary[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [counts, setCounts] = React.useState<Record<string, number>>({ call: 0, email: 0, meeting: 0, note: 0, task: 0, total: 0 })\n const [hasMore, setHasMore] = React.useState(false)\n const [loadedPages, setLoadedPages] = React.useState(1)\n const [localRefreshKey, setLocalRefreshKey] = React.useState(0)\n const historyRequestSeqRef = React.useRef(0)\n const handleActivityChanged = React.useCallback(() => {\n setLocalRefreshKey((current) => current + 1)\n }, [])\n\n React.useEffect(() => {\n const timeout = window.setTimeout(() => setSearch(searchInput.trim()), 300)\n return () => window.clearTimeout(timeout)\n }, [searchInput])\n\n React.useEffect(() => {\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 const result = payload.result ?? payload\n setCounts({\n call: result.call ?? 0,\n email: result.email ?? 0,\n meeting: result.meeting ?? 0,\n note: result.note ?? 0,\n task: result.task ?? 0,\n total: result.total ?? 0,\n })\n } catch {\n setCounts({ call: 0, email: 0, meeting: 0, note: 0, task: 0, total: 0 })\n }\n })()\n return () => controller.abort()\n }, [entityId, refreshKey, localRefreshKey])\n\n const loadHistory = React.useCallback(async (options: { signal: AbortSignal; requestSeq: number }) => {\n const { signal, requestSeq } = options\n const isStale = () => signal.aborted || requestSeq !== historyRequestSeqRef.current\n setLoading(true)\n setError(null)\n try {\n const rangeStart = computeRangeStart(dateRange).toISOString()\n const pageSize = 20\n const canonicalItems: InteractionSummary[] = []\n let nextCursor: string | undefined\n let firstPageHasMore = false\n let pagesLoaded = 0\n\n const taskFilterActive = activeTypes.includes('task')\n do {\n const params = new URLSearchParams({\n entityId,\n limit: String(pageSize),\n from: rangeStart,\n })\n if (!taskFilterActive) params.set('excludeInteractionType', 'task')\n if (activeTypes.length > 0) params.set('type', activeTypes.join(','))\n if (search) params.set('search', search)\n if (sortMode === 'recent') {\n params.set('sortField', 'occurredAt')\n params.set('sortDir', 'desc')\n } else {\n params.set('sortField', 'title')\n params.set('sortDir', sortMode === 'title-asc' ? 'asc' : 'desc')\n }\n if (nextCursor) params.set('cursor', nextCursor)\n\n const response = await readApiResultOrThrow<InteractionListResponse>(\n `/api/customers/interactions?${params.toString()}`,\n { signal },\n )\n if (isStale()) return\n const pageItems = Array.isArray(response.items) ? response.items : []\n canonicalItems.push(...pageItems)\n nextCursor = response.nextCursor\n if (!firstPageHasMore) firstPageHasMore = Boolean(response.nextCursor)\n pagesLoaded += 1\n } while (nextCursor && pagesLoaded < loadedPages)\n\n let combined = canonicalItems\n\n if (!useCanonicalInteractions) {\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (let legacyPage = 1; legacyPage <= loadedPages; legacyPage += 1) {\n const legacyPayload = await readApiResultOrThrow<{ items?: ActivitySummary[]; totalPages?: number }>(\n `/api/customers/activities?entityId=${encodeURIComponent(entityId)}&page=${legacyPage}&pageSize=20&sortField=occurredAt&sortDir=desc`,\n { signal },\n ).catch(() => ({ items: [] as ActivitySummary[], totalPages: 1 }))\n if (isStale()) return\n legacyItems.push(...(Array.isArray(legacyPayload.items) ? legacyPayload.items.map(normalizeLegacyActivity) : []))\n legacyTotalPages = typeof legacyPayload.totalPages === 'number' ? legacyPayload.totalPages : legacyTotalPages\n if (legacyPage >= legacyTotalPages) break\n }\n const rangeStartDate = computeRangeStart(dateRange)\n const filteredLegacy = legacyItems.filter((item) => {\n if (activeTypes.length > 0 && !activeTypes.includes(item.interactionType)) return false\n if (!matchesSearch(item, search)) return false\n return isWithinRange(item, rangeStartDate)\n })\n const deduped = new Map<string, InteractionSummary>()\n ;[...canonicalItems, ...filteredLegacy].forEach((item) => {\n if (!deduped.has(item.id)) deduped.set(item.id, item)\n })\n combined = Array.from(deduped.values())\n firstPageHasMore = firstPageHasMore || legacyTotalPages > loadedPages\n }\n\n if (!isStale()) {\n setActivities(sortActivities(combined, sortMode))\n setHasMore(firstPageHasMore)\n }\n } catch (loadError) {\n if (!isStale() && !isAbortError(loadError)) {\n setActivities([])\n setHasMore(false)\n setError(t('customers.activityLog.error', 'Failed to load activity history'))\n }\n } finally {\n if (!isStale()) setLoading(false)\n }\n }, [activeTypes, dateRange, entityId, loadedPages, search, sortMode, t, useCanonicalInteractions])\n\n React.useEffect(() => {\n const controller = new AbortController()\n const requestSeq = historyRequestSeqRef.current + 1\n historyRequestSeqRef.current = requestSeq\n void loadHistory({ signal: controller.signal, requestSeq })\n return () => controller.abort()\n }, [loadHistory, refreshKey, localRefreshKey])\n\n React.useEffect(() => {\n setLoadedPages(1)\n }, [activeTypes, dateRange, entityId, search, sortMode, useCanonicalInteractions])\n\n const filteredLabel = activeTypes.length > 0\n ? activeTypes.map((type) => t(`customers.timeline.filter.${type}`, type)).join(', ')\n : t('customers.timeline.filter.all', 'All')\n\n const handleTypeToggle = React.useCallback((type: string) => {\n setActiveTypes((current) => (\n current.includes(type)\n ? current.filter((entry) => entry !== type)\n : [...current, type]\n ))\n }, [])\n\n const handleLoadMore = React.useCallback(() => {\n setLoadedPages((current) => current + 1)\n }, [])\n\n return (\n <div className=\"rounded-xl border bg-card\">\n <div className=\"flex items-center gap-2 border-b px-5 py-4\">\n <Clock3 className=\"size-4 text-muted-foreground\" />\n <div className=\"min-w-0\">\n <h3 className=\"text-base font-semibold text-foreground\">\n {t('customers.activityLog.title', 'Activity history')}\n </h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.timeline.history.filtered', 'filtered: {{types}} \u00B7 {{count}} results', {\n types: filteredLabel,\n count: activities.length,\n })}\n </p>\n </div>\n </div>\n\n <div className=\"space-y-4 px-5 py-4\">\n <div className=\"flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between\">\n <div className=\"relative w-full max-w-md\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\" />\n <Input\n value={searchInput}\n onChange={(event) => {\n setSearchInput(event.target.value)\n }}\n placeholder={t('customers.activityLog.searchPlaceholder', 'Search by title, note, or author')}\n className=\"h-9 pl-9\"\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-overline font-semibold uppercase tracking-[0.12em] text-muted-foreground\">\n {t('customers.changelog.filter', 'Filter')}:\n </span>\n {TYPE_FILTERS.map((filter) => {\n const isActive = activeTypes.includes(filter.value)\n return (\n <Button\n key={filter.value}\n type=\"button\"\n variant={isActive ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => handleTypeToggle(filter.value)}\n className=\"h-auto rounded-full px-2.5 py-1 text-xs\"\n >\n {t(filter.labelKey, filter.fallback)}\n <span className={isActive ? 'ml-1 text-primary-foreground/80' : 'ml-1 text-muted-foreground'}>\n {counts[filter.value] ?? 0}\n </span>\n </Button>\n )\n })}\n\n <Select\n value={dateRange}\n onValueChange={(value) => {\n setDateRange(value as '7d' | '30d' | '90d')\n }}\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.activityLog.filters.dateRangeLabel', 'Date range')}\n className=\"w-auto\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"7d\">{t('customers.changelog.last7days', 'Last 7 days')}</SelectItem>\n <SelectItem value=\"30d\">{t('customers.changelog.last30days', 'Last 30 days')}</SelectItem>\n <SelectItem value=\"90d\">{t('customers.changelog.last90days', 'Last 90 days')}</SelectItem>\n </SelectContent>\n </Select>\n\n <Select\n value={sortMode}\n onValueChange={(value) => {\n setSortMode(value as 'recent' | 'title-asc' | 'title-desc')\n }}\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.activityLog.filters.sortLabel', 'Sort order')}\n className=\"w-auto\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"recent\">{t('customers.activityLog.sort.recent', 'Sort: newest')}</SelectItem>\n <SelectItem value=\"title-asc\">{t('customers.activityLog.sort.titleAsc', 'Sort: Name A-Z')}</SelectItem>\n <SelectItem value=\"title-desc\">{t('customers.activityLog.sort.titleDesc', 'Sort: Name Z-A')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {loading && activities.length === 0 ? (\n <LoadingMessage label={t('customers.people.detail.activities.loading', 'Loading activities\u2026')} className=\"min-h-[220px] justify-center\" />\n ) : error ? (\n <ErrorMessage label={error} />\n ) : activities.length === 0 ? (\n <TabEmptyState\n title={t('customers.timeline.empty', 'No activities match the current filters.')}\n description={t('customers.activityLog.emptyDescription', 'Try broadening the date range or removing some filters.')}\n />\n ) : (\n <div className=\"space-y-4\">\n {activities.map((activity, index) => {\n const currentYear = new Date(toTimelineTimestamp(activity)).getFullYear()\n const previousYear = index > 0 ? new Date(toTimelineTimestamp(activities[index - 1])).getFullYear() : null\n const showYearSeparator = previousYear !== null && currentYear !== previousYear\n return (\n <React.Fragment key={activity.id}>\n {showYearSeparator ? (\n <div className=\"flex items-center gap-3 py-1\">\n <div className=\"h-px flex-1 bg-border\" />\n <span className=\"text-xs font-semibold text-muted-foreground\">\u00B7 {currentYear} \u00B7</span>\n <div className=\"h-px flex-1 bg-border\" />\n </div>\n ) : null}\n <ActivityCard\n activity={activity}\n onOpen={onEditActivity}\n onChanged={handleActivityChanged}\n runMutation={runMutation}\n />\n </React.Fragment>\n )\n })}\n\n {hasMore ? (\n <div className=\"pt-2 text-center\">\n <Button type=\"button\" variant=\"link\" size=\"sm\" onClick={handleLoadMore} className=\"text-sm\">\n {t('customers.activities.loadMore', 'Load more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nexport default ActivityHistorySection\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,7 @@ function ActivityLogTab({
8
8
  onScheduleRequested,
9
9
  onAddActivity,
10
10
  onEditActivity,
11
+ runGuardedMutation,
11
12
  refreshKey = 0,
12
13
  useCanonicalInteractions = false,
13
14
  entityCompanyName
@@ -34,7 +35,8 @@ function ActivityLogTab({
34
35
  entityId,
35
36
  useCanonicalInteractions,
36
37
  refreshKey,
37
- onEditActivity
38
+ onEditActivity,
39
+ runMutation: runGuardedMutation
38
40
  }
39
41
  )
40
42
  ] });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ActivityLogTab.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport { ActivitiesCard } from './ActivitiesCard'\nimport type { ActivityKind } from './ActivitiesAddNewMenu'\nimport { ActivityHistorySection } from './ActivityHistorySection'\nimport type { InteractionSummary } from './types'\n\ntype GuardedMutationRunner = <T,>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\ntype ActivityLogTabProps = {\n entityId: string\n plannedActivities: InteractionSummary[]\n /** @deprecated No longer used after the ActivitiesCard refactor. Kept optional for callers; remove after one minor cycle. */\n onActivityCreated?: () => void\n onScheduleRequested: () => void\n onAddActivity?: (kind: ActivityKind) => void\n /** @deprecated No longer used after the ActivitiesCard refactor. Kept optional for callers; remove after one minor cycle. */\n onMarkDone?: (id: string) => void\n onEditActivity: (activity: InteractionSummary) => void\n /** @deprecated No longer used after the ActivitiesCard refactor. Kept optional for callers; remove after one minor cycle. */\n onCancelActivity?: (id: string) => void\n /** @deprecated No longer used after the ActivitiesCard refactor. Kept optional for callers; remove after one minor cycle. */\n runGuardedMutation?: GuardedMutationRunner\n refreshKey?: number\n useCanonicalInteractions?: boolean\n /** Optional parent-entity company name; surfaces in planned event subtitles when no deal is set. */\n entityCompanyName?: string | null\n}\n\nexport function ActivityLogTab({\n entityId,\n plannedActivities,\n onScheduleRequested,\n onAddActivity,\n onEditActivity,\n refreshKey = 0,\n useCanonicalInteractions = false,\n entityCompanyName,\n}: ActivityLogTabProps) {\n const handleAddNew = (kind: ActivityKind) => {\n if (onAddActivity) onAddActivity(kind)\n else onScheduleRequested()\n }\n\n return (\n <div className=\"space-y-4\">\n <ActivitiesCard\n entityId={entityId}\n plannedActivities={plannedActivities}\n refreshKey={refreshKey}\n onAddNew={handleAddNew}\n onEditActivity={onEditActivity}\n entityCompanyName={entityCompanyName}\n />\n\n <ActivityHistorySection\n entityId={entityId}\n useCanonicalInteractions={useCanonicalInteractions}\n refreshKey={refreshKey}\n onEditActivity={onEditActivity}\n />\n </div>\n )\n}\n\nexport default ActivityLogTab\n"],
5
- "mappings": ";AAgDI,SACE,KADF;AA9CJ,SAAS,sBAAsB;AAE/B,SAAS,8BAA8B;AA4BhC,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,2BAA2B;AAAA,EAC3B;AACF,GAAwB;AACtB,QAAM,eAAe,CAAC,SAAuB;AAC3C,QAAI,cAAe,eAAc,IAAI;AAAA,QAChC,qBAAoB;AAAA,EAC3B;AAEA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;AAEA,IAAO,yBAAQ;",
4
+ "sourcesContent": ["'use client'\n\nimport { ActivitiesCard } from './ActivitiesCard'\nimport type { ActivityKind } from './ActivitiesAddNewMenu'\nimport { ActivityHistorySection } from './ActivityHistorySection'\nimport type { InteractionSummary } from './types'\n\ntype GuardedMutationRunner = <T,>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\ntype ActivityLogTabProps = {\n entityId: string\n plannedActivities: InteractionSummary[]\n /** @deprecated No longer used after the ActivitiesCard refactor. Kept optional for callers; remove after one minor cycle. */\n onActivityCreated?: () => void\n onScheduleRequested: () => void\n onAddActivity?: (kind: ActivityKind) => void\n /** @deprecated No longer used after the ActivitiesCard refactor. Kept optional for callers; remove after one minor cycle. */\n onMarkDone?: (id: string) => void\n onEditActivity: (activity: InteractionSummary) => void\n /** @deprecated No longer used after the ActivitiesCard refactor. Kept optional for callers; remove after one minor cycle. */\n onCancelActivity?: (id: string) => void\n /**\n * Guarded-mutation runner from the parent page. When provided, per-row mutations\n * (e.g. ActivityCard \"Mark done\") route through `useGuardedMutation` so the global\n * injection contract and retry-last-mutation context apply.\n */\n runGuardedMutation?: GuardedMutationRunner\n refreshKey?: number\n useCanonicalInteractions?: boolean\n /** Optional parent-entity company name; surfaces in planned event subtitles when no deal is set. */\n entityCompanyName?: string | null\n}\n\nexport function ActivityLogTab({\n entityId,\n plannedActivities,\n onScheduleRequested,\n onAddActivity,\n onEditActivity,\n runGuardedMutation,\n refreshKey = 0,\n useCanonicalInteractions = false,\n entityCompanyName,\n}: ActivityLogTabProps) {\n const handleAddNew = (kind: ActivityKind) => {\n if (onAddActivity) onAddActivity(kind)\n else onScheduleRequested()\n }\n\n return (\n <div className=\"space-y-4\">\n <ActivitiesCard\n entityId={entityId}\n plannedActivities={plannedActivities}\n refreshKey={refreshKey}\n onAddNew={handleAddNew}\n onEditActivity={onEditActivity}\n entityCompanyName={entityCompanyName}\n />\n\n <ActivityHistorySection\n entityId={entityId}\n useCanonicalInteractions={useCanonicalInteractions}\n refreshKey={refreshKey}\n onEditActivity={onEditActivity}\n runMutation={runGuardedMutation}\n />\n </div>\n )\n}\n\nexport default ActivityLogTab\n"],
5
+ "mappings": ";AAqDI,SACE,KADF;AAnDJ,SAAS,sBAAsB;AAE/B,SAAS,8BAA8B;AAgChC,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,2BAA2B;AAAA,EAC3B;AACF,GAAwB;AACtB,QAAM,eAAe,CAAC,SAAuB;AAC3C,QAAI,cAAe,eAAc,IAAI;AAAA,QAChC,qBAAoB;AAAA,EAC3B;AAEA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,aAAa;AAAA;AAAA,IACf;AAAA,KACF;AAEJ;AAEA,IAAO,yBAAQ;",
6
6
  "names": []
7
7
  }
@@ -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
  }