@open-mercato/core 0.6.4-develop.4305.1.efaf0ebab1 → 0.6.4-develop.4322.1.7bf54b8070

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +16 -3
  2. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
  3. package/dist/modules/customers/api/companies/[id]/route.js +4 -0
  4. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  5. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +21 -5
  6. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  7. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -5
  8. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  9. package/dist/modules/customers/commands/companies.js +48 -32
  10. package/dist/modules/customers/commands/companies.js.map +2 -2
  11. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +18 -3
  12. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  13. package/dist/modules/entities/api/entities.js +4 -1
  14. package/dist/modules/entities/api/entities.js.map +2 -2
  15. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +14 -9
  16. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  17. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +21 -3
  18. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +2 -2
  19. package/dist/modules/integrations/backend/integrations/[id]/page.js +18 -2
  20. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  21. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +18 -3
  22. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  23. package/jest.config.cjs +3 -0
  24. package/package.json +7 -7
  25. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +20 -3
  26. package/src/modules/customers/api/companies/[id]/route.ts +4 -0
  27. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +25 -5
  28. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +25 -5
  29. package/src/modules/customers/commands/companies.ts +51 -34
  30. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +21 -3
  31. package/src/modules/entities/api/entities.ts +4 -1
  32. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +22 -11
  33. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +36 -3
  34. package/src/modules/integrations/backend/integrations/[id]/page.tsx +21 -2
  35. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +21 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/integrations/backend/integrations/bundle/%5Bid%5D/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { 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 type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\n\ntype CredentialField = IntegrationCredentialField\n\nconst UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])\n\nfunction isEditableCredentialField(field: CredentialField): boolean {\n return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)\n}\n\ntype BundleIntegration = {\n id: string\n title: string\n description?: string\n category?: string\n isEnabled: boolean\n}\n\ntype BundleDetail = {\n integration: {\n id: string\n title: string\n description?: string\n bundleId?: string\n }\n bundle?: {\n id: string\n title: string\n description?: string\n credentials?: { fields: CredentialField[] }\n }\n bundleIntegrations: BundleIntegration[]\n state: { isEnabled: boolean }\n hasCredentials: boolean\n}\n\ntype BundleConfigPageProps = {\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 bundleId = parts.at(-1)\n if (!bundleId || bundleId === 'bundle' || bundleId === 'integrations') return undefined\n return decodeURIComponent(bundleId)\n}\n\nexport default function BundleConfigPage({ params }: BundleConfigPageProps) {\n const pathname = usePathname()\n const bundleId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n\n const [detail, setDetail] = React.useState<BundleDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})\n const [isSavingCreds, setIsSavingCreds] = React.useState(false)\n const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())\n\n const resolveCurrentBundleId = React.useCallback(() => {\n return bundleId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [bundleId])\n\n const load = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n setError(null)\n const call = await apiCall<BundleDetail>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}`,\n undefined,\n { fallback: null },\n )\n if (!call.ok || !call.result) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setDetail(call.result)\n\n const credCall = await apiCall<{ credentials: Record<string, unknown> }>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,\n undefined,\n { fallback: null },\n )\n if (credCall.ok && credCall.result?.credentials) {\n const next = { ...credCall.result.credentials }\n if (currentBundleId === 'storage_s3') {\n const authMode = next.authMode\n if (authMode !== 'access_keys' && authMode !== 'ambient') {\n const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey)\n next.authMode = hasKeys ? 'access_keys' : 'ambient'\n }\n }\n setCredValues(next)\n }\n setIsLoading(false)\n }, [resolveCurrentBundleId, t])\n\n React.useEffect(() => { void load() }, [load])\n\n const handleSaveCredentials = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) return\n setIsSavingCreds(true)\n const call = await apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentials: credValues }),\n }, { fallback: null })\n if (call.ok) {\n flash(t('integrations.detail.credentials.saved'), 'success')\n } else {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n }\n setIsSavingCreds(false)\n }, [resolveCurrentBundleId, credValues, t])\n\n const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean) => {\n setTogglingIds((prev) => new Set(prev).add(integrationId))\n const call = await apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ isEnabled: enabled }),\n }, { fallback: null })\n if (call.ok) {\n setDetail((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n bundleIntegrations: prev.bundleIntegrations.map((item) =>\n item.id === integrationId ? { ...item, isEnabled: enabled } : item,\n ),\n }\n })\n } else {\n flash(t('integrations.detail.stateError'), 'error')\n }\n setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })\n }, [t])\n\n const handleBulkToggle = React.useCallback(async (enabled: boolean) => {\n if (!detail) return\n const targets = detail.bundleIntegrations.filter((item) => item.isEnabled !== enabled)\n await Promise.all(targets.map((item) => handleToggle(item.id, enabled)))\n }, [detail, handleToggle])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>\n if (error || !detail?.bundle) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>\n\n const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)\n\n function isFieldVisible(field: CredentialField): boolean {\n if (!field.visibleWhen) return true\n return credValues[field.visibleWhen.field] === field.visibleWhen.equals\n }\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div>\n <Link href=\"/backend/integrations\" className=\"text-sm text-muted-foreground hover:underline\">\n {t('integrations.detail.back')}\n </Link>\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold\">{detail.bundle.title}</h1>\n {detail.bundle.description && (\n <p className=\"text-muted-foreground mt-1\">{detail.bundle.description}</p>\n )}\n </div>\n\n {credFields.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>{t('integrations.bundle.sharedCredentials')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {credFields.filter(isFieldVisible).map((field) => (\n <div key={field.key} className=\"space-y-1.5\">\n <label className=\"text-sm font-medium\">\n {field.label}{field.required && <span className=\"text-red-500 ml-0.5\">*</span>}\n </label>\n {field.type === 'select' && field.options ? (\n <Select\n value={(credValues[field.key] as string) || undefined}\n onValueChange={(value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? '' }))}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"\u2014\" />\n </SelectTrigger>\n <SelectContent>\n {field.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : field.type === 'boolean' ? (\n <Switch\n checked={Boolean(credValues[field.key])}\n onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}\n />\n ) : (\n <Input\n type={field.type === 'secret' ? 'password' : 'text'}\n placeholder={field.placeholder}\n value={(credValues[field.key] as string) ?? ''}\n onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}\n />\n )}\n </div>\n ))}\n <Button type=\"button\" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>\n {isSavingCreds ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('integrations.detail.credentials.save')}\n </Button>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle>{t('integrations.bundle.integrationToggles')}</CardTitle>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(true)}>\n {t('integrations.marketplace.enableAll')}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(false)}>\n {t('integrations.marketplace.disableAll')}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {detail.bundleIntegrations.map((item) => (\n <div key={item.id} className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <Link\n href={`/backend/integrations/${encodeURIComponent(item.id)}`}\n className=\"text-sm font-medium hover:underline\"\n >\n {item.title}\n </Link>\n {item.category && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">{item.category}</Badge>\n )}\n {item.description && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{item.description}</p>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n <Button asChild variant=\"ghost\" size=\"sm\">\n <Link href={`/backend/integrations/${encodeURIComponent(item.id)}`}>\n {t('integrations.bundle.configureIntegration')}\n </Link>\n </Button>\n <Switch\n checked={item.isEnabled}\n disabled={togglingIds.has(item.id)}\n onCheckedChange={(checked) => void handleToggle(item.id, checked)}\n />\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { 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 type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\n\ntype CredentialField = IntegrationCredentialField\n\nconst UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])\n\nfunction isEditableCredentialField(field: CredentialField): boolean {\n return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)\n}\n\ntype BundleIntegration = {\n id: string\n title: string\n description?: string\n category?: string\n isEnabled: boolean\n}\n\ntype BundleDetail = {\n integration: {\n id: string\n title: string\n description?: string\n bundleId?: string\n }\n bundle?: {\n id: string\n title: string\n description?: string\n credentials?: { fields: CredentialField[] }\n }\n bundleIntegrations: BundleIntegration[]\n state: { isEnabled: boolean }\n hasCredentials: boolean\n}\n\ntype BundleConfigPageProps = {\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 bundleId = parts.at(-1)\n if (!bundleId || bundleId === 'bundle' || bundleId === 'integrations') return undefined\n return decodeURIComponent(bundleId)\n}\n\nexport default function BundleConfigPage({ params }: BundleConfigPageProps) {\n const pathname = usePathname()\n const bundleId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n\n const [detail, setDetail] = React.useState<BundleDetail | 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 [credValues, setCredValues] = React.useState<Record<string, unknown>>({})\n const [isSavingCreds, setIsSavingCreds] = React.useState(false)\n const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())\n\n const resolveCurrentBundleId = React.useCallback(() => {\n return bundleId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [bundleId])\n\n const load = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n setError(null)\n setIsNotFound(false)\n const call = await apiCall<BundleDetail>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}`,\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('integrations.detail.loadError'))\n }\n setIsLoading(false)\n return\n }\n setDetail(call.result)\n\n const credCall = await apiCall<{ credentials: Record<string, unknown> }>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,\n undefined,\n { fallback: null },\n )\n if (credCall.ok && credCall.result?.credentials) {\n const next = { ...credCall.result.credentials }\n if (currentBundleId === 'storage_s3') {\n const authMode = next.authMode\n if (authMode !== 'access_keys' && authMode !== 'ambient') {\n const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey)\n next.authMode = hasKeys ? 'access_keys' : 'ambient'\n }\n }\n setCredValues(next)\n }\n setIsLoading(false)\n }, [resolveCurrentBundleId, t])\n\n React.useEffect(() => { void load() }, [load])\n\n const handleSaveCredentials = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) return\n setIsSavingCreds(true)\n const call = await apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentials: credValues }),\n }, { fallback: null })\n if (call.ok) {\n flash(t('integrations.detail.credentials.saved'), 'success')\n } else {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n }\n setIsSavingCreds(false)\n }, [resolveCurrentBundleId, credValues, t])\n\n const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean) => {\n setTogglingIds((prev) => new Set(prev).add(integrationId))\n const call = await apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ isEnabled: enabled }),\n }, { fallback: null })\n if (call.ok) {\n setDetail((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n bundleIntegrations: prev.bundleIntegrations.map((item) =>\n item.id === integrationId ? { ...item, isEnabled: enabled } : item,\n ),\n }\n })\n } else {\n flash(t('integrations.detail.stateError'), 'error')\n }\n setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })\n }, [t])\n\n const handleBulkToggle = React.useCallback(async (enabled: boolean) => {\n if (!detail) return\n const targets = detail.bundleIntegrations.filter((item) => item.isEnabled !== enabled)\n await Promise.all(targets.map((item) => handleToggle(item.id, enabled)))\n }, [detail, handleToggle])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('integrations.detail.notFound', 'Integration not found.')}\n backHref=\"/backend/integrations\"\n backLabel={t('integrations.detail.backToList', 'Back to integrations')}\n />\n </PageBody>\n </Page>\n )\n }\n if (error || !detail?.bundle) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>\n\n const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)\n\n function isFieldVisible(field: CredentialField): boolean {\n if (!field.visibleWhen) return true\n return credValues[field.visibleWhen.field] === field.visibleWhen.equals\n }\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div>\n <Link href=\"/backend/integrations\" className=\"text-sm text-muted-foreground hover:underline\">\n {t('integrations.detail.back')}\n </Link>\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold\">{detail.bundle.title}</h1>\n {detail.bundle.description && (\n <p className=\"text-muted-foreground mt-1\">{detail.bundle.description}</p>\n )}\n </div>\n\n {credFields.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>{t('integrations.bundle.sharedCredentials')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {credFields.filter(isFieldVisible).map((field) => (\n <div key={field.key} className=\"space-y-1.5\">\n <label className=\"text-sm font-medium\">\n {field.label}{field.required && <span className=\"text-red-500 ml-0.5\">*</span>}\n </label>\n {field.type === 'select' && field.options ? (\n <Select\n value={(credValues[field.key] as string) || undefined}\n onValueChange={(value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? '' }))}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"\u2014\" />\n </SelectTrigger>\n <SelectContent>\n {field.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : field.type === 'boolean' ? (\n <Switch\n checked={Boolean(credValues[field.key])}\n onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}\n />\n ) : (\n <Input\n type={field.type === 'secret' ? 'password' : 'text'}\n placeholder={field.placeholder}\n value={(credValues[field.key] as string) ?? ''}\n onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}\n />\n )}\n </div>\n ))}\n <Button type=\"button\" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>\n {isSavingCreds ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('integrations.detail.credentials.save')}\n </Button>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle>{t('integrations.bundle.integrationToggles')}</CardTitle>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(true)}>\n {t('integrations.marketplace.enableAll')}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(false)}>\n {t('integrations.marketplace.disableAll')}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {detail.bundleIntegrations.map((item) => (\n <div key={item.id} className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <Link\n href={`/backend/integrations/${encodeURIComponent(item.id)}`}\n className=\"text-sm font-medium hover:underline\"\n >\n {item.title}\n </Link>\n {item.category && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">{item.category}</Badge>\n )}\n {item.description && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{item.description}</p>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n <Button asChild variant=\"ghost\" size=\"sm\">\n <Link href={`/backend/integrations/${encodeURIComponent(item.id)}`}>\n {t('integrations.bundle.configureIntegration')}\n </Link>\n </Button>\n <Switch\n checked={item.isEnabled}\n disabled={togglingIds.has(item.id)}\n onCheckedChange={(checked) => void handleToggle(item.id, checked)}\n />\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/jest.config.cjs CHANGED
@@ -1,5 +1,8 @@
1
1
  /** @type {import('jest').Config} */
2
+ const base = require('../../jest.config.base.cjs')
3
+
2
4
  module.exports = {
5
+ ...base,
3
6
  testEnvironment: 'node',
4
7
  testTimeout: 30000,
5
8
  watchman: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.4-develop.4305.1.efaf0ebab1",
3
+ "version": "0.6.4-develop.4322.1.7bf54b8070",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -243,16 +243,16 @@
243
243
  "zod": "^4.4.3"
244
244
  },
245
245
  "peerDependencies": {
246
- "@open-mercato/ai-assistant": "0.6.4-develop.4305.1.efaf0ebab1",
247
- "@open-mercato/shared": "0.6.4-develop.4305.1.efaf0ebab1",
248
- "@open-mercato/ui": "0.6.4-develop.4305.1.efaf0ebab1",
246
+ "@open-mercato/ai-assistant": "0.6.4-develop.4322.1.7bf54b8070",
247
+ "@open-mercato/shared": "0.6.4-develop.4322.1.7bf54b8070",
248
+ "@open-mercato/ui": "0.6.4-develop.4322.1.7bf54b8070",
249
249
  "react": "^19.0.0",
250
250
  "react-dom": "^19.0.0"
251
251
  },
252
252
  "devDependencies": {
253
- "@open-mercato/ai-assistant": "0.6.4-develop.4305.1.efaf0ebab1",
254
- "@open-mercato/shared": "0.6.4-develop.4305.1.efaf0ebab1",
255
- "@open-mercato/ui": "0.6.4-develop.4305.1.efaf0ebab1",
253
+ "@open-mercato/ai-assistant": "0.6.4-develop.4322.1.7bf54b8070",
254
+ "@open-mercato/shared": "0.6.4-develop.4322.1.7bf54b8070",
255
+ "@open-mercato/ui": "0.6.4-develop.4322.1.7bf54b8070",
256
256
  "@testing-library/dom": "^10.4.1",
257
257
  "@testing-library/jest-dom": "^6.9.1",
258
258
  "@testing-library/react": "^16.3.1",
@@ -3,7 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { useRouter } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
- import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
6
+ import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
7
7
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
8
8
  import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
9
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
@@ -49,6 +49,7 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
49
49
  const [exchangeRate, setExchangeRate] = React.useState<ExchangeRateData | null>(null)
50
50
  const [loading, setLoading] = React.useState(true)
51
51
  const [error, setError] = React.useState<string | null>(null)
52
+ const [isNotFound, setIsNotFound] = React.useState(false)
52
53
 
53
54
  const loadOptions = React.useCallback(
54
55
  (query?: string) => loadCurrencyOptions(apiCall, query),
@@ -62,8 +63,10 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
62
63
  const response = await apiCall<{ items: ExchangeRateData[] }>(`/api/currencies/exchange-rates?id=${params?.id}`)
63
64
  if (response.ok && response.result && response.result.items.length > 0) {
64
65
  setExchangeRate(response.result.items[0])
66
+ } else if (response.ok) {
67
+ setIsNotFound(true)
65
68
  } else {
66
- setError(t('exchangeRates.form.errors.notFound'))
69
+ setError(t('exchangeRates.form.errors.load'))
67
70
  }
68
71
  } catch (err) {
69
72
  setError(t('exchangeRates.form.errors.load'))
@@ -89,11 +92,25 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
89
92
  )
90
93
  }
91
94
 
95
+ if (isNotFound) {
96
+ return (
97
+ <Page>
98
+ <PageBody>
99
+ <RecordNotFoundState
100
+ label={t('exchangeRates.form.errors.notFound')}
101
+ backHref="/backend/exchange-rates"
102
+ backLabel={t('exchangeRates.form.actions.backToList', 'Back to exchange rates')}
103
+ />
104
+ </PageBody>
105
+ </Page>
106
+ )
107
+ }
108
+
92
109
  if (error || !exchangeRate) {
93
110
  return (
94
111
  <Page>
95
112
  <PageBody>
96
- <ErrorMessage label={error || t('exchangeRates.form.errors.notFound')} />
113
+ <ErrorMessage label={error ?? t('exchangeRates.form.errors.load')} />
97
114
  </PageBody>
98
115
  </Page>
99
116
  )
@@ -933,6 +933,8 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
933
933
  organizationId: company.organizationId,
934
934
  tenantId: company.tenantId,
935
935
  isActive: company.isActive,
936
+ temperature: company.temperature ?? null,
937
+ renewalQuarter: company.renewalQuarter ?? null,
936
938
  createdAt: company.createdAt.toISOString(),
937
939
  updatedAt: company.updatedAt.toISOString(),
938
940
  },
@@ -1150,6 +1152,8 @@ const companyDetailResponseSchema = z.object({
1150
1152
  organizationId: z.string().uuid().nullable().optional(),
1151
1153
  tenantId: z.string().uuid().nullable().optional(),
1152
1154
  isActive: z.boolean().optional(),
1155
+ temperature: z.string().nullable().optional(),
1156
+ renewalQuarter: z.string().nullable().optional(),
1153
1157
  createdAt: z.string(),
1154
1158
  updatedAt: z.string(),
1155
1159
  }),
@@ -10,7 +10,7 @@ import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/u
10
10
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
11
11
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
12
  import { Button } from '@open-mercato/ui/primitives/button'
13
- import { AttachmentsSection, ErrorMessage, LoadingMessage, type SectionAction } from '@open-mercato/ui/backend/detail'
13
+ import { AttachmentsSection, ErrorMessage, LoadingMessage, RecordNotFoundState, type SectionAction } from '@open-mercato/ui/backend/detail'
14
14
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
15
15
  import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
16
16
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
@@ -54,6 +54,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
54
54
  const [data, setData] = React.useState<CompanyOverview | null>(null)
55
55
  const [isLoading, setIsLoading] = React.useState(true)
56
56
  const [error, setError] = React.useState<string | null>(null)
57
+ const [isNotFound, setIsNotFound] = React.useState(false)
57
58
 
58
59
  // Tab state
59
60
  const initialTab = React.useMemo(() => {
@@ -113,7 +114,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
113
114
  const initialLoadDoneRef = React.useRef(false)
114
115
  const loadData = React.useCallback(async () => {
115
116
  if (!id) {
116
- setError(t('customers.companies.detail.error.notFound', 'Company not found.'))
117
+ setIsNotFound(true)
117
118
  setIsLoading(false)
118
119
  return
119
120
  }
@@ -121,6 +122,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
121
122
  setIsLoading(true)
122
123
  }
123
124
  setError(null)
125
+ setIsNotFound(false)
124
126
  try {
125
127
  const payload = await readApiResultOrThrow<CompanyOverview>(
126
128
  `/api/customers/companies/${encodeURIComponent(id)}`,
@@ -129,8 +131,12 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
129
131
  )
130
132
  setData(payload as CompanyOverview)
131
133
  } catch (err) {
132
- const message = err instanceof Error ? err.message : t('customers.companies.detail.error.load', 'Failed to load company.')
133
- setError(message)
134
+ if ((err as { status?: number }).status === 404) {
135
+ setIsNotFound(true)
136
+ } else {
137
+ const message = err instanceof Error ? err.message : t('customers.companies.detail.error.load', 'Failed to load company.')
138
+ setError(message)
139
+ }
134
140
  if (!initialLoadDoneRef.current) setData(null)
135
141
  } finally {
136
142
  setIsLoading(false)
@@ -373,12 +379,26 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
373
379
  )
374
380
  }
375
381
 
382
+ if (isNotFound) {
383
+ return (
384
+ <Page>
385
+ <PageBody>
386
+ <RecordNotFoundState
387
+ label={t('customers.companies.detail.error.notFound', 'Company not found.')}
388
+ backHref="/backend/customers/companies"
389
+ backLabel={t('customers.companies.detail.actions.backToList', 'Back to companies')}
390
+ />
391
+ </PageBody>
392
+ </Page>
393
+ )
394
+ }
395
+
376
396
  if (error || !data?.company?.id) {
377
397
  return (
378
398
  <Page>
379
399
  <PageBody>
380
400
  <ErrorMessage
381
- label={error || t('customers.companies.detail.error.notFound', 'Company not found.')}
401
+ label={error ?? t('customers.companies.detail.error.load', 'Failed to load company.')}
382
402
  action={(
383
403
  <Button asChild variant="outline">
384
404
  <Link href="/backend/customers/companies">
@@ -16,7 +16,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
16
16
  import { useT } from '@open-mercato/shared/lib/i18n/context'
17
17
  import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
18
18
  import { Button } from '@open-mercato/ui/primitives/button'
19
- import { AttachmentsSection, ErrorMessage, LoadingMessage, type SectionAction } from '@open-mercato/ui/backend/detail'
19
+ import { AttachmentsSection, ErrorMessage, LoadingMessage, RecordNotFoundState, type SectionAction } from '@open-mercato/ui/backend/detail'
20
20
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
21
21
  import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
22
22
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
@@ -63,6 +63,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
63
63
  const [data, setData] = React.useState<PersonOverview | null>(null)
64
64
  const [isLoading, setIsLoading] = React.useState(true)
65
65
  const [error, setError] = React.useState<string | null>(null)
66
+ const [isNotFound, setIsNotFound] = React.useState(false)
66
67
 
67
68
  // Form state lifted for header Save button
68
69
  const [isDirty, setIsDirty] = React.useState(false)
@@ -124,7 +125,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
124
125
  const initialLoadDoneRef = React.useRef(false)
125
126
  const loadData = React.useCallback(async () => {
126
127
  if (!id) {
127
- setError(t('customers.people.detail.error.notFound', 'Person not found.'))
128
+ setIsNotFound(true)
128
129
  setIsLoading(false)
129
130
  return
130
131
  }
@@ -132,6 +133,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
132
133
  setIsLoading(true)
133
134
  }
134
135
  setError(null)
136
+ setIsNotFound(false)
135
137
  try {
136
138
  const payload = await readApiResultOrThrow<PersonOverview>(
137
139
  `/api/customers/people/${encodeURIComponent(id)}`,
@@ -140,8 +142,12 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
140
142
  )
141
143
  setData(payload as PersonOverview)
142
144
  } catch (err) {
143
- const message = err instanceof Error ? err.message : t('customers.people.detail.error.load', 'Failed to load person.')
144
- setError(message)
145
+ if ((err as { status?: number }).status === 404) {
146
+ setIsNotFound(true)
147
+ } else {
148
+ const message = err instanceof Error ? err.message : t('customers.people.detail.error.load', 'Failed to load person.')
149
+ setError(message)
150
+ }
145
151
  if (!initialLoadDoneRef.current) setData(null)
146
152
  } finally {
147
153
  setIsLoading(false)
@@ -375,12 +381,26 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
375
381
  )
376
382
  }
377
383
 
384
+ if (isNotFound) {
385
+ return (
386
+ <Page>
387
+ <PageBody>
388
+ <RecordNotFoundState
389
+ label={t('customers.people.detail.error.notFound', 'Person not found.')}
390
+ backHref="/backend/customers/people"
391
+ backLabel={t('customers.people.detail.actions.backToList', 'Back to people')}
392
+ />
393
+ </PageBody>
394
+ </Page>
395
+ )
396
+ }
397
+
378
398
  if (error || !data?.person?.id || !initialValues) {
379
399
  return (
380
400
  <Page>
381
401
  <PageBody>
382
402
  <ErrorMessage
383
- label={error || t('customers.people.detail.error.notFound', 'Person not found.')}
403
+ label={error ?? t('customers.people.detail.error.load', 'Failed to load person.')}
384
404
  action={(
385
405
  <Button asChild variant="outline">
386
406
  <Link href="/backend/customers/people">
@@ -201,6 +201,8 @@ type CompanySnapshot = {
201
201
  status: string | null
202
202
  lifecycleStage: string | null
203
203
  source: string | null
204
+ temperature: string | null
205
+ renewalQuarter: string | null
204
206
  nextInteractionAt: Date | null
205
207
  nextInteractionName: string | null
206
208
  nextInteractionRefId: string | null
@@ -297,6 +299,8 @@ async function loadCompanySnapshot(em: EntityManager, id: string): Promise<Compa
297
299
  status: entity.status ?? null,
298
300
  lifecycleStage: entity.lifecycleStage ?? null,
299
301
  source: entity.source ?? null,
302
+ temperature: entity.temperature ?? null,
303
+ renewalQuarter: entity.renewalQuarter ?? null,
300
304
  nextInteractionAt: entity.nextInteractionAt ?? null,
301
305
  nextInteractionName: entity.nextInteractionName ?? null,
302
306
  nextInteractionRefId: entity.nextInteractionRefId ?? null,
@@ -499,6 +503,8 @@ const createCompanyCommand: CommandHandler<CompanyCreateInput, { entityId: strin
499
503
  nextInteractionIcon,
500
504
  nextInteractionColor,
501
505
  isActive: parsed.isActive ?? true,
506
+ temperature: parsed.temperature ?? null,
507
+ renewalQuarter: parsed.renewalQuarter ?? null,
502
508
  })
503
509
 
504
510
  const profile = em.create(CustomerCompanyProfile, {
@@ -613,41 +619,44 @@ const updateCompanyCommand: CommandHandler<CompanyUpdateInput, { entityId: strin
613
619
  const profile = await em.findOne(CustomerCompanyProfile, { entity: record })
614
620
  if (!profile) throw new CrudHttpError(404, { error: 'Company profile not found' })
615
621
 
616
- if (parsed.displayName !== undefined) record.displayName = parsed.displayName
617
- if (parsed.description !== undefined) record.description = parsed.description ?? null
618
- if (parsed.ownerUserId !== undefined) record.ownerUserId = parsed.ownerUserId ?? null
619
- if (parsed.primaryEmail !== undefined) record.primaryEmail = parsed.primaryEmail ?? null
620
- if (parsed.primaryPhone !== undefined) record.primaryPhone = normalizeOptionalString(parsed.primaryPhone)
621
- if (parsed.status !== undefined) record.status = parsed.status ?? null
622
- if (parsed.lifecycleStage !== undefined) record.lifecycleStage = parsed.lifecycleStage ?? null
623
- if (parsed.source !== undefined) record.source = parsed.source ?? null
624
- if (parsed.isActive !== undefined) record.isActive = parsed.isActive
625
-
626
- if (parsed.nextInteraction) {
627
- record.nextInteractionAt = parsed.nextInteraction.at
628
- record.nextInteractionName = parsed.nextInteraction.name.trim()
629
- record.nextInteractionRefId = normalizeOptionalString(parsed.nextInteraction.refId) ?? null
630
- record.nextInteractionIcon = normalizeOptionalString(parsed.nextInteraction.icon)
631
- record.nextInteractionColor = normalizeHexColor(parsed.nextInteraction.color)
632
- } else if (parsed.nextInteraction === null) {
633
- record.nextInteractionAt = null
634
- record.nextInteractionName = null
635
- record.nextInteractionRefId = null
636
- record.nextInteractionIcon = null
637
- record.nextInteractionColor = null
638
- }
639
-
640
- if (parsed.legalName !== undefined) profile.legalName = parsed.legalName ?? null
641
- if (parsed.brandName !== undefined) profile.brandName = parsed.brandName ?? null
642
- if (parsed.domain !== undefined) profile.domain = parsed.domain ?? null
643
- if (parsed.websiteUrl !== undefined) profile.websiteUrl = parsed.websiteUrl ?? null
644
- if (parsed.industry !== undefined) profile.industry = parsed.industry ?? null
645
- if (parsed.sizeBucket !== undefined) profile.sizeBucket = parsed.sizeBucket ?? null
646
- if (parsed.annualRevenue !== undefined) {
647
- profile.annualRevenue = parsed.annualRevenue !== null && parsed.annualRevenue !== undefined ? String(parsed.annualRevenue) : null
648
- }
649
-
650
622
  await withAtomicFlush(em, [
623
+ () => {
624
+ if (parsed.displayName !== undefined) record.displayName = parsed.displayName
625
+ if (parsed.description !== undefined) record.description = parsed.description ?? null
626
+ if (parsed.ownerUserId !== undefined) record.ownerUserId = parsed.ownerUserId ?? null
627
+ if (parsed.primaryEmail !== undefined) record.primaryEmail = parsed.primaryEmail ?? null
628
+ if (parsed.primaryPhone !== undefined) record.primaryPhone = normalizeOptionalString(parsed.primaryPhone)
629
+ if (parsed.status !== undefined) record.status = parsed.status ?? null
630
+ if (parsed.lifecycleStage !== undefined) record.lifecycleStage = parsed.lifecycleStage ?? null
631
+ if (parsed.source !== undefined) record.source = parsed.source ?? null
632
+ if (parsed.isActive !== undefined) record.isActive = parsed.isActive
633
+ if (parsed.temperature !== undefined) record.temperature = parsed.temperature ?? null
634
+ if (parsed.renewalQuarter !== undefined) record.renewalQuarter = parsed.renewalQuarter ?? null
635
+
636
+ if (parsed.nextInteraction) {
637
+ record.nextInteractionAt = parsed.nextInteraction.at
638
+ record.nextInteractionName = parsed.nextInteraction.name.trim()
639
+ record.nextInteractionRefId = normalizeOptionalString(parsed.nextInteraction.refId) ?? null
640
+ record.nextInteractionIcon = normalizeOptionalString(parsed.nextInteraction.icon)
641
+ record.nextInteractionColor = normalizeHexColor(parsed.nextInteraction.color)
642
+ } else if (parsed.nextInteraction === null) {
643
+ record.nextInteractionAt = null
644
+ record.nextInteractionName = null
645
+ record.nextInteractionRefId = null
646
+ record.nextInteractionIcon = null
647
+ record.nextInteractionColor = null
648
+ }
649
+
650
+ if (parsed.legalName !== undefined) profile.legalName = parsed.legalName ?? null
651
+ if (parsed.brandName !== undefined) profile.brandName = parsed.brandName ?? null
652
+ if (parsed.domain !== undefined) profile.domain = parsed.domain ?? null
653
+ if (parsed.websiteUrl !== undefined) profile.websiteUrl = parsed.websiteUrl ?? null
654
+ if (parsed.industry !== undefined) profile.industry = parsed.industry ?? null
655
+ if (parsed.sizeBucket !== undefined) profile.sizeBucket = parsed.sizeBucket ?? null
656
+ if (parsed.annualRevenue !== undefined) {
657
+ profile.annualRevenue = parsed.annualRevenue !== null && parsed.annualRevenue !== undefined ? String(parsed.annualRevenue) : null
658
+ }
659
+ },
651
660
  () => syncEntityTags(em, record, parsed.tags),
652
661
  ], { transaction: true })
653
662
 
@@ -718,6 +727,8 @@ const updateCompanyCommand: CommandHandler<CompanyUpdateInput, { entityId: strin
718
727
  status: before.entity.status,
719
728
  lifecycleStage: before.entity.lifecycleStage,
720
729
  source: before.entity.source,
730
+ temperature: before.entity.temperature,
731
+ renewalQuarter: before.entity.renewalQuarter,
721
732
  nextInteractionAt: before.entity.nextInteractionAt,
722
733
  nextInteractionName: before.entity.nextInteractionName,
723
734
  nextInteractionRefId: before.entity.nextInteractionRefId,
@@ -735,6 +746,8 @@ const updateCompanyCommand: CommandHandler<CompanyUpdateInput, { entityId: strin
735
746
  entity.status = before.entity.status
736
747
  entity.lifecycleStage = before.entity.lifecycleStage
737
748
  entity.source = before.entity.source
749
+ entity.temperature = before.entity.temperature
750
+ entity.renewalQuarter = before.entity.renewalQuarter
738
751
  entity.nextInteractionAt = before.entity.nextInteractionAt
739
752
  entity.nextInteractionName = before.entity.nextInteractionName
740
753
  entity.nextInteractionRefId = before.entity.nextInteractionRefId
@@ -1045,6 +1058,8 @@ const deleteCompanyCommand: CommandHandler<{ body?: Record<string, unknown>; que
1045
1058
  status: before.entity.status,
1046
1059
  lifecycleStage: before.entity.lifecycleStage,
1047
1060
  source: before.entity.source,
1061
+ temperature: before.entity.temperature,
1062
+ renewalQuarter: before.entity.renewalQuarter,
1048
1063
  nextInteractionAt: before.entity.nextInteractionAt,
1049
1064
  nextInteractionName: before.entity.nextInteractionName,
1050
1065
  nextInteractionRefId: before.entity.nextInteractionRefId,
@@ -1063,6 +1078,8 @@ const deleteCompanyCommand: CommandHandler<{ body?: Record<string, unknown>; que
1063
1078
  entity.status = before.entity.status
1064
1079
  entity.lifecycleStage = before.entity.lifecycleStage
1065
1080
  entity.source = before.entity.source
1081
+ entity.temperature = before.entity.temperature
1082
+ entity.renewalQuarter = before.entity.renewalQuarter
1066
1083
  entity.nextInteractionAt = before.entity.nextInteractionAt
1067
1084
  entity.nextInteractionName = before.entity.nextInteractionName
1068
1085
  entity.nextInteractionRefId = before.entity.nextInteractionRefId
@@ -12,8 +12,7 @@ import { Spinner } from '@open-mercato/ui/primitives/spinner'
12
12
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
13
13
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
- import { LoadingMessage } from '@open-mercato/ui/backend/detail'
16
- import { ErrorMessage } from '@open-mercato/ui/backend/detail'
15
+ import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
17
16
  import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
18
17
  import { RotateCcw, XCircle } from 'lucide-react'
19
18
 
@@ -106,6 +105,7 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
106
105
  const [run, setRun] = React.useState<SyncRunDetail | null>(null)
107
106
  const [isLoading, setIsLoading] = React.useState(true)
108
107
  const [error, setError] = React.useState<string | null>(null)
108
+ const [isNotFound, setIsNotFound] = React.useState(false)
109
109
  const [logs, setLogs] = React.useState<LogEntry[]>([])
110
110
  const [isLoadingLogs, setIsLoadingLogs] = React.useState(false)
111
111
 
@@ -124,13 +124,18 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
124
124
  setIsLoading(false)
125
125
  return
126
126
  }
127
+ setIsNotFound(false)
127
128
  const call = await apiCall<SyncRunDetail>(
128
129
  `/api/data_sync/runs/${encodeURIComponent(currentRunId)}`,
129
130
  undefined,
130
131
  { fallback: null },
131
132
  )
132
133
  if (!call.ok || !call.result) {
133
- setError(t('data_sync.runs.detail.loadError'))
134
+ if (call.status === 404) {
135
+ setIsNotFound(true)
136
+ } else {
137
+ setError(t('data_sync.runs.detail.loadError'))
138
+ }
134
139
  setIsLoading(false)
135
140
  return
136
141
  }
@@ -243,6 +248,19 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
243
248
  }, [resolveCurrentRunId, router, t])
244
249
 
245
250
  if (isLoading) return <Page><PageBody><LoadingMessage label={t('data_sync.runs.detail.title')} /></PageBody></Page>
251
+ if (isNotFound) {
252
+ return (
253
+ <Page>
254
+ <PageBody>
255
+ <RecordNotFoundState
256
+ label={t('data_sync.runs.detail.notFound', 'Sync run not found.')}
257
+ backHref="/backend/data-sync"
258
+ backLabel={t('data_sync.runs.detail.back')}
259
+ />
260
+ </PageBody>
261
+ </Page>
262
+ )
263
+ }
246
264
  if (error || !run) return <Page><PageBody><ErrorMessage label={error ?? t('data_sync.runs.detail.loadError')} /></PageBody></Page>
247
265
 
248
266
  const totalProcessed = run.createdCount + run.updatedCount + run.skippedCount + run.failedCount
@@ -63,7 +63,10 @@ export async function GET(req: Request) {
63
63
 
64
64
  const byId = new Map<string, any>()
65
65
  for (const g of generated) byId.set(g.entityId, g)
66
- for (const cu of custom) byId.set(cu.entityId, { ...byId.get(cu.entityId), ...cu })
66
+ for (const cu of custom) {
67
+ const existing = byId.get(cu.entityId)
68
+ byId.set(cu.entityId, { ...existing, ...cu, source: existing?.source ?? cu.source })
69
+ }
67
70
 
68
71
  // Count field definitions scoped to current tenant/org (same scoping as custom entities)
69
72
  const defsWhere: any = { isActive: true }
@@ -473,20 +473,13 @@ export default function EditDefinitionsPage({ params }: { params?: { entityId?:
473
473
  flash('Please fix validation errors in field definitions', 'error')
474
474
  throw createCrudFormError('Please fix validation errors in field definitions')
475
475
  }
476
- if (entitySource === 'custom') {
477
- const partial = upsertCustomEntitySchema
478
- .pick({ label: true, description: true, labelField: true as any, defaultEditor: true as any })
479
- .extend({ showInSidebar: z.boolean().optional() }) as unknown as z.ZodTypeAny
480
- const normalized = {
481
- ...(vals as any),
482
- defaultEditor: (vals as any)?.defaultEditor || undefined,
483
- }
484
- const parsed = partial.safeParse(normalized)
485
- if (!parsed.success) throw createCrudFormError('Validation failed')
476
+ {
477
+ const entityPayload = buildEntityMetadataPayload(entitySource, vals)
478
+ if (!entityPayload) throw createCrudFormError('Validation failed')
486
479
  const callEntity = await apiCall('/api/entities/entities', {
487
480
  method: 'POST',
488
481
  headers: { 'content-type': 'application/json' },
489
- body: JSON.stringify({ entityId, ...(parsed.data as any) }),
482
+ body: JSON.stringify({ entityId, ...entityPayload }),
490
483
  })
491
484
  if (!callEntity.ok) {
492
485
  await raiseCrudError(callEntity.response, 'Failed to save entity')
@@ -603,3 +596,21 @@ export default function EditDefinitionsPage({ params }: { params?: { entityId?:
603
596
  </Page>
604
597
  )
605
598
  }
599
+
600
+ export function buildEntityMetadataPayload(
601
+ entitySource: 'code' | 'custom',
602
+ vals: Record<string, unknown>,
603
+ ): Record<string, unknown> | null {
604
+ const partial = entitySource === 'custom'
605
+ ? upsertCustomEntitySchema
606
+ .pick({ label: true, description: true, labelField: true as any, defaultEditor: true as any })
607
+ .extend({ showInSidebar: z.boolean().optional() }) as unknown as z.ZodTypeAny
608
+ : upsertCustomEntitySchema
609
+ .pick({ label: true, description: true, defaultEditor: true as any }) as unknown as z.ZodTypeAny
610
+ const normalized = {
611
+ ...vals,
612
+ defaultEditor: typeof vals.defaultEditor === 'string' && vals.defaultEditor ? vals.defaultEditor : undefined,
613
+ }
614
+ const parsed = partial.safeParse(normalized)
615
+ return parsed.success ? (parsed.data as Record<string, unknown>) : null
616
+ }