@open-mercato/core 0.6.4-develop.3929.1.fcf7afece2 → 0.6.4-develop.3944.1.4100aa7fbe

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 (72) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/backend/customers/deals/create/page.js +3 -61
  3. package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
  4. package/dist/modules/customers/components/detail/DealForm.js +2 -0
  5. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  6. package/dist/modules/customers/components/detail/create/CreateDealForm.js +233 -0
  7. package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +7 -0
  8. package/dist/modules/customers/components/detail/create/DealAssociationsField.js +209 -0
  9. package/dist/modules/customers/components/detail/create/DealAssociationsField.js.map +7 -0
  10. package/dist/modules/customers/components/detail/create/DealAssociationsSection.js +67 -0
  11. package/dist/modules/customers/components/detail/create/DealAssociationsSection.js.map +7 -0
  12. package/dist/modules/customers/components/detail/create/DealCreateSidebar.js +73 -0
  13. package/dist/modules/customers/components/detail/create/DealCreateSidebar.js.map +7 -0
  14. package/dist/modules/customers/components/detail/create/DealCurrencyField.js +92 -0
  15. package/dist/modules/customers/components/detail/create/DealCurrencyField.js.map +7 -0
  16. package/dist/modules/customers/components/detail/create/DealCustomAttributes.js +81 -0
  17. package/dist/modules/customers/components/detail/create/DealCustomAttributes.js.map +7 -0
  18. package/dist/modules/customers/components/detail/create/DealDetailsFields.js +171 -0
  19. package/dist/modules/customers/components/detail/create/DealDetailsFields.js.map +7 -0
  20. package/dist/modules/customers/components/detail/create/DealFormField.js +24 -0
  21. package/dist/modules/customers/components/detail/create/DealFormField.js.map +7 -0
  22. package/dist/modules/customers/components/detail/create/DealSectionCard.js +29 -0
  23. package/dist/modules/customers/components/detail/create/DealSectionCard.js.map +7 -0
  24. package/dist/modules/customers/components/detail/create/DealTipsCard.js +19 -0
  25. package/dist/modules/customers/components/detail/create/DealTipsCard.js.map +7 -0
  26. package/dist/modules/customers/components/detail/create/PipelineSelect.js +41 -0
  27. package/dist/modules/customers/components/detail/create/PipelineSelect.js.map +7 -0
  28. package/dist/modules/customers/components/detail/create/PipelineStageSelect.js +49 -0
  29. package/dist/modules/customers/components/detail/create/PipelineStageSelect.js.map +7 -0
  30. package/dist/modules/customers/components/detail/create/SuffixInput.js +21 -0
  31. package/dist/modules/customers/components/detail/create/SuffixInput.js.map +7 -0
  32. package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js +270 -0
  33. package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js.map +7 -0
  34. package/dist/modules/customers/components/detail/create/dealFormTypes.js +17 -0
  35. package/dist/modules/customers/components/detail/create/dealFormTypes.js.map +7 -0
  36. package/dist/modules/customers/components/detail/create/dealNumericInput.js +16 -0
  37. package/dist/modules/customers/components/detail/create/dealNumericInput.js.map +7 -0
  38. package/dist/modules/customers/components/detail/create/useDealCustomFields.js +93 -0
  39. package/dist/modules/customers/components/detail/create/useDealCustomFields.js.map +7 -0
  40. package/dist/modules/customers/components/detail/create/useDealPipelines.js +59 -0
  41. package/dist/modules/customers/components/detail/create/useDealPipelines.js.map +7 -0
  42. package/dist/modules/customers/components/formConfig.js +4 -2
  43. package/dist/modules/customers/components/formConfig.js.map +2 -2
  44. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +5 -2
  45. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
  46. package/package.json +7 -7
  47. package/src/modules/customers/backend/customers/deals/create/page.tsx +3 -64
  48. package/src/modules/customers/components/detail/DealForm.tsx +2 -0
  49. package/src/modules/customers/components/detail/create/CreateDealForm.tsx +254 -0
  50. package/src/modules/customers/components/detail/create/DealAssociationsField.tsx +253 -0
  51. package/src/modules/customers/components/detail/create/DealAssociationsSection.tsx +72 -0
  52. package/src/modules/customers/components/detail/create/DealCreateSidebar.tsx +79 -0
  53. package/src/modules/customers/components/detail/create/DealCurrencyField.tsx +108 -0
  54. package/src/modules/customers/components/detail/create/DealCustomAttributes.tsx +118 -0
  55. package/src/modules/customers/components/detail/create/DealDetailsFields.tsx +171 -0
  56. package/src/modules/customers/components/detail/create/DealFormField.tsx +39 -0
  57. package/src/modules/customers/components/detail/create/DealSectionCard.tsx +40 -0
  58. package/src/modules/customers/components/detail/create/DealTipsCard.tsx +26 -0
  59. package/src/modules/customers/components/detail/create/PipelineSelect.tsx +55 -0
  60. package/src/modules/customers/components/detail/create/PipelineStageSelect.tsx +70 -0
  61. package/src/modules/customers/components/detail/create/SuffixInput.tsx +20 -0
  62. package/src/modules/customers/components/detail/create/dealCustomFieldControl.tsx +310 -0
  63. package/src/modules/customers/components/detail/create/dealFormTypes.ts +29 -0
  64. package/src/modules/customers/components/detail/create/dealNumericInput.ts +20 -0
  65. package/src/modules/customers/components/detail/create/useDealCustomFields.ts +118 -0
  66. package/src/modules/customers/components/detail/create/useDealPipelines.ts +80 -0
  67. package/src/modules/customers/components/formConfig.tsx +3 -0
  68. package/src/modules/customers/i18n/de.json +26 -0
  69. package/src/modules/customers/i18n/en.json +26 -0
  70. package/src/modules/customers/i18n/es.json +26 -0
  71. package/src/modules/customers/i18n/pl.json +26 -0
  72. package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +12 -1
@@ -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
@@ -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'