@open-mercato/core 0.6.6-develop.6299.1.29224e2d86 → 0.6.6-develop.6304.1.4cf2b975cb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -22
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +8 -7
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +6 -5
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +32 -10
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +77 -32
- package/dist/modules/data_sync/components/IntegrationScheduleTab.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]/route.js +6 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
- package/dist/modules/messages/api/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/api/people/[id]/route.ts +7 -0
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +29 -22
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +8 -7
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +6 -5
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +32 -10
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +77 -33
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +7 -0
- package/src/modules/dictionaries/api/[dictionaryId]/route.ts +7 -0
- package/src/modules/messages/api/route.ts +8 -0
- package/dist/modules/customers/components/detail/CompanyDashboardTab.js +0 -133
- package/dist/modules/customers/components/detail/CompanyDashboardTab.js.map +0 -7
- package/src/modules/customers/components/detail/CompanyDashboardTab.tsx +0 -161
|
@@ -7,12 +7,13 @@ import { KpiCard } from "@open-mercato/ui/backend/charts/KpiCard";
|
|
|
7
7
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
8
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "@open-mercato/shared/lib/browser/
|
|
10
|
+
readVersionedIdSet,
|
|
11
|
+
writeVersionedIdSet,
|
|
12
|
+
clearVersionedPreference
|
|
13
|
+
} from "@open-mercato/shared/lib/browser/versionedPreference";
|
|
14
14
|
import { formatCurrency } from "./utils.js";
|
|
15
15
|
const STORAGE_KEY = "om:company-detail-kpi-hidden";
|
|
16
|
+
const STORAGE_VERSION = 1;
|
|
16
17
|
function sumActiveDeals(deals) {
|
|
17
18
|
return deals.filter((d) => d.status !== "won" && d.status !== "lost" && d.status !== "closed").reduce((sum, d) => {
|
|
18
19
|
const amount = typeof d.valueAmount === "number" ? d.valueAmount : parseFloat(String(d.valueAmount ?? "0"));
|
|
@@ -80,20 +81,20 @@ function CompanyKpiBar({ data }) {
|
|
|
80
81
|
return Math.floor((Date.now() - earliest) / (365.25 * 864e5));
|
|
81
82
|
}, [data.interactions, data.kpis?.clientTenureYears]);
|
|
82
83
|
const [hiddenTiles, setHiddenTiles] = React.useState(
|
|
83
|
-
() =>
|
|
84
|
+
() => readVersionedIdSet(STORAGE_KEY, STORAGE_VERSION)
|
|
84
85
|
);
|
|
85
86
|
const toggleTile = React.useCallback((tileId) => {
|
|
86
87
|
setHiddenTiles((prev) => {
|
|
87
88
|
const next = new Set(prev);
|
|
88
89
|
if (next.has(tileId)) next.delete(tileId);
|
|
89
90
|
else next.add(tileId);
|
|
90
|
-
|
|
91
|
+
writeVersionedIdSet(STORAGE_KEY, STORAGE_VERSION, next);
|
|
91
92
|
return next;
|
|
92
93
|
});
|
|
93
94
|
}, []);
|
|
94
95
|
const showAllTiles = React.useCallback(() => {
|
|
95
96
|
setHiddenTiles(/* @__PURE__ */ new Set());
|
|
96
|
-
|
|
97
|
+
clearVersionedPreference(STORAGE_KEY);
|
|
97
98
|
}, []);
|
|
98
99
|
const kpiTiles = React.useMemo(() => [
|
|
99
100
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/CompanyKpiBar.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { EyeOff } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts/KpiCard'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport {\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { EyeOff } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts/KpiCard'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport {\n readVersionedIdSet,\n writeVersionedIdSet,\n clearVersionedPreference,\n} from '@open-mercato/shared/lib/browser/versionedPreference'\nimport type { CompanyOverview, DealSummary, InteractionSummary } from '../formConfig'\nimport { formatCurrency } from './utils'\n\nconst STORAGE_KEY = 'om:company-detail-kpi-hidden'\nconst STORAGE_VERSION = 1\n\nfunction sumActiveDeals(deals: DealSummary[]): number {\n return deals\n .filter((d) => d.status !== 'won' && d.status !== 'lost' && d.status !== 'closed')\n .reduce((sum, d) => {\n const amount = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))\n return sum + (Number.isFinite(amount) ? amount : 0)\n }, 0)\n}\n\nfunction getActiveDeals(deals: DealSummary[]): DealSummary[] {\n return deals.filter((d) => d.status !== 'won' && d.status !== 'lost' && d.status !== 'closed')\n}\n\nfunction computeActivityTrend(interactions: InteractionSummary[]): KpiTrend | undefined {\n const now = Date.now()\n const weekMs = 7 * 86_400_000\n const thisWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n return d && now - new Date(d).getTime() < weekMs\n }).length\n const lastWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n if (!d) return false\n const diff = now - new Date(d).getTime()\n return diff >= weekMs && diff < weekMs * 2\n }).length\n if (lastWeek === 0 && thisWeek === 0) return undefined\n if (lastWeek === 0) return { value: 100, direction: 'up' }\n const pct = ((thisWeek - lastWeek) / lastWeek) * 100\n if (Math.abs(pct) < 0.5) return { value: 0, direction: 'unchanged' }\n return { value: Math.abs(pct), direction: pct > 0 ? 'up' : 'down' }\n}\n\nfunction computeDealTrend(deals: DealSummary[]): KpiTrend | undefined {\n const active = deals.filter((d) => d.status !== 'won' && d.status !== 'lost' && d.status !== 'closed')\n if (active.length === 0) return undefined\n const now = Date.now()\n const monthMs = 30 * 86_400_000\n const recentDeals = active.filter((d) => d.createdAt && now - new Date(d.createdAt).getTime() < monthMs).length\n if (recentDeals > 0) return { value: recentDeals * 10, direction: 'up' }\n return { value: 0, direction: 'unchanged' }\n}\n\ntype CompanyKpiBarProps = {\n data: CompanyOverview\n}\n\nexport function CompanyKpiBar({ data }: CompanyKpiBarProps) {\n const t = useT()\n\n const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals])\n const activeDealsValue = React.useMemo(\n () => data.kpis?.activeDealsValue ?? sumActiveDeals(data.deals),\n [data.deals, data.kpis?.activeDealsValue],\n )\n const dealCurrency = data.kpis?.dealCurrency ?? activeDeals[0]?.valueCurrency ?? data.deals[0]?.valueCurrency ?? 'PLN'\n const activityTrend = React.useMemo(\n () => data.kpis?.activityTrend ?? computeActivityTrend(data.interactions),\n [data.interactions, data.kpis?.activityTrend],\n )\n const dealTrend = React.useMemo(() => computeDealTrend(data.deals), [data.deals])\n\n const ltvValue = React.useMemo(() => {\n if (data.kpis?.ltvValue !== undefined) return data.kpis.ltvValue\n const wonDeals = data.deals.filter((d) => d.status === 'won')\n if (wonDeals.length === 0) return null\n return wonDeals.reduce((sum, d) => {\n const amt = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))\n return sum + (Number.isFinite(amt) ? amt : 0)\n }, 0)\n }, [data.deals, data.kpis?.ltvValue])\n\n const clientTenureYears = React.useMemo(() => {\n if (data.kpis?.clientTenureYears !== undefined) return data.kpis.clientTenureYears\n const allDates = data.interactions\n .map((i) => i.occurredAt ?? i.scheduledAt ?? i.createdAt)\n .filter(Boolean)\n .map((d) => new Date(d).getTime())\n if (allDates.length === 0) return null\n const earliest = Math.min(...allDates)\n return Math.floor((Date.now() - earliest) / (365.25 * 86_400_000))\n }, [data.interactions, data.kpis?.clientTenureYears])\n\n const [hiddenTiles, setHiddenTiles] = React.useState<Set<string>>(\n () => readVersionedIdSet(STORAGE_KEY, STORAGE_VERSION),\n )\n\n const toggleTile = React.useCallback((tileId: string) => {\n setHiddenTiles((prev) => {\n const next = new Set(prev)\n if (next.has(tileId)) next.delete(tileId)\n else next.add(tileId)\n writeVersionedIdSet(STORAGE_KEY, STORAGE_VERSION, next)\n return next\n })\n }, [])\n\n const showAllTiles = React.useCallback(() => {\n setHiddenTiles(new Set())\n clearVersionedPreference(STORAGE_KEY)\n }, [])\n\n const kpiTiles = React.useMemo(() => [\n {\n id: 'activeDeals',\n title: t('customers.companies.dashboard.kpi.activeDeals', 'ACTIVE DEALS'),\n value: activeDealsValue,\n trend: dealTrend,\n formatValue: (v: number) => formatCurrency(v, dealCurrency),\n comparisonLabel: `${data.kpis?.activeDealsCount ?? activeDeals.length} ${(data.kpis?.activeDealsCount ?? activeDeals.length) === 1 ? 'pipeline' : 'pipelines'}`,\n },\n {\n id: 'activities',\n title: t('customers.companies.dashboard.kpi.activities', 'ACTIVITIES'),\n value: data.kpis?.activityCount ?? data.interactions.length,\n trend: activityTrend,\n comparisonLabel: t('customers.companies.dashboard.kpi.last12months', 'last 12 months'),\n },\n {\n id: 'ltv',\n title: t('customers.companies.dashboard.kpi.ltv', 'CUSTOMER VALUE (LTV)'),\n value: ltvValue,\n formatValue: ltvValue !== null ? (v: number) => formatCurrency(v, dealCurrency) : undefined,\n comparisonLabel: ltvValue !== null\n ? t('customers.companies.dashboard.kpi.wonDeals', 'won deals total')\n : t('customers.companies.dashboard.kpi.noWonDeals', 'No won deals'),\n },\n {\n id: 'clientSince',\n title: t('customers.companies.dashboard.kpi.clientSince', 'CLIENT SINCE'),\n value: clientTenureYears,\n formatValue: clientTenureYears !== null\n ? (v: number) => v < 1\n ? `< 1 ${t('customers.companies.dashboard.kpi.year', 'year')}`\n : `${v} ${v === 1 ? t('customers.companies.dashboard.kpi.year', 'year') : t('customers.companies.dashboard.kpi.years', 'years')}`\n : undefined,\n comparisonLabel: clientTenureYears !== null\n ? `${data.kpis?.completedDealsCount ?? data.deals.filter((d) => d.status === 'won').length} ${t('customers.companies.dashboard.kpi.completedDeals', 'completed deals')}`\n : t('customers.companies.dashboard.kpi.noInteractions', 'No interactions yet'),\n },\n ], [t, activeDealsValue, dealTrend, dealCurrency, activeDeals.length, activityTrend, ltvValue, clientTenureYears, data.deals, data.interactions.length, data.kpis])\n\n const visibleTiles = kpiTiles.filter((tile) => !hiddenTiles.has(tile.id))\n\n return (\n <div className=\"space-y-2\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n {visibleTiles.map((tile) => (\n <div key={tile.id} className=\"group relative\">\n <KpiCard\n title={tile.title}\n value={tile.value}\n trend={tile.trend}\n formatValue={tile.formatValue}\n comparisonLabel={tile.comparisonLabel}\n />\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n onClick={() => toggleTile(tile.id)}\n className=\"absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-60\"\n aria-label={t('customers.companies.dashboard.hideTile', 'Hide tile')}\n >\n <EyeOff className=\"size-3.5\" />\n </IconButton>\n </div>\n ))}\n </div>\n {hiddenTiles.size > 0 && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {t('customers.companies.dashboard.hiddenTiles', '{{count}} tiles hidden', { count: hiddenTiles.size })}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto px-1.5 py-0.5 text-xs hover:bg-transparent\"\n onClick={showAllTiles}\n >\n {t('customers.companies.dashboard.showAll', 'Show all')}\n </Button>\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,9 +8,9 @@ import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
|
8
8
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
9
9
|
import { apiCallOrThrow, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "@open-mercato/shared/lib/browser/
|
|
11
|
+
readVersionedIdSet,
|
|
12
|
+
writeVersionedIdSet
|
|
13
|
+
} from "@open-mercato/shared/lib/browser/versionedPreference";
|
|
14
14
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
15
15
|
import { createTranslatorWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
16
16
|
import { useAppEvent } from "@open-mercato/ui/backend/injection/useAppEvent";
|
|
@@ -22,6 +22,7 @@ import { RolesSection } from "./RolesSection.js";
|
|
|
22
22
|
import { LinkEntityDialog } from "../linking/LinkEntityDialog.js";
|
|
23
23
|
import { createPersonLinkAdapter } from "../linking/adapters/personAdapter.js";
|
|
24
24
|
const COMPANY_PEOPLE_PAGE_SIZE = 20;
|
|
25
|
+
const STARRED_PEOPLE_STORAGE_VERSION = 1;
|
|
25
26
|
function normalizeCompanyPerson(record) {
|
|
26
27
|
const id = typeof record.id === "string" ? record.id : null;
|
|
27
28
|
if (!id) return null;
|
|
@@ -106,7 +107,7 @@ function CompanyPeopleSection({
|
|
|
106
107
|
const [listTotalCount, setListTotalCount] = React.useState(initialPeople.length);
|
|
107
108
|
const [listLoading, setListLoading] = React.useState(true);
|
|
108
109
|
const [starredIds, setStarredIds] = React.useState(
|
|
109
|
-
() =>
|
|
110
|
+
() => readVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION)
|
|
110
111
|
);
|
|
111
112
|
const pendingPeopleChangeRef = React.useRef(false);
|
|
112
113
|
const runWriteMutation = React.useCallback(
|
|
@@ -124,7 +125,7 @@ function CompanyPeopleSection({
|
|
|
124
125
|
const next = new Set(prev);
|
|
125
126
|
if (next.has(personId)) next.delete(personId);
|
|
126
127
|
else next.add(personId);
|
|
127
|
-
|
|
128
|
+
writeVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION, next);
|
|
128
129
|
return next;
|
|
129
130
|
});
|
|
130
131
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/CompanyPeopleSection.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Users, Link2, Plus, Filter } from 'lucide-react'\nimport { EmptyState } from '@open-mercato/ui/backend/EmptyState'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport {\n readJsonFromLocalStorage,\n writeJsonToLocalStorage,\n} from '@open-mercato/shared/lib/browser/safeLocalStorage'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport type { SectionAction, TabEmptyStateConfig, Translator } from './types'\nimport { CreatePersonDialog } from './CreatePersonDialog'\nimport { PersonCard } from './PersonCard'\nimport { coerceDisplayName } from '../../lib/displayName'\nimport { DecisionMakersFooter } from './DecisionMakersFooter'\nimport { RolesSection } from './RolesSection'\nimport { LinkEntityDialog, type LinkEntityOption } from '../linking/LinkEntityDialog'\nimport { createPersonLinkAdapter } from '../linking/adapters/personAdapter'\n\ntype GuardedMutationRunner = <T>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\nexport type CompanyPersonSummary = {\n id: string\n displayName: string\n primaryEmail?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycleStage?: string | null\n jobTitle?: string | null\n department?: string | null\n createdAt?: string | null\n organizationId?: string | null\n temperature?: string | null\n source?: string | null\n linkedAt?: string | null\n}\n\nexport type CompanyPeopleSectionProps = {\n companyId: string\n companyName?: string\n initialPeople: CompanyPersonSummary[]\n addActionLabel: string\n emptyLabel: string\n emptyState: TabEmptyStateConfig\n onPeopleChange?: (next: CompanyPersonSummary[]) => void\n onActionChange?: (action: SectionAction | null) => void\n translator?: Translator\n onLoadingChange?: (isLoading: boolean) => void\n onDataRefresh?: () => Promise<void> | void\n runGuardedMutation?: GuardedMutationRunner\n}\n\nconst COMPANY_PEOPLE_PAGE_SIZE = 20\n\nfunction normalizeCompanyPerson(record: Record<string, unknown>): CompanyPersonSummary | null {\n const id = typeof record.id === 'string' ? record.id : null\n if (!id) return null\n const displayName =\n typeof record.displayName === 'string' && record.displayName.trim().length\n ? record.displayName.trim()\n : typeof record.display_name === 'string' && record.display_name.trim().length\n ? record.display_name.trim()\n : null\n if (!displayName) return null\n return {\n id,\n displayName,\n primaryEmail:\n typeof record.primaryEmail === 'string'\n ? record.primaryEmail\n : typeof record.primary_email === 'string'\n ? record.primary_email\n : null,\n primaryPhone:\n typeof record.primaryPhone === 'string'\n ? record.primaryPhone\n : typeof record.primary_phone === 'string'\n ? record.primary_phone\n : null,\n status:\n typeof record.status === 'string'\n ? record.status\n : null,\n lifecycleStage:\n typeof record.lifecycleStage === 'string'\n ? record.lifecycleStage\n : typeof record.lifecycle_stage === 'string'\n ? record.lifecycle_stage\n : null,\n jobTitle:\n typeof record.jobTitle === 'string'\n ? record.jobTitle\n : typeof record.job_title === 'string'\n ? record.job_title\n : null,\n department:\n typeof record.department === 'string'\n ? record.department\n : null,\n createdAt:\n typeof record.createdAt === 'string'\n ? record.createdAt\n : typeof record.created_at === 'string'\n ? record.created_at\n : null,\n organizationId:\n typeof record.organizationId === 'string'\n ? record.organizationId\n : typeof record.organization_id === 'string'\n ? record.organization_id\n : null,\n temperature:\n typeof record.temperature === 'string'\n ? record.temperature\n : null,\n source:\n typeof record.source === 'string'\n ? record.source\n : null,\n linkedAt:\n typeof record.linkedAt === 'string'\n ? record.linkedAt\n : typeof record.linked_at === 'string'\n ? record.linked_at\n : null,\n }\n}\n\nfunction mergeCompanyPeople(items: CompanyPersonSummary[]): CompanyPersonSummary[] {\n const merged = new Map<string, CompanyPersonSummary>()\n items.forEach((item) => merged.set(item.id, item))\n return Array.from(merged.values())\n}\n\nfunction matchesCompanyPersonSearch(person: CompanyPersonSummary, query: string): boolean {\n const normalizedQuery = query.trim().toLowerCase()\n if (!normalizedQuery.length) return true\n const haystack = [\n person.displayName,\n person.jobTitle ?? '',\n person.primaryEmail ?? '',\n person.primaryPhone ?? '',\n person.department ?? '',\n ]\n .join(' ')\n .toLowerCase()\n return haystack.includes(normalizedQuery)\n}\n\nfunction sortCompanyPeople(\n items: CompanyPersonSummary[],\n sortMode: 'name-asc' | 'name-desc' | 'recent',\n): CompanyPersonSummary[] {\n return [...items].sort((left, right) => {\n if (sortMode === 'recent') {\n const leftTimestamp = Date.parse(left.linkedAt ?? left.createdAt ?? '') || 0\n const rightTimestamp = Date.parse(right.linkedAt ?? right.createdAt ?? '') || 0\n return rightTimestamp - leftTimestamp\n }\n const leftLabel = coerceDisplayName(left.displayName).trim().toLowerCase()\n const rightLabel = coerceDisplayName(right.displayName).trim().toLowerCase()\n if (sortMode === 'name-desc') return rightLabel.localeCompare(leftLabel)\n return leftLabel.localeCompare(rightLabel)\n })\n}\n\nexport function CompanyPeopleSection({\n companyId,\n companyName,\n initialPeople,\n addActionLabel,\n emptyLabel,\n emptyState,\n onPeopleChange,\n onActionChange,\n translator,\n onLoadingChange,\n onDataRefresh,\n runGuardedMutation,\n}: CompanyPeopleSectionProps) {\n const tHook = useT()\n const fallbackTranslator = React.useMemo<Translator>(\n () => createTranslatorWithFallback(tHook),\n [tHook],\n )\n const translate: Translator = translator ?? fallbackTranslator\n const [people, setPeople] = React.useState<CompanyPersonSummary[]>(initialPeople)\n const [removingId, setRemovingId] = React.useState<string | null>(null)\n const [linkDialogOpen, setLinkDialogOpen] = React.useState(false)\n const [createDialogOpen, setCreateDialogOpen] = React.useState(false)\n const [searchQuery, setSearchQuery] = React.useState('')\n const [sortMode, setSortMode] = React.useState<'name-asc' | 'name-desc' | 'recent'>('name-asc')\n const [filtersOpen, setFiltersOpen] = React.useState(true)\n const [visiblePeople, setVisiblePeople] = React.useState<CompanyPersonSummary[]>([])\n const [listPage, setListPage] = React.useState(1)\n const [listTotalPages, setListTotalPages] = React.useState(1)\n const [listTotalCount, setListTotalCount] = React.useState(initialPeople.length)\n const [listLoading, setListLoading] = React.useState(true)\n const [starredIds, setStarredIds] = React.useState<Set<string>>(\n () => new Set(readJsonFromLocalStorage<string[]>(`om:starred-people:${companyId}`, [])),\n )\n const pendingPeopleChangeRef = React.useRef(false)\n\n const runWriteMutation = React.useCallback(\n async <T,>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n ): Promise<T> => {\n if (!runGuardedMutation) {\n return operation()\n }\n return runGuardedMutation(operation, mutationPayload)\n },\n [runGuardedMutation],\n )\n\n const toggleStar = React.useCallback(\n (personId: string) => {\n setStarredIds((prev) => {\n const next = new Set(prev)\n if (next.has(personId)) next.delete(personId)\n else next.add(personId)\n writeJsonToLocalStorage(`om:starred-people:${companyId}`, [...next])\n return next\n })\n },\n [companyId],\n )\n\n const displayedPeople = React.useMemo(\n () => (visiblePeople.length > 0 ? visiblePeople : people),\n [people, visiblePeople],\n )\n const totalLinkedPeople = listTotalCount > 0 ? listTotalCount : displayedPeople.length\n const decisionMakerNames = React.useMemo(\n () =>\n displayedPeople\n .filter((person) => starredIds.has(person.id))\n .map((person) => person.displayName),\n [displayedPeople, starredIds],\n )\n\n React.useEffect(() => {\n const action: SectionAction = {\n label: addActionLabel,\n onClick: () => {\n setCreateDialogOpen(true)\n },\n }\n onActionChange?.(action)\n return () => {\n onActionChange?.(null)\n }\n }, [addActionLabel, onActionChange])\n\n React.useEffect(() => {\n pendingPeopleChangeRef.current = false\n setPeople(initialPeople)\n }, [initialPeople])\n\n React.useEffect(() => {\n if (!pendingPeopleChangeRef.current) return\n pendingPeopleChangeRef.current = false\n onPeopleChange?.(people)\n }, [onPeopleChange, people])\n\n const loadVisiblePeople = React.useCallback(async () => {\n setListLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(listPage),\n pageSize: String(COMPANY_PEOPLE_PAGE_SIZE),\n sort: sortMode,\n })\n if (searchQuery.trim().length > 0) {\n params.set('search', searchQuery.trim())\n }\n const payload = await readApiResultOrThrow<{\n items?: CompanyPersonSummary[]\n page?: number\n total?: number\n totalPages?: number\n }>(\n `/api/customers/companies/${encodeURIComponent(companyId)}/people?${params.toString()}`,\n undefined,\n {\n errorMessage: translate(\n 'customers.companies.detail.people.loadError',\n 'Failed to load people.',\n ),\n },\n )\n const nextTotalCount = typeof payload.total === 'number' ? payload.total : 0\n setVisiblePeople(Array.isArray(payload.items) ? payload.items : [])\n setListPage(typeof payload.page === 'number' ? payload.page : listPage)\n setListTotalCount((current) =>\n searchQuery.trim().length > 0 ? Math.max(current, nextTotalCount) : nextTotalCount,\n )\n setListTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)\n } catch {\n setVisiblePeople([])\n if (searchQuery.trim().length === 0) {\n setListTotalCount(0)\n }\n setListTotalPages(1)\n } finally {\n setListLoading(false)\n }\n }, [companyId, listPage, searchQuery, sortMode, translate])\n\n React.useEffect(() => {\n void loadVisiblePeople()\n }, [loadVisiblePeople])\n\n useAppEvent('customers.person_company_link.deleted', (event) => {\n const payload = event.payload as { companyEntityId?: string | null } | null | undefined\n if (payload && payload.companyEntityId === companyId) {\n void loadVisiblePeople()\n }\n }, [companyId, loadVisiblePeople])\n\n React.useEffect(() => {\n setListPage(1)\n }, [searchQuery, sortMode])\n\n const applyPeopleChange = React.useCallback(\n (updater: (current: CompanyPersonSummary[]) => CompanyPersonSummary[]) => {\n setPeople((current) => {\n const next = updater(current)\n if (next !== current) {\n pendingPeopleChangeRef.current = true\n }\n return next\n })\n },\n [],\n )\n\n const handleLinkConfirm = React.useCallback(\n async ({\n addedIds,\n optionsById,\n }: {\n addedIds: string[]\n removedIds: string[]\n optionsById: Record<string, LinkEntityOption>\n }) => {\n if (!addedIds.length) return\n onLoadingChange?.(true)\n try {\n for (const personId of addedIds) {\n await runWriteMutation(\n () =>\n // optimistic-lock-exempt: person-company link add/remove\n apiCallOrThrow(\n `/api/customers/people/${encodeURIComponent(personId)}/companies`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ companyId }),\n },\n {\n errorMessage: translate(\n 'customers.companies.detail.people.linkError',\n 'Failed to link person to company.',\n ),\n },\n ),\n {\n personId,\n companyId,\n },\n )\n }\n const optimisticPeople: CompanyPersonSummary[] = addedIds\n .map((personId): CompanyPersonSummary | null => {\n const option = optionsById[personId]\n if (!option) return null\n return {\n id: option.id,\n displayName: option.label,\n primaryEmail: null,\n primaryPhone: null,\n jobTitle: option.subtitle ?? null,\n }\n })\n .filter((entry): entry is CompanyPersonSummary => entry !== null)\n if (optimisticPeople.length > 0) {\n applyPeopleChange((current) => mergeCompanyPeople([...current, ...optimisticPeople]))\n setListTotalCount((current) => current + optimisticPeople.length)\n }\n await loadVisiblePeople()\n flash(\n addedIds.length === 1\n ? translate(\n 'customers.companies.detail.people.linkSuccess',\n 'Person linked to company.',\n )\n : translate(\n 'customers.companies.detail.people.linkSuccessMultiple',\n '{{count}} people linked to company.',\n { count: String(addedIds.length) },\n ),\n 'success',\n )\n } catch (err) {\n try {\n await onDataRefresh?.()\n } catch {\n // preserve original linking error for the user\n }\n const message =\n err instanceof Error\n ? err.message\n : translate(\n 'customers.companies.detail.people.linkError',\n 'Failed to link person to company.',\n )\n flash(message, 'error')\n throw err\n } finally {\n onLoadingChange?.(false)\n }\n },\n [\n applyPeopleChange,\n companyId,\n loadVisiblePeople,\n onDataRefresh,\n onLoadingChange,\n runWriteMutation,\n translate,\n ],\n )\n\n const personLinkAdapter = React.useMemo(\n () =>\n createPersonLinkAdapter({\n dialogTitle: translate('customers.linking.person.dialogTitle', 'Link person'),\n dialogSubtitle: companyName\n ? translate(\n 'customers.linking.person.dialogSubtitleFor',\n 'Link an existing contact to {{name}}',\n { name: companyName },\n )\n : translate(\n 'customers.linking.person.dialogSubtitle',\n 'Link an existing contact to this company',\n ),\n sectionLabel: translate('customers.linking.person.sectionLabel', 'MATCHING CONTACTS'),\n searchPlaceholder: translate(\n 'customers.linking.person.searchPlaceholder',\n 'Search all people\u2026',\n ),\n searchEmptyHint: translate(\n 'customers.linking.person.searchEmpty',\n 'No matching people found.',\n ),\n selectedEmptyHint: translate(\n 'customers.linking.person.selectedEmpty',\n 'No people selected.',\n ),\n confirmButtonLabel: translate('customers.linking.person.confirmButton', 'Link person'),\n showLinkSettings: true,\n roleOptions: [\n { id: 'decision_maker', label: 'Decision maker' },\n { id: 'budget_holder', label: 'Budget holder' },\n { id: 'stakeholder', label: 'Stakeholder' },\n { id: 'contact', label: 'Contact' },\n ],\n excludeLinkedCompanyId: companyId,\n addNew: {\n title: translate('customers.linking.person.addNew', 'Add new contact'),\n subtitle: translate(\n 'customers.linking.person.addNewSubtitle',\n 'Company will be filled in automatically',\n ),\n render: ({ onCancel }) => (\n <CreatePersonDialog\n open\n onClose={onCancel}\n companyId={companyId}\n companyName={companyName ?? companyId}\n runGuardedMutation={runWriteMutation}\n onPersonCreated={() => {\n // CreatePersonDialog already created and linked the person to this company\n // via the companyEntityId payload field. Refresh the on-page list and close\n // both the nested and outer dialogs so the user can see the new entry.\n void loadVisiblePeople()\n void onDataRefresh?.()\n setLinkDialogOpen(false)\n onCancel()\n }}\n />\n ),\n },\n }),\n [companyId, companyName, loadVisiblePeople, onDataRefresh, runWriteMutation, translate],\n )\n\n const handleRemove = React.useCallback(\n async (personId: string) => {\n if (!personId || removingId) return\n setRemovingId(personId)\n onLoadingChange?.(true)\n try {\n await runWriteMutation(\n () =>\n // optimistic-lock-exempt: person-company link add/remove\n apiCallOrThrow(\n `/api/customers/people/${encodeURIComponent(personId)}/companies/${encodeURIComponent(companyId)}`,\n { method: 'DELETE' },\n {\n errorMessage: translate(\n 'customers.companies.detail.people.removeError',\n 'Failed to unlink person from company.',\n ),\n },\n ),\n {\n personId,\n companyId,\n },\n )\n await loadVisiblePeople()\n flash(\n translate(\n 'customers.companies.detail.people.removeSuccess',\n 'Person unlinked from company.',\n ),\n 'success',\n )\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : translate(\n 'customers.companies.detail.people.removeError',\n 'Failed to unlink person from company.',\n )\n flash(message, 'error')\n } finally {\n setRemovingId(null)\n onLoadingChange?.(false)\n }\n },\n [\n companyId,\n loadVisiblePeople,\n onLoadingChange,\n removingId,\n runWriteMutation,\n translate,\n ],\n )\n\n const linkAction = (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setLinkDialogOpen(true)}\n >\n <Link2 className=\"mr-1.5 h-4 w-4\" />\n {translate(\n 'customers.companies.detail.people.linkAction',\n 'Link existing person',\n )}\n </Button>\n )\n const addPersonAction = (\n <Button type=\"button\" size=\"sm\" onClick={() => setCreateDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n {addActionLabel}\n </Button>\n )\n\n if (!listLoading && totalLinkedPeople === 0) {\n return (\n <>\n <EmptyState\n icon={<Users className=\"h-10 w-10 text-muted-foreground\" />}\n title={emptyState.title}\n actionLabel={emptyState.actionLabel}\n onAction={() => setCreateDialogOpen(true)}\n >\n <p className=\"text-sm text-muted-foreground\">{emptyLabel}</p>\n <div className=\"mt-4\">{linkAction}</div>\n </EmptyState>\n <LinkEntityDialog\n open={linkDialogOpen}\n onOpenChange={setLinkDialogOpen}\n adapter={personLinkAdapter}\n initialSelectedIds={[]}\n onConfirm={handleLinkConfirm}\n runGuardedMutation={runWriteMutation}\n />\n <CreatePersonDialog\n open={createDialogOpen}\n onClose={() => setCreateDialogOpen(false)}\n companyId={companyId}\n companyName={companyName ?? companyId}\n runGuardedMutation={runWriteMutation}\n onPersonCreated={() => {\n setCreateDialogOpen(false)\n void loadVisiblePeople()\n void onDataRefresh?.()\n }}\n />\n </>\n )\n }\n\n return (\n <>\n <div className=\"space-y-4\">\n <RolesSection\n entityType=\"company\"\n entityId={companyId}\n entityName={companyName ?? null}\n />\n\n <section className=\"rounded-lg border bg-card px-4 py-4 sm:px-5\">\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between\">\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-base font-semibold\">\n {translate(\n 'customers.companies.detail.people.sectionTitle',\n 'People',\n )}\n </h3>\n <Badge\n variant=\"secondary\"\n className=\"rounded-full px-2 py-0 text-xs font-semibold\"\n >\n {totalLinkedPeople}\n </Badge>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {translate(\n 'customers.companies.detail.people.sectionSubtitle',\n 'Employees and decision makers on the client side',\n )}\n </p>\n </div>\n <div className=\"flex flex-wrap items-center gap-2 xl:justify-end\">\n {linkAction}\n {addPersonAction}\n </div>\n </div>\n\n {totalLinkedPeople > 0 ? (\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center\">\n {filtersOpen ? (\n <div className=\"min-w-0 flex-1\">\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n placeholder={translate(\n 'customers.companies.detail.people.searchPlaceholder',\n 'Search by name, role, email...',\n )}\n className=\"h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring\"\n />\n </div>\n ) : null}\n <div className=\"flex flex-wrap items-center gap-2 lg:justify-end\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setFiltersOpen((current) => !current)}\n className=\"h-10\"\n >\n <Filter className=\"mr-1.5 h-4 w-4\" />\n {translate(\n 'customers.companies.detail.people.filter',\n 'Filters',\n )}\n </Button>\n {filtersOpen ? (\n <select\n value={sortMode}\n onChange={(event) =>\n setSortMode(event.target.value as 'name-asc' | 'name-desc' | 'recent')\n }\n className=\"h-10 min-w-[11rem] rounded-md border bg-background px-3 text-sm focus:outline-none focus:ring-1 focus:ring-ring\"\n >\n <option value=\"name-asc\">\n {translate(\n 'customers.companies.detail.people.sortNameAsc',\n 'Sort: Name A-Z',\n )}\n </option>\n <option value=\"name-desc\">\n {translate(\n 'customers.companies.detail.people.sortNameDesc',\n 'Sort: Name Z-A',\n )}\n </option>\n <option value=\"recent\">\n {translate(\n 'customers.companies.detail.people.sortRecent',\n 'Sort: Recently linked',\n )}\n </option>\n </select>\n ) : null}\n </div>\n </div>\n ) : null}\n\n {listLoading ? (\n <p className=\"py-6 text-center text-sm text-muted-foreground\">\n {translate('customers.companies.detail.people.loading', 'Loading people\u2026')}\n </p>\n ) : visiblePeople.length > 0 ? (\n <>\n <div\n className=\"grid items-start gap-4\"\n style={{\n gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 19.5rem), 1fr))',\n }}\n >\n {visiblePeople.map((person) => (\n <PersonCard\n key={person.id}\n person={person}\n isStarred={starredIds.has(person.id)}\n onToggleStar={toggleStar}\n onUnlink={handleRemove}\n />\n ))}\n </div>\n {listTotalPages > 1 ? (\n <div className=\"flex items-center justify-between border-t border-border/60 pt-3 text-sm text-muted-foreground\">\n <span>\n {translate(\n 'customers.companies.detail.people.pageSummary',\n 'Page {{page}} of {{total}}',\n {\n page: listPage,\n total: listTotalPages,\n },\n )}\n </span>\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setListPage((current) => Math.max(1, current - 1))}\n disabled={listPage <= 1}\n >\n {translate('customers.companies.detail.people.previous', 'Previous')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() =>\n setListPage((current) => Math.min(listTotalPages, current + 1))\n }\n disabled={listPage >= listTotalPages}\n >\n {translate('customers.companies.detail.people.next', 'Next')}\n </Button>\n </div>\n </div>\n ) : null}\n </>\n ) : totalLinkedPeople > 0 ? (\n <p className=\"py-6 text-center text-sm text-muted-foreground\">\n {translate(\n 'customers.companies.detail.people.noSearchResults',\n 'No people match your search.',\n )}\n </p>\n ) : null}\n </div>\n </section>\n\n <DecisionMakersFooter\n names={decisionMakerNames}\n onSendInvitation={() => {\n const starredEmails = displayedPeople\n .filter((person) => starredIds.has(person.id) && person.primaryEmail)\n .map((person) => person.primaryEmail!)\n if (starredEmails.length > 0) {\n window.open(`mailto:${starredEmails.join(',')}`, '_blank')\n }\n }}\n />\n </div>\n\n <LinkEntityDialog\n open={linkDialogOpen}\n onOpenChange={setLinkDialogOpen}\n adapter={personLinkAdapter}\n initialSelectedIds={[]}\n onConfirm={handleLinkConfirm}\n runGuardedMutation={runWriteMutation}\n />\n\n <CreatePersonDialog\n open={createDialogOpen}\n onClose={() => setCreateDialogOpen(false)}\n companyId={companyId}\n companyName={companyName ?? companyId}\n runGuardedMutation={runWriteMutation}\n onPersonCreated={() => {\n setCreateDialogOpen(false)\n void loadVisiblePeople()\n void onDataRefresh?.()\n }}\n />\n </>\n )\n}\n\nexport default CompanyPeopleSection\n\nexport { mergeCompanyPeople, matchesCompanyPersonSearch, sortCompanyPeople, normalizeCompanyPerson }\n"],
|
|
5
|
-
"mappings": ";AAueY,SAqGN,UArGM,KA+ER,YA/EQ;AAreZ,YAAY,WAAW;AACvB,SAAS,OAAO,OAAO,MAAM,cAAc;AAC3C,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,aAAa;AACtB,SAAS,gBAAgB,4BAA4B;AACrD;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AACrB,SAAS,oCAAoC;AAC7C,SAAS,mBAAmB;AAE5B,SAAS,0BAA0B;AACnC,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,wBAA+C;AACxD,SAAS,+BAA+B;AAsCxC,MAAM,2BAA2B;AAEjC,SAAS,uBAAuB,QAA8D;AAC5F,QAAM,KAAK,OAAO,OAAO,OAAO,WAAW,OAAO,KAAK;AACvD,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,cACJ,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,EAAE,SAChE,OAAO,YAAY,KAAK,IACxB,OAAO,OAAO,iBAAiB,YAAY,OAAO,aAAa,KAAK,EAAE,SACpE,OAAO,aAAa,KAAK,IACzB;AACR,MAAI,CAAC,YAAa,QAAO;AACzB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cACE,OAAO,OAAO,iBAAiB,WAC3B,OAAO,eACP,OAAO,OAAO,kBAAkB,WAC9B,OAAO,gBACP;AAAA,IACR,cACE,OAAO,OAAO,iBAAiB,WAC3B,OAAO,eACP,OAAO,OAAO,kBAAkB,WAC9B,OAAO,gBACP;AAAA,IACR,QACE,OAAO,OAAO,WAAW,WACrB,OAAO,SACP;AAAA,IACN,gBACE,OAAO,OAAO,mBAAmB,WAC7B,OAAO,iBACP,OAAO,OAAO,oBAAoB,WAChC,OAAO,kBACP;AAAA,IACR,UACE,OAAO,OAAO,aAAa,WACvB,OAAO,WACP,OAAO,OAAO,cAAc,WAC1B,OAAO,YACP;AAAA,IACR,YACE,OAAO,OAAO,eAAe,WACzB,OAAO,aACP;AAAA,IACN,WACE,OAAO,OAAO,cAAc,WACxB,OAAO,YACP,OAAO,OAAO,eAAe,WAC3B,OAAO,aACP;AAAA,IACR,gBACE,OAAO,OAAO,mBAAmB,WAC7B,OAAO,iBACP,OAAO,OAAO,oBAAoB,WAChC,OAAO,kBACP;AAAA,IACR,aACE,OAAO,OAAO,gBAAgB,WAC1B,OAAO,cACP;AAAA,IACN,QACE,OAAO,OAAO,WAAW,WACrB,OAAO,SACP;AAAA,IACN,UACE,OAAO,OAAO,aAAa,WACvB,OAAO,WACP,OAAO,OAAO,cAAc,WAC1B,OAAO,YACP;AAAA,EACV;AACF;AAEA,SAAS,mBAAmB,OAAuD;AACjF,QAAM,SAAS,oBAAI,IAAkC;AACrD,QAAM,QAAQ,CAAC,SAAS,OAAO,IAAI,KAAK,IAAI,IAAI,CAAC;AACjD,SAAO,MAAM,KAAK,OAAO,OAAO,CAAC;AACnC;AAEA,SAAS,2BAA2B,QAA8B,OAAwB;AACxF,QAAM,kBAAkB,MAAM,KAAK,EAAE,YAAY;AACjD,MAAI,CAAC,gBAAgB,OAAQ,QAAO;AACpC,QAAM,WAAW;AAAA,IACf,OAAO;AAAA,IACP,OAAO,YAAY;AAAA,IACnB,OAAO,gBAAgB;AAAA,IACvB,OAAO,gBAAgB;AAAA,IACvB,OAAO,cAAc;AAAA,EACvB,EACG,KAAK,GAAG,EACR,YAAY;AACf,SAAO,SAAS,SAAS,eAAe;AAC1C;AAEA,SAAS,kBACP,OACA,UACwB;AACxB,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AACtC,QAAI,aAAa,UAAU;AACzB,YAAM,gBAAgB,KAAK,MAAM,KAAK,YAAY,KAAK,aAAa,EAAE,KAAK;AAC3E,YAAM,iBAAiB,KAAK,MAAM,MAAM,YAAY,MAAM,aAAa,EAAE,KAAK;AAC9E,aAAO,iBAAiB;AAAA,IAC1B;AACA,UAAM,YAAY,kBAAkB,KAAK,WAAW,EAAE,KAAK,EAAE,YAAY;AACzE,UAAM,aAAa,kBAAkB,MAAM,WAAW,EAAE,KAAK,EAAE,YAAY;AAC3E,QAAI,aAAa,YAAa,QAAO,WAAW,cAAc,SAAS;AACvE,WAAO,UAAU,cAAc,UAAU;AAAA,EAC3C,CAAC;AACH;AAEO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA8B;AAC5B,QAAM,QAAQ,KAAK;AACnB,QAAM,qBAAqB,MAAM;AAAA,IAC/B,MAAM,6BAA6B,KAAK;AAAA,IACxC,CAAC,KAAK;AAAA,EACR;AACA,QAAM,YAAwB,cAAc;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAiC,aAAa;AAChF,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAwB,IAAI;AACtE,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAS,KAAK;AAChE,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAS,KAAK;AACpE,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,EAAE;AACvD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAA8C,UAAU;AAC9F,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,IAAI;AACzD,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAiC,CAAC,CAAC;AACnF,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,CAAC;AAChD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAS,CAAC;AAC5D,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAS,cAAc,MAAM;AAC/E,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,IAAI;AACzD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM;AAAA,IACxC,MAAM,IAAI,IAAI,yBAAmC,qBAAqB,SAAS,IAAI,CAAC,CAAC,CAAC;AAAA,EACxF;AACA,QAAM,yBAAyB,MAAM,OAAO,KAAK;AAEjD,QAAM,mBAAmB,MAAM;AAAA,IAC7B,OACE,WACA,oBACe;AACf,UAAI,CAAC,oBAAoB;AACvB,eAAO,UAAU;AAAA,MACnB;AACA,aAAO,mBAAmB,WAAW,eAAe;AAAA,IACtD;AAAA,IACA,CAAC,kBAAkB;AAAA,EACrB;AAEA,QAAM,aAAa,MAAM;AAAA,IACvB,CAAC,aAAqB;AACpB,oBAAc,CAAC,SAAS;AACtB,cAAM,OAAO,IAAI,IAAI,IAAI;AACzB,YAAI,KAAK,IAAI,QAAQ,EAAG,MAAK,OAAO,QAAQ;AAAA,YACvC,MAAK,IAAI,QAAQ;AACtB,gCAAwB,qBAAqB,SAAS,IAAI,CAAC,GAAG,IAAI,CAAC;AACnE,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,IACA,CAAC,SAAS;AAAA,EACZ;AAEA,QAAM,kBAAkB,MAAM;AAAA,IAC5B,MAAO,cAAc,SAAS,IAAI,gBAAgB;AAAA,IAClD,CAAC,QAAQ,aAAa;AAAA,EACxB;AACA,QAAM,oBAAoB,iBAAiB,IAAI,iBAAiB,gBAAgB;AAChF,QAAM,qBAAqB,MAAM;AAAA,IAC/B,MACE,gBACG,OAAO,CAAC,WAAW,WAAW,IAAI,OAAO,EAAE,CAAC,EAC5C,IAAI,CAAC,WAAW,OAAO,WAAW;AAAA,IACvC,CAAC,iBAAiB,UAAU;AAAA,EAC9B;AAEA,QAAM,UAAU,MAAM;AACpB,UAAM,SAAwB;AAAA,MAC5B,OAAO;AAAA,MACP,SAAS,MAAM;AACb,4BAAoB,IAAI;AAAA,MAC1B;AAAA,IACF;AACA,qBAAiB,MAAM;AACvB,WAAO,MAAM;AACX,uBAAiB,IAAI;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,gBAAgB,cAAc,CAAC;AAEnC,QAAM,UAAU,MAAM;AACpB,2BAAuB,UAAU;AACjC,cAAU,aAAa;AAAA,EACzB,GAAG,CAAC,aAAa,CAAC;AAElB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,uBAAuB,QAAS;AACrC,2BAAuB,UAAU;AACjC,qBAAiB,MAAM;AAAA,EACzB,GAAG,CAAC,gBAAgB,MAAM,CAAC;AAE3B,QAAM,oBAAoB,MAAM,YAAY,YAAY;AACtD,mBAAe,IAAI;AACnB,QAAI;AACF,YAAM,SAAS,IAAI,gBAAgB;AAAA,QACjC,MAAM,OAAO,QAAQ;AAAA,QACrB,UAAU,OAAO,wBAAwB;AAAA,QACzC,MAAM;AAAA,MACR,CAAC;AACD,UAAI,YAAY,KAAK,EAAE,SAAS,GAAG;AACjC,eAAO,IAAI,UAAU,YAAY,KAAK,CAAC;AAAA,MACzC;AACA,YAAM,UAAU,MAAM;AAAA,QAMpB,4BAA4B,mBAAmB,SAAS,CAAC,WAAW,OAAO,SAAS,CAAC;AAAA,QACrF;AAAA,QACA;AAAA,UACE,cAAc;AAAA,YACZ;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AACA,YAAM,iBAAiB,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAC3E,uBAAiB,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC,CAAC;AAClE,kBAAY,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO,QAAQ;AACtE;AAAA,QAAkB,CAAC,YACjB,YAAY,KAAK,EAAE,SAAS,IAAI,KAAK,IAAI,SAAS,cAAc,IAAI;AAAA,MACtE;AACA,wBAAkB,OAAO,QAAQ,eAAe,WAAW,QAAQ,aAAa,CAAC;AAAA,IACnF,QAAQ;AACN,uBAAiB,CAAC,CAAC;AACnB,UAAI,YAAY,KAAK,EAAE,WAAW,GAAG;AACnC,0BAAkB,CAAC;AAAA,MACrB;AACA,wBAAkB,CAAC;AAAA,IACrB,UAAE;AACA,qBAAe,KAAK;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,WAAW,UAAU,aAAa,UAAU,SAAS,CAAC;AAE1D,QAAM,UAAU,MAAM;AACpB,SAAK,kBAAkB;AAAA,EACzB,GAAG,CAAC,iBAAiB,CAAC;AAEtB,cAAY,yCAAyC,CAAC,UAAU;AAC9D,UAAM,UAAU,MAAM;AACtB,QAAI,WAAW,QAAQ,oBAAoB,WAAW;AACpD,WAAK,kBAAkB;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,WAAW,iBAAiB,CAAC;AAEjC,QAAM,UAAU,MAAM;AACpB,gBAAY,CAAC;AAAA,EACf,GAAG,CAAC,aAAa,QAAQ,CAAC;AAE1B,QAAM,oBAAoB,MAAM;AAAA,IAC9B,CAAC,YAAyE;AACxE,gBAAU,CAAC,YAAY;AACrB,cAAM,OAAO,QAAQ,OAAO;AAC5B,YAAI,SAAS,SAAS;AACpB,iCAAuB,UAAU;AAAA,QACnC;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,oBAAoB,MAAM;AAAA,IAC9B,OAAO;AAAA,MACL;AAAA,MACA;AAAA,IACF,MAIM;AACJ,UAAI,CAAC,SAAS,OAAQ;AACtB,wBAAkB,IAAI;AACtB,UAAI;AACF,mBAAW,YAAY,UAAU;AAC/B,gBAAM;AAAA,YACJ;AAAA;AAAA,cAEE;AAAA,gBACE,yBAAyB,mBAAmB,QAAQ,CAAC;AAAA,gBACrD;AAAA,kBACE,QAAQ;AAAA,kBACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,kBAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,CAAC;AAAA,gBACpC;AAAA,gBACA;AAAA,kBACE,cAAc;AAAA,oBACZ;AAAA,oBACA;AAAA,kBACF;AAAA,gBACF;AAAA,cACF;AAAA;AAAA,YACF;AAAA,cACE;AAAA,cACA;AAAA,YACF;AAAA,UACF;AAAA,QACF;AACA,cAAM,mBAA2C,SAC9C,IAAI,CAAC,aAA0C;AAC9C,gBAAM,SAAS,YAAY,QAAQ;AACnC,cAAI,CAAC,OAAQ,QAAO;AACpB,iBAAO;AAAA,YACL,IAAI,OAAO;AAAA,YACX,aAAa,OAAO;AAAA,YACpB,cAAc;AAAA,YACd,cAAc;AAAA,YACd,UAAU,OAAO,YAAY;AAAA,UAC/B;AAAA,QACF,CAAC,EACA,OAAO,CAAC,UAAyC,UAAU,IAAI;AAClE,YAAI,iBAAiB,SAAS,GAAG;AAC/B,4BAAkB,CAAC,YAAY,mBAAmB,CAAC,GAAG,SAAS,GAAG,gBAAgB,CAAC,CAAC;AACpF,4BAAkB,CAAC,YAAY,UAAU,iBAAiB,MAAM;AAAA,QAClE;AACA,cAAM,kBAAkB;AACxB;AAAA,UACE,SAAS,WAAW,IAChB;AAAA,YACE;AAAA,YACA;AAAA,UACF,IACA;AAAA,YACE;AAAA,YACA;AAAA,YACA,EAAE,OAAO,OAAO,SAAS,MAAM,EAAE;AAAA,UACnC;AAAA,UACJ;AAAA,QACF;AAAA,MACF,SAAS,KAAK;AACZ,YAAI;AACF,gBAAM,gBAAgB;AAAA,QACxB,QAAQ;AAAA,QAER;AACA,cAAM,UACJ,eAAe,QACX,IAAI,UACJ;AAAA,UACE;AAAA,UACA;AAAA,QACF;AACN,cAAM,SAAS,OAAO;AACtB,cAAM;AAAA,MACR,UAAE;AACA,0BAAkB,KAAK;AAAA,MACzB;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,oBAAoB,MAAM;AAAA,IAC9B,MACE,wBAAwB;AAAA,MACtB,aAAa,UAAU,wCAAwC,aAAa;AAAA,MAC5E,gBAAgB,cACZ;AAAA,QACE;AAAA,QACA;AAAA,QACA,EAAE,MAAM,YAAY;AAAA,MACtB,IACA;AAAA,QACE;AAAA,QACA;AAAA,MACF;AAAA,MACJ,cAAc,UAAU,yCAAyC,mBAAmB;AAAA,MACpF,mBAAmB;AAAA,QACjB;AAAA,QACA;AAAA,MACF;AAAA,MACA,iBAAiB;AAAA,QACf;AAAA,QACA;AAAA,MACF;AAAA,MACA,mBAAmB;AAAA,QACjB;AAAA,QACA;AAAA,MACF;AAAA,MACA,oBAAoB,UAAU,0CAA0C,aAAa;AAAA,MACrF,kBAAkB;AAAA,MAClB,aAAa;AAAA,QACX,EAAE,IAAI,kBAAkB,OAAO,iBAAiB;AAAA,QAChD,EAAE,IAAI,iBAAiB,OAAO,gBAAgB;AAAA,QAC9C,EAAE,IAAI,eAAe,OAAO,cAAc;AAAA,QAC1C,EAAE,IAAI,WAAW,OAAO,UAAU;AAAA,MACpC;AAAA,MACA,wBAAwB;AAAA,MACxB,QAAQ;AAAA,QACN,OAAO,UAAU,mCAAmC,iBAAiB;AAAA,QACrE,UAAU;AAAA,UACR;AAAA,UACA;AAAA,QACF;AAAA,QACA,QAAQ,CAAC,EAAE,SAAS,MAClB;AAAA,UAAC;AAAA;AAAA,YACC,MAAI;AAAA,YACJ,SAAS;AAAA,YACT;AAAA,YACA,aAAa,eAAe;AAAA,YAC5B,oBAAoB;AAAA,YACpB,iBAAiB,MAAM;AAIrB,mBAAK,kBAAkB;AACvB,mBAAK,gBAAgB;AACrB,gCAAkB,KAAK;AACvB,uBAAS;AAAA,YACX;AAAA;AAAA,QACF;AAAA,MAEJ;AAAA,IACF,CAAC;AAAA,IACH,CAAC,WAAW,aAAa,mBAAmB,eAAe,kBAAkB,SAAS;AAAA,EACxF;AAEA,QAAM,eAAe,MAAM;AAAA,IACzB,OAAO,aAAqB;AAC1B,UAAI,CAAC,YAAY,WAAY;AAC7B,oBAAc,QAAQ;AACtB,wBAAkB,IAAI;AACtB,UAAI;AACF,cAAM;AAAA,UACJ;AAAA;AAAA,YAEE;AAAA,cACE,yBAAyB,mBAAmB,QAAQ,CAAC,cAAc,mBAAmB,SAAS,CAAC;AAAA,cAChG,EAAE,QAAQ,SAAS;AAAA,cACnB;AAAA,gBACE,cAAc;AAAA,kBACZ;AAAA,kBACA;AAAA,gBACF;AAAA,cACF;AAAA,YACF;AAAA;AAAA,UACF;AAAA,YACE;AAAA,YACA;AAAA,UACF;AAAA,QACF;AACA,cAAM,kBAAkB;AACxB;AAAA,UACE;AAAA,YACE;AAAA,YACA;AAAA,UACF;AAAA,UACA;AAAA,QACF;AAAA,MACF,SAAS,KAAK;AACZ,cAAM,UACJ,eAAe,QACX,IAAI,UACJ;AAAA,UACE;AAAA,UACA;AAAA,QACF;AACN,cAAM,SAAS,OAAO;AAAA,MACxB,UAAE;AACA,sBAAc,IAAI;AAClB,0BAAkB,KAAK;AAAA,MACzB;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aACJ;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,SAAS,MAAM,kBAAkB,IAAI;AAAA,MAErC;AAAA,4BAAC,SAAM,WAAU,kBAAiB;AAAA,QACjC;AAAA,UACC;AAAA,UACA;AAAA,QACF;AAAA;AAAA;AAAA,EACF;AAEF,QAAM,kBACJ,qBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAS,MAAM,oBAAoB,IAAI,GACrE;AAAA,wBAAC,QAAK,WAAU,kBAAiB;AAAA,IAChC;AAAA,KACH;AAGF,MAAI,CAAC,eAAe,sBAAsB,GAAG;AAC3C,WACE,iCACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,oBAAC,SAAM,WAAU,mCAAkC;AAAA,UACzD,OAAO,WAAW;AAAA,UAClB,aAAa,WAAW;AAAA,UACxB,UAAU,MAAM,oBAAoB,IAAI;AAAA,UAExC;AAAA,gCAAC,OAAE,WAAU,iCAAiC,sBAAW;AAAA,YACzD,oBAAC,SAAI,WAAU,QAAQ,sBAAW;AAAA;AAAA;AAAA,MACpC;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,cAAc;AAAA,UACd,SAAS;AAAA,UACT,oBAAoB,CAAC;AAAA,UACrB,WAAW;AAAA,UACX,oBAAoB;AAAA;AAAA,MACtB;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,SAAS,MAAM,oBAAoB,KAAK;AAAA,UACxC;AAAA,UACA,aAAa,eAAe;AAAA,UAC5B,oBAAoB;AAAA,UACpB,iBAAiB,MAAM;AACrB,gCAAoB,KAAK;AACzB,iBAAK,kBAAkB;AACvB,iBAAK,gBAAgB;AAAA,UACvB;AAAA;AAAA,MACF;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,iCACE;AAAA,yBAAC,SAAI,WAAU,aACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,YAAW;AAAA,UACX,UAAU;AAAA,UACV,YAAY,eAAe;AAAA;AAAA,MAC7B;AAAA,MAEA,oBAAC,aAAQ,WAAU,+CACjB,+BAAC,SAAI,WAAU,uBACb;AAAA,6BAAC,SAAI,WAAU,qEACb;AAAA,+BAAC,SAAI,WAAU,aACb;AAAA,iCAAC,SAAI,WAAU,2BACb;AAAA,kCAAC,QAAG,WAAU,2BACX;AAAA,gBACC;AAAA,gBACA;AAAA,cACF,GACF;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAQ;AAAA,kBACR,WAAU;AAAA,kBAET;AAAA;AAAA,cACH;AAAA,eACF;AAAA,YACA,oBAAC,OAAE,WAAU,iCACV;AAAA,cACC;AAAA,cACA;AAAA,YACF,GACF;AAAA,aACF;AAAA,UACA,qBAAC,SAAI,WAAU,oDACZ;AAAA;AAAA,YACA;AAAA,aACH;AAAA,WACF;AAAA,QAEC,oBAAoB,IACnB,qBAAC,SAAI,WAAU,mDACZ;AAAA,wBACC,oBAAC,SAAI,WAAU,kBACb;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,OAAO;AAAA,cACP,UAAU,CAAC,UAAU,eAAe,MAAM,OAAO,KAAK;AAAA,cACtD,aAAa;AAAA,gBACX;AAAA,gBACA;AAAA,cACF;AAAA,cACA,WAAU;AAAA;AAAA,UACZ,GACF,IACE;AAAA,UACJ,qBAAC,SAAI,WAAU,oDACb;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,SAAS,MAAM,eAAe,CAAC,YAAY,CAAC,OAAO;AAAA,gBACnD,WAAU;AAAA,gBAEV;AAAA,sCAAC,UAAO,WAAU,kBAAiB;AAAA,kBAClC;AAAA,oBACC;AAAA,oBACA;AAAA,kBACF;AAAA;AAAA;AAAA,YACF;AAAA,YACC,cACC;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO;AAAA,gBACP,UAAU,CAAC,UACT,YAAY,MAAM,OAAO,KAA4C;AAAA,gBAEvE,WAAU;AAAA,gBAEV;AAAA,sCAAC,YAAO,OAAM,YACX;AAAA,oBACC;AAAA,oBACA;AAAA,kBACF,GACF;AAAA,kBACA,oBAAC,YAAO,OAAM,aACX;AAAA,oBACC;AAAA,oBACA;AAAA,kBACF,GACF;AAAA,kBACA,oBAAC,YAAO,OAAM,UACX;AAAA,oBACC;AAAA,oBACA;AAAA,kBACF,GACF;AAAA;AAAA;AAAA,YACF,IACE;AAAA,aACN;AAAA,WACF,IACE;AAAA,QAEH,cACC,oBAAC,OAAE,WAAU,kDACV,oBAAU,6CAA6C,sBAAiB,GAC3E,IACE,cAAc,SAAS,IACzB,iCACE;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,qBAAqB;AAAA,cACvB;AAAA,cAEC,wBAAc,IAAI,CAAC,WAClB;AAAA,gBAAC;AAAA;AAAA,kBAEC;AAAA,kBACA,WAAW,WAAW,IAAI,OAAO,EAAE;AAAA,kBACnC,cAAc;AAAA,kBACd,UAAU;AAAA;AAAA,gBAJL,OAAO;AAAA,cAKd,CACD;AAAA;AAAA,UACH;AAAA,UACC,iBAAiB,IAChB,qBAAC,SAAI,WAAU,kGACb;AAAA,gCAAC,UACE;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,gBACE,MAAM;AAAA,gBACN,OAAO;AAAA,cACT;AAAA,YACF,GACF;AAAA,YACA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,SAAS,MAAM,YAAY,CAAC,YAAY,KAAK,IAAI,GAAG,UAAU,CAAC,CAAC;AAAA,kBAChE,UAAU,YAAY;AAAA,kBAErB,oBAAU,8CAA8C,UAAU;AAAA;AAAA,cACrE;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,SAAS,MACP,YAAY,CAAC,YAAY,KAAK,IAAI,gBAAgB,UAAU,CAAC,CAAC;AAAA,kBAEhE,UAAU,YAAY;AAAA,kBAErB,oBAAU,0CAA0C,MAAM;AAAA;AAAA,cAC7D;AAAA,eACF;AAAA,aACF,IACE;AAAA,WACN,IACE,oBAAoB,IACtB,oBAAC,OAAE,WAAU,kDACV;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF,IACE;AAAA,SACN,GACF;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO;AAAA,UACP,kBAAkB,MAAM;AACtB,kBAAM,gBAAgB,gBACnB,OAAO,CAAC,WAAW,WAAW,IAAI,OAAO,EAAE,KAAK,OAAO,YAAY,EACnE,IAAI,CAAC,WAAW,OAAO,YAAa;AACvC,gBAAI,cAAc,SAAS,GAAG;AAC5B,qBAAO,KAAK,UAAU,cAAc,KAAK,GAAG,CAAC,IAAI,QAAQ;AAAA,YAC3D;AAAA,UACF;AAAA;AAAA,MACF;AAAA,OACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,cAAc;AAAA,QACd,SAAS;AAAA,QACT,oBAAoB,CAAC;AAAA,QACrB,WAAW;AAAA,QACX,oBAAoB;AAAA;AAAA,IACtB;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,SAAS,MAAM,oBAAoB,KAAK;AAAA,QACxC;AAAA,QACA,aAAa,eAAe;AAAA,QAC5B,oBAAoB;AAAA,QACpB,iBAAiB,MAAM;AACrB,8BAAoB,KAAK;AACzB,eAAK,kBAAkB;AACvB,eAAK,gBAAgB;AAAA,QACvB;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;AAEA,IAAO,+BAAQ;",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Users, Link2, Plus, Filter } from 'lucide-react'\nimport { EmptyState } from '@open-mercato/ui/backend/EmptyState'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport {\n readVersionedIdSet,\n writeVersionedIdSet,\n} from '@open-mercato/shared/lib/browser/versionedPreference'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport type { SectionAction, TabEmptyStateConfig, Translator } from './types'\nimport { CreatePersonDialog } from './CreatePersonDialog'\nimport { PersonCard } from './PersonCard'\nimport { coerceDisplayName } from '../../lib/displayName'\nimport { DecisionMakersFooter } from './DecisionMakersFooter'\nimport { RolesSection } from './RolesSection'\nimport { LinkEntityDialog, type LinkEntityOption } from '../linking/LinkEntityDialog'\nimport { createPersonLinkAdapter } from '../linking/adapters/personAdapter'\n\ntype GuardedMutationRunner = <T>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\nexport type CompanyPersonSummary = {\n id: string\n displayName: string\n primaryEmail?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycleStage?: string | null\n jobTitle?: string | null\n department?: string | null\n createdAt?: string | null\n organizationId?: string | null\n temperature?: string | null\n source?: string | null\n linkedAt?: string | null\n}\n\nexport type CompanyPeopleSectionProps = {\n companyId: string\n companyName?: string\n initialPeople: CompanyPersonSummary[]\n addActionLabel: string\n emptyLabel: string\n emptyState: TabEmptyStateConfig\n onPeopleChange?: (next: CompanyPersonSummary[]) => void\n onActionChange?: (action: SectionAction | null) => void\n translator?: Translator\n onLoadingChange?: (isLoading: boolean) => void\n onDataRefresh?: () => Promise<void> | void\n runGuardedMutation?: GuardedMutationRunner\n}\n\nconst COMPANY_PEOPLE_PAGE_SIZE = 20\nconst STARRED_PEOPLE_STORAGE_VERSION = 1\n\nfunction normalizeCompanyPerson(record: Record<string, unknown>): CompanyPersonSummary | null {\n const id = typeof record.id === 'string' ? record.id : null\n if (!id) return null\n const displayName =\n typeof record.displayName === 'string' && record.displayName.trim().length\n ? record.displayName.trim()\n : typeof record.display_name === 'string' && record.display_name.trim().length\n ? record.display_name.trim()\n : null\n if (!displayName) return null\n return {\n id,\n displayName,\n primaryEmail:\n typeof record.primaryEmail === 'string'\n ? record.primaryEmail\n : typeof record.primary_email === 'string'\n ? record.primary_email\n : null,\n primaryPhone:\n typeof record.primaryPhone === 'string'\n ? record.primaryPhone\n : typeof record.primary_phone === 'string'\n ? record.primary_phone\n : null,\n status:\n typeof record.status === 'string'\n ? record.status\n : null,\n lifecycleStage:\n typeof record.lifecycleStage === 'string'\n ? record.lifecycleStage\n : typeof record.lifecycle_stage === 'string'\n ? record.lifecycle_stage\n : null,\n jobTitle:\n typeof record.jobTitle === 'string'\n ? record.jobTitle\n : typeof record.job_title === 'string'\n ? record.job_title\n : null,\n department:\n typeof record.department === 'string'\n ? record.department\n : null,\n createdAt:\n typeof record.createdAt === 'string'\n ? record.createdAt\n : typeof record.created_at === 'string'\n ? record.created_at\n : null,\n organizationId:\n typeof record.organizationId === 'string'\n ? record.organizationId\n : typeof record.organization_id === 'string'\n ? record.organization_id\n : null,\n temperature:\n typeof record.temperature === 'string'\n ? record.temperature\n : null,\n source:\n typeof record.source === 'string'\n ? record.source\n : null,\n linkedAt:\n typeof record.linkedAt === 'string'\n ? record.linkedAt\n : typeof record.linked_at === 'string'\n ? record.linked_at\n : null,\n }\n}\n\nfunction mergeCompanyPeople(items: CompanyPersonSummary[]): CompanyPersonSummary[] {\n const merged = new Map<string, CompanyPersonSummary>()\n items.forEach((item) => merged.set(item.id, item))\n return Array.from(merged.values())\n}\n\nfunction matchesCompanyPersonSearch(person: CompanyPersonSummary, query: string): boolean {\n const normalizedQuery = query.trim().toLowerCase()\n if (!normalizedQuery.length) return true\n const haystack = [\n person.displayName,\n person.jobTitle ?? '',\n person.primaryEmail ?? '',\n person.primaryPhone ?? '',\n person.department ?? '',\n ]\n .join(' ')\n .toLowerCase()\n return haystack.includes(normalizedQuery)\n}\n\nfunction sortCompanyPeople(\n items: CompanyPersonSummary[],\n sortMode: 'name-asc' | 'name-desc' | 'recent',\n): CompanyPersonSummary[] {\n return [...items].sort((left, right) => {\n if (sortMode === 'recent') {\n const leftTimestamp = Date.parse(left.linkedAt ?? left.createdAt ?? '') || 0\n const rightTimestamp = Date.parse(right.linkedAt ?? right.createdAt ?? '') || 0\n return rightTimestamp - leftTimestamp\n }\n const leftLabel = coerceDisplayName(left.displayName).trim().toLowerCase()\n const rightLabel = coerceDisplayName(right.displayName).trim().toLowerCase()\n if (sortMode === 'name-desc') return rightLabel.localeCompare(leftLabel)\n return leftLabel.localeCompare(rightLabel)\n })\n}\n\nexport function CompanyPeopleSection({\n companyId,\n companyName,\n initialPeople,\n addActionLabel,\n emptyLabel,\n emptyState,\n onPeopleChange,\n onActionChange,\n translator,\n onLoadingChange,\n onDataRefresh,\n runGuardedMutation,\n}: CompanyPeopleSectionProps) {\n const tHook = useT()\n const fallbackTranslator = React.useMemo<Translator>(\n () => createTranslatorWithFallback(tHook),\n [tHook],\n )\n const translate: Translator = translator ?? fallbackTranslator\n const [people, setPeople] = React.useState<CompanyPersonSummary[]>(initialPeople)\n const [removingId, setRemovingId] = React.useState<string | null>(null)\n const [linkDialogOpen, setLinkDialogOpen] = React.useState(false)\n const [createDialogOpen, setCreateDialogOpen] = React.useState(false)\n const [searchQuery, setSearchQuery] = React.useState('')\n const [sortMode, setSortMode] = React.useState<'name-asc' | 'name-desc' | 'recent'>('name-asc')\n const [filtersOpen, setFiltersOpen] = React.useState(true)\n const [visiblePeople, setVisiblePeople] = React.useState<CompanyPersonSummary[]>([])\n const [listPage, setListPage] = React.useState(1)\n const [listTotalPages, setListTotalPages] = React.useState(1)\n const [listTotalCount, setListTotalCount] = React.useState(initialPeople.length)\n const [listLoading, setListLoading] = React.useState(true)\n const [starredIds, setStarredIds] = React.useState<Set<string>>(\n () => readVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION),\n )\n const pendingPeopleChangeRef = React.useRef(false)\n\n const runWriteMutation = React.useCallback(\n async <T,>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n ): Promise<T> => {\n if (!runGuardedMutation) {\n return operation()\n }\n return runGuardedMutation(operation, mutationPayload)\n },\n [runGuardedMutation],\n )\n\n const toggleStar = React.useCallback(\n (personId: string) => {\n setStarredIds((prev) => {\n const next = new Set(prev)\n if (next.has(personId)) next.delete(personId)\n else next.add(personId)\n writeVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION, next)\n return next\n })\n },\n [companyId],\n )\n\n const displayedPeople = React.useMemo(\n () => (visiblePeople.length > 0 ? visiblePeople : people),\n [people, visiblePeople],\n )\n const totalLinkedPeople = listTotalCount > 0 ? listTotalCount : displayedPeople.length\n const decisionMakerNames = React.useMemo(\n () =>\n displayedPeople\n .filter((person) => starredIds.has(person.id))\n .map((person) => person.displayName),\n [displayedPeople, starredIds],\n )\n\n React.useEffect(() => {\n const action: SectionAction = {\n label: addActionLabel,\n onClick: () => {\n setCreateDialogOpen(true)\n },\n }\n onActionChange?.(action)\n return () => {\n onActionChange?.(null)\n }\n }, [addActionLabel, onActionChange])\n\n React.useEffect(() => {\n pendingPeopleChangeRef.current = false\n setPeople(initialPeople)\n }, [initialPeople])\n\n React.useEffect(() => {\n if (!pendingPeopleChangeRef.current) return\n pendingPeopleChangeRef.current = false\n onPeopleChange?.(people)\n }, [onPeopleChange, people])\n\n const loadVisiblePeople = React.useCallback(async () => {\n setListLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(listPage),\n pageSize: String(COMPANY_PEOPLE_PAGE_SIZE),\n sort: sortMode,\n })\n if (searchQuery.trim().length > 0) {\n params.set('search', searchQuery.trim())\n }\n const payload = await readApiResultOrThrow<{\n items?: CompanyPersonSummary[]\n page?: number\n total?: number\n totalPages?: number\n }>(\n `/api/customers/companies/${encodeURIComponent(companyId)}/people?${params.toString()}`,\n undefined,\n {\n errorMessage: translate(\n 'customers.companies.detail.people.loadError',\n 'Failed to load people.',\n ),\n },\n )\n const nextTotalCount = typeof payload.total === 'number' ? payload.total : 0\n setVisiblePeople(Array.isArray(payload.items) ? payload.items : [])\n setListPage(typeof payload.page === 'number' ? payload.page : listPage)\n setListTotalCount((current) =>\n searchQuery.trim().length > 0 ? Math.max(current, nextTotalCount) : nextTotalCount,\n )\n setListTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)\n } catch {\n setVisiblePeople([])\n if (searchQuery.trim().length === 0) {\n setListTotalCount(0)\n }\n setListTotalPages(1)\n } finally {\n setListLoading(false)\n }\n }, [companyId, listPage, searchQuery, sortMode, translate])\n\n React.useEffect(() => {\n void loadVisiblePeople()\n }, [loadVisiblePeople])\n\n useAppEvent('customers.person_company_link.deleted', (event) => {\n const payload = event.payload as { companyEntityId?: string | null } | null | undefined\n if (payload && payload.companyEntityId === companyId) {\n void loadVisiblePeople()\n }\n }, [companyId, loadVisiblePeople])\n\n React.useEffect(() => {\n setListPage(1)\n }, [searchQuery, sortMode])\n\n const applyPeopleChange = React.useCallback(\n (updater: (current: CompanyPersonSummary[]) => CompanyPersonSummary[]) => {\n setPeople((current) => {\n const next = updater(current)\n if (next !== current) {\n pendingPeopleChangeRef.current = true\n }\n return next\n })\n },\n [],\n )\n\n const handleLinkConfirm = React.useCallback(\n async ({\n addedIds,\n optionsById,\n }: {\n addedIds: string[]\n removedIds: string[]\n optionsById: Record<string, LinkEntityOption>\n }) => {\n if (!addedIds.length) return\n onLoadingChange?.(true)\n try {\n for (const personId of addedIds) {\n await runWriteMutation(\n () =>\n // optimistic-lock-exempt: person-company link add/remove\n apiCallOrThrow(\n `/api/customers/people/${encodeURIComponent(personId)}/companies`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ companyId }),\n },\n {\n errorMessage: translate(\n 'customers.companies.detail.people.linkError',\n 'Failed to link person to company.',\n ),\n },\n ),\n {\n personId,\n companyId,\n },\n )\n }\n const optimisticPeople: CompanyPersonSummary[] = addedIds\n .map((personId): CompanyPersonSummary | null => {\n const option = optionsById[personId]\n if (!option) return null\n return {\n id: option.id,\n displayName: option.label,\n primaryEmail: null,\n primaryPhone: null,\n jobTitle: option.subtitle ?? null,\n }\n })\n .filter((entry): entry is CompanyPersonSummary => entry !== null)\n if (optimisticPeople.length > 0) {\n applyPeopleChange((current) => mergeCompanyPeople([...current, ...optimisticPeople]))\n setListTotalCount((current) => current + optimisticPeople.length)\n }\n await loadVisiblePeople()\n flash(\n addedIds.length === 1\n ? translate(\n 'customers.companies.detail.people.linkSuccess',\n 'Person linked to company.',\n )\n : translate(\n 'customers.companies.detail.people.linkSuccessMultiple',\n '{{count}} people linked to company.',\n { count: String(addedIds.length) },\n ),\n 'success',\n )\n } catch (err) {\n try {\n await onDataRefresh?.()\n } catch {\n // preserve original linking error for the user\n }\n const message =\n err instanceof Error\n ? err.message\n : translate(\n 'customers.companies.detail.people.linkError',\n 'Failed to link person to company.',\n )\n flash(message, 'error')\n throw err\n } finally {\n onLoadingChange?.(false)\n }\n },\n [\n applyPeopleChange,\n companyId,\n loadVisiblePeople,\n onDataRefresh,\n onLoadingChange,\n runWriteMutation,\n translate,\n ],\n )\n\n const personLinkAdapter = React.useMemo(\n () =>\n createPersonLinkAdapter({\n dialogTitle: translate('customers.linking.person.dialogTitle', 'Link person'),\n dialogSubtitle: companyName\n ? translate(\n 'customers.linking.person.dialogSubtitleFor',\n 'Link an existing contact to {{name}}',\n { name: companyName },\n )\n : translate(\n 'customers.linking.person.dialogSubtitle',\n 'Link an existing contact to this company',\n ),\n sectionLabel: translate('customers.linking.person.sectionLabel', 'MATCHING CONTACTS'),\n searchPlaceholder: translate(\n 'customers.linking.person.searchPlaceholder',\n 'Search all people\u2026',\n ),\n searchEmptyHint: translate(\n 'customers.linking.person.searchEmpty',\n 'No matching people found.',\n ),\n selectedEmptyHint: translate(\n 'customers.linking.person.selectedEmpty',\n 'No people selected.',\n ),\n confirmButtonLabel: translate('customers.linking.person.confirmButton', 'Link person'),\n showLinkSettings: true,\n roleOptions: [\n { id: 'decision_maker', label: 'Decision maker' },\n { id: 'budget_holder', label: 'Budget holder' },\n { id: 'stakeholder', label: 'Stakeholder' },\n { id: 'contact', label: 'Contact' },\n ],\n excludeLinkedCompanyId: companyId,\n addNew: {\n title: translate('customers.linking.person.addNew', 'Add new contact'),\n subtitle: translate(\n 'customers.linking.person.addNewSubtitle',\n 'Company will be filled in automatically',\n ),\n render: ({ onCancel }) => (\n <CreatePersonDialog\n open\n onClose={onCancel}\n companyId={companyId}\n companyName={companyName ?? companyId}\n runGuardedMutation={runWriteMutation}\n onPersonCreated={() => {\n // CreatePersonDialog already created and linked the person to this company\n // via the companyEntityId payload field. Refresh the on-page list and close\n // both the nested and outer dialogs so the user can see the new entry.\n void loadVisiblePeople()\n void onDataRefresh?.()\n setLinkDialogOpen(false)\n onCancel()\n }}\n />\n ),\n },\n }),\n [companyId, companyName, loadVisiblePeople, onDataRefresh, runWriteMutation, translate],\n )\n\n const handleRemove = React.useCallback(\n async (personId: string) => {\n if (!personId || removingId) return\n setRemovingId(personId)\n onLoadingChange?.(true)\n try {\n await runWriteMutation(\n () =>\n // optimistic-lock-exempt: person-company link add/remove\n apiCallOrThrow(\n `/api/customers/people/${encodeURIComponent(personId)}/companies/${encodeURIComponent(companyId)}`,\n { method: 'DELETE' },\n {\n errorMessage: translate(\n 'customers.companies.detail.people.removeError',\n 'Failed to unlink person from company.',\n ),\n },\n ),\n {\n personId,\n companyId,\n },\n )\n await loadVisiblePeople()\n flash(\n translate(\n 'customers.companies.detail.people.removeSuccess',\n 'Person unlinked from company.',\n ),\n 'success',\n )\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : translate(\n 'customers.companies.detail.people.removeError',\n 'Failed to unlink person from company.',\n )\n flash(message, 'error')\n } finally {\n setRemovingId(null)\n onLoadingChange?.(false)\n }\n },\n [\n companyId,\n loadVisiblePeople,\n onLoadingChange,\n removingId,\n runWriteMutation,\n translate,\n ],\n )\n\n const linkAction = (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setLinkDialogOpen(true)}\n >\n <Link2 className=\"mr-1.5 h-4 w-4\" />\n {translate(\n 'customers.companies.detail.people.linkAction',\n 'Link existing person',\n )}\n </Button>\n )\n const addPersonAction = (\n <Button type=\"button\" size=\"sm\" onClick={() => setCreateDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n {addActionLabel}\n </Button>\n )\n\n if (!listLoading && totalLinkedPeople === 0) {\n return (\n <>\n <EmptyState\n icon={<Users className=\"h-10 w-10 text-muted-foreground\" />}\n title={emptyState.title}\n actionLabel={emptyState.actionLabel}\n onAction={() => setCreateDialogOpen(true)}\n >\n <p className=\"text-sm text-muted-foreground\">{emptyLabel}</p>\n <div className=\"mt-4\">{linkAction}</div>\n </EmptyState>\n <LinkEntityDialog\n open={linkDialogOpen}\n onOpenChange={setLinkDialogOpen}\n adapter={personLinkAdapter}\n initialSelectedIds={[]}\n onConfirm={handleLinkConfirm}\n runGuardedMutation={runWriteMutation}\n />\n <CreatePersonDialog\n open={createDialogOpen}\n onClose={() => setCreateDialogOpen(false)}\n companyId={companyId}\n companyName={companyName ?? companyId}\n runGuardedMutation={runWriteMutation}\n onPersonCreated={() => {\n setCreateDialogOpen(false)\n void loadVisiblePeople()\n void onDataRefresh?.()\n }}\n />\n </>\n )\n }\n\n return (\n <>\n <div className=\"space-y-4\">\n <RolesSection\n entityType=\"company\"\n entityId={companyId}\n entityName={companyName ?? null}\n />\n\n <section className=\"rounded-lg border bg-card px-4 py-4 sm:px-5\">\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between\">\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-base font-semibold\">\n {translate(\n 'customers.companies.detail.people.sectionTitle',\n 'People',\n )}\n </h3>\n <Badge\n variant=\"secondary\"\n className=\"rounded-full px-2 py-0 text-xs font-semibold\"\n >\n {totalLinkedPeople}\n </Badge>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {translate(\n 'customers.companies.detail.people.sectionSubtitle',\n 'Employees and decision makers on the client side',\n )}\n </p>\n </div>\n <div className=\"flex flex-wrap items-center gap-2 xl:justify-end\">\n {linkAction}\n {addPersonAction}\n </div>\n </div>\n\n {totalLinkedPeople > 0 ? (\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center\">\n {filtersOpen ? (\n <div className=\"min-w-0 flex-1\">\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n placeholder={translate(\n 'customers.companies.detail.people.searchPlaceholder',\n 'Search by name, role, email...',\n )}\n className=\"h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring\"\n />\n </div>\n ) : null}\n <div className=\"flex flex-wrap items-center gap-2 lg:justify-end\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setFiltersOpen((current) => !current)}\n className=\"h-10\"\n >\n <Filter className=\"mr-1.5 h-4 w-4\" />\n {translate(\n 'customers.companies.detail.people.filter',\n 'Filters',\n )}\n </Button>\n {filtersOpen ? (\n <select\n value={sortMode}\n onChange={(event) =>\n setSortMode(event.target.value as 'name-asc' | 'name-desc' | 'recent')\n }\n className=\"h-10 min-w-[11rem] rounded-md border bg-background px-3 text-sm focus:outline-none focus:ring-1 focus:ring-ring\"\n >\n <option value=\"name-asc\">\n {translate(\n 'customers.companies.detail.people.sortNameAsc',\n 'Sort: Name A-Z',\n )}\n </option>\n <option value=\"name-desc\">\n {translate(\n 'customers.companies.detail.people.sortNameDesc',\n 'Sort: Name Z-A',\n )}\n </option>\n <option value=\"recent\">\n {translate(\n 'customers.companies.detail.people.sortRecent',\n 'Sort: Recently linked',\n )}\n </option>\n </select>\n ) : null}\n </div>\n </div>\n ) : null}\n\n {listLoading ? (\n <p className=\"py-6 text-center text-sm text-muted-foreground\">\n {translate('customers.companies.detail.people.loading', 'Loading people\u2026')}\n </p>\n ) : visiblePeople.length > 0 ? (\n <>\n <div\n className=\"grid items-start gap-4\"\n style={{\n gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 19.5rem), 1fr))',\n }}\n >\n {visiblePeople.map((person) => (\n <PersonCard\n key={person.id}\n person={person}\n isStarred={starredIds.has(person.id)}\n onToggleStar={toggleStar}\n onUnlink={handleRemove}\n />\n ))}\n </div>\n {listTotalPages > 1 ? (\n <div className=\"flex items-center justify-between border-t border-border/60 pt-3 text-sm text-muted-foreground\">\n <span>\n {translate(\n 'customers.companies.detail.people.pageSummary',\n 'Page {{page}} of {{total}}',\n {\n page: listPage,\n total: listTotalPages,\n },\n )}\n </span>\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setListPage((current) => Math.max(1, current - 1))}\n disabled={listPage <= 1}\n >\n {translate('customers.companies.detail.people.previous', 'Previous')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() =>\n setListPage((current) => Math.min(listTotalPages, current + 1))\n }\n disabled={listPage >= listTotalPages}\n >\n {translate('customers.companies.detail.people.next', 'Next')}\n </Button>\n </div>\n </div>\n ) : null}\n </>\n ) : totalLinkedPeople > 0 ? (\n <p className=\"py-6 text-center text-sm text-muted-foreground\">\n {translate(\n 'customers.companies.detail.people.noSearchResults',\n 'No people match your search.',\n )}\n </p>\n ) : null}\n </div>\n </section>\n\n <DecisionMakersFooter\n names={decisionMakerNames}\n onSendInvitation={() => {\n const starredEmails = displayedPeople\n .filter((person) => starredIds.has(person.id) && person.primaryEmail)\n .map((person) => person.primaryEmail!)\n if (starredEmails.length > 0) {\n window.open(`mailto:${starredEmails.join(',')}`, '_blank')\n }\n }}\n />\n </div>\n\n <LinkEntityDialog\n open={linkDialogOpen}\n onOpenChange={setLinkDialogOpen}\n adapter={personLinkAdapter}\n initialSelectedIds={[]}\n onConfirm={handleLinkConfirm}\n runGuardedMutation={runWriteMutation}\n />\n\n <CreatePersonDialog\n open={createDialogOpen}\n onClose={() => setCreateDialogOpen(false)}\n companyId={companyId}\n companyName={companyName ?? companyId}\n runGuardedMutation={runWriteMutation}\n onPersonCreated={() => {\n setCreateDialogOpen(false)\n void loadVisiblePeople()\n void onDataRefresh?.()\n }}\n />\n </>\n )\n}\n\nexport default CompanyPeopleSection\n\nexport { mergeCompanyPeople, matchesCompanyPersonSearch, sortCompanyPeople, normalizeCompanyPerson }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,6 +12,7 @@ import { LogList } from "@open-mercato/ui/backend/LogList";
|
|
|
12
12
|
import { Progress } from "@open-mercato/ui/primitives/progress";
|
|
13
13
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
14
14
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
15
|
+
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
15
16
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
16
17
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
17
18
|
import { LoadingMessage, ErrorMessage, RecordNotFoundState } from "@open-mercato/ui/backend/detail";
|
|
@@ -40,6 +41,9 @@ function SyncRunDetailPage({ params }) {
|
|
|
40
41
|
const router = useRouter();
|
|
41
42
|
const runId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname);
|
|
42
43
|
const t = useT();
|
|
44
|
+
const { runMutation } = useGuardedMutation({
|
|
45
|
+
contextId: "data_sync.runDetail"
|
|
46
|
+
});
|
|
43
47
|
const [run, setRun] = React.useState(null);
|
|
44
48
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
45
49
|
const [error, setError] = React.useState(null);
|
|
@@ -141,31 +145,49 @@ function SyncRunDetailPage({ params }) {
|
|
|
141
145
|
const handleCancel = React.useCallback(async () => {
|
|
142
146
|
const currentRunId = resolveCurrentRunId();
|
|
143
147
|
if (!currentRunId) return;
|
|
144
|
-
const call = await
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
const call = await runMutation({
|
|
149
|
+
// optimistic-lock-exempt: run lifecycle action endpoint (cancel), not a concurrent record edit
|
|
150
|
+
operation: () => apiCall(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/cancel`, {
|
|
151
|
+
method: "POST"
|
|
152
|
+
}, { fallback: null }),
|
|
153
|
+
mutationPayload: { runId: currentRunId },
|
|
154
|
+
context: {
|
|
155
|
+
operation: "update",
|
|
156
|
+
actionId: "cancel-sync-run",
|
|
157
|
+
runId: currentRunId
|
|
158
|
+
}
|
|
159
|
+
});
|
|
147
160
|
if (call.ok) {
|
|
148
161
|
flash(t("data_sync.runs.detail.cancelSuccess"), "success");
|
|
149
162
|
void loadRun();
|
|
150
163
|
} else {
|
|
151
164
|
flash(t("data_sync.runs.detail.cancelError"), "error");
|
|
152
165
|
}
|
|
153
|
-
}, [resolveCurrentRunId, t, loadRun]);
|
|
166
|
+
}, [resolveCurrentRunId, runMutation, t, loadRun]);
|
|
154
167
|
const handleRetry = React.useCallback(async () => {
|
|
155
168
|
const currentRunId = resolveCurrentRunId();
|
|
156
169
|
if (!currentRunId) return;
|
|
157
|
-
const call = await
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
170
|
+
const call = await runMutation({
|
|
171
|
+
// optimistic-lock-exempt: starts a new retry run (create), not a concurrent record edit
|
|
172
|
+
operation: () => apiCall(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/retry`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
body: JSON.stringify({ fromBeginning: false })
|
|
176
|
+
}, { fallback: null }),
|
|
177
|
+
mutationPayload: { runId: currentRunId, fromBeginning: false },
|
|
178
|
+
context: {
|
|
179
|
+
operation: "create",
|
|
180
|
+
actionId: "retry-sync-run",
|
|
181
|
+
runId: currentRunId
|
|
182
|
+
}
|
|
183
|
+
});
|
|
162
184
|
if (call.ok && call.result) {
|
|
163
185
|
flash(t("data_sync.runs.detail.retrySuccess"), "success");
|
|
164
186
|
router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`);
|
|
165
187
|
} else {
|
|
166
188
|
flash(t("data_sync.runs.detail.retryError"), "error");
|
|
167
189
|
}
|
|
168
|
-
}, [resolveCurrentRunId, router, t]);
|
|
190
|
+
}, [resolveCurrentRunId, router, runMutation, t]);
|
|
169
191
|
if (isLoading) return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(LoadingMessage, { label: t("data_sync.runs.detail.title") }) }) });
|
|
170
192
|
if (isNotFound) {
|
|
171
193
|
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/data_sync/backend/data-sync/runs/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { usePathname, useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { LogList, type LogListEntry } from '@open-mercato/ui/backend/LogList'\nimport { Progress } from '@open-mercato/ui/primitives/progress'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { RotateCcw, XCircle } from 'lucide-react'\nimport { getSyncRunStatusVariant } from '../../../../lib/syncRunStatus'\n\ntype SyncRunDetail = {\n id: string\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'\n createdCount: number\n updatedCount: number\n skippedCount: number\n failedCount: number\n batchesCompleted: number\n lastError: string | null\n progressJobId: string | null\n progressJob: {\n id: string\n status: string\n progressPercent: number\n processedCount: number\n totalCount: number | null\n etaSeconds: number | null\n meta?: Record<string, unknown> | null\n } | null\n triggeredBy: string | null\n createdAt: string\n updatedAt: string\n}\n\ntype ProgressEventPayload = {\n jobId?: string\n status?: string\n progressPercent?: number\n processedCount?: number\n totalCount?: number | null\n etaSeconds?: number | null\n meta?: Record<string, unknown> | null\n}\n\ntype LogEntry = {\n id: string\n level: 'info' | 'warn' | 'error'\n message: string\n createdAt: string\n payload?: Record<string, unknown> | null\n}\n\nfunction formatEtaSeconds(seconds: number): string {\n if (seconds < 60) return `${seconds}s`\n if (seconds < 3600) return `${Math.ceil(seconds / 60)}m`\n const hours = Math.floor(seconds / 3600)\n const minutes = Math.ceil((seconds % 3600) / 60)\n return `${hours}h ${minutes}m`\n}\n\ntype SyncRunDetailPageProps = {\n params?: {\n id?: string | string[]\n }\n}\n\nfunction resolveRouteId(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0]\n return value\n}\n\nfunction resolvePathnameId(pathname: string): string | undefined {\n const parts = pathname.split('/').filter(Boolean)\n const runId = parts.at(-1)\n if (!runId || runId === 'runs' || runId === 'data-sync') return undefined\n return decodeURIComponent(runId)\n}\n\nexport default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {\n const pathname = usePathname()\n const router = useRouter()\n const runId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n\n const [run, setRun] = React.useState<SyncRunDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n const [logs, setLogs] = React.useState<LogEntry[]>([])\n const [isLoadingLogs, setIsLoadingLogs] = React.useState(false)\n\n const resolveCurrentRunId = React.useCallback(() => {\n return runId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [runId])\n\n const loadRun = React.useCallback(async () => {\n const currentRunId = resolveCurrentRunId()\n if (!currentRunId) {\n setError(t('data_sync.runs.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsNotFound(false)\n const call = await apiCall<SyncRunDetail>(\n `/api/data_sync/runs/${encodeURIComponent(currentRunId)}`,\n undefined,\n { fallback: null },\n )\n if (!call.ok || !call.result) {\n if (call.status === 404) {\n setIsNotFound(true)\n } else {\n setError(t('data_sync.runs.detail.loadError'))\n }\n setIsLoading(false)\n return\n }\n setRun(call.result)\n setIsLoading(false)\n }, [resolveCurrentRunId, t])\n\n const loadLogs = React.useCallback(async () => {\n const currentRunId = resolveCurrentRunId()\n if (!currentRunId) return\n setIsLoadingLogs(true)\n const params = new URLSearchParams({ runId: currentRunId, pageSize: '50' })\n const call = await apiCall<{ items: LogEntry[] }>(\n `/api/integrations/logs?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n if (call.ok && call.result) {\n setLogs(call.result.items)\n }\n setIsLoadingLogs(false)\n }, [resolveCurrentRunId])\n\n React.useEffect(() => {\n void loadRun()\n void loadLogs()\n }, [loadRun, loadLogs])\n\n const handleProgressEvent = React.useCallback((payload: ProgressEventPayload) => {\n const eventJobId = typeof payload.jobId === 'string' ? payload.jobId : null\n if (!eventJobId) return\n\n setRun((current) => {\n if (!current?.progressJobId || current.progressJobId !== eventJobId) return current\n return {\n ...current,\n status: (payload.status as SyncRunDetail['status']) ?? current.status,\n progressJob: {\n id: eventJobId,\n status: payload.status ?? current.progressJob?.status ?? current.status,\n progressPercent: payload.progressPercent ?? current.progressJob?.progressPercent ?? 0,\n processedCount: payload.processedCount ?? current.progressJob?.processedCount ?? 0,\n totalCount: payload.totalCount ?? current.progressJob?.totalCount ?? null,\n etaSeconds: payload.etaSeconds ?? current.progressJob?.etaSeconds ?? null,\n meta: payload.meta ?? current.progressJob?.meta ?? null,\n },\n }\n })\n }, [])\n\n useAppEvent('progress.job.updated', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n }, [handleProgressEvent])\n\n useAppEvent('progress.job.started', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n }, [handleProgressEvent])\n\n useAppEvent('progress.job.completed', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n void loadRun()\n void loadLogs()\n }, [handleProgressEvent, loadLogs, loadRun])\n\n useAppEvent('progress.job.failed', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n void loadRun()\n void loadLogs()\n }, [handleProgressEvent, loadLogs, loadRun])\n\n useAppEvent('progress.job.cancelled', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n void loadRun()\n void loadLogs()\n }, [handleProgressEvent, loadLogs, loadRun])\n\n useAppEvent('om:bridge:reconnected', () => {\n void loadRun()\n void loadLogs()\n }, [loadLogs, loadRun])\n\n const handleCancel = React.useCallback(async () => {\n const currentRunId = resolveCurrentRunId()\n if (!currentRunId) return\n const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/cancel`, {\n method: 'POST',\n }, { fallback: null })\n if (call.ok) {\n flash(t('data_sync.runs.detail.cancelSuccess'), 'success')\n void loadRun()\n } else {\n flash(t('data_sync.runs.detail.cancelError'), 'error')\n }\n }, [resolveCurrentRunId, t, loadRun])\n\n const handleRetry = React.useCallback(async () => {\n const currentRunId = resolveCurrentRunId()\n if (!currentRunId) return\n const call = await apiCall<{ id: string }>(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/retry`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ fromBeginning: false }),\n }, { fallback: null })\n if (call.ok && call.result) {\n flash(t('data_sync.runs.detail.retrySuccess'), 'success')\n router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`)\n } else {\n flash(t('data_sync.runs.detail.retryError'), 'error')\n }\n }, [resolveCurrentRunId, router, t])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('data_sync.runs.detail.title')} /></PageBody></Page>\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('data_sync.runs.detail.notFound', 'Sync run not found.')}\n backHref=\"/backend/data-sync\"\n backLabel={t('data_sync.runs.detail.back')}\n />\n </PageBody>\n </Page>\n )\n }\n if (error || !run) return <Page><PageBody><ErrorMessage label={error ?? t('data_sync.runs.detail.loadError')} /></PageBody></Page>\n\n const totalProcessed = run.createdCount + run.updatedCount + run.skippedCount + run.failedCount\n const progressPercent = run.progressJob?.progressPercent ?? (run.status === 'completed' ? 100 : 0)\n const progressStatus = run.progressJob?.status ?? run.status\n const processedCount = run.progressJob?.processedCount ?? totalProcessed\n const hasProgressTotal = typeof run.progressJob?.totalCount === 'number' && run.progressJob.totalCount > 0\n const etaLabel = run.progressJob?.etaSeconds && run.progressJob.etaSeconds > 0\n ? formatEtaSeconds(run.progressJob.etaSeconds)\n : null\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/data-sync\"\n backLabel={t('data_sync.runs.detail.back')}\n entityTypeLabel={t('data_sync.runs.detail.title')}\n title={`${run.integrationId} \u2014 ${run.entityType}`}\n statusBadge={(\n <div className=\"mt-2 flex flex-wrap gap-2\">\n <Badge variant=\"outline\">{t(`data_sync.dashboard.direction.${run.direction}`)}</Badge>\n <StatusBadge variant={getSyncRunStatusVariant(run.status)}>\n {t(`data_sync.dashboard.status.${run.status}`)}\n </StatusBadge>\n {run.triggeredBy ? <Badge variant=\"outline\">{run.triggeredBy}</Badge> : null}\n </div>\n )}\n actionsContent={(\n <>\n {(run.status === 'running' || run.status === 'pending') ? (\n <Button type=\"button\" variant=\"destructive\" size=\"sm\" onClick={() => void handleCancel()}>\n <XCircle className=\"mr-2 h-4 w-4\" />\n {t('data_sync.runs.detail.cancel')}\n </Button>\n ) : null}\n {run.status === 'failed' ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleRetry()}>\n <RotateCcw className=\"mr-2 h-4 w-4\" />\n {t('data_sync.runs.detail.retry')}\n </Button>\n ) : null}\n </>\n )}\n />\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between gap-3\">\n <CardTitle>{t('data_sync.runs.detail.progress')}</CardTitle>\n <StatusBadge variant={getSyncRunStatusVariant(progressStatus)}>\n {t(`data_sync.dashboard.status.${progressStatus}`)}\n </StatusBadge>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"font-medium\">\n {hasProgressTotal\n ? t('data_sync.runs.detail.progress.percent', { percent: progressPercent })\n : t('data_sync.runs.detail.progress.itemsProcessed', { count: processedCount })}\n </span>\n {etaLabel ? (\n <span className=\"text-muted-foreground\">\n {t('data_sync.runs.detail.progress.eta', { eta: etaLabel })}\n </span>\n ) : null}\n </div>\n {hasProgressTotal ? (\n <Progress value={progressPercent} className=\"h-3\" />\n ) : (\n <div className=\"relative h-3 w-full overflow-hidden rounded-full bg-secondary\">\n <div className=\"absolute inset-y-0 left-0 w-1/2 animate-pulse rounded-full bg-primary/80\" />\n <div className=\"absolute inset-y-0 right-0 w-1/3 rounded-full bg-primary/10\" />\n </div>\n )}\n <div className=\"flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground\">\n <span>\n {hasProgressTotal\n ? t('data_sync.runs.detail.progress.itemsProcessedTotal', {\n processed: processedCount,\n total: run.progressJob?.totalCount ?? 0,\n })\n : t('data_sync.runs.detail.progress.itemsProcessed', { count: processedCount })}\n </span>\n <span>{t('data_sync.runs.detail.progress.batches', { count: run.batchesCompleted })}</span>\n </div>\n </CardContent>\n </Card>\n\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-6 text-center\">\n <div className=\"text-2xl font-bold text-status-success-text\">{run.createdCount}</div>\n <p className=\"text-sm text-muted-foreground\">{t('data_sync.runs.detail.counters.created')}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-6 text-center\">\n <div className=\"text-2xl font-bold text-status-info-text\">{run.updatedCount}</div>\n <p className=\"text-sm text-muted-foreground\">{t('data_sync.runs.detail.counters.updated')}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-6 text-center\">\n <div className=\"text-2xl font-bold text-muted-foreground\">{run.skippedCount}</div>\n <p className=\"text-sm text-muted-foreground\">{t('data_sync.runs.detail.counters.skipped')}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-6 text-center\">\n <div className=\"text-2xl font-bold text-status-error-text\">{run.failedCount}</div>\n <p className=\"text-sm text-muted-foreground\">{t('data_sync.runs.detail.counters.failed')}</p>\n </CardContent>\n </Card>\n </div>\n\n {run.lastError && (\n <Card className=\"border-status-error-border bg-status-error-bg\">\n <CardHeader>\n <CardTitle className=\"text-status-error-text\">{t('data_sync.runs.detail.error')}</CardTitle>\n </CardHeader>\n <CardContent>\n <pre className=\"text-sm text-status-error-text whitespace-pre-wrap\">{run.lastError}</pre>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>{t('data_sync.runs.detail.logs')}</CardTitle>\n </CardHeader>\n <CardContent>\n {isLoadingLogs ? (\n <div className=\"flex justify-center py-4\"><Spinner /></div>\n ) : (\n <LogList\n entries={logs.map<LogListEntry>((log) => ({\n id: log.id,\n time: new Date(log.createdAt).toLocaleString(),\n level: log.level,\n message: log.message,\n body: log.payload ? (\n <pre className=\"overflow-x-auto whitespace-pre-wrap rounded-md border bg-card p-3 text-xs\">\n {JSON.stringify(log.payload, null, 2)}\n </pre>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t('data_sync.runs.detail.logs.noPayload', 'No payload recorded for this log entry.')}\n </p>\n ),\n }))}\n emptyMessage={t('data_sync.runs.detail.noLogs')}\n />\n )}\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { usePathname, useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { LogList, type LogListEntry } from '@open-mercato/ui/backend/LogList'\nimport { Progress } from '@open-mercato/ui/primitives/progress'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { RotateCcw, XCircle } from 'lucide-react'\nimport { getSyncRunStatusVariant } from '../../../../lib/syncRunStatus'\n\ntype SyncRunDetail = {\n id: string\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'\n createdCount: number\n updatedCount: number\n skippedCount: number\n failedCount: number\n batchesCompleted: number\n lastError: string | null\n progressJobId: string | null\n progressJob: {\n id: string\n status: string\n progressPercent: number\n processedCount: number\n totalCount: number | null\n etaSeconds: number | null\n meta?: Record<string, unknown> | null\n } | null\n triggeredBy: string | null\n createdAt: string\n updatedAt: string\n}\n\ntype ProgressEventPayload = {\n jobId?: string\n status?: string\n progressPercent?: number\n processedCount?: number\n totalCount?: number | null\n etaSeconds?: number | null\n meta?: Record<string, unknown> | null\n}\n\ntype LogEntry = {\n id: string\n level: 'info' | 'warn' | 'error'\n message: string\n createdAt: string\n payload?: Record<string, unknown> | null\n}\n\nfunction formatEtaSeconds(seconds: number): string {\n if (seconds < 60) return `${seconds}s`\n if (seconds < 3600) return `${Math.ceil(seconds / 60)}m`\n const hours = Math.floor(seconds / 3600)\n const minutes = Math.ceil((seconds % 3600) / 60)\n return `${hours}h ${minutes}m`\n}\n\ntype SyncRunDetailPageProps = {\n params?: {\n id?: string | string[]\n }\n}\n\nfunction resolveRouteId(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0]\n return value\n}\n\nfunction resolvePathnameId(pathname: string): string | undefined {\n const parts = pathname.split('/').filter(Boolean)\n const runId = parts.at(-1)\n if (!runId || runId === 'runs' || runId === 'data-sync') return undefined\n return decodeURIComponent(runId)\n}\n\nexport default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {\n const pathname = usePathname()\n const router = useRouter()\n const runId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n const { runMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: 'data_sync.runDetail',\n })\n\n const [run, setRun] = React.useState<SyncRunDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n const [logs, setLogs] = React.useState<LogEntry[]>([])\n const [isLoadingLogs, setIsLoadingLogs] = React.useState(false)\n\n const resolveCurrentRunId = React.useCallback(() => {\n return runId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [runId])\n\n const loadRun = React.useCallback(async () => {\n const currentRunId = resolveCurrentRunId()\n if (!currentRunId) {\n setError(t('data_sync.runs.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsNotFound(false)\n const call = await apiCall<SyncRunDetail>(\n `/api/data_sync/runs/${encodeURIComponent(currentRunId)}`,\n undefined,\n { fallback: null },\n )\n if (!call.ok || !call.result) {\n if (call.status === 404) {\n setIsNotFound(true)\n } else {\n setError(t('data_sync.runs.detail.loadError'))\n }\n setIsLoading(false)\n return\n }\n setRun(call.result)\n setIsLoading(false)\n }, [resolveCurrentRunId, t])\n\n const loadLogs = React.useCallback(async () => {\n const currentRunId = resolveCurrentRunId()\n if (!currentRunId) return\n setIsLoadingLogs(true)\n const params = new URLSearchParams({ runId: currentRunId, pageSize: '50' })\n const call = await apiCall<{ items: LogEntry[] }>(\n `/api/integrations/logs?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n if (call.ok && call.result) {\n setLogs(call.result.items)\n }\n setIsLoadingLogs(false)\n }, [resolveCurrentRunId])\n\n React.useEffect(() => {\n void loadRun()\n void loadLogs()\n }, [loadRun, loadLogs])\n\n const handleProgressEvent = React.useCallback((payload: ProgressEventPayload) => {\n const eventJobId = typeof payload.jobId === 'string' ? payload.jobId : null\n if (!eventJobId) return\n\n setRun((current) => {\n if (!current?.progressJobId || current.progressJobId !== eventJobId) return current\n return {\n ...current,\n status: (payload.status as SyncRunDetail['status']) ?? current.status,\n progressJob: {\n id: eventJobId,\n status: payload.status ?? current.progressJob?.status ?? current.status,\n progressPercent: payload.progressPercent ?? current.progressJob?.progressPercent ?? 0,\n processedCount: payload.processedCount ?? current.progressJob?.processedCount ?? 0,\n totalCount: payload.totalCount ?? current.progressJob?.totalCount ?? null,\n etaSeconds: payload.etaSeconds ?? current.progressJob?.etaSeconds ?? null,\n meta: payload.meta ?? current.progressJob?.meta ?? null,\n },\n }\n })\n }, [])\n\n useAppEvent('progress.job.updated', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n }, [handleProgressEvent])\n\n useAppEvent('progress.job.started', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n }, [handleProgressEvent])\n\n useAppEvent('progress.job.completed', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n void loadRun()\n void loadLogs()\n }, [handleProgressEvent, loadLogs, loadRun])\n\n useAppEvent('progress.job.failed', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n void loadRun()\n void loadLogs()\n }, [handleProgressEvent, loadLogs, loadRun])\n\n useAppEvent('progress.job.cancelled', (event) => {\n handleProgressEvent(event.payload as ProgressEventPayload)\n void loadRun()\n void loadLogs()\n }, [handleProgressEvent, loadLogs, loadRun])\n\n useAppEvent('om:bridge:reconnected', () => {\n void loadRun()\n void loadLogs()\n }, [loadLogs, loadRun])\n\n const handleCancel = React.useCallback(async () => {\n const currentRunId = resolveCurrentRunId()\n if (!currentRunId) return\n const call = await runMutation({\n // optimistic-lock-exempt: run lifecycle action endpoint (cancel), not a concurrent record edit\n operation: () => apiCall(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/cancel`, {\n method: 'POST',\n }, { fallback: null }),\n mutationPayload: { runId: currentRunId },\n context: {\n operation: 'update',\n actionId: 'cancel-sync-run',\n runId: currentRunId,\n },\n })\n if (call.ok) {\n flash(t('data_sync.runs.detail.cancelSuccess'), 'success')\n void loadRun()\n } else {\n flash(t('data_sync.runs.detail.cancelError'), 'error')\n }\n }, [resolveCurrentRunId, runMutation, t, loadRun])\n\n const handleRetry = React.useCallback(async () => {\n const currentRunId = resolveCurrentRunId()\n if (!currentRunId) return\n const call = await runMutation({\n // optimistic-lock-exempt: starts a new retry run (create), not a concurrent record edit\n operation: () => apiCall<{ id: string }>(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/retry`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ fromBeginning: false }),\n }, { fallback: null }),\n mutationPayload: { runId: currentRunId, fromBeginning: false },\n context: {\n operation: 'create',\n actionId: 'retry-sync-run',\n runId: currentRunId,\n },\n })\n if (call.ok && call.result) {\n flash(t('data_sync.runs.detail.retrySuccess'), 'success')\n router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`)\n } else {\n flash(t('data_sync.runs.detail.retryError'), 'error')\n }\n }, [resolveCurrentRunId, router, runMutation, t])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('data_sync.runs.detail.title')} /></PageBody></Page>\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('data_sync.runs.detail.notFound', 'Sync run not found.')}\n backHref=\"/backend/data-sync\"\n backLabel={t('data_sync.runs.detail.back')}\n />\n </PageBody>\n </Page>\n )\n }\n if (error || !run) return <Page><PageBody><ErrorMessage label={error ?? t('data_sync.runs.detail.loadError')} /></PageBody></Page>\n\n const totalProcessed = run.createdCount + run.updatedCount + run.skippedCount + run.failedCount\n const progressPercent = run.progressJob?.progressPercent ?? (run.status === 'completed' ? 100 : 0)\n const progressStatus = run.progressJob?.status ?? run.status\n const processedCount = run.progressJob?.processedCount ?? totalProcessed\n const hasProgressTotal = typeof run.progressJob?.totalCount === 'number' && run.progressJob.totalCount > 0\n const etaLabel = run.progressJob?.etaSeconds && run.progressJob.etaSeconds > 0\n ? formatEtaSeconds(run.progressJob.etaSeconds)\n : null\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/data-sync\"\n backLabel={t('data_sync.runs.detail.back')}\n entityTypeLabel={t('data_sync.runs.detail.title')}\n title={`${run.integrationId} \u2014 ${run.entityType}`}\n statusBadge={(\n <div className=\"mt-2 flex flex-wrap gap-2\">\n <Badge variant=\"outline\">{t(`data_sync.dashboard.direction.${run.direction}`)}</Badge>\n <StatusBadge variant={getSyncRunStatusVariant(run.status)}>\n {t(`data_sync.dashboard.status.${run.status}`)}\n </StatusBadge>\n {run.triggeredBy ? <Badge variant=\"outline\">{run.triggeredBy}</Badge> : null}\n </div>\n )}\n actionsContent={(\n <>\n {(run.status === 'running' || run.status === 'pending') ? (\n <Button type=\"button\" variant=\"destructive\" size=\"sm\" onClick={() => void handleCancel()}>\n <XCircle className=\"mr-2 h-4 w-4\" />\n {t('data_sync.runs.detail.cancel')}\n </Button>\n ) : null}\n {run.status === 'failed' ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleRetry()}>\n <RotateCcw className=\"mr-2 h-4 w-4\" />\n {t('data_sync.runs.detail.retry')}\n </Button>\n ) : null}\n </>\n )}\n />\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between gap-3\">\n <CardTitle>{t('data_sync.runs.detail.progress')}</CardTitle>\n <StatusBadge variant={getSyncRunStatusVariant(progressStatus)}>\n {t(`data_sync.dashboard.status.${progressStatus}`)}\n </StatusBadge>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"font-medium\">\n {hasProgressTotal\n ? t('data_sync.runs.detail.progress.percent', { percent: progressPercent })\n : t('data_sync.runs.detail.progress.itemsProcessed', { count: processedCount })}\n </span>\n {etaLabel ? (\n <span className=\"text-muted-foreground\">\n {t('data_sync.runs.detail.progress.eta', { eta: etaLabel })}\n </span>\n ) : null}\n </div>\n {hasProgressTotal ? (\n <Progress value={progressPercent} className=\"h-3\" />\n ) : (\n <div className=\"relative h-3 w-full overflow-hidden rounded-full bg-secondary\">\n <div className=\"absolute inset-y-0 left-0 w-1/2 animate-pulse rounded-full bg-primary/80\" />\n <div className=\"absolute inset-y-0 right-0 w-1/3 rounded-full bg-primary/10\" />\n </div>\n )}\n <div className=\"flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground\">\n <span>\n {hasProgressTotal\n ? t('data_sync.runs.detail.progress.itemsProcessedTotal', {\n processed: processedCount,\n total: run.progressJob?.totalCount ?? 0,\n })\n : t('data_sync.runs.detail.progress.itemsProcessed', { count: processedCount })}\n </span>\n <span>{t('data_sync.runs.detail.progress.batches', { count: run.batchesCompleted })}</span>\n </div>\n </CardContent>\n </Card>\n\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-6 text-center\">\n <div className=\"text-2xl font-bold text-status-success-text\">{run.createdCount}</div>\n <p className=\"text-sm text-muted-foreground\">{t('data_sync.runs.detail.counters.created')}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-6 text-center\">\n <div className=\"text-2xl font-bold text-status-info-text\">{run.updatedCount}</div>\n <p className=\"text-sm text-muted-foreground\">{t('data_sync.runs.detail.counters.updated')}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-6 text-center\">\n <div className=\"text-2xl font-bold text-muted-foreground\">{run.skippedCount}</div>\n <p className=\"text-sm text-muted-foreground\">{t('data_sync.runs.detail.counters.skipped')}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-6 text-center\">\n <div className=\"text-2xl font-bold text-status-error-text\">{run.failedCount}</div>\n <p className=\"text-sm text-muted-foreground\">{t('data_sync.runs.detail.counters.failed')}</p>\n </CardContent>\n </Card>\n </div>\n\n {run.lastError && (\n <Card className=\"border-status-error-border bg-status-error-bg\">\n <CardHeader>\n <CardTitle className=\"text-status-error-text\">{t('data_sync.runs.detail.error')}</CardTitle>\n </CardHeader>\n <CardContent>\n <pre className=\"text-sm text-status-error-text whitespace-pre-wrap\">{run.lastError}</pre>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>{t('data_sync.runs.detail.logs')}</CardTitle>\n </CardHeader>\n <CardContent>\n {isLoadingLogs ? (\n <div className=\"flex justify-center py-4\"><Spinner /></div>\n ) : (\n <LogList\n entries={logs.map<LogListEntry>((log) => ({\n id: log.id,\n time: new Date(log.createdAt).toLocaleString(),\n level: log.level,\n message: log.message,\n body: log.payload ? (\n <pre className=\"overflow-x-auto whitespace-pre-wrap rounded-md border bg-card p-3 text-xs\">\n {JSON.stringify(log.payload, null, 2)}\n </pre>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t('data_sync.runs.detail.logs.noPayload', 'No payload recorded for this log entry.')}\n </p>\n ),\n }))}\n emptyMessage={t('data_sync.runs.detail.noLogs')}\n />\n )}\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["params"]
|
|
7
7
|
}
|