@open-mercato/core 0.6.3-develop.3894.1.352abf4240 → 0.6.3

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 (101) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/global.d.js +1 -0
  3. package/dist/global.d.js.map +7 -0
  4. package/dist/modules/catalog/commands/variants.js +11 -5
  5. package/dist/modules/catalog/commands/variants.js.map +2 -2
  6. package/dist/modules/customers/backend/customers/deals/create/page.js +3 -61
  7. package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
  8. package/dist/modules/customers/components/detail/DealForm.js +2 -0
  9. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  10. package/dist/modules/customers/components/detail/create/CreateDealForm.js +233 -0
  11. package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +7 -0
  12. package/dist/modules/customers/components/detail/create/DealAssociationsField.js +209 -0
  13. package/dist/modules/customers/components/detail/create/DealAssociationsField.js.map +7 -0
  14. package/dist/modules/customers/components/detail/create/DealAssociationsSection.js +67 -0
  15. package/dist/modules/customers/components/detail/create/DealAssociationsSection.js.map +7 -0
  16. package/dist/modules/customers/components/detail/create/DealCreateSidebar.js +73 -0
  17. package/dist/modules/customers/components/detail/create/DealCreateSidebar.js.map +7 -0
  18. package/dist/modules/customers/components/detail/create/DealCurrencyField.js +92 -0
  19. package/dist/modules/customers/components/detail/create/DealCurrencyField.js.map +7 -0
  20. package/dist/modules/customers/components/detail/create/DealCustomAttributes.js +81 -0
  21. package/dist/modules/customers/components/detail/create/DealCustomAttributes.js.map +7 -0
  22. package/dist/modules/customers/components/detail/create/DealDetailsFields.js +171 -0
  23. package/dist/modules/customers/components/detail/create/DealDetailsFields.js.map +7 -0
  24. package/dist/modules/customers/components/detail/create/DealFormField.js +24 -0
  25. package/dist/modules/customers/components/detail/create/DealFormField.js.map +7 -0
  26. package/dist/modules/customers/components/detail/create/DealSectionCard.js +29 -0
  27. package/dist/modules/customers/components/detail/create/DealSectionCard.js.map +7 -0
  28. package/dist/modules/customers/components/detail/create/DealTipsCard.js +19 -0
  29. package/dist/modules/customers/components/detail/create/DealTipsCard.js.map +7 -0
  30. package/dist/modules/customers/components/detail/create/PipelineSelect.js +41 -0
  31. package/dist/modules/customers/components/detail/create/PipelineSelect.js.map +7 -0
  32. package/dist/modules/customers/components/detail/create/PipelineStageSelect.js +49 -0
  33. package/dist/modules/customers/components/detail/create/PipelineStageSelect.js.map +7 -0
  34. package/dist/modules/customers/components/detail/create/SuffixInput.js +21 -0
  35. package/dist/modules/customers/components/detail/create/SuffixInput.js.map +7 -0
  36. package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js +270 -0
  37. package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js.map +7 -0
  38. package/dist/modules/customers/components/detail/create/dealFormTypes.js +17 -0
  39. package/dist/modules/customers/components/detail/create/dealFormTypes.js.map +7 -0
  40. package/dist/modules/customers/components/detail/create/dealNumericInput.js +16 -0
  41. package/dist/modules/customers/components/detail/create/dealNumericInput.js.map +7 -0
  42. package/dist/modules/customers/components/detail/create/useDealCustomFields.js +93 -0
  43. package/dist/modules/customers/components/detail/create/useDealCustomFields.js.map +7 -0
  44. package/dist/modules/customers/components/detail/create/useDealPipelines.js +59 -0
  45. package/dist/modules/customers/components/detail/create/useDealPipelines.js.map +7 -0
  46. package/dist/modules/customers/components/formConfig.js +4 -2
  47. package/dist/modules/customers/components/formConfig.js.map +2 -2
  48. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +5 -2
  49. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
  50. package/dist/modules/feature_toggles/lib/feature-flag-check.js +13 -5
  51. package/dist/modules/feature_toggles/lib/feature-flag-check.js.map +2 -2
  52. package/dist/modules/query_index/subscribers/coverage_refresh.js +6 -1
  53. package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
  54. package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js +33 -5
  55. package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js.map +2 -2
  56. package/dist/modules/workflows/components/WorkflowGraph.js +29 -186
  57. package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
  58. package/dist/modules/workflows/components/WorkflowGraphImpl.js +196 -0
  59. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +7 -0
  60. package/package.json +8 -9
  61. package/src/global.d.ts +9 -0
  62. package/src/modules/catalog/commands/variants.ts +14 -5
  63. package/src/modules/customers/backend/customers/deals/create/page.tsx +3 -64
  64. package/src/modules/customers/components/detail/DealForm.tsx +2 -0
  65. package/src/modules/customers/components/detail/create/CreateDealForm.tsx +254 -0
  66. package/src/modules/customers/components/detail/create/DealAssociationsField.tsx +253 -0
  67. package/src/modules/customers/components/detail/create/DealAssociationsSection.tsx +72 -0
  68. package/src/modules/customers/components/detail/create/DealCreateSidebar.tsx +79 -0
  69. package/src/modules/customers/components/detail/create/DealCurrencyField.tsx +108 -0
  70. package/src/modules/customers/components/detail/create/DealCustomAttributes.tsx +118 -0
  71. package/src/modules/customers/components/detail/create/DealDetailsFields.tsx +171 -0
  72. package/src/modules/customers/components/detail/create/DealFormField.tsx +39 -0
  73. package/src/modules/customers/components/detail/create/DealSectionCard.tsx +40 -0
  74. package/src/modules/customers/components/detail/create/DealTipsCard.tsx +26 -0
  75. package/src/modules/customers/components/detail/create/PipelineSelect.tsx +55 -0
  76. package/src/modules/customers/components/detail/create/PipelineStageSelect.tsx +70 -0
  77. package/src/modules/customers/components/detail/create/SuffixInput.tsx +20 -0
  78. package/src/modules/customers/components/detail/create/dealCustomFieldControl.tsx +310 -0
  79. package/src/modules/customers/components/detail/create/dealFormTypes.ts +29 -0
  80. package/src/modules/customers/components/detail/create/dealNumericInput.ts +20 -0
  81. package/src/modules/customers/components/detail/create/useDealCustomFields.ts +118 -0
  82. package/src/modules/customers/components/detail/create/useDealPipelines.ts +80 -0
  83. package/src/modules/customers/components/formConfig.tsx +3 -0
  84. package/src/modules/customers/i18n/de.json +26 -0
  85. package/src/modules/customers/i18n/en.json +26 -0
  86. package/src/modules/customers/i18n/es.json +26 -0
  87. package/src/modules/customers/i18n/pl.json +26 -0
  88. package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +12 -1
  89. package/src/modules/feature_toggles/lib/feature-flag-check.ts +14 -4
  90. package/src/modules/query_index/subscribers/coverage_refresh.ts +7 -1
  91. package/src/modules/resources/i18n/de.json +1 -0
  92. package/src/modules/resources/i18n/en.json +1 -0
  93. package/src/modules/resources/i18n/es.json +1 -0
  94. package/src/modules/resources/i18n/pl.json +1 -0
  95. package/src/modules/sales/i18n/de.json +2 -0
  96. package/src/modules/sales/i18n/en.json +2 -0
  97. package/src/modules/sales/i18n/es.json +2 -0
  98. package/src/modules/sales/i18n/pl.json +2 -0
  99. package/src/modules/sync_excel/widgets/injection/upload-config/target-options.ts +40 -5
  100. package/src/modules/workflows/components/WorkflowGraph.tsx +39 -235
  101. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +233 -0
@@ -0,0 +1,79 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Sparkles } from 'lucide-react'
5
+ import { DealSectionCard } from './DealSectionCard'
6
+ import { DealTipsCard } from './DealTipsCard'
7
+ import {
8
+ DealCustomAttributes,
9
+ type DealCustomAttributesLoadState,
10
+ } from './DealCustomAttributes'
11
+ import type { Translate } from './dealFormTypes'
12
+
13
+ export type DealCreateSidebarProps = {
14
+ tr: Translate
15
+ customValues: Record<string, unknown>
16
+ onCustomChange: (key: string, value: unknown) => void
17
+ errors: Record<string, string>
18
+ disabled: boolean
19
+ customCount: number
20
+ manageHref: string
21
+ onCustomLoaded: (state: DealCustomAttributesLoadState) => void
22
+ }
23
+
24
+ export function DealCreateSidebar({
25
+ tr,
26
+ customValues,
27
+ onCustomChange,
28
+ errors,
29
+ disabled,
30
+ customCount,
31
+ manageHref,
32
+ onCustomLoaded,
33
+ }: DealCreateSidebarProps) {
34
+ return (
35
+ <div className="space-y-4">
36
+ <DealSectionCard
37
+ icon={Sparkles}
38
+ title={tr('customers.deals.create.sections.custom.title', 'Custom attributes')}
39
+ subtitle={tr('customers.deals.create.sections.custom.subtitle', '{count} fields defined for this tenant', {
40
+ count: customCount,
41
+ })}
42
+ >
43
+ <DealCustomAttributes
44
+ values={customValues}
45
+ onChange={onCustomChange}
46
+ errors={errors}
47
+ disabled={disabled}
48
+ manageHref={manageHref}
49
+ labels={{
50
+ manage: tr('customers.deals.create.sections.custom.manage', 'Manage fields'),
51
+ empty: tr('customers.deals.create.sections.custom.empty', 'No custom fields defined for deals yet.'),
52
+ loading: tr('customers.deals.create.sections.custom.loading', 'Loading custom fields…'),
53
+ }}
54
+ onLoaded={onCustomLoaded}
55
+ />
56
+ </DealSectionCard>
57
+
58
+ <DealTipsCard
59
+ title={tr('customers.deals.create.tips.title', 'Tips for better deals')}
60
+ tips={[
61
+ tr(
62
+ 'customers.deals.create.tips.item1',
63
+ 'Use the company name + short deliverable format in the title (e.g. "Copperleaf — Q3 Renewal")',
64
+ ),
65
+ tr(
66
+ 'customers.deals.create.tips.item2',
67
+ 'Set probability based on pipeline stage: Qual 10-25%, Proposal 30-50%, Negotiation 50-75%, Contract 75-90%',
68
+ ),
69
+ tr(
70
+ 'customers.deals.create.tips.item3',
71
+ 'Link primary decision maker as first person — they get default email CC on activities',
72
+ ),
73
+ ]}
74
+ />
75
+ </div>
76
+ )
77
+ }
78
+
79
+ export default DealCreateSidebar
@@ -0,0 +1,108 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '@open-mercato/shared/lib/utils'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { DictionaryEntrySelect } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
7
+ import { useCurrencyDictionary } from '../hooks/useCurrencyDictionary'
8
+
9
+ const CURRENCY_PRIORITY = ['EUR', 'USD', 'GBP', 'PLN'] as const
10
+
11
+ export type DealCurrencyFieldProps = {
12
+ id?: string
13
+ value: string
14
+ onChange: (code: string) => void
15
+ disabled?: boolean
16
+ }
17
+
18
+ export function DealCurrencyField({ id, value, onChange, disabled = false }: DealCurrencyFieldProps) {
19
+ const t = useT()
20
+ const { data, error: rawError, isLoading, refetch } = useCurrencyDictionary()
21
+ const dictError = rawError
22
+ ? rawError instanceof Error
23
+ ? rawError.message
24
+ : String(rawError)
25
+ : null
26
+
27
+ const resolvedError = React.useMemo(() => {
28
+ if (dictError) return dictError
29
+ if (!isLoading && !data) {
30
+ return t('customers.deals.form.currency.missing', 'Currency dictionary is not configured yet.')
31
+ }
32
+ return null
33
+ }, [data, dictError, isLoading, t])
34
+
35
+ const fetchOptions = React.useCallback(async () => {
36
+ let payload = data ?? null
37
+ if (!payload) {
38
+ try {
39
+ payload = await refetch()
40
+ } catch (err) {
41
+ const message = err instanceof Error ? err.message : String(err ?? '')
42
+ throw new Error(message || t('customers.deals.form.currency.error', 'Failed to load currency dictionary.'))
43
+ }
44
+ }
45
+ if (!payload) {
46
+ throw new Error(t('customers.deals.form.currency.missing', 'Currency dictionary is not configured yet.'))
47
+ }
48
+ const priorityOrder = new Map<string, number>()
49
+ CURRENCY_PRIORITY.forEach((code, index) => priorityOrder.set(code, index))
50
+ const prioritized: { value: string; label: string; color: string | null; icon: string | null }[] = []
51
+ const remainder: { value: string; label: string; color: string | null; icon: string | null }[] = []
52
+ payload.entries.forEach((entry) => {
53
+ const code = entry.value.toUpperCase()
54
+ const label = entry.label && entry.label.length ? `${code} – ${entry.label}` : code
55
+ const option = { value: code, label, color: null, icon: null }
56
+ if (priorityOrder.has(code)) prioritized.push(option)
57
+ else remainder.push(option)
58
+ })
59
+ prioritized.sort((a, b) => priorityOrder.get(a.value)! - priorityOrder.get(b.value)!)
60
+ remainder.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }))
61
+ return [...prioritized, ...remainder]
62
+ }, [data, refetch, t])
63
+
64
+ const labels = React.useMemo(
65
+ () => ({
66
+ placeholder: t('customers.deals.form.currency.placeholder', 'Select currency…'),
67
+ addLabel: t('customers.deals.form.currency.add', 'Add currency'),
68
+ dialogTitle: t('customers.deals.form.currency.dialogTitle', 'Add currency'),
69
+ valueLabel: t('customers.deals.form.currency.valueLabel', 'Currency code'),
70
+ valuePlaceholder: t('customers.deals.form.currency.valuePlaceholder', 'e.g. USD'),
71
+ labelLabel: t('customers.deals.form.currency.labelLabel', 'Label'),
72
+ labelPlaceholder: t('customers.deals.form.currency.labelPlaceholder', 'Display name shown in UI'),
73
+ emptyError: t('customers.deals.form.currency.error.required', 'Currency code is required.'),
74
+ cancelLabel: t('customers.deals.form.currency.cancel', 'Cancel'),
75
+ saveLabel: t('customers.deals.form.currency.save', 'Save'),
76
+ errorLoad: t('customers.deals.form.currency.error', 'Failed to load currency dictionary.'),
77
+ errorSave: t('customers.deals.form.currency.error', 'Failed to load currency dictionary.'),
78
+ loadingLabel: t('customers.deals.form.currency.loading', 'Loading currencies…'),
79
+ manageTitle: t('customers.deals.form.currency.manage', 'Manage currency dictionary'),
80
+ }),
81
+ [t],
82
+ )
83
+
84
+ return (
85
+ <div className="space-y-1">
86
+ <DictionaryEntrySelect
87
+ id={id}
88
+ value={value || undefined}
89
+ onChange={(next) => onChange(next ?? '')}
90
+ fetchOptions={fetchOptions}
91
+ labels={labels}
92
+ manageHref="/backend/config/dictionaries?key=currency"
93
+ allowInlineCreate={false}
94
+ allowAppearance={false}
95
+ selectClassName="w-full"
96
+ disabled={disabled}
97
+ showLabelInput={false}
98
+ />
99
+ {resolvedError ? (
100
+ <p className={cn('text-xs', dictError ? 'text-status-error-text' : 'text-muted-foreground')}>
101
+ {resolvedError}
102
+ </p>
103
+ ) : null}
104
+ </div>
105
+ )
106
+ }
107
+
108
+ export default DealCurrencyField
@@ -0,0 +1,118 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Plus } from 'lucide-react'
5
+ import type { CrudField } from '@open-mercato/ui/backend/CrudForm'
6
+ import type { CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
7
+ import { fetchCustomFieldFormStructure } from '@open-mercato/ui/backend/utils/customFieldForms'
8
+ import { LinkButton } from '@open-mercato/ui/primitives/link-button'
9
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
10
+ import { E } from '#generated/entities.ids.generated'
11
+ import { DealCustomFieldControl } from './dealCustomFieldControl'
12
+
13
+ export type DealCustomAttributesLoadState = {
14
+ fields: CrudField[]
15
+ definitions: CustomFieldDefDto[]
16
+ }
17
+
18
+ export type DealCustomAttributesProps = {
19
+ values: Record<string, unknown>
20
+ onChange: (key: string, value: unknown) => void
21
+ errors?: Record<string, string>
22
+ disabled?: boolean
23
+ manageHref: string
24
+ labels: {
25
+ manage: string
26
+ empty: string
27
+ loading: string
28
+ }
29
+ onLoaded?: (state: DealCustomAttributesLoadState) => void
30
+ }
31
+
32
+ function ManageFieldsLink({ href, label }: { href: string; label: string }) {
33
+ return (
34
+ <LinkButton asChild variant="gray" size="sm">
35
+ <a href={href} className="inline-flex items-center gap-1">
36
+ <Plus className="size-3.5" />
37
+ {label}
38
+ </a>
39
+ </LinkButton>
40
+ )
41
+ }
42
+
43
+ export function DealCustomAttributes({
44
+ values,
45
+ onChange,
46
+ errors,
47
+ disabled = false,
48
+ manageHref,
49
+ labels,
50
+ onLoaded,
51
+ }: DealCustomAttributesProps) {
52
+ const [fields, setFields] = React.useState<CrudField[]>([])
53
+ const [isLoading, setIsLoading] = React.useState(true)
54
+ const onLoadedRef = React.useRef(onLoaded)
55
+
56
+ React.useEffect(() => {
57
+ onLoadedRef.current = onLoaded
58
+ }, [onLoaded])
59
+
60
+ React.useEffect(() => {
61
+ let cancelled = false
62
+ setIsLoading(true)
63
+ fetchCustomFieldFormStructure([E.customers.customer_deal])
64
+ .then((result) => {
65
+ if (cancelled) return
66
+ setFields(result.fields)
67
+ setIsLoading(false)
68
+ onLoadedRef.current?.({ fields: result.fields, definitions: result.definitions })
69
+ })
70
+ .catch(() => {
71
+ if (cancelled) return
72
+ setFields([])
73
+ setIsLoading(false)
74
+ onLoadedRef.current?.({ fields: [], definitions: [] })
75
+ })
76
+ return () => {
77
+ cancelled = true
78
+ }
79
+ }, [])
80
+
81
+ if (isLoading) {
82
+ return (
83
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
84
+ <Spinner className="size-4" />
85
+ {labels.loading}
86
+ </div>
87
+ )
88
+ }
89
+
90
+ if (fields.length === 0) {
91
+ return (
92
+ <div className="space-y-3">
93
+ <p className="text-sm text-muted-foreground">{labels.empty}</p>
94
+ <ManageFieldsLink href={manageHref} label={labels.manage} />
95
+ </div>
96
+ )
97
+ }
98
+
99
+ return (
100
+ <div className="space-y-4">
101
+ {fields.map((field) => (
102
+ <DealCustomFieldControl
103
+ key={field.id}
104
+ field={field}
105
+ value={values[field.id]}
106
+ onChange={(next) => onChange(field.id, next)}
107
+ error={errors?.[field.id]}
108
+ disabled={disabled}
109
+ />
110
+ ))}
111
+ <div className="pt-1">
112
+ <ManageFieldsLink href={manageHref} label={labels.manage} />
113
+ </div>
114
+ </div>
115
+ )
116
+ }
117
+
118
+ export default DealCustomAttributes
@@ -0,0 +1,171 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { format } from 'date-fns/format'
5
+ import { parseISO } from 'date-fns/parseISO'
6
+ import { Input } from '@open-mercato/ui/primitives/input'
7
+ import { Textarea } from '@open-mercato/ui/primitives/textarea'
8
+ import { DatePicker } from '@open-mercato/ui/primitives/date-picker'
9
+ import { DictionarySelectField } from '../../formConfig'
10
+ import { createDictionarySelectLabels } from '../utils'
11
+ import { DealFormField } from './DealFormField'
12
+ import { PipelineSelect } from './PipelineSelect'
13
+ import { PipelineStageSelect } from './PipelineStageSelect'
14
+ import { SuffixInput } from './SuffixInput'
15
+ import { DealCurrencyField } from './DealCurrencyField'
16
+ import { sanitizeAmount, sanitizeProbability } from './dealNumericInput'
17
+ import type { BaseValues } from './dealFormTypes'
18
+ import type { PipelineOption, PipelineStageOption } from './useDealPipelines'
19
+
20
+ type Translate = (key: string, fallback: string, params?: Record<string, string | number>) => string
21
+
22
+ export type DealDetailsFieldsProps = {
23
+ values: BaseValues
24
+ errors: Record<string, string>
25
+ isSubmitting: boolean
26
+ patch: (partial: Partial<BaseValues>) => void
27
+ onPipelineChange: (id: string) => void
28
+ pipelines: PipelineOption[]
29
+ stages: PipelineStageOption[]
30
+ statusLabels: ReturnType<typeof createDictionarySelectLabels>
31
+ tr: Translate
32
+ }
33
+
34
+ function toDate(value: string): Date | null {
35
+ if (!value) return null
36
+ const parsed = parseISO(value)
37
+ return Number.isNaN(parsed.getTime()) ? null : parsed
38
+ }
39
+
40
+ export function DealDetailsFields({
41
+ values,
42
+ errors,
43
+ isSubmitting,
44
+ patch,
45
+ onPipelineChange,
46
+ pipelines,
47
+ stages,
48
+ statusLabels,
49
+ tr,
50
+ }: DealDetailsFieldsProps) {
51
+ return (
52
+ <>
53
+ <DealFormField
54
+ fieldId="title"
55
+ label={tr('customers.deals.create.fields.title', 'Deal title')}
56
+ required
57
+ hint={tr('customers.deals.create.hints.title', 'Short, descriptive name shown on pipeline cards')}
58
+ error={errors.title}
59
+ >
60
+ <Input
61
+ value={values.title}
62
+ onChange={(event) => patch({ title: event.target.value })}
63
+ aria-invalid={errors.title ? true : undefined}
64
+ disabled={isSubmitting}
65
+ />
66
+ </DealFormField>
67
+
68
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
69
+ <DealFormField fieldId="status" label={tr('customers.people.detail.deals.fields.status', 'Status')}>
70
+ <DictionarySelectField
71
+ kind="deal-statuses"
72
+ value={values.status || undefined}
73
+ onChange={(next) => patch({ status: next ?? '' })}
74
+ labels={statusLabels}
75
+ selectClassName="w-full"
76
+ showActiveAppearance={false}
77
+ />
78
+ </DealFormField>
79
+ <DealFormField fieldId="pipelineId" label={tr('customers.people.detail.deals.fields.pipeline', 'Pipeline')}>
80
+ <PipelineSelect
81
+ pipelines={pipelines}
82
+ value={values.pipelineId}
83
+ onChange={onPipelineChange}
84
+ disabled={isSubmitting}
85
+ placeholder={tr('customers.deals.form.pipeline.placeholder', 'Select pipeline…')}
86
+ />
87
+ </DealFormField>
88
+ </div>
89
+
90
+ <DealFormField
91
+ fieldId="pipelineStageId"
92
+ label={tr('customers.people.detail.deals.fields.pipelineStage', 'Pipeline stage')}
93
+ hint={tr('customers.deals.create.hints.pipelineStage', 'Stages depend on the selected pipeline')}
94
+ >
95
+ <PipelineStageSelect
96
+ stages={stages}
97
+ value={values.pipelineStageId}
98
+ onChange={(id) => patch({ pipelineStageId: id })}
99
+ disabled={isSubmitting || !values.pipelineId}
100
+ placeholder={tr('customers.deals.form.pipelineStage.placeholder', 'Select stage…')}
101
+ formatCount={(position, total) =>
102
+ tr('customers.deals.create.fields.stageOf', '· stage {position} of {total}', { position, total })
103
+ }
104
+ />
105
+ </DealFormField>
106
+
107
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
108
+ <DealFormField
109
+ fieldId="valueAmount"
110
+ label={tr('customers.deals.create.fields.valueAmount', 'Deal value')}
111
+ hint={tr('customers.deals.create.hints.valueAmount', 'Potential revenue from this opportunity')}
112
+ error={errors.valueAmount}
113
+ >
114
+ <SuffixInput
115
+ suffix={values.valueCurrency}
116
+ inputMode="decimal"
117
+ value={values.valueAmount}
118
+ onChange={(event) => patch({ valueAmount: sanitizeAmount(event.target.value) })}
119
+ placeholder="0"
120
+ aria-invalid={errors.valueAmount ? true : undefined}
121
+ disabled={isSubmitting}
122
+ />
123
+ </DealFormField>
124
+ <DealFormField fieldId="valueCurrency" label={tr('customers.people.detail.deals.fields.valueCurrency', 'Currency')}>
125
+ <DealCurrencyField
126
+ value={values.valueCurrency}
127
+ onChange={(code) => patch({ valueCurrency: code })}
128
+ disabled={isSubmitting}
129
+ />
130
+ </DealFormField>
131
+ </div>
132
+
133
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
134
+ <DealFormField
135
+ fieldId="probability"
136
+ label={tr('customers.deals.create.fields.probability', 'Probability')}
137
+ hint={tr('customers.deals.create.hints.probability', '0 – 100%, used for weighted pipeline value')}
138
+ error={errors.probability}
139
+ >
140
+ <SuffixInput
141
+ suffix="%"
142
+ inputMode="numeric"
143
+ value={values.probability}
144
+ onChange={(event) => patch({ probability: sanitizeProbability(event.target.value) })}
145
+ placeholder="0"
146
+ aria-invalid={errors.probability ? true : undefined}
147
+ disabled={isSubmitting}
148
+ />
149
+ </DealFormField>
150
+ <DealFormField fieldId="expectedCloseAt" label={tr('customers.deals.create.fields.expectedCloseAt', 'Expected close date')}>
151
+ <DatePicker
152
+ value={toDate(values.expectedCloseAt)}
153
+ onChange={(date) => patch({ expectedCloseAt: date ? format(date, 'yyyy-MM-dd') : '' })}
154
+ disabled={isSubmitting}
155
+ placeholder={tr('customers.deals.create.fields.datePlaceholder', 'Pick a date')}
156
+ />
157
+ </DealFormField>
158
+ </div>
159
+
160
+ <DealFormField fieldId="description" label={tr('customers.people.detail.deals.fields.description', 'Description')}>
161
+ <Textarea
162
+ value={values.description}
163
+ onChange={(event) => patch({ description: event.target.value })}
164
+ disabled={isSubmitting}
165
+ />
166
+ </DealFormField>
167
+ </>
168
+ )
169
+ }
170
+
171
+ export default DealDetailsFields
@@ -0,0 +1,39 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Label } from '@open-mercato/ui/primitives/label'
5
+
6
+ export type DealFormFieldProps = {
7
+ label: string
8
+ /**
9
+ * Stable field key exposed as `data-crud-field-id` and used as the control's `id`.
10
+ * Lets Playwright target the control via the project's `data-crud-field-id` convention
11
+ * (see .ai/lessons.md). Falls back to a generated id for label/control association only.
12
+ */
13
+ fieldId?: string
14
+ required?: boolean
15
+ hint?: string
16
+ error?: string
17
+ children: React.ReactNode
18
+ }
19
+
20
+ export function DealFormField({ label, fieldId, required, hint, error, children }: DealFormFieldProps) {
21
+ const generatedId = React.useId()
22
+ const controlId = fieldId ?? generatedId
23
+ const control = React.isValidElement(children)
24
+ ? React.cloneElement(children as React.ReactElement<{ id?: string }>, { id: controlId })
25
+ : children
26
+ return (
27
+ <div className="space-y-2" data-crud-field-id={fieldId}>
28
+ <Label htmlFor={controlId}>
29
+ {label}
30
+ {required ? <span className="text-destructive"> *</span> : null}
31
+ </Label>
32
+ {control}
33
+ {hint ? <p className="text-xs text-muted-foreground">{hint}</p> : null}
34
+ {error ? <p className="text-xs text-status-error-text">{error}</p> : null}
35
+ </div>
36
+ )
37
+ }
38
+
39
+ export default DealFormField
@@ -0,0 +1,40 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '@open-mercato/shared/lib/utils'
5
+
6
+ export type DealSectionCardProps = {
7
+ icon: React.ComponentType<{ className?: string }>
8
+ title: string
9
+ subtitle?: React.ReactNode
10
+ actions?: React.ReactNode
11
+ children: React.ReactNode
12
+ className?: string
13
+ }
14
+
15
+ export function DealSectionCard({
16
+ icon: Icon,
17
+ title,
18
+ subtitle,
19
+ actions,
20
+ children,
21
+ className,
22
+ }: DealSectionCardProps) {
23
+ return (
24
+ <section className={cn('rounded-lg border border-border bg-card shadow-sm p-6 space-y-6', className)}>
25
+ <div className="flex items-center justify-between gap-3">
26
+ <div className="flex min-w-0 items-center gap-3">
27
+ <div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-brand-violet/10">
28
+ <Icon className="size-4 text-brand-violet" />
29
+ </div>
30
+ <div className="flex min-w-0 flex-col gap-0.5">
31
+ <p className="text-base font-semibold text-foreground">{title}</p>
32
+ {subtitle ? <p className="text-xs text-muted-foreground">{subtitle}</p> : null}
33
+ </div>
34
+ </div>
35
+ {actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
36
+ </div>
37
+ <div className="space-y-4">{children}</div>
38
+ </section>
39
+ )
40
+ }
@@ -0,0 +1,26 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Wand2 } from 'lucide-react'
5
+
6
+ export type DealTipsCardProps = {
7
+ title: string
8
+ tips: string[]
9
+ }
10
+
11
+ export function DealTipsCard({ title, tips }: DealTipsCardProps) {
12
+ return (
13
+ <div className="rounded-lg border-l-4 border-status-info-border bg-status-info-bg px-5 py-4 space-y-3">
14
+ <div className="flex items-center gap-2">
15
+ <Wand2 className="size-4 text-status-info-icon" />
16
+ <p className="text-sm font-semibold text-status-info-text">{title}</p>
17
+ </div>
18
+ {tips.map((tip, index) => (
19
+ <div key={`${tip}-${index}`} className="flex items-start gap-2">
20
+ <span className="mt-1.5 size-1.5 shrink-0 rounded-full bg-status-info-icon" />
21
+ <p className="text-xs text-muted-foreground">{tip}</p>
22
+ </div>
23
+ ))}
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Flag } from 'lucide-react'
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectTriggerLeading,
11
+ SelectValue,
12
+ } from '@open-mercato/ui/primitives/select'
13
+ import type { PipelineOption } from './useDealPipelines'
14
+
15
+ export type PipelineSelectProps = {
16
+ id?: string
17
+ pipelines: PipelineOption[]
18
+ value?: string | null
19
+ onChange: (id: string) => void
20
+ disabled?: boolean
21
+ placeholder: string
22
+ }
23
+
24
+ export function PipelineSelect({
25
+ id,
26
+ pipelines,
27
+ value,
28
+ onChange,
29
+ disabled = false,
30
+ placeholder,
31
+ }: PipelineSelectProps) {
32
+ return (
33
+ <Select
34
+ value={typeof value === 'string' && value ? value : undefined}
35
+ onValueChange={(next) => onChange(next ?? '')}
36
+ disabled={disabled}
37
+ >
38
+ <SelectTrigger id={id} size="default">
39
+ <SelectTriggerLeading>
40
+ <Flag className="size-4 text-muted-foreground" aria-hidden="true" />
41
+ </SelectTriggerLeading>
42
+ <SelectValue placeholder={placeholder} />
43
+ </SelectTrigger>
44
+ <SelectContent>
45
+ {pipelines.map((pipeline) => (
46
+ <SelectItem key={pipeline.id} value={pipeline.id}>
47
+ {pipeline.name}
48
+ </SelectItem>
49
+ ))}
50
+ </SelectContent>
51
+ </Select>
52
+ )
53
+ }
54
+
55
+ export default PipelineSelect