@open-mercato/core 0.10.1-develop.7761.1.9dfaf470ed → 0.10.1-develop.7762.1.657bb7236b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +1 -0
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +3 -2
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +11 -12
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +3 -2
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/create/page.js +6 -0
- package/dist/modules/customers/backend/customers/companies/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +25 -25
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealInjectedTabs.js +8 -27
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealInjectedTabs.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +175 -171
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +25 -24
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/create/page.js +9 -0
- package/dist/modules/customers/backend/customers/people/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +140 -144
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailHeader.js +2 -0
- package/dist/modules/customers/components/detail/CompanyDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +13 -4
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/CreatePersonDialog.js +13 -3
- package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/CustomerDetailSidebar.js +29 -0
- package/dist/modules/customers/components/detail/CustomerDetailSidebar.js.map +7 -0
- package/dist/modules/customers/components/detail/DealDetailHeader.js +4 -2
- package/dist/modules/customers/components/detail/DealDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +9 -1
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +5 -0
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailHeader.js +2 -0
- package/dist/modules/customers/components/detail/PersonDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +9 -1
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/create/CreateDealForm.js +160 -176
- package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/useCustomerInjectedTabs.js +47 -0
- package/dist/modules/customers/components/detail/useCustomerInjectedTabs.js.map +7 -0
- package/dist/modules/customers/components/formConfig.js +3 -0
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/extension-points.js +14 -4
- package/dist/modules/customers/extension-points.js.map +2 -2
- package/dist/modules/customers/lib/detailResponseEnrichment.js +38 -0
- package/dist/modules/customers/lib/detailResponseEnrichment.js.map +7 -0
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +4 -4
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -0
- package/src/modules/customers/api/companies/[id]/route.ts +3 -2
- package/src/modules/customers/api/deals/[id]/route.ts +12 -12
- package/src/modules/customers/api/people/[id]/route.ts +3 -2
- package/src/modules/customers/backend/customers/companies/create/page.tsx +6 -0
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +27 -30
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealInjectedTabs.tsx +9 -44
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +7 -3
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +25 -27
- package/src/modules/customers/backend/customers/people/create/page.tsx +9 -0
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +21 -33
- package/src/modules/customers/components/detail/CompanyDetailHeader.tsx +3 -0
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +15 -4
- package/src/modules/customers/components/detail/CreatePersonDialog.tsx +15 -4
- package/src/modules/customers/components/detail/CustomerDetailSidebar.tsx +41 -0
- package/src/modules/customers/components/detail/DealDetailHeader.tsx +4 -1
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +11 -1
- package/src/modules/customers/components/detail/DealForm.tsx +5 -0
- package/src/modules/customers/components/detail/PersonDetailHeader.tsx +3 -0
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +11 -1
- package/src/modules/customers/components/detail/create/CreateDealForm.tsx +126 -209
- package/src/modules/customers/components/detail/useCustomerInjectedTabs.tsx +64 -0
- package/src/modules/customers/components/formConfig.tsx +3 -0
- package/src/modules/customers/extension-points.ts +14 -4
- package/src/modules/customers/lib/detailResponseEnrichment.ts +53 -0
- package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/PersonDetailHeader.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Phone, Mail, Building2, Trash2, Pencil } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { ObjectHistoryButton } from './ObjectHistoryButton'\nimport { PersonTagsDialog } from './PersonTagsDialog'\nimport { useCustomerDictionary, invalidateCustomerDictionary } from './hooks/useCustomerDictionary'\nimport { renderDictionaryIcon } from '../../../dictionaries/components/dictionaryAppearance'\nimport type { TagSummary } from './types'\nimport type { TagsSectionController } from '@open-mercato/ui/backend/detail'\nimport type { PersonOverview } from '../formConfig'\nimport type { CustomerDictionaryMap } from '@open-mercato/core/modules/customers/lib/dictionaries'\nimport { formatFallbackLabel } from './utils'\n\nconst HEADER_ICON_BUTTON_CLASS = 'size-8 rounded-md'\n\ntype PersonDetailHeaderProps = {\n data: PersonOverview\n onTagsChange: (tags: TagSummary[]) => void\n tagsSectionControllerRef: React.RefObject<TagsSectionController | null>\n onSave: () => void\n onDelete: () => Promise<void>\n isDirty: boolean\n isSaving: boolean\n /** Callback to focus a specific field in the Zone 1 CrudForm by field name. */\n onFocusField?: (fieldName: string) => void\n /**\n * @deprecated Kept for backward compatibility. The \"+ Link company\" header CTA was removed;\n * company linking now happens exclusively through the Zone 2 Companies tab via\n * `PersonCompaniesSection`. This prop is a no-op and will be removed in a future major release.\n */\n onOpenCompaniesTab?: () => void\n /** Callback to reload person data after tags dialog save. */\n onDataReload?: () => void\n}\n\nfunction DictionaryBadge({ value, map, categoryIcon, className }: { value: string; map: CustomerDictionaryMap | undefined; categoryIcon?: React.ReactNode; className?: string }) {\n const entry = map?.[value]\n const color = entry?.color ?? null\n const icon = entry?.icon ?? null\n const label = entry?.label ?? formatFallbackLabel(value)\n const colorStyle: React.CSSProperties | undefined = color\n ? { color, borderColor: color, backgroundColor: `${color}1A` }\n : undefined\n return (\n <Badge\n variant=\"outline\"\n className={cn(\n 'rounded-sm gap-1.5 text-xs font-medium',\n className,\n )}\n style={colorStyle}\n >\n {icon ? renderDictionaryIcon(icon, 'size-2.5') : categoryIcon ?? null}\n {label}\n </Badge>\n )\n}\n\n/** Renders a tag badge with color-based text/border/background from TagSummary.color. */\nfunction TagBadge({ tag }: { tag: TagSummary }) {\n const colorStyle: React.CSSProperties | undefined = tag.color\n ? { color: tag.color, borderColor: tag.color, backgroundColor: `${tag.color}1A` }\n : undefined\n return (\n <Badge variant=\"outline\" className=\"rounded-sm gap-1.5 text-xs font-medium\" style={colorStyle}>\n {tag.label}\n </Badge>\n )\n}\n\nexport function PersonDetailHeader({\n data,\n onTagsChange,\n tagsSectionControllerRef,\n onSave,\n onDelete,\n isDirty,\n isSaving,\n onFocusField,\n onOpenCompaniesTab,\n onDataReload,\n}: PersonDetailHeaderProps) {\n const t = useT()\n const queryClient = useQueryClient()\n const [manageTagsOpen, setManageTagsOpen] = React.useState(false)\n const person = data.person\n const profile = data.profile\n const displayName = person.displayName || t('customers.people.detail.untitled', 'Untitled')\n\n const jobTitle = profile?.jobTitle ?? null\n const linkedCompanies = React.useMemo(() => {\n const items = Array.isArray(data.companies) && data.companies.length > 0\n ? data.companies\n : data.company\n ? [{ ...data.company, isPrimary: Boolean(data.isPrimary) }]\n : []\n return items\n }, [data.companies, data.company, data.isPrimary])\n const visibleCompanies = React.useMemo(() => linkedCompanies.slice(0, 3), [linkedCompanies])\n const hiddenCompaniesCount = Math.max(0, linkedCompanies.length - visibleCompanies.length)\n const visibleTags = React.useMemo(() => data.tags.slice(0, 6), [data.tags])\n const hiddenTagsCount = Math.max(0, data.tags.length - visibleTags.length)\n const primaryCompany = linkedCompanies.find((entry) => entry.isPrimary) ?? linkedCompanies[0] ?? null\n const companyName = primaryCompany?.displayName ?? null\n const companyId = primaryCompany?.id ?? profile?.companyEntityId ?? null\n\n // Fetch dictionary maps for colored badge rendering (scoped to person's organization)\n const personOrgId = person.organizationId ?? null\n const { data: statusDict } = useCustomerDictionary('statuses', 0, personOrgId)\n const { data: lifecycleDict } = useCustomerDictionary('lifecycle-stages', 0, personOrgId)\n const { data: sourceDict } = useCustomerDictionary('sources', 0, personOrgId)\n const { data: temperatureDict } = useCustomerDictionary('temperature', 0, personOrgId)\n const { data: renewalQuarterDict } = useCustomerDictionary('renewal-quarters', 0, personOrgId)\n\n return (\n <div className=\"rounded-lg border bg-card px-6 py-5\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:gap-5\">\n {/* Avatar */}\n <Avatar label={displayName} size=\"xl\" variant=\"monochrome\" />\n\n {/* Person info */}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h1 className=\"truncate text-2xl font-bold text-foreground\">{displayName}</h1>\n {data.isPrimary && (\n <span className=\"shrink-0 rounded-sm bg-status-warning-bg px-1.5 py-0.5 text-overline font-bold text-status-warning-text\">\n {t('customers.people.detail.header.primary', 'PRIMARY')}\n </span>\n )}\n </div>\n\n {/* Subtitle: job title + company link */}\n {(jobTitle || companyName) && (\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\n {jobTitle}\n {jobTitle && companyName && ` ${t('customers.people.detail.header.at', 'at')} `}\n {companyName && companyId && (\n <Link href={`/backend/customers/companies-v2/${companyId}`} className=\"text-primary hover:underline\">\n {companyName}\n </Link>\n )}\n {companyName && !companyId && companyName}\n </p>\n )}\n\n {/* Contact row */}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-x-5 gap-y-1 text-sm text-muted-foreground\">\n {person.primaryPhone && (\n <span className=\"inline-flex items-center gap-1.5\">\n <Phone className=\"size-3.5\" />\n <a href={`tel:${person.primaryPhone}`} className=\"hover:text-foreground\">{person.primaryPhone}</a>\n </span>\n )}\n {person.primaryEmail && (\n <span className=\"inline-flex items-center gap-1.5\">\n <Mail className=\"size-3.5\" />\n <a href={`mailto:${person.primaryEmail}`} className=\"hover:text-foreground\">{person.primaryEmail}</a>\n </span>\n )}\n </div>\n\n {/* Company chips (annotation 1a) */}\n {linkedCompanies.length > 0 && (\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">\n <Building2 className=\"mr-1 inline size-3.5\" />\n {t('customers.people.detail.header.companies', 'Companies')} ({linkedCompanies.length}):\n </span>\n {visibleCompanies.map((company) => (\n <Link\n key={company.id}\n href={`/backend/customers/companies-v2/${company.id}`}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-status-info-bg',\n company.isPrimary\n ? 'border-status-info-border bg-status-info-bg text-status-info-text'\n : 'border-border bg-background text-foreground',\n )}\n >\n <Building2 className=\"size-3\" />\n {company.displayName}\n {company.isPrimary ? (\n <span className=\"rounded-sm bg-status-info-icon px-1 py-px text-overline font-bold text-white\">\n {t('customers.people.detail.header.primary', 'PRIMARY')}\n </span>\n ) : null}\n </Link>\n ))}\n {hiddenCompaniesCount > 0 ? (\n <Badge variant=\"outline\" className=\"rounded-sm text-xs font-semibold\">\n +{hiddenCompaniesCount} {t('customers.people.detail.header.more', 'more')}\n </Badge>\n ) : null}\n </div>\n )}\n\n {/* Status badges + inline tags + manage tags */}\n <div className=\"mt-2.5 flex flex-wrap items-center gap-2\">\n {person.status && (\n <DictionaryBadge value={person.status} map={statusDict?.map} />\n )}\n {person.lifecycleStage && (\n <DictionaryBadge value={person.lifecycleStage} map={lifecycleDict?.map} />\n )}\n {person.source && (\n <DictionaryBadge value={person.source} map={sourceDict?.map} />\n )}\n {person.temperature && (\n <DictionaryBadge value={person.temperature} map={temperatureDict?.map} />\n )}\n {person.renewalQuarter && (\n <DictionaryBadge value={person.renewalQuarter} map={renewalQuarterDict?.map} />\n )}\n {/* Inline tag pills */}\n {visibleTags.map((tag) => (\n <TagBadge key={tag.id ?? tag.label} tag={tag} />\n ))}\n {hiddenTagsCount > 0 ? (\n <Badge variant=\"outline\" className=\"rounded-sm gap-1.5 text-xs font-medium\">\n +{hiddenTagsCount} {t('customers.people.detail.header.more', 'more')}\n </Badge>\n ) : null}\n {/* Manage tags \u2014 opens dialog directly */}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto rounded-sm px-2 py-1 text-xs font-medium text-muted-foreground hover:text-foreground\"\n onClick={() => setManageTagsOpen(true)}\n >\n <Pencil className=\"mr-1 size-3\" />\n {t('customers.people.detail.actions.manageTags', 'Edit tags')}\n </Button>\n </div>\n </div>\n\n {/* Right side: actions */}\n <div className=\"flex w-full shrink-0 items-center justify-start gap-2 sm:w-auto sm:justify-end\">\n <SendObjectMessageDialog\n object={{\n entityModule: 'customers',\n entityType: 'person',\n entityId: person.id,\n previewData: {\n title: displayName,\n subtitle: person.primaryEmail ?? companyName ?? undefined,\n },\n }}\n viewHref={`/backend/customers/people-v2/${person.id}`}\n buttonVariant=\"outline\"\n buttonSize=\"icon\"\n buttonClassName={HEADER_ICON_BUTTON_CLASS}\n buttonLabel={t('customers.people.detail.actions.sendMessage', 'Send message')}\n />\n <ObjectHistoryButton\n resourceKind=\"customers.person\"\n resourceId={person.id}\n organizationId={person.organizationId ?? undefined}\n />\n <IconButton\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n aria-label={t('customers.people.detail.actions.delete', 'Delete')}\n onClick={() => {\n void onDelete()\n }}\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={onSave}\n disabled={!isDirty || isSaving}\n >\n {t('customers.people.detail.actions.save', 'Save')}\n </Button>\n </div>\n </div>\n <PersonTagsDialog\n open={manageTagsOpen}\n onClose={() => setManageTagsOpen(false)}\n personId={person.id}\n personOrganizationId={person.organizationId ?? null}\n personData={{\n status: person.status,\n lifecycleStage: person.lifecycleStage,\n source: person.source,\n temperature: person.temperature,\n renewalQuarter: person.renewalQuarter,\n jobTitle: data.profile?.jobTitle ?? null,\n customFields: data.customFields,\n tags: data.tags,\n }}\n onSaved={() => {\n // Invalidate dictionary caches so header badges pick up fresh colors\n void invalidateCustomerDictionary(queryClient, 'statuses')\n void invalidateCustomerDictionary(queryClient, 'lifecycle-stages')\n void invalidateCustomerDictionary(queryClient, 'sources')\n void invalidateCustomerDictionary(queryClient, 'temperature')\n void invalidateCustomerDictionary(queryClient, 'renewal-quarters')\n onDataReload?.()\n }}\n />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Phone, Mail, Building2, Trash2, Pencil } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { ObjectHistoryButton } from './ObjectHistoryButton'\nimport { PersonTagsDialog } from './PersonTagsDialog'\nimport { useCustomerDictionary, invalidateCustomerDictionary } from './hooks/useCustomerDictionary'\nimport { renderDictionaryIcon } from '../../../dictionaries/components/dictionaryAppearance'\nimport type { TagSummary } from './types'\nimport type { TagsSectionController } from '@open-mercato/ui/backend/detail'\nimport type { PersonOverview } from '../formConfig'\nimport type { CustomerDictionaryMap } from '@open-mercato/core/modules/customers/lib/dictionaries'\nimport { formatFallbackLabel } from './utils'\n\nconst HEADER_ICON_BUTTON_CLASS = 'size-8 rounded-md'\n\ntype PersonDetailHeaderProps = {\n data: PersonOverview\n onTagsChange: (tags: TagSummary[]) => void\n tagsSectionControllerRef: React.RefObject<TagsSectionController | null>\n onSave: () => void\n onDelete: () => Promise<void>\n isDirty: boolean\n isSaving: boolean\n actions?: React.ReactNode\n /** Callback to focus a specific field in the Zone 1 CrudForm by field name. */\n onFocusField?: (fieldName: string) => void\n /**\n * @deprecated Kept for backward compatibility. The \"+ Link company\" header CTA was removed;\n * company linking now happens exclusively through the Zone 2 Companies tab via\n * `PersonCompaniesSection`. This prop is a no-op and will be removed in a future major release.\n */\n onOpenCompaniesTab?: () => void\n /** Callback to reload person data after tags dialog save. */\n onDataReload?: () => void\n}\n\nfunction DictionaryBadge({ value, map, categoryIcon, className }: { value: string; map: CustomerDictionaryMap | undefined; categoryIcon?: React.ReactNode; className?: string }) {\n const entry = map?.[value]\n const color = entry?.color ?? null\n const icon = entry?.icon ?? null\n const label = entry?.label ?? formatFallbackLabel(value)\n const colorStyle: React.CSSProperties | undefined = color\n ? { color, borderColor: color, backgroundColor: `${color}1A` }\n : undefined\n return (\n <Badge\n variant=\"outline\"\n className={cn(\n 'rounded-sm gap-1.5 text-xs font-medium',\n className,\n )}\n style={colorStyle}\n >\n {icon ? renderDictionaryIcon(icon, 'size-2.5') : categoryIcon ?? null}\n {label}\n </Badge>\n )\n}\n\n/** Renders a tag badge with color-based text/border/background from TagSummary.color. */\nfunction TagBadge({ tag }: { tag: TagSummary }) {\n const colorStyle: React.CSSProperties | undefined = tag.color\n ? { color: tag.color, borderColor: tag.color, backgroundColor: `${tag.color}1A` }\n : undefined\n return (\n <Badge variant=\"outline\" className=\"rounded-sm gap-1.5 text-xs font-medium\" style={colorStyle}>\n {tag.label}\n </Badge>\n )\n}\n\nexport function PersonDetailHeader({\n data,\n onTagsChange,\n tagsSectionControllerRef,\n onSave,\n onDelete,\n isDirty,\n isSaving,\n actions,\n onFocusField,\n onOpenCompaniesTab,\n onDataReload,\n}: PersonDetailHeaderProps) {\n const t = useT()\n const queryClient = useQueryClient()\n const [manageTagsOpen, setManageTagsOpen] = React.useState(false)\n const person = data.person\n const profile = data.profile\n const displayName = person.displayName || t('customers.people.detail.untitled', 'Untitled')\n\n const jobTitle = profile?.jobTitle ?? null\n const linkedCompanies = React.useMemo(() => {\n const items = Array.isArray(data.companies) && data.companies.length > 0\n ? data.companies\n : data.company\n ? [{ ...data.company, isPrimary: Boolean(data.isPrimary) }]\n : []\n return items\n }, [data.companies, data.company, data.isPrimary])\n const visibleCompanies = React.useMemo(() => linkedCompanies.slice(0, 3), [linkedCompanies])\n const hiddenCompaniesCount = Math.max(0, linkedCompanies.length - visibleCompanies.length)\n const visibleTags = React.useMemo(() => data.tags.slice(0, 6), [data.tags])\n const hiddenTagsCount = Math.max(0, data.tags.length - visibleTags.length)\n const primaryCompany = linkedCompanies.find((entry) => entry.isPrimary) ?? linkedCompanies[0] ?? null\n const companyName = primaryCompany?.displayName ?? null\n const companyId = primaryCompany?.id ?? profile?.companyEntityId ?? null\n\n // Fetch dictionary maps for colored badge rendering (scoped to person's organization)\n const personOrgId = person.organizationId ?? null\n const { data: statusDict } = useCustomerDictionary('statuses', 0, personOrgId)\n const { data: lifecycleDict } = useCustomerDictionary('lifecycle-stages', 0, personOrgId)\n const { data: sourceDict } = useCustomerDictionary('sources', 0, personOrgId)\n const { data: temperatureDict } = useCustomerDictionary('temperature', 0, personOrgId)\n const { data: renewalQuarterDict } = useCustomerDictionary('renewal-quarters', 0, personOrgId)\n\n return (\n <div className=\"rounded-lg border bg-card px-6 py-5\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:gap-5\">\n {/* Avatar */}\n <Avatar label={displayName} size=\"xl\" variant=\"monochrome\" />\n\n {/* Person info */}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h1 className=\"truncate text-2xl font-bold text-foreground\">{displayName}</h1>\n {data.isPrimary && (\n <span className=\"shrink-0 rounded-sm bg-status-warning-bg px-1.5 py-0.5 text-overline font-bold text-status-warning-text\">\n {t('customers.people.detail.header.primary', 'PRIMARY')}\n </span>\n )}\n </div>\n\n {/* Subtitle: job title + company link */}\n {(jobTitle || companyName) && (\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\n {jobTitle}\n {jobTitle && companyName && ` ${t('customers.people.detail.header.at', 'at')} `}\n {companyName && companyId && (\n <Link href={`/backend/customers/companies-v2/${companyId}`} className=\"text-primary hover:underline\">\n {companyName}\n </Link>\n )}\n {companyName && !companyId && companyName}\n </p>\n )}\n\n {/* Contact row */}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-x-5 gap-y-1 text-sm text-muted-foreground\">\n {person.primaryPhone && (\n <span className=\"inline-flex items-center gap-1.5\">\n <Phone className=\"size-3.5\" />\n <a href={`tel:${person.primaryPhone}`} className=\"hover:text-foreground\">{person.primaryPhone}</a>\n </span>\n )}\n {person.primaryEmail && (\n <span className=\"inline-flex items-center gap-1.5\">\n <Mail className=\"size-3.5\" />\n <a href={`mailto:${person.primaryEmail}`} className=\"hover:text-foreground\">{person.primaryEmail}</a>\n </span>\n )}\n </div>\n\n {/* Company chips (annotation 1a) */}\n {linkedCompanies.length > 0 && (\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">\n <Building2 className=\"mr-1 inline size-3.5\" />\n {t('customers.people.detail.header.companies', 'Companies')} ({linkedCompanies.length}):\n </span>\n {visibleCompanies.map((company) => (\n <Link\n key={company.id}\n href={`/backend/customers/companies-v2/${company.id}`}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-status-info-bg',\n company.isPrimary\n ? 'border-status-info-border bg-status-info-bg text-status-info-text'\n : 'border-border bg-background text-foreground',\n )}\n >\n <Building2 className=\"size-3\" />\n {company.displayName}\n {company.isPrimary ? (\n <span className=\"rounded-sm bg-status-info-icon px-1 py-px text-overline font-bold text-white\">\n {t('customers.people.detail.header.primary', 'PRIMARY')}\n </span>\n ) : null}\n </Link>\n ))}\n {hiddenCompaniesCount > 0 ? (\n <Badge variant=\"outline\" className=\"rounded-sm text-xs font-semibold\">\n +{hiddenCompaniesCount} {t('customers.people.detail.header.more', 'more')}\n </Badge>\n ) : null}\n </div>\n )}\n\n {/* Status badges + inline tags + manage tags */}\n <div className=\"mt-2.5 flex flex-wrap items-center gap-2\">\n {person.status && (\n <DictionaryBadge value={person.status} map={statusDict?.map} />\n )}\n {person.lifecycleStage && (\n <DictionaryBadge value={person.lifecycleStage} map={lifecycleDict?.map} />\n )}\n {person.source && (\n <DictionaryBadge value={person.source} map={sourceDict?.map} />\n )}\n {person.temperature && (\n <DictionaryBadge value={person.temperature} map={temperatureDict?.map} />\n )}\n {person.renewalQuarter && (\n <DictionaryBadge value={person.renewalQuarter} map={renewalQuarterDict?.map} />\n )}\n {/* Inline tag pills */}\n {visibleTags.map((tag) => (\n <TagBadge key={tag.id ?? tag.label} tag={tag} />\n ))}\n {hiddenTagsCount > 0 ? (\n <Badge variant=\"outline\" className=\"rounded-sm gap-1.5 text-xs font-medium\">\n +{hiddenTagsCount} {t('customers.people.detail.header.more', 'more')}\n </Badge>\n ) : null}\n {/* Manage tags \u2014 opens dialog directly */}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto rounded-sm px-2 py-1 text-xs font-medium text-muted-foreground hover:text-foreground\"\n onClick={() => setManageTagsOpen(true)}\n >\n <Pencil className=\"mr-1 size-3\" />\n {t('customers.people.detail.actions.manageTags', 'Edit tags')}\n </Button>\n </div>\n </div>\n\n {/* Right side: actions */}\n <div className=\"flex w-full shrink-0 items-center justify-start gap-2 sm:w-auto sm:justify-end\">\n {actions}\n <SendObjectMessageDialog\n object={{\n entityModule: 'customers',\n entityType: 'person',\n entityId: person.id,\n previewData: {\n title: displayName,\n subtitle: person.primaryEmail ?? companyName ?? undefined,\n },\n }}\n viewHref={`/backend/customers/people-v2/${person.id}`}\n buttonVariant=\"outline\"\n buttonSize=\"icon\"\n buttonClassName={HEADER_ICON_BUTTON_CLASS}\n buttonLabel={t('customers.people.detail.actions.sendMessage', 'Send message')}\n />\n <ObjectHistoryButton\n resourceKind=\"customers.person\"\n resourceId={person.id}\n organizationId={person.organizationId ?? undefined}\n />\n <IconButton\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n aria-label={t('customers.people.detail.actions.delete', 'Delete')}\n onClick={() => {\n void onDelete()\n }}\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={onSave}\n disabled={!isDirty || isSaving}\n >\n {t('customers.people.detail.actions.save', 'Save')}\n </Button>\n </div>\n </div>\n <PersonTagsDialog\n open={manageTagsOpen}\n onClose={() => setManageTagsOpen(false)}\n personId={person.id}\n personOrganizationId={person.organizationId ?? null}\n personData={{\n status: person.status,\n lifecycleStage: person.lifecycleStage,\n source: person.source,\n temperature: person.temperature,\n renewalQuarter: person.renewalQuarter,\n jobTitle: data.profile?.jobTitle ?? null,\n customFields: data.customFields,\n tags: data.tags,\n }}\n onSaved={() => {\n // Invalidate dictionary caches so header badges pick up fresh colors\n void invalidateCustomerDictionary(queryClient, 'statuses')\n void invalidateCustomerDictionary(queryClient, 'lifecycle-stages')\n void invalidateCustomerDictionary(queryClient, 'sources')\n void invalidateCustomerDictionary(queryClient, 'temperature')\n void invalidateCustomerDictionary(queryClient, 'renewal-quarters')\n onDataReload?.()\n }}\n />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "lucide-react";
|
|
18
18
|
import { registerComponent } from "@open-mercato/shared/modules/widgets/component-registry";
|
|
19
19
|
import { useRegisteredComponent } from "@open-mercato/ui/backend/injection/useRegisteredComponent";
|
|
20
|
+
const PERSON_DETAIL_TAB_IDS = ["activities", "emails", "deals", "companies", "addresses", "tasks", "changelog", "files"];
|
|
20
21
|
const PERSON_DETAIL_TABS_COMPONENT_ID = "section:customers.people.detailTabs";
|
|
21
22
|
const SUPPORTED_TAB_IDS = /* @__PURE__ */ new Set(["activities", "emails", "deals", "companies", "addresses", "tasks", "changelog", "files"]);
|
|
22
23
|
function resolveLegacyTab(tab, knownTabIds) {
|
|
@@ -33,6 +34,7 @@ function DefaultPersonDetailTabs({
|
|
|
33
34
|
activeTab,
|
|
34
35
|
onTabChange,
|
|
35
36
|
injectedTabs = [],
|
|
37
|
+
isLoadingInjectedTabs = false,
|
|
36
38
|
hiddenTabIds = [],
|
|
37
39
|
activitiesCount = 0,
|
|
38
40
|
dealsCount = 0,
|
|
@@ -100,12 +102,17 @@ function DefaultPersonDetailTabs({
|
|
|
100
102
|
const hidden = new Set(hiddenTabIds);
|
|
101
103
|
return [
|
|
102
104
|
...builtInTabs,
|
|
103
|
-
...injectedTabs.map((tab) => ({
|
|
105
|
+
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
104
106
|
id: tab.id,
|
|
105
107
|
label: tab.label
|
|
106
108
|
}))
|
|
107
109
|
].filter((tab) => !hidden.has(tab.id));
|
|
108
110
|
}, [builtInTabs, hiddenTabIds, injectedTabs]);
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
if (isLoadingInjectedTabs || allTabs.some((tab) => tab.id === activeTab)) return;
|
|
113
|
+
const fallback = allTabs[0]?.id;
|
|
114
|
+
if (fallback) onTabChange(fallback);
|
|
115
|
+
}, [activeTab, allTabs, isLoadingInjectedTabs, onTabChange]);
|
|
109
116
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
110
117
|
/* @__PURE__ */ jsxs("div", { className: "flex items-end justify-between gap-2 border-b", children: [
|
|
111
118
|
/* @__PURE__ */ jsx(
|
|
@@ -160,6 +167,7 @@ function PersonDetailTabs(props) {
|
|
|
160
167
|
}
|
|
161
168
|
export {
|
|
162
169
|
PERSON_DETAIL_TABS_COMPONENT_ID,
|
|
170
|
+
PERSON_DETAIL_TAB_IDS,
|
|
163
171
|
PersonDetailTabs,
|
|
164
172
|
resolveLegacyTab
|
|
165
173
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/PersonDetailTabs.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport {\n SquareCheckBig,\n Mail,\n Briefcase,\n Building2,\n Check,\n History,\n Paperclip,\n Plus,\n MapPin,\n} from 'lucide-react'\nimport type { SectionAction } from '@open-mercato/ui/backend/detail'\nimport { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\n\nexport type PersonTabId =\n | 'activities'\n | 'emails'\n | 'deals'\n | 'companies'\n | 'addresses'\n | 'tasks'\n | 'changelog'\n | 'files'\n | string\n\ntype TabDef = {\n id: PersonTabId\n label: string\n icon?: React.ReactNode\n count?: React.ReactNode\n}\n\nexport const PERSON_DETAIL_TABS_COMPONENT_ID = 'section:customers.people.detailTabs'\n\nexport type PersonDetailTabsProps = {\n activeTab: PersonTabId\n onTabChange: (tab: PersonTabId) => void\n injectedTabs?: Array<{ id: string; label: string }>\n hiddenTabIds?: string[]\n activitiesCount?: number\n dealsCount?: number\n companiesCount?: number\n addressesCount?: number\n tasksCount?: number\n filesCount?: number\n sectionAction?: SectionAction | null\n children: React.ReactNode\n}\n\nconst SUPPORTED_TAB_IDS = new Set<PersonTabId>(['activities', 'emails', 'deals', 'companies', 'addresses', 'tasks', 'changelog', 'files'])\n\nexport function resolveLegacyTab(tab: string | null | undefined, knownTabIds?: Iterable<string>): PersonTabId {\n if (!tab) return 'activities'\n if (SUPPORTED_TAB_IDS.has(tab as PersonTabId)) return tab as PersonTabId\n if (knownTabIds && new Set(knownTabIds).has(tab)) return tab\n return 'activities'\n}\n\nfunction formatTabCount(count: number): string | number | undefined {\n if (count <= 0) return undefined\n return count > 999 ? '999+' : count\n}\n\nfunction DefaultPersonDetailTabs({\n activeTab,\n onTabChange,\n injectedTabs = [],\n hiddenTabIds = [],\n activitiesCount = 0,\n dealsCount = 0,\n companiesCount = 0,\n addressesCount = 0,\n tasksCount = 0,\n filesCount = 0,\n sectionAction = null,\n children,\n}: PersonDetailTabsProps) {\n const t = useT()\n\n const builtInTabs: TabDef[] = React.useMemo(\n () => [\n {\n id: 'activities',\n label: t('customers.people.detail.tabs.activities', 'Activities'),\n icon: <SquareCheckBig className=\"size-4\" />,\n count: formatTabCount(activitiesCount),\n },\n {\n id: 'emails',\n label: t('customers.people.detail.tabs.emails', 'Emails'),\n icon: <Mail className=\"size-4\" />,\n },\n {\n id: 'deals',\n label: t('customers.people.detail.tabs.deals', 'Deals'),\n icon: <Briefcase className=\"size-4\" />,\n count: formatTabCount(dealsCount),\n },\n {\n id: 'companies',\n label: t('customers.people.detail.tabs.companies', 'Companies'),\n icon: <Building2 className=\"size-4\" />,\n count: formatTabCount(companiesCount),\n },\n {\n id: 'addresses',\n label: t('customers.people.detail.tabs.addresses', 'Addresses'),\n icon: <MapPin className=\"size-4\" />,\n count: formatTabCount(addressesCount),\n },\n {\n id: 'tasks',\n label: t('customers.people.detail.tabs.tasks', 'Tasks'),\n icon: <Check className=\"size-4\" />,\n count: formatTabCount(tasksCount),\n },\n {\n id: 'changelog',\n label: t('customers.people.detail.tabs.changelog', 'Change log'),\n icon: <History className=\"size-4\" />,\n count: 'NEW',\n },\n {\n id: 'files',\n label: t('customers.people.detail.tabs.files', 'Files'),\n icon: <Paperclip className=\"size-4\" />,\n count: formatTabCount(filesCount),\n },\n ],\n [t, activitiesCount, dealsCount, companiesCount, addressesCount, tasksCount, filesCount],\n )\n\n const allTabs: TabDef[] = React.useMemo(() => {\n const hidden = new Set(hiddenTabIds)\n return [\n ...builtInTabs,\n ...injectedTabs.map((tab) => ({\n id: tab.id as PersonTabId,\n label: tab.label,\n })),\n ].filter((tab) => !hidden.has(tab.id))\n }, [builtInTabs, hiddenTabIds, injectedTabs])\n\n return (\n <div>\n {/* Tab navigation \u2014 full width above both zones */}\n <div className=\"flex items-end justify-between gap-2 border-b\">\n <Tabs\n value={activeTab}\n onValueChange={(value) => onTabChange(value as PersonTabId)}\n variant=\"underline\"\n className=\"min-w-0 flex-1\"\n >\n <TabsList\n aria-label={t('customers.people.detail.tabs.label', 'Person detail sections')}\n className=\"-mb-px w-full overflow-x-auto border-b-0 px-1\"\n >\n {allTabs.map((tab) => (\n <TabsTrigger key={tab.id} value={tab.id} leading={tab.icon} count={tab.count}>\n {tab.label}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n {sectionAction ? (\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={sectionAction.onClick}\n disabled={sectionAction.disabled}\n className=\"mb-1.5 mr-1 shrink-0\"\n >\n <Plus className=\"mr-1.5 h-4 w-4\" />\n {sectionAction.label}\n </Button>\n ) : null}\n </div>\n\n {/* Two-column content below tabs */}\n <div className=\"pt-6\">\n {children}\n </div>\n </div>\n )\n}\n\nregisterComponent<PersonDetailTabsProps>({\n id: PERSON_DETAIL_TABS_COMPONENT_ID,\n component: DefaultPersonDetailTabs,\n metadata: {\n module: 'customers',\n description: 'Person detail tab navigation and content.',\n },\n})\n\nexport function PersonDetailTabs(props: PersonDetailTabsProps) {\n const ResolvedTabs = useRegisteredComponent<PersonDetailTabsProps>(\n PERSON_DETAIL_TABS_COMPONENT_ID,\n DefaultPersonDetailTabs,\n )\n return <ResolvedTabs {...props} />\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport {\n SquareCheckBig,\n Mail,\n Briefcase,\n Building2,\n Check,\n History,\n Paperclip,\n Plus,\n MapPin,\n} from 'lucide-react'\nimport type { SectionAction } from '@open-mercato/ui/backend/detail'\nimport { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\n\nexport const PERSON_DETAIL_TAB_IDS = ['activities', 'emails', 'deals', 'companies', 'addresses', 'tasks', 'changelog', 'files'] as const\n\nexport type PersonTabId =\n | 'activities'\n | 'emails'\n | 'deals'\n | 'companies'\n | 'addresses'\n | 'tasks'\n | 'changelog'\n | 'files'\n | string\n\ntype TabDef = {\n id: PersonTabId\n label: string\n icon?: React.ReactNode\n count?: React.ReactNode\n}\n\nexport const PERSON_DETAIL_TABS_COMPONENT_ID = 'section:customers.people.detailTabs'\n\nexport type PersonDetailTabsProps = {\n activeTab: PersonTabId\n onTabChange: (tab: PersonTabId) => void\n injectedTabs?: Array<{ id: string; label: string }>\n isLoadingInjectedTabs?: boolean\n hiddenTabIds?: string[]\n activitiesCount?: number\n dealsCount?: number\n companiesCount?: number\n addressesCount?: number\n tasksCount?: number\n filesCount?: number\n sectionAction?: SectionAction | null\n children: React.ReactNode\n}\n\nconst SUPPORTED_TAB_IDS = new Set<PersonTabId>(['activities', 'emails', 'deals', 'companies', 'addresses', 'tasks', 'changelog', 'files'])\n\nexport function resolveLegacyTab(tab: string | null | undefined, knownTabIds?: Iterable<string>): PersonTabId {\n if (!tab) return 'activities'\n if (SUPPORTED_TAB_IDS.has(tab as PersonTabId)) return tab as PersonTabId\n if (knownTabIds && new Set(knownTabIds).has(tab)) return tab\n return 'activities'\n}\n\nfunction formatTabCount(count: number): string | number | undefined {\n if (count <= 0) return undefined\n return count > 999 ? '999+' : count\n}\n\nfunction DefaultPersonDetailTabs({\n activeTab,\n onTabChange,\n injectedTabs = [],\n isLoadingInjectedTabs = false,\n hiddenTabIds = [],\n activitiesCount = 0,\n dealsCount = 0,\n companiesCount = 0,\n addressesCount = 0,\n tasksCount = 0,\n filesCount = 0,\n sectionAction = null,\n children,\n}: PersonDetailTabsProps) {\n const t = useT()\n\n const builtInTabs: TabDef[] = React.useMemo(\n () => [\n {\n id: 'activities',\n label: t('customers.people.detail.tabs.activities', 'Activities'),\n icon: <SquareCheckBig className=\"size-4\" />,\n count: formatTabCount(activitiesCount),\n },\n {\n id: 'emails',\n label: t('customers.people.detail.tabs.emails', 'Emails'),\n icon: <Mail className=\"size-4\" />,\n },\n {\n id: 'deals',\n label: t('customers.people.detail.tabs.deals', 'Deals'),\n icon: <Briefcase className=\"size-4\" />,\n count: formatTabCount(dealsCount),\n },\n {\n id: 'companies',\n label: t('customers.people.detail.tabs.companies', 'Companies'),\n icon: <Building2 className=\"size-4\" />,\n count: formatTabCount(companiesCount),\n },\n {\n id: 'addresses',\n label: t('customers.people.detail.tabs.addresses', 'Addresses'),\n icon: <MapPin className=\"size-4\" />,\n count: formatTabCount(addressesCount),\n },\n {\n id: 'tasks',\n label: t('customers.people.detail.tabs.tasks', 'Tasks'),\n icon: <Check className=\"size-4\" />,\n count: formatTabCount(tasksCount),\n },\n {\n id: 'changelog',\n label: t('customers.people.detail.tabs.changelog', 'Change log'),\n icon: <History className=\"size-4\" />,\n count: 'NEW',\n },\n {\n id: 'files',\n label: t('customers.people.detail.tabs.files', 'Files'),\n icon: <Paperclip className=\"size-4\" />,\n count: formatTabCount(filesCount),\n },\n ],\n [t, activitiesCount, dealsCount, companiesCount, addressesCount, tasksCount, filesCount],\n )\n\n const allTabs: TabDef[] = React.useMemo(() => {\n const hidden = new Set(hiddenTabIds)\n return [\n ...builtInTabs,\n ...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({\n id: tab.id as PersonTabId,\n label: tab.label,\n })),\n ].filter((tab) => !hidden.has(tab.id))\n }, [builtInTabs, hiddenTabIds, injectedTabs])\n\n React.useEffect(() => {\n if (isLoadingInjectedTabs || allTabs.some((tab) => tab.id === activeTab)) return\n const fallback = allTabs[0]?.id\n if (fallback) onTabChange(fallback)\n }, [activeTab, allTabs, isLoadingInjectedTabs, onTabChange])\n\n return (\n <div>\n {/* Tab navigation \u2014 full width above both zones */}\n <div className=\"flex items-end justify-between gap-2 border-b\">\n <Tabs\n value={activeTab}\n onValueChange={(value) => onTabChange(value as PersonTabId)}\n variant=\"underline\"\n className=\"min-w-0 flex-1\"\n >\n <TabsList\n aria-label={t('customers.people.detail.tabs.label', 'Person detail sections')}\n className=\"-mb-px w-full overflow-x-auto border-b-0 px-1\"\n >\n {allTabs.map((tab) => (\n <TabsTrigger key={tab.id} value={tab.id} leading={tab.icon} count={tab.count}>\n {tab.label}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n {sectionAction ? (\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={sectionAction.onClick}\n disabled={sectionAction.disabled}\n className=\"mb-1.5 mr-1 shrink-0\"\n >\n <Plus className=\"mr-1.5 h-4 w-4\" />\n {sectionAction.label}\n </Button>\n ) : null}\n </div>\n\n {/* Two-column content below tabs */}\n <div className=\"pt-6\">\n {children}\n </div>\n </div>\n )\n}\n\nregisterComponent<PersonDetailTabsProps>({\n id: PERSON_DETAIL_TABS_COMPONENT_ID,\n component: DefaultPersonDetailTabs,\n metadata: {\n module: 'customers',\n description: 'Person detail tab navigation and content.',\n },\n})\n\nexport function PersonDetailTabs(props: PersonDetailTabsProps) {\n const ResolvedTabs = useRegisteredComponent<PersonDetailTabsProps>(\n PERSON_DETAIL_TABS_COMPONENT_ID,\n DefaultPersonDetailTabs,\n )\n return <ResolvedTabs {...props} />\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,10 +8,13 @@ import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
|
8
8
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
9
9
|
import { createCrud } from "@open-mercato/ui/backend/utils/crud";
|
|
10
10
|
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
11
|
+
import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
|
|
11
12
|
import { useCurrentUserId } from "@open-mercato/ui/backend/utils/useCurrentUserId";
|
|
12
13
|
import { FormHeader } from "@open-mercato/ui/backend/forms";
|
|
13
14
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
14
15
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
16
|
+
import { E } from "../../../../../generated/entities.ids.generated.js";
|
|
17
|
+
import { extensionPoints } from "../../../extension-points.js";
|
|
15
18
|
import { dealFormSchema } from "../DealForm.js";
|
|
16
19
|
import { createDictionarySelectLabels } from "../utils.js";
|
|
17
20
|
import { DealSectionCard } from "./DealSectionCard.js";
|
|
@@ -24,6 +27,19 @@ import { EMPTY_VALUES } from "./dealFormTypes.js";
|
|
|
24
27
|
const CONTEXT_ID = "customers.deals.create";
|
|
25
28
|
const DEAL_ENTITY_ID = "customers:customer_deal";
|
|
26
29
|
const CUSTOM_FIELDS_MANAGE_HREF = `/backend/entities/system/${encodeURIComponent(DEAL_ENTITY_ID)}`;
|
|
30
|
+
function readCreateValues(values) {
|
|
31
|
+
const result = { ...EMPTY_VALUES };
|
|
32
|
+
for (const key of Object.keys(EMPTY_VALUES)) {
|
|
33
|
+
if (key === "personIds" || key === "companyIds") {
|
|
34
|
+
const raw = values[key];
|
|
35
|
+
result[key] = Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : [];
|
|
36
|
+
} else {
|
|
37
|
+
const raw = values[key];
|
|
38
|
+
result[key] = typeof raw === "string" || typeof raw === "number" ? String(raw) : "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
27
43
|
function CreateDealForm({ returnTo, initialValues }) {
|
|
28
44
|
const t = useT();
|
|
29
45
|
const router = useRouter();
|
|
@@ -31,108 +47,51 @@ function CreateDealForm({ returnTo, initialValues }) {
|
|
|
31
47
|
(key, fallback, params) => translateWithFallback(t, key, fallback, params),
|
|
32
48
|
[t]
|
|
33
49
|
);
|
|
34
|
-
const [values, setValues] = React.useState(() => {
|
|
35
|
-
const definedSeedEntries = Object.entries(initialValues ?? {}).filter(([, seedValue]) => seedValue !== void 0);
|
|
36
|
-
return { ...EMPTY_VALUES, ...Object.fromEntries(definedSeedEntries) };
|
|
37
|
-
});
|
|
38
|
-
const [errors, setErrors] = React.useState({});
|
|
39
|
-
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
40
50
|
const currentUserId = useCurrentUserId();
|
|
41
51
|
const ownerInitialOption = React.useMemo(
|
|
42
52
|
() => currentUserId ? { id: currentUserId, name: tr("customers.filters.currentUser", "Current user") } : null,
|
|
43
53
|
[currentUserId, tr]
|
|
44
54
|
);
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}, [
|
|
55
|
+
const seedValues = React.useMemo(() => {
|
|
56
|
+
const seeded = {
|
|
57
|
+
...EMPTY_VALUES,
|
|
58
|
+
...Object.fromEntries(Object.entries(initialValues ?? {}).filter(([, value]) => value !== void 0))
|
|
59
|
+
};
|
|
60
|
+
return seeded.ownerUserId || !currentUserId ? seeded : { ...seeded, ownerUserId: currentUserId };
|
|
61
|
+
}, [initialValues, currentUserId]);
|
|
62
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
63
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({ contextId: CONTEXT_ID, blockedMessage: tr("ui.forms.flash.saveBlocked", "Save blocked by validation") });
|
|
52
64
|
const { pipelines, stages, loadStages } = useDealPipelines();
|
|
53
|
-
const {
|
|
54
|
-
customValues,
|
|
55
|
-
customFieldsLoaded,
|
|
56
|
-
customCount,
|
|
57
|
-
handleCustomChange,
|
|
58
|
-
handleCustomAttributesLoaded,
|
|
59
|
-
validateCustomFields,
|
|
60
|
-
collectNormalizedCustomValues
|
|
61
|
-
} = useDealCustomFields(tr);
|
|
62
|
-
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
63
|
-
contextId: CONTEXT_ID,
|
|
64
|
-
blockedMessage: tr("ui.forms.flash.saveBlocked", "Save blocked by validation")
|
|
65
|
-
});
|
|
65
|
+
const { customFieldsLoaded, customCount, handleCustomAttributesLoaded, collectNormalizedCustomValues } = useDealCustomFields(tr);
|
|
66
66
|
const statusLabels = React.useMemo(
|
|
67
67
|
() => createDictionarySelectLabels("deal-statuses", (key, fallback) => tr(key, fallback ?? key)),
|
|
68
68
|
[tr]
|
|
69
69
|
);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const parsed = dealFormSchema.safeParse(merged);
|
|
93
|
-
if (!parsed.success) {
|
|
94
|
-
const fieldErrors = {};
|
|
95
|
-
for (const issue of parsed.error.issues) {
|
|
96
|
-
const key = typeof issue.path[0] === "string" ? issue.path[0] : void 0;
|
|
97
|
-
if (key && !fieldErrors[key]) fieldErrors[key] = tr(issue.message, issue.message);
|
|
98
|
-
}
|
|
99
|
-
setErrors(fieldErrors);
|
|
100
|
-
const firstMessage = Object.values(fieldErrors)[0];
|
|
101
|
-
if (firstMessage) flash(firstMessage, "error");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const customFieldErrors = validateCustomFields(merged);
|
|
105
|
-
if (Object.keys(customFieldErrors).length) {
|
|
106
|
-
setErrors(customFieldErrors);
|
|
107
|
-
const firstMessage = Object.values(customFieldErrors)[0];
|
|
108
|
-
if (firstMessage) flash(firstMessage, "error");
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
setErrors({});
|
|
70
|
+
const handleCancel = React.useCallback(() => router.push(returnTo), [returnTo, router]);
|
|
71
|
+
const handleSubmit = React.useCallback(async (values) => {
|
|
72
|
+
const data = dealFormSchema.parse(values);
|
|
73
|
+
const payload = {
|
|
74
|
+
title: data.title,
|
|
75
|
+
status: data.status || void 0,
|
|
76
|
+
pipelineId: data.pipelineId || void 0,
|
|
77
|
+
pipelineStageId: data.pipelineStageId || void 0,
|
|
78
|
+
valueAmount: typeof data.valueAmount === "number" ? data.valueAmount : void 0,
|
|
79
|
+
valueCurrency: data.valueCurrency || void 0,
|
|
80
|
+
probability: typeof data.probability === "number" ? data.probability : void 0,
|
|
81
|
+
expectedCloseAt: data.expectedCloseAt ? new Date(data.expectedCloseAt).toISOString() : void 0,
|
|
82
|
+
description: data.description || void 0,
|
|
83
|
+
// `null`, not omitted — matching DealForm, so both create surfaces send the same
|
|
84
|
+
// shape. `dealCreateSchema.ownerUserId` is `.optional().nullable()` and
|
|
85
|
+
// createDealCommand maps null to null.
|
|
86
|
+
ownerUserId: data.ownerUserId && data.ownerUserId.length ? data.ownerUserId : null,
|
|
87
|
+
personIds: data.personIds?.length ? data.personIds : void 0,
|
|
88
|
+
companyIds: data.companyIds?.length ? data.companyIds : void 0
|
|
89
|
+
};
|
|
90
|
+
const custom = collectNormalizedCustomValues(values);
|
|
91
|
+
if (Object.keys(custom).length) payload.customFields = custom;
|
|
112
92
|
setIsSubmitting(true);
|
|
113
93
|
try {
|
|
114
|
-
const
|
|
115
|
-
const expectedCloseAt = data.expectedCloseAt && data.expectedCloseAt.length ? new Date(data.expectedCloseAt).toISOString() : void 0;
|
|
116
|
-
const payload = {
|
|
117
|
-
title: data.title,
|
|
118
|
-
status: data.status || void 0,
|
|
119
|
-
pipelineId: data.pipelineId || void 0,
|
|
120
|
-
pipelineStageId: data.pipelineStageId || void 0,
|
|
121
|
-
valueAmount: typeof data.valueAmount === "number" ? data.valueAmount : void 0,
|
|
122
|
-
valueCurrency: data.valueCurrency || void 0,
|
|
123
|
-
probability: typeof data.probability === "number" ? data.probability : void 0,
|
|
124
|
-
expectedCloseAt,
|
|
125
|
-
description: data.description && data.description.length ? data.description : void 0,
|
|
126
|
-
// `null`, not omitted — matching DealForm, so both create surfaces send the same
|
|
127
|
-
// shape. `dealCreateSchema.ownerUserId` is `.optional().nullable()` (validators.ts:183)
|
|
128
|
-
// and createDealCommand maps null to null (commands/deals.ts:591).
|
|
129
|
-
ownerUserId: data.ownerUserId && data.ownerUserId.length ? data.ownerUserId : null,
|
|
130
|
-
personIds: values.personIds.length ? values.personIds : void 0,
|
|
131
|
-
companyIds: values.companyIds.length ? values.companyIds : void 0
|
|
132
|
-
};
|
|
133
|
-
const custom = collectNormalizedCustomValues(merged);
|
|
134
|
-
if (Object.keys(custom).length) payload.customFields = custom;
|
|
135
|
-
await runMutation({
|
|
94
|
+
const { result } = await runMutation({
|
|
136
95
|
operation: () => createCrud("customers/deals", payload, {
|
|
137
96
|
errorMessage: tr("customers.deals.create.error", "Failed to create deal.")
|
|
138
97
|
}),
|
|
@@ -140,111 +99,136 @@ function CreateDealForm({ returnTo, initialValues }) {
|
|
|
140
99
|
mutationPayload: payload
|
|
141
100
|
});
|
|
142
101
|
flash(tr("customers.people.detail.deals.success", "Deal created."), "success");
|
|
143
|
-
|
|
144
|
-
} catch (err) {
|
|
145
|
-
const message = err instanceof Error ? err.message : tr("customers.deals.create.error", "Failed to create deal.");
|
|
146
|
-
flash(message, "error");
|
|
102
|
+
return { resourceId: result?.id ?? result?.entityId };
|
|
147
103
|
} finally {
|
|
148
104
|
setIsSubmitting(false);
|
|
149
105
|
}
|
|
150
|
-
}, [
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
);
|
|
172
|
-
const onFormSubmit = React.useCallback(
|
|
173
|
-
(event) => {
|
|
174
|
-
event.preventDefault();
|
|
175
|
-
handleSubmit();
|
|
176
|
-
},
|
|
177
|
-
[handleSubmit]
|
|
178
|
-
);
|
|
179
|
-
const cancelLabel = tr("customers.deals.create.cancel", "Cancel");
|
|
180
|
-
const submitLabel = tr("customers.deals.create.submit", "Create deal");
|
|
181
|
-
const submitDisabled = !customFieldsLoaded;
|
|
182
|
-
return /* @__PURE__ */ jsxs("form", { className: "mx-auto max-w-screen-2xl", onKeyDown, onSubmit: onFormSubmit, noValidate: true, children: [
|
|
183
|
-
/* @__PURE__ */ jsx(
|
|
184
|
-
FormHeader,
|
|
185
|
-
{
|
|
186
|
-
backHref: returnTo,
|
|
187
|
-
backLabel: tr("customers.deals.create.back", "Back to deals")
|
|
188
|
-
}
|
|
189
|
-
),
|
|
190
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-6 grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_330px]", children: [
|
|
191
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
192
|
-
/* @__PURE__ */ jsx(
|
|
106
|
+
}, [collectNormalizedCustomValues, tr, runMutation, retryLastMutation]);
|
|
107
|
+
const handleSubmitSuccess = React.useCallback(() => {
|
|
108
|
+
router.push(returnTo);
|
|
109
|
+
}, [router, returnTo]);
|
|
110
|
+
const fields = React.useMemo(() => Object.keys(EMPTY_VALUES).map((id) => ({
|
|
111
|
+
id,
|
|
112
|
+
label: tr(`customers.people.detail.deals.fields.${id}`, id),
|
|
113
|
+
type: "text",
|
|
114
|
+
required: id === "title"
|
|
115
|
+
})), [tr]);
|
|
116
|
+
const groups = React.useMemo(() => [
|
|
117
|
+
{
|
|
118
|
+
id: "details",
|
|
119
|
+
bare: true,
|
|
120
|
+
column: 1,
|
|
121
|
+
fields: Object.keys(EMPTY_VALUES).filter((id) => id !== "personIds" && id !== "companyIds"),
|
|
122
|
+
component: ({ values, setValue, errors, injectedFields }) => {
|
|
123
|
+
const patch = (partial) => {
|
|
124
|
+
for (const [key, value] of Object.entries(partial)) setValue(key, value);
|
|
125
|
+
};
|
|
126
|
+
return /* @__PURE__ */ jsxs(
|
|
193
127
|
DealSectionCard,
|
|
194
128
|
{
|
|
195
129
|
icon: Briefcase,
|
|
196
130
|
title: tr("customers.deals.create.title", "Create deal"),
|
|
197
131
|
subtitle: tr("customers.deals.create.sections.details.subtitle", "Core opportunity info"),
|
|
198
132
|
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
199
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: handleCancel, disabled: isSubmitting, children:
|
|
200
|
-
/* @__PURE__ */ jsxs(Button, { type: "
|
|
133
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: handleCancel, disabled: isSubmitting, children: tr("customers.deals.create.cancel", "Cancel") }),
|
|
134
|
+
/* @__PURE__ */ jsxs(Button, { type: "submit", form: CONTEXT_ID, disabled: isSubmitting || !customFieldsLoaded, children: [
|
|
201
135
|
isSubmitting ? /* @__PURE__ */ jsx(Spinner, { className: "size-4" }) : /* @__PURE__ */ jsx(Save, { className: "size-4" }),
|
|
202
|
-
|
|
136
|
+
tr("customers.deals.create.submit", "Create deal")
|
|
203
137
|
] })
|
|
204
138
|
] }),
|
|
205
|
-
children:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ jsx(
|
|
141
|
+
DealDetailsFields,
|
|
142
|
+
{
|
|
143
|
+
values: readCreateValues(values),
|
|
144
|
+
errors,
|
|
145
|
+
isSubmitting,
|
|
146
|
+
patch,
|
|
147
|
+
onPipelineChange: (pipelineId) => {
|
|
148
|
+
patch({ pipelineId, pipelineStageId: "" });
|
|
149
|
+
void loadStages(pipelineId).catch(() => {
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
pipelines,
|
|
153
|
+
stages,
|
|
154
|
+
statusLabels,
|
|
155
|
+
ownerInitialOption,
|
|
156
|
+
tr
|
|
157
|
+
}
|
|
158
|
+
),
|
|
159
|
+
injectedFields
|
|
160
|
+
]
|
|
220
161
|
}
|
|
221
|
-
)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "associations",
|
|
167
|
+
bare: true,
|
|
168
|
+
column: 1,
|
|
169
|
+
fields: ["personIds", "companyIds"],
|
|
170
|
+
component: ({ values, setValue, injectedFields }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
222
171
|
/* @__PURE__ */ jsx(
|
|
223
172
|
DealAssociationsSection,
|
|
224
173
|
{
|
|
225
174
|
tr,
|
|
226
|
-
personIds: values.personIds,
|
|
227
|
-
companyIds: values.companyIds,
|
|
228
|
-
onPeopleChange: (next) =>
|
|
229
|
-
onCompaniesChange: (next) =>
|
|
175
|
+
personIds: readCreateValues(values).personIds,
|
|
176
|
+
companyIds: readCreateValues(values).companyIds,
|
|
177
|
+
onPeopleChange: (next) => setValue("personIds", next),
|
|
178
|
+
onCompaniesChange: (next) => setValue("companyIds", next),
|
|
230
179
|
disabled: isSubmitting
|
|
231
180
|
}
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
181
|
+
),
|
|
182
|
+
injectedFields
|
|
183
|
+
] })
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "custom",
|
|
187
|
+
bare: true,
|
|
188
|
+
column: 2,
|
|
189
|
+
component: ({ values, setValue, errors, injectedFields }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
190
|
+
/* @__PURE__ */ jsx(
|
|
191
|
+
DealCreateSidebar,
|
|
192
|
+
{
|
|
193
|
+
tr,
|
|
194
|
+
customValues: values,
|
|
195
|
+
onCustomChange: setValue,
|
|
196
|
+
errors,
|
|
197
|
+
disabled: isSubmitting,
|
|
198
|
+
customCount,
|
|
199
|
+
manageHref: CUSTOM_FIELDS_MANAGE_HREF,
|
|
200
|
+
onCustomLoaded: handleCustomAttributesLoaded
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
injectedFields
|
|
204
|
+
] })
|
|
205
|
+
}
|
|
206
|
+
], [tr, handleCancel, isSubmitting, customFieldsLoaded, loadStages, pipelines, stages, statusLabels, ownerInitialOption, customCount, handleCustomAttributesLoaded]);
|
|
207
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-screen-2xl", onKeyDown: (event) => {
|
|
208
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
if (!isSubmitting && customFieldsLoaded) document.getElementById(CONTEXT_ID)?.closest("form")?.requestSubmit();
|
|
211
|
+
}
|
|
212
|
+
}, children: [
|
|
213
|
+
/* @__PURE__ */ jsx(FormHeader, { backHref: returnTo, backLabel: tr("customers.deals.create.back", "Back to deals") }),
|
|
214
|
+
/* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(
|
|
215
|
+
CrudForm,
|
|
216
|
+
{
|
|
217
|
+
embedded: true,
|
|
218
|
+
hideFooterActions: true,
|
|
219
|
+
formId: CONTEXT_ID,
|
|
220
|
+
entityId: "customers.deal",
|
|
221
|
+
resourceKind: "customers.deal",
|
|
222
|
+
entityIds: [E.customers.customer_deal],
|
|
223
|
+
injectionSpotId: extensionPoints.hosts.dealForm.spotId,
|
|
224
|
+
fields,
|
|
225
|
+
groups,
|
|
226
|
+
initialValues: seedValues,
|
|
227
|
+
schema: dealFormSchema,
|
|
228
|
+
onSubmit: handleSubmit,
|
|
229
|
+
onSubmitSuccess: handleSubmitSuccess
|
|
230
|
+
}
|
|
231
|
+
) })
|
|
248
232
|
] });
|
|
249
233
|
}
|
|
250
234
|
var CreateDealForm_default = CreateDealForm;
|