@open-mercato/core 0.6.6-develop.6314.1.c7b8291aa2 → 0.6.6-develop.6330.1.a261878aa8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +7 -12
  3. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  4. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +5 -2
  5. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  6. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -1
  7. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  8. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +2 -1
  9. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js.map +2 -2
  10. package/dist/modules/customers/ai-tools/_shared.js +158 -0
  11. package/dist/modules/customers/ai-tools/_shared.js.map +7 -0
  12. package/dist/modules/customers/ai-tools/companies-pack.js +15 -121
  13. package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
  14. package/dist/modules/customers/ai-tools/people-pack.js +9 -107
  15. package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
  16. package/dist/modules/customers/components/calendar/CalendarScreen.js +1 -0
  17. package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
  18. package/dist/modules/customers/components/calendar/EventPeekPopover.js +12 -1
  19. package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
  20. package/dist/modules/customers/components/calendar/TimeGrid.js +3 -0
  21. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  22. package/dist/modules/customers/components/calendar/types.js.map +1 -1
  23. package/dist/modules/customers/components/detail/EntityTagsDialog.js +76 -67
  24. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +3 -3
  25. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +4 -3
  26. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  27. package/dist/modules/data_sync/api/runs.js +1 -0
  28. package/dist/modules/data_sync/api/runs.js.map +2 -2
  29. package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
  30. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  31. package/dist/modules/data_sync/data/validators.js +1 -0
  32. package/dist/modules/data_sync/data/validators.js.map +2 -2
  33. package/dist/modules/data_sync/lib/sync-run-service.js +20 -0
  34. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  35. package/dist/modules/entities/api/records.js +11 -1
  36. package/dist/modules/entities/api/records.js.map +2 -2
  37. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +17 -10
  38. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  39. package/dist/modules/messages/api/unread-count/route.js +32 -0
  40. package/dist/modules/messages/api/unread-count/route.js.map +2 -2
  41. package/dist/modules/query_index/components/QueryIndexesTable.js +13 -5
  42. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  43. package/dist/modules/shipping_carriers/i18n/en.js +11 -1
  44. package/dist/modules/shipping_carriers/i18n/en.js.map +2 -2
  45. package/dist/modules/shipping_carriers/i18n/pl.js +11 -1
  46. package/dist/modules/shipping_carriers/i18n/pl.js.map +2 -2
  47. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js +25 -0
  48. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js.map +7 -0
  49. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js +3 -17
  50. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js.map +2 -2
  51. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +19 -14
  52. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  53. package/package.json +7 -7
  54. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +7 -10
  55. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -7
  56. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +3 -2
  57. package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +3 -2
  58. package/src/modules/customers/ai-tools/_shared.ts +270 -0
  59. package/src/modules/customers/ai-tools/companies-pack.ts +17 -157
  60. package/src/modules/customers/ai-tools/people-pack.ts +11 -133
  61. package/src/modules/customers/components/calendar/CalendarScreen.tsx +1 -0
  62. package/src/modules/customers/components/calendar/EventPeekPopover.tsx +29 -11
  63. package/src/modules/customers/components/calendar/TimeGrid.tsx +3 -0
  64. package/src/modules/customers/components/calendar/types.ts +1 -0
  65. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +89 -71
  66. package/src/modules/customers/i18n/de.json +1 -0
  67. package/src/modules/customers/i18n/en.json +1 -0
  68. package/src/modules/customers/i18n/es.json +1 -0
  69. package/src/modules/customers/i18n/pl.json +1 -0
  70. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +6 -3
  71. package/src/modules/data_sync/api/runs.ts +1 -0
  72. package/src/modules/data_sync/backend/data-sync/page.tsx +3 -1
  73. package/src/modules/data_sync/data/validators.ts +1 -0
  74. package/src/modules/data_sync/i18n/de.json +1 -0
  75. package/src/modules/data_sync/i18n/en.json +1 -0
  76. package/src/modules/data_sync/i18n/es.json +1 -0
  77. package/src/modules/data_sync/i18n/pl.json +1 -0
  78. package/src/modules/data_sync/lib/sync-run-service.ts +23 -0
  79. package/src/modules/entities/api/records.ts +17 -1
  80. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +25 -12
  81. package/src/modules/messages/api/unread-count/route.ts +40 -0
  82. package/src/modules/query_index/components/QueryIndexesTable.tsx +17 -15
  83. package/src/modules/shipping_carriers/i18n/de.json +11 -1
  84. package/src/modules/shipping_carriers/i18n/en.json +11 -1
  85. package/src/modules/shipping_carriers/i18n/en.ts +10 -0
  86. package/src/modules/shipping_carriers/i18n/es.json +11 -1
  87. package/src/modules/shipping_carriers/i18n/pl.json +11 -1
  88. package/src/modules/shipping_carriers/i18n/pl.ts +10 -0
  89. package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.tsx +35 -0
  90. package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx +3 -22
  91. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +30 -13
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx"],
4
- "sourcesContent": ["import type { InjectionColumnWidget } from '@open-mercato/shared/modules/widgets/injection'\n\nconst SHIPPING_STATUS_COLORS: Record<string, string> = {\n pending: 'bg-gray-100 text-gray-700',\n label_created: 'bg-slate-100 text-slate-700',\n picked_up: 'bg-sky-100 text-sky-700',\n in_transit: 'bg-blue-100 text-blue-700',\n out_for_delivery: 'bg-indigo-100 text-indigo-700',\n delivered: 'bg-green-100 text-green-700',\n returned: 'bg-amber-100 text-amber-700',\n cancelled: 'bg-red-100 text-red-700',\n failed_delivery: 'bg-orange-100 text-orange-700',\n}\n\nfunction formatShippingStatusLabel(value: string): string {\n return value.replace(/_/g, ' ')\n}\n\nconst widget: InjectionColumnWidget = {\n metadata: {\n id: 'shipping_carriers.injection.tracking-status-badge',\n priority: 45,\n },\n columns: [\n {\n id: 'carrier_status_badge',\n header: 'shipping_carriers.column.shippingStatus',\n accessorKey: '_carrier.status',\n sortable: false,\n cell: ({ getValue }) => {\n const value = getValue()\n if (typeof value !== 'string' || value.length === 0) return null\n const colors = SHIPPING_STATUS_COLORS[value] ?? 'bg-gray-100 text-gray-700'\n return (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${colors}`}>\n {formatShippingStatusLabel(value)}\n </span>\n )\n },\n },\n ],\n}\n\nexport default widget\n"],
5
- "mappings": "AAkCU;AAhCV,MAAM,yBAAiD;AAAA,EACrD,SAAS;AAAA,EACT,eAAe;AAAA,EACf,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,UAAU;AAAA,EACV,WAAW;AAAA,EACX,iBAAiB;AACnB;AAEA,SAAS,0BAA0B,OAAuB;AACxD,SAAO,MAAM,QAAQ,MAAM,GAAG;AAChC;AAEA,MAAM,SAAgC;AAAA,EACpC,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,UAAU;AAAA,EACZ;AAAA,EACA,SAAS;AAAA,IACP;AAAA,MACE,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,UAAU;AAAA,MACV,MAAM,CAAC,EAAE,SAAS,MAAM;AACtB,cAAM,QAAQ,SAAS;AACvB,YAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,cAAM,SAAS,uBAAuB,KAAK,KAAK;AAChD,eACE,oBAAC,UAAK,WAAW,yEAAyE,MAAM,IAC7F,oCAA0B,KAAK,GAClC;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;",
4
+ "sourcesContent": ["import * as React from 'react'\nimport type { InjectionColumnWidget } from '@open-mercato/shared/modules/widgets/injection'\nimport ShippingStatusBadgeWidget from './widget.client'\n\nconst widget: InjectionColumnWidget = {\n metadata: {\n id: 'shipping_carriers.injection.tracking-status-badge',\n priority: 45,\n },\n columns: [\n {\n id: 'carrier_status_badge',\n header: 'shipping_carriers.column.shippingStatus',\n accessorKey: '_carrier.status',\n sortable: false,\n cell: ({ getValue }) => {\n const value = getValue()\n if (typeof value !== 'string' || value.length === 0) return null\n return React.createElement(ShippingStatusBadgeWidget, { status: value })\n },\n },\n ],\n}\n\nexport default widget\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;AAEvB,OAAO,+BAA+B;AAEtC,MAAM,SAAgC;AAAA,EACpC,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,UAAU;AAAA,EACZ;AAAA,EACA,SAAS;AAAA,IACP;AAAA,MACE,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,UAAU;AAAA,MACV,MAAM,CAAC,EAAE,SAAS,MAAM;AACtB,cAAM,QAAQ,SAAS;AACvB,YAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,eAAO,MAAM,cAAc,2BAA2B,EAAE,QAAQ,MAAM,CAAC;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
@@ -3,8 +3,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useParams } from "next/navigation";
5
5
  import Link from "next/link";
6
+ import { useQuery } from "@tanstack/react-query";
6
7
  import { ExternalLink, Languages } from "lucide-react";
7
8
  import { useT } from "@open-mercato/shared/lib/i18n/context";
9
+ import { hasAllFeatures } from "@open-mercato/shared/lib/auth/featureMatch";
10
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
8
11
  import { Button } from "@open-mercato/ui/primitives/button";
9
12
  import {
10
13
  Drawer,
@@ -16,21 +19,23 @@ import {
16
19
  } from "@open-mercato/ui/primitives/drawer";
17
20
  import { TranslationManager } from "../../../components/TranslationManager.js";
18
21
  import { extractRecordId } from "../../../lib/extract-record-id.js";
22
+ const TRANSLATION_ACCESS_FEATURES = ["translations.view"];
19
23
  function useTranslationAccess() {
20
- const [hasAccess, setHasAccess] = React.useState(false);
21
- React.useEffect(() => {
22
- let mounted = true;
23
- const nativeFetch = window.__omOriginalFetch || fetch;
24
- nativeFetch("/api/translations/locales", { credentials: "include" }).then((res) => {
25
- if (mounted) setHasAccess(res.ok);
26
- }).catch(() => {
27
- if (mounted) setHasAccess(false);
28
- });
29
- return () => {
30
- mounted = false;
31
- };
32
- }, []);
33
- return hasAccess;
24
+ const { data } = useQuery({
25
+ queryKey: ["translations", "widget-access", ...TRANSLATION_ACCESS_FEATURES],
26
+ queryFn: async () => {
27
+ const res = await apiCall("/api/auth/feature-check", {
28
+ method: "POST",
29
+ headers: { "content-type": "application/json" },
30
+ body: JSON.stringify({ features: TRANSLATION_ACCESS_FEATURES })
31
+ });
32
+ if (!res.ok) return false;
33
+ return hasAllFeatures(TRANSLATION_ACCESS_FEATURES, res.result?.granted ?? []);
34
+ },
35
+ staleTime: 5 * 60 * 1e3,
36
+ retry: false
37
+ });
38
+ return data === true;
34
39
  }
35
40
  function TranslationWidget({ context, data }) {
36
41
  const entityType = context?.entityId;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/translations/widgets/injection/translation-manager/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useParams } from 'next/navigation'\nimport Link from 'next/link'\nimport { ExternalLink, Languages } from 'lucide-react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Drawer,\n DrawerBody,\n DrawerContent,\n DrawerDescription,\n DrawerHeader,\n DrawerTitle,\n} from '@open-mercato/ui/primitives/drawer'\nimport { TranslationManager } from '../../../components/TranslationManager'\nimport { extractRecordId } from '../../../lib/extract-record-id'\n\ntype WidgetContext = { entityId?: string; recordId?: string }\ntype WidgetData = Record<string, unknown> & { id?: string | number }\n\nfunction useTranslationAccess(): boolean {\n const [hasAccess, setHasAccess] = React.useState(false)\n React.useEffect(() => {\n let mounted = true\n // Use the original fetch to bypass the global apiFetch wrapper\n // that redirects to login on 403. This lets us gracefully hide the widget\n // when the user lacks translations.view instead of crashing the page.\n const nativeFetch = ((window as any).__omOriginalFetch as typeof fetch) || fetch\n nativeFetch('/api/translations/locales', { credentials: 'include' })\n .then((res) => { if (mounted) setHasAccess(res.ok) })\n .catch(() => { if (mounted) setHasAccess(false) })\n return () => { mounted = false }\n }, [])\n return hasAccess\n}\n\nexport default function TranslationWidget({ context, data }: InjectionWidgetComponentProps<WidgetContext, WidgetData>) {\n const entityType = context?.entityId\n const params = useParams()\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const hasAccess = useTranslationAccess()\n\n const contextRecordId = typeof context?.recordId === 'string' && context.recordId.trim().length > 0\n ? context.recordId.trim()\n : undefined\n const dataRecordId = data?.id === undefined || data.id === null ? undefined : String(data.id)\n const routeRecordId = params ? extractRecordId(params as Record<string, string | string[]>) : undefined\n const recordId = contextRecordId ?? dataRecordId ?? routeRecordId\n const canRender = Boolean(entityType && recordId && hasAccess)\n\n if (!canRender) return null\n\n return (\n <Drawer open={open} onOpenChange={setOpen}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setOpen(true)}\n aria-label={t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n title={t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n >\n <Languages className=\"size-4\" />\n </Button>\n <DrawerContent className=\"max-w-4xl\">\n <DrawerHeader>\n <DrawerTitle>\n {t('translations.widgets.translationManager.groupLabel', 'Translations')}\n </DrawerTitle>\n <DrawerDescription>\n {t(\n 'translations.widgets.translationManager.groupDescription',\n 'Manage translations for this record across supported locales.',\n )}\n </DrawerDescription>\n </DrawerHeader>\n <DrawerBody>\n <TranslationManager\n mode=\"embedded\"\n compact\n entityType={entityType}\n recordId={recordId}\n baseValues={data}\n />\n </DrawerBody>\n {/* Custom footer: 2 inline link affordances (not action buttons),\n wrap-friendly + left-aligned. DS DrawerFooter layouts assume\n buttons in the trailing slot, so we hand-roll the row with\n the DrawerFooter padding convention (`px-6 pt-4 pb-5`). */}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 px-6 pt-4 pb-5\">\n <Link\n href={`/backend/entities/system/${encodeURIComponent(entityType!)}`}\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <Languages className=\"size-3\" />\n {t('translations.widgets.translationManager.customFieldLabels', 'Custom fields translations')}\n <ExternalLink className=\"size-2.5\" />\n </Link>\n <Link\n href=\"/backend/config/translations\"\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <Languages className=\"size-3\" />\n {t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n <ExternalLink className=\"size-2.5\" />\n </Link>\n </div>\n </DrawerContent>\n </Drawer>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useParams } from 'next/navigation'\nimport Link from 'next/link'\nimport { useQuery } from '@tanstack/react-query'\nimport { ExternalLink, Languages } from 'lucide-react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Drawer,\n DrawerBody,\n DrawerContent,\n DrawerDescription,\n DrawerHeader,\n DrawerTitle,\n} from '@open-mercato/ui/primitives/drawer'\nimport { TranslationManager } from '../../../components/TranslationManager'\nimport { extractRecordId } from '../../../lib/extract-record-id'\n\ntype WidgetContext = { entityId?: string; recordId?: string }\ntype WidgetData = Record<string, unknown> & { id?: string | number }\n\ntype FeatureCheckResponse = {\n ok: boolean\n granted: string[]\n userId: string\n}\n\nconst TRANSLATION_ACCESS_FEATURES = ['translations.view']\n\n// Probe access through the shared apiCall/React Query path so multiple injected\n// widget instances dedupe a single request via the query key. feature-check\n// always answers 200 with the granted feature list, so a user without\n// translations.view is hidden gracefully \u2014 no login redirect, no forbidden flash.\nfunction useTranslationAccess(): boolean {\n const { data } = useQuery<boolean>({\n queryKey: ['translations', 'widget-access', ...TRANSLATION_ACCESS_FEATURES],\n queryFn: async () => {\n const res = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: TRANSLATION_ACCESS_FEATURES }),\n })\n if (!res.ok) return false\n return hasAllFeatures(TRANSLATION_ACCESS_FEATURES, res.result?.granted ?? [])\n },\n staleTime: 5 * 60 * 1000,\n retry: false,\n })\n return data === true\n}\n\nexport default function TranslationWidget({ context, data }: InjectionWidgetComponentProps<WidgetContext, WidgetData>) {\n const entityType = context?.entityId\n const params = useParams()\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const hasAccess = useTranslationAccess()\n\n const contextRecordId = typeof context?.recordId === 'string' && context.recordId.trim().length > 0\n ? context.recordId.trim()\n : undefined\n const dataRecordId = data?.id === undefined || data.id === null ? undefined : String(data.id)\n const routeRecordId = params ? extractRecordId(params as Record<string, string | string[]>) : undefined\n const recordId = contextRecordId ?? dataRecordId ?? routeRecordId\n const canRender = Boolean(entityType && recordId && hasAccess)\n\n if (!canRender) return null\n\n return (\n <Drawer open={open} onOpenChange={setOpen}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setOpen(true)}\n aria-label={t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n title={t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n >\n <Languages className=\"size-4\" />\n </Button>\n <DrawerContent className=\"max-w-4xl\">\n <DrawerHeader>\n <DrawerTitle>\n {t('translations.widgets.translationManager.groupLabel', 'Translations')}\n </DrawerTitle>\n <DrawerDescription>\n {t(\n 'translations.widgets.translationManager.groupDescription',\n 'Manage translations for this record across supported locales.',\n )}\n </DrawerDescription>\n </DrawerHeader>\n <DrawerBody>\n <TranslationManager\n mode=\"embedded\"\n compact\n entityType={entityType}\n recordId={recordId}\n baseValues={data}\n />\n </DrawerBody>\n {/* Custom footer: 2 inline link affordances (not action buttons),\n wrap-friendly + left-aligned. DS DrawerFooter layouts assume\n buttons in the trailing slot, so we hand-roll the row with\n the DrawerFooter padding convention (`px-6 pt-4 pb-5`). */}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 px-6 pt-4 pb-5\">\n <Link\n href={`/backend/entities/system/${encodeURIComponent(entityType!)}`}\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <Languages className=\"size-3\" />\n {t('translations.widgets.translationManager.customFieldLabels', 'Custom fields translations')}\n <ExternalLink className=\"size-2.5\" />\n </Link>\n <Link\n href=\"/backend/config/translations\"\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <Languages className=\"size-3\" />\n {t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n <ExternalLink className=\"size-2.5\" />\n </Link>\n </div>\n </DrawerContent>\n </Drawer>\n )\n}\n"],
5
+ "mappings": ";AAmFQ,cAGA,YAHA;AAjFR,YAAY,WAAW;AACvB,SAAS,iBAAiB;AAC1B,OAAO,UAAU;AACjB,SAAS,gBAAgB;AACzB,SAAS,cAAc,iBAAiB;AAExC,SAAS,YAAY;AACrB,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAWhC,MAAM,8BAA8B,CAAC,mBAAmB;AAMxD,SAAS,uBAAgC;AACvC,QAAM,EAAE,KAAK,IAAI,SAAkB;AAAA,IACjC,UAAU,CAAC,gBAAgB,iBAAiB,GAAG,2BAA2B;AAAA,IAC1E,SAAS,YAAY;AACnB,YAAM,MAAM,MAAM,QAA8B,2BAA2B;AAAA,QACzE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,4BAA4B,CAAC;AAAA,MAChE,CAAC;AACD,UAAI,CAAC,IAAI,GAAI,QAAO;AACpB,aAAO,eAAe,6BAA6B,IAAI,QAAQ,WAAW,CAAC,CAAC;AAAA,IAC9E;AAAA,IACA,WAAW,IAAI,KAAK;AAAA,IACpB,OAAO;AAAA,EACT,CAAC;AACD,SAAO,SAAS;AAClB;AAEe,SAAR,kBAAmC,EAAE,SAAS,KAAK,GAA6D;AACrH,QAAM,aAAa,SAAS;AAC5B,QAAM,SAAS,UAAU;AACzB,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,YAAY,qBAAqB;AAEvC,QAAM,kBAAkB,OAAO,SAAS,aAAa,YAAY,QAAQ,SAAS,KAAK,EAAE,SAAS,IAC9F,QAAQ,SAAS,KAAK,IACtB;AACJ,QAAM,eAAe,MAAM,OAAO,UAAa,KAAK,OAAO,OAAO,SAAY,OAAO,KAAK,EAAE;AAC5F,QAAM,gBAAgB,SAAS,gBAAgB,MAA2C,IAAI;AAC9F,QAAM,WAAW,mBAAmB,gBAAgB;AACpD,QAAM,YAAY,QAAQ,cAAc,YAAY,SAAS;AAE7D,MAAI,CAAC,UAAW,QAAO;AAEvB,SACE,qBAAC,UAAO,MAAY,cAAc,SAChC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS,MAAM,QAAQ,IAAI;AAAA,QAC3B,cAAY,EAAE,uDAAuD,qBAAqB;AAAA,QAC1F,OAAO,EAAE,uDAAuD,qBAAqB;AAAA,QAErF,8BAAC,aAAU,WAAU,UAAS;AAAA;AAAA,IAChC;AAAA,IACA,qBAAC,iBAAc,WAAU,aACvB;AAAA,2BAAC,gBACC;AAAA,4BAAC,eACE,YAAE,sDAAsD,cAAc,GACzE;AAAA,QACA,oBAAC,qBACE;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF;AAAA,SACF;AAAA,MACA,oBAAC,cACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAO;AAAA,UACP;AAAA,UACA;AAAA,UACA,YAAY;AAAA;AAAA,MACd,GACF;AAAA,MAKA,qBAAC,SAAI,WAAU,iDACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,4BAA4B,mBAAmB,UAAW,CAAC;AAAA,YACjE,WAAU;AAAA,YAEV;AAAA,kCAAC,aAAU,WAAU,UAAS;AAAA,cAC7B,EAAE,6DAA6D,4BAA4B;AAAA,cAC5F,oBAAC,gBAAa,WAAU,YAAW;AAAA;AAAA;AAAA,QACrC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YAEV;AAAA,kCAAC,aAAU,WAAU,UAAS;AAAA,cAC7B,EAAE,uDAAuD,qBAAqB;AAAA,cAC/E,oBAAC,gBAAa,WAAU,YAAW;AAAA;AAAA;AAAA,QACrC;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6314.1.c7b8291aa2",
3
+ "version": "0.6.6-develop.6330.1.a261878aa8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -246,16 +246,16 @@
246
246
  "zod": "^4.4.3"
247
247
  },
248
248
  "peerDependencies": {
249
- "@open-mercato/ai-assistant": "0.6.6-develop.6314.1.c7b8291aa2",
250
- "@open-mercato/shared": "0.6.6-develop.6314.1.c7b8291aa2",
251
- "@open-mercato/ui": "0.6.6-develop.6314.1.c7b8291aa2",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6330.1.a261878aa8",
250
+ "@open-mercato/shared": "0.6.6-develop.6330.1.a261878aa8",
251
+ "@open-mercato/ui": "0.6.6-develop.6330.1.a261878aa8",
252
252
  "react": "^19.0.0",
253
253
  "react-dom": "^19.0.0"
254
254
  },
255
255
  "devDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6314.1.c7b8291aa2",
257
- "@open-mercato/shared": "0.6.6-develop.6314.1.c7b8291aa2",
258
- "@open-mercato/ui": "0.6.6-develop.6314.1.c7b8291aa2",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6330.1.a261878aa8",
257
+ "@open-mercato/shared": "0.6.6-develop.6330.1.a261878aa8",
258
+ "@open-mercato/ui": "0.6.6-develop.6330.1.a261878aa8",
259
259
  "@testing-library/dom": "^10.4.1",
260
260
  "@testing-library/jest-dom": "^6.9.1",
261
261
  "@testing-library/react": "^16.3.1",
@@ -7,6 +7,7 @@ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
8
8
  import type { CrudField, CrudFormGroup, CrudFormGroupComponentProps } from '@open-mercato/ui/backend/CrudForm'
9
9
  import { Button } from '@open-mercato/ui/primitives/button'
10
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
10
11
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
11
12
  import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
12
13
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
@@ -106,12 +107,9 @@ function PortalPermissionsEditor({ values, setValue }: CrudFormGroupComponentPro
106
107
  <div className="flex items-center justify-between border-b px-4 py-3">
107
108
  <span className="text-sm font-semibold">{t(group.labelKey, group.fallback)}</span>
108
109
  <label className="flex items-center gap-2 text-xs text-muted-foreground">
109
- <input
110
- type="checkbox"
111
- checked={allSelected}
112
- ref={(el) => { if (el) el.indeterminate = someSelected && !allSelected }}
113
- onChange={() => handleGroupToggle(groupFeatures)}
114
- className="rounded border-border"
110
+ <Checkbox
111
+ checked={allSelected ? true : (someSelected ? 'indeterminate' : false)}
112
+ onCheckedChange={() => handleGroupToggle(groupFeatures)}
115
113
  />
116
114
  {t('customer_accounts.admin.roleDetail.selectAll', 'Select all')}
117
115
  </label>
@@ -121,11 +119,10 @@ function PortalPermissionsEditor({ values, setValue }: CrudFormGroupComponentPro
121
119
  const feature = PORTAL_FEATURES.find((portalFeature) => portalFeature.id === featureId)
122
120
  return (
123
121
  <label key={featureId} className="flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors">
124
- <input
125
- type="checkbox"
122
+ <Checkbox
126
123
  checked={features.includes(featureId)}
127
- onChange={() => handleFeatureToggle(featureId)}
128
- className="mt-0.5 rounded border-border"
124
+ onCheckedChange={() => handleFeatureToggle(featureId)}
125
+ className="mt-0.5"
129
126
  />
130
127
  <div className="space-y-0.5">
131
128
  <div className="text-sm font-medium">{feature ? t(feature.labelKey, feature.fallback) : featureId}</div>
@@ -191,20 +191,19 @@ function CreateUserDialog({
191
191
  {roleOptions.map((role) => {
192
192
  const isSelected = selectedRoleIds.includes(role.id)
193
193
  return (
194
- <button
194
+ <Button
195
195
  key={role.id}
196
196
  type="button"
197
+ size="2xs"
198
+ variant={isSelected ? 'secondary' : 'outline'}
199
+ aria-pressed={isSelected}
197
200
  onClick={() => setSelectedRoleIds((prev) =>
198
201
  prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],
199
202
  )}
200
- className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${
201
- isSelected
202
- ? 'border-primary bg-primary/10 text-primary'
203
- : 'border-border bg-background text-muted-foreground hover:bg-muted'
204
- }`}
203
+ className="rounded-full"
205
204
  >
206
205
  {role.label}
207
- </button>
206
+ </Button>
208
207
  )
209
208
  })}
210
209
  </div>
@@ -8,6 +8,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
8
8
  import { Button } from '@open-mercato/ui/primitives/button'
9
9
  import { Input } from '@open-mercato/ui/primitives/input'
10
10
  import { EmailInput } from '@open-mercato/ui/primitives/email-input'
11
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
11
12
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
12
13
 
13
14
  interface AccountStatusData {
@@ -305,9 +306,9 @@ export default function AccountStatusWidget({ context }: AccountStatusProps) {
305
306
  <div className="space-y-1 text-sm">
306
307
  <div className="flex justify-between">
307
308
  <span className="text-muted-foreground">{t('common.status', 'Status')}</span>
308
- <span className={data.isActive ? 'text-green-600' : 'text-red-600'}>
309
+ <StatusBadge variant={data.isActive ? 'success' : 'error'} dot>
309
310
  {data.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}
310
- </span>
311
+ </StatusBadge>
311
312
  </div>
312
313
  <div className="flex justify-between">
313
314
  <span className="text-muted-foreground">{t('common.email', 'Email')}</span>
@@ -3,6 +3,7 @@
3
3
  import { useQuery } from '@tanstack/react-query'
4
4
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
6
7
 
7
8
  interface CompanyUser {
8
9
  id: string
@@ -59,9 +60,9 @@ export default function CompanyUsersWidget({ context }: CompanyUsersProps) {
59
60
  <div className="text-xs text-muted-foreground">{user.email}</div>
60
61
  </div>
61
62
  <div className="flex items-center gap-2">
62
- <span className={`text-xs ${user.isActive ? 'text-green-600' : 'text-red-600'}`}>
63
+ <StatusBadge variant={user.isActive ? 'success' : 'error'} dot>
63
64
  {user.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}
64
- </span>
65
+ </StatusBadge>
65
66
  <a
66
67
  href={`/backend/customer_accounts/users/${user.id}`}
67
68
  className="text-xs text-primary hover:underline"
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Shared helpers for customers AI tool packs.
3
+ *
4
+ * Mirrors the catalog precedent (`packages/core/src/modules/catalog/ai-tools/_shared.ts`):
5
+ * the company and people packs previously carried byte-for-byte-identical copies
6
+ * of the date-to-ISO helper, the `resolveEm` / `buildScope` accessors, the
7
+ * list-row → summary mapper, and the related-records builder
8
+ * (addresses / activities / notes / tasks / interactions / tags / deals, plus a
9
+ * companies-only `people` mapper). Centralizing them here gives both packs one
10
+ * source of truth so a change to the related-records output shape no longer has
11
+ * to be applied — and kept in sync — in two places.
12
+ *
13
+ * This is a pure internal refactor: tool names, input schemas, `requiredFeatures`,
14
+ * and emitted output shapes stay identical.
15
+ */
16
+ import type { EntityManager } from '@mikro-orm/postgresql'
17
+ import type { AiToolExecutionContext } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'
18
+ import type { CustomersToolContext } from './types'
19
+
20
+ export function toIso(value: unknown): string | null {
21
+ if (!value) return null
22
+ const dt = value instanceof Date ? value : new Date(String(value))
23
+ if (Number.isNaN(dt.getTime())) return null
24
+ return dt.toISOString()
25
+ }
26
+
27
+ export function resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {
28
+ return ctx.container.resolve<EntityManager>('em')
29
+ }
30
+
31
+ export function buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {
32
+ return {
33
+ tenantId,
34
+ organizationId: ctx.organizationId,
35
+ }
36
+ }
37
+
38
+ /* -------------------------------------------------------------------------- */
39
+ /* List-row summary mapper */
40
+ /* -------------------------------------------------------------------------- */
41
+
42
+ export type CustomerListApiItemBase = {
43
+ id?: string
44
+ display_name?: string | null
45
+ displayName?: string | null
46
+ primary_email?: string | null
47
+ primaryEmail?: string | null
48
+ primary_phone?: string | null
49
+ primaryPhone?: string | null
50
+ status?: string | null
51
+ lifecycle_stage?: string | null
52
+ lifecycleStage?: string | null
53
+ source?: string | null
54
+ owner_user_id?: string | null
55
+ ownerUserId?: string | null
56
+ organization_id?: string | null
57
+ organizationId?: string | null
58
+ tenant_id?: string | null
59
+ tenantId?: string | null
60
+ created_at?: string | null
61
+ createdAt?: string | null
62
+ }
63
+
64
+ export type CustomerListSummary = {
65
+ id: string | undefined
66
+ displayName: string | null
67
+ primaryEmail: string | null
68
+ primaryPhone: string | null
69
+ status: string | null
70
+ lifecycleStage: string | null
71
+ source: string | null
72
+ ownerUserId: string | null
73
+ organizationId: string | null
74
+ tenantId: string | null
75
+ createdAt: string | null
76
+ }
77
+
78
+ export function toCustomerListSummary(row: CustomerListApiItemBase): CustomerListSummary {
79
+ const createdAtRaw = row.created_at ?? row.createdAt ?? null
80
+ const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
81
+ return {
82
+ id: row.id,
83
+ displayName: row.display_name ?? row.displayName ?? null,
84
+ primaryEmail: row.primary_email ?? row.primaryEmail ?? null,
85
+ primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
86
+ status: row.status ?? null,
87
+ lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
88
+ source: row.source ?? null,
89
+ ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
90
+ organizationId: row.organization_id ?? row.organizationId ?? null,
91
+ tenantId: row.tenant_id ?? row.tenantId ?? null,
92
+ createdAt,
93
+ }
94
+ }
95
+
96
+ /* -------------------------------------------------------------------------- */
97
+ /* Related-records mappers */
98
+ /* -------------------------------------------------------------------------- */
99
+
100
+ function asRows(value: unknown): Array<Record<string, unknown>> {
101
+ return Array.isArray(value) ? (value as Array<Record<string, unknown>>) : []
102
+ }
103
+
104
+ export function mapAddresses(rows: Array<Record<string, unknown>>) {
105
+ return rows.map((address) => ({
106
+ id: address.id,
107
+ name: address.name ?? null,
108
+ purpose: address.purpose ?? null,
109
+ addressLine1: address.addressLine1 ?? null,
110
+ addressLine2: address.addressLine2 ?? null,
111
+ city: address.city ?? null,
112
+ region: address.region ?? null,
113
+ postalCode: address.postalCode ?? null,
114
+ country: address.country ?? null,
115
+ isPrimary: !!address.isPrimary,
116
+ }))
117
+ }
118
+
119
+ export function mapActivities(rows: Array<Record<string, unknown>>) {
120
+ return rows.map((activity) => ({
121
+ id: activity.id,
122
+ activityType: activity.activityType,
123
+ subject: activity.subject ?? null,
124
+ body: activity.body ?? null,
125
+ occurredAt: toIso(activity.occurredAt),
126
+ createdAt: toIso(activity.createdAt),
127
+ }))
128
+ }
129
+
130
+ export function mapNotes(rows: Array<Record<string, unknown>>) {
131
+ return rows.map((comment) => ({
132
+ id: comment.id,
133
+ body: comment.body,
134
+ authorUserId: comment.authorUserId ?? null,
135
+ createdAt: toIso(comment.createdAt),
136
+ }))
137
+ }
138
+
139
+ export function mapTasks(rows: Array<Record<string, unknown>>) {
140
+ return rows.map((task) => ({
141
+ id: task.id,
142
+ todoId: task.todoId ?? task.id,
143
+ todoSource: task.todoSource ?? null,
144
+ createdAt: toIso(task.createdAt),
145
+ }))
146
+ }
147
+
148
+ export function mapInteractions(rows: Array<Record<string, unknown>>) {
149
+ return rows.map((interaction) => ({
150
+ id: interaction.id,
151
+ interactionType: interaction.interactionType,
152
+ title: interaction.title ?? null,
153
+ status: interaction.status,
154
+ scheduledAt: toIso(interaction.scheduledAt),
155
+ occurredAt: toIso(interaction.occurredAt),
156
+ }))
157
+ }
158
+
159
+ export type RelatedTag = { id: string; slug: string; label: string; color: string | null }
160
+
161
+ export function mapTags(rows: Array<Record<string, unknown>>): RelatedTag[] {
162
+ return rows
163
+ .map((tag) => {
164
+ if (!tag || typeof tag !== 'object') return null
165
+ const id = typeof tag.id === 'string' ? tag.id : null
166
+ const label = typeof tag.label === 'string' ? tag.label : null
167
+ if (!id || !label) return null
168
+ const slug = typeof tag.slug === 'string' ? tag.slug : label
169
+ const color = typeof tag.color === 'string' ? tag.color : null
170
+ return { id, slug, label, color }
171
+ })
172
+ .filter((entry): entry is RelatedTag => entry !== null)
173
+ }
174
+
175
+ export type RelatedDeal = {
176
+ id: string
177
+ title: string
178
+ status: string | null
179
+ pipelineStageId: string | null
180
+ valueAmount: string | null
181
+ valueCurrency: string | null
182
+ }
183
+
184
+ export function mapDeals(rows: Array<Record<string, unknown>>): RelatedDeal[] {
185
+ return rows
186
+ .map((deal) => {
187
+ if (!deal || typeof deal !== 'object') return null
188
+ const id = typeof deal.id === 'string' ? deal.id : null
189
+ if (!id) return null
190
+ return {
191
+ id,
192
+ title: typeof deal.title === 'string' ? deal.title : '',
193
+ status: typeof deal.status === 'string' ? deal.status : null,
194
+ pipelineStageId: typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,
195
+ valueAmount:
196
+ typeof deal.valueAmount === 'string'
197
+ ? deal.valueAmount
198
+ : deal.valueAmount === null || deal.valueAmount === undefined
199
+ ? null
200
+ : String(deal.valueAmount),
201
+ valueCurrency: typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,
202
+ }
203
+ })
204
+ .filter((value): value is RelatedDeal => value !== null)
205
+ }
206
+
207
+ export type RelatedPerson = {
208
+ id: string
209
+ displayName: string
210
+ primaryEmail: string | null
211
+ primaryPhone: string | null
212
+ jobTitle: string | null
213
+ department: string | null
214
+ }
215
+
216
+ export function mapPeople(rows: Array<Record<string, unknown>>): RelatedPerson[] {
217
+ return rows
218
+ .map((person) => {
219
+ if (!person || typeof person !== 'object') return null
220
+ const id = typeof person.id === 'string' ? person.id : null
221
+ const displayName = typeof person.displayName === 'string' ? person.displayName : null
222
+ if (!id || !displayName) return null
223
+ return {
224
+ id,
225
+ displayName,
226
+ primaryEmail: typeof person.primaryEmail === 'string' ? person.primaryEmail : null,
227
+ primaryPhone: typeof person.primaryPhone === 'string' ? person.primaryPhone : null,
228
+ jobTitle: typeof person.jobTitle === 'string' ? person.jobTitle : null,
229
+ department: typeof person.department === 'string' ? person.department : null,
230
+ }
231
+ })
232
+ .filter((value): value is RelatedPerson => value !== null)
233
+ }
234
+
235
+ export type CustomerRelatedRecords = {
236
+ addresses: ReturnType<typeof mapAddresses>
237
+ activities: ReturnType<typeof mapActivities>
238
+ notes: ReturnType<typeof mapNotes>
239
+ tasks: ReturnType<typeof mapTasks>
240
+ interactions: ReturnType<typeof mapInteractions>
241
+ tags: RelatedTag[]
242
+ deals: RelatedDeal[]
243
+ people?: RelatedPerson[]
244
+ }
245
+
246
+ /**
247
+ * Builds the related-records block from a customers detail API payload
248
+ * (`addresses`, `activities`, `comments`, `todos`, `interactions`, `tags`,
249
+ * `deals`, and — for companies — `people`). The key order matches the
250
+ * pre-refactor literals so the emitted shape is unchanged. Pass
251
+ * `includePeople: true` to add the companies-only `people` collection.
252
+ */
253
+ export function buildRelatedRecords(
254
+ data: Record<string, unknown>,
255
+ options: { includePeople?: boolean } = {},
256
+ ): CustomerRelatedRecords {
257
+ const related: CustomerRelatedRecords = {
258
+ addresses: mapAddresses(asRows(data.addresses)),
259
+ activities: mapActivities(asRows(data.activities)),
260
+ notes: mapNotes(asRows(data.comments)),
261
+ tasks: mapTasks(asRows(data.todos)),
262
+ interactions: mapInteractions(asRows(data.interactions)),
263
+ tags: mapTags(asRows(data.tags)),
264
+ deals: mapDeals(asRows(data.deals)),
265
+ }
266
+ if (options.includePeople) {
267
+ related.people = mapPeople(asRows(data.people))
268
+ }
269
+ return related
270
+ }