@open-mercato/core 0.6.8-develop.7029.1.a1bb3363af → 0.6.8-develop.7031.1.005201cd70
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/dist/modules/api_keys/backend/api-keys/page.js +3 -1
- package/dist/modules/api_keys/backend/api-keys/page.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentLibrary.js +2 -0
- package/dist/modules/attachments/components/AttachmentLibrary.js.map +2 -2
- package/dist/modules/audit_logs/backend/audit-logs/page.js +6 -0
- package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
- package/dist/modules/auth/backend/roles/page.js +3 -1
- package/dist/modules/auth/backend/roles/page.js.map +2 -2
- package/dist/modules/auth/backend/users/page.js +2 -1
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/business_rules/backend/logs/page.js +3 -1
- package/dist/modules/business_rules/backend/logs/page.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/page.js +3 -1
- package/dist/modules/business_rules/backend/rules/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/page.js +3 -1
- package/dist/modules/business_rules/backend/sets/page.js.map +2 -2
- package/dist/modules/catalog/ai-agents.js +1 -1
- package/dist/modules/catalog/ai-agents.js.map +1 -1
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +2 -2
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +5 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +2 -2
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js +2 -0
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductsDataTable.js +7 -1
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +2 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +3 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
- package/dist/modules/configs/lib/system-status.js +9 -0
- package/dist/modules/configs/lib/system-status.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +3 -1
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +3 -1
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +3 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +3 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customers/ai-tools/companies-pack.js +4 -1
- package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/deals-pack.js +4 -1
- package/dist/modules/customers/ai-tools/deals-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/people-pack.js +4 -1
- package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
- package/dist/modules/customers/api/deals/map/route.js +2 -1
- package/dist/modules/customers/api/deals/map/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +3 -1
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +5 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +3 -1
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/CustomerTodosTable.js +1 -0
- package/dist/modules/customers/components/CustomerTodosTable.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/page.js +3 -1
- package/dist/modules/devices/backend/devices/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/page.js +2 -1
- package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/page.js +2 -1
- package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
- package/dist/modules/entities/api/records.js +4 -2
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +3 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/plots/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/plots/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/statements/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/statements/page.js.map +2 -2
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js +1 -0
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js.map +2 -2
- package/dist/modules/feature_toggles/components/OverridesTable.js +1 -0
- package/dist/modules/feature_toggles/components/OverridesTable.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +3 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +3 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +4 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +3 -1
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js +3 -0
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/push_notifications/backend/push_notifications/page.js +3 -1
- package/dist/modules/push_notifications/backend/push_notifications/page.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +108 -48
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/page.js +3 -1
- package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/page.js +3 -1
- package/dist/modules/resources/backend/resources/resources/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +3 -0
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +3 -0
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +3 -0
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +4 -0
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/page.js +3 -0
- package/dist/modules/staff/backend/staff/leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/my-leave-requests/page.js +3 -0
- package/dist/modules/staff/backend/staff/my-leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/page.js +3 -0
- package/dist/modules/staff/backend/staff/team-members/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/page.js +3 -0
- package/dist/modules/staff/backend/staff/team-roles/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +3 -0
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/page.js +3 -0
- package/dist/modules/staff/backend/staff/teams/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js +3 -0
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/page.js +3 -0
- package/dist/modules/warranty_claims/backend/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +3 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +2 -2
- package/dist/modules/wms/commands/inventory-actions.js +3 -0
- package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js +4 -0
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js +1 -0
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotsListPage.js +1 -0
- package/dist/modules/wms/components/backend/WmsLotsListPage.js.map +2 -2
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js +27 -20
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +3 -1
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/dist/modules/workflows/backend/events/page.js +3 -1
- package/dist/modules/workflows/backend/events/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/page.js +3 -1
- package/dist/modules/workflows/backend/instances/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/page.js +3 -1
- package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/api_keys/backend/api-keys/page.tsx +4 -1
- package/src/modules/attachments/components/AttachmentLibrary.tsx +3 -0
- package/src/modules/audit_logs/backend/audit-logs/page.tsx +8 -0
- package/src/modules/auth/backend/roles/page.tsx +4 -2
- package/src/modules/auth/backend/users/page.tsx +3 -2
- package/src/modules/business_rules/backend/logs/page.tsx +4 -1
- package/src/modules/business_rules/backend/rules/page.tsx +4 -1
- package/src/modules/business_rules/backend/sets/page.tsx +4 -1
- package/src/modules/catalog/ai-agents.ts +1 -1
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +2 -2
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +12 -5
- package/src/modules/catalog/components/categories/CategoriesDataTable.tsx +3 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +7 -0
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/ko.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +4 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +4 -0
- package/src/modules/configs/i18n/de.json +2 -0
- package/src/modules/configs/i18n/en.json +2 -0
- package/src/modules/configs/i18n/es.json +2 -0
- package/src/modules/configs/i18n/ko.json +2 -0
- package/src/modules/configs/i18n/pl.json +2 -0
- package/src/modules/configs/lib/system-status.ts +9 -0
- package/src/modules/currencies/backend/currencies/page.tsx +4 -1
- package/src/modules/currencies/backend/exchange-rates/page.tsx +4 -1
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +4 -1
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +4 -1
- package/src/modules/customers/ai-tools/companies-pack.ts +4 -1
- package/src/modules/customers/ai-tools/deals-pack.ts +4 -1
- package/src/modules/customers/ai-tools/people-pack.ts +4 -1
- package/src/modules/customers/api/deals/map/route.ts +1 -0
- package/src/modules/customers/backend/customers/companies/page.tsx +4 -1
- package/src/modules/customers/backend/customers/deals/page.tsx +8 -2
- package/src/modules/customers/backend/customers/people/page.tsx +4 -1
- package/src/modules/customers/components/CustomerTodosTable.tsx +2 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +4 -1
- package/src/modules/devices/backend/devices/page.tsx +4 -1
- package/src/modules/directory/backend/directory/organizations/page.tsx +3 -1
- package/src/modules/directory/backend/directory/tenants/page.tsx +3 -1
- package/src/modules/entities/api/records.ts +2 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +4 -1
- package/src/modules/eudr/backend/eudr/evidence-submissions/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/plots/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/product-mappings/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/statements/page.tsx +4 -0
- package/src/modules/feature_toggles/components/FeatureTogglesTable.tsx +2 -1
- package/src/modules/feature_toggles/components/OverridesTable.tsx +2 -1
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +4 -0
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +4 -0
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +4 -0
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +4 -1
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +4 -0
- package/src/modules/push_notifications/backend/push_notifications/page.tsx +4 -1
- package/src/modules/query_index/lib/engine.ts +156 -53
- package/src/modules/resources/backend/resources/resource-types/page.tsx +4 -1
- package/src/modules/resources/backend/resources/resources/page.tsx +4 -1
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +4 -0
- package/src/modules/sales/backend/sales/channels/page.tsx +4 -0
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +4 -0
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +5 -0
- package/src/modules/staff/backend/staff/leave-requests/page.tsx +4 -0
- package/src/modules/staff/backend/staff/my-leave-requests/page.tsx +4 -0
- package/src/modules/staff/backend/staff/team-members/page.tsx +6 -0
- package/src/modules/staff/backend/staff/team-roles/page.tsx +4 -0
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +4 -0
- package/src/modules/staff/backend/staff/teams/page.tsx +4 -0
- package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.tsx +4 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +4 -0
- package/src/modules/wms/commands/inventory-actions.ts +3 -0
- package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +5 -0
- package/src/modules/wms/components/backend/WmsInventoryConsolePage.tsx +2 -0
- package/src/modules/wms/components/backend/WmsLotsListPage.tsx +2 -0
- package/src/modules/wms/components/backend/inventoryMutationLoaders.ts +36 -20
- package/src/modules/workflows/backend/definitions/page.tsx +4 -1
- package/src/modules/workflows/backend/events/page.tsx +4 -1
- package/src/modules/workflows/backend/instances/page.tsx +4 -1
- package/src/modules/workflows/backend/tasks/page.tsx +4 -1
|
@@ -192,6 +192,7 @@ function CustomerAccountsPage() {
|
|
|
192
192
|
const [pageSize] = React.useState(50);
|
|
193
193
|
const [total, setTotal] = React.useState(0);
|
|
194
194
|
const [totalPages, setTotalPages] = React.useState(1);
|
|
195
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false);
|
|
195
196
|
const [search, setSearch] = React.useState("");
|
|
196
197
|
const [filterValues, setFilterValues] = React.useState({});
|
|
197
198
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
@@ -264,6 +265,7 @@ function CustomerAccountsPage() {
|
|
|
264
265
|
setRows(items);
|
|
265
266
|
setTotal(typeof payload?.total === "number" ? payload.total : items.length);
|
|
266
267
|
setTotalPages(typeof payload?.totalPages === "number" ? payload.totalPages : 1);
|
|
268
|
+
setTotalIsCapped(payload?.totalIsCapped === true);
|
|
267
269
|
} catch (err) {
|
|
268
270
|
if (!cancelled) {
|
|
269
271
|
const message = err instanceof Error ? err.message : t("customer_accounts.admin.error.loadUsers", "Failed to load customer users");
|
|
@@ -501,7 +503,7 @@ function CustomerAccountsPage() {
|
|
|
501
503
|
]
|
|
502
504
|
}
|
|
503
505
|
),
|
|
504
|
-
pagination: { page, pageSize, total, totalPages, onPageChange: setPage },
|
|
506
|
+
pagination: { page, pageSize, total, totalPages, totalIsCapped, onPageChange: setPage },
|
|
505
507
|
isLoading
|
|
506
508
|
}
|
|
507
509
|
),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customer_accounts/backend/customer_accounts/users/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/customer_accounts/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Globe, Settings } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\n\ntype UserRow = {\n id: string\n displayName: string\n email: string\n emailVerified: boolean\n isActive: boolean\n lastLoginAt: string | null\n roles: Array<{ id: string; name: string; slug: string }>\n createdAt: string\n updatedAt?: string | null\n personEntityId: string | null\n customerEntityId: string | null\n}\n\ntype UsersResponse = {\n items?: UserRow[]\n total?: number\n totalPages?: number\n}\n\nfunction formatDate(value: string | null | undefined, fallback: string): string {\n if (!value) return fallback\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return fallback\n return date.toLocaleDateString()\n}\n\nasync function fetchRoleFilterOptions(): Promise<Array<{ value: string; label: string; id: string }>> {\n try {\n const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (!call.ok) return []\n const items = Array.isArray(call.result?.items) ? call.result!.items : []\n return items\n .filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string')\n .map((item) => ({ value: item.id, label: item.name, id: item.id }))\n } catch {\n return []\n }\n}\n\nfunction CreateUserDialog({\n open,\n onOpenChange,\n roleOptions,\n onCreated,\n onRunMutation,\n}: {\n open: boolean\n onOpenChange: (next: boolean) => void\n roleOptions: Array<{ id: string; label: string }>\n onCreated: () => void\n onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>\n}) {\n const t = useT()\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [password, setPassword] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n const resetForm = React.useCallback(() => {\n setEmail('')\n setDisplayName('')\n setPassword('')\n setSelectedRoleIds([])\n }, [])\n\n const handleSubmit = React.useCallback(async (event: React.FormEvent) => {\n event.preventDefault()\n if (!email.trim() || !displayName.trim() || !password.trim()) {\n flash(t('customer_accounts.admin.createUser.error.required', 'Email, name, and password are required'), 'error')\n return\n }\n setIsSubmitting(true)\n try {\n await onRunMutation(async () => {\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: email.trim(),\n displayName: displayName.trim(),\n password,\n roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : undefined,\n }),\n },\n )\n if (!call.ok) {\n flash(call.result?.error || t('customer_accounts.admin.createUser.error.save', 'Failed to create user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.createUser.flash.created', 'User created'), 'success')\n resetForm()\n onOpenChange(false)\n onCreated()\n })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.createUser.error.save', 'Failed to create user')\n flash(message, 'error')\n } finally {\n setIsSubmitting(false)\n }\n }, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n const form = (event.target as HTMLElement).closest('form')\n if (form) form.requestSubmit()\n }\n }, [])\n\n return (\n <Dialog open={open} onOpenChange={(next) => { if (!next) resetForm(); onOpenChange(next) }}>\n <DialogContent className=\"sm:max-w-lg\">\n <DialogHeader>\n <DialogTitle>{t('customer_accounts.admin.createUser.title', 'Create Customer User')}</DialogTitle>\n </DialogHeader>\n <form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-email\">\n {t('customer_accounts.admin.createUser.fields.email', 'Email')}\n </label>\n <EmailInput\n id=\"create-email\"\n required\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.emailPlaceholder', 'user@example.com')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-name\">\n {t('customer_accounts.admin.createUser.fields.displayName', 'Display Name')}\n </label>\n <Input\n id=\"create-name\"\n type=\"text\"\n required\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.displayNamePlaceholder', 'John Doe')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-password\">\n {t('customer_accounts.admin.createUser.fields.password', 'Password')}\n </label>\n <PasswordInput\n id=\"create-password\"\n required\n minLength={8}\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.passwordPlaceholder', 'Min. 8 characters')}\n autoComplete=\"new-password\"\n />\n </div>\n {roleOptions.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium\">{t('customer_accounts.admin.createUser.fields.roles', 'Roles')}</p>\n <div className=\"flex flex-wrap gap-2\">\n {roleOptions.map((role) => {\n const isSelected = selectedRoleIds.includes(role.id)\n return (\n <Button\n key={role.id}\n type=\"button\"\n size=\"2xs\"\n variant={isSelected ? 'secondary' : 'outline'}\n aria-pressed={isSelected}\n onClick={() => setSelectedRoleIds((prev) =>\n prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],\n )}\n className=\"rounded-full\"\n >\n {role.label}\n </Button>\n )\n })}\n </div>\n </div>\n )}\n <div className=\"flex justify-end gap-2 pt-2\">\n <Button type=\"button\" variant=\"outline\" onClick={() => { resetForm(); onOpenChange(false) }}>\n {t('customer_accounts.admin.createUser.actions.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting\n ? t('customer_accounts.admin.createUser.actions.creating', 'Creating...')\n : t('customer_accounts.admin.createUser.actions.create', 'Create User')}\n </Button>\n </div>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default function CustomerAccountsPage() {\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<UserRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(50)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [roleOptions, setRoleOptions] = React.useState<Array<{ value: string; label: string; id: string }>>([])\n const [createDialogOpen, setCreateDialogOpen] = React.useState(false)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n entityType: string\n entityId?: string\n }>({\n contextId: 'customer_accounts:users-list',\n })\n\n const runMutationWithContext = React.useCallback(\n async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {\n return runMutation({\n operation,\n mutationPayload,\n context: { entityType: 'customer_accounts:user' },\n })\n },\n [runMutation],\n )\n\n React.useEffect(() => {\n let cancelled = false\n fetchRoleFilterOptions().then((opts) => {\n if (!cancelled) setRoleOptions(opts)\n })\n return () => { cancelled = true }\n }, [])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'status',\n label: t('customer_accounts.admin.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'active', label: t('customer_accounts.admin.filters.active', 'Active') },\n { value: 'inactive', label: t('customer_accounts.admin.filters.inactive', 'Inactive') },\n ],\n },\n {\n id: 'roleId',\n label: t('customer_accounts.admin.filters.role', 'Role'),\n type: 'select',\n options: roleOptions,\n },\n ], [roleOptions, t])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const status = filterValues.status\n if (typeof status === 'string' && status.trim()) params.set('status', status)\n const roleId = filterValues.roleId\n if (typeof roleId === 'string' && roleId.trim()) params.set('roleId', roleId)\n return params.toString()\n }, [filterValues, page, pageSize, search])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const fallback: UsersResponse = { items: [], total: 0, totalPages: 1 }\n const payload = await readApiResultOrThrow<UsersResponse>(\n `/api/customer_accounts/admin/users?${queryParams}`,\n undefined,\n { errorMessage: t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users'), fallback },\n )\n if (cancelled) return\n const items = Array.isArray(payload?.items) ? payload.items : []\n setRows(items)\n setTotal(typeof payload?.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload?.totalPages === 'number' ? payload.totalPages : 1)\n } catch (err) {\n if (!cancelled) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [queryParams, reloadToken, t])\n\n const handleToggleActive = React.useCallback(async (user: UserRow) => {\n const nextActive = !user.isActive\n const actionLabel = nextActive\n ? t('customer_accounts.admin.actions.activate', 'Activate')\n : t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.toggleActive', '{{action}} user \"{{name}}\"?', {\n action: actionLabel,\n name: user.displayName || user.email,\n }),\n variant: nextActive ? 'default' : 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ isActive: nextActive }),\n },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.toggleActive', 'Failed to update user status'), 'error')\n return\n }\n flash(\n nextActive\n ? t('customer_accounts.admin.flash.activated', 'User activated')\n : t('customer_accounts.admin.flash.deactivated', 'User deactivated'),\n 'success',\n )\n setReloadToken((token) => token + 1)\n }, { userId: user.id, isActive: nextActive })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.toggleActive', 'Failed to update user status')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleDelete = React.useCallback(async (user: UserRow) => {\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.delete', 'Delete user \"{{name}}\"?', {\n name: user.displayName || user.email,\n }),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n { method: 'DELETE' },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')\n setReloadToken((token) => token + 1)\n }, { id: user.id })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const columns = React.useMemo<ColumnDef<UserRow>[]>(() => {\n const noValue = <span className=\"text-muted-foreground text-sm\">-</span>\n return [\n {\n accessorKey: 'displayName',\n header: t('customer_accounts.admin.columns.displayName', 'Name'),\n cell: ({ row }) => (\n <Link\n href={`/backend/customer_accounts/users/${row.original.id}`}\n className=\"font-medium hover:underline\"\n >\n {row.original.displayName || row.original.email}\n </Link>\n ),\n },\n {\n accessorKey: 'email',\n header: t('customer_accounts.admin.columns.email', 'Email'),\n },\n {\n accessorKey: 'emailVerified',\n header: t('customer_accounts.admin.columns.emailVerified', 'Verified'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.emailVerified\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n {row.original.emailVerified\n ? t('customer_accounts.admin.verified', 'Yes')\n : t('customer_accounts.admin.unverified', 'No')}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('customer_accounts.admin.columns.status', 'Status'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.isActive\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-error-bg text-status-error-text'\n }`}>\n {row.original.isActive\n ? t('customer_accounts.admin.active', 'Active')\n : t('customer_accounts.admin.inactive', 'Inactive')}\n </span>\n ),\n },\n {\n accessorKey: 'lastLoginAt',\n header: t('customer_accounts.admin.columns.lastLogin', 'Last Login'),\n cell: ({ row }) => formatDate(row.original.lastLoginAt, '-') || noValue,\n },\n {\n accessorKey: 'roles',\n header: t('customer_accounts.admin.columns.roles', 'Roles'),\n cell: ({ row }) => {\n const roles = row.original.roles\n if (!roles || !roles.length) return noValue\n return <span className=\"text-sm\">{roles.map((r) => r.name).join(', ')}</span>\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('customer_accounts.admin.columns.createdAt', 'Created'),\n cell: ({ row }) => formatDate(row.original.createdAt, '-'),\n },\n ]\n }, [t])\n\n return (\n <Page>\n <PageBody className=\"space-y-4\">\n <div className=\"rounded-lg border border-status-info-border bg-status-info-bg p-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.title', 'Customer Portal')}\n </h3>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.description', 'Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.')}\n </p>\n <p className=\"mt-1.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.url', 'Portal URL: {url}', {\n url: `${typeof window !== 'undefined' ? window.location.origin : ''}/[org-slug]/portal`,\n })}\n </p>\n <p className=\"mt-0.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.credentials', 'Demo credentials: alice.johnson@example.com / Password123!')}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <Link href=\"/backend/customer_accounts/settings\">\n <Settings className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.openConfiguration', 'Open Configuration')}\n </Link>\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <a href={`${typeof window !== 'undefined' ? window.location.origin : ''}/portal`} target=\"_blank\" rel=\"noopener noreferrer\">\n <Globe className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.open', 'Open Portal')}\n </a>\n </Button>\n </div>\n </div>\n </div>\n <DataTable<UserRow>\n stickyActionsColumn\n title={t('customer_accounts.admin.title', 'Users')}\n actions={(\n <Button onClick={() => setCreateDialogOpen(true)}>\n {t('customer_accounts.admin.actions.createUser', 'Create User')}\n </Button>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('customer_accounts.admin.searchPlaceholder', 'Search by name or email...')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n perspective={{ tableId: extensionPoints.hosts.usersTable.tableId }}\n emptyState={(\n <ListEmptyState\n entityName={t('customer_accounts.admin.title', 'Users')}\n onCreate={() => setCreateDialogOpen(true)}\n createLabel={t('customer_accounts.admin.actions.createUser', 'Create User')}\n />\n )}\n onRowClick={(row) => router.push(`/backend/customer_accounts/users/${row.id}`)}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'view',\n label: t('customer_accounts.admin.actions.view', 'View'),\n onSelect: () => { router.push(`/backend/customer_accounts/users/${row.id}`) },\n },\n {\n id: 'toggle-active',\n label: row.isActive\n ? t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n : t('customer_accounts.admin.actions.activate', 'Activate'),\n onSelect: () => { void handleToggleActive(row) },\n },\n {\n id: 'delete',\n label: t('customer_accounts.admin.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => { void handleDelete(row) },\n },\n ]}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n <CreateUserDialog\n open={createDialogOpen}\n onOpenChange={setCreateDialogOpen}\n roleOptions={roleOptions}\n onCreated={() => setReloadToken((token) => token + 1)}\n onRunMutation={runMutationWithContext}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/customer_accounts/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Globe, Settings } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\n\ntype UserRow = {\n id: string\n displayName: string\n email: string\n emailVerified: boolean\n isActive: boolean\n lastLoginAt: string | null\n roles: Array<{ id: string; name: string; slug: string }>\n createdAt: string\n updatedAt?: string | null\n personEntityId: string | null\n customerEntityId: string | null\n}\n\ntype UsersResponse = {\n items?: UserRow[]\n total?: number\n totalPages?: number\n totalIsCapped?: boolean\n}\n\nfunction formatDate(value: string | null | undefined, fallback: string): string {\n if (!value) return fallback\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return fallback\n return date.toLocaleDateString()\n}\n\nasync function fetchRoleFilterOptions(): Promise<Array<{ value: string; label: string; id: string }>> {\n try {\n const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (!call.ok) return []\n const items = Array.isArray(call.result?.items) ? call.result!.items : []\n return items\n .filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string')\n .map((item) => ({ value: item.id, label: item.name, id: item.id }))\n } catch {\n return []\n }\n}\n\nfunction CreateUserDialog({\n open,\n onOpenChange,\n roleOptions,\n onCreated,\n onRunMutation,\n}: {\n open: boolean\n onOpenChange: (next: boolean) => void\n roleOptions: Array<{ id: string; label: string }>\n onCreated: () => void\n onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>\n}) {\n const t = useT()\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [password, setPassword] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n const resetForm = React.useCallback(() => {\n setEmail('')\n setDisplayName('')\n setPassword('')\n setSelectedRoleIds([])\n }, [])\n\n const handleSubmit = React.useCallback(async (event: React.FormEvent) => {\n event.preventDefault()\n if (!email.trim() || !displayName.trim() || !password.trim()) {\n flash(t('customer_accounts.admin.createUser.error.required', 'Email, name, and password are required'), 'error')\n return\n }\n setIsSubmitting(true)\n try {\n await onRunMutation(async () => {\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: email.trim(),\n displayName: displayName.trim(),\n password,\n roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : undefined,\n }),\n },\n )\n if (!call.ok) {\n flash(call.result?.error || t('customer_accounts.admin.createUser.error.save', 'Failed to create user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.createUser.flash.created', 'User created'), 'success')\n resetForm()\n onOpenChange(false)\n onCreated()\n })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.createUser.error.save', 'Failed to create user')\n flash(message, 'error')\n } finally {\n setIsSubmitting(false)\n }\n }, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n const form = (event.target as HTMLElement).closest('form')\n if (form) form.requestSubmit()\n }\n }, [])\n\n return (\n <Dialog open={open} onOpenChange={(next) => { if (!next) resetForm(); onOpenChange(next) }}>\n <DialogContent className=\"sm:max-w-lg\">\n <DialogHeader>\n <DialogTitle>{t('customer_accounts.admin.createUser.title', 'Create Customer User')}</DialogTitle>\n </DialogHeader>\n <form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-email\">\n {t('customer_accounts.admin.createUser.fields.email', 'Email')}\n </label>\n <EmailInput\n id=\"create-email\"\n required\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.emailPlaceholder', 'user@example.com')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-name\">\n {t('customer_accounts.admin.createUser.fields.displayName', 'Display Name')}\n </label>\n <Input\n id=\"create-name\"\n type=\"text\"\n required\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.displayNamePlaceholder', 'John Doe')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-password\">\n {t('customer_accounts.admin.createUser.fields.password', 'Password')}\n </label>\n <PasswordInput\n id=\"create-password\"\n required\n minLength={8}\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.passwordPlaceholder', 'Min. 8 characters')}\n autoComplete=\"new-password\"\n />\n </div>\n {roleOptions.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium\">{t('customer_accounts.admin.createUser.fields.roles', 'Roles')}</p>\n <div className=\"flex flex-wrap gap-2\">\n {roleOptions.map((role) => {\n const isSelected = selectedRoleIds.includes(role.id)\n return (\n <Button\n key={role.id}\n type=\"button\"\n size=\"2xs\"\n variant={isSelected ? 'secondary' : 'outline'}\n aria-pressed={isSelected}\n onClick={() => setSelectedRoleIds((prev) =>\n prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],\n )}\n className=\"rounded-full\"\n >\n {role.label}\n </Button>\n )\n })}\n </div>\n </div>\n )}\n <div className=\"flex justify-end gap-2 pt-2\">\n <Button type=\"button\" variant=\"outline\" onClick={() => { resetForm(); onOpenChange(false) }}>\n {t('customer_accounts.admin.createUser.actions.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting\n ? t('customer_accounts.admin.createUser.actions.creating', 'Creating...')\n : t('customer_accounts.admin.createUser.actions.create', 'Create User')}\n </Button>\n </div>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default function CustomerAccountsPage() {\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<UserRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(50)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [roleOptions, setRoleOptions] = React.useState<Array<{ value: string; label: string; id: string }>>([])\n const [createDialogOpen, setCreateDialogOpen] = React.useState(false)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n entityType: string\n entityId?: string\n }>({\n contextId: 'customer_accounts:users-list',\n })\n\n const runMutationWithContext = React.useCallback(\n async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {\n return runMutation({\n operation,\n mutationPayload,\n context: { entityType: 'customer_accounts:user' },\n })\n },\n [runMutation],\n )\n\n React.useEffect(() => {\n let cancelled = false\n fetchRoleFilterOptions().then((opts) => {\n if (!cancelled) setRoleOptions(opts)\n })\n return () => { cancelled = true }\n }, [])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'status',\n label: t('customer_accounts.admin.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'active', label: t('customer_accounts.admin.filters.active', 'Active') },\n { value: 'inactive', label: t('customer_accounts.admin.filters.inactive', 'Inactive') },\n ],\n },\n {\n id: 'roleId',\n label: t('customer_accounts.admin.filters.role', 'Role'),\n type: 'select',\n options: roleOptions,\n },\n ], [roleOptions, t])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const status = filterValues.status\n if (typeof status === 'string' && status.trim()) params.set('status', status)\n const roleId = filterValues.roleId\n if (typeof roleId === 'string' && roleId.trim()) params.set('roleId', roleId)\n return params.toString()\n }, [filterValues, page, pageSize, search])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const fallback: UsersResponse = { items: [], total: 0, totalPages: 1 }\n const payload = await readApiResultOrThrow<UsersResponse>(\n `/api/customer_accounts/admin/users?${queryParams}`,\n undefined,\n { errorMessage: t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users'), fallback },\n )\n if (cancelled) return\n const items = Array.isArray(payload?.items) ? payload.items : []\n setRows(items)\n setTotal(typeof payload?.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload?.totalPages === 'number' ? payload.totalPages : 1)\n setTotalIsCapped(payload?.totalIsCapped === true)\n } catch (err) {\n if (!cancelled) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [queryParams, reloadToken, t])\n\n const handleToggleActive = React.useCallback(async (user: UserRow) => {\n const nextActive = !user.isActive\n const actionLabel = nextActive\n ? t('customer_accounts.admin.actions.activate', 'Activate')\n : t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.toggleActive', '{{action}} user \"{{name}}\"?', {\n action: actionLabel,\n name: user.displayName || user.email,\n }),\n variant: nextActive ? 'default' : 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ isActive: nextActive }),\n },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.toggleActive', 'Failed to update user status'), 'error')\n return\n }\n flash(\n nextActive\n ? t('customer_accounts.admin.flash.activated', 'User activated')\n : t('customer_accounts.admin.flash.deactivated', 'User deactivated'),\n 'success',\n )\n setReloadToken((token) => token + 1)\n }, { userId: user.id, isActive: nextActive })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.toggleActive', 'Failed to update user status')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleDelete = React.useCallback(async (user: UserRow) => {\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.delete', 'Delete user \"{{name}}\"?', {\n name: user.displayName || user.email,\n }),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n { method: 'DELETE' },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')\n setReloadToken((token) => token + 1)\n }, { id: user.id })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const columns = React.useMemo<ColumnDef<UserRow>[]>(() => {\n const noValue = <span className=\"text-muted-foreground text-sm\">-</span>\n return [\n {\n accessorKey: 'displayName',\n header: t('customer_accounts.admin.columns.displayName', 'Name'),\n cell: ({ row }) => (\n <Link\n href={`/backend/customer_accounts/users/${row.original.id}`}\n className=\"font-medium hover:underline\"\n >\n {row.original.displayName || row.original.email}\n </Link>\n ),\n },\n {\n accessorKey: 'email',\n header: t('customer_accounts.admin.columns.email', 'Email'),\n },\n {\n accessorKey: 'emailVerified',\n header: t('customer_accounts.admin.columns.emailVerified', 'Verified'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.emailVerified\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n {row.original.emailVerified\n ? t('customer_accounts.admin.verified', 'Yes')\n : t('customer_accounts.admin.unverified', 'No')}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('customer_accounts.admin.columns.status', 'Status'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.isActive\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-error-bg text-status-error-text'\n }`}>\n {row.original.isActive\n ? t('customer_accounts.admin.active', 'Active')\n : t('customer_accounts.admin.inactive', 'Inactive')}\n </span>\n ),\n },\n {\n accessorKey: 'lastLoginAt',\n header: t('customer_accounts.admin.columns.lastLogin', 'Last Login'),\n cell: ({ row }) => formatDate(row.original.lastLoginAt, '-') || noValue,\n },\n {\n accessorKey: 'roles',\n header: t('customer_accounts.admin.columns.roles', 'Roles'),\n cell: ({ row }) => {\n const roles = row.original.roles\n if (!roles || !roles.length) return noValue\n return <span className=\"text-sm\">{roles.map((r) => r.name).join(', ')}</span>\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('customer_accounts.admin.columns.createdAt', 'Created'),\n cell: ({ row }) => formatDate(row.original.createdAt, '-'),\n },\n ]\n }, [t])\n\n return (\n <Page>\n <PageBody className=\"space-y-4\">\n <div className=\"rounded-lg border border-status-info-border bg-status-info-bg p-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.title', 'Customer Portal')}\n </h3>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.description', 'Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.')}\n </p>\n <p className=\"mt-1.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.url', 'Portal URL: {url}', {\n url: `${typeof window !== 'undefined' ? window.location.origin : ''}/[org-slug]/portal`,\n })}\n </p>\n <p className=\"mt-0.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.credentials', 'Demo credentials: alice.johnson@example.com / Password123!')}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <Link href=\"/backend/customer_accounts/settings\">\n <Settings className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.openConfiguration', 'Open Configuration')}\n </Link>\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <a href={`${typeof window !== 'undefined' ? window.location.origin : ''}/portal`} target=\"_blank\" rel=\"noopener noreferrer\">\n <Globe className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.open', 'Open Portal')}\n </a>\n </Button>\n </div>\n </div>\n </div>\n <DataTable<UserRow>\n stickyActionsColumn\n title={t('customer_accounts.admin.title', 'Users')}\n actions={(\n <Button onClick={() => setCreateDialogOpen(true)}>\n {t('customer_accounts.admin.actions.createUser', 'Create User')}\n </Button>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('customer_accounts.admin.searchPlaceholder', 'Search by name or email...')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n perspective={{ tableId: extensionPoints.hosts.usersTable.tableId }}\n emptyState={(\n <ListEmptyState\n entityName={t('customer_accounts.admin.title', 'Users')}\n onCreate={() => setCreateDialogOpen(true)}\n createLabel={t('customer_accounts.admin.actions.createUser', 'Create User')}\n />\n )}\n onRowClick={(row) => router.push(`/backend/customer_accounts/users/${row.id}`)}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'view',\n label: t('customer_accounts.admin.actions.view', 'View'),\n onSelect: () => { router.push(`/backend/customer_accounts/users/${row.id}`) },\n },\n {\n id: 'toggle-active',\n label: row.isActive\n ? t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n : t('customer_accounts.admin.actions.activate', 'Activate'),\n onSelect: () => { void handleToggleActive(row) },\n },\n {\n id: 'delete',\n label: t('customer_accounts.admin.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => { void handleDelete(row) },\n },\n ]}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, totalIsCapped, onPageChange: setPage }}\n isLoading={isLoading}\n />\n <CreateUserDialog\n open={createDialogOpen}\n onOpenChange={setCreateDialogOpen}\n roleOptions={roleOptions}\n onCreated={() => setReloadToken((token) => token + 1)}\n onRunMutation={runMutationWithContext}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,7 +18,7 @@ const listCompaniesInput = z.object({
|
|
|
18
18
|
const listCompaniesTool = defineApiBackedAiTool({
|
|
19
19
|
name: "customers.list_companies",
|
|
20
20
|
displayName: "List companies",
|
|
21
|
-
description:
|
|
21
|
+
description: 'Search / list companies for the caller tenant + organization. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor ("at least N", render it as "N+"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',
|
|
22
22
|
inputSchema: listCompaniesInput,
|
|
23
23
|
requiredFeatures: ["customers.companies.view"],
|
|
24
24
|
toOperation: (input, ctx) => {
|
|
@@ -53,6 +53,9 @@ const listCompaniesTool = defineApiBackedAiTool({
|
|
|
53
53
|
sizeBucket: row.size_bucket ?? row.sizeBucket ?? null
|
|
54
54
|
})),
|
|
55
55
|
total: typeof data.total === "number" ? data.total : 0,
|
|
56
|
+
// A capped count reports a floor: phrase the total as "at least N" and
|
|
57
|
+
// never treat it as proof that pagination is exhausted.
|
|
58
|
+
totalIsCapped: data.totalIsCapped === true,
|
|
56
59
|
limit,
|
|
57
60
|
offset
|
|
58
61
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/ai-tools/companies-pack.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_companies` is now an API-backed wrapper over\n * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_company` to a single\n * in-process call to `GET /api/customers/companies/<id>?include=...` over the\n * documented aggregate detail route. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n */\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\nimport {\n buildRelatedRecords,\n toCustomerListSummary,\n toIso,\n type CustomerRelatedRecords,\n} from './_shared'\n\nconst listCompaniesInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against display name / email / domain. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to companies carrying at least one of these tag ids.'),\n })\n .passthrough()\n\ntype ListCompaniesInput = z.infer<typeof listCompaniesInput>\n\ntype ListCompaniesApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n domain?: string | null\n website_url?: string | null\n websiteUrl?: string | null\n industry?: string | null\n size_bucket?: string | null\n sizeBucket?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListCompaniesApiResponse = {\n items?: ListCompaniesApiItem[]\n total?: number\n}\n\ntype ListCompaniesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listCompaniesTool = defineApiBackedAiTool<\n ListCompaniesInput,\n ListCompaniesApiResponse,\n ListCompaniesOutput\n>({\n name: 'customers.list_companies',\n displayName: 'List companies',\n description:\n 'Search / list companies for the caller tenant + organization. Returns { items, total, limit, offset }.',\n inputSchema: listCompaniesInput,\n requiredFeatures: ['customers.companies.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/companies',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListCompaniesApiResponse\n const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => ({\n ...toCustomerListSummary(row),\n domain: row.domain ?? null,\n websiteUrl: row.website_url ?? row.websiteUrl ?? null,\n industry: row.industry ?? null,\n sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,\n })),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getCompanyInput = z.object({\n companyId: z.string().uuid().describe('Company entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetCompanyInput = z.infer<typeof getCompanyInput>\n\nconst getCompanyTool: CustomersAiToolDefinition = {\n name: 'customers.get_company',\n displayName: 'Get company',\n description:\n 'Fetch a company customer record by id with profile fields and (optionally) notes, activities, deals, people, addresses, tasks, tags, and custom fields. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getCompanyInput,\n requiredFeatures: ['customers.companies.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetCompanyInput = getCompanyInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/companies/${input.companyId}`,\n }\n if (includeRelated) {\n operation.query = {\n include: 'addresses,comments,activities,interactions,deals,todos,people',\n }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, companyId: input.companyId }\n }\n throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const companyRow = (data.company ?? null) as Record<string, unknown> | null\n if (!companyRow) {\n return { found: false as const, companyId: input.companyId }\n }\n const profileRow = (data.profile ?? null) as Record<string, unknown> | null\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: CustomerRelatedRecords | null = null\n if (includeRelated) {\n related = buildRelatedRecords(data, { includePeople: true })\n }\n return {\n found: true as const,\n company: {\n id: companyRow.id,\n displayName: companyRow.displayName ?? null,\n description: companyRow.description ?? null,\n primaryEmail: companyRow.primaryEmail ?? null,\n primaryPhone: companyRow.primaryPhone ?? null,\n status: companyRow.status ?? null,\n lifecycleStage: companyRow.lifecycleStage ?? null,\n source: companyRow.source ?? null,\n ownerUserId: companyRow.ownerUserId ?? null,\n organizationId: companyRow.organizationId ?? null,\n tenantId: companyRow.tenantId ?? null,\n createdAt: toIso(companyRow.createdAt),\n updatedAt: toIso(companyRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n legalName: profileRow.legalName ?? null,\n brandName: profileRow.brandName ?? null,\n domain: profileRow.domain ?? null,\n websiteUrl: profileRow.websiteUrl ?? null,\n industry: profileRow.industry ?? null,\n sizeBucket: profileRow.sizeBucket ?? null,\n annualRevenue: profileRow.annualRevenue ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]\n\nexport default companiesAiTools\n"],
|
|
5
|
-
"mappings": "AAaA,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,OAGK;AACP,SAAS,yBAAoF;AAC7F;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,MAAM,qBAAqB,EACxB,OAAO;AAAA,EACN,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,6FAA6F;AAAA,EACtI,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,+CAA+C;AAAA,EAC3G,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,qCAAqC;AAAA,EACzF,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,SAAS,+DAA+D;AACtH,CAAC,EACA,YAAY;AA4Cf,MAAM,oBAAoB,sBAIxB;AAAA,EACA,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,0BAA0B;AAAA,EAC7C,aAAa,CAAC,OAAO,QAAQ;AAC3B,sBAAkB,GAAsC;AACxD,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI;AAE1C,UAAM,QAAsE;AAAA,MAC1E;AAAA,MACA,UAAU;AAAA,IACZ;AACA,QAAI,MAAM,GAAG,KAAK,EAAG,OAAM,SAAS,MAAM,EAAE,KAAK;AACjD,QAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,EAAG,OAAM,SAAS,MAAM,KAAK,KAAK,GAAG;AAE3E,UAAM,YAAmC;AAAA,MACvC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,aAAa,CAAC,UAAU,UAAU;AAChC,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAQ,SAAS,QAAQ,CAAC;AAChC,UAAM,WAAmC,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;AACnF,WAAO;AAAA,MACL,OAAO,SAAS,IAAI,CAAC,SAAS;AAAA,QAC5B,GAAG,sBAAsB,GAAG;AAAA,QAC5B,QAAQ,IAAI,UAAU;AAAA,QACtB,YAAY,IAAI,eAAe,IAAI,cAAc;AAAA,QACjD,UAAU,IAAI,YAAY;AAAA,QAC1B,YAAY,IAAI,eAAe,IAAI,cAAc;AAAA,MACnD,EAAE;AAAA,MACF,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,
|
|
4
|
+
"sourcesContent": ["/**\n * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_companies` is now an API-backed wrapper over\n * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_company` to a single\n * in-process call to `GET /api/customers/companies/<id>?include=...` over the\n * documented aggregate detail route. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n */\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\nimport {\n buildRelatedRecords,\n toCustomerListSummary,\n toIso,\n type CustomerRelatedRecords,\n} from './_shared'\n\nconst listCompaniesInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against display name / email / domain. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to companies carrying at least one of these tag ids.'),\n })\n .passthrough()\n\ntype ListCompaniesInput = z.infer<typeof listCompaniesInput>\n\ntype ListCompaniesApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n domain?: string | null\n website_url?: string | null\n websiteUrl?: string | null\n industry?: string | null\n size_bucket?: string | null\n sizeBucket?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListCompaniesApiResponse = {\n items?: ListCompaniesApiItem[]\n total?: number\n}\n\ntype ListCompaniesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listCompaniesTool = defineApiBackedAiTool<\n ListCompaniesInput,\n ListCompaniesApiResponse,\n ListCompaniesOutput\n>({\n name: 'customers.list_companies',\n displayName: 'List companies',\n description:\n 'Search / list companies for the caller tenant + organization. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor (\"at least N\", render it as \"N+\"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',\n inputSchema: listCompaniesInput,\n requiredFeatures: ['customers.companies.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/companies',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListCompaniesApiResponse\n const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => ({\n ...toCustomerListSummary(row),\n domain: row.domain ?? null,\n websiteUrl: row.website_url ?? row.websiteUrl ?? null,\n industry: row.industry ?? null,\n sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,\n })),\n total: typeof data.total === 'number' ? data.total : 0,\n // A capped count reports a floor: phrase the total as \"at least N\" and\n // never treat it as proof that pagination is exhausted.\n totalIsCapped: (data as { totalIsCapped?: boolean }).totalIsCapped === true,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getCompanyInput = z.object({\n companyId: z.string().uuid().describe('Company entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetCompanyInput = z.infer<typeof getCompanyInput>\n\nconst getCompanyTool: CustomersAiToolDefinition = {\n name: 'customers.get_company',\n displayName: 'Get company',\n description:\n 'Fetch a company customer record by id with profile fields and (optionally) notes, activities, deals, people, addresses, tasks, tags, and custom fields. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getCompanyInput,\n requiredFeatures: ['customers.companies.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetCompanyInput = getCompanyInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/companies/${input.companyId}`,\n }\n if (includeRelated) {\n operation.query = {\n include: 'addresses,comments,activities,interactions,deals,todos,people',\n }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, companyId: input.companyId }\n }\n throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const companyRow = (data.company ?? null) as Record<string, unknown> | null\n if (!companyRow) {\n return { found: false as const, companyId: input.companyId }\n }\n const profileRow = (data.profile ?? null) as Record<string, unknown> | null\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: CustomerRelatedRecords | null = null\n if (includeRelated) {\n related = buildRelatedRecords(data, { includePeople: true })\n }\n return {\n found: true as const,\n company: {\n id: companyRow.id,\n displayName: companyRow.displayName ?? null,\n description: companyRow.description ?? null,\n primaryEmail: companyRow.primaryEmail ?? null,\n primaryPhone: companyRow.primaryPhone ?? null,\n status: companyRow.status ?? null,\n lifecycleStage: companyRow.lifecycleStage ?? null,\n source: companyRow.source ?? null,\n ownerUserId: companyRow.ownerUserId ?? null,\n organizationId: companyRow.organizationId ?? null,\n tenantId: companyRow.tenantId ?? null,\n createdAt: toIso(companyRow.createdAt),\n updatedAt: toIso(companyRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n legalName: profileRow.legalName ?? null,\n brandName: profileRow.brandName ?? null,\n domain: profileRow.domain ?? null,\n websiteUrl: profileRow.websiteUrl ?? null,\n industry: profileRow.industry ?? null,\n sizeBucket: profileRow.sizeBucket ?? null,\n annualRevenue: profileRow.annualRevenue ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]\n\nexport default companiesAiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -29,7 +29,7 @@ const listDealsInput = z.object({
|
|
|
29
29
|
const listDealsTool = defineApiBackedAiTool({
|
|
30
30
|
name: "customers.list_deals",
|
|
31
31
|
displayName: "List deals",
|
|
32
|
-
description:
|
|
32
|
+
description: 'Search / list deals for the caller tenant + organization. Optional filters include linked person / company / pipeline stage. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor ("at least N", render it as "N+"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',
|
|
33
33
|
inputSchema: listDealsInput,
|
|
34
34
|
requiredFeatures: ["customers.deals.view"],
|
|
35
35
|
toOperation: (input, ctx) => {
|
|
@@ -83,6 +83,9 @@ const listDealsTool = defineApiBackedAiTool({
|
|
|
83
83
|
};
|
|
84
84
|
}),
|
|
85
85
|
total: typeof data.total === "number" ? data.total : 0,
|
|
86
|
+
// A capped count reports a floor: phrase the total as "at least N" and
|
|
87
|
+
// never treat it as proof that pagination is exhausted.
|
|
88
|
+
totalIsCapped: data.totalIsCapped === true,
|
|
86
89
|
limit,
|
|
87
90
|
offset
|
|
88
91
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/ai-tools/deals-pack.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `customers.list_deals` + `customers.get_deal` (Phase 1 WS-C, Step 3.9).\n * `customers.update_deal_stage` mutation tool (Phase 3 WS-C, Step 5.13).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_deals` is now an API-backed wrapper over\n * `GET /api/customers/deals`. Tool name, schema, requiredFeatures, and output\n * shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_deal` to the documented\n * aggregate detail route. The handler issues 1 call without `includeRelated`\n * (`GET /customers/deals/<id>`) and 3 bounded calls with `includeRelated`\n * (deal detail + activities + comments by `dealId`). The 3-call cap matches\n * the spec's residual N+1 budget; deeper aggregation can earn a first-class\n * API later without touching the AI surface.\n */\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerPipelineStage,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n type CustomersToolLoadBeforeSingleRecord,\n} from './types'\n\nfunction resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nconst listDealsInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against deal title / description. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n personId: z.string().uuid().optional().describe('Return only deals linked to this person entity id.'),\n companyId: z.string().uuid().optional().describe('Return only deals linked to this company entity id.'),\n pipelineStageId: z.string().uuid().optional().describe('Return only deals at this pipeline stage.'),\n status: z.string().optional().describe('Filter by deal status (e.g. \"open\", \"won\", \"lost\").'),\n })\n .passthrough()\n\ntype ListDealsInput = z.infer<typeof listDealsInput>\n\ntype ListDealsApiItem = {\n id?: string\n title?: string | null\n description?: string | null\n status?: string | null\n pipeline_id?: string | null\n pipelineId?: string | null\n pipeline_stage_id?: string | null\n pipelineStageId?: string | null\n value_amount?: string | number | null\n valueAmount?: string | number | null\n value_currency?: string | null\n valueCurrency?: string | null\n probability?: number | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n expected_close_at?: string | null\n expectedCloseAt?: string | null\n source?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListDealsApiResponse = {\n items?: ListDealsApiItem[]\n total?: number\n}\n\ntype ListDealsOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listDealsTool = defineApiBackedAiTool<ListDealsInput, ListDealsApiResponse, ListDealsOutput>({\n name: 'customers.list_deals',\n displayName: 'List deals',\n description:\n 'Search / list deals for the caller tenant + organization. Optional filters include linked person / company / pipeline stage.',\n inputSchema: listDealsInput,\n requiredFeatures: ['customers.deals.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.personId) query.personId = input.personId\n if (input.companyId) query.companyId = input.companyId\n if (input.pipelineStageId) query.pipelineStageId = input.pipelineStageId\n if (input.status) query.status = input.status\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/deals',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListDealsApiResponse\n const rawItems: ListDealsApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const expectedCloseRaw = row.expected_close_at ?? row.expectedCloseAt ?? null\n const expectedCloseAt = expectedCloseRaw ? new Date(String(expectedCloseRaw)).toISOString() : null\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n title: row.title ?? null,\n description: row.description ?? null,\n status: row.status ?? null,\n pipelineId: row.pipeline_id ?? row.pipelineId ?? null,\n pipelineStageId: row.pipeline_stage_id ?? row.pipelineStageId ?? null,\n valueAmount: row.value_amount ?? row.valueAmount ?? null,\n valueCurrency: row.value_currency ?? row.valueCurrency ?? null,\n probability: row.probability ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n expectedCloseAt,\n source: row.source ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getDealInput = z.object({\n dealId: z.string().uuid().describe('Deal id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, linked people and companies (each capped at 100).'),\n})\n\ntype GetDealInput = z.infer<typeof getDealInput>\n\nfunction toIsoDeal(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getDealTool: CustomersAiToolDefinition = {\n name: 'customers.get_deal',\n displayName: 'Get deal',\n description:\n 'Fetch a deal by id with fields and (optionally) notes, activities, linked people, and linked companies. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getDealInput,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetDealInput = getDealInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n\n const detailResponse = await runner.run<Record<string, unknown>>({\n method: 'GET',\n path: `/customers/deals/${input.dealId}`,\n })\n if (!detailResponse.success) {\n if (detailResponse.statusCode === 404 || detailResponse.statusCode === 403) {\n return { found: false as const, dealId: input.dealId }\n }\n throw new Error(detailResponse.error ?? `Failed to fetch deal ${input.dealId}`)\n }\n const detail = (detailResponse.data ?? {}) as Record<string, unknown>\n const dealRow = (detail.deal ?? null) as Record<string, unknown> | null\n if (!dealRow) {\n return { found: false as const, dealId: input.dealId }\n }\n const customFields = (detail.customFields ?? {}) as Record<string, unknown>\n const peopleRows = Array.isArray(detail.people) ? (detail.people as Array<Record<string, unknown>>) : []\n const companiesRows = Array.isArray(detail.companies)\n ? (detail.companies as Array<Record<string, unknown>>)\n : []\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const [activitiesResponse, commentsResponse] = await Promise.all([\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/activities',\n query: { dealId: input.dealId, page: 1, pageSize: 100, sortField: 'occurredAt', sortDir: 'desc' },\n }),\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/comments',\n query: { dealId: input.dealId, page: 1, pageSize: 100 },\n }),\n ])\n const activities =\n activitiesResponse.success && Array.isArray(activitiesResponse.data?.items)\n ? (activitiesResponse.data!.items as Array<Record<string, unknown>>)\n : []\n const comments =\n commentsResponse.success && Array.isArray(commentsResponse.data?.items)\n ? (commentsResponse.data!.items as Array<Record<string, unknown>>)\n : []\n\n related = {\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType ?? activity.activity_type ?? null,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIsoDeal(activity.occurredAt ?? activity.occurred_at),\n createdAt: toIsoDeal(activity.createdAt ?? activity.created_at),\n })),\n notes: comments.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? comment.author_user_id ?? null,\n createdAt: toIsoDeal(comment.createdAt ?? comment.created_at),\n })),\n people: peopleRows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n if (!id) return null\n const subtitle = typeof person.subtitle === 'string' ? person.subtitle : null\n const label = typeof person.label === 'string' ? person.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: subtitle && subtitle.includes('@') ? subtitle : null,\n primaryPhone: subtitle && !subtitle.includes('@') ? subtitle : null,\n participantRole: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } => value !== null,\n ),\n companies: companiesRows\n .map((company) => {\n if (!company || typeof company !== 'object') return null\n const id = typeof company.id === 'string' ? company.id : null\n if (!id) return null\n const label = typeof company.label === 'string' ? company.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: null as string | null,\n primaryPhone: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } => value !== null,\n ),\n }\n }\n\n return {\n found: true as const,\n deal: {\n id: dealRow.id,\n title: typeof dealRow.title === 'string' ? dealRow.title : '',\n description: dealRow.description ?? null,\n status: dealRow.status ?? null,\n pipelineId: dealRow.pipelineId ?? null,\n pipelineStageId: dealRow.pipelineStageId ?? null,\n valueAmount: dealRow.valueAmount ?? null,\n valueCurrency: dealRow.valueCurrency ?? null,\n probability: dealRow.probability ?? null,\n ownerUserId: dealRow.ownerUserId ?? null,\n expectedCloseAt: toIsoDeal(dealRow.expectedCloseAt),\n source: dealRow.source ?? null,\n organizationId: dealRow.organizationId ?? null,\n tenantId: dealRow.tenantId ?? null,\n createdAt: toIsoDeal(dealRow.createdAt),\n updatedAt: toIsoDeal(dealRow.updatedAt),\n },\n customFields,\n related,\n }\n },\n}\n\n/**\n * Mutation tool: move a deal to a different pipeline stage. Step 5.13 \u2014 first\n * mutation-capable flow on the pending-action contract.\n *\n * Accepts either `toPipelineStageId` (UUID \u2014 preferred, tenant-scoped stage\n * record) or `toStage` (free-form string that maps to `CustomerDeal.status`\n * for pipeline roots like `open`/`won`/`lost`). Exactly one must be provided.\n *\n * The handler delegates to the existing `customers.deals.update` command so\n * all side effects (audit log, `customers.deal.updated` event, query index\n * refresh, notifications) stay identical to a direct API write.\n */\n// LLMs frequently emit `\"\"` for \"not provided\" \u2014 coerce blanks (and surrounding\n// whitespace) to `undefined` BEFORE the per-field validators run so the\n// `.uuid()` check on `toPipelineStageId` does not blow up on an empty string\n// the caller actually meant as \"skip this field\".\nconst blankToUndefined = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length === 0 ? undefined : trimmed\n}\n\nconst updateDealStageInput = z\n .object({\n dealId: z.string().uuid().describe('Deal id (UUID) to update.'),\n toPipelineStageId: z\n .preprocess(blankToUndefined, z.string().uuid().optional())\n .describe('Target pipeline stage id (UUID). Preferred \u2014 tenant-scoped stage record.'),\n toStage: z\n .preprocess(blankToUndefined, z.string().min(1).max(50).optional())\n .describe(\n 'Target status slug (e.g. \"open\", \"won\", \"lost\"). Used when the deal does not belong to a managed pipeline.',\n ),\n })\n .refine(\n (value) => Boolean(value.toPipelineStageId) !== Boolean(value.toStage),\n {\n message: 'Provide exactly one of toPipelineStageId or toStage.',\n path: ['toPipelineStageId'],\n },\n )\n\ntype UpdateDealStageInput = z.infer<typeof updateDealStageInput>\n\nfunction recordVersionFromUpdatedAt(updatedAt: Date | null | undefined): string | null {\n if (!updatedAt) return null\n const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt)\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n}\n\nfunction titleStatus(value: string | null | undefined): string | undefined {\n if (!value) return undefined\n return value\n .split(/[_\\s-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nasync function loadDealWithStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n dealId: string,\n): Promise<CustomerDeal | null> {\n const where: Record<string, unknown> = { id: dealId, tenantId, deletedAt: null }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const deal = await findOneWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n where as FilterQuery<CustomerDeal>,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!deal || deal.tenantId !== tenantId) return null\n if (ctx.organizationId && deal.organizationId !== ctx.organizationId) return null\n return deal\n}\n\nasync function loadPipelineStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n stageId: string,\n organizationId: string,\n): Promise<CustomerPipelineStage | null> {\n return findOneWithDecryption<CustomerPipelineStage>(\n em,\n CustomerPipelineStage,\n {\n id: stageId,\n tenantId,\n organizationId,\n },\n undefined,\n buildScope(ctx, tenantId),\n )\n}\n\nconst updateDealStageTool: CustomersAiToolDefinition = {\n name: 'customers.update_deal_stage',\n displayName: 'Update deal stage',\n description:\n 'Move a deal to a different pipeline stage (by stage id) or change its top-level status (e.g. \"open\", \"won\", \"lost\"). Mutation tool \u2014 flows through the AI pending-action approval gate.',\n inputSchema: updateDealStageInput as z.ZodType<unknown>,\n requiredFeatures: ['customers.deals.manage'],\n tags: ['write', 'customers'],\n isMutation: true,\n loadBeforeRecord: async (rawInput, ctx): Promise<CustomersToolLoadBeforeSingleRecord | null> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) return null\n let afterStatus = deal.status ?? null\n let afterPipelineStageId = deal.pipelineStageId ?? null\n let afterPipelineStageLabel = deal.pipelineStage ?? null\n if (input.toPipelineStageId) {\n const organizationId = deal.organizationId ?? ctx.organizationId ?? null\n const stage = organizationId\n ? await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n : null\n afterPipelineStageId = input.toPipelineStageId\n afterPipelineStageLabel = stage?.label ?? input.toPipelineStageId\n } else if (input.toStage) {\n afterStatus = input.toStage\n }\n const beforeStatus = deal.status ?? null\n const beforePipelineStageId = deal.pipelineStageId ?? null\n const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId\n return {\n recordId: deal.id,\n entityType: 'customers.deal',\n recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),\n before: {\n status: beforeStatus,\n pipelineStageId: beforePipelineStageId,\n },\n after: {\n status: afterStatus,\n pipelineStageId: afterPipelineStageId,\n },\n display: {\n fieldLabels: {\n status: 'Status',\n pipelineStageId: 'Pipeline stage',\n },\n before: {\n ...(beforeStatus ? { status: titleStatus(beforeStatus) } : {}),\n ...(beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}),\n },\n after: {\n ...(afterStatus ? { status: titleStatus(afterStatus) } : {}),\n ...(afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}),\n },\n },\n }\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) {\n throw new Error(`Deal \"${input.dealId}\" is not accessible to the caller.`)\n }\n const organizationId = deal.organizationId\n if (!organizationId) {\n throw new Error(`Deal \"${input.dealId}\" has no organization scope.`)\n }\n\n const before = {\n status: deal.status ?? null,\n pipelineStage: deal.pipelineStage ?? null,\n pipelineStageId: deal.pipelineStageId ?? null,\n }\n\n const body: Record<string, unknown> = {\n id: deal.id,\n tenantId,\n organizationId,\n }\n if (input.toPipelineStageId) {\n const stage = await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n if (!stage) {\n throw new Error('Pipeline stage not found.')\n }\n body.pipelineStageId = input.toPipelineStageId\n } else if (input.toStage) {\n body.status = input.toStage\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run({\n method: 'PUT',\n path: '/customers/deals',\n body,\n })\n if (!response.success) {\n throw new Error(response.error ?? `Failed to update deal \"${deal.id}\"`)\n }\n\n const after = await loadDealWithStage(em, ctx, tenantId, deal.id)\n return {\n recordId: deal.id,\n commandName: 'customers.deals.update',\n before,\n after: after\n ? {\n status: after.status ?? null,\n pipelineStage: after.pipelineStage ?? null,\n pipelineStageId: after.pipelineStageId ?? null,\n }\n : null,\n }\n },\n}\n\nexport const dealsAiTools: CustomersAiToolDefinition[] = [listDealsTool, getDealTool, updateDealStageTool]\n\nexport default dealsAiTools\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * `customers.list_deals` + `customers.get_deal` (Phase 1 WS-C, Step 3.9).\n * `customers.update_deal_stage` mutation tool (Phase 3 WS-C, Step 5.13).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_deals` is now an API-backed wrapper over\n * `GET /api/customers/deals`. Tool name, schema, requiredFeatures, and output\n * shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_deal` to the documented\n * aggregate detail route. The handler issues 1 call without `includeRelated`\n * (`GET /customers/deals/<id>`) and 3 bounded calls with `includeRelated`\n * (deal detail + activities + comments by `dealId`). The 3-call cap matches\n * the spec's residual N+1 budget; deeper aggregation can earn a first-class\n * API later without touching the AI surface.\n */\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerPipelineStage,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n type CustomersToolLoadBeforeSingleRecord,\n} from './types'\n\nfunction resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nconst listDealsInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against deal title / description. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n personId: z.string().uuid().optional().describe('Return only deals linked to this person entity id.'),\n companyId: z.string().uuid().optional().describe('Return only deals linked to this company entity id.'),\n pipelineStageId: z.string().uuid().optional().describe('Return only deals at this pipeline stage.'),\n status: z.string().optional().describe('Filter by deal status (e.g. \"open\", \"won\", \"lost\").'),\n })\n .passthrough()\n\ntype ListDealsInput = z.infer<typeof listDealsInput>\n\ntype ListDealsApiItem = {\n id?: string\n title?: string | null\n description?: string | null\n status?: string | null\n pipeline_id?: string | null\n pipelineId?: string | null\n pipeline_stage_id?: string | null\n pipelineStageId?: string | null\n value_amount?: string | number | null\n valueAmount?: string | number | null\n value_currency?: string | null\n valueCurrency?: string | null\n probability?: number | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n expected_close_at?: string | null\n expectedCloseAt?: string | null\n source?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListDealsApiResponse = {\n items?: ListDealsApiItem[]\n total?: number\n}\n\ntype ListDealsOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listDealsTool = defineApiBackedAiTool<ListDealsInput, ListDealsApiResponse, ListDealsOutput>({\n name: 'customers.list_deals',\n displayName: 'List deals',\n description:\n 'Search / list deals for the caller tenant + organization. Optional filters include linked person / company / pipeline stage. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor (\"at least N\", render it as \"N+\"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',\n inputSchema: listDealsInput,\n requiredFeatures: ['customers.deals.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.personId) query.personId = input.personId\n if (input.companyId) query.companyId = input.companyId\n if (input.pipelineStageId) query.pipelineStageId = input.pipelineStageId\n if (input.status) query.status = input.status\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/deals',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListDealsApiResponse\n const rawItems: ListDealsApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const expectedCloseRaw = row.expected_close_at ?? row.expectedCloseAt ?? null\n const expectedCloseAt = expectedCloseRaw ? new Date(String(expectedCloseRaw)).toISOString() : null\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n title: row.title ?? null,\n description: row.description ?? null,\n status: row.status ?? null,\n pipelineId: row.pipeline_id ?? row.pipelineId ?? null,\n pipelineStageId: row.pipeline_stage_id ?? row.pipelineStageId ?? null,\n valueAmount: row.value_amount ?? row.valueAmount ?? null,\n valueCurrency: row.value_currency ?? row.valueCurrency ?? null,\n probability: row.probability ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n expectedCloseAt,\n source: row.source ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n // A capped count reports a floor: phrase the total as \"at least N\" and\n // never treat it as proof that pagination is exhausted.\n totalIsCapped: (data as { totalIsCapped?: boolean }).totalIsCapped === true,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getDealInput = z.object({\n dealId: z.string().uuid().describe('Deal id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, linked people and companies (each capped at 100).'),\n})\n\ntype GetDealInput = z.infer<typeof getDealInput>\n\nfunction toIsoDeal(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getDealTool: CustomersAiToolDefinition = {\n name: 'customers.get_deal',\n displayName: 'Get deal',\n description:\n 'Fetch a deal by id with fields and (optionally) notes, activities, linked people, and linked companies. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getDealInput,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetDealInput = getDealInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n\n const detailResponse = await runner.run<Record<string, unknown>>({\n method: 'GET',\n path: `/customers/deals/${input.dealId}`,\n })\n if (!detailResponse.success) {\n if (detailResponse.statusCode === 404 || detailResponse.statusCode === 403) {\n return { found: false as const, dealId: input.dealId }\n }\n throw new Error(detailResponse.error ?? `Failed to fetch deal ${input.dealId}`)\n }\n const detail = (detailResponse.data ?? {}) as Record<string, unknown>\n const dealRow = (detail.deal ?? null) as Record<string, unknown> | null\n if (!dealRow) {\n return { found: false as const, dealId: input.dealId }\n }\n const customFields = (detail.customFields ?? {}) as Record<string, unknown>\n const peopleRows = Array.isArray(detail.people) ? (detail.people as Array<Record<string, unknown>>) : []\n const companiesRows = Array.isArray(detail.companies)\n ? (detail.companies as Array<Record<string, unknown>>)\n : []\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const [activitiesResponse, commentsResponse] = await Promise.all([\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/activities',\n query: { dealId: input.dealId, page: 1, pageSize: 100, sortField: 'occurredAt', sortDir: 'desc' },\n }),\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/comments',\n query: { dealId: input.dealId, page: 1, pageSize: 100 },\n }),\n ])\n const activities =\n activitiesResponse.success && Array.isArray(activitiesResponse.data?.items)\n ? (activitiesResponse.data!.items as Array<Record<string, unknown>>)\n : []\n const comments =\n commentsResponse.success && Array.isArray(commentsResponse.data?.items)\n ? (commentsResponse.data!.items as Array<Record<string, unknown>>)\n : []\n\n related = {\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType ?? activity.activity_type ?? null,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIsoDeal(activity.occurredAt ?? activity.occurred_at),\n createdAt: toIsoDeal(activity.createdAt ?? activity.created_at),\n })),\n notes: comments.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? comment.author_user_id ?? null,\n createdAt: toIsoDeal(comment.createdAt ?? comment.created_at),\n })),\n people: peopleRows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n if (!id) return null\n const subtitle = typeof person.subtitle === 'string' ? person.subtitle : null\n const label = typeof person.label === 'string' ? person.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: subtitle && subtitle.includes('@') ? subtitle : null,\n primaryPhone: subtitle && !subtitle.includes('@') ? subtitle : null,\n participantRole: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } => value !== null,\n ),\n companies: companiesRows\n .map((company) => {\n if (!company || typeof company !== 'object') return null\n const id = typeof company.id === 'string' ? company.id : null\n if (!id) return null\n const label = typeof company.label === 'string' ? company.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: null as string | null,\n primaryPhone: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } => value !== null,\n ),\n }\n }\n\n return {\n found: true as const,\n deal: {\n id: dealRow.id,\n title: typeof dealRow.title === 'string' ? dealRow.title : '',\n description: dealRow.description ?? null,\n status: dealRow.status ?? null,\n pipelineId: dealRow.pipelineId ?? null,\n pipelineStageId: dealRow.pipelineStageId ?? null,\n valueAmount: dealRow.valueAmount ?? null,\n valueCurrency: dealRow.valueCurrency ?? null,\n probability: dealRow.probability ?? null,\n ownerUserId: dealRow.ownerUserId ?? null,\n expectedCloseAt: toIsoDeal(dealRow.expectedCloseAt),\n source: dealRow.source ?? null,\n organizationId: dealRow.organizationId ?? null,\n tenantId: dealRow.tenantId ?? null,\n createdAt: toIsoDeal(dealRow.createdAt),\n updatedAt: toIsoDeal(dealRow.updatedAt),\n },\n customFields,\n related,\n }\n },\n}\n\n/**\n * Mutation tool: move a deal to a different pipeline stage. Step 5.13 \u2014 first\n * mutation-capable flow on the pending-action contract.\n *\n * Accepts either `toPipelineStageId` (UUID \u2014 preferred, tenant-scoped stage\n * record) or `toStage` (free-form string that maps to `CustomerDeal.status`\n * for pipeline roots like `open`/`won`/`lost`). Exactly one must be provided.\n *\n * The handler delegates to the existing `customers.deals.update` command so\n * all side effects (audit log, `customers.deal.updated` event, query index\n * refresh, notifications) stay identical to a direct API write.\n */\n// LLMs frequently emit `\"\"` for \"not provided\" \u2014 coerce blanks (and surrounding\n// whitespace) to `undefined` BEFORE the per-field validators run so the\n// `.uuid()` check on `toPipelineStageId` does not blow up on an empty string\n// the caller actually meant as \"skip this field\".\nconst blankToUndefined = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length === 0 ? undefined : trimmed\n}\n\nconst updateDealStageInput = z\n .object({\n dealId: z.string().uuid().describe('Deal id (UUID) to update.'),\n toPipelineStageId: z\n .preprocess(blankToUndefined, z.string().uuid().optional())\n .describe('Target pipeline stage id (UUID). Preferred \u2014 tenant-scoped stage record.'),\n toStage: z\n .preprocess(blankToUndefined, z.string().min(1).max(50).optional())\n .describe(\n 'Target status slug (e.g. \"open\", \"won\", \"lost\"). Used when the deal does not belong to a managed pipeline.',\n ),\n })\n .refine(\n (value) => Boolean(value.toPipelineStageId) !== Boolean(value.toStage),\n {\n message: 'Provide exactly one of toPipelineStageId or toStage.',\n path: ['toPipelineStageId'],\n },\n )\n\ntype UpdateDealStageInput = z.infer<typeof updateDealStageInput>\n\nfunction recordVersionFromUpdatedAt(updatedAt: Date | null | undefined): string | null {\n if (!updatedAt) return null\n const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt)\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n}\n\nfunction titleStatus(value: string | null | undefined): string | undefined {\n if (!value) return undefined\n return value\n .split(/[_\\s-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nasync function loadDealWithStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n dealId: string,\n): Promise<CustomerDeal | null> {\n const where: Record<string, unknown> = { id: dealId, tenantId, deletedAt: null }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const deal = await findOneWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n where as FilterQuery<CustomerDeal>,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!deal || deal.tenantId !== tenantId) return null\n if (ctx.organizationId && deal.organizationId !== ctx.organizationId) return null\n return deal\n}\n\nasync function loadPipelineStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n stageId: string,\n organizationId: string,\n): Promise<CustomerPipelineStage | null> {\n return findOneWithDecryption<CustomerPipelineStage>(\n em,\n CustomerPipelineStage,\n {\n id: stageId,\n tenantId,\n organizationId,\n },\n undefined,\n buildScope(ctx, tenantId),\n )\n}\n\nconst updateDealStageTool: CustomersAiToolDefinition = {\n name: 'customers.update_deal_stage',\n displayName: 'Update deal stage',\n description:\n 'Move a deal to a different pipeline stage (by stage id) or change its top-level status (e.g. \"open\", \"won\", \"lost\"). Mutation tool \u2014 flows through the AI pending-action approval gate.',\n inputSchema: updateDealStageInput as z.ZodType<unknown>,\n requiredFeatures: ['customers.deals.manage'],\n tags: ['write', 'customers'],\n isMutation: true,\n loadBeforeRecord: async (rawInput, ctx): Promise<CustomersToolLoadBeforeSingleRecord | null> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) return null\n let afterStatus = deal.status ?? null\n let afterPipelineStageId = deal.pipelineStageId ?? null\n let afterPipelineStageLabel = deal.pipelineStage ?? null\n if (input.toPipelineStageId) {\n const organizationId = deal.organizationId ?? ctx.organizationId ?? null\n const stage = organizationId\n ? await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n : null\n afterPipelineStageId = input.toPipelineStageId\n afterPipelineStageLabel = stage?.label ?? input.toPipelineStageId\n } else if (input.toStage) {\n afterStatus = input.toStage\n }\n const beforeStatus = deal.status ?? null\n const beforePipelineStageId = deal.pipelineStageId ?? null\n const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId\n return {\n recordId: deal.id,\n entityType: 'customers.deal',\n recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),\n before: {\n status: beforeStatus,\n pipelineStageId: beforePipelineStageId,\n },\n after: {\n status: afterStatus,\n pipelineStageId: afterPipelineStageId,\n },\n display: {\n fieldLabels: {\n status: 'Status',\n pipelineStageId: 'Pipeline stage',\n },\n before: {\n ...(beforeStatus ? { status: titleStatus(beforeStatus) } : {}),\n ...(beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}),\n },\n after: {\n ...(afterStatus ? { status: titleStatus(afterStatus) } : {}),\n ...(afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}),\n },\n },\n }\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) {\n throw new Error(`Deal \"${input.dealId}\" is not accessible to the caller.`)\n }\n const organizationId = deal.organizationId\n if (!organizationId) {\n throw new Error(`Deal \"${input.dealId}\" has no organization scope.`)\n }\n\n const before = {\n status: deal.status ?? null,\n pipelineStage: deal.pipelineStage ?? null,\n pipelineStageId: deal.pipelineStageId ?? null,\n }\n\n const body: Record<string, unknown> = {\n id: deal.id,\n tenantId,\n organizationId,\n }\n if (input.toPipelineStageId) {\n const stage = await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n if (!stage) {\n throw new Error('Pipeline stage not found.')\n }\n body.pipelineStageId = input.toPipelineStageId\n } else if (input.toStage) {\n body.status = input.toStage\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run({\n method: 'PUT',\n path: '/customers/deals',\n body,\n })\n if (!response.success) {\n throw new Error(response.error ?? `Failed to update deal \"${deal.id}\"`)\n }\n\n const after = await loadDealWithStage(em, ctx, tenantId, deal.id)\n return {\n recordId: deal.id,\n commandName: 'customers.deals.update',\n before,\n after: after\n ? {\n status: after.status ?? null,\n pipelineStage: after.pipelineStage ?? null,\n pipelineStageId: after.pipelineStageId ?? null,\n }\n : null,\n }\n },\n}\n\nexport const dealsAiTools: CustomersAiToolDefinition[] = [listDealsTool, getDealTool, updateDealStageTool]\n\nexport default dealsAiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -26,7 +26,7 @@ const listPeopleInput = z.object({
|
|
|
26
26
|
const listPeopleTool = defineApiBackedAiTool({
|
|
27
27
|
name: "customers.list_people",
|
|
28
28
|
displayName: "List people",
|
|
29
|
-
description:
|
|
29
|
+
description: 'Search / list people (CRM persons) for the caller tenant + organization. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor ("at least N", render it as "N+"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',
|
|
30
30
|
inputSchema: listPeopleInput,
|
|
31
31
|
requiredFeatures: ["customers.people.view"],
|
|
32
32
|
toOperation: async (input, ctx) => {
|
|
@@ -73,6 +73,9 @@ const listPeopleTool = defineApiBackedAiTool({
|
|
|
73
73
|
return {
|
|
74
74
|
items: rawItems.map((row) => toCustomerListSummary(row)),
|
|
75
75
|
total: typeof data.total === "number" ? data.total : 0,
|
|
76
|
+
// A capped count reports a floor: phrase the total as "at least N" and
|
|
77
|
+
// never treat it as proof that pagination is exhausted.
|
|
78
|
+
totalIsCapped: data.totalIsCapped === true,
|
|
76
79
|
limit,
|
|
77
80
|
offset
|
|
78
81
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/ai-tools/people-pack.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `customers.list_people` + `customers.get_person` (Phase 1 WS-C, Step 3.9).\n *\n * Read-only tools scoped to `ctx.tenantId` / `ctx.organizationId` that wrap\n * the existing customers query engine + encryption helpers. Mutation tools\n * are deferred to Step 5.13+ under the pending-action contract.\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_people` is now an API-backed wrapper over\n * `GET /api/customers/people`. The `companyId` AI input has no inclusion\n * equivalent on the route (the route exposes `excludeLinkedCompanyId` only)\n * so it is pre-resolved against `CustomerPersonProfile.company` and threaded\n * through the route's `ids` filter.\n *\n * Phase 3c of the same spec migrates `customers.get_person` to a single\n * in-process call to `GET /api/customers/people/<id>?include=...` (the\n * documented aggregate detail route). Tool name, schema, requiredFeatures,\n * and output shape are unchanged.\n */\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerPersonProfile,\n} from '../data/entities'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\nimport {\n buildRelatedRecords,\n buildScope,\n resolveEm,\n toCustomerListSummary,\n toIso,\n type CustomerRelatedRecords,\n} from './_shared'\n\nconst NIL_UUID = '00000000-0000-0000-0000-000000000000'\n\nconst listPeopleInput = z\n .object({\n q: z.string().trim().optional().describe('Optional search text matched against display name / email / phone. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to persons carrying at least one of these tag ids.'),\n companyId: z.string().uuid().optional().describe('Restrict to persons linked to the given company entity.'),\n })\n .passthrough()\n\ntype ListPeopleInput = z.infer<typeof listPeopleInput>\n\ntype ListPeopleApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListPeopleApiResponse = {\n items?: ListPeopleApiItem[]\n total?: number\n}\n\ntype ListPeopleOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listPeopleTool = defineApiBackedAiTool<ListPeopleInput, ListPeopleApiResponse, ListPeopleOutput>({\n name: 'customers.list_people',\n displayName: 'List people',\n description:\n 'Search / list people (CRM persons) for the caller tenant + organization. Returns { items, total, limit, offset }.',\n inputSchema: listPeopleInput,\n requiredFeatures: ['customers.people.view'],\n toOperation: async (input, ctx) => {\n const { tenantId } = assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n if (input.companyId) {\n const em = resolveEm(ctx)\n const profiles = await findWithDecryption<CustomerPersonProfile>(\n em,\n CustomerPersonProfile,\n { tenantId, company: input.companyId } as never,\n undefined,\n buildScope(ctx, tenantId),\n )\n const ids = profiles\n .map((profile) => {\n const entity = (profile as { entity?: unknown }).entity\n if (!entity) return null\n if (typeof entity === 'string') return entity\n const candidate = (entity as { id?: unknown }).id\n return typeof candidate === 'string' ? candidate : null\n })\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n // Empty match \u2014 feed a non-existent uuid so the route returns\n // { items: [], total: 0 } without us bypassing the API.\n query.ids = ids.length ? ids.join(',') : NIL_UUID\n }\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/people',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListPeopleApiResponse\n const rawItems: ListPeopleApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => toCustomerListSummary(row)),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getPersonInput = z.object({\n personId: z.string().uuid().describe('Person entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetPersonInput = z.infer<typeof getPersonInput>\n\ntype ApiPersonDetailRow = Record<string, unknown> | null | undefined\n\nconst getPersonTool: CustomersAiToolDefinition = {\n name: 'customers.get_person',\n displayName: 'Get person',\n description:\n 'Fetch a person customer record by id with profile fields and (optionally) notes, activities, deals, addresses, tasks, tags, and custom fields. Returns { found: false } when the record is outside tenant/org scope or missing.',\n inputSchema: getPersonInput,\n requiredFeatures: ['customers.people.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: GetPersonInput = getPersonInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/people/${input.personId}`,\n }\n if (includeRelated) {\n operation.query = { include: 'addresses,comments,activities,interactions,deals,todos' }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, personId: input.personId }\n }\n throw new Error(response.error ?? `Failed to fetch person ${input.personId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const personRow = (data.person ?? null) as ApiPersonDetailRow\n if (!personRow) {\n return { found: false as const, personId: input.personId }\n }\n const profileRow = (data.profile ?? null) as ApiPersonDetailRow\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: CustomerRelatedRecords | null = null\n if (includeRelated) {\n related = buildRelatedRecords(data)\n }\n\n return {\n found: true as const,\n person: {\n id: personRow.id,\n displayName: personRow.displayName ?? null,\n description: personRow.description ?? null,\n primaryEmail: personRow.primaryEmail ?? null,\n primaryPhone: personRow.primaryPhone ?? null,\n status: personRow.status ?? null,\n lifecycleStage: personRow.lifecycleStage ?? null,\n source: personRow.source ?? null,\n ownerUserId: personRow.ownerUserId ?? null,\n organizationId: personRow.organizationId ?? null,\n tenantId: personRow.tenantId ?? null,\n createdAt: toIso(personRow.createdAt),\n updatedAt: toIso(personRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n firstName: profileRow.firstName ?? null,\n lastName: profileRow.lastName ?? null,\n preferredName: profileRow.preferredName ?? null,\n jobTitle: profileRow.jobTitle ?? null,\n department: profileRow.department ?? null,\n seniority: profileRow.seniority ?? null,\n timezone: profileRow.timezone ?? null,\n linkedInUrl: profileRow.linkedInUrl ?? null,\n twitterUrl: profileRow.twitterUrl ?? null,\n companyEntityId: profileRow.companyEntityId ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const peopleAiTools: CustomersAiToolDefinition[] = [listPeopleTool, getPersonTool]\n\nexport default peopleAiTools\n"],
|
|
5
|
-
"mappings": "AAmBA,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,OAGK;AACP,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,OACK;AACP,SAAS,yBAAoF;AAC7F;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,MAAM,WAAW;AAEjB,MAAM,kBAAkB,EACrB,OAAO;AAAA,EACN,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,qGAAqG;AAAA,EAC9I,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,+CAA+C;AAAA,EAC3G,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,qCAAqC;AAAA,EACzF,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,SAAS,6DAA6D;AAAA,EAClH,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,yDAAyD;AAC5G,CAAC,EACA,YAAY;AAsCf,MAAM,iBAAiB,sBAAgF;AAAA,EACrG,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,uBAAuB;AAAA,EAC1C,aAAa,OAAO,OAAO,QAAQ;AACjC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAsC;AAC7E,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI;AAE1C,UAAM,QAAsE;AAAA,MAC1E;AAAA,MACA,UAAU;AAAA,IACZ;AACA,QAAI,MAAM,GAAG,KAAK,EAAG,OAAM,SAAS,MAAM,EAAE,KAAK;AACjD,QAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,EAAG,OAAM,SAAS,MAAM,KAAK,KAAK,GAAG;AAE3E,QAAI,MAAM,WAAW;AACnB,YAAM,KAAK,UAAU,GAAG;AACxB,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,QACA,EAAE,UAAU,SAAS,MAAM,UAAU;AAAA,QACrC;AAAA,QACA,WAAW,KAAK,QAAQ;AAAA,MAC1B;AACA,YAAM,MAAM,SACT,IAAI,CAAC,YAAY;AAChB,cAAM,SAAU,QAAiC;AACjD,YAAI,CAAC,OAAQ,QAAO;AACpB,YAAI,OAAO,WAAW,SAAU,QAAO;AACvC,cAAM,YAAa,OAA4B;AAC/C,eAAO,OAAO,cAAc,WAAW,YAAY;AAAA,MACrD,CAAC,EACA,OAAO,CAAC,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC;AAGnF,YAAM,MAAM,IAAI,SAAS,IAAI,KAAK,GAAG,IAAI;AAAA,IAC3C;AAEA,UAAM,YAAmC;AAAA,MACvC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,aAAa,CAAC,UAAU,UAAU;AAChC,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAQ,SAAS,QAAQ,CAAC;AAChC,UAAM,WAAgC,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;AAChF,WAAO;AAAA,MACL,OAAO,SAAS,IAAI,CAAC,QAAQ,sBAAsB,GAAG,CAAC;AAAA,MACvD,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,
|
|
4
|
+
"sourcesContent": ["/**\n * `customers.list_people` + `customers.get_person` (Phase 1 WS-C, Step 3.9).\n *\n * Read-only tools scoped to `ctx.tenantId` / `ctx.organizationId` that wrap\n * the existing customers query engine + encryption helpers. Mutation tools\n * are deferred to Step 5.13+ under the pending-action contract.\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_people` is now an API-backed wrapper over\n * `GET /api/customers/people`. The `companyId` AI input has no inclusion\n * equivalent on the route (the route exposes `excludeLinkedCompanyId` only)\n * so it is pre-resolved against `CustomerPersonProfile.company` and threaded\n * through the route's `ids` filter.\n *\n * Phase 3c of the same spec migrates `customers.get_person` to a single\n * in-process call to `GET /api/customers/people/<id>?include=...` (the\n * documented aggregate detail route). Tool name, schema, requiredFeatures,\n * and output shape are unchanged.\n */\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerPersonProfile,\n} from '../data/entities'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\nimport {\n buildRelatedRecords,\n buildScope,\n resolveEm,\n toCustomerListSummary,\n toIso,\n type CustomerRelatedRecords,\n} from './_shared'\n\nconst NIL_UUID = '00000000-0000-0000-0000-000000000000'\n\nconst listPeopleInput = z\n .object({\n q: z.string().trim().optional().describe('Optional search text matched against display name / email / phone. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to persons carrying at least one of these tag ids.'),\n companyId: z.string().uuid().optional().describe('Restrict to persons linked to the given company entity.'),\n })\n .passthrough()\n\ntype ListPeopleInput = z.infer<typeof listPeopleInput>\n\ntype ListPeopleApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListPeopleApiResponse = {\n items?: ListPeopleApiItem[]\n total?: number\n}\n\ntype ListPeopleOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listPeopleTool = defineApiBackedAiTool<ListPeopleInput, ListPeopleApiResponse, ListPeopleOutput>({\n name: 'customers.list_people',\n displayName: 'List people',\n description:\n 'Search / list people (CRM persons) for the caller tenant + organization. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor (\"at least N\", render it as \"N+\"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',\n inputSchema: listPeopleInput,\n requiredFeatures: ['customers.people.view'],\n toOperation: async (input, ctx) => {\n const { tenantId } = assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n if (input.companyId) {\n const em = resolveEm(ctx)\n const profiles = await findWithDecryption<CustomerPersonProfile>(\n em,\n CustomerPersonProfile,\n { tenantId, company: input.companyId } as never,\n undefined,\n buildScope(ctx, tenantId),\n )\n const ids = profiles\n .map((profile) => {\n const entity = (profile as { entity?: unknown }).entity\n if (!entity) return null\n if (typeof entity === 'string') return entity\n const candidate = (entity as { id?: unknown }).id\n return typeof candidate === 'string' ? candidate : null\n })\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n // Empty match \u2014 feed a non-existent uuid so the route returns\n // { items: [], total: 0 } without us bypassing the API.\n query.ids = ids.length ? ids.join(',') : NIL_UUID\n }\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/people',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListPeopleApiResponse\n const rawItems: ListPeopleApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => toCustomerListSummary(row)),\n total: typeof data.total === 'number' ? data.total : 0,\n // A capped count reports a floor: phrase the total as \"at least N\" and\n // never treat it as proof that pagination is exhausted.\n totalIsCapped: (data as { totalIsCapped?: boolean }).totalIsCapped === true,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getPersonInput = z.object({\n personId: z.string().uuid().describe('Person entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetPersonInput = z.infer<typeof getPersonInput>\n\ntype ApiPersonDetailRow = Record<string, unknown> | null | undefined\n\nconst getPersonTool: CustomersAiToolDefinition = {\n name: 'customers.get_person',\n displayName: 'Get person',\n description:\n 'Fetch a person customer record by id with profile fields and (optionally) notes, activities, deals, addresses, tasks, tags, and custom fields. Returns { found: false } when the record is outside tenant/org scope or missing.',\n inputSchema: getPersonInput,\n requiredFeatures: ['customers.people.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: GetPersonInput = getPersonInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/people/${input.personId}`,\n }\n if (includeRelated) {\n operation.query = { include: 'addresses,comments,activities,interactions,deals,todos' }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, personId: input.personId }\n }\n throw new Error(response.error ?? `Failed to fetch person ${input.personId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const personRow = (data.person ?? null) as ApiPersonDetailRow\n if (!personRow) {\n return { found: false as const, personId: input.personId }\n }\n const profileRow = (data.profile ?? null) as ApiPersonDetailRow\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: CustomerRelatedRecords | null = null\n if (includeRelated) {\n related = buildRelatedRecords(data)\n }\n\n return {\n found: true as const,\n person: {\n id: personRow.id,\n displayName: personRow.displayName ?? null,\n description: personRow.description ?? null,\n primaryEmail: personRow.primaryEmail ?? null,\n primaryPhone: personRow.primaryPhone ?? null,\n status: personRow.status ?? null,\n lifecycleStage: personRow.lifecycleStage ?? null,\n source: personRow.source ?? null,\n ownerUserId: personRow.ownerUserId ?? null,\n organizationId: personRow.organizationId ?? null,\n tenantId: personRow.tenantId ?? null,\n createdAt: toIso(personRow.createdAt),\n updatedAt: toIso(personRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n firstName: profileRow.firstName ?? null,\n lastName: profileRow.lastName ?? null,\n preferredName: profileRow.preferredName ?? null,\n jobTitle: profileRow.jobTitle ?? null,\n department: profileRow.department ?? null,\n seniority: profileRow.seniority ?? null,\n timezone: profileRow.timezone ?? null,\n linkedInUrl: profileRow.linkedInUrl ?? null,\n twitterUrl: profileRow.twitterUrl ?? null,\n companyEntityId: profileRow.companyEntityId ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const peopleAiTools: CustomersAiToolDefinition[] = [listPeopleTool, getPersonTool]\n\nexport default peopleAiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|