@open-mercato/core 0.6.7-develop.6652.1.3ab996caba → 0.6.7-develop.6660.1.90e1e2eef6
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/business_rules/lib/value-resolver.js +7 -0
- package/dist/modules/business_rules/lib/value-resolver.js.map +2 -2
- package/dist/modules/currencies/commands/currencies.js +22 -8
- package/dist/modules/currencies/commands/currencies.js.map +2 -2
- package/dist/modules/currencies/commands/exchange-rates.js +22 -8
- package/dist/modules/currencies/commands/exchange-rates.js.map +2 -2
- package/dist/modules/currencies/commands/scope.js +22 -0
- package/dist/modules/currencies/commands/scope.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesSection.js +5 -4
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +5 -4
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +6 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.js +3 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +6 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.js +3 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js +107 -92
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
- package/dist/modules/feature_toggles/api/global/route.js +6 -0
- package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
- package/dist/modules/feature_toggles/commands/global.js +9 -9
- package/dist/modules/feature_toggles/commands/global.js.map +2 -2
- package/dist/modules/query_index/lib/subscriber-scope.js +63 -10
- package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
- package/dist/modules/query_index/subscribers/delete_one.js +20 -6
- package/dist/modules/query_index/subscribers/delete_one.js.map +2 -2
- package/dist/modules/query_index/subscribers/upsert_one.js +8 -3
- package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js +2 -0
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +4 -0
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/business_rules/lib/value-resolver.ts +16 -0
- package/src/modules/currencies/commands/currencies.ts +26 -8
- package/src/modules/currencies/commands/exchange-rates.ts +26 -8
- package/src/modules/currencies/commands/scope.ts +32 -0
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +10 -6
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +6 -3
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +6 -0
- package/src/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.ts +3 -0
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +6 -0
- package/src/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.ts +3 -0
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +15 -4
- package/src/modules/feature_toggles/api/global/route.ts +7 -1
- package/src/modules/feature_toggles/commands/global.ts +9 -12
- package/src/modules/query_index/lib/subscriber-scope.ts +98 -14
- package/src/modules/query_index/subscribers/delete_one.ts +21 -10
- package/src/modules/query_index/subscribers/upsert_one.ts +8 -3
- package/src/modules/workflows/AGENTS.md +22 -0
- package/src/modules/workflows/api/definitions/[id]/customize/route.ts +9 -0
- package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +7 -0
|
@@ -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, Search } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCallOrThrow, 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'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\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 runGuardedMutation,\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 taskFilterActive = filterTypes.includes('task')\n const canonicalParams = new URLSearchParams({\n entityId,\n limit: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n // Hide tasks from the activity timeline by default \u2014 they have their own tab \u2014\n // but lift the exclusion when the user explicitly toggled the Task chip on\n // (mirrors `ActivityHistorySection.tsx` after the #1805 fix).\n if (!taskFilterActive) canonicalParams.set('excludeInteractionType', 'task')\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 // Legacy fallback uses known page numbers, so request every page up front\n // and resolve them together instead of awaiting each one sequentially.\n const legacyPageNumbers = Array.from({ length: loadedPages }, (_, index) => index + 1)\n const legacyPayloads = await Promise.all(\n legacyPageNumbers.map((legacyPage) => {\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 return readApiResultOrThrow<{ items?: ActivitySummary[]; totalPages?: number }>(\n `/api/customers/activities?${legacyParams.toString()}`,\n ).catch(() => ({ items: [] as ActivitySummary[], totalPages: 1 }))\n }),\n )\n // Merge in page order so timeline ordering stays stable regardless of\n // which request settles first.\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (const legacyPayload of legacyPayloads) {\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 logger.error('customers.activities.history failed', { err: 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 handleMarkDone = React.useCallback(async (activityId: string) => {\n try {\n const operation = () =>\n apiCallOrThrow('/api/customers/interactions/complete', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: activityId, occurredAt: new Date().toISOString() }),\n })\n if (runGuardedMutation) {\n await runGuardedMutation(operation, {\n id: activityId,\n status: 'done',\n operation: 'completeActivity',\n })\n } else {\n await operation()\n }\n flash(t('customers.activities.actions.markDoneSuccess', 'Activity marked done'), 'success')\n await loadActivities()\n } catch (err) {\n logger.warn('Mark done failed', { component: 'ActivitiesSection', activityId, err })\n flash(t('customers.activities.actions.markDoneError', 'Could not mark activity as done'), 'error')\n }\n }, [loadActivities, runGuardedMutation, t])\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) => logger.warn('loadActivities failed', { component: 'ActivitiesSection', 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) => logger.warn('resolve author names failed', { component: 'ActivitiesSection', 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\n activities={visibleActivities}\n onEdit={onEditActivity}\n onMarkDone={handleMarkDone}\n />\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": ";
|
|
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 { apiCallOrThrow, 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'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\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 /** Interaction type hidden from the timeline by default ('task' unless overridden); pass null to show every type. */\n excludeInteractionType?: string | null\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 runGuardedMutation,\n excludeInteractionType = 'task',\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 excludedFilterActive = excludeInteractionType ? filterTypes.includes(excludeInteractionType) : true\n const canonicalParams = new URLSearchParams({\n entityId,\n limit: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n // Hide the configured type (tasks by default \u2014 they have their own tab)\n // from the activity timeline, but lift the exclusion when the user\n // explicitly toggled that type's chip on (mirrors\n // `ActivityHistorySection.tsx` after the #1805 fix).\n if (excludeInteractionType && !excludedFilterActive) canonicalParams.set('excludeInteractionType', excludeInteractionType)\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 // Legacy fallback uses known page numbers, so request every page up front\n // and resolve them together instead of awaiting each one sequentially.\n const legacyPageNumbers = Array.from({ length: loadedPages }, (_, index) => index + 1)\n const legacyPayloads = await Promise.all(\n legacyPageNumbers.map((legacyPage) => {\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 return readApiResultOrThrow<{ items?: ActivitySummary[]; totalPages?: number }>(\n `/api/customers/activities?${legacyParams.toString()}`,\n ).catch(() => ({ items: [] as ActivitySummary[], totalPages: 1 }))\n }),\n )\n // Merge in page order so timeline ordering stays stable regardless of\n // which request settles first.\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (const legacyPayload of legacyPayloads) {\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 logger.error('customers.activities.history failed', { err: 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, excludeInteractionType, filterDateFrom, filterDateTo, filterTypes, loadedPages, useCanonicalInteractions, refreshKey, t])\n\n React.useEffect(() => {\n setLoadedPages(1)\n }, [dealId, entityId, filterDateFrom, filterDateTo, filterTypes, useCanonicalInteractions])\n\n const handleMarkDone = React.useCallback(async (activityId: string) => {\n try {\n const operation = () =>\n apiCallOrThrow('/api/customers/interactions/complete', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: activityId, occurredAt: new Date().toISOString() }),\n })\n if (runGuardedMutation) {\n await runGuardedMutation(operation, {\n id: activityId,\n status: 'done',\n operation: 'completeActivity',\n })\n } else {\n await operation()\n }\n flash(t('customers.activities.actions.markDoneSuccess', 'Activity marked done'), 'success')\n await loadActivities()\n } catch (err) {\n logger.warn('Mark done failed', { component: 'ActivitiesSection', activityId, err })\n flash(t('customers.activities.actions.markDoneError', 'Could not mark activity as done'), 'error')\n }\n }, [loadActivities, runGuardedMutation, t])\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) => logger.warn('loadActivities failed', { component: 'ActivitiesSection', 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) => logger.warn('resolve author names failed', { component: 'ActivitiesSection', 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\n activities={visibleActivities}\n onEdit={onEditActivity}\n onMarkDone={handleMarkDone}\n />\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
|
}
|
|
@@ -108,7 +108,8 @@ function ActivityHistorySection({
|
|
|
108
108
|
useCanonicalInteractions = false,
|
|
109
109
|
refreshKey = 0,
|
|
110
110
|
onEditActivity,
|
|
111
|
-
runMutation
|
|
111
|
+
runMutation,
|
|
112
|
+
excludeInteractionType = "task"
|
|
112
113
|
}) {
|
|
113
114
|
const t = useT();
|
|
114
115
|
const [searchInput, setSearchInput] = React.useState("");
|
|
@@ -166,14 +167,14 @@ function ActivityHistorySection({
|
|
|
166
167
|
let nextCursor;
|
|
167
168
|
let firstPageHasMore = false;
|
|
168
169
|
let pagesLoaded = 0;
|
|
169
|
-
const
|
|
170
|
+
const excludedFilterActive = excludeInteractionType ? activeTypes.includes(excludeInteractionType) : true;
|
|
170
171
|
do {
|
|
171
172
|
const params = new URLSearchParams({
|
|
172
173
|
entityId,
|
|
173
174
|
limit: String(pageSize),
|
|
174
175
|
from: rangeStart
|
|
175
176
|
});
|
|
176
|
-
if (!
|
|
177
|
+
if (excludeInteractionType && !excludedFilterActive) params.set("excludeInteractionType", excludeInteractionType);
|
|
177
178
|
if (activeTypes.length > 0) params.set("type", activeTypes.join(","));
|
|
178
179
|
if (search) params.set("search", search);
|
|
179
180
|
if (sortMode === "recent") {
|
|
@@ -239,7 +240,7 @@ function ActivityHistorySection({
|
|
|
239
240
|
} finally {
|
|
240
241
|
if (!isStale()) setLoading(false);
|
|
241
242
|
}
|
|
242
|
-
}, [activeTypes, dateRange, entityId, loadedPages, search, sortMode, t, useCanonicalInteractions]);
|
|
243
|
+
}, [activeTypes, dateRange, entityId, excludeInteractionType, loadedPages, search, sortMode, t, useCanonicalInteractions]);
|
|
243
244
|
React.useEffect(() => {
|
|
244
245
|
const controller = new AbortController();
|
|
245
246
|
const requestSeq = historyRequestSeqRef.current + 1;
|
|
@@ -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 {\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 // Legacy fallback uses known page numbers, so request every page in\n // parallel (sharing the abort signal) and merge them in page order.\n const legacyPageNumbers = Array.from({ length: loadedPages }, (_, index) => index + 1)\n const legacyPayloads = await Promise.all(\n legacyPageNumbers.map((legacyPage) =>\n 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 ),\n )\n if (isStale()) return\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (const legacyPayload of legacyPayloads) {\n legacyItems.push(...(Array.isArray(legacyPayload.items) ? legacyPayload.items.map(normalizeLegacyActivity) : []))\n legacyTotalPages = typeof legacyPayload.totalPages === 'number' ? legacyPayload.totalPages : legacyTotalPages\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": ";AAyVQ,cACA,YADA;AAvVR,YAAY,WAAW;AACvB,SAAS,QAAQ,cAAc;AAC/B,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc,gBAAgB,qBAAqB;AAC5D,SAAS,4BAA4B;AAErC,SAAS,oBAAoB;AAwC7B,MAAM,eAAe;AAAA,EACnB,EAAE,OAAO,QAAQ,UAAU,kCAAkC,UAAU,OAAO;AAAA,EAC9E,EAAE,OAAO,SAAS,UAAU,mCAAmC,UAAU,QAAQ;AAAA,EACjF,EAAE,OAAO,WAAW,UAAU,qCAAqC,UAAU,UAAU;AAAA,EACvF,EAAE,OAAO,QAAQ,UAAU,kCAAkC,UAAU,OAAO;AAAA,EAC9E,EAAE,OAAO,QAAQ,UAAU,kCAAkC,UAAU,OAAO;AAChF;AAEA,SAAS,kBAAkB,OAAmC;AAC5D,QAAM,OAAO,oBAAI,KAAK;AACtB,OAAK,SAAS,GAAG,GAAG,GAAG,CAAC;AACxB,QAAM,OAAO,OAAO,SAAS,MAAM,QAAQ,KAAK,EAAE,GAAG,EAAE,KAAK;AAC5D,OAAK,QAAQ,KAAK,QAAQ,IAAI,IAAI;AAClC,SAAO;AACT;AAEA,SAAS,oBAAoB,UAAsC;AACjE,SAAO,SAAS,cAAc,SAAS,eAAe,SAAS;AACjE;AAEA,SAAS,wBAAwB,UAA+C;AAC9E,SAAO;AAAA,IACL,IAAI,SAAS;AAAA,IACb,iBAAiB,SAAS;AAAA,IAC1B,OAAO,SAAS,WAAW;AAAA,IAC3B,MAAM,SAAS,QAAQ;AAAA,IACvB,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,YAAY,SAAS,cAAc;AAAA,IACnC,UAAU;AAAA,IACV,cAAc,SAAS,gBAAgB;AAAA,IACvC,aAAa;AAAA,IACb,gBAAgB,SAAS,kBAAkB;AAAA,IAC3C,iBAAiB,SAAS,mBAAmB;AAAA,IAC7C,QAAQ;AAAA,IACR,UAAU,SAAS,YAAY;AAAA,IAC/B,QAAQ,SAAS,UAAU;AAAA,IAC3B,gBAAgB;AAAA,IAChB,UAAU;AAAA,IACV,YAAY,SAAS,cAAc;AAAA,IACnC,aAAa,SAAS,eAAe;AAAA,IACrC,WAAW,SAAS,aAAa;AAAA,IACjC,cAAc,SAAS,gBAAgB;AAAA,IACvC,WAAW,SAAS;AAAA,IACpB,WAAW,SAAS;AAAA,EACtB;AACF;AAEA,SAAS,uBAAuB,OAAmD;AACjF,QAAM,MAAM,KAAK,IAAI;AACrB,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AACtC,UAAM,gBAAgB,KAAK,cAAc,IAAI,KAAK,KAAK,WAAW,EAAE,QAAQ,IAAI,OAAO;AACvF,UAAM,iBAAiB,MAAM,cAAc,IAAI,KAAK,MAAM,WAAW,EAAE,QAAQ,IAAI,OAAO;AAC1F,UAAM,gBAAgB,KAAK,WAAW,aAAa,OAAO,SAAS,aAAa;AAChF,UAAM,iBAAiB,MAAM,WAAW,aAAa,OAAO,SAAS,cAAc;AACnF,UAAM,iBAAiB,iBAAiB,iBAAiB;AACzD,UAAM,kBAAkB,kBAAkB,kBAAkB;AAE5D,QAAI,mBAAmB,gBAAiB,QAAO,iBAAiB,KAAK;AACrE,QAAI,kBAAkB,gBAAiB,QAAO,gBAAgB;AAE9D,UAAM,UAAU,oBAAoB,KAAK,EAAE,cAAc,oBAAoB,IAAI,CAAC;AAClF,QAAI,YAAY,EAAG,QAAO;AAC1B,WAAO,MAAM,GAAG,cAAc,KAAK,EAAE;AAAA,EACvC,CAAC;AACH;AAEA,SAAS,eAAe,OAA6B,UAAiD;AACpG,MAAI,aAAa,SAAU,QAAO,uBAAuB,KAAK;AAC9D,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AACtC,UAAM,aAAa,KAAK,SAAS,KAAK,QAAQ,KAAK,mBAAmB,IAAI,YAAY;AACtF,UAAM,cAAc,MAAM,SAAS,MAAM,QAAQ,MAAM,mBAAmB,IAAI,YAAY;AAC1F,WAAO,aAAa,cAChB,UAAU,cAAc,UAAU,IAClC,WAAW,cAAc,SAAS;AAAA,EACxC,CAAC;AACH;AAEA,SAAS,cAAc,UAA8B,OAAwB;AAC3E,QAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,WAAW;AAAA,IACf,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS,UAAU;AAAA,EACrB,EACG,OAAO,OAAO,EACd,KAAK,GAAG,EACR,YAAY;AACf,SAAO,SAAS,SAAS,UAAU;AACrC;AAEA,SAAS,cAAc,UAA8B,OAAsB;AACzE,QAAM,YAAY,IAAI,KAAK,oBAAoB,QAAQ,CAAC;AACxD,MAAI,OAAO,MAAM,UAAU,QAAQ,CAAC,EAAG,QAAO;AAC9C,SAAO,aAAa;AACtB;AAEA,SAAS,aAAa,OAAyB;AAC7C,SACE,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,SACT,MAA6B,SAAS;AAE3C;AAEO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA,2BAA2B;AAAA,EAC3B,aAAa;AAAA,EACb;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,EAAE;AACvD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,EAAE;AAC7C,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAmB,CAAC,CAAC;AACjE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAA+B,KAAK;AAC5E,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAgD,QAAQ;AAC9F,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAA+B,CAAC,CAAC;AAC3E,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAiC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,CAAC;AAChI,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,CAAC;AACtD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,CAAC;AAC9D,QAAM,uBAAuB,MAAM,OAAO,CAAC;AAC3C,QAAM,wBAAwB,MAAM,YAAY,MAAM;AACpD,uBAAmB,CAAC,YAAY,UAAU,CAAC;AAAA,EAC7C,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,UAAM,UAAU,OAAO,WAAW,MAAM,UAAU,YAAY,KAAK,CAAC,GAAG,GAAG;AAC1E,WAAO,MAAM,OAAO,aAAa,OAAO;AAAA,EAC1C,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,UAAU,MAAM;AACpB,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,UAAU,MAAM;AAAA,UACpB,+CAA+C,mBAAmB,QAAQ,CAAC;AAAA,UAC3E,EAAE,QAAQ,WAAW,OAAO;AAAA,QAC9B;AACA,cAAM,SAAS,QAAQ,UAAU;AACjC,kBAAU;AAAA,UACR,MAAM,OAAO,QAAQ;AAAA,UACrB,OAAO,OAAO,SAAS;AAAA,UACvB,SAAS,OAAO,WAAW;AAAA,UAC3B,MAAM,OAAO,QAAQ;AAAA,UACrB,MAAM,OAAO,QAAQ;AAAA,UACrB,OAAO,OAAO,SAAS;AAAA,QACzB,CAAC;AAAA,MACH,QAAQ;AACN,kBAAU,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,CAAC;AAAA,MACzE;AAAA,IACF,GAAG;AACH,WAAO,MAAM,WAAW,MAAM;AAAA,EAChC,GAAG,CAAC,UAAU,YAAY,eAAe,CAAC;AAE1C,QAAM,cAAc,MAAM,YAAY,OAAO,YAAyD;AACpG,UAAM,EAAE,QAAQ,WAAW,IAAI;AAC/B,UAAM,UAAU,MAAM,OAAO,WAAW,eAAe,qBAAqB;AAC5E,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,aAAa,kBAAkB,SAAS,EAAE,YAAY;AAC5D,YAAM,WAAW;AACjB,YAAM,iBAAuC,CAAC;AAC9C,UAAI;AACJ,UAAI,mBAAmB;AACvB,UAAI,cAAc;AAElB,YAAM,mBAAmB,YAAY,SAAS,MAAM;AACpD,SAAG;AACD,cAAM,SAAS,IAAI,gBAAgB;AAAA,UACjC;AAAA,UACA,OAAO,OAAO,QAAQ;AAAA,UACtB,MAAM;AAAA,QACR,CAAC;AACD,YAAI,CAAC,iBAAkB,QAAO,IAAI,0BAA0B,MAAM;AAClE,YAAI,YAAY,SAAS,EAAG,QAAO,IAAI,QAAQ,YAAY,KAAK,GAAG,CAAC;AACpE,YAAI,OAAQ,QAAO,IAAI,UAAU,MAAM;AACvC,YAAI,aAAa,UAAU;AACzB,iBAAO,IAAI,aAAa,YAAY;AACpC,iBAAO,IAAI,WAAW,MAAM;AAAA,QAC9B,OAAO;AACL,iBAAO,IAAI,aAAa,OAAO;AAC/B,iBAAO,IAAI,WAAW,aAAa,cAAc,QAAQ,MAAM;AAAA,QACjE;AACA,YAAI,WAAY,QAAO,IAAI,UAAU,UAAU;AAE/C,cAAM,WAAW,MAAM;AAAA,UACrB,+BAA+B,OAAO,SAAS,CAAC;AAAA,UAChD,EAAE,OAAO;AAAA,QACX;AACA,YAAI,QAAQ,EAAG;AACf,cAAM,YAAY,MAAM,QAAQ,SAAS,KAAK,IAAI,SAAS,QAAQ,CAAC;AACpE,uBAAe,KAAK,GAAG,SAAS;AAChC,qBAAa,SAAS;AACtB,YAAI,CAAC,iBAAkB,oBAAmB,QAAQ,SAAS,UAAU;AACrE,uBAAe;AAAA,MACjB,SAAS,cAAc,cAAc;AAErC,UAAI,WAAW;AAEf,UAAI,CAAC,0BAA0B;AAG7B,cAAM,oBAAoB,MAAM,KAAK,EAAE,QAAQ,YAAY,GAAG,CAAC,GAAG,UAAU,QAAQ,CAAC;AACrF,cAAM,iBAAiB,MAAM,QAAQ;AAAA,UACnC,kBAAkB;AAAA,YAAI,CAAC,eACrB;AAAA,cACE,sCAAsC,mBAAmB,QAAQ,CAAC,SAAS,UAAU;AAAA,cACrF,EAAE,OAAO;AAAA,YACX,EAAE,MAAM,OAAO,EAAE,OAAO,CAAC,GAAwB,YAAY,EAAE,EAAE;AAAA,UACnE;AAAA,QACF;AACA,YAAI,QAAQ,EAAG;AACf,cAAM,cAAoC,CAAC;AAC3C,YAAI,mBAAmB;AACvB,mBAAW,iBAAiB,gBAAgB;AAC1C,sBAAY,KAAK,GAAI,MAAM,QAAQ,cAAc,KAAK,IAAI,cAAc,MAAM,IAAI,uBAAuB,IAAI,CAAC,CAAE;AAChH,6BAAmB,OAAO,cAAc,eAAe,WAAW,cAAc,aAAa;AAAA,QAC/F;AACA,cAAM,iBAAiB,kBAAkB,SAAS;AAClD,cAAM,iBAAiB,YAAY,OAAO,CAAC,SAAS;AAClD,cAAI,YAAY,SAAS,KAAK,CAAC,YAAY,SAAS,KAAK,eAAe,EAAG,QAAO;AAClF,cAAI,CAAC,cAAc,MAAM,MAAM,EAAG,QAAO;AACzC,iBAAO,cAAc,MAAM,cAAc;AAAA,QAC3C,CAAC;AACD,cAAM,UAAU,oBAAI,IAAgC;AACnD,SAAC,GAAG,gBAAgB,GAAG,cAAc,EAAE,QAAQ,CAAC,SAAS;AACxD,cAAI,CAAC,QAAQ,IAAI,KAAK,EAAE,EAAG,SAAQ,IAAI,KAAK,IAAI,IAAI;AAAA,QACtD,CAAC;AACD,mBAAW,MAAM,KAAK,QAAQ,OAAO,CAAC;AACtC,2BAAmB,oBAAoB,mBAAmB;AAAA,MAC5D;AAEA,UAAI,CAAC,QAAQ,GAAG;AACd,sBAAc,eAAe,UAAU,QAAQ,CAAC;AAChD,mBAAW,gBAAgB;AAAA,MAC7B;AAAA,IACF,SAAS,WAAW;AAClB,UAAI,CAAC,QAAQ,KAAK,CAAC,aAAa,SAAS,GAAG;AAC1C,sBAAc,CAAC,CAAC;AAChB,mBAAW,KAAK;AAChB,iBAAS,EAAE,+BAA+B,iCAAiC,CAAC;AAAA,MAC9E;AAAA,IACF,UAAE;AACA,UAAI,CAAC,QAAQ,EAAG,YAAW,KAAK;AAAA,IAClC;AAAA,EACF,GAAG,CAAC,aAAa,WAAW,UAAU,aAAa,QAAQ,UAAU,GAAG,wBAAwB,CAAC;AAEjG,QAAM,UAAU,MAAM;AACpB,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,aAAa,qBAAqB,UAAU;AAClD,yBAAqB,UAAU;AAC/B,SAAK,YAAY,EAAE,QAAQ,WAAW,QAAQ,WAAW,CAAC;AAC1D,WAAO,MAAM,WAAW,MAAM;AAAA,EAChC,GAAG,CAAC,aAAa,YAAY,eAAe,CAAC;AAE7C,QAAM,UAAU,MAAM;AACpB,mBAAe,CAAC;AAAA,EAClB,GAAG,CAAC,aAAa,WAAW,UAAU,QAAQ,UAAU,wBAAwB,CAAC;AAEjF,QAAM,gBAAgB,YAAY,SAAS,IACvC,YAAY,IAAI,CAAC,SAAS,EAAE,6BAA6B,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,IACjF,EAAE,iCAAiC,KAAK;AAE5C,QAAM,mBAAmB,MAAM,YAAY,CAAC,SAAiB;AAC3D,mBAAe,CAAC,YACd,QAAQ,SAAS,IAAI,IACjB,QAAQ,OAAO,CAAC,UAAU,UAAU,IAAI,IACxC,CAAC,GAAG,SAAS,IAAI,CACtB;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,MAAM,YAAY,MAAM;AAC7C,mBAAe,CAAC,YAAY,UAAU,CAAC;AAAA,EACzC,GAAG,CAAC,CAAC;AAEL,SACE,qBAAC,SAAI,WAAU,6BACb;AAAA,yBAAC,SAAI,WAAU,8CACb;AAAA,0BAAC,UAAO,WAAU,gCAA+B;AAAA,MACjD,qBAAC,SAAI,WAAU,WACb;AAAA,4BAAC,QAAG,WAAU,2CACX,YAAE,+BAA+B,kBAAkB,GACtD;AAAA,QACA,oBAAC,OAAE,WAAU,iCACV,YAAE,uCAAuC,8CAA2C;AAAA,UACnF,OAAO;AAAA,UACP,OAAO,WAAW;AAAA,QACpB,CAAC,GACH;AAAA,SACF;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,uBACb;AAAA,2BAAC,SAAI,WAAU,sEACb;AAAA,6BAAC,SAAI,WAAU,4BACb;AAAA,8BAAC,UAAO,WAAU,6FAA4F;AAAA,UAC9G;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU,CAAC,UAAU;AACnB,+BAAe,MAAM,OAAO,KAAK;AAAA,cACnC;AAAA,cACA,aAAa,EAAE,2CAA2C,kCAAkC;AAAA,cAC5F,WAAU;AAAA;AAAA,UACZ;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,qCACb;AAAA,+BAAC,UAAK,WAAU,iFACb;AAAA,cAAE,8BAA8B,QAAQ;AAAA,YAAE;AAAA,aAC7C;AAAA,UACC,aAAa,IAAI,CAAC,WAAW;AAC5B,kBAAM,WAAW,YAAY,SAAS,OAAO,KAAK;AAClD,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,MAAK;AAAA,gBACL,SAAS,WAAW,YAAY;AAAA,gBAChC,MAAK;AAAA,gBACL,SAAS,MAAM,iBAAiB,OAAO,KAAK;AAAA,gBAC5C,WAAU;AAAA,gBAET;AAAA,oBAAE,OAAO,UAAU,OAAO,QAAQ;AAAA,kBACnC,oBAAC,UAAK,WAAW,WAAW,oCAAoC,8BAC7D,iBAAO,OAAO,KAAK,KAAK,GAC3B;AAAA;AAAA;AAAA,cAVK,OAAO;AAAA,YAWd;AAAA,UAEJ,CAAC;AAAA,UAED;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,eAAe,CAAC,UAAU;AACxB,6BAAa,KAA6B;AAAA,cAC5C;AAAA,cAEA;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,cAAY,EAAE,gDAAgD,YAAY;AAAA,oBAC1E,WAAU;AAAA,oBAEV,8BAAC,eAAY;AAAA;AAAA,gBACf;AAAA,gBACA,qBAAC,iBACC;AAAA,sCAAC,cAAW,OAAM,MAAM,YAAE,iCAAiC,aAAa,GAAE;AAAA,kBAC1E,oBAAC,cAAW,OAAM,OAAO,YAAE,kCAAkC,cAAc,GAAE;AAAA,kBAC7E,oBAAC,cAAW,OAAM,OAAO,YAAE,kCAAkC,cAAc,GAAE;AAAA,mBAC/E;AAAA;AAAA;AAAA,UACF;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,eAAe,CAAC,UAAU;AACxB,4BAAY,KAA8C;AAAA,cAC5D;AAAA,cAEA;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,cAAY,EAAE,2CAA2C,YAAY;AAAA,oBACrE,WAAU;AAAA,oBAEV,8BAAC,eAAY;AAAA;AAAA,gBACf;AAAA,gBACA,qBAAC,iBACC;AAAA,sCAAC,cAAW,OAAM,UAAU,YAAE,qCAAqC,cAAc,GAAE;AAAA,kBACnF,oBAAC,cAAW,OAAM,aAAa,YAAE,uCAAuC,gBAAgB,GAAE;AAAA,kBAC1F,oBAAC,cAAW,OAAM,cAAc,YAAE,wCAAwC,gBAAgB,GAAE;AAAA,mBAC9F;AAAA;AAAA;AAAA,UACF;AAAA,WACF;AAAA,SACF;AAAA,MAEC,WAAW,WAAW,WAAW,IAChC,oBAAC,kBAAe,OAAO,EAAE,8CAA8C,0BAAqB,GAAG,WAAU,gCAA+B,IACtI,QACF,oBAAC,gBAAa,OAAO,OAAO,IAC1B,WAAW,WAAW,IACxB;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,4BAA4B,0CAA0C;AAAA,UAC/E,aAAa,EAAE,0CAA0C,yDAAyD;AAAA;AAAA,MACpH,IAEA,qBAAC,SAAI,WAAU,aACZ;AAAA,mBAAW,IAAI,CAAC,UAAU,UAAU;AACnC,gBAAM,cAAc,IAAI,KAAK,oBAAoB,QAAQ,CAAC,EAAE,YAAY;AACxE,gBAAM,eAAe,QAAQ,IAAI,IAAI,KAAK,oBAAoB,WAAW,QAAQ,CAAC,CAAC,CAAC,EAAE,YAAY,IAAI;AACtG,gBAAM,oBAAoB,iBAAiB,QAAQ,gBAAgB;AACnE,iBACE,qBAAC,MAAM,UAAN,EACE;AAAA,gCACC,qBAAC,SAAI,WAAU,gCACb;AAAA,kCAAC,SAAI,WAAU,yBAAwB;AAAA,cACvC,qBAAC,UAAK,WAAU,+CAA8C;AAAA;AAAA,gBAAG;AAAA,gBAAY;AAAA,iBAAE;AAAA,cAC/E,oBAAC,SAAI,WAAU,yBAAwB;AAAA,eACzC,IACE;AAAA,YACJ;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA,QAAQ;AAAA,gBACR,WAAW;AAAA,gBACX;AAAA;AAAA,YACF;AAAA,eAbmB,SAAS,EAc9B;AAAA,QAEJ,CAAC;AAAA,QAEA,UACC,oBAAC,SAAI,WAAU,oBACb,8BAAC,UAAO,MAAK,UAAS,SAAQ,QAAO,MAAK,MAAK,SAAS,gBAAgB,WAAU,WAC/E,YAAE,iCAAiC,WAAW,GACjD,GACF,IACE;AAAA,SACN;AAAA,OAEJ;AAAA,KACF;AAEJ;AAEA,IAAO,iCAAQ;",
|
|
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 /** Interaction type hidden from the history by default ('task' unless overridden); pass null to show every type. */\n excludeInteractionType?: string | null\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 excludeInteractionType = 'task',\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 excludedFilterActive = excludeInteractionType ? activeTypes.includes(excludeInteractionType) : true\n do {\n const params = new URLSearchParams({\n entityId,\n limit: String(pageSize),\n from: rangeStart,\n })\n if (excludeInteractionType && !excludedFilterActive) params.set('excludeInteractionType', excludeInteractionType)\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 // Legacy fallback uses known page numbers, so request every page in\n // parallel (sharing the abort signal) and merge them in page order.\n const legacyPageNumbers = Array.from({ length: loadedPages }, (_, index) => index + 1)\n const legacyPayloads = await Promise.all(\n legacyPageNumbers.map((legacyPage) =>\n 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 ),\n )\n if (isStale()) return\n const legacyItems: InteractionSummary[] = []\n let legacyTotalPages = 1\n for (const legacyPayload of legacyPayloads) {\n legacyItems.push(...(Array.isArray(legacyPayload.items) ? legacyPayload.items.map(normalizeLegacyActivity) : []))\n legacyTotalPages = typeof legacyPayload.totalPages === 'number' ? legacyPayload.totalPages : legacyTotalPages\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, excludeInteractionType, 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
|
}
|
|
@@ -155,6 +155,9 @@ async function PATCH(req, ctx) {
|
|
|
155
155
|
if (isCrudHttpError(err)) {
|
|
156
156
|
return NextResponse.json(err.body, { status: err.status });
|
|
157
157
|
}
|
|
158
|
+
if (err instanceof z.ZodError) {
|
|
159
|
+
return NextResponse.json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
160
|
+
}
|
|
158
161
|
logger.error("Failed to update dictionary entry", { err });
|
|
159
162
|
return NextResponse.json({ error: "Failed to update dictionary entry" }, { status: 500 });
|
|
160
163
|
}
|
|
@@ -227,6 +230,9 @@ async function DELETE(req, ctx) {
|
|
|
227
230
|
if (isCrudHttpError(err)) {
|
|
228
231
|
return NextResponse.json(err.body, { status: err.status });
|
|
229
232
|
}
|
|
233
|
+
if (err instanceof z.ZodError) {
|
|
234
|
+
return NextResponse.json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
235
|
+
}
|
|
230
236
|
logger.error("Failed to delete dictionary entry", { err });
|
|
231
237
|
return NextResponse.json({ error: "Failed to delete dictionary entry" }, { status: 500 });
|
|
232
238
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/dictionaries/api/%5BdictionaryId%5D/entries/%5BentryId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'\nimport { updateDictionaryEntrySchema } from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n dictionaryEntryParamsSchema,\n dictionaryEntryResponseSchema,\n dictionariesErrorSchema,\n dictionariesOkSchema,\n dictionariesTag,\n updateDictionaryEntrySchema as updateEntryDocSchema,\n} from '../../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\nconst paramsSchema = z.object({\n dictionaryId: z.string().uuid(),\n entryId: z.string().uuid(),\n})\n\nasync function loadDictionary(context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>, id: string) {\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const dictionary = await context.em.findOne(Dictionary, {\n id,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n deletedAt: null,\n })\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nasync function loadEntry(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n dictionary: Dictionary,\n entryId: string,\n) {\n const entry = await context.em.findOne(DictionaryEntry, {\n id: entryId,\n dictionary,\n organizationId: dictionary.organizationId,\n tenantId: context.tenantId,\n })\n if (!entry) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.entry_not_found', 'Dictionary entry not found') })\n }\n return entry\n}\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { dictionaryId?: string; entryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId, entryId } = paramsSchema.parse({\n dictionaryId: ctx.params?.dictionaryId,\n entryId: ctx.params?.entryId,\n })\n const dictionary = await loadDictionary(context, dictionaryId)\n const entry = await loadEntry(context, dictionary, entryId)\n await enforceCommandOptimisticLockWithGuards(context.container, {\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n current: entry.updatedAt ?? null,\n request: req,\n })\n const rawBody = await req.json().catch(() => ({}))\n const payload = updateDictionaryEntrySchema.parse(rawBody)\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n // These nested routes don't use the CRUD factory, so invoke the command bus explicitly.\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const input = { ...(payload as Record<string, unknown>), id: entryId }\n const { result, logEntry } = await commandBus.execute('dictionaries.entries.update', {\n input,\n ctx: context.ctx,\n })\n const updateResult = (result ?? {}) as { entryId?: string | null }\n const updatedEntryId = typeof updateResult.entryId === 'string' ? updateResult.entryId : null\n if (!updatedEntryId) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_update_failed', 'Failed to update dictionary entry') })\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.entry',\n resourceId: updatedEntryId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const updated = await findOneWithDecryption(\n context.em.fork(),\n DictionaryEntry,\n updatedEntryId,\n { populate: ['dictionary'] },\n { tenantId: context.auth.tenantId ?? null, organizationId: context.auth.orgId ?? null },\n )\n if (!updated) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_update_failed', 'Failed to update dictionary entry') })\n }\n const response = NextResponse.json({\n id: updated.id,\n value: updated.value,\n label: updated.label,\n color: updated.color,\n icon: updated.icon,\n position: updated.position ?? 0,\n isDefault: updated.isDefault ?? false,\n createdAt: updated.createdAt,\n updatedAt: updated.updatedAt,\n })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'dictionaries.entry',\n resourceId: updatedEntryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('Failed to update dictionary entry', { err })\n return NextResponse.json({ error: 'Failed to update dictionary entry' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { dictionaryId?: string; entryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n const { dictionaryId, entryId } = paramsSchema.parse({\n dictionaryId: ctx.params?.dictionaryId,\n entryId: ctx.params?.entryId,\n })\n const dictionary = await loadDictionary(context, dictionaryId)\n const entry = await loadEntry(context, dictionary, entryId)\n await enforceCommandOptimisticLockWithGuards(context.container, {\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n current: entry.updatedAt ?? null,\n request: req,\n })\n\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { logEntry } = await commandBus.execute('dictionaries.entries.delete', {\n input: { body: { id: entry.id } },\n ctx: context.ctx,\n })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const response = NextResponse.json({ ok: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'dictionaries.entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('Failed to delete dictionary entry', { err })\n return NextResponse.json({ error: 'Failed to delete dictionary entry' }, { status: 500 })\n }\n}\n\nconst dictionaryEntryPatchDoc: OpenApiMethodDoc = {\n summary: 'Update dictionary entry',\n description: 'Updates the specified dictionary entry using the command bus pipeline.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: updateEntryDocSchema,\n description: 'Fields to update on the dictionary entry.',\n },\n responses: [\n { status: 200, description: 'Dictionary entry updated.', schema: dictionaryEntryResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary or entry not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to update entry', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryEntryDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete dictionary entry',\n description: 'Deletes the specified dictionary entry via the command bus.',\n tags: [dictionariesTag],\n responses: [\n { status: 200, description: 'Entry deleted.', schema: dictionariesOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary or entry not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to delete entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary entry resource',\n pathParams: dictionaryEntryParamsSchema,\n methods: {\n PATCH: dictionaryEntryPatchDoc,\n DELETE: dictionaryEntryDeleteDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'\nimport { updateDictionaryEntrySchema } from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n dictionaryEntryParamsSchema,\n dictionaryEntryResponseSchema,\n dictionariesErrorSchema,\n dictionariesOkSchema,\n dictionariesTag,\n updateDictionaryEntrySchema as updateEntryDocSchema,\n} from '../../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\nconst paramsSchema = z.object({\n dictionaryId: z.string().uuid(),\n entryId: z.string().uuid(),\n})\n\nasync function loadDictionary(context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>, id: string) {\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const dictionary = await context.em.findOne(Dictionary, {\n id,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n deletedAt: null,\n })\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nasync function loadEntry(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n dictionary: Dictionary,\n entryId: string,\n) {\n const entry = await context.em.findOne(DictionaryEntry, {\n id: entryId,\n dictionary,\n organizationId: dictionary.organizationId,\n tenantId: context.tenantId,\n })\n if (!entry) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.entry_not_found', 'Dictionary entry not found') })\n }\n return entry\n}\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { dictionaryId?: string; entryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId, entryId } = paramsSchema.parse({\n dictionaryId: ctx.params?.dictionaryId,\n entryId: ctx.params?.entryId,\n })\n const dictionary = await loadDictionary(context, dictionaryId)\n const entry = await loadEntry(context, dictionary, entryId)\n await enforceCommandOptimisticLockWithGuards(context.container, {\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n current: entry.updatedAt ?? null,\n request: req,\n })\n const rawBody = await req.json().catch(() => ({}))\n const payload = updateDictionaryEntrySchema.parse(rawBody)\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n // These nested routes don't use the CRUD factory, so invoke the command bus explicitly.\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const input = { ...(payload as Record<string, unknown>), id: entryId }\n const { result, logEntry } = await commandBus.execute('dictionaries.entries.update', {\n input,\n ctx: context.ctx,\n })\n const updateResult = (result ?? {}) as { entryId?: string | null }\n const updatedEntryId = typeof updateResult.entryId === 'string' ? updateResult.entryId : null\n if (!updatedEntryId) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_update_failed', 'Failed to update dictionary entry') })\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.entry',\n resourceId: updatedEntryId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const updated = await findOneWithDecryption(\n context.em.fork(),\n DictionaryEntry,\n updatedEntryId,\n { populate: ['dictionary'] },\n { tenantId: context.auth.tenantId ?? null, organizationId: context.auth.orgId ?? null },\n )\n if (!updated) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_update_failed', 'Failed to update dictionary entry') })\n }\n const response = NextResponse.json({\n id: updated.id,\n value: updated.value,\n label: updated.label,\n color: updated.color,\n icon: updated.icon,\n position: updated.position ?? 0,\n isDefault: updated.isDefault ?? false,\n createdAt: updated.createdAt,\n updatedAt: updated.updatedAt,\n })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'dictionaries.entry',\n resourceId: updatedEntryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to update dictionary entry', { err })\n return NextResponse.json({ error: 'Failed to update dictionary entry' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { dictionaryId?: string; entryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n const { dictionaryId, entryId } = paramsSchema.parse({\n dictionaryId: ctx.params?.dictionaryId,\n entryId: ctx.params?.entryId,\n })\n const dictionary = await loadDictionary(context, dictionaryId)\n const entry = await loadEntry(context, dictionary, entryId)\n await enforceCommandOptimisticLockWithGuards(context.container, {\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n current: entry.updatedAt ?? null,\n request: req,\n })\n\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { logEntry } = await commandBus.execute('dictionaries.entries.delete', {\n input: { body: { id: entry.id } },\n ctx: context.ctx,\n })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.entry',\n resourceId: entry.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const response = NextResponse.json({ ok: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'dictionaries.entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to delete dictionary entry', { err })\n return NextResponse.json({ error: 'Failed to delete dictionary entry' }, { status: 500 })\n }\n}\n\nconst dictionaryEntryPatchDoc: OpenApiMethodDoc = {\n summary: 'Update dictionary entry',\n description: 'Updates the specified dictionary entry using the command bus pipeline.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: updateEntryDocSchema,\n description: 'Fields to update on the dictionary entry.',\n },\n responses: [\n { status: 200, description: 'Dictionary entry updated.', schema: dictionaryEntryResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary or entry not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to update entry', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryEntryDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete dictionary entry',\n description: 'Deletes the specified dictionary entry via the command bus.',\n tags: [dictionariesTag],\n responses: [\n { status: 200, description: 'Entry deleted.', schema: dictionariesOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary or entry not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to delete entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary entry resource',\n pathParams: dictionaryEntryParamsSchema,\n methods: {\n PATCH: dictionaryEntryPatchDoc,\n DELETE: dictionaryEntryDeleteDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -117,6 +117,9 @@ async function POST(req, ctx) {
|
|
|
117
117
|
if (isCrudHttpError(err)) {
|
|
118
118
|
return NextResponse.json(err.body, { status: err.status });
|
|
119
119
|
}
|
|
120
|
+
if (err instanceof z.ZodError) {
|
|
121
|
+
return NextResponse.json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
122
|
+
}
|
|
120
123
|
logger.error("Failed to reorder dictionary entries", { err });
|
|
121
124
|
return NextResponse.json({ error: "Failed to reorder dictionary entries" }, { status: 500 });
|
|
122
125
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/dictionaries/api/%5BdictionaryId%5D/entries/reorder/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext } from '@open-mercato/core/modules/dictionaries/api/context'\nimport {\n reorderDictionaryEntriesCommandSchema,\n reorderDictionaryEntriesSchema,\n type ReorderDictionaryEntriesCommandInput,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport {\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesOkSchema,\n dictionariesTag,\n reorderEntriesRequestSchema,\n} from '../../../openapi'\nimport { resolveDictionaryActorId } from '../../../context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const dictionaryEm = context.em.fork()\n const dictionary = await findOneWithDecryption(\n dictionaryEm,\n Dictionary,\n {\n id: dictionaryId,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: context.tenantId, organizationId: context.organizationId },\n )\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n\n const payload = reorderDictionaryEntriesSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionaryId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandInput = reorderDictionaryEntriesCommandSchema.parse({\n dictionaryId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n entries: payload.entries,\n } satisfies ReorderDictionaryEntriesCommandInput)\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<ReorderDictionaryEntriesCommandInput, { dictionaryId: string; updatedIds: string[] }>(\n 'dictionaries.entries.reorder',\n {\n input: commandInput,\n ctx: context.ctx,\n },\n )\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionaryId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = NextResponse.json({ ok: true })\n if (logEntry?.undoToken && logEntry.id && logEntry.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'dictionaries.dictionary',\n resourceId: logEntry.resourceId ?? dictionaryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : new Date().toISOString(),\n }),\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('Failed to reorder dictionary entries', { err })\n return NextResponse.json({ error: 'Failed to reorder dictionary entries' }, { status: 500 })\n }\n}\n\nconst reorderPostDoc: OpenApiMethodDoc = {\n summary: 'Reorder dictionary entries',\n description: 'Updates the position of dictionary entries for drag-and-drop reordering.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: reorderEntriesRequestSchema,\n description: 'Array of entry IDs with their new positions.',\n },\n responses: [\n { status: 200, description: 'Entries reordered.', schema: dictionariesOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to reorder entries', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Reorder dictionary entries',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n POST: reorderPostDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext } from '@open-mercato/core/modules/dictionaries/api/context'\nimport {\n reorderDictionaryEntriesCommandSchema,\n reorderDictionaryEntriesSchema,\n type ReorderDictionaryEntriesCommandInput,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport {\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesOkSchema,\n dictionariesTag,\n reorderEntriesRequestSchema,\n} from '../../../openapi'\nimport { resolveDictionaryActorId } from '../../../context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const dictionaryEm = context.em.fork()\n const dictionary = await findOneWithDecryption(\n dictionaryEm,\n Dictionary,\n {\n id: dictionaryId,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: context.tenantId, organizationId: context.organizationId },\n )\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n\n const payload = reorderDictionaryEntriesSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionaryId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandInput = reorderDictionaryEntriesCommandSchema.parse({\n dictionaryId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n entries: payload.entries,\n } satisfies ReorderDictionaryEntriesCommandInput)\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<ReorderDictionaryEntriesCommandInput, { dictionaryId: string; updatedIds: string[] }>(\n 'dictionaries.entries.reorder',\n {\n input: commandInput,\n ctx: context.ctx,\n },\n )\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionaryId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = NextResponse.json({ ok: true })\n if (logEntry?.undoToken && logEntry.id && logEntry.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'dictionaries.dictionary',\n resourceId: logEntry.resourceId ?? dictionaryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : new Date().toISOString(),\n }),\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to reorder dictionary entries', { err })\n return NextResponse.json({ error: 'Failed to reorder dictionary entries' }, { status: 500 })\n }\n}\n\nconst reorderPostDoc: OpenApiMethodDoc = {\n summary: 'Reorder dictionary entries',\n description: 'Updates the position of dictionary entries for drag-and-drop reordering.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: reorderEntriesRequestSchema,\n description: 'Array of entry IDs with their new positions.',\n },\n responses: [\n { status: 200, description: 'Entries reordered.', schema: dictionariesOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to reorder entries', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Reorder dictionary entries',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n POST: reorderPostDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -129,6 +129,9 @@ async function GET(req, ctx) {
|
|
|
129
129
|
if (isCrudHttpError(err)) {
|
|
130
130
|
return NextResponse.json(err.body, { status: err.status });
|
|
131
131
|
}
|
|
132
|
+
if (err instanceof z.ZodError) {
|
|
133
|
+
return NextResponse.json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
134
|
+
}
|
|
132
135
|
logger.error("Failed to load dictionary entries", { err });
|
|
133
136
|
return NextResponse.json({ error: "Failed to load dictionary entries" }, { status: 500 });
|
|
134
137
|
}
|
|
@@ -219,6 +222,9 @@ async function POST(req, ctx) {
|
|
|
219
222
|
if (isCrudHttpError(err)) {
|
|
220
223
|
return NextResponse.json(err.body, { status: err.status });
|
|
221
224
|
}
|
|
225
|
+
if (err instanceof z.ZodError) {
|
|
226
|
+
return NextResponse.json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
227
|
+
}
|
|
222
228
|
logger.error("Failed to create dictionary entry", { err });
|
|
223
229
|
return NextResponse.json({ error: "Failed to create dictionary entry" }, { status: 500 });
|
|
224
230
|
}
|