@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.
- package/.turbo/turbo-build.log +1 -1
- 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/package.json +7 -7
- 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
|
@@ -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'
|