@open-mercato/core 0.4.6-develop-f7d3079656 → 0.4.6-develop-0861f05ea9

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 (107) hide show
  1. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +17 -154
  2. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +3 -3
  3. package/dist/modules/currencies/backend/exchange-rates/create/page.js +14 -152
  4. package/dist/modules/currencies/backend/exchange-rates/create/page.js.map +2 -2
  5. package/dist/modules/currencies/lib/exchangeRateFormConfig.js +167 -0
  6. package/dist/modules/currencies/lib/exchangeRateFormConfig.js.map +7 -0
  7. package/dist/modules/customers/api/dashboard/widgets/utils.js +1 -34
  8. package/dist/modules/customers/api/dashboard/widgets/utils.js.map +2 -2
  9. package/dist/modules/customers/commands/activities.js +3 -8
  10. package/dist/modules/customers/commands/activities.js.map +2 -2
  11. package/dist/modules/customers/commands/comments.js +2 -8
  12. package/dist/modules/customers/commands/comments.js.map +2 -2
  13. package/dist/modules/dashboards/lib/widgetScope.js +38 -0
  14. package/dist/modules/dashboards/lib/widgetScope.js.map +7 -0
  15. package/dist/modules/entities/lib/makeActivityRoute.js +265 -0
  16. package/dist/modules/entities/lib/makeActivityRoute.js.map +7 -0
  17. package/dist/modules/resources/api/activities.js +24 -232
  18. package/dist/modules/resources/api/activities.js.map +2 -2
  19. package/dist/modules/resources/commands/activities.js +3 -8
  20. package/dist/modules/resources/commands/activities.js.map +2 -2
  21. package/dist/modules/resources/commands/comments.js +2 -8
  22. package/dist/modules/resources/commands/comments.js.map +2 -2
  23. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js +27 -182
  24. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js.map +2 -2
  25. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js +28 -183
  26. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js.map +2 -2
  27. package/dist/modules/sales/api/order-line-statuses/route.js +15 -194
  28. package/dist/modules/sales/api/order-line-statuses/route.js.map +2 -2
  29. package/dist/modules/sales/api/order-lines/route.js +15 -281
  30. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  31. package/dist/modules/sales/api/order-statuses/route.js +15 -194
  32. package/dist/modules/sales/api/order-statuses/route.js.map +2 -2
  33. package/dist/modules/sales/api/payment-statuses/route.js +15 -194
  34. package/dist/modules/sales/api/payment-statuses/route.js.map +2 -2
  35. package/dist/modules/sales/api/quote-lines/route.js +15 -279
  36. package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
  37. package/dist/modules/sales/api/shipment-statuses/route.js +15 -194
  38. package/dist/modules/sales/api/shipment-statuses/route.js.map +2 -2
  39. package/dist/modules/sales/components/PaymentMethodsSettings.js +3 -84
  40. package/dist/modules/sales/components/PaymentMethodsSettings.js.map +2 -2
  41. package/dist/modules/sales/components/ProviderFieldInput.js +86 -0
  42. package/dist/modules/sales/components/ProviderFieldInput.js.map +7 -0
  43. package/dist/modules/sales/components/ShippingMethodsSettings.js +3 -82
  44. package/dist/modules/sales/components/ShippingMethodsSettings.js.map +2 -2
  45. package/dist/modules/sales/lib/makeSalesLineRoute.js +308 -0
  46. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +7 -0
  47. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js +206 -0
  48. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js.map +7 -0
  49. package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js +178 -0
  50. package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js.map +7 -0
  51. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +1 -39
  52. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +2 -2
  53. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +1 -39
  54. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +2 -2
  55. package/dist/modules/sales/widgets/dashboard/shared.js +46 -0
  56. package/dist/modules/sales/widgets/dashboard/shared.js.map +7 -0
  57. package/dist/modules/staff/api/activities.js +24 -232
  58. package/dist/modules/staff/api/activities.js.map +2 -2
  59. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +14 -34
  60. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  61. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +15 -34
  62. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
  63. package/dist/modules/staff/commands/activities.js +3 -8
  64. package/dist/modules/staff/commands/activities.js.map +2 -2
  65. package/dist/modules/staff/commands/comments.js +2 -8
  66. package/dist/modules/staff/commands/comments.js.map +2 -2
  67. package/dist/modules/staff/lib/leaveRequestHelpers.js +41 -0
  68. package/dist/modules/staff/lib/leaveRequestHelpers.js.map +7 -0
  69. package/package.json +2 -2
  70. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +20 -180
  71. package/src/modules/currencies/backend/exchange-rates/create/page.tsx +16 -175
  72. package/src/modules/currencies/lib/exchangeRateFormConfig.ts +200 -0
  73. package/src/modules/customers/api/dashboard/widgets/utils.ts +1 -53
  74. package/src/modules/customers/commands/activities.ts +2 -8
  75. package/src/modules/customers/commands/comments.ts +2 -8
  76. package/src/modules/dashboards/i18n/de.json +3 -0
  77. package/src/modules/dashboards/i18n/en.json +3 -0
  78. package/src/modules/dashboards/i18n/es.json +3 -0
  79. package/src/modules/dashboards/i18n/pl.json +3 -0
  80. package/src/modules/dashboards/lib/widgetScope.ts +53 -0
  81. package/src/modules/entities/lib/makeActivityRoute.ts +327 -0
  82. package/src/modules/resources/api/activities.ts +25 -269
  83. package/src/modules/resources/commands/activities.ts +2 -7
  84. package/src/modules/resources/commands/comments.ts +2 -8
  85. package/src/modules/sales/api/dashboard/widgets/new-orders/route.ts +29 -244
  86. package/src/modules/sales/api/dashboard/widgets/new-quotes/route.ts +30 -245
  87. package/src/modules/sales/api/order-line-statuses/route.ts +16 -209
  88. package/src/modules/sales/api/order-lines/route.ts +16 -300
  89. package/src/modules/sales/api/order-statuses/route.ts +16 -209
  90. package/src/modules/sales/api/payment-statuses/route.ts +16 -209
  91. package/src/modules/sales/api/quote-lines/route.ts +16 -298
  92. package/src/modules/sales/api/shipment-statuses/route.ts +16 -209
  93. package/src/modules/sales/components/PaymentMethodsSettings.tsx +3 -88
  94. package/src/modules/sales/components/ProviderFieldInput.tsx +85 -0
  95. package/src/modules/sales/components/ShippingMethodsSettings.tsx +3 -86
  96. package/src/modules/sales/lib/makeSalesLineRoute.ts +345 -0
  97. package/src/modules/sales/lib/makeStatusDictionaryRoute.ts +229 -0
  98. package/src/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.ts +247 -0
  99. package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +7 -50
  100. package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +7 -49
  101. package/src/modules/sales/widgets/dashboard/shared.ts +44 -0
  102. package/src/modules/staff/api/activities.ts +25 -269
  103. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +15 -69
  104. package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +16 -65
  105. package/src/modules/staff/commands/activities.ts +2 -7
  106. package/src/modules/staff/commands/comments.ts +2 -8
  107. package/src/modules/staff/lib/leaveRequestHelpers.ts +78 -0
@@ -1,14 +1,20 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter, useParams } from 'next/navigation'
4
+ import { useRouter } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
- import { CrudForm, type CrudFormGroup, type CrudFieldOption } from '@open-mercato/ui/backend/CrudForm'
6
+ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
7
+ import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
7
8
  import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
8
- import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
9
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
10
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
11
11
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
+ import {
13
+ loadCurrencyOptions,
14
+ exchangeRateGroups,
15
+ validateExchangeRateForm,
16
+ buildExchangeRatePayload,
17
+ } from '../../../lib/exchangeRateFormConfig'
12
18
 
13
19
  /**
14
20
  * Formats a Date object to YYYY-MM-DDTHH:MM format in local timezone
@@ -36,13 +42,6 @@ type ExchangeRateData = {
36
42
  tenantId: string
37
43
  }
38
44
 
39
- type CurrencyOption = {
40
- id: string
41
- code: string
42
- name: string
43
- isActive: boolean
44
- }
45
-
46
45
  export default function EditExchangeRatePage({ params }: { params?: { id?: string } }) {
47
46
  const t = useT()
48
47
  const router = useRouter()
@@ -51,30 +50,10 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
51
50
  const [loading, setLoading] = React.useState(true)
52
51
  const [error, setError] = React.useState<string | null>(null)
53
52
 
54
- const loadCurrencyOptions = React.useCallback(async (query?: string): Promise<CrudFieldOption[]> => {
55
- try {
56
- const params = new URLSearchParams()
57
- if (query) {
58
- params.set('search', query)
59
- }
60
- params.set('isActive', 'true')
61
- params.set('pageSize', '100')
62
-
63
- const call = await apiCall<{ items: CurrencyOption[] }>(
64
- `/api/currencies/currencies?${params.toString()}`
65
- )
66
-
67
- if (call.ok && call.result?.items) {
68
- return call.result.items.map((c) => ({
69
- value: c.code,
70
- label: c.code,
71
- }))
72
- }
73
- } catch (error) {
74
- console.error('Failed to load currencies:', error)
75
- }
76
- return []
77
- }, [])
53
+ const loadOptions = React.useCallback(
54
+ (query?: string) => loadCurrencyOptions(apiCall, query),
55
+ []
56
+ )
78
57
 
79
58
  // Load exchange rate data
80
59
  React.useEffect(() => {
@@ -95,93 +74,16 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
95
74
  loadExchangeRate()
96
75
  }, [params, t])
97
76
 
98
- const groups = React.useMemo<CrudFormGroup[]>(
99
- () => [
100
- {
101
- id: 'rate-details',
102
- column: 1,
103
- fields: [
104
- {
105
- id: 'fromCurrencyCode',
106
- type: 'combobox',
107
- label: t('exchangeRates.form.field.fromCurrency'),
108
- placeholder: t('exchangeRates.form.field.fromCurrencyPlaceholder'),
109
- required: true,
110
- loadOptions: loadCurrencyOptions,
111
- allowCustomValues: false,
112
- description: t('exchangeRates.form.field.fromCurrencyHelp'),
113
- },
114
- {
115
- id: 'toCurrencyCode',
116
- type: 'combobox',
117
- label: t('exchangeRates.form.field.toCurrency'),
118
- placeholder: t('exchangeRates.form.field.toCurrencyPlaceholder'),
119
- required: true,
120
- loadOptions: loadCurrencyOptions,
121
- allowCustomValues: false,
122
- description: t('exchangeRates.form.field.toCurrencyHelp'),
123
- },
124
- {
125
- id: 'rate',
126
- type: 'number',
127
- label: t('exchangeRates.form.field.rate'),
128
- placeholder: '1.00000000',
129
- required: true,
130
- description: t('exchangeRates.form.field.rateHelp'),
131
- },
132
- {
133
- id: 'date',
134
- type: 'datetime-local',
135
- label: t('exchangeRates.form.field.date'),
136
- required: true,
137
- description: t('exchangeRates.form.field.dateHelp'),
138
- },
139
- ],
140
- },
141
- {
142
- id: 'metadata',
143
- column: 2,
144
- title: t('exchangeRates.form.group.metadata'),
145
- fields: [
146
- {
147
- id: 'source',
148
- type: 'text',
149
- label: t('exchangeRates.form.field.source'),
150
- placeholder: t('exchangeRates.form.field.sourcePlaceholder'),
151
- required: true,
152
- description: t('exchangeRates.form.field.sourceHelp'),
153
- },
154
- {
155
- id: 'type',
156
- type: 'select',
157
- label: t('exchangeRates.form.field.type'),
158
- placeholder: t('exchangeRates.form.field.typePlaceholder'),
159
- required: false,
160
- description: t('exchangeRates.form.field.typeHelp'),
161
- options: [
162
- { value: '', label: t('exchangeRates.form.field.typeNone') },
163
- { value: 'buy', label: t('exchangeRates.form.field.typeBuy') },
164
- { value: 'sell', label: t('exchangeRates.form.field.typeSell') },
165
- ],
166
- },
167
- {
168
- id: 'isActive',
169
- type: 'checkbox',
170
- label: t('exchangeRates.form.field.isActive'),
171
- },
172
- ],
173
- },
174
- ],
175
- [t, loadCurrencyOptions]
77
+ const groups = React.useMemo(
78
+ () => exchangeRateGroups(t, loadOptions),
79
+ [t, loadOptions]
176
80
  )
177
81
 
178
82
  if (loading) {
179
83
  return (
180
84
  <Page>
181
85
  <PageBody>
182
- <div className="flex items-center justify-center p-8">
183
- <div className="text-muted-foreground">{t('exchangeRates.form.loading')}</div>
184
- </div>
86
+ <LoadingMessage label={t('exchangeRates.form.loading')} />
185
87
  </PageBody>
186
88
  </Page>
187
89
  )
@@ -191,7 +93,7 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
191
93
  return (
192
94
  <Page>
193
95
  <PageBody>
194
- <div className="text-destructive">{error || t('exchangeRates.form.errors.notFound')}</div>
96
+ <ErrorMessage label={error || t('exchangeRates.form.errors.notFound')} />
195
97
  </PageBody>
196
98
  </Page>
197
99
  )
@@ -218,72 +120,10 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
218
120
  submitLabel={t('exchangeRates.form.action.save')}
219
121
  cancelHref="/backend/exchange-rates"
220
122
  onSubmit={async (values) => {
221
- // Validate currency codes
222
- const fromCode = String(values.fromCurrencyCode || '').trim().toUpperCase()
223
- const toCode = String(values.toCurrencyCode || '').trim().toUpperCase()
224
-
225
- if (!/^[A-Z]{3}$/.test(fromCode)) {
226
- throw createCrudFormError(t('exchangeRates.form.errors.fromCurrencyFormat'), {
227
- fromCurrencyCode: t('exchangeRates.form.errors.currencyCodeFormat'),
228
- })
229
- }
230
-
231
- if (!/^[A-Z]{3}$/.test(toCode)) {
232
- throw createCrudFormError(t('exchangeRates.form.errors.toCurrencyFormat'), {
233
- toCurrencyCode: t('exchangeRates.form.errors.currencyCodeFormat'),
234
- })
235
- }
236
-
237
- if (fromCode === toCode) {
238
- throw createCrudFormError(t('exchangeRates.form.errors.sameCurrency'), {
239
- toCurrencyCode: t('exchangeRates.form.errors.sameCurrency'),
240
- })
241
- }
242
-
243
- // Validate rate
244
- const rate = parseFloat(String(values.rate || '0'))
245
- if (isNaN(rate) || rate <= 0) {
246
- throw createCrudFormError(t('exchangeRates.form.errors.invalidRate'), {
247
- rate: t('exchangeRates.form.errors.invalidRate'),
248
- })
249
- }
250
-
251
- // Validate date
252
- const date = values.date ? new Date(String(values.date)) : null
253
-
254
- if (!date || isNaN(date.getTime())) {
255
- throw createCrudFormError(t('exchangeRates.form.errors.invalidDate'), {
256
- date: t('exchangeRates.form.errors.invalidDate'),
257
- })
258
- }
259
-
260
- // Validate source
261
- const source = String(values.source || '').trim()
262
- if (!source || source.length < 2) {
263
- throw createCrudFormError(t('exchangeRates.form.errors.sourceTooShort'), {
264
- source: t('exchangeRates.form.errors.sourceTooShort'),
265
- })
266
- }
267
- if (source.length > 50) {
268
- throw createCrudFormError(t('exchangeRates.form.errors.sourceTooLong'), {
269
- source: t('exchangeRates.form.errors.sourceTooLong'),
270
- })
271
- }
272
- if (!/^[a-zA-Z0-9\s\-_]+$/.test(source)) {
273
- throw createCrudFormError(t('exchangeRates.form.errors.sourceInvalidFormat'), {
274
- source: t('exchangeRates.form.errors.sourceInvalidFormat'),
275
- })
276
- }
277
-
123
+ const validated = validateExchangeRateForm(values, t)
278
124
  const payload = {
279
125
  id: exchangeRate.id,
280
- fromCurrencyCode: fromCode,
281
- toCurrencyCode: toCode,
282
- rate: rate.toFixed(8),
283
- date: date.toISOString(),
284
- source,
285
- type: values.type && values.type !== '' ? values.type : null,
286
- isActive: values.isActive !== false,
126
+ ...buildExchangeRatePayload(values, validated),
287
127
  }
288
128
 
289
129
  await updateCrud('currencies/exchange-rates', payload)
@@ -3,129 +3,32 @@
3
3
  import * as React from 'react'
4
4
  import { useRouter } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
- import { CrudForm, type CrudFormGroup, type CrudFieldOption } from '@open-mercato/ui/backend/CrudForm'
6
+ import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
7
7
  import { createCrud } from '@open-mercato/ui/backend/utils/crud'
8
- import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
9
8
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
9
  import { useT } from '@open-mercato/shared/lib/i18n/context'
11
10
  import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
12
11
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
13
-
14
- type CurrencyOption = {
15
- id: string
16
- code: string
17
- name: string
18
- isActive: boolean
19
- }
12
+ import {
13
+ loadCurrencyOptions,
14
+ exchangeRateGroups,
15
+ validateExchangeRateForm,
16
+ buildExchangeRatePayload,
17
+ } from '../../../lib/exchangeRateFormConfig'
20
18
 
21
19
  export default function CreateExchangeRatePage() {
22
20
  const t = useT()
23
21
  const router = useRouter()
24
22
  const { organizationId, tenantId } = useOrganizationScopeDetail()
25
23
 
26
- const loadCurrencyOptions = React.useCallback(async (query?: string): Promise<CrudFieldOption[]> => {
27
- try {
28
- const params = new URLSearchParams()
29
- if (query) {
30
- params.set('search', query)
31
- }
32
- params.set('isActive', 'true')
33
- params.set('pageSize', '100')
34
-
35
- const call = await apiCall<{ items: CurrencyOption[] }>(
36
- `/api/currencies/currencies?${params.toString()}`
37
- )
38
-
39
- if (call.ok && call.result?.items) {
40
- return call.result.items.map((c) => ({
41
- value: c.code,
42
- label: c.code,
43
- }))
44
- }
45
- } catch (error) {
46
- console.error('Failed to load currencies:', error)
47
- }
48
- return []
49
- }, [])
24
+ const loadOptions = React.useCallback(
25
+ (query?: string) => loadCurrencyOptions(apiCall, query),
26
+ []
27
+ )
50
28
 
51
- const groups = React.useMemo<CrudFormGroup[]>(
52
- () => [
53
- {
54
- id: 'rate-details',
55
- column: 1,
56
- fields: [
57
- {
58
- id: 'fromCurrencyCode',
59
- type: 'combobox',
60
- label: t('exchangeRates.form.field.fromCurrency'),
61
- placeholder: t('exchangeRates.form.field.fromCurrencyPlaceholder'),
62
- required: true,
63
- loadOptions: loadCurrencyOptions,
64
- allowCustomValues: false,
65
- description: t('exchangeRates.form.field.fromCurrencyHelp'),
66
- },
67
- {
68
- id: 'toCurrencyCode',
69
- type: 'combobox',
70
- label: t('exchangeRates.form.field.toCurrency'),
71
- placeholder: t('exchangeRates.form.field.toCurrencyPlaceholder'),
72
- required: true,
73
- loadOptions: loadCurrencyOptions,
74
- allowCustomValues: false,
75
- description: t('exchangeRates.form.field.toCurrencyHelp'),
76
- },
77
- {
78
- id: 'rate',
79
- type: 'number',
80
- label: t('exchangeRates.form.field.rate'),
81
- placeholder: '1.00000000',
82
- required: true,
83
- description: t('exchangeRates.form.field.rateHelp'),
84
- },
85
- {
86
- id: 'date',
87
- type: 'datetime-local',
88
- label: t('exchangeRates.form.field.date'),
89
- required: true,
90
- description: t('exchangeRates.form.field.dateHelp'),
91
- },
92
- ],
93
- },
94
- {
95
- id: 'metadata',
96
- column: 2,
97
- title: t('exchangeRates.form.group.metadata'),
98
- fields: [
99
- {
100
- id: 'source',
101
- type: 'text',
102
- label: t('exchangeRates.form.field.source'),
103
- placeholder: t('exchangeRates.form.field.sourcePlaceholder'),
104
- required: true,
105
- description: t('exchangeRates.form.field.sourceHelp'),
106
- },
107
- {
108
- id: 'type',
109
- type: 'select',
110
- label: t('exchangeRates.form.field.type'),
111
- placeholder: t('exchangeRates.form.field.typePlaceholder'),
112
- required: false,
113
- description: t('exchangeRates.form.field.typeHelp'),
114
- options: [
115
- { value: '', label: t('exchangeRates.form.field.typeNone') },
116
- { value: 'buy', label: t('exchangeRates.form.field.typeBuy') },
117
- { value: 'sell', label: t('exchangeRates.form.field.typeSell') },
118
- ],
119
- },
120
- {
121
- id: 'isActive',
122
- type: 'checkbox',
123
- label: t('exchangeRates.form.field.isActive'),
124
- },
125
- ],
126
- },
127
- ],
128
- [t, loadCurrencyOptions]
29
+ const groups = React.useMemo(
30
+ () => exchangeRateGroups(t, loadOptions),
31
+ [t, loadOptions]
129
32
  )
130
33
 
131
34
  return (
@@ -139,73 +42,11 @@ export default function CreateExchangeRatePage() {
139
42
  submitLabel={t('exchangeRates.form.action.create')}
140
43
  cancelHref="/backend/exchange-rates"
141
44
  onSubmit={async (values) => {
142
- // Validate currency codes
143
- const fromCode = String(values.fromCurrencyCode || '').trim().toUpperCase()
144
- const toCode = String(values.toCurrencyCode || '').trim().toUpperCase()
145
-
146
- if (!/^[A-Z]{3}$/.test(fromCode)) {
147
- throw createCrudFormError(t('exchangeRates.form.errors.fromCurrencyFormat'), {
148
- fromCurrencyCode: t('exchangeRates.form.errors.currencyCodeFormat'),
149
- })
150
- }
151
-
152
- if (!/^[A-Z]{3}$/.test(toCode)) {
153
- throw createCrudFormError(t('exchangeRates.form.errors.toCurrencyFormat'), {
154
- toCurrencyCode: t('exchangeRates.form.errors.currencyCodeFormat'),
155
- })
156
- }
157
-
158
- if (fromCode === toCode) {
159
- throw createCrudFormError(t('exchangeRates.form.errors.sameCurrency'), {
160
- toCurrencyCode: t('exchangeRates.form.errors.sameCurrency'),
161
- })
162
- }
163
-
164
- // Validate rate
165
- const rate = parseFloat(String(values.rate || '0'))
166
- if (isNaN(rate) || rate <= 0) {
167
- throw createCrudFormError(t('exchangeRates.form.errors.invalidRate'), {
168
- rate: t('exchangeRates.form.errors.invalidRate'),
169
- })
170
- }
171
-
172
- // Validate date
173
- const date = values.date ? new Date(String(values.date)) : null
174
-
175
- if (!date || isNaN(date.getTime())) {
176
- throw createCrudFormError(t('exchangeRates.form.errors.invalidDate'), {
177
- date: t('exchangeRates.form.errors.invalidDate'),
178
- })
179
- }
180
-
181
- // Validate source
182
- const source = String(values.source || '').trim()
183
- if (!source || source.length < 2) {
184
- throw createCrudFormError(t('exchangeRates.form.errors.sourceTooShort'), {
185
- source: t('exchangeRates.form.errors.sourceTooShort'),
186
- })
187
- }
188
- if (source.length > 50) {
189
- throw createCrudFormError(t('exchangeRates.form.errors.sourceTooLong'), {
190
- source: t('exchangeRates.form.errors.sourceTooLong'),
191
- })
192
- }
193
- if (!/^[a-zA-Z0-9\s\-_]+$/.test(source)) {
194
- throw createCrudFormError(t('exchangeRates.form.errors.sourceInvalidFormat'), {
195
- source: t('exchangeRates.form.errors.sourceInvalidFormat'),
196
- })
197
- }
198
-
45
+ const validated = validateExchangeRateForm(values, t)
199
46
  const payload = {
200
47
  organizationId,
201
48
  tenantId,
202
- fromCurrencyCode: fromCode,
203
- toCurrencyCode: toCode,
204
- rate: rate.toFixed(8),
205
- date: date.toISOString(),
206
- source,
207
- type: values.type && values.type !== '' ? values.type : null,
208
- isActive: values.isActive !== false,
49
+ ...buildExchangeRatePayload(values, validated),
209
50
  }
210
51
 
211
52
  await createCrud('currencies/exchange-rates', payload)
@@ -0,0 +1,200 @@
1
+ import type { CrudFormGroup, CrudFieldOption } from '@open-mercato/ui/backend/CrudForm'
2
+ import type { ApiCallResult } from '@open-mercato/ui/backend/utils/apiCall'
3
+ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
4
+
5
+ export type CurrencyOption = {
6
+ id: string
7
+ code: string
8
+ name: string
9
+ isActive: boolean
10
+ }
11
+
12
+ type ApiCallFn = <T>(input: RequestInfo | URL, init?: RequestInit) => Promise<ApiCallResult<T>>
13
+
14
+ export async function loadCurrencyOptions(
15
+ apiCallFn: ApiCallFn,
16
+ query?: string,
17
+ ): Promise<CrudFieldOption[]> {
18
+ try {
19
+ const params = new URLSearchParams()
20
+ if (query) {
21
+ params.set('search', query)
22
+ }
23
+ params.set('isActive', 'true')
24
+ params.set('pageSize', '100')
25
+
26
+ const call = await apiCallFn<{ items: CurrencyOption[] }>(
27
+ `/api/currencies/currencies?${params.toString()}`
28
+ )
29
+
30
+ if (call.ok && call.result?.items) {
31
+ return call.result.items.map((c) => ({
32
+ value: c.code,
33
+ label: c.code,
34
+ }))
35
+ }
36
+ } catch (error) {
37
+ console.error('Failed to load currencies:', error)
38
+ }
39
+ return []
40
+ }
41
+
42
+ export function exchangeRateGroups(
43
+ t: (key: string) => string,
44
+ loadOptions: (query?: string) => Promise<CrudFieldOption[]>,
45
+ ): CrudFormGroup[] {
46
+ return [
47
+ {
48
+ id: 'rate-details',
49
+ column: 1,
50
+ fields: [
51
+ {
52
+ id: 'fromCurrencyCode',
53
+ type: 'combobox',
54
+ label: t('exchangeRates.form.field.fromCurrency'),
55
+ placeholder: t('exchangeRates.form.field.fromCurrencyPlaceholder'),
56
+ required: true,
57
+ loadOptions,
58
+ allowCustomValues: false,
59
+ description: t('exchangeRates.form.field.fromCurrencyHelp'),
60
+ },
61
+ {
62
+ id: 'toCurrencyCode',
63
+ type: 'combobox',
64
+ label: t('exchangeRates.form.field.toCurrency'),
65
+ placeholder: t('exchangeRates.form.field.toCurrencyPlaceholder'),
66
+ required: true,
67
+ loadOptions,
68
+ allowCustomValues: false,
69
+ description: t('exchangeRates.form.field.toCurrencyHelp'),
70
+ },
71
+ {
72
+ id: 'rate',
73
+ type: 'number',
74
+ label: t('exchangeRates.form.field.rate'),
75
+ placeholder: '1.00000000',
76
+ required: true,
77
+ description: t('exchangeRates.form.field.rateHelp'),
78
+ },
79
+ {
80
+ id: 'date',
81
+ type: 'datetime-local',
82
+ label: t('exchangeRates.form.field.date'),
83
+ required: true,
84
+ description: t('exchangeRates.form.field.dateHelp'),
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ id: 'metadata',
90
+ column: 2,
91
+ title: t('exchangeRates.form.group.metadata'),
92
+ fields: [
93
+ {
94
+ id: 'source',
95
+ type: 'text',
96
+ label: t('exchangeRates.form.field.source'),
97
+ placeholder: t('exchangeRates.form.field.sourcePlaceholder'),
98
+ required: true,
99
+ description: t('exchangeRates.form.field.sourceHelp'),
100
+ },
101
+ {
102
+ id: 'type',
103
+ type: 'select',
104
+ label: t('exchangeRates.form.field.type'),
105
+ placeholder: t('exchangeRates.form.field.typePlaceholder'),
106
+ required: false,
107
+ description: t('exchangeRates.form.field.typeHelp'),
108
+ options: [
109
+ { value: '', label: t('exchangeRates.form.field.typeNone') },
110
+ { value: 'buy', label: t('exchangeRates.form.field.typeBuy') },
111
+ { value: 'sell', label: t('exchangeRates.form.field.typeSell') },
112
+ ],
113
+ },
114
+ {
115
+ id: 'isActive',
116
+ type: 'checkbox',
117
+ label: t('exchangeRates.form.field.isActive'),
118
+ },
119
+ ],
120
+ },
121
+ ]
122
+ }
123
+
124
+ export function validateExchangeRateForm(
125
+ values: Record<string, unknown>,
126
+ t: (key: string) => string,
127
+ ): { fromCode: string; toCode: string; rate: number; date: Date; source: string } {
128
+ const fromCode = String(values.fromCurrencyCode || '').trim().toUpperCase()
129
+ const toCode = String(values.toCurrencyCode || '').trim().toUpperCase()
130
+
131
+ if (!/^[A-Z]{3}$/.test(fromCode)) {
132
+ throw createCrudFormError(t('exchangeRates.form.errors.fromCurrencyFormat'), {
133
+ fromCurrencyCode: t('exchangeRates.form.errors.currencyCodeFormat'),
134
+ })
135
+ }
136
+
137
+ if (!/^[A-Z]{3}$/.test(toCode)) {
138
+ throw createCrudFormError(t('exchangeRates.form.errors.toCurrencyFormat'), {
139
+ toCurrencyCode: t('exchangeRates.form.errors.currencyCodeFormat'),
140
+ })
141
+ }
142
+
143
+ if (fromCode === toCode) {
144
+ throw createCrudFormError(t('exchangeRates.form.errors.sameCurrency'), {
145
+ toCurrencyCode: t('exchangeRates.form.errors.sameCurrency'),
146
+ })
147
+ }
148
+
149
+ const rate = parseFloat(String(values.rate || '0'))
150
+ if (isNaN(rate) || rate <= 0) {
151
+ throw createCrudFormError(t('exchangeRates.form.errors.invalidRate'), {
152
+ rate: t('exchangeRates.form.errors.invalidRate'),
153
+ })
154
+ }
155
+
156
+ const date = values.date ? new Date(String(values.date)) : null
157
+
158
+ if (!date || isNaN(date.getTime())) {
159
+ throw createCrudFormError(t('exchangeRates.form.errors.invalidDate'), {
160
+ date: t('exchangeRates.form.errors.invalidDate'),
161
+ })
162
+ }
163
+
164
+ const source = String(values.source || '').trim()
165
+ if (!source || source.length < 2) {
166
+ throw createCrudFormError(t('exchangeRates.form.errors.sourceTooShort'), {
167
+ source: t('exchangeRates.form.errors.sourceTooShort'),
168
+ })
169
+ }
170
+ if (source.length > 50) {
171
+ throw createCrudFormError(t('exchangeRates.form.errors.sourceTooLong'), {
172
+ source: t('exchangeRates.form.errors.sourceTooLong'),
173
+ })
174
+ }
175
+ if (!/^[a-zA-Z0-9\s\-_]+$/.test(source)) {
176
+ throw createCrudFormError(t('exchangeRates.form.errors.sourceInvalidFormat'), {
177
+ source: t('exchangeRates.form.errors.sourceInvalidFormat'),
178
+ })
179
+ }
180
+
181
+ return { fromCode, toCode, rate, date, source }
182
+ }
183
+
184
+ export function buildExchangeRatePayload(values: Record<string, unknown>, validated: {
185
+ fromCode: string
186
+ toCode: string
187
+ rate: number
188
+ date: Date
189
+ source: string
190
+ }) {
191
+ return {
192
+ fromCurrencyCode: validated.fromCode,
193
+ toCurrencyCode: validated.toCode,
194
+ rate: validated.rate.toFixed(8),
195
+ date: validated.date.toISOString(),
196
+ source: validated.source,
197
+ type: values.type && values.type !== '' ? values.type : null,
198
+ isActive: values.isActive !== false,
199
+ }
200
+ }