@open-mercato/core 0.6.1-develop.3227.1.2b7b8ab258 → 0.6.1-develop.3256.1.fe3dec2464
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/catalog/ai-agents.js +4 -0
- package/dist/modules/catalog/ai-agents.js.map +2 -2
- package/dist/modules/customers/ai-agents.js.map +2 -2
- package/dist/modules/customers/api/companies/route.js +11 -1
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +11 -1
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +78 -35
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +74 -30
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +78 -35
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/commands/people.js +20 -0
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +40 -17
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/catalog/ai-agents.ts +8 -49
- package/src/modules/customers/ai-agents.ts +4 -51
- package/src/modules/customers/api/companies/route.ts +10 -0
- package/src/modules/customers/api/people/route.ts +10 -0
- package/src/modules/customers/backend/customers/companies/page.tsx +95 -35
- package/src/modules/customers/backend/customers/deals/page.tsx +91 -30
- package/src/modules/customers/backend/customers/people/page.tsx +96 -35
- package/src/modules/customers/commands/people.ts +30 -0
- package/src/modules/customers/i18n/de.json +8 -0
- package/src/modules/customers/i18n/en.json +8 -0
- package/src/modules/customers/i18n/es.json +8 -0
- package/src/modules/customers/i18n/pl.json +8 -0
- package/src/modules/entities/components/EncryptionManager.tsx +50 -21
- package/src/modules/inbox_ops/lib/llmProvider.ts +10 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/components/EncryptionManager.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery, useMutation } from '@tanstack/react-query'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Plus, Save, Trash2 } from 'lucide-react'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getEntityFields } from '#generated/entity-fields-registry'\n\ntype EntityOption = { entityId: string; label?: string; source?: string }\n\ntype EncryptionFieldRow = {\n id: string\n field: string\n hashField?: string | null\n}\n\ntype EncryptionMapResponse = {\n entityId: string\n fields?: Array<{ field: string; hashField?: string | null }>\n isActive?: boolean\n}\n\ntype CanonicalOption = { value: string; label?: string }\n\nfunction normalizeToken(value: string): string {\n return value\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[^a-z0-9]/gi, '')\n .toLowerCase()\n}\n\nfunction canonicalizeFieldName(raw: string, options: CanonicalOption[]): string {\n const trimmed = typeof raw === 'string' ? raw.trim() : ''\n if (!trimmed) return ''\n const normalized = normalizeToken(trimmed)\n for (const option of options) {\n const optionValue = typeof option.value === 'string' ? option.value.trim() : ''\n if (optionValue && normalizeToken(optionValue) === normalized) return optionValue\n if (option.label && normalizeToken(option.label) === normalized) return optionValue\n }\n return trimmed\n}\n\nfunction normalizeFieldRows(raw: EncryptionMapResponse | undefined, options: CanonicalOption[]): EncryptionFieldRow[] {\n const rows = Array.isArray(raw?.fields) ? raw!.fields : []\n return rows.map((entry, idx) => ({\n id: `${entry.field}-${idx}`,\n field: canonicalizeFieldName(entry.field, options),\n hashField: entry.hashField ? canonicalizeFieldName(entry.hashField, options) : null,\n }))\n}\n\nfunction buildRowId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()\n return `row-${Math.random().toString(36).slice(2)}`\n}\n\nexport function EncryptionManager() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const [selectedEntityId, setSelectedEntityId] = React.useState('')\n const [fields, setFields] = React.useState<EncryptionFieldRow[]>([])\n const [isActive, setIsActive] = React.useState(true)\n const [baseFieldOptions, setBaseFieldOptions] = React.useState<Array<{ value: string; label: string }>>([])\n const [hasUserEdited, setHasUserEdited] = React.useState(false)\n const { data: fieldDefs = [], isLoading: loadingFieldDefs } = useCustomFieldDefs(selectedEntityId ? [selectedEntityId] : [], {\n enabled: !!selectedEntityId,\n })\n const canonicalOptions = React.useMemo<CanonicalOption[]>(() => {\n const options: CanonicalOption[] = []\n for (const option of baseFieldOptions) {\n if (!option.value) continue\n if (options.some((opt) => opt.value === option.value)) continue\n options.push(option)\n }\n for (const def of fieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key || options.some((opt) => opt.value === key)) continue\n const label = typeof def.label === 'string' ? def.label : undefined\n options.push({ value: key, label })\n }\n return options\n }, [baseFieldOptions, fieldDefs])\n const fieldOptions = React.useMemo(() => {\n const entries = new Map<string, string>()\n for (const option of baseFieldOptions) {\n if (!option.value || entries.has(option.value)) continue\n entries.set(option.value, option.label || option.value)\n }\n for (const def of fieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key || entries.has(key)) continue\n const label = typeof def.label === 'string' && def.label.trim().length ? def.label : key\n entries.set(key, label)\n }\n for (const row of fields) {\n const main = row.field?.trim()\n if (main && !entries.has(main)) entries.set(main, main)\n const hash = row.hashField?.trim()\n if (hash && !entries.has(hash)) entries.set(hash, hash)\n }\n return Array.from(entries.entries()).map(([value, label]) => ({ value, label }))\n }, [baseFieldOptions, fieldDefs, fields])\n\n const { data: entities, isLoading: loadingEntities, error: entitiesError } = useQuery<{ items: EntityOption[] }>({\n queryKey: ['entities-list', scopeVersion],\n queryFn: async () =>\n readApiResultOrThrow('/api/entities/entities', undefined, {\n errorMessage: t('entities.encryption.errors.loadEntities', 'Failed to load entities'),\n }),\n })\n\n React.useEffect(() => {\n if (!selectedEntityId && entities?.items?.length) {\n const first = entities.items[0]\n setSelectedEntityId(first.entityId)\n }\n }, [entities, selectedEntityId])\n\n React.useEffect(() => {\n if (!selectedEntityId) {\n setBaseFieldOptions([])\n return\n }\n const parts = selectedEntityId.split(':')\n const entitySlug = parts[1]\n if (!entitySlug) {\n setBaseFieldOptions([])\n return\n }\n\n // Use static registry instead of dynamic import for Turbopack compatibility\n const mod = getEntityFields(entitySlug)\n if (!mod) {\n console.warn('[encryption] No fields found for entity', entitySlug)\n setBaseFieldOptions([])\n return\n }\n\n const options: Array<{ value: string; label: string }> = []\n for (const raw of Object.values(mod)) {\n if (typeof raw !== 'string' || !raw.trim()) continue\n const value = raw.trim()\n if (options.some((opt) => opt.value === value)) continue\n const label = value\n .split('_')\n .map((segment) => (segment ? `${segment[0].toUpperCase()}${segment.slice(1)}` : ''))\n .join(' ')\n .trim() || value\n options.push({ value, label })\n }\n setBaseFieldOptions(options)\n }, [selectedEntityId])\n\n const {\n data: map,\n isLoading: loadingMap,\n isError: mapError,\n refetch: refetchMap,\n } = useQuery<EncryptionMapResponse>({\n queryKey: ['encryption-map', selectedEntityId, scopeVersion],\n enabled: !!selectedEntityId,\n queryFn: async () =>\n readApiResultOrThrow(`/api/entities/encryption?entityId=${encodeURIComponent(selectedEntityId)}`, undefined, {\n errorMessage: t('entities.encryption.errors.loadMap', 'Failed to load encryption map'),\n }),\n })\n\n const mapSignature = React.useMemo(() => JSON.stringify(map ?? null), [map])\n const lastMapSignatureRef = React.useRef<string | null>(null)\n\n React.useEffect(() => {\n const signature = mapSignature\n const isSameMap = signature === lastMapSignatureRef.current\n if (isSameMap && hasUserEdited) return\n if (!map) {\n setFields([])\n setIsActive(true)\n setHasUserEdited(false)\n lastMapSignatureRef.current = signature\n return\n }\n setFields(normalizeFieldRows(map, canonicalOptions))\n setIsActive(map?.isActive !== false)\n setHasUserEdited(false)\n lastMapSignatureRef.current = signature\n }, [mapSignature, map, canonicalOptions, hasUserEdited])\n\n const mutation = useMutation({\n mutationFn: async () => {\n const trimmed = fields\n .map((row) => ({\n field: canonicalizeFieldName(row.field, canonicalOptions).trim(),\n hashField: row.hashField ? canonicalizeFieldName(row.hashField, canonicalOptions).trim() : '',\n }))\n .filter((row) => row.field.length > 0)\n .map((row) => ({\n field: row.field,\n hashField: row.hashField ? row.hashField : null,\n }))\n if (!selectedEntityId) {\n throw new Error(t('entities.encryption.errors.selectEntity', 'Select an entity before saving'))\n }\n if (!trimmed.length) {\n throw new Error(t('entities.encryption.errors.noFields', 'Add at least one field to encrypt'))\n }\n const res = await apiCall('/api/entities/encryption', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ entityId: selectedEntityId, fields: trimmed, isActive }),\n })\n if (!res.ok) {\n await raiseCrudError(res.response, t('entities.encryption.errors.save', 'Failed to save encryption map'))\n }\n return true\n },\n onSuccess: () => {\n flash(t('entities.encryption.flash.saved', 'Encryption map saved'), 'success')\n void refetchMap()\n },\n onError: (err: any) => {\n const message = err?.message || t('entities.encryption.errors.save', 'Failed to save encryption map')\n flash(message, 'error')\n },\n })\n\n const addField = () => {\n setHasUserEdited(true)\n setFields((prev) => [...prev, { id: buildRowId(), field: '', hashField: null }])\n }\n\n const updateField = (id: string, patch: Partial<EncryptionFieldRow>) => {\n setHasUserEdited(true)\n setFields((prev) => prev.map((row) => (row.id === id ? { ...row, ...patch } : row)))\n }\n\n const removeField = (id: string) => {\n setHasUserEdited(true)\n setFields((prev) => prev.filter((row) => row.id !== id))\n }\n\n const renderFields = () => {\n if (loadingMap || loadingFieldDefs) {\n return (\n <LoadingMessage\n label={t('entities.encryption.loading', 'Loading encryption map\u2026')}\n className=\"border-0 bg-transparent p-4\"\n />\n )\n }\n if (mapError) {\n return (\n <ErrorMessage\n label={t('entities.encryption.errors.loadMap', 'Failed to load encryption map')}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => void refetchMap()}>\n {t('entities.encryption.actions.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n if (!fields.length) {\n return (\n <div className=\"rounded border bg-background/80 p-4 text-sm text-muted-foreground\">\n {t('entities.encryption.empty', 'No fields are encrypted yet. Add the first one below.')}\n </div>\n )\n }\n const withFallbackOption = (value?: string | null) => {\n if (value && !fieldOptions.some((opt) => opt.value === value)) {\n return [...fieldOptions, { value, label: value }]\n }\n return fieldOptions\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[720px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('entities.encryption.fields.field', 'Field name')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('entities.encryption.fields.hash', 'Hash field (optional)')}\n </th>\n <th className=\"px-3 py-2 text-right\">\n {t('entities.encryption.fields.actions', 'Actions')}\n </th>\n </tr>\n </thead>\n <tbody>\n {fields.map((row) => {\n const fieldOpts = withFallbackOption(row.field)\n const hashOpts = withFallbackOption(row.hashField)\n return (\n <tr key={row.id} className=\"border-t\">\n <td className=\"px-3 py-2 align-top\">\n <Select\n value={row.field || undefined}\n onValueChange={(value) => updateField(row.id, { field: value ?? '' })}\n >\n <SelectTrigger>\n <SelectValue placeholder={t('entities.encryption.fields.selectField', 'Select field')} />\n </SelectTrigger>\n <SelectContent>\n {fieldOpts.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <Select\n value={row.hashField || undefined}\n onValueChange={(value) => updateField(row.id, { hashField: value || null })}\n >\n <SelectTrigger>\n <SelectValue placeholder={t('entities.encryption.fields.selectHash', 'Select hash field (optional)')} />\n </SelectTrigger>\n <SelectContent>\n {hashOpts.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"mt-1 text-overline text-muted-foreground\">\n {t('entities.encryption.fields.hashHint', 'Use when lookups must stay deterministic (e.g., login by email).')}\n </p>\n </td>\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-9 w-9 px-0\"\n aria-label={t('entities.encryption.actions.remove', 'Remove')}\n onClick={() => removeField(row.id)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('entities.encryption.title', 'Encryption')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('entities.encryption.description', 'Manage which entity fields are encrypted with tenant keys and optional hash columns.')}\n </p>\n </div>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end\">\n <div className=\"flex-1 space-y-3\">\n <div>\n <label className=\"text-xs text-muted-foreground\">\n {t('entities.encryption.selectEntity', 'Choose entity')}\n </label>\n <Select\n value={selectedEntityId || undefined}\n onValueChange={(value) => setSelectedEntityId(value ?? '')}\n disabled={loadingEntities || !!entitiesError}\n >\n <SelectTrigger className=\"mt-1\">\n <SelectValue placeholder={t('entities.encryption.placeholder', 'Select an entity')} />\n </SelectTrigger>\n <SelectContent>\n {(entities?.items || []).map((item) => (\n <SelectItem key={item.entityId} value={item.entityId}>\n {item.label || item.entityId} {item.source === 'custom' ? `(${t('entities.encryption.source.custom', 'custom')})` : ''}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {entitiesError ? (\n <p className=\"mt-1 text-xs text-red-600\">\n {t('entities.encryption.errors.loadEntities', 'Failed to load entities')}\n </p>\n ) : (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('entities.encryption.entityHint', 'Maps apply per tenant/organization. Use field names from your entities.')}\n </p>\n )}\n </div>\n <label className=\"inline-flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={isActive}\n onChange={(event) => setIsActive(event.target.checked)}\n />\n {t('entities.encryption.active', 'Encryption enabled for this entity')}\n </label>\n </div>\n </div>\n <div className=\"rounded-lg border bg-background/80 p-4\">\n <div className=\"mb-3 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-medium\">{t('entities.encryption.fields.title', 'Encrypted fields')}</h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('entities.encryption.fields.subtitle', 'List the attributes that should be encrypted with the tenant key.')}\n </p>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={addField}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('entities.encryption.actions.add', 'Add field')}\n </Button>\n </div>\n {renderFields()}\n </div>\n <div className=\"flex justify-end\">\n <Button\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || loadingEntities || !!entitiesError || !selectedEntityId}\n >\n <Save className=\"mr-2 h-4 w-4\" />\n {mutation.isPending\n ? t('entities.encryption.actions.saving', 'Saving\u2026')\n : t('entities.encryption.actions.save', 'Save encryption map')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery, useMutation } from '@tanstack/react-query'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useCustomFieldDefs, type CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Plus, Save, Trash2 } from 'lucide-react'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getEntityFields } from '#generated/entity-fields-registry'\n\ntype EntityOption = { entityId: string; label?: string; source?: string }\ntype FieldOption = { value: string; label: string }\n\ntype EncryptionFieldRow = {\n id: string\n field: string\n hashField?: string | null\n}\n\ntype EncryptionMapResponse = {\n entityId: string\n fields?: Array<{ field: string; hashField?: string | null }>\n isActive?: boolean\n}\n\ntype CanonicalOption = { value: string; label?: string }\n\nconst EMPTY_FIELD_DEFS: CustomFieldDefDto[] = []\n\nfunction normalizeToken(value: string): string {\n return value\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[^a-z0-9]/gi, '')\n .toLowerCase()\n}\n\nfunction canonicalizeFieldName(raw: string, options: CanonicalOption[]): string {\n const trimmed = typeof raw === 'string' ? raw.trim() : ''\n if (!trimmed) return ''\n const normalized = normalizeToken(trimmed)\n for (const option of options) {\n const optionValue = typeof option.value === 'string' ? option.value.trim() : ''\n if (optionValue && normalizeToken(optionValue) === normalized) return optionValue\n if (option.label && normalizeToken(option.label) === normalized) return optionValue\n }\n return trimmed\n}\n\nfunction normalizeFieldRows(raw: EncryptionMapResponse | undefined, options: CanonicalOption[]): EncryptionFieldRow[] {\n const rows = Array.isArray(raw?.fields) ? raw!.fields : []\n return rows.map((entry, idx) => ({\n id: `${entry.field}-${idx}`,\n field: canonicalizeFieldName(entry.field, options),\n hashField: entry.hashField ? canonicalizeFieldName(entry.hashField, options) : null,\n }))\n}\n\nfunction areFieldRowsEqual(a: EncryptionFieldRow[], b: EncryptionFieldRow[]): boolean {\n if (a === b) return true\n if (a.length !== b.length) return false\n return a.every((row, idx) => {\n const other = b[idx]\n return !!other && row.id === other.id && row.field === other.field && (row.hashField ?? null) === (other.hashField ?? null)\n })\n}\n\nfunction areFieldOptionsEqual(a: FieldOption[], b: FieldOption[]): boolean {\n if (a === b) return true\n if (a.length !== b.length) return false\n return a.every((option, idx) => {\n const other = b[idx]\n return !!other && option.value === other.value && option.label === other.label\n })\n}\n\nfunction buildRowId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()\n return `row-${Math.random().toString(36).slice(2)}`\n}\n\nexport function EncryptionManager() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const [selectedEntityId, setSelectedEntityId] = React.useState('')\n const [fields, setFields] = React.useState<EncryptionFieldRow[]>([])\n const [isActive, setIsActive] = React.useState(true)\n const [baseFieldOptions, setBaseFieldOptions] = React.useState<FieldOption[]>([])\n const [hasUserEdited, setHasUserEdited] = React.useState(false)\n const { data: rawFieldDefs, isLoading: loadingFieldDefs } = useCustomFieldDefs(selectedEntityId ? [selectedEntityId] : [], {\n enabled: !!selectedEntityId,\n })\n const fieldDefs = rawFieldDefs ?? EMPTY_FIELD_DEFS\n const canonicalOptions = React.useMemo<CanonicalOption[]>(() => {\n const options: CanonicalOption[] = []\n for (const option of baseFieldOptions) {\n if (!option.value) continue\n if (options.some((opt) => opt.value === option.value)) continue\n options.push(option)\n }\n for (const def of fieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key || options.some((opt) => opt.value === key)) continue\n const label = typeof def.label === 'string' ? def.label : undefined\n options.push({ value: key, label })\n }\n return options\n }, [baseFieldOptions, fieldDefs])\n const fieldOptions = React.useMemo(() => {\n const entries = new Map<string, string>()\n for (const option of baseFieldOptions) {\n if (!option.value || entries.has(option.value)) continue\n entries.set(option.value, option.label || option.value)\n }\n for (const def of fieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key || entries.has(key)) continue\n const label = typeof def.label === 'string' && def.label.trim().length ? def.label : key\n entries.set(key, label)\n }\n for (const row of fields) {\n const main = row.field?.trim()\n if (main && !entries.has(main)) entries.set(main, main)\n const hash = row.hashField?.trim()\n if (hash && !entries.has(hash)) entries.set(hash, hash)\n }\n return Array.from(entries.entries()).map(([value, label]) => ({ value, label }))\n }, [baseFieldOptions, fieldDefs, fields])\n\n const { data: entities, isLoading: loadingEntities, error: entitiesError } = useQuery<{ items: EntityOption[] }>({\n queryKey: ['entities-list', scopeVersion],\n queryFn: async () =>\n readApiResultOrThrow('/api/entities/entities', undefined, {\n errorMessage: t('entities.encryption.errors.loadEntities', 'Failed to load entities'),\n }),\n })\n\n React.useEffect(() => {\n if (!selectedEntityId && entities?.items?.length) {\n const first = entities.items[0]\n setSelectedEntityId(first.entityId)\n }\n }, [entities, selectedEntityId])\n\n React.useEffect(() => {\n if (!selectedEntityId) {\n setBaseFieldOptions((prev) => (prev.length ? [] : prev))\n return\n }\n const parts = selectedEntityId.split(':')\n const entitySlug = parts[1]\n if (!entitySlug) {\n setBaseFieldOptions((prev) => (prev.length ? [] : prev))\n return\n }\n\n // Use static registry instead of dynamic import for Turbopack compatibility\n const mod = getEntityFields(entitySlug)\n if (!mod) {\n console.warn('[encryption] No fields found for entity', entitySlug)\n setBaseFieldOptions((prev) => (prev.length ? [] : prev))\n return\n }\n\n const options: FieldOption[] = []\n for (const raw of Object.values(mod)) {\n if (typeof raw !== 'string' || !raw.trim()) continue\n const value = raw.trim()\n if (options.some((opt) => opt.value === value)) continue\n const label = value\n .split('_')\n .map((segment) => (segment ? `${segment[0].toUpperCase()}${segment.slice(1)}` : ''))\n .join(' ')\n .trim() || value\n options.push({ value, label })\n }\n setBaseFieldOptions((prev) => (areFieldOptionsEqual(prev, options) ? prev : options))\n }, [selectedEntityId])\n\n const {\n data: map,\n isLoading: loadingMap,\n isError: mapError,\n refetch: refetchMap,\n } = useQuery<EncryptionMapResponse>({\n queryKey: ['encryption-map', selectedEntityId, scopeVersion],\n enabled: !!selectedEntityId,\n queryFn: async () =>\n readApiResultOrThrow(`/api/entities/encryption?entityId=${encodeURIComponent(selectedEntityId)}`, undefined, {\n errorMessage: t('entities.encryption.errors.loadMap', 'Failed to load encryption map'),\n }),\n })\n\n const mapSignature = React.useMemo(() => JSON.stringify(map ?? null), [map])\n const lastMapSignatureRef = React.useRef<string | null>(null)\n\n React.useEffect(() => {\n const signature = mapSignature\n const isSameMap = signature === lastMapSignatureRef.current\n if (isSameMap && hasUserEdited) return\n if (!map) {\n setFields((prev) => (prev.length ? [] : prev))\n setIsActive((prev) => (prev === true ? prev : true))\n setHasUserEdited(false)\n lastMapSignatureRef.current = signature\n return\n }\n const nextFields = normalizeFieldRows(map, canonicalOptions)\n setFields((prev) => (areFieldRowsEqual(prev, nextFields) ? prev : nextFields))\n const nextIsActive = map?.isActive !== false\n setIsActive((prev) => (prev === nextIsActive ? prev : nextIsActive))\n setHasUserEdited(false)\n lastMapSignatureRef.current = signature\n }, [mapSignature, map, canonicalOptions, hasUserEdited])\n\n const mutation = useMutation({\n mutationFn: async () => {\n const trimmed = fields\n .map((row) => ({\n field: canonicalizeFieldName(row.field, canonicalOptions).trim(),\n hashField: row.hashField ? canonicalizeFieldName(row.hashField, canonicalOptions).trim() : '',\n }))\n .filter((row) => row.field.length > 0)\n .map((row) => ({\n field: row.field,\n hashField: row.hashField ? row.hashField : null,\n }))\n if (!selectedEntityId) {\n throw new Error(t('entities.encryption.errors.selectEntity', 'Select an entity before saving'))\n }\n if (!trimmed.length) {\n throw new Error(t('entities.encryption.errors.noFields', 'Add at least one field to encrypt'))\n }\n const res = await apiCall('/api/entities/encryption', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ entityId: selectedEntityId, fields: trimmed, isActive }),\n })\n if (!res.ok) {\n await raiseCrudError(res.response, t('entities.encryption.errors.save', 'Failed to save encryption map'))\n }\n return true\n },\n onSuccess: () => {\n flash(t('entities.encryption.flash.saved', 'Encryption map saved'), 'success')\n void refetchMap()\n },\n onError: (err: any) => {\n const message = err?.message || t('entities.encryption.errors.save', 'Failed to save encryption map')\n flash(message, 'error')\n },\n })\n\n const addField = () => {\n setHasUserEdited(true)\n setFields((prev) => [...prev, { id: buildRowId(), field: '', hashField: null }])\n }\n\n const updateField = (id: string, patch: Partial<EncryptionFieldRow>) => {\n setHasUserEdited(true)\n setFields((prev) => prev.map((row) => (row.id === id ? { ...row, ...patch } : row)))\n }\n\n const removeField = (id: string) => {\n setHasUserEdited(true)\n setFields((prev) => prev.filter((row) => row.id !== id))\n }\n\n const renderFields = () => {\n if (loadingMap || loadingFieldDefs) {\n return (\n <LoadingMessage\n label={t('entities.encryption.loading', 'Loading encryption map\u2026')}\n className=\"border-0 bg-transparent p-4\"\n />\n )\n }\n if (mapError) {\n return (\n <ErrorMessage\n label={t('entities.encryption.errors.loadMap', 'Failed to load encryption map')}\n action={(\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void refetchMap()}>\n {t('entities.encryption.actions.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n if (!fields.length) {\n return (\n <div className=\"rounded border bg-background/80 p-4 text-sm text-muted-foreground\">\n {t('entities.encryption.empty', 'No fields are encrypted yet. Add the first one below.')}\n </div>\n )\n }\n const withFallbackOption = (value?: string | null) => {\n if (value && !fieldOptions.some((opt) => opt.value === value)) {\n return [...fieldOptions, { value, label: value }]\n }\n return fieldOptions\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[720px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('entities.encryption.fields.field', 'Field name')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('entities.encryption.fields.hash', 'Hash field (optional)')}\n </th>\n <th className=\"px-3 py-2 text-right\">\n {t('entities.encryption.fields.actions', 'Actions')}\n </th>\n </tr>\n </thead>\n <tbody>\n {fields.map((row) => {\n const fieldOpts = withFallbackOption(row.field)\n const hashOpts = withFallbackOption(row.hashField)\n return (\n <tr key={row.id} className=\"border-t\">\n <td className=\"px-3 py-2 align-top\">\n <Select\n value={row.field || undefined}\n onValueChange={(value) => updateField(row.id, { field: value ?? '' })}\n >\n <SelectTrigger>\n <SelectValue placeholder={t('entities.encryption.fields.selectField', 'Select field')} />\n </SelectTrigger>\n <SelectContent>\n {fieldOpts.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <Select\n value={row.hashField || undefined}\n onValueChange={(value) => updateField(row.id, { hashField: value || null })}\n >\n <SelectTrigger>\n <SelectValue placeholder={t('entities.encryption.fields.selectHash', 'Select hash field (optional)')} />\n </SelectTrigger>\n <SelectContent>\n {hashOpts.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"mt-1 text-overline text-muted-foreground\">\n {t('entities.encryption.fields.hashHint', 'Use when lookups must stay deterministic (e.g., login by email).')}\n </p>\n </td>\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-9 w-9 px-0\"\n aria-label={t('entities.encryption.actions.remove', 'Remove')}\n onClick={() => removeField(row.id)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('entities.encryption.title', 'Encryption')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('entities.encryption.description', 'Manage which entity fields are encrypted with tenant keys and optional hash columns.')}\n </p>\n </div>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end\">\n <div className=\"flex-1 space-y-3\">\n <div>\n <label className=\"text-xs text-muted-foreground\">\n {t('entities.encryption.selectEntity', 'Choose entity')}\n </label>\n <Select\n value={selectedEntityId || undefined}\n onValueChange={(value) => setSelectedEntityId(value ?? '')}\n disabled={loadingEntities || !!entitiesError}\n >\n <SelectTrigger className=\"mt-1\">\n <SelectValue placeholder={t('entities.encryption.placeholder', 'Select an entity')} />\n </SelectTrigger>\n <SelectContent>\n {(entities?.items || []).map((item) => (\n <SelectItem key={item.entityId} value={item.entityId}>\n {item.label || item.entityId} {item.source === 'custom' ? `(${t('entities.encryption.source.custom', 'custom')})` : ''}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {entitiesError ? (\n <p className=\"mt-1 text-xs text-status-error-text\">\n {t('entities.encryption.errors.loadEntities', 'Failed to load entities')}\n </p>\n ) : (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('entities.encryption.entityHint', 'Maps apply per tenant/organization. Use field names from your entities.')}\n </p>\n )}\n </div>\n <div className=\"inline-flex items-center gap-2 text-sm\">\n <Checkbox\n id=\"entities-encryption-active\"\n checked={isActive}\n onCheckedChange={(checked) => setIsActive(checked === true)}\n />\n <label htmlFor=\"entities-encryption-active\">\n {t('entities.encryption.active', 'Encryption enabled for this entity')}\n </label>\n </div>\n </div>\n </div>\n <div className=\"rounded-lg border bg-background/80 p-4\">\n <div className=\"mb-3 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-medium\">{t('entities.encryption.fields.title', 'Encrypted fields')}</h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('entities.encryption.fields.subtitle', 'List the attributes that should be encrypted with the tenant key.')}\n </p>\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={addField}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('entities.encryption.actions.add', 'Add field')}\n </Button>\n </div>\n {renderFields()}\n </div>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || loadingEntities || !!entitiesError || !selectedEntityId}\n >\n <Save className=\"mr-2 h-4 w-4\" />\n {mutation.isPending\n ? t('entities.encryption.actions.saving', 'Saving\u2026')\n : t('entities.encryption.actions.save', 'Save encryption map')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AA2RQ,cAmCI,YAnCJ;AAzRR,YAAY,WAAW;AACvB,SAAS,UAAU,mBAAmB;AACtC,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,aAAa;AACtB,SAAS,SAAS,4BAA4B;AAC9C,SAAS,sBAAsB;AAC/B,SAAS,0BAAkD;AAC3D,SAAS,MAAM,MAAM,cAAc;AACnC,SAAS,mCAAmC;AAC5C,SAAS,YAAY;AACrB,SAAS,uBAAuB;AAmBhC,MAAM,mBAAwC,CAAC;AAE/C,SAAS,eAAe,OAAuB;AAC7C,SAAO,MACJ,QAAQ,sBAAsB,OAAO,EACrC,QAAQ,eAAe,EAAE,EACzB,YAAY;AACjB;AAEA,SAAS,sBAAsB,KAAa,SAAoC;AAC9E,QAAM,UAAU,OAAO,QAAQ,WAAW,IAAI,KAAK,IAAI;AACvD,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,aAAa,eAAe,OAAO;AACzC,aAAW,UAAU,SAAS;AAC5B,UAAM,cAAc,OAAO,OAAO,UAAU,WAAW,OAAO,MAAM,KAAK,IAAI;AAC7E,QAAI,eAAe,eAAe,WAAW,MAAM,WAAY,QAAO;AACtE,QAAI,OAAO,SAAS,eAAe,OAAO,KAAK,MAAM,WAAY,QAAO;AAAA,EAC1E;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,KAAwC,SAAkD;AACpH,QAAM,OAAO,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAK,SAAS,CAAC;AACzD,SAAO,KAAK,IAAI,CAAC,OAAO,SAAS;AAAA,IAC/B,IAAI,GAAG,MAAM,KAAK,IAAI,GAAG;AAAA,IACzB,OAAO,sBAAsB,MAAM,OAAO,OAAO;AAAA,IACjD,WAAW,MAAM,YAAY,sBAAsB,MAAM,WAAW,OAAO,IAAI;AAAA,EACjF,EAAE;AACJ;AAEA,SAAS,kBAAkB,GAAyB,GAAkC;AACpF,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,SAAO,EAAE,MAAM,CAAC,KAAK,QAAQ;AAC3B,UAAM,QAAQ,EAAE,GAAG;AACnB,WAAO,CAAC,CAAC,SAAS,IAAI,OAAO,MAAM,MAAM,IAAI,UAAU,MAAM,UAAU,IAAI,aAAa,WAAW,MAAM,aAAa;AAAA,EACxH,CAAC;AACH;AAEA,SAAS,qBAAqB,GAAkB,GAA2B;AACzE,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,SAAO,EAAE,MAAM,CAAC,QAAQ,QAAQ;AAC9B,UAAM,QAAQ,EAAE,GAAG;AACnB,WAAO,CAAC,CAAC,SAAS,OAAO,UAAU,MAAM,SAAS,OAAO,UAAU,MAAM;AAAA,EAC3E,CAAC;AACH;AAEA,SAAS,aAAqB;AAC5B,MAAI,OAAO,WAAW,eAAe,OAAO,WAAY,QAAO,OAAO,WAAW;AACjF,SAAO,OAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AACnD;AAEO,SAAS,oBAAoB;AAClC,QAAM,IAAI,KAAK;AACf,QAAM,eAAe,4BAA4B;AACjD,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAS,EAAE;AACjE,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA+B,CAAC,CAAC;AACnE,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,IAAI;AACnD,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAwB,CAAC,CAAC;AAChF,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAC9D,QAAM,EAAE,MAAM,cAAc,WAAW,iBAAiB,IAAI,mBAAmB,mBAAmB,CAAC,gBAAgB,IAAI,CAAC,GAAG;AAAA,IACzH,SAAS,CAAC,CAAC;AAAA,EACb,CAAC;AACD,QAAM,YAAY,gBAAgB;AAClC,QAAM,mBAAmB,MAAM,QAA2B,MAAM;AAC9D,UAAM,UAA6B,CAAC;AACpC,eAAW,UAAU,kBAAkB;AACrC,UAAI,CAAC,OAAO,MAAO;AACnB,UAAI,QAAQ,KAAK,CAAC,QAAQ,IAAI,UAAU,OAAO,KAAK,EAAG;AACvD,cAAQ,KAAK,MAAM;AAAA,IACrB;AACA,eAAW,OAAO,WAAW;AAC3B,YAAM,MAAM,OAAO,IAAI,QAAQ,WAAW,IAAI,IAAI,KAAK,IAAI;AAC3D,UAAI,CAAC,OAAO,QAAQ,KAAK,CAAC,QAAQ,IAAI,UAAU,GAAG,EAAG;AACtD,YAAM,QAAQ,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ;AAC1D,cAAQ,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;AAAA,IACpC;AACA,WAAO;AAAA,EACT,GAAG,CAAC,kBAAkB,SAAS,CAAC;AAChC,QAAM,eAAe,MAAM,QAAQ,MAAM;AACvC,UAAM,UAAU,oBAAI,IAAoB;AACxC,eAAW,UAAU,kBAAkB;AACrC,UAAI,CAAC,OAAO,SAAS,QAAQ,IAAI,OAAO,KAAK,EAAG;AAChD,cAAQ,IAAI,OAAO,OAAO,OAAO,SAAS,OAAO,KAAK;AAAA,IACxD;AACA,eAAW,OAAO,WAAW;AAC3B,YAAM,MAAM,OAAO,IAAI,QAAQ,WAAW,IAAI,IAAI,KAAK,IAAI;AAC3D,UAAI,CAAC,OAAO,QAAQ,IAAI,GAAG,EAAG;AAC9B,YAAM,QAAQ,OAAO,IAAI,UAAU,YAAY,IAAI,MAAM,KAAK,EAAE,SAAS,IAAI,QAAQ;AACrF,cAAQ,IAAI,KAAK,KAAK;AAAA,IACxB;AACA,eAAW,OAAO,QAAQ;AACxB,YAAM,OAAO,IAAI,OAAO,KAAK;AAC7B,UAAI,QAAQ,CAAC,QAAQ,IAAI,IAAI,EAAG,SAAQ,IAAI,MAAM,IAAI;AACtD,YAAM,OAAO,IAAI,WAAW,KAAK;AACjC,UAAI,QAAQ,CAAC,QAAQ,IAAI,IAAI,EAAG,SAAQ,IAAI,MAAM,IAAI;AAAA,IACxD;AACA,WAAO,MAAM,KAAK,QAAQ,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,EAAE,OAAO,MAAM,EAAE;AAAA,EACjF,GAAG,CAAC,kBAAkB,WAAW,MAAM,CAAC;AAExC,QAAM,EAAE,MAAM,UAAU,WAAW,iBAAiB,OAAO,cAAc,IAAI,SAAoC;AAAA,IAC/G,UAAU,CAAC,iBAAiB,YAAY;AAAA,IACxC,SAAS,YACP,qBAAqB,0BAA0B,QAAW;AAAA,MACxD,cAAc,EAAE,2CAA2C,yBAAyB;AAAA,IACtF,CAAC;AAAA,EACL,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,oBAAoB,UAAU,OAAO,QAAQ;AAChD,YAAM,QAAQ,SAAS,MAAM,CAAC;AAC9B,0BAAoB,MAAM,QAAQ;AAAA,IACpC;AAAA,EACF,GAAG,CAAC,UAAU,gBAAgB,CAAC;AAE/B,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAkB;AACrB,0BAAoB,CAAC,SAAU,KAAK,SAAS,CAAC,IAAI,IAAK;AACvD;AAAA,IACF;AACA,UAAM,QAAQ,iBAAiB,MAAM,GAAG;AACxC,UAAM,aAAa,MAAM,CAAC;AAC1B,QAAI,CAAC,YAAY;AACf,0BAAoB,CAAC,SAAU,KAAK,SAAS,CAAC,IAAI,IAAK;AACvD;AAAA,IACF;AAGA,UAAM,MAAM,gBAAgB,UAAU;AACtC,QAAI,CAAC,KAAK;AACR,cAAQ,KAAK,2CAA2C,UAAU;AAClE,0BAAoB,CAAC,SAAU,KAAK,SAAS,CAAC,IAAI,IAAK;AACvD;AAAA,IACF;AAEA,UAAM,UAAyB,CAAC;AAChC,eAAW,OAAO,OAAO,OAAO,GAAG,GAAG;AACpC,UAAI,OAAO,QAAQ,YAAY,CAAC,IAAI,KAAK,EAAG;AAC5C,YAAM,QAAQ,IAAI,KAAK;AACvB,UAAI,QAAQ,KAAK,CAAC,QAAQ,IAAI,UAAU,KAAK,EAAG;AAChD,YAAM,QAAQ,MACX,MAAM,GAAG,EACT,IAAI,CAAC,YAAa,UAAU,GAAG,QAAQ,CAAC,EAAE,YAAY,CAAC,GAAG,QAAQ,MAAM,CAAC,CAAC,KAAK,EAAG,EAClF,KAAK,GAAG,EACR,KAAK,KAAK;AACb,cAAQ,KAAK,EAAE,OAAO,MAAM,CAAC;AAAA,IAC/B;AACA,wBAAoB,CAAC,SAAU,qBAAqB,MAAM,OAAO,IAAI,OAAO,OAAQ;AAAA,EACtF,GAAG,CAAC,gBAAgB,CAAC;AAErB,QAAM;AAAA,IACJ,MAAM;AAAA,IACN,WAAW;AAAA,IACX,SAAS;AAAA,IACT,SAAS;AAAA,EACX,IAAI,SAAgC;AAAA,IAClC,UAAU,CAAC,kBAAkB,kBAAkB,YAAY;AAAA,IAC3D,SAAS,CAAC,CAAC;AAAA,IACX,SAAS,YACP,qBAAqB,qCAAqC,mBAAmB,gBAAgB,CAAC,IAAI,QAAW;AAAA,MAC3G,cAAc,EAAE,sCAAsC,+BAA+B;AAAA,IACvF,CAAC;AAAA,EACL,CAAC;AAED,QAAM,eAAe,MAAM,QAAQ,MAAM,KAAK,UAAU,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC;AAC3E,QAAM,sBAAsB,MAAM,OAAsB,IAAI;AAE5D,QAAM,UAAU,MAAM;AACpB,UAAM,YAAY;AAClB,UAAM,YAAY,cAAc,oBAAoB;AACpD,QAAI,aAAa,cAAe;AAChC,QAAI,CAAC,KAAK;AACR,gBAAU,CAAC,SAAU,KAAK,SAAS,CAAC,IAAI,IAAK;AAC7C,kBAAY,CAAC,SAAU,SAAS,OAAO,OAAO,IAAK;AACnD,uBAAiB,KAAK;AACtB,0BAAoB,UAAU;AAC9B;AAAA,IACF;AACA,UAAM,aAAa,mBAAmB,KAAK,gBAAgB;AAC3D,cAAU,CAAC,SAAU,kBAAkB,MAAM,UAAU,IAAI,OAAO,UAAW;AAC7E,UAAM,eAAe,KAAK,aAAa;AACvC,gBAAY,CAAC,SAAU,SAAS,eAAe,OAAO,YAAa;AACnE,qBAAiB,KAAK;AACtB,wBAAoB,UAAU;AAAA,EAChC,GAAG,CAAC,cAAc,KAAK,kBAAkB,aAAa,CAAC;AAEvD,QAAM,WAAW,YAAY;AAAA,IAC3B,YAAY,YAAY;AACtB,YAAM,UAAU,OACb,IAAI,CAAC,SAAS;AAAA,QACb,OAAO,sBAAsB,IAAI,OAAO,gBAAgB,EAAE,KAAK;AAAA,QAC/D,WAAW,IAAI,YAAY,sBAAsB,IAAI,WAAW,gBAAgB,EAAE,KAAK,IAAI;AAAA,MAC7F,EAAE,EACD,OAAO,CAAC,QAAQ,IAAI,MAAM,SAAS,CAAC,EACpC,IAAI,CAAC,SAAS;AAAA,QACb,OAAO,IAAI;AAAA,QACX,WAAW,IAAI,YAAY,IAAI,YAAY;AAAA,MAC7C,EAAE;AACJ,UAAI,CAAC,kBAAkB;AACrB,cAAM,IAAI,MAAM,EAAE,2CAA2C,gCAAgC,CAAC;AAAA,MAChG;AACA,UAAI,CAAC,QAAQ,QAAQ;AACnB,cAAM,IAAI,MAAM,EAAE,uCAAuC,mCAAmC,CAAC;AAAA,MAC/F;AACA,YAAM,MAAM,MAAM,QAAQ,4BAA4B;AAAA,QACpD,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,kBAAkB,QAAQ,SAAS,SAAS,CAAC;AAAA,MAChF,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,eAAe,IAAI,UAAU,EAAE,mCAAmC,+BAA+B,CAAC;AAAA,MAC1G;AACA,aAAO;AAAA,IACT;AAAA,IACA,WAAW,MAAM;AACf,YAAM,EAAE,mCAAmC,sBAAsB,GAAG,SAAS;AAC7E,WAAK,WAAW;AAAA,IAClB;AAAA,IACA,SAAS,CAAC,QAAa;AACrB,YAAM,UAAU,KAAK,WAAW,EAAE,mCAAmC,+BAA+B;AACpG,YAAM,SAAS,OAAO;AAAA,IACxB;AAAA,EACF,CAAC;AAED,QAAM,WAAW,MAAM;AACrB,qBAAiB,IAAI;AACrB,cAAU,CAAC,SAAS,CAAC,GAAG,MAAM,EAAE,IAAI,WAAW,GAAG,OAAO,IAAI,WAAW,KAAK,CAAC,CAAC;AAAA,EACjF;AAEA,QAAM,cAAc,CAAC,IAAY,UAAuC;AACtE,qBAAiB,IAAI;AACrB,cAAU,CAAC,SAAS,KAAK,IAAI,CAAC,QAAS,IAAI,OAAO,KAAK,EAAE,GAAG,KAAK,GAAG,MAAM,IAAI,GAAI,CAAC;AAAA,EACrF;AAEA,QAAM,cAAc,CAAC,OAAe;AAClC,qBAAiB,IAAI;AACrB,cAAU,CAAC,SAAS,KAAK,OAAO,CAAC,QAAQ,IAAI,OAAO,EAAE,CAAC;AAAA,EACzD;AAEA,QAAM,eAAe,MAAM;AACzB,QAAI,cAAc,kBAAkB;AAClC,aACE;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,+BAA+B,8BAAyB;AAAA,UACjE,WAAU;AAAA;AAAA,MACZ;AAAA,IAEJ;AACA,QAAI,UAAU;AACZ,aACE;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,sCAAsC,+BAA+B;AAAA,UAC9E,QACE,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,MAAM,KAAK,WAAW,GAC9E,YAAE,qCAAqC,OAAO,GACjD;AAAA;AAAA,MAEJ;AAAA,IAEJ;AACA,QAAI,CAAC,OAAO,QAAQ;AAClB,aACE,oBAAC,SAAI,WAAU,qEACZ,YAAE,6BAA6B,uDAAuD,GACzF;AAAA,IAEJ;AACA,UAAM,qBAAqB,CAAC,UAA0B;AACpD,UAAI,SAAS,CAAC,aAAa,KAAK,CAAC,QAAQ,IAAI,UAAU,KAAK,GAAG;AAC7D,eAAO,CAAC,GAAG,cAAc,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,MAClD;AACA,aAAO;AAAA,IACT;AACA,WACE,oBAAC,SAAI,WAAU,mBACb,+BAAC,WAAM,WAAU,gCACf;AAAA,0BAAC,WACC,+BAAC,QAAG,WAAU,yDACZ;AAAA,4BAAC,QAAG,WAAU,uBACX,YAAE,oCAAoC,YAAY,GACrD;AAAA,QACA,oBAAC,QAAG,WAAU,uBACX,YAAE,mCAAmC,uBAAuB,GAC/D;AAAA,QACA,oBAAC,QAAG,WAAU,wBACX,YAAE,sCAAsC,SAAS,GACpD;AAAA,SACF,GACF;AAAA,MACA,oBAAC,WACE,iBAAO,IAAI,CAAC,QAAQ;AACnB,cAAM,YAAY,mBAAmB,IAAI,KAAK;AAC9C,cAAM,WAAW,mBAAmB,IAAI,SAAS;AACjD,eACE,qBAAC,QAAgB,WAAU,YACzB;AAAA,8BAAC,QAAG,WAAU,uBACZ;AAAA,YAAC;AAAA;AAAA,cACC,OAAO,IAAI,SAAS;AAAA,cACpB,eAAe,CAAC,UAAU,YAAY,IAAI,IAAI,EAAE,OAAO,SAAS,GAAG,CAAC;AAAA,cAEpE;AAAA,oCAAC,iBACC,8BAAC,eAAY,aAAa,EAAE,0CAA0C,cAAc,GAAG,GACzF;AAAA,gBACA,oBAAC,iBACE,oBAAU,IAAI,CAAC,WACd,oBAAC,cAA8B,OAAO,OAAO,OAC1C,iBAAO,SADO,OAAO,KAExB,CACD,GACH;AAAA;AAAA;AAAA,UACF,GACF;AAAA,UACA,qBAAC,QAAG,WAAU,uBACZ;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO,IAAI,aAAa;AAAA,gBACxB,eAAe,CAAC,UAAU,YAAY,IAAI,IAAI,EAAE,WAAW,SAAS,KAAK,CAAC;AAAA,gBAE1E;AAAA,sCAAC,iBACC,8BAAC,eAAY,aAAa,EAAE,yCAAyC,8BAA8B,GAAG,GACxG;AAAA,kBACA,oBAAC,iBACE,mBAAS,IAAI,CAAC,WACb,oBAAC,cAA8B,OAAO,OAAO,OAC1C,iBAAO,SADO,OAAO,KAExB,CACD,GACH;AAAA;AAAA;AAAA,YACF;AAAA,YACA,oBAAC,OAAE,WAAU,4CACV,YAAE,uCAAuC,kEAAkE,GAC9G;AAAA,aACF;AAAA,UACA,oBAAC,QAAG,WAAU,kCACZ;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,cAAY,EAAE,sCAAsC,QAAQ;AAAA,cAC5D,SAAS,MAAM,YAAY,IAAI,EAAE;AAAA,cAEjC,8BAAC,UAAO,WAAU,WAAU;AAAA;AAAA,UAC9B,GACF;AAAA,aAjDO,IAAI,EAkDb;AAAA,MAEJ,CAAC,GACH;AAAA,OACF,GACF;AAAA,EAEJ;AAEA,SACE,oBAAC,SAAI,WAAU,aACb,+BAAC,SAAI,WAAU,+DACb;AAAA,yBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,QAAG,WAAU,yBAAyB,YAAE,6BAA6B,YAAY,GAAE;AAAA,MACpF,oBAAC,OAAE,WAAU,iCACV,YAAE,mCAAmC,sFAAsF,GAC9H;AAAA,OACF;AAAA,IACA,oBAAC,SAAI,WAAU,gDACb,+BAAC,SAAI,WAAU,oBACb;AAAA,2BAAC,SACC;AAAA,4BAAC,WAAM,WAAU,iCACd,YAAE,oCAAoC,eAAe,GACxD;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,oBAAoB;AAAA,YAC3B,eAAe,CAAC,UAAU,oBAAoB,SAAS,EAAE;AAAA,YACzD,UAAU,mBAAmB,CAAC,CAAC;AAAA,YAE/B;AAAA,kCAAC,iBAAc,WAAU,QACvB,8BAAC,eAAY,aAAa,EAAE,mCAAmC,kBAAkB,GAAG,GACtF;AAAA,cACA,oBAAC,iBACG,qBAAU,SAAS,CAAC,GAAG,IAAI,CAAC,SAC5B,qBAAC,cAA+B,OAAO,KAAK,UACzC;AAAA,qBAAK,SAAS,KAAK;AAAA,gBAAS;AAAA,gBAAE,KAAK,WAAW,WAAW,IAAI,EAAE,qCAAqC,QAAQ,CAAC,MAAM;AAAA,mBADrG,KAAK,QAEtB,CACD,GACH;AAAA;AAAA;AAAA,QACF;AAAA,QACC,gBACC,oBAAC,OAAE,WAAU,uCACV,YAAE,2CAA2C,yBAAyB,GACzE,IAEA,oBAAC,OAAE,WAAU,sCACV,YAAE,kCAAkC,yEAAyE,GAChH;AAAA,SAEJ;AAAA,MACA,qBAAC,SAAI,WAAU,0CACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,SAAS;AAAA,YACT,iBAAiB,CAAC,YAAY,YAAY,YAAY,IAAI;AAAA;AAAA,QAC5D;AAAA,QACA,oBAAC,WAAM,SAAQ,8BACZ,YAAE,8BAA8B,oCAAoC,GACvE;AAAA,SACF;AAAA,OACF,GACF;AAAA,IACA,qBAAC,SAAI,WAAU,0CACb;AAAA,2BAAC,SAAI,WAAU,0CACb;AAAA,6BAAC,SACC;AAAA,8BAAC,QAAG,WAAU,uBAAuB,YAAE,oCAAoC,kBAAkB,GAAE;AAAA,UAC/F,oBAAC,OAAE,WAAU,iCACV,YAAE,uCAAuC,mEAAmE,GAC/G;AAAA,WACF;AAAA,QACA,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,UACzD;AAAA,8BAAC,QAAK,WAAU,gBAAe;AAAA,UAC9B,EAAE,mCAAmC,WAAW;AAAA,WACnD;AAAA,SACF;AAAA,MACC,aAAa;AAAA,OAChB;AAAA,IACA,oBAAC,SAAI,WAAU,oBACb;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM,SAAS,OAAO;AAAA,QAC/B,UAAU,SAAS,aAAa,mBAAmB,CAAC,CAAC,iBAAiB,CAAC;AAAA,QAEvE;AAAA,8BAAC,QAAK,WAAU,gBAAe;AAAA,UAC9B,SAAS,YACN,EAAE,sCAAsC,cAAS,IACjD,EAAE,oCAAoC,qBAAqB;AAAA;AAAA;AAAA,IACjE,GACF;AAAA,KACF,GACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/inbox_ops/lib/llmProvider.ts"],
|
|
4
|
-
"sourcesContent": ["import { generateObject } from 'ai'\nimport type { AwilixContainer } from 'awilix'\nimport { createContainer } from 'awilix'\nimport {\n resolveAiProviderIdFromEnv,\n resolveFirstConfiguredOpenCodeProvider,\n resolveOpenCodeModel,\n requireOpenCodeProviderApiKey,\n resolveOpenCodeProviderId,\n type OpenCodeProviderId,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport {\n AiModelFactoryError,\n createModelFactory,\n type AiModelFactory,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/model-factory'\nimport { extractionOutputSchema } from '../data/validators'\n\n// Vercel AI SDK provider factories return LanguageModelV1 but generateObject()\n// expects a narrower LanguageModel union. The types are structurally compatible\n// at runtime; the cast is required until the AI SDK unifies its model types.\ntype AiModel = Parameters<typeof generateObject>[0]['model']\nfunction asAiModel(model: unknown): AiModel {\n return model as AiModel\n}\n\n/**\n * Step 5.1 \u2014 thin backward-compatibility shim. The public surface of this\n * module (`resolveExtractionProviderId`, `createStructuredModel`,\n * `withTimeout`, `runExtractionWithConfiguredProvider`) is unchanged so\n * `ai-tools.ts`, `translationProvider.ts`, and `extractionWorker.ts` continue\n * to compile and pass their existing tests.\n *\n * The model-instantiation path inside {@link runExtractionWithConfiguredProvider}\n * now delegates to the shared {@link createModelFactory} so every\n * AI-runtime caller shares one resolution order. The legacy\n * `OPENCODE_MODEL` / `OPENCODE_PROVIDER` envs remain honored via\n * {@link resolveExtractionProviderId} and {@link resolveOpenCodeModel} so\n * inbox_ops deployments do not see a behavior change \u2014 the factory is\n * consulted first (honoring `OM_AI_INBOX_OPS_MODEL` \u2014 legacy `INBOX_OPS_AI_MODEL` \u2014 + `input.modelOverride`),\n * with the legacy path as the fallback when no registry provider is\n * configured (preserving the historical error messages).\n */\n\nexport function resolveExtractionProviderId(): OpenCodeProviderId {\n // Honors OM_AI_PROVIDER first, then the legacy OPENCODE_PROVIDER, then the\n // first configured provider from `OPEN_CODE_PROVIDER_IDS`, then the unified\n // default (currently `openai`). Mirrors the precedence applied by the\n // shared model factory so the BC fallback path stays consistent.\n const explicit = (process.env.OM_AI_PROVIDER ?? process.env.OPENCODE_PROVIDER ?? '').trim()\n if (explicit.length > 0) {\n return resolveAiProviderIdFromEnv(process.env)\n }\n\n const firstConfiguredProvider = resolveFirstConfiguredOpenCodeProvider()\n if (firstConfiguredProvider) {\n return firstConfiguredProvider\n }\n\n return resolveOpenCodeProviderId(undefined)\n}\n\nexport async function createStructuredModel(\n providerId: OpenCodeProviderId,\n apiKey: string,\n modelId: string,\n): Promise<AiModel> {\n switch (providerId) {\n case 'anthropic': {\n const { createAnthropic } = await import('@ai-sdk/anthropic')\n return asAiModel(createAnthropic({ apiKey })(modelId))\n }\n case 'openai': {\n const { createOpenAI } = await import('@ai-sdk/openai')\n return asAiModel(createOpenAI({ apiKey })(modelId))\n }\n case 'google': {\n const { createGoogleGenerativeAI } = await import('@ai-sdk/google')\n return asAiModel(createGoogleGenerativeAI({ apiKey })(modelId))\n }\n default:\n throw new Error(`Unsupported provider: ${providerId}`)\n }\n}\n\nexport async function withTimeout<T>(\n operation: Promise<T>,\n timeoutMs: number,\n timeoutMessage: string,\n): Promise<T> {\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs)\n })\n\n try {\n return await Promise.race([operation, timeoutPromise])\n } finally {\n if (timeoutHandle) {\n clearTimeout(timeoutHandle)\n }\n }\n}\n\n/**\n * Test-only seam for the factory-delegation regression suite. Production\n * callers MUST use {@link runExtractionWithConfiguredProvider} directly; the\n * suite overrides this binding via `jest.spyOn` to assert the shim actually\n * reaches `createModelFactory` without stubbing `@open-mercato/ai-assistant`.\n */\nexport const __inboxOpsLlmProviderInternal = {\n createModelFactory,\n createContainer,\n}\n\nfunction tryFactoryResolution(input: {\n modelOverride?: string | null\n}): { modelId: string; providerId: OpenCodeProviderId; model: AiModel } | null {\n let factory: AiModelFactory\n try {\n const container = __inboxOpsLlmProviderInternal.createContainer()\n factory = __inboxOpsLlmProviderInternal.createModelFactory(container as AwilixContainer)\n } catch {\n return null\n }\n try {\n const resolution = factory.resolveModel({\n moduleId: 'inbox_ops',\n callerOverride: input.modelOverride ?? undefined,\n })\n const providerId = resolveOpenCodeProviderId(resolution.providerId)\n return {\n modelId: resolution.modelId,\n providerId,\n model: asAiModel(resolution.model),\n }\n } catch (err) {\n if (err instanceof AiModelFactoryError) {\n // Fall back to the legacy path so the shim keeps throwing the original\n // OPENCODE_*-era error messages existing tests/consumers rely on.\n return null\n }\n throw err\n }\n}\n\nexport async function runExtractionWithConfiguredProvider(input: {\n systemPrompt: string\n userPrompt: string\n modelOverride?: string | null\n timeoutMs: number\n}): Promise<{\n object: ReturnType<typeof extractionOutputSchema.parse>\n totalTokens: number\n modelWithProvider: string\n}> {\n const factoryResolution = tryFactoryResolution({ modelOverride: input.modelOverride })\n\n let model: AiModel\n let modelWithProvider: string\n if (factoryResolution) {\n model = factoryResolution.model\n modelWithProvider = `${factoryResolution.providerId}/${factoryResolution.modelId}`\n } else {\n const providerId = resolveExtractionProviderId()\n const apiKey = requireOpenCodeProviderApiKey(providerId)\n const modelConfig = resolveOpenCodeModel(providerId, {\n overrideModel: input.modelOverride,\n })\n model = await createStructuredModel(providerId, apiKey, modelConfig.modelId)\n modelWithProvider = modelConfig.modelWithProvider\n }\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: extractionOutputSchema,\n system: input.systemPrompt,\n prompt: input.userPrompt,\n temperature: 0,\n }),\n input.timeoutMs,\n `LLM extraction timed out after ${input.timeoutMs}ms`,\n )\n\n return {\n object: result.object,\n totalTokens: Number(result.usage?.totalTokens ?? 0) || 0,\n modelWithProvider,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,sBAAsB;AAE/B,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,8BAA8B;AAMvC,SAAS,UAAU,OAAyB;AAC1C,SAAO;AACT;AAoBO,SAAS,8BAAkD;AAKhE,QAAM,YAAY,QAAQ,IAAI,kBAAkB,QAAQ,IAAI,qBAAqB,IAAI,KAAK;AAC1F,MAAI,SAAS,SAAS,GAAG;AACvB,WAAO,2BAA2B,QAAQ,GAAG;AAAA,EAC/C;AAEA,QAAM,0BAA0B,uCAAuC;AACvE,MAAI,yBAAyB;AAC3B,WAAO;AAAA,EACT;AAEA,SAAO,0BAA0B,MAAS;AAC5C;AAEA,eAAsB,sBACpB,YACA,QACA,SACkB;AAClB,UAAQ,YAAY;AAAA,IAClB,KAAK,aAAa;AAChB,YAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,mBAAmB;AAC5D,aAAO,UAAU,gBAAgB,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC;AAAA,IACvD;AAAA,IACA,KAAK,UAAU;AACb,YAAM,EAAE,aAAa,IAAI,MAAM,OAAO,gBAAgB;AACtD,aAAO,UAAU,aAAa,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC;AAAA,IACpD;AAAA,IACA,KAAK,UAAU;AACb,YAAM,EAAE,yBAAyB,IAAI,MAAM,OAAO,gBAAgB;AAClE,aAAO,UAAU,yBAAyB,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC;AAAA,IAChE;AAAA,IACA;AACE,YAAM,IAAI,MAAM,yBAAyB,UAAU,EAAE;AAAA,EACzD;AACF;AAEA,eAAsB,YACpB,WACA,WACA,gBACY;AACZ,MAAI;AAEJ,QAAM,iBAAiB,IAAI,QAAe,CAAC,GAAG,WAAW;AACvD,oBAAgB,WAAW,MAAM,OAAO,IAAI,MAAM,cAAc,CAAC,GAAG,SAAS;AAAA,EAC/E,CAAC;AAED,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK,CAAC,WAAW,cAAc,CAAC;AAAA,EACvD,UAAE;AACA,QAAI,eAAe;AACjB,mBAAa,aAAa;AAAA,IAC5B;AAAA,EACF;AACF;AAQO,MAAM,gCAAgC;AAAA,EAC3C;AAAA,EACA;AACF;AAEA,SAAS,qBAAqB,OAEiD;AAC7E,MAAI;AACJ,MAAI;AACF,UAAM,YAAY,8BAA8B,gBAAgB;AAChE,cAAU,8BAA8B,mBAAmB,SAA4B;AAAA,EACzF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,aAAa,QAAQ,aAAa;AAAA,MACtC,UAAU;AAAA,MACV,gBAAgB,MAAM,iBAAiB;AAAA,IACzC,CAAC;AACD,UAAM,aAAa,0BAA0B,WAAW,UAAU;AAClE,WAAO;AAAA,MACL,SAAS,WAAW;AAAA,MACpB;AAAA,MACA,OAAO,UAAU,WAAW,KAAK;AAAA,IACnC;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,eAAe,qBAAqB;AAGtC,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,oCAAoC,OASvD;AACD,QAAM,oBAAoB,qBAAqB,EAAE,eAAe,MAAM,cAAc,CAAC;AAErF,MAAI;AACJ,MAAI;AACJ,MAAI,mBAAmB;AACrB,YAAQ,kBAAkB;AAC1B,wBAAoB,GAAG,kBAAkB,UAAU,IAAI,kBAAkB,OAAO;AAAA,EAClF,OAAO;
|
|
4
|
+
"sourcesContent": ["import { generateObject } from 'ai'\nimport type { AwilixContainer } from 'awilix'\nimport { createContainer } from 'awilix'\nimport {\n resolveAiProviderIdFromEnv,\n resolveFirstConfiguredOpenCodeProvider,\n resolveOpenCodeModel,\n requireOpenCodeProviderApiKey,\n resolveOpenCodeProviderId,\n type OpenCodeProviderId,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport {\n AiModelFactoryError,\n createModelFactory,\n type AiModelFactory,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/model-factory'\nimport { extractionOutputSchema } from '../data/validators'\n\n// Vercel AI SDK provider factories return LanguageModelV1 but generateObject()\n// expects a narrower LanguageModel union. The types are structurally compatible\n// at runtime; the cast is required until the AI SDK unifies its model types.\ntype AiModel = Parameters<typeof generateObject>[0]['model']\nfunction asAiModel(model: unknown): AiModel {\n return model as AiModel\n}\n\n/**\n * Step 5.1 \u2014 thin backward-compatibility shim. The public surface of this\n * module (`resolveExtractionProviderId`, `createStructuredModel`,\n * `withTimeout`, `runExtractionWithConfiguredProvider`) is unchanged so\n * `ai-tools.ts`, `translationProvider.ts`, and `extractionWorker.ts` continue\n * to compile and pass their existing tests.\n *\n * The model-instantiation path inside {@link runExtractionWithConfiguredProvider}\n * now delegates to the shared {@link createModelFactory} so every\n * AI-runtime caller shares one resolution order. The legacy\n * `OPENCODE_MODEL` / `OPENCODE_PROVIDER` envs remain honored via\n * {@link resolveExtractionProviderId} and {@link resolveOpenCodeModel} so\n * inbox_ops deployments do not see a behavior change \u2014 the factory is\n * consulted first (honoring `OM_AI_INBOX_OPS_MODEL` \u2014 legacy `INBOX_OPS_AI_MODEL` \u2014 + `input.modelOverride`),\n * with the legacy path as the fallback when no registry provider is\n * configured (preserving the historical error messages).\n */\n\nexport function resolveExtractionProviderId(): OpenCodeProviderId {\n // Honors OM_AI_PROVIDER first, then the legacy OPENCODE_PROVIDER, then the\n // first configured provider from `OPEN_CODE_PROVIDER_IDS`, then the unified\n // default (currently `openai`). Mirrors the precedence applied by the\n // shared model factory so the BC fallback path stays consistent.\n const explicit = (process.env.OM_AI_PROVIDER ?? process.env.OPENCODE_PROVIDER ?? '').trim()\n if (explicit.length > 0) {\n return resolveAiProviderIdFromEnv(process.env)\n }\n\n const firstConfiguredProvider = resolveFirstConfiguredOpenCodeProvider()\n if (firstConfiguredProvider) {\n return firstConfiguredProvider\n }\n\n return resolveOpenCodeProviderId(undefined)\n}\n\nexport async function createStructuredModel(\n providerId: OpenCodeProviderId,\n apiKey: string,\n modelId: string,\n): Promise<AiModel> {\n switch (providerId) {\n case 'anthropic': {\n const { createAnthropic } = await import('@ai-sdk/anthropic')\n return asAiModel(createAnthropic({ apiKey })(modelId))\n }\n case 'openai': {\n const { createOpenAI } = await import('@ai-sdk/openai')\n return asAiModel(createOpenAI({ apiKey })(modelId))\n }\n case 'google': {\n const { createGoogleGenerativeAI } = await import('@ai-sdk/google')\n return asAiModel(createGoogleGenerativeAI({ apiKey })(modelId))\n }\n default:\n throw new Error(`Unsupported provider: ${providerId}`)\n }\n}\n\nexport async function withTimeout<T>(\n operation: Promise<T>,\n timeoutMs: number,\n timeoutMessage: string,\n): Promise<T> {\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs)\n })\n\n try {\n return await Promise.race([operation, timeoutPromise])\n } finally {\n if (timeoutHandle) {\n clearTimeout(timeoutHandle)\n }\n }\n}\n\n/**\n * Test-only seam for the factory-delegation regression suite. Production\n * callers MUST use {@link runExtractionWithConfiguredProvider} directly; the\n * suite overrides this binding via `jest.spyOn` to assert the shim actually\n * reaches `createModelFactory` without stubbing `@open-mercato/ai-assistant`.\n */\nexport const __inboxOpsLlmProviderInternal = {\n createModelFactory,\n createContainer,\n}\n\nfunction tryFactoryResolution(input: {\n modelOverride?: string | null\n}): { modelId: string; providerId: OpenCodeProviderId; model: AiModel } | null {\n let factory: AiModelFactory\n try {\n const container = __inboxOpsLlmProviderInternal.createContainer()\n factory = __inboxOpsLlmProviderInternal.createModelFactory(container as AwilixContainer)\n } catch {\n return null\n }\n try {\n const resolution = factory.resolveModel({\n moduleId: 'inbox_ops',\n callerOverride: input.modelOverride ?? undefined,\n })\n const providerId = resolveOpenCodeProviderId(resolution.providerId)\n return {\n modelId: resolution.modelId,\n providerId,\n model: asAiModel(resolution.model),\n }\n } catch (err) {\n if (err instanceof AiModelFactoryError) {\n // Fall back to the legacy path so the shim keeps throwing the original\n // OPENCODE_*-era error messages existing tests/consumers rely on.\n return null\n }\n throw err\n }\n}\n\nexport async function runExtractionWithConfiguredProvider(input: {\n systemPrompt: string\n userPrompt: string\n modelOverride?: string | null\n timeoutMs: number\n}): Promise<{\n object: ReturnType<typeof extractionOutputSchema.parse>\n totalTokens: number\n modelWithProvider: string\n}> {\n const factoryResolution = tryFactoryResolution({ modelOverride: input.modelOverride })\n\n let model: AiModel\n let modelWithProvider: string\n if (factoryResolution) {\n model = factoryResolution.model\n modelWithProvider = `${factoryResolution.providerId}/${factoryResolution.modelId}`\n } else {\n // BC: Legacy OPENCODE_PROVIDER / OPENCODE_MODEL path. This branch only\n // runs when createModelFactory throws AiModelFactoryError('no_provider_configured'),\n // meaning none of the new-style provider env vars (ANTHROPIC_API_KEY,\n // OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, OM_AI_PROVIDER, ...) is\n // set. The OPENCODE_* envs are deliberately kept as BC fallbacks instead\n // of being treated as canonical OM_AI_* values because they are scoped to\n // the OpenCode Code Mode stack (spec\n // 2026-04-27-ai-agents-provider-model-baseurl-overrides, R1 mitigation).\n // Keeping this fallback alive preserves backward compatibility for existing\n // inbox_ops deployments that have not yet migrated to the new env vars.\n const providerId = resolveExtractionProviderId()\n const apiKey = requireOpenCodeProviderApiKey(providerId)\n const modelConfig = resolveOpenCodeModel(providerId, {\n overrideModel: input.modelOverride,\n })\n model = await createStructuredModel(providerId, apiKey, modelConfig.modelId)\n modelWithProvider = modelConfig.modelWithProvider\n }\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: extractionOutputSchema,\n system: input.systemPrompt,\n prompt: input.userPrompt,\n temperature: 0,\n }),\n input.timeoutMs,\n `LLM extraction timed out after ${input.timeoutMs}ms`,\n )\n\n return {\n object: result.object,\n totalTokens: Number(result.usage?.totalTokens ?? 0) || 0,\n modelWithProvider,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.1-develop.
|
|
3
|
+
"version": "0.6.1-develop.3256.1.fe3dec2464",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -237,12 +237,12 @@
|
|
|
237
237
|
"ts-pattern": "^5.0.0"
|
|
238
238
|
},
|
|
239
239
|
"peerDependencies": {
|
|
240
|
-
"@open-mercato/ai-assistant": "0.6.1-develop.
|
|
241
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
240
|
+
"@open-mercato/ai-assistant": "0.6.1-develop.3256.1.fe3dec2464",
|
|
241
|
+
"@open-mercato/shared": "0.6.1-develop.3256.1.fe3dec2464"
|
|
242
242
|
},
|
|
243
243
|
"devDependencies": {
|
|
244
|
-
"@open-mercato/ai-assistant": "0.6.1-develop.
|
|
245
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
244
|
+
"@open-mercato/ai-assistant": "0.6.1-develop.3256.1.fe3dec2464",
|
|
245
|
+
"@open-mercato/shared": "0.6.1-develop.3256.1.fe3dec2464",
|
|
246
246
|
"@testing-library/dom": "^10.4.1",
|
|
247
247
|
"@testing-library/jest-dom": "^6.9.1",
|
|
248
248
|
"@testing-library/react": "^16.3.1",
|
|
@@ -26,61 +26,16 @@
|
|
|
26
26
|
* prompt-override merges can address sections by name. The composed
|
|
27
27
|
* text is fed into `systemPrompt` so the current runtime continues to
|
|
28
28
|
* work.
|
|
29
|
-
*
|
|
30
|
-
* Local type declarations mirror the public shapes from
|
|
31
|
-
* `@open-mercato/ai-assistant`. `@open-mercato/core` does not depend on
|
|
32
|
-
* `@open-mercato/ai-assistant` (see the companion comment in
|
|
33
|
-
* `ai-tools/types.ts` and the Step 4.7 / 4.8 implementation notes), so
|
|
34
|
-
* the generator imports this file via the app's bundler and the runtime
|
|
35
|
-
* graph resolves through `apps/mercato/.mercato/generated/ai-agents.generated.ts`.
|
|
36
29
|
*/
|
|
37
|
-
import type {
|
|
30
|
+
import type {
|
|
31
|
+
AiAgentDefinition,
|
|
32
|
+
AiAgentPageContextInput,
|
|
33
|
+
} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-agent-definition'
|
|
38
34
|
import {
|
|
39
35
|
hydrateCatalogAssistantContext,
|
|
40
36
|
hydrateMerchandisingAssistantContext,
|
|
41
37
|
} from './ai-agents-context'
|
|
42
38
|
|
|
43
|
-
type AiAgentExecutionMode = 'chat' | 'object'
|
|
44
|
-
type AiAgentMutationPolicy = 'read-only' | 'confirm-required' | 'destructive-confirm-required'
|
|
45
|
-
type AiAgentAcceptedMediaType = 'image' | 'pdf' | 'file'
|
|
46
|
-
type AiAgentDataOperation = 'read' | 'search' | 'aggregate'
|
|
47
|
-
|
|
48
|
-
interface AiAgentPageContextInput {
|
|
49
|
-
entityType: string
|
|
50
|
-
recordId: string
|
|
51
|
-
container: AwilixContainer
|
|
52
|
-
tenantId: string | null
|
|
53
|
-
organizationId: string | null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface AiAgentDataCapabilities {
|
|
57
|
-
entities?: string[]
|
|
58
|
-
operations?: AiAgentDataOperation[]
|
|
59
|
-
searchableFields?: string[]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface AiAgentDefinition {
|
|
63
|
-
id: string
|
|
64
|
-
moduleId: string
|
|
65
|
-
label: string
|
|
66
|
-
description: string
|
|
67
|
-
systemPrompt: string
|
|
68
|
-
allowedTools: string[]
|
|
69
|
-
executionMode?: AiAgentExecutionMode
|
|
70
|
-
defaultModel?: string
|
|
71
|
-
acceptedMediaTypes?: AiAgentAcceptedMediaType[]
|
|
72
|
-
requiredFeatures?: string[]
|
|
73
|
-
uiParts?: string[]
|
|
74
|
-
readOnly?: boolean
|
|
75
|
-
mutationPolicy?: AiAgentMutationPolicy
|
|
76
|
-
maxSteps?: number
|
|
77
|
-
output?: unknown
|
|
78
|
-
resolvePageContext?: (ctx: AiAgentPageContextInput) => Promise<string | null>
|
|
79
|
-
keywords?: string[]
|
|
80
|
-
domain?: string
|
|
81
|
-
dataCapabilities?: AiAgentDataCapabilities
|
|
82
|
-
}
|
|
83
|
-
|
|
84
39
|
type PromptSectionName =
|
|
85
40
|
| 'role'
|
|
86
41
|
| 'scope'
|
|
@@ -314,6 +269,8 @@ const agent: AiAgentDefinition = {
|
|
|
314
269
|
requiredFeatures: [...REQUIRED_FEATURES],
|
|
315
270
|
readOnly: true,
|
|
316
271
|
mutationPolicy: 'read-only',
|
|
272
|
+
defaultProvider: 'openai',
|
|
273
|
+
defaultModel: 'gpt-5-mini',
|
|
317
274
|
keywords: ['catalog', 'products', 'categories', 'variants', 'prices', 'offers', 'media'],
|
|
318
275
|
domain: 'catalog',
|
|
319
276
|
dataCapabilities: {
|
|
@@ -506,6 +463,8 @@ const merchandisingAgent: AiAgentDefinition = {
|
|
|
506
463
|
// the operator via the pending-action approval card. Per-tenant override
|
|
507
464
|
// can downgrade to `read-only` to lock writes without a redeploy.
|
|
508
465
|
mutationPolicy: 'confirm-required',
|
|
466
|
+
defaultProvider: 'openai',
|
|
467
|
+
defaultModel: 'gpt-5-mini',
|
|
509
468
|
keywords: ['catalog', 'merchandising', 'products', 'attributes', 'pricing', 'copy'],
|
|
510
469
|
domain: 'catalog',
|
|
511
470
|
dataCapabilities: {
|
|
@@ -20,60 +20,13 @@
|
|
|
20
20
|
* the structured template is additionally exported so downstream Phases
|
|
21
21
|
* (5.3 prompt-override merge, 5.2 resolvePageContext hydration) can
|
|
22
22
|
* address sections by name.
|
|
23
|
-
*
|
|
24
|
-
* Local type declarations mirror the public shapes from
|
|
25
|
-
* `@open-mercato/ai-assistant`. The customers module does not depend on
|
|
26
|
-
* `@open-mercato/ai-assistant` (see the companion comment in
|
|
27
|
-
* `ai-tools/types.ts`) — the generator imports this file via the app's
|
|
28
|
-
* bundler, so the runtime graph resolves through
|
|
29
|
-
* `apps/mercato/.mercato/generated/ai-agents.generated.ts`. Keeping the
|
|
30
|
-
* type declarations local mirrors how the `customers/ai-tools/types.ts`
|
|
31
|
-
* handles `AiToolDefinition`.
|
|
32
23
|
*/
|
|
33
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
AiAgentDefinition,
|
|
26
|
+
AiAgentPageContextInput,
|
|
27
|
+
} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-agent-definition'
|
|
34
28
|
import { hydrateCustomersAccountContext } from './ai-agents-context'
|
|
35
29
|
|
|
36
|
-
type AiAgentExecutionMode = 'chat' | 'object'
|
|
37
|
-
type AiAgentMutationPolicy = 'read-only' | 'confirm-required' | 'destructive-confirm-required'
|
|
38
|
-
type AiAgentAcceptedMediaType = 'image' | 'pdf' | 'file'
|
|
39
|
-
type AiAgentDataOperation = 'read' | 'search' | 'aggregate'
|
|
40
|
-
|
|
41
|
-
interface AiAgentPageContextInput {
|
|
42
|
-
entityType: string
|
|
43
|
-
recordId: string
|
|
44
|
-
container: AwilixContainer
|
|
45
|
-
tenantId: string | null
|
|
46
|
-
organizationId: string | null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface AiAgentDataCapabilities {
|
|
50
|
-
entities?: string[]
|
|
51
|
-
operations?: AiAgentDataOperation[]
|
|
52
|
-
searchableFields?: string[]
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface AiAgentDefinition {
|
|
56
|
-
id: string
|
|
57
|
-
moduleId: string
|
|
58
|
-
label: string
|
|
59
|
-
description: string
|
|
60
|
-
systemPrompt: string
|
|
61
|
-
allowedTools: string[]
|
|
62
|
-
executionMode?: AiAgentExecutionMode
|
|
63
|
-
defaultModel?: string
|
|
64
|
-
acceptedMediaTypes?: AiAgentAcceptedMediaType[]
|
|
65
|
-
requiredFeatures?: string[]
|
|
66
|
-
uiParts?: string[]
|
|
67
|
-
readOnly?: boolean
|
|
68
|
-
mutationPolicy?: AiAgentMutationPolicy
|
|
69
|
-
maxSteps?: number
|
|
70
|
-
output?: unknown
|
|
71
|
-
resolvePageContext?: (ctx: AiAgentPageContextInput) => Promise<string | null>
|
|
72
|
-
keywords?: string[]
|
|
73
|
-
domain?: string
|
|
74
|
-
dataCapabilities?: AiAgentDataCapabilities
|
|
75
|
-
}
|
|
76
|
-
|
|
77
30
|
type PromptSectionName =
|
|
78
31
|
| 'role'
|
|
79
32
|
| 'scope'
|
|
@@ -528,5 +528,15 @@ export const openApi = createCustomersCrudOpenApi({
|
|
|
528
528
|
schema: z.object({ id: z.string().uuid() }),
|
|
529
529
|
responseSchema: defaultOkResponseSchema,
|
|
530
530
|
description: 'Deletes a company by id. The identifier can be provided via body or query.',
|
|
531
|
+
errors: [
|
|
532
|
+
{
|
|
533
|
+
status: 422,
|
|
534
|
+
description: 'Company has dependent records (people, deals, or direct staff); unlink or reassign before delete.',
|
|
535
|
+
schema: z.object({
|
|
536
|
+
error: z.string(),
|
|
537
|
+
code: z.literal('COMPANY_HAS_DEPENDENTS'),
|
|
538
|
+
}),
|
|
539
|
+
},
|
|
540
|
+
],
|
|
531
541
|
},
|
|
532
542
|
})
|
|
@@ -559,5 +559,15 @@ export const openApi = createCustomersCrudOpenApi({
|
|
|
559
559
|
schema: z.object({ id: z.string().uuid() }),
|
|
560
560
|
responseSchema: defaultOkResponseSchema,
|
|
561
561
|
description: 'Deletes a person by id. Request body or query may provide the identifier.',
|
|
562
|
+
errors: [
|
|
563
|
+
{
|
|
564
|
+
status: 422,
|
|
565
|
+
description: 'Person has dependent records (e.g. linked deals); unlink or reassign before delete.',
|
|
566
|
+
schema: z.object({
|
|
567
|
+
error: z.string(),
|
|
568
|
+
code: z.literal('PERSON_HAS_DEPENDENTS'),
|
|
569
|
+
}),
|
|
570
|
+
},
|
|
571
|
+
],
|
|
562
572
|
},
|
|
563
573
|
})
|
|
@@ -10,6 +10,9 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
10
10
|
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
11
11
|
import { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
12
12
|
import { buildCrudExportUrl } from '@open-mercato/ui/backend/utils/crud'
|
|
13
|
+
import { groupBulkDeleteFailures, runBulkDelete } from '@open-mercato/ui/backend/utils/bulkDelete'
|
|
14
|
+
import { coalesceLastOperations } from '@open-mercato/ui/backend/operations/store'
|
|
15
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
13
16
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
14
17
|
import { E } from '#generated/entities.ids.generated'
|
|
15
18
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
@@ -161,6 +164,26 @@ export default function CustomersCompaniesPage() {
|
|
|
161
164
|
setPageSize(newSize)
|
|
162
165
|
setPage(1)
|
|
163
166
|
}, [])
|
|
167
|
+
|
|
168
|
+
const bulkMutationContextId = 'customers-companies-list:bulk-delete'
|
|
169
|
+
const { runMutation: runBulkMutation, retryLastMutation: retryBulkMutation } = useGuardedMutation<{
|
|
170
|
+
formId: string
|
|
171
|
+
resourceKind: string
|
|
172
|
+
retryLastMutation: () => Promise<boolean>
|
|
173
|
+
}>({
|
|
174
|
+
contextId: bulkMutationContextId,
|
|
175
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
176
|
+
})
|
|
177
|
+
const singleMutationContextId = 'customers-companies-list:single-delete'
|
|
178
|
+
const { runMutation: runSingleMutation, retryLastMutation: retrySingleMutation } = useGuardedMutation<{
|
|
179
|
+
formId: string
|
|
180
|
+
resourceKind: string
|
|
181
|
+
resourceId: string
|
|
182
|
+
retryLastMutation: () => Promise<boolean>
|
|
183
|
+
}>({
|
|
184
|
+
contextId: singleMutationContextId,
|
|
185
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
186
|
+
})
|
|
164
187
|
const fetchDictionaryEntries = React.useCallback(async (kind: DictionaryKindKey) => {
|
|
165
188
|
try {
|
|
166
189
|
const data = await ensureCustomerDictionary(queryClient, kind, scopeVersion)
|
|
@@ -460,14 +483,24 @@ export default function CustomersCompaniesPage() {
|
|
|
460
483
|
})
|
|
461
484
|
if (!confirmed) return
|
|
462
485
|
try {
|
|
463
|
-
await
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
486
|
+
await runSingleMutation({
|
|
487
|
+
operation: async () => {
|
|
488
|
+
await apiCallOrThrow(
|
|
489
|
+
`/api/customers/companies?id=${encodeURIComponent(company.id)}`,
|
|
490
|
+
{
|
|
491
|
+
method: 'DELETE',
|
|
492
|
+
headers: { 'content-type': 'application/json' },
|
|
493
|
+
},
|
|
494
|
+
{ errorMessage: t('customers.companies.list.deleteError') },
|
|
495
|
+
)
|
|
468
496
|
},
|
|
469
|
-
|
|
470
|
-
|
|
497
|
+
context: {
|
|
498
|
+
formId: singleMutationContextId,
|
|
499
|
+
resourceKind: 'customers.company',
|
|
500
|
+
resourceId: company.id,
|
|
501
|
+
retryLastMutation: retrySingleMutation,
|
|
502
|
+
},
|
|
503
|
+
})
|
|
471
504
|
setRows((prev) => prev.filter((row) => row.id !== company.id))
|
|
472
505
|
setTotal((prev) => Math.max(prev - 1, 0))
|
|
473
506
|
handleRefresh()
|
|
@@ -476,7 +509,7 @@ export default function CustomersCompaniesPage() {
|
|
|
476
509
|
const message = err instanceof Error ? err.message : t('customers.companies.list.deleteError')
|
|
477
510
|
flash(message, 'error')
|
|
478
511
|
}
|
|
479
|
-
}, [confirm, handleRefresh, t])
|
|
512
|
+
}, [confirm, handleRefresh, retrySingleMutation, runSingleMutation, singleMutationContextId, t])
|
|
480
513
|
|
|
481
514
|
const handleBulkDelete = React.useCallback(async (selectedRows: CompanyRow[]) => {
|
|
482
515
|
const confirmed = await confirm({
|
|
@@ -485,44 +518,71 @@ export default function CustomersCompaniesPage() {
|
|
|
485
518
|
variant: 'destructive',
|
|
486
519
|
})
|
|
487
520
|
if (!confirmed) return false
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
521
|
+
|
|
522
|
+
const { succeeded, failures } = await runBulkMutation({
|
|
523
|
+
operation: async () =>
|
|
524
|
+
runBulkDelete(
|
|
525
|
+
selectedRows,
|
|
526
|
+
async (row) => {
|
|
527
|
+
await apiCallOrThrow(`/api/customers/companies?id=${encodeURIComponent(row.id)}`, {
|
|
528
|
+
method: 'DELETE',
|
|
529
|
+
headers: { 'content-type': 'application/json' },
|
|
530
|
+
})
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
fallbackErrorMessage: t('customers.companies.list.deleteError', 'Failed to delete company.'),
|
|
534
|
+
logTag: 'customers.companies.list',
|
|
535
|
+
},
|
|
536
|
+
),
|
|
537
|
+
context: {
|
|
538
|
+
formId: bulkMutationContextId,
|
|
539
|
+
resourceKind: 'customers.company',
|
|
540
|
+
retryLastMutation: retryBulkMutation,
|
|
541
|
+
},
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
if (succeeded.length > 0) {
|
|
545
|
+
const succeededIds = new Set(succeeded.map((r) => r.id))
|
|
546
|
+
setRows((prev) => prev.filter((r) => !succeededIds.has(r.id)))
|
|
547
|
+
setTotal((prev) => Math.max(0, prev - succeeded.length))
|
|
548
|
+
setReloadToken((prev) => prev + 1)
|
|
549
|
+
if (succeeded.length > 1) {
|
|
550
|
+
coalesceLastOperations(succeeded.length, {
|
|
551
|
+
commandId: 'customers.companies.delete',
|
|
552
|
+
actionLabel: t('customers.companies.list.bulkDelete.operationLabel', 'Delete {count} companies', { count: succeeded.length }),
|
|
553
|
+
resourceKind: 'customers.company',
|
|
495
554
|
})
|
|
496
|
-
deletedCount++
|
|
497
|
-
} catch (err) {
|
|
498
|
-
failedIds.push(row.id)
|
|
499
|
-
console.warn('[customers.companies.list] bulk delete failed', row.id, err)
|
|
500
555
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
})
|
|
507
|
-
setTotal((prev) => Math.max(0, prev - deletedCount))
|
|
508
|
-
if (failedIds.length === 0) {
|
|
509
|
-
flash(t('customers.companies.list.bulkDelete.success', '{count} companies deleted', { count: deletedCount }), 'success')
|
|
556
|
+
if (failures.length === 0) {
|
|
557
|
+
flash(
|
|
558
|
+
t('customers.companies.list.bulkDelete.success', '{count} companies deleted', { count: succeeded.length }),
|
|
559
|
+
'success',
|
|
560
|
+
)
|
|
510
561
|
} else {
|
|
511
562
|
flash(
|
|
512
563
|
t('customers.companies.list.bulkDelete.partial', '{deleted} of {total} companies deleted; {failed} failed', {
|
|
513
|
-
deleted:
|
|
564
|
+
deleted: succeeded.length,
|
|
514
565
|
total: selectedRows.length,
|
|
515
|
-
failed:
|
|
566
|
+
failed: failures.length,
|
|
516
567
|
}),
|
|
517
568
|
'warning',
|
|
518
569
|
)
|
|
519
570
|
}
|
|
520
|
-
setReloadToken((prev) => prev + 1)
|
|
521
|
-
} else if (failedIds.length > 0) {
|
|
522
|
-
flash(t('customers.companies.list.bulkDelete.failed', 'Failed to delete {count} companies', { count: failedIds.length }), 'error')
|
|
523
571
|
}
|
|
524
|
-
|
|
525
|
-
|
|
572
|
+
|
|
573
|
+
for (const group of groupBulkDeleteFailures(failures)) {
|
|
574
|
+
const message = group.count === 1
|
|
575
|
+
? group.sampleMessage
|
|
576
|
+
: t(
|
|
577
|
+
'customers.companies.list.bulkDelete.failedGroup',
|
|
578
|
+
'{count} companies could not be deleted: {message}',
|
|
579
|
+
{ count: group.count, message: group.sampleMessage },
|
|
580
|
+
)
|
|
581
|
+
flash(message, 'error')
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return succeeded.length > 0
|
|
585
|
+
}, [bulkMutationContextId, confirm, retryBulkMutation, runBulkMutation, t])
|
|
526
586
|
|
|
527
587
|
const handleFiltersApply = React.useCallback((values: FilterValues) => {
|
|
528
588
|
const next: FilterValues = {}
|