@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/global.d.js +1 -0
- package/dist/global.d.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +11 -5
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/create/page.js +3 -61
- package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +2 -0
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/create/CreateDealForm.js +233 -0
- package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsField.js +209 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsField.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsSection.js +67 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsSection.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealCreateSidebar.js +73 -0
- package/dist/modules/customers/components/detail/create/DealCreateSidebar.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealCurrencyField.js +92 -0
- package/dist/modules/customers/components/detail/create/DealCurrencyField.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealCustomAttributes.js +81 -0
- package/dist/modules/customers/components/detail/create/DealCustomAttributes.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealDetailsFields.js +171 -0
- package/dist/modules/customers/components/detail/create/DealDetailsFields.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealFormField.js +24 -0
- package/dist/modules/customers/components/detail/create/DealFormField.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealSectionCard.js +29 -0
- package/dist/modules/customers/components/detail/create/DealSectionCard.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealTipsCard.js +19 -0
- package/dist/modules/customers/components/detail/create/DealTipsCard.js.map +7 -0
- package/dist/modules/customers/components/detail/create/PipelineSelect.js +41 -0
- package/dist/modules/customers/components/detail/create/PipelineSelect.js.map +7 -0
- package/dist/modules/customers/components/detail/create/PipelineStageSelect.js +49 -0
- package/dist/modules/customers/components/detail/create/PipelineStageSelect.js.map +7 -0
- package/dist/modules/customers/components/detail/create/SuffixInput.js +21 -0
- package/dist/modules/customers/components/detail/create/SuffixInput.js.map +7 -0
- package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js +270 -0
- package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js.map +7 -0
- package/dist/modules/customers/components/detail/create/dealFormTypes.js +17 -0
- package/dist/modules/customers/components/detail/create/dealFormTypes.js.map +7 -0
- package/dist/modules/customers/components/detail/create/dealNumericInput.js +16 -0
- package/dist/modules/customers/components/detail/create/dealNumericInput.js.map +7 -0
- package/dist/modules/customers/components/detail/create/useDealCustomFields.js +93 -0
- package/dist/modules/customers/components/detail/create/useDealCustomFields.js.map +7 -0
- package/dist/modules/customers/components/detail/create/useDealPipelines.js +59 -0
- package/dist/modules/customers/components/detail/create/useDealPipelines.js.map +7 -0
- package/dist/modules/customers/components/formConfig.js +4 -2
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +5 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
- package/dist/modules/feature_toggles/lib/feature-flag-check.js +13 -5
- package/dist/modules/feature_toggles/lib/feature-flag-check.js.map +2 -2
- package/dist/modules/query_index/subscribers/coverage_refresh.js +6 -1
- package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
- package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js +33 -5
- package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraph.js +29 -186
- package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraphImpl.js +196 -0
- package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +7 -0
- package/package.json +8 -9
- package/src/global.d.ts +9 -0
- package/src/modules/catalog/commands/variants.ts +14 -5
- package/src/modules/customers/backend/customers/deals/create/page.tsx +3 -64
- package/src/modules/customers/components/detail/DealForm.tsx +2 -0
- package/src/modules/customers/components/detail/create/CreateDealForm.tsx +254 -0
- package/src/modules/customers/components/detail/create/DealAssociationsField.tsx +253 -0
- package/src/modules/customers/components/detail/create/DealAssociationsSection.tsx +72 -0
- package/src/modules/customers/components/detail/create/DealCreateSidebar.tsx +79 -0
- package/src/modules/customers/components/detail/create/DealCurrencyField.tsx +108 -0
- package/src/modules/customers/components/detail/create/DealCustomAttributes.tsx +118 -0
- package/src/modules/customers/components/detail/create/DealDetailsFields.tsx +171 -0
- package/src/modules/customers/components/detail/create/DealFormField.tsx +39 -0
- package/src/modules/customers/components/detail/create/DealSectionCard.tsx +40 -0
- package/src/modules/customers/components/detail/create/DealTipsCard.tsx +26 -0
- package/src/modules/customers/components/detail/create/PipelineSelect.tsx +55 -0
- package/src/modules/customers/components/detail/create/PipelineStageSelect.tsx +70 -0
- package/src/modules/customers/components/detail/create/SuffixInput.tsx +20 -0
- package/src/modules/customers/components/detail/create/dealCustomFieldControl.tsx +310 -0
- package/src/modules/customers/components/detail/create/dealFormTypes.ts +29 -0
- package/src/modules/customers/components/detail/create/dealNumericInput.ts +20 -0
- package/src/modules/customers/components/detail/create/useDealCustomFields.ts +118 -0
- package/src/modules/customers/components/detail/create/useDealPipelines.ts +80 -0
- package/src/modules/customers/components/formConfig.tsx +3 -0
- package/src/modules/customers/i18n/de.json +26 -0
- package/src/modules/customers/i18n/en.json +26 -0
- package/src/modules/customers/i18n/es.json +26 -0
- package/src/modules/customers/i18n/pl.json +26 -0
- package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +12 -1
- package/src/modules/feature_toggles/lib/feature-flag-check.ts +14 -4
- package/src/modules/query_index/subscribers/coverage_refresh.ts +7 -1
- package/src/modules/resources/i18n/de.json +1 -0
- package/src/modules/resources/i18n/en.json +1 -0
- package/src/modules/resources/i18n/es.json +1 -0
- package/src/modules/resources/i18n/pl.json +1 -0
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/sync_excel/widgets/injection/upload-config/target-options.ts +40 -5
- package/src/modules/workflows/components/WorkflowGraph.tsx +39 -235
- package/src/modules/workflows/components/WorkflowGraphImpl.tsx +233 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from '@open-mercato/ui/primitives/select'
|
|
11
|
+
import type { PipelineStageOption } from './useDealPipelines'
|
|
12
|
+
|
|
13
|
+
export type PipelineStageSelectProps = {
|
|
14
|
+
id?: string
|
|
15
|
+
stages: PipelineStageOption[]
|
|
16
|
+
value?: string | null
|
|
17
|
+
onChange: (id: string) => void
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
placeholder: string
|
|
20
|
+
formatCount: (position: number, total: number) => string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function PipelineStageSelect({
|
|
24
|
+
id,
|
|
25
|
+
stages,
|
|
26
|
+
value,
|
|
27
|
+
onChange,
|
|
28
|
+
disabled = false,
|
|
29
|
+
placeholder,
|
|
30
|
+
formatCount,
|
|
31
|
+
}: PipelineStageSelectProps) {
|
|
32
|
+
const selectedIndex = React.useMemo(
|
|
33
|
+
() => (value ? stages.findIndex((stage) => stage.id === value) : -1),
|
|
34
|
+
[stages, value],
|
|
35
|
+
)
|
|
36
|
+
const selectedStage = selectedIndex >= 0 ? stages[selectedIndex] : null
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Select
|
|
40
|
+
value={typeof value === 'string' && value ? value : undefined}
|
|
41
|
+
onValueChange={(next) => onChange(next ?? '')}
|
|
42
|
+
disabled={disabled || !stages.length}
|
|
43
|
+
>
|
|
44
|
+
<SelectTrigger id={id} size="default">
|
|
45
|
+
<SelectValue placeholder={placeholder}>
|
|
46
|
+
{selectedStage ? (
|
|
47
|
+
<span className="flex min-w-0 items-center gap-2 truncate">
|
|
48
|
+
<span className="truncate">{selectedStage.label}</span>
|
|
49
|
+
<span className="text-muted-foreground">
|
|
50
|
+
{formatCount(selectedIndex + 1, stages.length)}
|
|
51
|
+
</span>
|
|
52
|
+
</span>
|
|
53
|
+
) : null}
|
|
54
|
+
</SelectValue>
|
|
55
|
+
</SelectTrigger>
|
|
56
|
+
<SelectContent>
|
|
57
|
+
{stages.map((stage, index) => (
|
|
58
|
+
<SelectItem key={stage.id} value={stage.id}>
|
|
59
|
+
<span className="truncate">{stage.label}</span>
|
|
60
|
+
<span className="text-muted-foreground">
|
|
61
|
+
{formatCount(index + 1, stages.length)}
|
|
62
|
+
</span>
|
|
63
|
+
</SelectItem>
|
|
64
|
+
))}
|
|
65
|
+
</SelectContent>
|
|
66
|
+
</Select>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default PipelineStageSelect
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
5
|
+
|
|
6
|
+
export type SuffixInputProps = React.ComponentPropsWithoutRef<typeof Input> & { suffix: string }
|
|
7
|
+
|
|
8
|
+
export const SuffixInput = React.forwardRef<HTMLInputElement, SuffixInputProps>(
|
|
9
|
+
({ suffix, ...props }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<Input
|
|
12
|
+
ref={ref}
|
|
13
|
+
rightIcon={<span className="text-sm font-medium text-muted-foreground">{suffix}</span>}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
SuffixInput.displayName = 'SuffixInput'
|
|
@@ -0,0 +1,310 @@
|
|
|
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 type { CrudField, CrudFieldOption } from '@open-mercato/ui/backend/CrudForm'
|
|
7
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
8
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
9
|
+
import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
10
|
+
import { CheckboxField } from '@open-mercato/ui/primitives/checkbox-field'
|
|
11
|
+
import { DatePicker } from '@open-mercato/ui/primitives/date-picker'
|
|
12
|
+
import { TagInput } from '@open-mercato/ui/primitives/tag-input'
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from '@open-mercato/ui/primitives/select'
|
|
20
|
+
import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
|
|
21
|
+
|
|
22
|
+
export type DealCustomFieldControlProps = {
|
|
23
|
+
field: CrudField
|
|
24
|
+
value: unknown
|
|
25
|
+
onChange: (value: unknown) => void
|
|
26
|
+
error?: string
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SELECT_CLEAR_SENTINEL = '__deal_custom_field_select_clear__'
|
|
31
|
+
|
|
32
|
+
function toStringArray(value: unknown): string[] {
|
|
33
|
+
if (!Array.isArray(value)) return []
|
|
34
|
+
return value.filter((item): item is string => typeof item === 'string')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toDate(value: unknown): Date | null {
|
|
38
|
+
if (value instanceof Date) return value
|
|
39
|
+
if (typeof value === 'string' && value) {
|
|
40
|
+
const parsed = parseISO(value)
|
|
41
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function useResolvedOptions(field: CrudField): CrudFieldOption[] {
|
|
47
|
+
const builtin = field.type === 'custom' ? null : field
|
|
48
|
+
const staticOptions = builtin?.options
|
|
49
|
+
const loadOptions = builtin?.loadOptions
|
|
50
|
+
const [loadedOptions, setLoadedOptions] = React.useState<CrudFieldOption[]>([])
|
|
51
|
+
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
if (staticOptions && staticOptions.length > 0) return
|
|
54
|
+
if (typeof loadOptions !== 'function') return
|
|
55
|
+
let cancelled = false
|
|
56
|
+
loadOptions()
|
|
57
|
+
.then((options) => {
|
|
58
|
+
if (!cancelled) setLoadedOptions(options)
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {
|
|
61
|
+
if (!cancelled) setLoadedOptions([])
|
|
62
|
+
})
|
|
63
|
+
return () => {
|
|
64
|
+
cancelled = true
|
|
65
|
+
}
|
|
66
|
+
}, [staticOptions, loadOptions])
|
|
67
|
+
|
|
68
|
+
if (staticOptions && staticOptions.length > 0) return staticOptions
|
|
69
|
+
return loadedOptions
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function FieldShell({
|
|
73
|
+
label,
|
|
74
|
+
required,
|
|
75
|
+
description,
|
|
76
|
+
error,
|
|
77
|
+
children,
|
|
78
|
+
}: {
|
|
79
|
+
label: string
|
|
80
|
+
required?: boolean
|
|
81
|
+
description?: React.ReactNode
|
|
82
|
+
error?: string
|
|
83
|
+
children: React.ReactNode
|
|
84
|
+
}) {
|
|
85
|
+
const fieldId = React.useId()
|
|
86
|
+
const control = React.isValidElement(children)
|
|
87
|
+
? React.cloneElement(children as React.ReactElement<{ id?: string }>, { id: fieldId })
|
|
88
|
+
: children
|
|
89
|
+
return (
|
|
90
|
+
<div className="space-y-1">
|
|
91
|
+
{label.trim().length > 0 ? (
|
|
92
|
+
<Label htmlFor={fieldId}>
|
|
93
|
+
{label}
|
|
94
|
+
{required ? <span className="text-destructive"> *</span> : null}
|
|
95
|
+
</Label>
|
|
96
|
+
) : null}
|
|
97
|
+
{control}
|
|
98
|
+
{description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
|
|
99
|
+
{error ? <p className="text-xs text-status-error-text">{error}</p> : null}
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function DealCustomFieldControl({
|
|
105
|
+
field,
|
|
106
|
+
value,
|
|
107
|
+
onChange,
|
|
108
|
+
error,
|
|
109
|
+
disabled = false,
|
|
110
|
+
}: DealCustomFieldControlProps) {
|
|
111
|
+
const options = useResolvedOptions(field)
|
|
112
|
+
const ariaInvalid = error ? true : undefined
|
|
113
|
+
|
|
114
|
+
if (field.type === 'checkbox') {
|
|
115
|
+
return (
|
|
116
|
+
<div className="space-y-1">
|
|
117
|
+
<CheckboxField
|
|
118
|
+
label={field.label}
|
|
119
|
+
description={field.description}
|
|
120
|
+
checked={value === true}
|
|
121
|
+
onCheckedChange={(next) => onChange(next === true)}
|
|
122
|
+
disabled={disabled}
|
|
123
|
+
aria-invalid={ariaInvalid}
|
|
124
|
+
/>
|
|
125
|
+
{error ? <p className="text-xs text-status-error-text">{error}</p> : null}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (field.type === 'custom') {
|
|
131
|
+
return (
|
|
132
|
+
<FieldShell
|
|
133
|
+
label={field.label}
|
|
134
|
+
required={field.required}
|
|
135
|
+
description={field.description}
|
|
136
|
+
error={error}
|
|
137
|
+
>
|
|
138
|
+
{field.component({
|
|
139
|
+
id: field.id,
|
|
140
|
+
value,
|
|
141
|
+
error,
|
|
142
|
+
setValue: onChange,
|
|
143
|
+
disabled,
|
|
144
|
+
autoFocus: false,
|
|
145
|
+
})}
|
|
146
|
+
</FieldShell>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let control: React.ReactNode
|
|
151
|
+
|
|
152
|
+
switch (field.type) {
|
|
153
|
+
case 'number':
|
|
154
|
+
control = (
|
|
155
|
+
<Input
|
|
156
|
+
type="number"
|
|
157
|
+
inputMode="numeric"
|
|
158
|
+
value={typeof value === 'number' || typeof value === 'string' ? String(value) : ''}
|
|
159
|
+
placeholder={field.placeholder}
|
|
160
|
+
disabled={disabled}
|
|
161
|
+
aria-invalid={ariaInvalid}
|
|
162
|
+
onChange={(event) => {
|
|
163
|
+
const raw = event.target.value
|
|
164
|
+
if (raw === '') {
|
|
165
|
+
onChange(undefined)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
const parsed = Number(raw)
|
|
169
|
+
onChange(Number.isNaN(parsed) ? raw : parsed)
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
)
|
|
173
|
+
break
|
|
174
|
+
case 'date':
|
|
175
|
+
case 'datepicker':
|
|
176
|
+
control = (
|
|
177
|
+
<DatePicker
|
|
178
|
+
value={toDate(value)}
|
|
179
|
+
onChange={(date) => onChange(date ? format(date, 'yyyy-MM-dd') : undefined)}
|
|
180
|
+
disabled={disabled}
|
|
181
|
+
placeholder={field.placeholder}
|
|
182
|
+
aria-invalid={ariaInvalid}
|
|
183
|
+
/>
|
|
184
|
+
)
|
|
185
|
+
break
|
|
186
|
+
case 'datetime':
|
|
187
|
+
case 'datetime-local':
|
|
188
|
+
control = (
|
|
189
|
+
<DatePicker
|
|
190
|
+
withTime
|
|
191
|
+
value={toDate(value)}
|
|
192
|
+
onChange={(date) => onChange(date ? date.toISOString() : undefined)}
|
|
193
|
+
disabled={disabled}
|
|
194
|
+
placeholder={field.placeholder}
|
|
195
|
+
aria-invalid={ariaInvalid}
|
|
196
|
+
/>
|
|
197
|
+
)
|
|
198
|
+
break
|
|
199
|
+
case 'tags':
|
|
200
|
+
control = (
|
|
201
|
+
<TagInput
|
|
202
|
+
value={toStringArray(value)}
|
|
203
|
+
onChange={(next) => onChange(next)}
|
|
204
|
+
placeholder={field.placeholder}
|
|
205
|
+
disabled={disabled}
|
|
206
|
+
aria-invalid={ariaInvalid}
|
|
207
|
+
/>
|
|
208
|
+
)
|
|
209
|
+
break
|
|
210
|
+
case 'richtext':
|
|
211
|
+
control = (
|
|
212
|
+
<Textarea
|
|
213
|
+
value={value == null ? '' : String(value)}
|
|
214
|
+
placeholder={field.placeholder}
|
|
215
|
+
disabled={disabled}
|
|
216
|
+
aria-invalid={ariaInvalid}
|
|
217
|
+
onChange={(event) => onChange(event.target.value)}
|
|
218
|
+
/>
|
|
219
|
+
)
|
|
220
|
+
break
|
|
221
|
+
case 'select': {
|
|
222
|
+
if (field.multiple) {
|
|
223
|
+
const selected = toStringArray(value)
|
|
224
|
+
control = (
|
|
225
|
+
<div className="flex flex-wrap gap-3">
|
|
226
|
+
{options.map((option) => {
|
|
227
|
+
const checked = selected.includes(option.value)
|
|
228
|
+
return (
|
|
229
|
+
<label key={option.value} className="inline-flex cursor-pointer items-center gap-2">
|
|
230
|
+
<Checkbox
|
|
231
|
+
checked={checked}
|
|
232
|
+
disabled={disabled}
|
|
233
|
+
onCheckedChange={(state) => {
|
|
234
|
+
const next = new Set(selected)
|
|
235
|
+
if (state === true) next.add(option.value)
|
|
236
|
+
else next.delete(option.value)
|
|
237
|
+
onChange(Array.from(next))
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
<span className="text-sm">{option.label}</span>
|
|
241
|
+
</label>
|
|
242
|
+
)
|
|
243
|
+
})}
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
} else {
|
|
247
|
+
const selectedValue = Array.isArray(value)
|
|
248
|
+
? String(value[0] ?? '')
|
|
249
|
+
: value == null
|
|
250
|
+
? ''
|
|
251
|
+
: String(value)
|
|
252
|
+
control = (
|
|
253
|
+
<Select
|
|
254
|
+
value={selectedValue}
|
|
255
|
+
disabled={disabled}
|
|
256
|
+
onValueChange={(next) => {
|
|
257
|
+
if (!next || next === SELECT_CLEAR_SENTINEL) {
|
|
258
|
+
onChange(null)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
onChange(next)
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<SelectTrigger aria-invalid={ariaInvalid}>
|
|
265
|
+
<SelectValue placeholder={field.placeholder} />
|
|
266
|
+
</SelectTrigger>
|
|
267
|
+
<SelectContent>
|
|
268
|
+
{!field.required && selectedValue ? (
|
|
269
|
+
<SelectItem value={SELECT_CLEAR_SENTINEL}>—</SelectItem>
|
|
270
|
+
) : null}
|
|
271
|
+
{options
|
|
272
|
+
.filter((option) => option.value !== '')
|
|
273
|
+
.map((option) => (
|
|
274
|
+
<SelectItem key={option.value} value={option.value}>
|
|
275
|
+
{option.label}
|
|
276
|
+
</SelectItem>
|
|
277
|
+
))}
|
|
278
|
+
</SelectContent>
|
|
279
|
+
</Select>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
case 'text':
|
|
285
|
+
default:
|
|
286
|
+
control = (
|
|
287
|
+
<Input
|
|
288
|
+
value={value == null ? '' : String(value)}
|
|
289
|
+
placeholder={field.placeholder}
|
|
290
|
+
disabled={disabled}
|
|
291
|
+
aria-invalid={ariaInvalid}
|
|
292
|
+
onChange={(event) => onChange(event.target.value)}
|
|
293
|
+
/>
|
|
294
|
+
)
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<FieldShell
|
|
300
|
+
label={field.label}
|
|
301
|
+
required={field.required}
|
|
302
|
+
description={field.description}
|
|
303
|
+
error={error}
|
|
304
|
+
>
|
|
305
|
+
{control}
|
|
306
|
+
</FieldShell>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default DealCustomFieldControl
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type Translate = (key: string, fallback: string, params?: Record<string, string | number>) => string
|
|
2
|
+
|
|
3
|
+
export type BaseValues = {
|
|
4
|
+
title: string
|
|
5
|
+
status: string
|
|
6
|
+
pipelineId: string
|
|
7
|
+
pipelineStageId: string
|
|
8
|
+
valueAmount: string
|
|
9
|
+
valueCurrency: string
|
|
10
|
+
probability: string
|
|
11
|
+
expectedCloseAt: string
|
|
12
|
+
description: string
|
|
13
|
+
personIds: string[]
|
|
14
|
+
companyIds: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const EMPTY_VALUES: BaseValues = {
|
|
18
|
+
title: '',
|
|
19
|
+
status: '',
|
|
20
|
+
pipelineId: '',
|
|
21
|
+
pipelineStageId: '',
|
|
22
|
+
valueAmount: '',
|
|
23
|
+
valueCurrency: '',
|
|
24
|
+
probability: '',
|
|
25
|
+
expectedCloseAt: '',
|
|
26
|
+
description: '',
|
|
27
|
+
personIds: [],
|
|
28
|
+
companyIds: [],
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input sanitizers for the deal create form's numeric fields. They guarantee the
|
|
3
|
+
* underlying state only ever holds a valid numeric string (or empty), so non-numeric
|
|
4
|
+
* input can never reach the zod schema and surface a raw "expected number" type error.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Keep only digits and a single decimal point; strips letters, signs, and extra dots. */
|
|
8
|
+
export function sanitizeAmount(raw: string): string {
|
|
9
|
+
const cleaned = raw.replace(/[^\d.]/g, '')
|
|
10
|
+
const firstDot = cleaned.indexOf('.')
|
|
11
|
+
if (firstDot === -1) return cleaned
|
|
12
|
+
return cleaned.slice(0, firstDot + 1) + cleaned.slice(firstDot + 1).replace(/\./g, '')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Digits only, clamped to 0–100, so probability is always a valid percentage. */
|
|
16
|
+
export function sanitizeProbability(raw: string): string {
|
|
17
|
+
const digits = raw.replace(/\D/g, '')
|
|
18
|
+
if (!digits) return ''
|
|
19
|
+
return String(Math.min(100, Number(digits)))
|
|
20
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
validateValuesAgainstDefs,
|
|
6
|
+
type CustomFieldDefLike,
|
|
7
|
+
} from '@open-mercato/shared/modules/entities/validation'
|
|
8
|
+
import type { CrudField } from '@open-mercato/ui/backend/CrudForm'
|
|
9
|
+
import type { CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
|
|
10
|
+
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
11
|
+
import { normalizeCustomFieldSubmitValue } from '../customFieldUtils'
|
|
12
|
+
import type { DealCustomAttributesLoadState } from './DealCustomAttributes'
|
|
13
|
+
import type { Translate } from './dealFormTypes'
|
|
14
|
+
|
|
15
|
+
function isEmptyCustomFieldValue(field: CrudField, value: unknown): boolean {
|
|
16
|
+
if (value === undefined || value === null) return true
|
|
17
|
+
if (typeof value === 'string') return value.trim() === ''
|
|
18
|
+
if (Array.isArray(value)) return value.length === 0
|
|
19
|
+
return field.type === 'checkbox' && value !== true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mapCustomDefinitionsForValidation(definitions: CustomFieldDefDto[]): CustomFieldDefLike[] {
|
|
23
|
+
return definitions.map((definition) => ({
|
|
24
|
+
key: definition.key,
|
|
25
|
+
kind: definition.kind,
|
|
26
|
+
configJson: {
|
|
27
|
+
validation: Array.isArray(definition.validation) ? definition.validation : [],
|
|
28
|
+
},
|
|
29
|
+
}))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type UseDealCustomFieldsResult = {
|
|
33
|
+
customValues: Record<string, unknown>
|
|
34
|
+
customFieldsLoaded: boolean
|
|
35
|
+
customCount: number
|
|
36
|
+
handleCustomChange: (key: string, value: unknown) => void
|
|
37
|
+
handleCustomAttributesLoaded: (state: DealCustomAttributesLoadState) => void
|
|
38
|
+
validateCustomFields: (source: Record<string, unknown>) => Record<string, string>
|
|
39
|
+
collectNormalizedCustomValues: (source: Record<string, unknown>) => Record<string, unknown>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useDealCustomFields(tr: Translate): UseDealCustomFieldsResult {
|
|
43
|
+
const [customValues, setCustomValues] = React.useState<Record<string, unknown>>({})
|
|
44
|
+
const [customFields, setCustomFields] = React.useState<CrudField[]>([])
|
|
45
|
+
const [customDefinitions, setCustomDefinitions] = React.useState<CustomFieldDefDto[]>([])
|
|
46
|
+
const [customFieldsLoaded, setCustomFieldsLoaded] = React.useState(false)
|
|
47
|
+
const [customCount, setCustomCount] = React.useState(0)
|
|
48
|
+
const customDefaultsAppliedRef = React.useRef(false)
|
|
49
|
+
|
|
50
|
+
const handleCustomChange = React.useCallback((key: string, value: unknown) => {
|
|
51
|
+
setCustomValues((current) => ({ ...current, [key]: value }))
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const handleCustomAttributesLoaded = React.useCallback((state: DealCustomAttributesLoadState) => {
|
|
55
|
+
setCustomFields(state.fields)
|
|
56
|
+
setCustomDefinitions(state.definitions)
|
|
57
|
+
setCustomFieldsLoaded(true)
|
|
58
|
+
setCustomCount(state.fields.length)
|
|
59
|
+
|
|
60
|
+
if (customDefaultsAppliedRef.current || state.definitions.length === 0) return
|
|
61
|
+
customDefaultsAppliedRef.current = true
|
|
62
|
+
setCustomValues((current) => {
|
|
63
|
+
let changed = false
|
|
64
|
+
const next = { ...current }
|
|
65
|
+
for (const definition of state.definitions) {
|
|
66
|
+
if (definition.defaultValue === undefined || definition.defaultValue === null) continue
|
|
67
|
+
const fieldId = `cf_${definition.key}`
|
|
68
|
+
if (next[fieldId] !== undefined) continue
|
|
69
|
+
next[fieldId] = definition.defaultValue
|
|
70
|
+
changed = true
|
|
71
|
+
}
|
|
72
|
+
return changed ? next : current
|
|
73
|
+
})
|
|
74
|
+
}, [])
|
|
75
|
+
|
|
76
|
+
const collectNormalizedCustomValues = React.useCallback((source: Record<string, unknown>) =>
|
|
77
|
+
collectCustomFieldValues(source, {
|
|
78
|
+
transform: (value) => normalizeCustomFieldSubmitValue(value),
|
|
79
|
+
}), [])
|
|
80
|
+
|
|
81
|
+
const validateCustomFields = React.useCallback((source: Record<string, unknown>) => {
|
|
82
|
+
const fieldErrors: Record<string, string> = {}
|
|
83
|
+
const requiredMessage = tr('ui.forms.errors.required', 'Required')
|
|
84
|
+
|
|
85
|
+
for (const field of customFields) {
|
|
86
|
+
if (!field.required) continue
|
|
87
|
+
if (isEmptyCustomFieldValue(field, source[field.id])) {
|
|
88
|
+
fieldErrors[field.id] = requiredMessage
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (customDefinitions.length > 0) {
|
|
93
|
+
const result = validateValuesAgainstDefs(
|
|
94
|
+
collectNormalizedCustomValues(source),
|
|
95
|
+
mapCustomDefinitionsForValidation(customDefinitions),
|
|
96
|
+
)
|
|
97
|
+
if (!result.ok) {
|
|
98
|
+
for (const [fieldId, message] of Object.entries(result.fieldErrors)) {
|
|
99
|
+
if (!fieldErrors[fieldId]) fieldErrors[fieldId] = tr(message, message)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return fieldErrors
|
|
105
|
+
}, [collectNormalizedCustomValues, customDefinitions, customFields, tr])
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
customValues,
|
|
109
|
+
customFieldsLoaded,
|
|
110
|
+
customCount,
|
|
111
|
+
handleCustomChange,
|
|
112
|
+
handleCustomAttributesLoaded,
|
|
113
|
+
validateCustomFields,
|
|
114
|
+
collectNormalizedCustomValues,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default useDealCustomFields
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
5
|
+
|
|
6
|
+
export type PipelineOption = {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
isDefault: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type PipelineStageOption = {
|
|
13
|
+
id: string
|
|
14
|
+
label: string
|
|
15
|
+
order: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type UseDealPipelinesResult = {
|
|
19
|
+
pipelines: PipelineOption[]
|
|
20
|
+
stages: PipelineStageOption[]
|
|
21
|
+
loadStages: (pipelineId: string) => Promise<void>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useDealPipelines(): UseDealPipelinesResult {
|
|
25
|
+
const [pipelines, setPipelines] = React.useState<PipelineOption[]>([])
|
|
26
|
+
const [stages, setStages] = React.useState<PipelineStageOption[]>([])
|
|
27
|
+
const mountedRef = React.useRef(true)
|
|
28
|
+
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
mountedRef.current = true
|
|
31
|
+
return () => {
|
|
32
|
+
mountedRef.current = false
|
|
33
|
+
}
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
let cancelled = false
|
|
38
|
+
;(async () => {
|
|
39
|
+
try {
|
|
40
|
+
const call = await apiCall<{ items: PipelineOption[] }>('/api/customers/pipelines')
|
|
41
|
+
if (cancelled || !mountedRef.current) return
|
|
42
|
+
if (call.ok && call.result?.items) {
|
|
43
|
+
setPipelines(call.result.items)
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
if (!cancelled && mountedRef.current) setPipelines([])
|
|
47
|
+
}
|
|
48
|
+
})().catch(() => {
|
|
49
|
+
// The inner try/catch already resets pipelines on failure; this guards the IIFE promise only.
|
|
50
|
+
})
|
|
51
|
+
return () => {
|
|
52
|
+
cancelled = true
|
|
53
|
+
}
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const loadStages = React.useCallback(async (pipelineId: string) => {
|
|
57
|
+
if (!pipelineId) {
|
|
58
|
+
if (mountedRef.current) setStages([])
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const call = await apiCall<{ items: PipelineStageOption[] }>(
|
|
63
|
+
`/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(pipelineId)}`,
|
|
64
|
+
)
|
|
65
|
+
if (!mountedRef.current) return
|
|
66
|
+
if (call.ok && call.result?.items) {
|
|
67
|
+
const sorted = [...call.result.items].sort((a, b) => a.order - b.order)
|
|
68
|
+
setStages(sorted)
|
|
69
|
+
} else {
|
|
70
|
+
setStages([])
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
if (mountedRef.current) setStages([])
|
|
74
|
+
}
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
return { pipelines, stages, loadStages }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default useDealPipelines
|
|
@@ -113,6 +113,7 @@ type DictionarySelectFieldProps = {
|
|
|
113
113
|
allowAppearance?: boolean
|
|
114
114
|
showManage?: boolean
|
|
115
115
|
showLabelInput?: boolean
|
|
116
|
+
showActiveAppearance?: boolean
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
const emailValidationSchema = z.string().email()
|
|
@@ -141,6 +142,7 @@ export function DictionarySelectField({
|
|
|
141
142
|
allowAppearance = false,
|
|
142
143
|
showManage = false,
|
|
143
144
|
showLabelInput = true,
|
|
145
|
+
showActiveAppearance = true,
|
|
144
146
|
}: DictionarySelectFieldProps) {
|
|
145
147
|
const t = useT()
|
|
146
148
|
const queryClient = useQueryClient()
|
|
@@ -237,6 +239,7 @@ export function DictionarySelectField({
|
|
|
237
239
|
allowAppearance={allowAppearance}
|
|
238
240
|
showManage={showManage}
|
|
239
241
|
showLabelInput={showLabelInput}
|
|
242
|
+
showActiveAppearance={showActiveAppearance}
|
|
240
243
|
/>
|
|
241
244
|
)
|
|
242
245
|
}
|