@open-mercato/core 0.6.1-develop.3081.21270ec58a → 0.6.1-develop.3102.d6e7e6d57a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +21 -1
- package/dist/modules/audit_logs/services/accessLogService.js +13 -0
- package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
- package/dist/modules/audit_logs/services/actionLogService.js +6 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/configs/cli.js +11 -0
- package/dist/modules/configs/cli.js.map +2 -2
- package/dist/modules/configs/lib/touchGeneratedBarrels.js +46 -0
- package/dist/modules/configs/lib/touchGeneratedBarrels.js.map +7 -0
- package/dist/modules/customers/api/activities/route.js +1 -52
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/counts/route.js +2 -1
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +21 -1
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +5 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js +62 -6
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +21 -6
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +37 -5
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +69 -17
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +94 -34
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityLogTab.js +3 -1
- package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +41 -8
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +19 -6
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js +4 -3
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +80 -12
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +65 -10
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +10 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/data/validators.js +74 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/legacyActivityBridge.js +61 -0
- package/dist/modules/customers/lib/legacyActivityBridge.js.map +7 -0
- package/dist/modules/integrations/lib/credentials-service.js +12 -1
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/messages/commands/actions.js +29 -14
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/messages/lib/actions.js +24 -4
- package/dist/modules/messages/lib/actions.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +49 -36
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/package.json +8 -8
- package/src/modules/audit_logs/services/accessLogService.ts +20 -0
- package/src/modules/audit_logs/services/actionLogService.ts +13 -5
- package/src/modules/configs/cli.ts +11 -0
- package/src/modules/configs/lib/touchGeneratedBarrels.ts +61 -0
- package/src/modules/customers/api/activities/route.ts +1 -76
- package/src/modules/customers/api/interactions/counts/route.ts +2 -1
- package/src/modules/customers/api/interactions/route.ts +28 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +14 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +92 -5
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +38 -6
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +37 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +79 -14
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +102 -33
- package/src/modules/customers/components/detail/ActivityLogTab.tsx +7 -1
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +39 -5
- package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +29 -7
- package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +3 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +96 -13
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +50 -4
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +21 -5
- package/src/modules/customers/data/validators.ts +85 -2
- package/src/modules/customers/i18n/de.json +11 -0
- package/src/modules/customers/i18n/en.json +11 -0
- package/src/modules/customers/i18n/es.json +11 -0
- package/src/modules/customers/i18n/pl.json +11 -0
- package/src/modules/customers/lib/legacyActivityBridge.ts +106 -0
- package/src/modules/integrations/lib/credentials-service.ts +15 -1
- package/src/modules/messages/commands/actions.ts +28 -13
- package/src/modules/messages/lib/actions.ts +34 -3
- 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)}
|
|
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
|
-
|
|
202
|
-
|
|
225
|
+
if (!isStale()) {
|
|
226
|
+
setActivities(sortActivities(combined, sortMode));
|
|
227
|
+
setHasMore(firstPageHasMore);
|
|
228
|
+
}
|
|
203
229
|
} catch (loadError) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
307
|
+
Select,
|
|
276
308
|
{
|
|
277
309
|
value: dateRange,
|
|
278
|
-
|
|
279
|
-
setDateRange(
|
|
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(
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
332
|
+
Select,
|
|
291
333
|
{
|
|
292
334
|
value: sortMode,
|
|
293
|
-
|
|
294
|
-
setSortMode(
|
|
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(
|
|
299
|
-
|
|
300
|
-
|
|
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(
|
|
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
|
|
5
|
-
"mappings": ";
|
|
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("
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
5
|
-
"mappings": ";
|
|
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
|
}
|