@open-mercato/core 0.8.1-develop.7221.1.ece7ca874e → 0.8.1-develop.7225.1.73190a4a95

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 (87) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.js +166 -0
  3. package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.js.map +7 -0
  4. package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.meta.js +16 -0
  5. package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.meta.js.map +7 -0
  6. package/dist/modules/catalog/backend/catalog/prices/create/page.js +68 -0
  7. package/dist/modules/catalog/backend/catalog/prices/create/page.js.map +7 -0
  8. package/dist/modules/catalog/backend/catalog/prices/create/page.meta.js +25 -0
  9. package/dist/modules/catalog/backend/catalog/prices/create/page.meta.js.map +7 -0
  10. package/dist/modules/catalog/backend/catalog/prices/page.js +11 -0
  11. package/dist/modules/catalog/backend/catalog/prices/page.js.map +7 -0
  12. package/dist/modules/catalog/backend/catalog/prices/page.meta.js +16 -0
  13. package/dist/modules/catalog/backend/catalog/prices/page.meta.js.map +7 -0
  14. package/dist/modules/catalog/components/prices/PriceScopeSelectors.js +201 -0
  15. package/dist/modules/catalog/components/prices/PriceScopeSelectors.js.map +7 -0
  16. package/dist/modules/catalog/components/prices/PricesDataTable.js +273 -0
  17. package/dist/modules/catalog/components/prices/PricesDataTable.js.map +7 -0
  18. package/dist/modules/catalog/components/prices/normalizePriceRecord.js +43 -0
  19. package/dist/modules/catalog/components/prices/normalizePriceRecord.js.map +7 -0
  20. package/dist/modules/catalog/components/prices/priceFormFields.js +195 -0
  21. package/dist/modules/catalog/components/prices/priceFormFields.js.map +7 -0
  22. package/dist/modules/catalog/components/prices/useBatchLabels.js +42 -0
  23. package/dist/modules/catalog/components/prices/useBatchLabels.js.map +7 -0
  24. package/dist/modules/catalog/data/validators.js +21 -2
  25. package/dist/modules/catalog/data/validators.js.map +2 -2
  26. package/dist/modules/catalog/extension-points.js +2 -1
  27. package/dist/modules/catalog/extension-points.js.map +2 -2
  28. package/dist/modules/catalog/lib/pricing.js +37 -8
  29. package/dist/modules/catalog/lib/pricing.js.map +2 -2
  30. package/dist/modules/communication_channels/backend/profile/communication-channels/ConnectChannelMenu.js +88 -0
  31. package/dist/modules/communication_channels/backend/profile/communication-channels/ConnectChannelMenu.js.map +7 -0
  32. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +6 -12
  33. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
  34. package/dist/modules/communication_channels/commands/connect-credential-channel.js +7 -2
  35. package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
  36. package/dist/modules/communication_channels/commands/ingest-inbound-message.js +11 -2
  37. package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
  38. package/dist/modules/communication_channels/extension-points.js +1 -1
  39. package/dist/modules/communication_channels/extension-points.js.map +1 -1
  40. package/dist/modules/communication_channels/lib/connect-channel.js +33 -0
  41. package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
  42. package/dist/modules/messages/api/route.js +5 -1
  43. package/dist/modules/messages/api/route.js.map +2 -2
  44. package/dist/modules/messages/commands/messages.js +3 -2
  45. package/dist/modules/messages/commands/messages.js.map +2 -2
  46. package/dist/modules/messages/data/validators.js +12 -2
  47. package/dist/modules/messages/data/validators.js.map +2 -2
  48. package/dist/modules/messages/subscribers/message-notification.js +4 -1
  49. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  50. package/package.json +7 -7
  51. package/src/modules/catalog/AGENTS.md +6 -2
  52. package/src/modules/catalog/backend/catalog/prices/[id]/edit/page.meta.ts +12 -0
  53. package/src/modules/catalog/backend/catalog/prices/[id]/edit/page.tsx +198 -0
  54. package/src/modules/catalog/backend/catalog/prices/create/page.meta.ts +23 -0
  55. package/src/modules/catalog/backend/catalog/prices/create/page.tsx +70 -0
  56. package/src/modules/catalog/backend/catalog/prices/page.meta.ts +12 -0
  57. package/src/modules/catalog/backend/catalog/prices/page.tsx +14 -0
  58. package/src/modules/catalog/components/prices/PriceScopeSelectors.tsx +243 -0
  59. package/src/modules/catalog/components/prices/PricesDataTable.tsx +320 -0
  60. package/src/modules/catalog/components/prices/normalizePriceRecord.ts +71 -0
  61. package/src/modules/catalog/components/prices/priceFormFields.tsx +228 -0
  62. package/src/modules/catalog/components/prices/useBatchLabels.ts +57 -0
  63. package/src/modules/catalog/data/validators.ts +43 -2
  64. package/src/modules/catalog/extension-points.ts +1 -0
  65. package/src/modules/catalog/i18n/de.json +72 -0
  66. package/src/modules/catalog/i18n/en.json +72 -0
  67. package/src/modules/catalog/i18n/es.json +72 -0
  68. package/src/modules/catalog/i18n/ko.json +72 -0
  69. package/src/modules/catalog/i18n/pl.json +72 -0
  70. package/src/modules/catalog/lib/pricing.ts +103 -9
  71. package/src/modules/communication_channels/backend/profile/communication-channels/ConnectChannelMenu.tsx +120 -0
  72. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +9 -10
  73. package/src/modules/communication_channels/commands/connect-credential-channel.ts +9 -0
  74. package/src/modules/communication_channels/commands/ingest-inbound-message.ts +17 -2
  75. package/src/modules/communication_channels/extension-points.ts +1 -1
  76. package/src/modules/communication_channels/i18n/de.json +2 -1
  77. package/src/modules/communication_channels/i18n/en.json +2 -1
  78. package/src/modules/communication_channels/i18n/es.json +2 -1
  79. package/src/modules/communication_channels/i18n/ko.json +2 -1
  80. package/src/modules/communication_channels/i18n/pl.json +2 -1
  81. package/src/modules/communication_channels/lib/adapter.ts +8 -0
  82. package/src/modules/communication_channels/lib/connect-channel.ts +49 -0
  83. package/src/modules/messages/api/route.ts +8 -2
  84. package/src/modules/messages/commands/messages.ts +17 -2
  85. package/src/modules/messages/data/validators.ts +26 -11
  86. package/src/modules/messages/subscribers/message-notification.ts +17 -5
  87. package/src/modules/sales/AGENTS.md +2 -3
@@ -0,0 +1,198 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
+ import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
6
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
7
+ import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
8
+ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
9
+ import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
10
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
11
+ import { E } from '#generated/entities.ids.generated'
12
+ import { usePriceFormFields, type PriceFormValues } from '../../../../../components/prices/priceFormFields'
13
+ import { normalizePriceRecord } from '../../../../../components/prices/normalizePriceRecord'
14
+
15
+ type PriceListResponse = {
16
+ items?: Array<Record<string, unknown>>
17
+ }
18
+
19
+ function toDateInputValue(value: string | null | undefined): string {
20
+ if (!value) return ''
21
+ const date = new Date(value)
22
+ if (Number.isNaN(date.getTime())) return ''
23
+ return date.toISOString().slice(0, 10)
24
+ }
25
+
26
+ function buildUpdatePayload(
27
+ id: string,
28
+ values: PriceFormValues,
29
+ t: (key: string, fallback?: string) => string,
30
+ ): Record<string, unknown> {
31
+ const productId = values.productId?.trim() || undefined
32
+ const variantId = values.variantId?.trim() || undefined
33
+ if (!productId && !variantId) {
34
+ const message = t('catalog.prices.form.errors.productOrVariantRequired', 'Choose a product or a variant.')
35
+ throw createCrudFormError(message, { productId: message })
36
+ }
37
+ return {
38
+ id,
39
+ productId,
40
+ variantId,
41
+ priceKindId: values.priceKindId?.trim() || undefined,
42
+ currencyCode: values.currencyCode?.trim() || undefined,
43
+ unitPriceNet: values.unitPriceNet?.trim() || undefined,
44
+ unitPriceGross: values.unitPriceGross?.trim() || undefined,
45
+ taxRate: values.taxRate?.trim() || undefined,
46
+ minQuantity: values.minQuantity ?? undefined,
47
+ maxQuantity: values.maxQuantity ?? undefined,
48
+ startsAt: values.startsAt?.trim() || undefined,
49
+ endsAt: values.endsAt?.trim() || undefined,
50
+ customerId: values.customerId?.trim() || undefined,
51
+ customerGroupId: values.customerGroupId?.trim() || undefined,
52
+ channelId: values.channelId?.trim() || undefined,
53
+ offerId: values.offerId?.trim() || undefined,
54
+ userId: values.userId?.trim() || undefined,
55
+ userGroupId: values.userGroupId?.trim() || undefined,
56
+ }
57
+ }
58
+
59
+ export default function EditCatalogPricePage({ params }: { params?: { id?: string } }) {
60
+ const priceId = params?.id ?? ''
61
+ const t = useT()
62
+ const { fields, groups } = usePriceFormFields()
63
+ const [initialValues, setInitialValues] = React.useState<PriceFormValues | null>(null)
64
+ const [loading, setLoading] = React.useState(true)
65
+ const [error, setError] = React.useState<string | null>(null)
66
+ const [isNotFound, setIsNotFound] = React.useState(false)
67
+
68
+ React.useEffect(() => {
69
+ if (!priceId) return
70
+ let cancelled = false
71
+ async function load() {
72
+ setLoading(true)
73
+ setError(null)
74
+ setIsNotFound(false)
75
+ try {
76
+ const { ok, status, result } = await apiCall<PriceListResponse>(
77
+ `/api/catalog/prices?ids=${encodeURIComponent(priceId)}&pageSize=1`,
78
+ )
79
+ if (!ok) {
80
+ if (status === 404) {
81
+ if (!cancelled) setIsNotFound(true)
82
+ return
83
+ }
84
+ throw new Error(t('catalog.prices.form.errors.load', 'Failed to load price rule'))
85
+ }
86
+ const raw = Array.isArray(result?.items) ? result.items[0] : null
87
+ if (!raw) {
88
+ if (!cancelled) setIsNotFound(true)
89
+ return
90
+ }
91
+ if (cancelled) return
92
+ const record = normalizePriceRecord(raw)
93
+ setInitialValues({
94
+ id: record.id,
95
+ productId: record.productId ?? '',
96
+ variantId: record.variantId ?? '',
97
+ priceKindId: record.priceKindId ?? '',
98
+ currencyCode: record.currencyCode ?? '',
99
+ unitPriceNet: record.unitPriceNet ?? '',
100
+ unitPriceGross: record.unitPriceGross ?? '',
101
+ taxRate: record.taxRate ?? '',
102
+ minQuantity: record.minQuantity ?? 1,
103
+ maxQuantity: record.maxQuantity ?? undefined,
104
+ startsAt: toDateInputValue(record.startsAt),
105
+ endsAt: toDateInputValue(record.endsAt),
106
+ customerId: record.customerId ?? '',
107
+ customerGroupId: record.customerGroupId ?? '',
108
+ channelId: record.channelId ?? '',
109
+ offerId: record.offerId ?? '',
110
+ userId: record.userId ?? '',
111
+ userGroupId: record.userGroupId ?? '',
112
+ updatedAt: record.updatedAt ?? null,
113
+ })
114
+ } catch (err) {
115
+ if (!cancelled) {
116
+ const fallback = t('catalog.prices.form.errors.load', 'Failed to load price rule')
117
+ const message = err instanceof Error ? err.message : fallback
118
+ setError(message)
119
+ }
120
+ } finally {
121
+ if (!cancelled) setLoading(false)
122
+ }
123
+ }
124
+ load()
125
+ return () => {
126
+ cancelled = true
127
+ }
128
+ }, [priceId, t])
129
+
130
+ if (!priceId) {
131
+ return (
132
+ <Page>
133
+ <PageBody>
134
+ <p className="text-sm text-destructive">
135
+ {t('catalog.prices.form.errors.idRequired', 'Price rule identifier is required.')}
136
+ </p>
137
+ </PageBody>
138
+ </Page>
139
+ )
140
+ }
141
+
142
+ if (isNotFound) {
143
+ return (
144
+ <Page>
145
+ <PageBody>
146
+ <RecordNotFoundState
147
+ label={t('catalog.prices.form.errors.notFound', 'Price rule not found')}
148
+ backHref="/backend/catalog/prices"
149
+ backLabel={t('catalog.prices.form.actions.backToList', 'Back to price rules')}
150
+ />
151
+ </PageBody>
152
+ </Page>
153
+ )
154
+ }
155
+
156
+ if (error && !loading) {
157
+ return (
158
+ <Page>
159
+ <PageBody>
160
+ <ErrorMessage label={error} />
161
+ </PageBody>
162
+ </Page>
163
+ )
164
+ }
165
+
166
+ return (
167
+ <Page>
168
+ <PageBody>
169
+ <CrudForm<PriceFormValues>
170
+ title={t('catalog.prices.form.editTitle', 'Edit price rule')}
171
+ titleHeadingLevel={1}
172
+ backHref="/backend/catalog/prices"
173
+ fields={fields}
174
+ groups={groups}
175
+ collapsibleGroups
176
+ entityId={E.catalog.catalog_product_price}
177
+ initialValues={initialValues ?? { id: priceId }}
178
+ optimisticLockUpdatedAt={initialValues?.updatedAt}
179
+ isLoading={loading}
180
+ loadingMessage={t('catalog.prices.form.loading', 'Loading price rule...')}
181
+ submitLabel={t('catalog.prices.form.action.save', 'Save')}
182
+ cancelHref="/backend/catalog/prices"
183
+ successRedirect={`/backend/catalog/prices?flash=${encodeURIComponent(t('catalog.prices.flash.updated', 'Price rule updated'))}&type=success`}
184
+ onSubmit={async (values) => {
185
+ const payload = buildUpdatePayload(priceId, values, t)
186
+ await updateCrud('catalog/prices', payload)
187
+ }}
188
+ onDelete={async () => {
189
+ await deleteCrud('catalog/prices', priceId, {
190
+ errorMessage: t('catalog.prices.form.errors.delete', 'Failed to delete price rule'),
191
+ })
192
+ }}
193
+ deleteRedirect={`/backend/catalog/prices?flash=${encodeURIComponent(t('catalog.prices.flash.deleted', 'Price rule deleted'))}&type=success`}
194
+ />
195
+ </PageBody>
196
+ </Page>
197
+ )
198
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react'
2
+
3
+ const createPageIcon = React.createElement(
4
+ 'svg',
5
+ { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },
6
+ React.createElement('path', { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }),
7
+ React.createElement('polyline', { points: '14 2 14 8 20 8' }),
8
+ React.createElement('path', { d: 'M12 11v6M9 14h6' }),
9
+ )
10
+
11
+ export const metadata = {
12
+ requireAuth: true,
13
+ requireFeatures: ['catalog.pricing.manage'],
14
+ pageTitle: 'Create price rule',
15
+ pageTitleKey: 'catalog.prices.form.createTitle',
16
+ pageGroup: 'Catalog',
17
+ pageGroupKey: 'catalog.nav.group',
18
+ icon: createPageIcon,
19
+ breadcrumb: [
20
+ { label: 'Price rules', labelKey: 'catalog.prices.page.title', href: '/backend/catalog/prices' },
21
+ { label: 'Create', labelKey: 'common.create' },
22
+ ],
23
+ }
@@ -0,0 +1,70 @@
1
+ "use client"
2
+
3
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
4
+ import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
5
+ import { createCrud } from '@open-mercato/ui/backend/utils/crud'
6
+ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
7
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
8
+ import { E } from '#generated/entities.ids.generated'
9
+ import { PRICE_FORM_INITIAL_VALUES, usePriceFormFields, type PriceFormValues } from '../../../../components/prices/priceFormFields'
10
+
11
+ function buildPricePayload(values: PriceFormValues, t: (key: string, fallback?: string) => string): Record<string, unknown> {
12
+ const productId = values.productId?.trim() || undefined
13
+ const variantId = values.variantId?.trim() || undefined
14
+ if (!productId && !variantId) {
15
+ const message = t('catalog.prices.form.errors.productOrVariantRequired', 'Choose a product or a variant.')
16
+ throw createCrudFormError(message, { productId: message })
17
+ }
18
+ const priceKindId = values.priceKindId?.trim() || undefined
19
+ const currencyCode = values.currencyCode?.trim() || undefined
20
+ const payload: Record<string, unknown> = {
21
+ productId,
22
+ variantId,
23
+ priceKindId,
24
+ currencyCode,
25
+ unitPriceNet: values.unitPriceNet?.trim() || undefined,
26
+ unitPriceGross: values.unitPriceGross?.trim() || undefined,
27
+ taxRate: values.taxRate?.trim() || undefined,
28
+ minQuantity: values.minQuantity ?? undefined,
29
+ maxQuantity: values.maxQuantity ?? undefined,
30
+ startsAt: values.startsAt?.trim() || undefined,
31
+ endsAt: values.endsAt?.trim() || undefined,
32
+ customerId: values.customerId?.trim() || undefined,
33
+ customerGroupId: values.customerGroupId?.trim() || undefined,
34
+ channelId: values.channelId?.trim() || undefined,
35
+ offerId: values.offerId?.trim() || undefined,
36
+ userId: values.userId?.trim() || undefined,
37
+ userGroupId: values.userGroupId?.trim() || undefined,
38
+ }
39
+ return payload
40
+ }
41
+
42
+ export default function CreateCatalogPricePage() {
43
+ const t = useT()
44
+ const { fields, groups } = usePriceFormFields()
45
+ const successMessage = encodeURIComponent(t('catalog.prices.flash.created', 'Price rule created'))
46
+
47
+ return (
48
+ <Page>
49
+ <PageBody>
50
+ <CrudForm<PriceFormValues>
51
+ title={t('catalog.prices.form.createTitle', 'Create price rule')}
52
+ titleHeadingLevel={1}
53
+ backHref="/backend/catalog/prices"
54
+ fields={fields}
55
+ groups={groups}
56
+ collapsibleGroups
57
+ entityId={E.catalog.catalog_product_price}
58
+ initialValues={PRICE_FORM_INITIAL_VALUES}
59
+ submitLabel={t('catalog.prices.form.action.save', 'Save')}
60
+ cancelHref="/backend/catalog/prices"
61
+ successRedirect={`/backend/catalog/prices?flash=${successMessage}&type=success`}
62
+ onSubmit={async (values) => {
63
+ const payload = buildPricePayload(values, t)
64
+ await createCrud('catalog/prices', payload)
65
+ }}
66
+ />
67
+ </PageBody>
68
+ </Page>
69
+ )
70
+ }
@@ -0,0 +1,12 @@
1
+ export const metadata = {
2
+ requireAuth: true,
3
+ requireFeatures: ['catalog.products.view'],
4
+ pageTitle: 'Price rules',
5
+ pageTitleKey: 'catalog.prices.page.title',
6
+ pageGroup: 'Catalog',
7
+ pageGroupKey: 'catalog.nav.group',
8
+ pagePriority: 30,
9
+ pageOrder: 110,
10
+ icon: 'tag',
11
+ breadcrumb: [{ label: 'Price rules', labelKey: 'catalog.prices.page.title' }],
12
+ }
@@ -0,0 +1,14 @@
1
+ "use client"
2
+
3
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
4
+ import PricesDataTable from '../../../components/prices/PricesDataTable'
5
+
6
+ export default function CatalogPricesPage() {
7
+ return (
8
+ <Page>
9
+ <PageBody>
10
+ <PricesDataTable />
11
+ </PageBody>
12
+ </Page>
13
+ )
14
+ }
@@ -0,0 +1,243 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs/ComboboxInput'
5
+ import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { createLogger } from '@open-mercato/shared/lib/logger'
8
+
9
+ const logger = createLogger('catalog').child({ component: 'PriceScopeSelectors' })
10
+
11
+ type RemoteItem = Record<string, unknown>
12
+
13
+ function pickString(item: RemoteItem, ...keys: string[]): string {
14
+ for (const key of keys) {
15
+ const value = item[key]
16
+ if (typeof value === 'string' && value.length) return value
17
+ }
18
+ return ''
19
+ }
20
+
21
+ async function loadOptions(
22
+ path: string,
23
+ params: Record<string, string>,
24
+ mapItem: (item: RemoteItem) => ComboboxOption | null,
25
+ ): Promise<ComboboxOption[]> {
26
+ try {
27
+ const search = new URLSearchParams(params)
28
+ const payload = await readApiResultOrThrow<{ items?: RemoteItem[] }>(
29
+ `${path}?${search.toString()}`,
30
+ undefined,
31
+ { fallback: { items: [] } },
32
+ )
33
+ const items = Array.isArray(payload.items) ? payload.items : []
34
+ return items.map(mapItem).filter((option): option is ComboboxOption => option !== null)
35
+ } catch (err) {
36
+ logger.error('catalog.prices.scopeSelect.load', { err, path })
37
+ return []
38
+ }
39
+ }
40
+
41
+ async function resolveOne(
42
+ path: string,
43
+ idParam: string,
44
+ id: string,
45
+ mapItem: (item: RemoteItem) => ComboboxOption | null,
46
+ ): Promise<string> {
47
+ const options = await loadOptions(path, { [idParam]: id, pageSize: '1' }, mapItem)
48
+ return options[0]?.label ?? id
49
+ }
50
+
51
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
52
+
53
+ /**
54
+ * A typed query that is itself a record id (a pasted UUID) is searched via
55
+ * the generic `ids=` narrowing instead of the entity's free-text `search=`
56
+ * filter — free-text search matches title/name/sku columns, never the id
57
+ * column, so pasting an exact id would otherwise return zero suggestions.
58
+ */
59
+ function buildLookupParams(query: string | undefined, pageSize: string): Record<string, string> {
60
+ const trimmed = (query ?? '').trim()
61
+ if (UUID_PATTERN.test(trimmed)) return { ids: trimmed, pageSize: '1' }
62
+ return { search: trimmed, pageSize }
63
+ }
64
+
65
+ export function PriceProductSelect({
66
+ value,
67
+ onChange,
68
+ disabled,
69
+ }: {
70
+ value: string
71
+ onChange: (next: string) => void
72
+ disabled?: boolean
73
+ }) {
74
+ const t = useT()
75
+ const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {
76
+ const id = pickString(item, 'id')
77
+ if (!id) return null
78
+ const title = pickString(item, 'title', 'name') || id
79
+ const sku = pickString(item, 'sku')
80
+ return { value: id, label: sku ? `${title} (${sku})` : title }
81
+ }, [])
82
+ return (
83
+ <ComboboxInput
84
+ value={value}
85
+ onChange={onChange}
86
+ disabled={disabled}
87
+ allowCustomValues={false}
88
+ clearable
89
+ placeholder={t('catalog.prices.select.product.placeholder', 'Search products…')}
90
+ loadSuggestions={(query) => loadOptions('/api/catalog/products', buildLookupParams(query, '10'), mapItem)}
91
+ resolveLabel={(id) => resolveOne('/api/catalog/products', 'id', id, mapItem)}
92
+ />
93
+ )
94
+ }
95
+
96
+ export function PriceVariantSelect({
97
+ productId,
98
+ value,
99
+ onChange,
100
+ }: {
101
+ productId: string
102
+ value: string
103
+ onChange: (next: string) => void
104
+ }) {
105
+ const t = useT()
106
+ const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {
107
+ const id = pickString(item, 'id')
108
+ if (!id) return null
109
+ const name = pickString(item, 'name') || id
110
+ const sku = pickString(item, 'sku')
111
+ return { value: id, label: sku ? `${name} (${sku})` : name }
112
+ }, [])
113
+ const disabled = !productId
114
+ return (
115
+ <ComboboxInput
116
+ value={value}
117
+ onChange={onChange}
118
+ disabled={disabled}
119
+ allowCustomValues={false}
120
+ clearable
121
+ placeholder={
122
+ disabled
123
+ ? t('catalog.prices.form.field.product', 'Product')
124
+ : t('catalog.prices.select.product.placeholder', 'Search products…')
125
+ }
126
+ loadSuggestions={(query) =>
127
+ disabled
128
+ ? Promise.resolve([])
129
+ : loadOptions('/api/catalog/variants', { ...buildLookupParams(query, '10'), productId }, mapItem)
130
+ }
131
+ resolveLabel={(id) => resolveOne('/api/catalog/variants', 'id', id, mapItem)}
132
+ />
133
+ )
134
+ }
135
+
136
+ export function PriceCustomerSelect({
137
+ value,
138
+ onChange,
139
+ }: {
140
+ value: string
141
+ onChange: (next: string) => void
142
+ }) {
143
+ const t = useT()
144
+ const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {
145
+ const id = pickString(item, 'id')
146
+ if (!id) return null
147
+ const name = pickString(item, 'displayName', 'display_name', 'name') || id
148
+ const email = pickString(item, 'primaryEmail', 'primary_email')
149
+ return { value: id, label: name, description: email || null }
150
+ }, [])
151
+ return (
152
+ <ComboboxInput
153
+ value={value}
154
+ onChange={onChange}
155
+ allowCustomValues={false}
156
+ clearable
157
+ placeholder={t('catalog.prices.select.customer.placeholder', 'Search customers…')}
158
+ loadSuggestions={(query) => loadOptions('/api/customers/people', buildLookupParams(query, '10'), mapItem)}
159
+ resolveLabel={(id) => resolveOne('/api/customers/people', 'id', id, mapItem)}
160
+ />
161
+ )
162
+ }
163
+
164
+ export function PriceChannelSelect({
165
+ value,
166
+ onChange,
167
+ }: {
168
+ value: string
169
+ onChange: (next: string) => void
170
+ }) {
171
+ const t = useT()
172
+ const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {
173
+ const id = pickString(item, 'id')
174
+ if (!id) return null
175
+ const name = pickString(item, 'name') || id
176
+ const code = pickString(item, 'code')
177
+ return { value: id, label: code ? `${name} (${code})` : name }
178
+ }, [])
179
+ return (
180
+ <ComboboxInput
181
+ value={value}
182
+ onChange={onChange}
183
+ allowCustomValues={false}
184
+ clearable
185
+ placeholder={t('catalog.prices.select.channel.placeholder', 'Select a channel…')}
186
+ loadSuggestions={(query) => loadOptions('/api/sales/channels', buildLookupParams(query, '50'), mapItem)}
187
+ resolveLabel={(id) => resolveOne('/api/sales/channels', 'id', id, mapItem)}
188
+ />
189
+ )
190
+ }
191
+
192
+ export function PricePriceKindSelect({
193
+ value,
194
+ onChange,
195
+ }: {
196
+ value: string
197
+ onChange: (next: string) => void
198
+ }) {
199
+ const t = useT()
200
+ const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {
201
+ const id = pickString(item, 'id')
202
+ if (!id) return null
203
+ const title = pickString(item, 'title') || id
204
+ const code = pickString(item, 'code')
205
+ return { value: id, label: code ? `${title} (${code})` : title }
206
+ }, [])
207
+ return (
208
+ <ComboboxInput
209
+ value={value}
210
+ onChange={onChange}
211
+ allowCustomValues={false}
212
+ placeholder={t('catalog.prices.select.priceKind.placeholder', 'Select a price kind…')}
213
+ loadSuggestions={(query) => loadOptions('/api/catalog/price-kinds', buildLookupParams(query, '20'), mapItem)}
214
+ resolveLabel={(id) => resolveOne('/api/catalog/price-kinds', 'id', id, mapItem)}
215
+ />
216
+ )
217
+ }
218
+
219
+ export function PriceCurrencySelect({
220
+ value,
221
+ onChange,
222
+ }: {
223
+ value: string
224
+ onChange: (next: string) => void
225
+ }) {
226
+ const t = useT()
227
+ const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {
228
+ const code = pickString(item, 'value')
229
+ if (!code) return null
230
+ const label = pickString(item, 'label') || code
231
+ return { value: code, label: label === code ? code : `${code} — ${label}` }
232
+ }, [])
233
+ return (
234
+ <ComboboxInput
235
+ value={value}
236
+ onChange={onChange}
237
+ allowCustomValues={false}
238
+ placeholder={t('catalog.prices.select.currency.placeholder', 'Select a currency…')}
239
+ loadSuggestions={(query) => loadOptions('/api/currencies/currencies/options', { search: query ?? '', limit: '20' }, mapItem)}
240
+ resolveLabel={(id) => resolveOne('/api/currencies/currencies/options', 'search', id, mapItem)}
241
+ />
242
+ )
243
+ }