@open-mercato/core 0.5.1-develop.2996.ce62fd491c → 0.5.1-develop.3032.01699048cb
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/dist/modules/customers/api/companies/[id]/route.js +30 -20
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/companies/route.js +12 -7
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +12 -7
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +12 -7
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +21 -0
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +27 -30
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +56 -0
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesCard.js +175 -0
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +324 -0
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesSection.js +62 -13
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityLogTab.js +14 -23
- package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +13 -13
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +35 -22
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
- package/dist/modules/customers/components/detail/AiActionChips.js +15 -22
- package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +196 -28
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/FooterFields.js +14 -2
- package/dist/modules/customers/components/detail/schedule/FooterFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +9 -2
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +9 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js +25 -4
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +20 -3
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/package.json +3 -3
- package/src/modules/customers/api/companies/[id]/route.ts +30 -20
- package/src/modules/customers/api/companies/route.ts +12 -7
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +12 -7
- package/src/modules/customers/api/people/route.ts +12 -7
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +22 -0
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +28 -21
- package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +67 -0
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +231 -0
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +390 -0
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +91 -40
- package/src/modules/customers/components/detail/ActivityLogTab.tsx +25 -23
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +15 -19
- package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +36 -29
- package/src/modules/customers/components/detail/AiActionChips.tsx +17 -23
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +233 -41
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +6 -2
- package/src/modules/customers/components/detail/schedule/FooterFields.tsx +22 -2
- package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +10 -2
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +10 -2
- package/src/modules/customers/components/detail/schedule/fieldConfig.ts +26 -6
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +32 -3
- package/src/modules/customers/i18n/de.json +69 -2
- package/src/modules/customers/i18n/en.json +69 -2
- package/src/modules/customers/i18n/es.json +69 -2
- package/src/modules/customers/i18n/pl.json +68 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { Clock } from "lucide-react";
|
|
4
|
+
import { Clock, Search } from "lucide-react";
|
|
5
5
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
6
|
import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
7
7
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
8
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
|
+
import { Kbd } from "@open-mercato/ui/primitives/kbd";
|
|
9
10
|
import { ActivityTimelineFilters } from "./ActivityTimelineFilters.js";
|
|
10
11
|
import { ActivityTimeline } from "./ActivityTimeline.js";
|
|
11
12
|
function toDateOnly(value) {
|
|
@@ -77,10 +78,37 @@ function ActivitiesSection({
|
|
|
77
78
|
const [filterTypes, setFilterTypes] = React.useState([]);
|
|
78
79
|
const [filterDateFrom, setFilterDateFrom] = React.useState("");
|
|
79
80
|
const [filterDateTo, setFilterDateTo] = React.useState("");
|
|
81
|
+
const [searchTerm, setSearchTerm] = React.useState("");
|
|
80
82
|
const [activities, setActivities] = React.useState([]);
|
|
81
83
|
const [loading, setLoading] = React.useState(false);
|
|
82
84
|
const [hasMore, setHasMore] = React.useState(false);
|
|
83
85
|
const [loadedPages, setLoadedPages] = React.useState(1);
|
|
86
|
+
const searchInputRef = React.useRef(null);
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
if (!entityId) return;
|
|
89
|
+
function handleShortcut(event) {
|
|
90
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "1") {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
searchInputRef.current?.focus();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
window.addEventListener("keydown", handleShortcut);
|
|
96
|
+
return () => window.removeEventListener("keydown", handleShortcut);
|
|
97
|
+
}, [entityId]);
|
|
98
|
+
const visibleActivities = React.useMemo(() => {
|
|
99
|
+
const term = searchTerm.trim().toLowerCase();
|
|
100
|
+
if (!term) return activities;
|
|
101
|
+
return activities.filter((activity) => {
|
|
102
|
+
const haystack = [
|
|
103
|
+
activity.title,
|
|
104
|
+
activity.body,
|
|
105
|
+
activity.authorName,
|
|
106
|
+
activity.dealTitle,
|
|
107
|
+
activity.interactionType
|
|
108
|
+
].filter((value) => typeof value === "string" && value.length > 0).join(" ").toLowerCase();
|
|
109
|
+
return haystack.includes(term);
|
|
110
|
+
});
|
|
111
|
+
}, [activities, searchTerm]);
|
|
84
112
|
React.useEffect(() => {
|
|
85
113
|
onActionChange?.(null);
|
|
86
114
|
return () => onActionChange?.(null);
|
|
@@ -212,12 +240,30 @@ function ActivitiesSection({
|
|
|
212
240
|
}).catch((err) => console.warn("[ActivitiesSection] resolve author names failed", err));
|
|
213
241
|
return () => controller.abort();
|
|
214
242
|
}, [activities]);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
243
|
+
const totalCount = activities.length;
|
|
244
|
+
const visibleCount = visibleActivities.length;
|
|
245
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3.5 rounded-[10px] border border-border bg-card pt-4 pb-[18px] px-[18px]", children: [
|
|
246
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
247
|
+
/* @__PURE__ */ jsx(Clock, { className: "size-[15px] text-muted-foreground" }),
|
|
248
|
+
/* @__PURE__ */ jsx("h3", { className: "text-[13px] font-semibold text-foreground", children: entityName ? t("customers.timeline.history.title", "Interaction history with {{name}}", { name: entityName }) : t("customers.timeline.history.titleGeneric", "Interaction history") })
|
|
249
|
+
] }),
|
|
250
|
+
/* @__PURE__ */ jsxs("label", { className: "relative flex items-center", children: [
|
|
251
|
+
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 size-5 text-muted-foreground", "aria-hidden": true }),
|
|
252
|
+
/* @__PURE__ */ jsx(
|
|
253
|
+
"input",
|
|
254
|
+
{
|
|
255
|
+
ref: searchInputRef,
|
|
256
|
+
type: "search",
|
|
257
|
+
value: searchTerm,
|
|
258
|
+
onChange: (event) => setSearchTerm(event.target.value),
|
|
259
|
+
placeholder: t("customers.timeline.history.searchPlaceholder", "Search..."),
|
|
260
|
+
"aria-label": t("customers.timeline.history.searchAriaLabel", "Search interaction history"),
|
|
261
|
+
className: "h-9 w-full rounded-[10px] border border-border bg-card pl-9 pr-14 text-sm text-foreground shadow-xs placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden"
|
|
262
|
+
}
|
|
263
|
+
),
|
|
264
|
+
/* @__PURE__ */ jsx(Kbd, { className: "pointer-events-none absolute right-2 hidden text-[11px] uppercase tracking-[0.48px] sm:inline-flex", children: "\u23181" })
|
|
265
|
+
] }),
|
|
266
|
+
/* @__PURE__ */ jsx(
|
|
221
267
|
ActivityTimelineFilters,
|
|
222
268
|
{
|
|
223
269
|
entityId,
|
|
@@ -233,13 +279,16 @@ function ActivitiesSection({
|
|
|
233
279
|
setFilterDateTo("");
|
|
234
280
|
}
|
|
235
281
|
}
|
|
236
|
-
)
|
|
237
|
-
loading &&
|
|
238
|
-
/* @__PURE__ */ jsx(ActivityTimeline, { activities, onEdit: onEditActivity }),
|
|
239
|
-
|
|
240
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("customers.activities.
|
|
282
|
+
),
|
|
283
|
+
loading && totalCount === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-dashed border-border/70 px-4 py-8 text-sm text-muted-foreground", children: t("customers.people.detail.activities.loading", "Loading activities\u2026") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
284
|
+
/* @__PURE__ */ jsx(ActivityTimeline, { activities: visibleActivities, onEdit: onEditActivity }),
|
|
285
|
+
totalCount > 0 ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-t border-border/60 pt-3", children: [
|
|
286
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: searchTerm.trim() ? t("customers.activities.seeMatching", "Showing {visible} of {total} activities", {
|
|
287
|
+
visible: visibleCount,
|
|
288
|
+
total: totalCount
|
|
289
|
+
}) : t("customers.activities.seeAll", "See all {count} activities", { count: totalCount }) }),
|
|
241
290
|
hasMore ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => setLoadedPages((value) => value + 1), children: t("customers.activities.loadMore", "Load more") }) : null
|
|
242
|
-
] })
|
|
291
|
+
] }) : null
|
|
243
292
|
] })
|
|
244
293
|
] });
|
|
245
294
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/ActivitiesSection.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Clock } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport type { SectionAction, TabEmptyStateConfig } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { ActivityTimelineFilters } from './ActivityTimelineFilters'\nimport { ActivityTimeline } from './ActivityTimeline'\nimport type { ActivitySummary, InteractionSummary } from './types'\n\ntype GuardedMutationRunner = <T>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\nexport type ActivitiesSectionProps = {\n entityId: string | null\n entityName?: string | null\n dealId?: string | null\n useCanonicalInteractions?: boolean\n addActionLabel: string\n emptyState: TabEmptyStateConfig\n onActionChange?: (action: SectionAction | null) => void\n onLoadingChange?: (isLoading: boolean) => void\n onDataRefresh?: () => void\n dealOptions?: Array<{ id: string; label: string }>\n entityOptions?: Array<{ id: string; label: string }>\n defaultEntityId?: string | null\n runGuardedMutation?: GuardedMutationRunner\n refreshKey?: number\n onEditActivity?: (activity: InteractionSummary) => void\n}\n\nfunction toDateOnly(value: string | null | undefined): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)\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) {\n return leftIsUpcoming ? -1 : 1\n }\n\n if (leftIsUpcoming && rightIsUpcoming) {\n if (leftScheduled === rightScheduled) return left.id.localeCompare(right.id)\n return leftScheduled - rightScheduled\n }\n\n const leftTime = left.occurredAt ?? left.createdAt\n const rightTime = right.occurredAt ?? right.createdAt\n const compare = rightTime.localeCompare(leftTime)\n if (compare !== 0) return compare\n return right.id.localeCompare(left.id)\n })\n}\n\nexport function ActivitiesSection({\n entityId,\n entityName,\n dealId,\n useCanonicalInteractions = false,\n onActionChange,\n onLoadingChange,\n refreshKey = 0,\n onEditActivity,\n}: ActivitiesSectionProps) {\n const t = useT()\n const [filterTypes, setFilterTypes] = React.useState<string[]>([])\n const [filterDateFrom, setFilterDateFrom] = React.useState('')\n const [filterDateTo, setFilterDateTo] = React.useState('')\n const [activities, setActivities] = React.useState<InteractionSummary[]>([])\n const [loading, setLoading] = React.useState(false)\n const [hasMore, setHasMore] = React.useState(false)\n const [loadedPages, setLoadedPages] = React.useState(1)\n\n React.useEffect(() => {\n onActionChange?.(null)\n return () => onActionChange?.(null)\n }, [onActionChange])\n\n React.useEffect(() => {\n onLoadingChange?.(loading)\n }, [loading, onLoadingChange])\n\n const loadActivities = React.useCallback(async () => {\n if (!entityId) {\n setActivities([])\n return\n }\n\n setLoading(true)\n try {\n // Always fetch canonical interactions (new activities are always created here)\n const canonicalParams = new URLSearchParams({\n entityId,\n limit: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n excludeInteractionType: 'task',\n })\n if (dealId) canonicalParams.set('dealId', dealId)\n if (filterTypes.length > 0) canonicalParams.set('type', filterTypes.join(','))\n if (filterDateFrom) canonicalParams.set('from', filterDateFrom)\n if (filterDateTo) canonicalParams.set('to', filterDateTo)\n\n const canonicalItems: InteractionSummary[] = []\n let canonicalCursor: string | undefined\n let canonicalHasMore = false\n let pageIndex = 0\n do {\n const params = new URLSearchParams(canonicalParams)\n if (canonicalCursor) params.set('cursor', canonicalCursor)\n const canonicalPayload = await readApiResultOrThrow<{ items?: InteractionSummary[]; nextCursor?: string }>(\n `/api/customers/interactions?${params.toString()}`,\n ).catch(() => ({ items: [] as InteractionSummary[], nextCursor: undefined }))\n canonicalItems.push(...(Array.isArray(canonicalPayload?.items) ? canonicalPayload.items : []))\n canonicalCursor = typeof canonicalPayload?.nextCursor === 'string' ? canonicalPayload.nextCursor : undefined\n canonicalHasMore = Boolean(canonicalCursor)\n pageIndex += 1\n } while (canonicalCursor && pageIndex < loadedPages)\n\n if (useCanonicalInteractions) {\n setActivities(sortTimelineActivities(canonicalItems))\n setHasMore(canonicalHasMore)\n return\n }\n\n // In legacy mode, also fetch legacy activities and merge with canonical\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (let legacyPage = 1; legacyPage <= loadedPages; legacyPage += 1) {\n const legacyParams = new URLSearchParams({\n entityId,\n page: String(legacyPage),\n pageSize: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n if (dealId) legacyParams.set('dealId', dealId)\n const legacyPayload = await readApiResultOrThrow<{ items?: ActivitySummary[]; totalPages?: number }>(\n `/api/customers/activities?${legacyParams.toString()}`,\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 }\n const legacyFiltered = legacyItems.filter((entry) => {\n if (filterTypes.length > 0 && !filterTypes.includes(entry.interactionType)) return false\n const dateOnly = toDateOnly(entry.occurredAt ?? entry.createdAt)\n if (filterDateFrom && dateOnly < filterDateFrom) return false\n if (filterDateTo && dateOnly > filterDateTo) return false\n return true\n })\n\n // Merge and deduplicate by id, sort newest first\n const seen = new Set<string>()\n const merged: InteractionSummary[] = []\n for (const item of [...canonicalItems, ...legacyFiltered]) {\n if (!seen.has(item.id)) {\n seen.add(item.id)\n merged.push(item)\n }\n }\n setActivities(sortTimelineActivities(merged))\n setHasMore(canonicalHasMore || legacyTotalPages > loadedPages)\n } catch (error) {\n console.error('customers.activities.history failed', error)\n flash(t('customers.activities.loadFailed', 'Failed to load activities.'), 'error')\n setActivities([])\n setHasMore(false)\n } finally {\n setLoading(false)\n }\n }, [dealId, entityId, filterDateFrom, filterDateTo, filterTypes, loadedPages, useCanonicalInteractions, refreshKey, t])\n\n React.useEffect(() => {\n setLoadedPages(1)\n }, [dealId, entityId, filterDateFrom, filterDateTo, filterTypes, useCanonicalInteractions])\n\n const resolvedUserIdsRef = React.useRef(new Set<string>())\n\n // Resolve missing author names from user IDs\n React.useEffect(() => {\n loadActivities()\n .then(() => { resolvedUserIdsRef.current = new Set() })\n .catch((err) => console.warn('[ActivitiesSection] loadActivities failed', err))\n }, [loadActivities])\n\n React.useEffect(() => {\n const unresolvedIds = new Set<string>()\n for (const a of activities) {\n if (a.authorUserId && !a.authorName && !resolvedUserIdsRef.current.has(a.authorUserId)) {\n unresolvedIds.add(a.authorUserId)\n }\n }\n if (unresolvedIds.size === 0) return\n\n for (const uid of unresolvedIds) resolvedUserIdsRef.current.add(uid)\n\n const controller = new AbortController()\n readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/auth/users?ids=${[...unresolvedIds].join(',')}`,\n { signal: controller.signal },\n )\n .then((data) => {\n const users = Array.isArray(data?.items) ? data.items : []\n const nameMap = new Map<string, string>()\n for (const user of users) {\n const userId = typeof user.id === 'string' ? user.id : null\n const name = typeof user.display_name === 'string' && user.display_name.trim()\n ? user.display_name.trim()\n : typeof user.email === 'string'\n ? user.email\n : null\n if (userId && name) nameMap.set(userId, name)\n }\n if (nameMap.size > 0) {\n setActivities((prev) =>\n prev.map((a) => {\n if (a.authorUserId && !a.authorName && nameMap.has(a.authorUserId)) {\n return { ...a, authorName: nameMap.get(a.authorUserId) ?? null }\n }\n return a\n }),\n )\n }\n })\n .catch((err) => console.warn('[ActivitiesSection] resolve author names failed', err))\n return () => controller.abort()\n }, [activities])\n\n return (\n <div className=\"rounded-2xl border border-border/70 bg-card p-5\">\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <Clock className=\"size-4 text-muted-foreground\" />\n <h3 className=\"text-base font-semibold text-foreground\">\n {entityName\n ? t('customers.timeline.history.title', 'Interaction history with {{name}}', { name: entityName })\n : t('customers.timeline.history.titleGeneric', 'Interaction history')}\n </h3>\n </div>\n </div>\n\n <div className=\"mb-4\">\n <ActivityTimelineFilters\n entityId={entityId}\n activeTypes={filterTypes}\n dateFrom={filterDateFrom}\n dateTo={filterDateTo}\n onTypesChange={setFilterTypes}\n onDateFromChange={setFilterDateFrom}\n onDateToChange={setFilterDateTo}\n onReset={() => {\n setFilterTypes([])\n setFilterDateFrom('')\n setFilterDateTo('')\n }}\n />\n </div>\n\n {loading && activities.length === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border/70 px-4 py-8 text-sm text-muted-foreground\">\n {t('customers.people.detail.activities.loading', 'Loading activities\u2026')}\n </div>\n ) : (\n <>\n <ActivityTimeline activities={activities} onEdit={onEditActivity} />\n {activities.length > 0 ? (\n <div className=\"border-t px-5 py-3\">\n <div className=\"flex items-center justify-between gap-3\">\n <span className=\"text-xs text-muted-foreground\">\n {t('customers.activities.seeAll', 'See all {count} activities', { count: activities.length })}\n </span>\n {hasMore ? (\n <Button type=\"button\" variant=\"link\" size=\"sm\" onClick={() => setLoadedPages((value) => value + 1)}>\n {t('customers.activities.loadMore', 'Load more')}\n </Button>\n ) : null}\n </div>\n </div>\n ) : null}\n </>\n )}\n </div>\n )\n}\n\nexport default ActivitiesSection\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Clock, Search } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport type { SectionAction, TabEmptyStateConfig } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Kbd } from '@open-mercato/ui/primitives/kbd'\nimport { ActivityTimelineFilters } from './ActivityTimelineFilters'\nimport { ActivityTimeline } from './ActivityTimeline'\nimport type { ActivitySummary, InteractionSummary } from './types'\n\ntype GuardedMutationRunner = <T>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\nexport type ActivitiesSectionProps = {\n entityId: string | null\n entityName?: string | null\n dealId?: string | null\n useCanonicalInteractions?: boolean\n addActionLabel: string\n emptyState: TabEmptyStateConfig\n onActionChange?: (action: SectionAction | null) => void\n onLoadingChange?: (isLoading: boolean) => void\n onDataRefresh?: () => void\n dealOptions?: Array<{ id: string; label: string }>\n entityOptions?: Array<{ id: string; label: string }>\n defaultEntityId?: string | null\n runGuardedMutation?: GuardedMutationRunner\n refreshKey?: number\n onEditActivity?: (activity: InteractionSummary) => void\n}\n\nfunction toDateOnly(value: string | null | undefined): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)\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) {\n return leftIsUpcoming ? -1 : 1\n }\n\n if (leftIsUpcoming && rightIsUpcoming) {\n if (leftScheduled === rightScheduled) return left.id.localeCompare(right.id)\n return leftScheduled - rightScheduled\n }\n\n const leftTime = left.occurredAt ?? left.createdAt\n const rightTime = right.occurredAt ?? right.createdAt\n const compare = rightTime.localeCompare(leftTime)\n if (compare !== 0) return compare\n return right.id.localeCompare(left.id)\n })\n}\n\nexport function ActivitiesSection({\n entityId,\n entityName,\n dealId,\n useCanonicalInteractions = false,\n onActionChange,\n onLoadingChange,\n refreshKey = 0,\n onEditActivity,\n}: ActivitiesSectionProps) {\n const t = useT()\n const [filterTypes, setFilterTypes] = React.useState<string[]>([])\n const [filterDateFrom, setFilterDateFrom] = React.useState('')\n const [filterDateTo, setFilterDateTo] = React.useState('')\n const [searchTerm, setSearchTerm] = React.useState('')\n const [activities, setActivities] = React.useState<InteractionSummary[]>([])\n const [loading, setLoading] = React.useState(false)\n const [hasMore, setHasMore] = React.useState(false)\n const [loadedPages, setLoadedPages] = React.useState(1)\n const searchInputRef = React.useRef<HTMLInputElement>(null)\n\n React.useEffect(() => {\n if (!entityId) return\n function handleShortcut(event: KeyboardEvent) {\n if ((event.metaKey || event.ctrlKey) && event.key === '1') {\n event.preventDefault()\n searchInputRef.current?.focus()\n }\n }\n window.addEventListener('keydown', handleShortcut)\n return () => window.removeEventListener('keydown', handleShortcut)\n }, [entityId])\n\n const visibleActivities = React.useMemo(() => {\n const term = searchTerm.trim().toLowerCase()\n if (!term) return activities\n return activities.filter((activity) => {\n const haystack = [\n activity.title,\n activity.body,\n activity.authorName,\n activity.dealTitle,\n activity.interactionType,\n ]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .join(' ')\n .toLowerCase()\n return haystack.includes(term)\n })\n }, [activities, searchTerm])\n\n React.useEffect(() => {\n onActionChange?.(null)\n return () => onActionChange?.(null)\n }, [onActionChange])\n\n React.useEffect(() => {\n onLoadingChange?.(loading)\n }, [loading, onLoadingChange])\n\n const loadActivities = React.useCallback(async () => {\n if (!entityId) {\n setActivities([])\n return\n }\n\n setLoading(true)\n try {\n // Always fetch canonical interactions (new activities are always created here)\n const canonicalParams = new URLSearchParams({\n entityId,\n limit: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n excludeInteractionType: 'task',\n })\n if (dealId) canonicalParams.set('dealId', dealId)\n if (filterTypes.length > 0) canonicalParams.set('type', filterTypes.join(','))\n if (filterDateFrom) canonicalParams.set('from', filterDateFrom)\n if (filterDateTo) canonicalParams.set('to', filterDateTo)\n\n const canonicalItems: InteractionSummary[] = []\n let canonicalCursor: string | undefined\n let canonicalHasMore = false\n let pageIndex = 0\n do {\n const params = new URLSearchParams(canonicalParams)\n if (canonicalCursor) params.set('cursor', canonicalCursor)\n const canonicalPayload = await readApiResultOrThrow<{ items?: InteractionSummary[]; nextCursor?: string }>(\n `/api/customers/interactions?${params.toString()}`,\n ).catch(() => ({ items: [] as InteractionSummary[], nextCursor: undefined }))\n canonicalItems.push(...(Array.isArray(canonicalPayload?.items) ? canonicalPayload.items : []))\n canonicalCursor = typeof canonicalPayload?.nextCursor === 'string' ? canonicalPayload.nextCursor : undefined\n canonicalHasMore = Boolean(canonicalCursor)\n pageIndex += 1\n } while (canonicalCursor && pageIndex < loadedPages)\n\n if (useCanonicalInteractions) {\n setActivities(sortTimelineActivities(canonicalItems))\n setHasMore(canonicalHasMore)\n return\n }\n\n // In legacy mode, also fetch legacy activities and merge with canonical\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (let legacyPage = 1; legacyPage <= loadedPages; legacyPage += 1) {\n const legacyParams = new URLSearchParams({\n entityId,\n page: String(legacyPage),\n pageSize: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n if (dealId) legacyParams.set('dealId', dealId)\n const legacyPayload = await readApiResultOrThrow<{ items?: ActivitySummary[]; totalPages?: number }>(\n `/api/customers/activities?${legacyParams.toString()}`,\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 }\n const legacyFiltered = legacyItems.filter((entry) => {\n if (filterTypes.length > 0 && !filterTypes.includes(entry.interactionType)) return false\n const dateOnly = toDateOnly(entry.occurredAt ?? entry.createdAt)\n if (filterDateFrom && dateOnly < filterDateFrom) return false\n if (filterDateTo && dateOnly > filterDateTo) return false\n return true\n })\n\n // Merge and deduplicate by id, sort newest first\n const seen = new Set<string>()\n const merged: InteractionSummary[] = []\n for (const item of [...canonicalItems, ...legacyFiltered]) {\n if (!seen.has(item.id)) {\n seen.add(item.id)\n merged.push(item)\n }\n }\n setActivities(sortTimelineActivities(merged))\n setHasMore(canonicalHasMore || legacyTotalPages > loadedPages)\n } catch (error) {\n console.error('customers.activities.history failed', error)\n flash(t('customers.activities.loadFailed', 'Failed to load activities.'), 'error')\n setActivities([])\n setHasMore(false)\n } finally {\n setLoading(false)\n }\n }, [dealId, entityId, filterDateFrom, filterDateTo, filterTypes, loadedPages, useCanonicalInteractions, refreshKey, t])\n\n React.useEffect(() => {\n setLoadedPages(1)\n }, [dealId, entityId, filterDateFrom, filterDateTo, filterTypes, useCanonicalInteractions])\n\n const resolvedUserIdsRef = React.useRef(new Set<string>())\n\n // Resolve missing author names from user IDs\n React.useEffect(() => {\n loadActivities()\n .then(() => { resolvedUserIdsRef.current = new Set() })\n .catch((err) => console.warn('[ActivitiesSection] loadActivities failed', err))\n }, [loadActivities])\n\n React.useEffect(() => {\n const unresolvedIds = new Set<string>()\n for (const a of activities) {\n if (a.authorUserId && !a.authorName && !resolvedUserIdsRef.current.has(a.authorUserId)) {\n unresolvedIds.add(a.authorUserId)\n }\n }\n if (unresolvedIds.size === 0) return\n\n for (const uid of unresolvedIds) resolvedUserIdsRef.current.add(uid)\n\n const controller = new AbortController()\n readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/auth/users?ids=${[...unresolvedIds].join(',')}`,\n { signal: controller.signal },\n )\n .then((data) => {\n const users = Array.isArray(data?.items) ? data.items : []\n const nameMap = new Map<string, string>()\n for (const user of users) {\n const userId = typeof user.id === 'string' ? user.id : null\n const name = typeof user.display_name === 'string' && user.display_name.trim()\n ? user.display_name.trim()\n : typeof user.email === 'string'\n ? user.email\n : null\n if (userId && name) nameMap.set(userId, name)\n }\n if (nameMap.size > 0) {\n setActivities((prev) =>\n prev.map((a) => {\n if (a.authorUserId && !a.authorName && nameMap.has(a.authorUserId)) {\n return { ...a, authorName: nameMap.get(a.authorUserId) ?? null }\n }\n return a\n }),\n )\n }\n })\n .catch((err) => console.warn('[ActivitiesSection] resolve author names failed', err))\n return () => controller.abort()\n }, [activities])\n\n const totalCount = activities.length\n const visibleCount = visibleActivities.length\n\n return (\n <div className=\"flex flex-col gap-3.5 rounded-[10px] border border-border bg-card pt-4 pb-[18px] px-[18px]\">\n <div className=\"flex items-center gap-2\">\n <Clock className=\"size-[15px] text-muted-foreground\" />\n <h3 className=\"text-[13px] font-semibold text-foreground\">\n {entityName\n ? t('customers.timeline.history.title', 'Interaction history with {{name}}', { name: entityName })\n : t('customers.timeline.history.titleGeneric', 'Interaction history')}\n </h3>\n </div>\n\n <label className=\"relative flex items-center\">\n <Search className=\"pointer-events-none absolute left-2.5 size-5 text-muted-foreground\" aria-hidden />\n <input\n ref={searchInputRef}\n type=\"search\"\n value={searchTerm}\n onChange={(event) => setSearchTerm(event.target.value)}\n placeholder={t('customers.timeline.history.searchPlaceholder', 'Search...')}\n aria-label={t('customers.timeline.history.searchAriaLabel', 'Search interaction history')}\n className=\"h-9 w-full rounded-[10px] border border-border bg-card pl-9 pr-14 text-sm text-foreground shadow-xs placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden\"\n />\n <Kbd className=\"pointer-events-none absolute right-2 hidden text-[11px] uppercase tracking-[0.48px] sm:inline-flex\">\n \u23181\n </Kbd>\n </label>\n\n <ActivityTimelineFilters\n entityId={entityId}\n activeTypes={filterTypes}\n dateFrom={filterDateFrom}\n dateTo={filterDateTo}\n onTypesChange={setFilterTypes}\n onDateFromChange={setFilterDateFrom}\n onDateToChange={setFilterDateTo}\n onReset={() => {\n setFilterTypes([])\n setFilterDateFrom('')\n setFilterDateTo('')\n }}\n />\n\n {loading && totalCount === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border/70 px-4 py-8 text-sm text-muted-foreground\">\n {t('customers.people.detail.activities.loading', 'Loading activities\u2026')}\n </div>\n ) : (\n <>\n <ActivityTimeline activities={visibleActivities} onEdit={onEditActivity} />\n {totalCount > 0 ? (\n <div className=\"flex items-center justify-between gap-3 border-t border-border/60 pt-3\">\n <span className=\"text-xs text-muted-foreground\">\n {searchTerm.trim()\n ? t('customers.activities.seeMatching', 'Showing {visible} of {total} activities', {\n visible: visibleCount,\n total: totalCount,\n })\n : t('customers.activities.seeAll', 'See all {count} activities', { count: totalCount })}\n </span>\n {hasMore ? (\n <Button type=\"button\" variant=\"link\" size=\"sm\" onClick={() => setLoadedPages((value) => value + 1)}>\n {t('customers.activities.loadMore', 'Load more')}\n </Button>\n ) : null}\n </div>\n ) : null}\n </>\n )}\n </div>\n )\n}\n\nexport default ActivitiesSection\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,40 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
import { PlannedActivitiesSection } from "./PlannedActivitiesSection.js";
|
|
3
|
+
import { ActivitiesCard } from "./ActivitiesCard.js";
|
|
5
4
|
import { ActivityHistorySection } from "./ActivityHistorySection.js";
|
|
6
5
|
function ActivityLogTab({
|
|
7
6
|
entityId,
|
|
8
7
|
plannedActivities,
|
|
9
|
-
onActivityCreated,
|
|
10
8
|
onScheduleRequested,
|
|
11
|
-
|
|
9
|
+
onAddActivity,
|
|
12
10
|
onEditActivity,
|
|
13
|
-
onCancelActivity,
|
|
14
|
-
runGuardedMutation,
|
|
15
11
|
refreshKey = 0,
|
|
16
|
-
useCanonicalInteractions = false
|
|
12
|
+
useCanonicalInteractions = false,
|
|
13
|
+
entityCompanyName
|
|
17
14
|
}) {
|
|
15
|
+
const handleAddNew = (kind) => {
|
|
16
|
+
if (onAddActivity) onAddActivity(kind);
|
|
17
|
+
else onScheduleRequested();
|
|
18
|
+
};
|
|
18
19
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
19
20
|
/* @__PURE__ */ jsx(
|
|
20
|
-
|
|
21
|
+
ActivitiesCard,
|
|
21
22
|
{
|
|
22
|
-
entityType: "company",
|
|
23
23
|
entityId,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
),
|
|
30
|
-
/* @__PURE__ */ jsx(
|
|
31
|
-
PlannedActivitiesSection,
|
|
32
|
-
{
|
|
33
|
-
activities: plannedActivities,
|
|
34
|
-
onComplete: onMarkDone,
|
|
35
|
-
onSchedule: onScheduleRequested,
|
|
36
|
-
onEdit: onEditActivity,
|
|
37
|
-
onCancel: onCancelActivity
|
|
24
|
+
plannedActivities,
|
|
25
|
+
refreshKey,
|
|
26
|
+
onAddNew: handleAddNew,
|
|
27
|
+
onEditActivity,
|
|
28
|
+
entityCompanyName
|
|
38
29
|
}
|
|
39
30
|
),
|
|
40
31
|
/* @__PURE__ */ jsx(
|
|
@@ -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 {
|
|
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 /** @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;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -52,30 +52,30 @@ function TimelineEntry({
|
|
|
52
52
|
return /* @__PURE__ */ jsx(
|
|
53
53
|
"div",
|
|
54
54
|
{
|
|
55
|
-
className: `
|
|
55
|
+
className: `py-2.5 ${withBorder ? "border-b border-border/60" : ""} ${onEdit ? "cursor-pointer hover:bg-accent/40 transition-colors" : ""}`,
|
|
56
56
|
onClick: () => onEdit?.(activity),
|
|
57
57
|
role: onEdit ? "button" : void 0,
|
|
58
58
|
tabIndex: onEdit ? 0 : void 0,
|
|
59
59
|
onKeyDown: onEdit ? (e) => {
|
|
60
60
|
if (e.key === "Enter") onEdit(activity);
|
|
61
61
|
} : void 0,
|
|
62
|
-
children: /* @__PURE__ */ jsxs("div", { className: "grid items-start gap-3", style: { gridTemplateColumns: "
|
|
63
|
-
/* @__PURE__ */ jsxs("div", { className: "shrink-0
|
|
64
|
-
/* @__PURE__ */ jsx("span", { className: "block text-
|
|
65
|
-
/* @__PURE__ */ jsx("span", { className: "block text-
|
|
62
|
+
children: /* @__PURE__ */ jsxs("div", { className: "grid items-start gap-3", style: { gridTemplateColumns: "75px 32px 1fr" }, children: [
|
|
63
|
+
/* @__PURE__ */ jsxs("div", { className: "shrink-0", children: [
|
|
64
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[11px] font-semibold leading-tight text-foreground", children: formatRelativeDate(dateStr, t) }),
|
|
65
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[10px] leading-tight text-muted-foreground", children: formatTime(dateStr) })
|
|
66
66
|
] }),
|
|
67
|
-
/* @__PURE__ */ jsx("div", { className: "flex size-
|
|
68
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
69
|
-
/* @__PURE__ */
|
|
67
|
+
/* @__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
|
+
/* @__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
70
|
title,
|
|
71
71
|
duration
|
|
72
|
-
] })
|
|
73
|
-
activity.body && activity.title && /* @__PURE__ */ jsx("p", { className: "
|
|
74
|
-
activity.authorName && /* @__PURE__ */ jsxs("div", { className: "
|
|
75
|
-
/* @__PURE__ */ jsx(User, { className: "size-
|
|
72
|
+
] }),
|
|
73
|
+
activity.body && activity.title && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: activity.body }),
|
|
74
|
+
activity.authorName && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-[10px] text-muted-foreground", children: [
|
|
75
|
+
/* @__PURE__ */ jsx(User, { className: "size-2.5 shrink-0" }),
|
|
76
76
|
/* @__PURE__ */ jsx("span", { children: t("customers.timeline.author", "by {{name}}", { name: activity.authorName }) })
|
|
77
77
|
] }),
|
|
78
|
-
/* @__PURE__ */ jsx(
|
|
78
|
+
/* @__PURE__ */ jsx(AiActionChips, { activityType: activity.interactionType })
|
|
79
79
|
] })
|
|
80
80
|
] })
|
|
81
81
|
}
|
|
@@ -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={`
|
|
5
|
-
"mappings": ";AAyBM,cAkBQ,YAlBR;AAxBN,YAAY,WAAW;AACvB,SAAS,OAAO,MAAM,OAAO,YAAY,YAAY;AACrD,SAAS,YAAY;AAErB,SAAS,qBAAqB;AAG9B,MAAM,aAA0E;AAAA,EAC9E,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AACR;AAOO,SAAS,iBAAiB,EAAE,YAAY,OAAO,GAA0B;AAC9E,QAAM,IAAI,KAAK;AAEf,MAAI,WAAW,WAAW,GAAG;AAC3B,WACE,oBAAC,SAAI,WAAU,kDACZ,YAAE,4BAA4B,0CAA0C,GAC3E;AAAA,EAEJ;AAEA,SACE,oBAAC,SACE,qBAAW,IAAI,CAAC,UAAU,UAAU;AACnC,UAAM,UAAU,SAAS,eAAe,SAAS,cAAc,SAAS;AACxE,UAAM,eAAe,UAAU,IAAI,KAAK,OAAO,EAAE,YAAY,IAAI;AACjE,UAAM,cAAc,QAAQ,IAAK,WAAW,QAAQ,CAAC,EAAE,eAAe,WAAW,QAAQ,CAAC,EAAE,cAAc,WAAW,QAAQ,CAAC,EAAE,YAAa;AAC7I,UAAM,WAAW,cAAc,IAAI,KAAK,WAAW,EAAE,YAAY,IAAI;AACrE,UAAM,oBAAoB,iBAAiB,QAAQ,aAAa,QAAQ,iBAAiB;AAEzF,WACE,qBAAC,MAAM,UAAN,EACE;AAAA,2BACC,qBAAC,SAAI,WAAU,qCACb;AAAA,4BAAC,SAAI,WAAU,yBAAwB;AAAA,QACvC,oBAAC,UAAK,WAAU,+CACb,YAAE,sCAAsC,UAAU,EAAE,MAAM,aAAa,CAAC,GAC3E;AAAA,QACA,oBAAC,SAAI,WAAU,yBAAwB;AAAA,SACzC;AAAA,MAEF;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,YAAY,QAAQ,WAAW,SAAS;AAAA,UACxC;AAAA;AAAA,MACF;AAAA,SAfmB,SAAS,EAgB9B;AAAA,EAEJ,CAAC,GACH;AAEJ;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,UAAU,SAAS,eAAe,SAAS,cAAc,SAAS;AACxE,QAAM,WAAW,WAAW,SAAS,eAAe;AACpD,QAAM,QAAQ,SAAS,SAAS,SAAS,QAAQ,SAAS;AAC1D,QAAM,WAAW,SAAS,WAAW,KAAK,SAAS,QAAQ,UAAU;AAErE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,
|
|
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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,14 +5,18 @@ import { Phone, Mail, Users, StickyNote, SlidersHorizontal } from "lucide-react"
|
|
|
5
5
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
6
6
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
7
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
|
+
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
8
9
|
import { Popover, PopoverContent, PopoverTrigger } from "@open-mercato/ui/primitives/popover";
|
|
9
10
|
import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
10
11
|
const FILTER_TYPES = [
|
|
12
|
+
{ type: "note", icon: StickyNote },
|
|
11
13
|
{ type: "call", icon: Phone },
|
|
12
|
-
{ type: "email", icon: Mail },
|
|
13
14
|
{ type: "meeting", icon: Users },
|
|
14
|
-
{ type: "
|
|
15
|
+
{ type: "email", icon: Mail }
|
|
15
16
|
];
|
|
17
|
+
const CHIP_BASE = "inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors";
|
|
18
|
+
const CHIP_INACTIVE = "border border-border bg-card text-muted-foreground hover:bg-accent/40";
|
|
19
|
+
const CHIP_ACTIVE = "border border-status-info-border bg-status-info-bg text-status-info-text";
|
|
16
20
|
function ActivityTimelineFilters({
|
|
17
21
|
entityId,
|
|
18
22
|
activeTypes,
|
|
@@ -25,6 +29,7 @@ function ActivityTimelineFilters({
|
|
|
25
29
|
}) {
|
|
26
30
|
const t = useT();
|
|
27
31
|
const hasActiveFilters = activeTypes.length > 0 || dateFrom || dateTo;
|
|
32
|
+
const allActive = activeTypes.length === 0;
|
|
28
33
|
const [counts, setCounts] = React.useState(null);
|
|
29
34
|
React.useEffect(() => {
|
|
30
35
|
if (!entityId) return;
|
|
@@ -49,28 +54,38 @@ function ActivityTimelineFilters({
|
|
|
49
54
|
onTypesChange([...activeTypes, type]);
|
|
50
55
|
}
|
|
51
56
|
}, [activeTypes, onTypesChange]);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
const handleSelectAll = React.useCallback(() => {
|
|
58
|
+
onTypesChange([]);
|
|
59
|
+
}, [onTypesChange]);
|
|
60
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
|
|
61
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2.5", children: [
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
"button",
|
|
64
|
+
{
|
|
65
|
+
type: "button",
|
|
66
|
+
onClick: handleSelectAll,
|
|
67
|
+
"aria-pressed": allActive,
|
|
68
|
+
className: cn(CHIP_BASE, allActive ? CHIP_ACTIVE : CHIP_INACTIVE),
|
|
69
|
+
children: /* @__PURE__ */ jsx("span", { children: t("customers.timeline.filter.all", "All Activities") })
|
|
70
|
+
}
|
|
71
|
+
),
|
|
55
72
|
FILTER_TYPES.map(({ type, icon: Icon }) => {
|
|
56
73
|
const isActive = activeTypes.includes(type);
|
|
57
74
|
const count = counts?.[type];
|
|
75
|
+
const hasCount = typeof count === "number" && count > 0;
|
|
58
76
|
return /* @__PURE__ */ jsxs(
|
|
59
|
-
|
|
77
|
+
"button",
|
|
60
78
|
{
|
|
61
79
|
type: "button",
|
|
62
|
-
variant: "outline",
|
|
63
|
-
size: "sm",
|
|
64
80
|
onClick: () => handleTypeToggle(type),
|
|
65
|
-
className: cn(
|
|
66
|
-
"h-7 rounded-full px-2.5 text-xs gap-1.5",
|
|
67
|
-
isActive ? "border-foreground bg-background text-foreground" : "border-border bg-background text-muted-foreground"
|
|
68
|
-
),
|
|
69
81
|
"aria-pressed": isActive,
|
|
82
|
+
className: cn(CHIP_BASE, isActive ? CHIP_ACTIVE : CHIP_INACTIVE),
|
|
70
83
|
children: [
|
|
71
|
-
/* @__PURE__ */ jsx(Icon, { className: "size-
|
|
72
|
-
/* @__PURE__ */
|
|
73
|
-
|
|
84
|
+
/* @__PURE__ */ jsx(Icon, { className: "size-[18px] shrink-0" }),
|
|
85
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
86
|
+
t(`customers.timeline.filter.${type}`, type),
|
|
87
|
+
hasCount ? ` ${count}` : ""
|
|
88
|
+
] })
|
|
74
89
|
]
|
|
75
90
|
},
|
|
76
91
|
type
|
|
@@ -78,17 +93,15 @@ function ActivityTimelineFilters({
|
|
|
78
93
|
})
|
|
79
94
|
] }),
|
|
80
95
|
/* @__PURE__ */ jsxs(Popover, { children: [
|
|
81
|
-
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */
|
|
82
|
-
|
|
96
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
97
|
+
IconButton,
|
|
83
98
|
{
|
|
84
99
|
type: "button",
|
|
85
100
|
variant: "outline",
|
|
86
101
|
size: "sm",
|
|
87
|
-
className: "
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
t("customers.people.detail.activities.moreFilters", "More")
|
|
91
|
-
]
|
|
102
|
+
className: "size-7 rounded-md text-muted-foreground",
|
|
103
|
+
"aria-label": t("customers.people.detail.activities.moreFilters", "More filters"),
|
|
104
|
+
children: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "size-3.5" })
|
|
92
105
|
}
|
|
93
106
|
) }),
|
|
94
107
|
/* @__PURE__ */ jsxs(PopoverContent, { align: "end", className: "w-72 space-y-3", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/ActivityTimelineFilters.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, SlidersHorizontal } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst FILTER_TYPES = [\n { type: '
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, SlidersHorizontal } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst FILTER_TYPES = [\n { type: 'note', icon: StickyNote },\n { type: 'call', icon: Phone },\n { type: 'meeting', icon: Users },\n { type: 'email', icon: Mail },\n] as const\n\ntype InteractionCounts = {\n call: number\n email: number\n meeting: number\n note: number\n total: number\n}\n\ninterface ActivityTimelineFiltersProps {\n entityId: string | null\n activeTypes: string[]\n dateFrom: string\n dateTo: string\n onTypesChange: (types: string[]) => void\n onDateFromChange: (value: string) => void\n onDateToChange: (value: string) => void\n onReset: () => void\n}\n\nconst CHIP_BASE = 'inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors'\nconst CHIP_INACTIVE = 'border border-border bg-card text-muted-foreground hover:bg-accent/40'\nconst CHIP_ACTIVE = 'border border-status-info-border bg-status-info-bg text-status-info-text'\n\nexport function ActivityTimelineFilters({\n entityId,\n activeTypes,\n dateFrom,\n dateTo,\n onTypesChange,\n onDateFromChange,\n onDateToChange,\n onReset,\n}: ActivityTimelineFiltersProps) {\n const t = useT()\n const hasActiveFilters = activeTypes.length > 0 || dateFrom || dateTo\n const allActive = activeTypes.length === 0\n const [counts, setCounts] = React.useState<InteractionCounts | null>(null)\n\n React.useEffect(() => {\n if (!entityId) return\n const controller = new AbortController()\n void (async () => {\n try {\n const nextCounts = await readApiResultOrThrow<InteractionCounts>(\n `/api/customers/interactions/counts?entityId=${encodeURIComponent(entityId)}`,\n { signal: controller.signal },\n )\n setCounts(nextCounts)\n } catch {\n setCounts(null)\n }\n })()\n return () => controller.abort()\n }, [entityId])\n\n const handleTypeToggle = React.useCallback((type: string) => {\n if (activeTypes.includes(type)) {\n onTypesChange(activeTypes.filter((filterType) => filterType !== type))\n } else {\n onTypesChange([...activeTypes, type])\n }\n }, [activeTypes, onTypesChange])\n\n const handleSelectAll = React.useCallback(() => {\n onTypesChange([])\n }, [onTypesChange])\n\n return (\n <div className=\"flex flex-col gap-3 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex flex-wrap items-center gap-2.5\">\n <button\n type=\"button\"\n onClick={handleSelectAll}\n aria-pressed={allActive}\n className={cn(CHIP_BASE, allActive ? CHIP_ACTIVE : CHIP_INACTIVE)}\n >\n <span>{t('customers.timeline.filter.all', 'All Activities')}</span>\n </button>\n\n {FILTER_TYPES.map(({ type, icon: Icon }) => {\n const isActive = activeTypes.includes(type)\n const count = counts?.[type as keyof InteractionCounts]\n const hasCount = typeof count === 'number' && count > 0\n return (\n <button\n key={type}\n type=\"button\"\n onClick={() => handleTypeToggle(type)}\n aria-pressed={isActive}\n className={cn(CHIP_BASE, isActive ? CHIP_ACTIVE : CHIP_INACTIVE)}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span>\n {t(`customers.timeline.filter.${type}`, type)}\n {hasCount ? ` ${count}` : ''}\n </span>\n </button>\n )\n })}\n </div>\n\n <Popover>\n <PopoverTrigger asChild>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"size-7 rounded-md text-muted-foreground\"\n aria-label={t('customers.people.detail.activities.moreFilters', 'More filters')}\n >\n <SlidersHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-72 space-y-3\">\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium\">\n {t('customers.activities.filters.dateRange', 'Date range')}\n </label>\n <div className=\"flex items-center gap-2\">\n <input\n type=\"date\"\n value={dateFrom}\n onChange={(event) => onDateFromChange(event.target.value)}\n className=\"h-8 w-full rounded-md border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={t('customers.timeline.filter.from', 'From date')}\n />\n <span className=\"shrink-0 text-xs text-muted-foreground\">\u2014</span>\n <input\n type=\"date\"\n value={dateTo}\n onChange={(event) => onDateToChange(event.target.value)}\n className=\"h-8 w-full rounded-md border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={t('customers.timeline.filter.to', 'To date')}\n />\n </div>\n </div>\n\n {hasActiveFilters ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onReset}\n className=\"h-7 w-full text-xs\"\n >\n {t('customers.activities.filters.clearAll', 'Clear filters')}\n </Button>\n ) : null}\n </PopoverContent>\n </Popover>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|