@open-mercato/core 0.8.1-develop.7263.1.bbc6db3437 → 0.8.1-develop.7267.1.2e95af80a6

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 (145) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities.ids.generated.js +0 -4
  3. package/dist/generated/entities.ids.generated.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +0 -21
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -0
  7. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  8. package/dist/modules/communication_channels/commands/ingest-inbound-message.js +13 -0
  9. package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
  10. package/dist/modules/communication_channels/lib/email-mime.js +19 -1
  11. package/dist/modules/communication_channels/lib/email-mime.js.map +2 -2
  12. package/dist/modules/customers/lib/link-channel-message-handler.js +17 -7
  13. package/dist/modules/customers/lib/link-channel-message-handler.js.map +3 -3
  14. package/dist/modules/messages/api/route.js +1 -0
  15. package/dist/modules/messages/api/route.js.map +2 -2
  16. package/dist/modules/messages/commands/messages.js +3 -1
  17. package/dist/modules/messages/commands/messages.js.map +2 -2
  18. package/dist/modules/messages/data/validators.js +10 -2
  19. package/dist/modules/messages/data/validators.js.map +2 -2
  20. package/dist/modules/wms/di.js +0 -3
  21. package/dist/modules/wms/di.js.map +2 -2
  22. package/generated/entities.ids.generated.ts +0 -4
  23. package/generated/entity-fields-registry.ts +0 -21
  24. package/package.json +7 -7
  25. package/src/modules/auth/i18n/de.json +0 -3
  26. package/src/modules/auth/i18n/en.json +0 -3
  27. package/src/modules/auth/i18n/es.json +0 -3
  28. package/src/modules/auth/i18n/ko.json +0 -3
  29. package/src/modules/auth/i18n/pl.json +0 -3
  30. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +8 -0
  31. package/src/modules/communication_channels/commands/ingest-inbound-message.ts +16 -0
  32. package/src/modules/communication_channels/lib/email-mime.ts +52 -2
  33. package/src/modules/customers/lib/link-channel-message-handler.ts +48 -5
  34. package/src/modules/messages/api/route.ts +6 -3
  35. package/src/modules/messages/commands/messages.ts +3 -1
  36. package/src/modules/messages/data/validators.ts +21 -12
  37. package/src/modules/wms/di.ts +0 -6
  38. package/dist/generated/entities/availability_policy/index.js +0 -41
  39. package/dist/generated/entities/availability_policy/index.js.map +0 -7
  40. package/dist/modules/availability/acl.js +0 -16
  41. package/dist/modules/availability/acl.js.map +0 -7
  42. package/dist/modules/availability/api/check/route.js +0 -95
  43. package/dist/modules/availability/api/check/route.js.map +0 -7
  44. package/dist/modules/availability/api/openapi.js +0 -27
  45. package/dist/modules/availability/api/openapi.js.map +0 -7
  46. package/dist/modules/availability/api/policies/resolve-preview/route.js +0 -58
  47. package/dist/modules/availability/api/policies/resolve-preview/route.js.map +0 -7
  48. package/dist/modules/availability/api/policies/route.js +0 -188
  49. package/dist/modules/availability/api/policies/route.js.map +0 -7
  50. package/dist/modules/availability/backend/availability/check/page.js +0 -185
  51. package/dist/modules/availability/backend/availability/check/page.js.map +0 -7
  52. package/dist/modules/availability/backend/availability/check/page.meta.js +0 -16
  53. package/dist/modules/availability/backend/availability/check/page.meta.js.map +0 -7
  54. package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js +0 -79
  55. package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js.map +0 -7
  56. package/dist/modules/availability/backend/availability/policies/ScopeFields.js +0 -53
  57. package/dist/modules/availability/backend/availability/policies/ScopeFields.js.map +0 -7
  58. package/dist/modules/availability/backend/availability/policies/[id]/page.js +0 -152
  59. package/dist/modules/availability/backend/availability/policies/[id]/page.js.map +0 -7
  60. package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js +0 -12
  61. package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js.map +0 -7
  62. package/dist/modules/availability/backend/availability/policies/create/page.js +0 -94
  63. package/dist/modules/availability/backend/availability/policies/create/page.js.map +0 -7
  64. package/dist/modules/availability/backend/availability/policies/create/page.meta.js +0 -12
  65. package/dist/modules/availability/backend/availability/policies/create/page.meta.js.map +0 -7
  66. package/dist/modules/availability/backend/availability/policies/formGroups.js +0 -75
  67. package/dist/modules/availability/backend/availability/policies/formGroups.js.map +0 -7
  68. package/dist/modules/availability/backend/availability/policies/page.js +0 -176
  69. package/dist/modules/availability/backend/availability/policies/page.js.map +0 -7
  70. package/dist/modules/availability/backend/availability/policies/page.meta.js +0 -16
  71. package/dist/modules/availability/backend/availability/policies/page.meta.js.map +0 -7
  72. package/dist/modules/availability/commands/policies.js +0 -312
  73. package/dist/modules/availability/commands/policies.js.map +0 -7
  74. package/dist/modules/availability/commands/scope.js +0 -19
  75. package/dist/modules/availability/commands/scope.js.map +0 -7
  76. package/dist/modules/availability/data/entities.js +0 -95
  77. package/dist/modules/availability/data/entities.js.map +0 -7
  78. package/dist/modules/availability/data/validators.js +0 -53
  79. package/dist/modules/availability/data/validators.js.map +0 -7
  80. package/dist/modules/availability/di.js +0 -41
  81. package/dist/modules/availability/di.js.map +0 -7
  82. package/dist/modules/availability/events.js +0 -18
  83. package/dist/modules/availability/events.js.map +0 -7
  84. package/dist/modules/availability/index.js +0 -15
  85. package/dist/modules/availability/index.js.map +0 -7
  86. package/dist/modules/availability/lib/policyResolution.js +0 -155
  87. package/dist/modules/availability/lib/policyResolution.js.map +0 -7
  88. package/dist/modules/availability/lib/tryResolve.js +0 -11
  89. package/dist/modules/availability/lib/tryResolve.js.map +0 -7
  90. package/dist/modules/availability/migrations/Migration20260922095844_availability.js +0 -16
  91. package/dist/modules/availability/migrations/Migration20260922095844_availability.js.map +0 -7
  92. package/dist/modules/availability/setup.js +0 -12
  93. package/dist/modules/availability/setup.js.map +0 -7
  94. package/dist/modules/wms/lib/availabilityCache.js +0 -85
  95. package/dist/modules/wms/lib/availabilityCache.js.map +0 -7
  96. package/dist/modules/wms/lib/availabilityCalculation.js +0 -178
  97. package/dist/modules/wms/lib/availabilityCalculation.js.map +0 -7
  98. package/dist/modules/wms/lib/availabilityProvider.js +0 -14
  99. package/dist/modules/wms/lib/availabilityProvider.js.map +0 -7
  100. package/dist/modules/wms/lib/sqlInClause.js +0 -9
  101. package/dist/modules/wms/lib/sqlInClause.js.map +0 -7
  102. package/dist/modules/wms/lib/tryResolve.js +0 -11
  103. package/dist/modules/wms/lib/tryResolve.js.map +0 -7
  104. package/dist/modules/wms/subscribers/invalidate-availability-cache.js +0 -16
  105. package/dist/modules/wms/subscribers/invalidate-availability-cache.js.map +0 -7
  106. package/generated/entities/availability_policy/index.ts +0 -19
  107. package/src/modules/availability/acl.ts +0 -12
  108. package/src/modules/availability/api/check/route.ts +0 -114
  109. package/src/modules/availability/api/openapi.ts +0 -28
  110. package/src/modules/availability/api/policies/resolve-preview/route.ts +0 -67
  111. package/src/modules/availability/api/policies/route.ts +0 -224
  112. package/src/modules/availability/backend/availability/check/page.meta.ts +0 -12
  113. package/src/modules/availability/backend/availability/check/page.tsx +0 -223
  114. package/src/modules/availability/backend/availability/policies/ResolutionPreviewPanel.tsx +0 -106
  115. package/src/modules/availability/backend/availability/policies/ScopeFields.tsx +0 -55
  116. package/src/modules/availability/backend/availability/policies/[id]/page.meta.ts +0 -8
  117. package/src/modules/availability/backend/availability/policies/[id]/page.tsx +0 -182
  118. package/src/modules/availability/backend/availability/policies/create/page.meta.ts +0 -8
  119. package/src/modules/availability/backend/availability/policies/create/page.tsx +0 -105
  120. package/src/modules/availability/backend/availability/policies/formGroups.ts +0 -77
  121. package/src/modules/availability/backend/availability/policies/page.meta.ts +0 -12
  122. package/src/modules/availability/backend/availability/policies/page.tsx +0 -196
  123. package/src/modules/availability/commands/policies.ts +0 -358
  124. package/src/modules/availability/commands/scope.ts +0 -26
  125. package/src/modules/availability/data/entities.ts +0 -92
  126. package/src/modules/availability/data/validators.ts +0 -70
  127. package/src/modules/availability/di.ts +0 -54
  128. package/src/modules/availability/events.ts +0 -17
  129. package/src/modules/availability/i18n/de.json +0 -81
  130. package/src/modules/availability/i18n/en.json +0 -81
  131. package/src/modules/availability/i18n/es.json +0 -81
  132. package/src/modules/availability/i18n/ko.json +0 -81
  133. package/src/modules/availability/i18n/pl.json +0 -81
  134. package/src/modules/availability/index.ts +0 -13
  135. package/src/modules/availability/lib/policyResolution.ts +0 -264
  136. package/src/modules/availability/lib/tryResolve.ts +0 -8
  137. package/src/modules/availability/migrations/.snapshot-open-mercato.json +0 -375
  138. package/src/modules/availability/migrations/Migration20260922095844_availability.ts +0 -13
  139. package/src/modules/availability/setup.ts +0 -10
  140. package/src/modules/wms/lib/availabilityCache.ts +0 -136
  141. package/src/modules/wms/lib/availabilityCalculation.ts +0 -289
  142. package/src/modules/wms/lib/availabilityProvider.ts +0 -29
  143. package/src/modules/wms/lib/sqlInClause.ts +0 -21
  144. package/src/modules/wms/lib/tryResolve.ts +0 -8
  145. package/src/modules/wms/subscribers/invalidate-availability-cache.ts +0 -30
@@ -1,55 +0,0 @@
1
- "use client"
2
- import * as React from 'react'
3
- import { Input } from '@open-mercato/ui/primitives/input'
4
- import { Label } from '@open-mercato/ui/primitives/label'
5
- import { useT } from '@open-mercato/shared/lib/i18n/context'
6
-
7
- export type PolicyScope = {
8
- productId: string
9
- variantId: string
10
- storeId: string
11
- }
12
-
13
- /**
14
- * Product/variant/store target fields, kept as controlled local state
15
- * (outside `CrudForm`'s own field list) so a value change can drive the live
16
- * resolution-chain preview immediately — `CrudForm` does not expose a
17
- * live-values callback. Rendered via `CrudForm`'s `contentHeader` slot.
18
- */
19
- export function ScopeFields({ value, onChange, disabled }: { value: PolicyScope; onChange: (next: PolicyScope) => void; disabled?: boolean }) {
20
- const t = useT()
21
- return (
22
- <div className="grid grid-cols-1 gap-4 rounded-md border p-4 sm:grid-cols-3">
23
- <div className="space-y-1.5">
24
- <Label htmlFor="availability-policy-product-id">{t('availability.policies.form.field.productId')}</Label>
25
- <Input
26
- id="availability-policy-product-id"
27
- value={value.productId}
28
- disabled={disabled}
29
- onChange={(e) => onChange({ ...value, productId: e.target.value.trim() })}
30
- placeholder={t('availability.policies.form.field.productId.placeholder')}
31
- />
32
- </div>
33
- <div className="space-y-1.5">
34
- <Label htmlFor="availability-policy-variant-id">{t('availability.policies.form.field.variantId')}</Label>
35
- <Input
36
- id="availability-policy-variant-id"
37
- value={value.variantId}
38
- disabled={disabled}
39
- onChange={(e) => onChange({ ...value, variantId: e.target.value.trim() })}
40
- placeholder={t('availability.policies.form.field.variantId.placeholder')}
41
- />
42
- </div>
43
- <div className="space-y-1.5">
44
- <Label htmlFor="availability-policy-store-id">{t('availability.policies.form.field.storeId')}</Label>
45
- <Input
46
- id="availability-policy-store-id"
47
- value={value.storeId}
48
- disabled={disabled}
49
- onChange={(e) => onChange({ ...value, storeId: e.target.value.trim() })}
50
- placeholder={t('availability.policies.form.field.storeId.placeholder')}
51
- />
52
- </div>
53
- </div>
54
- )
55
- }
@@ -1,8 +0,0 @@
1
- export const metadata = {
2
- requireAuth: true,
3
- requireFeatures: ['availability.policies.view'],
4
- breadcrumb: [
5
- { label: 'Availability Policies', labelKey: 'availability.policies.nav.title', href: '/backend/availability/policies' },
6
- { label: 'Edit', labelKey: 'availability.policies.edit.title' },
7
- ],
8
- }
@@ -1,182 +0,0 @@
1
- "use client"
2
- import * as React from 'react'
3
- import { useRouter } from 'next/navigation'
4
- import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
5
- import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
6
- import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
7
- import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
8
- import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
- import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
10
- import { useT } from '@open-mercato/shared/lib/i18n/context'
11
- import { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'
12
- import { hasFeature } from '@open-mercato/shared/security/features'
13
- import { ScopeFields, type PolicyScope } from '../ScopeFields'
14
- import { ResolutionPreviewPanel } from '../ResolutionPreviewPanel'
15
- import { buildPolicyFieldGroups } from '../formGroups'
16
-
17
- type PolicyRecord = {
18
- id: string
19
- storeId: string | null
20
- productId: string | null
21
- variantId: string | null
22
- isStockManaged: boolean
23
- allowBackorder: boolean
24
- backorderLeadTimeDays: number | null
25
- preorderReleaseAt: string | null
26
- lowStockThreshold: number | null
27
- minOrderQuantity: number | null
28
- maxOrderQuantity: number | null
29
- quantityIncrement: number | null
30
- hideWhenOutOfStock: boolean
31
- isActive: boolean
32
- updatedAt: string | null
33
- }
34
-
35
- type FormValues = {
36
- isStockManaged?: boolean
37
- allowBackorder?: boolean
38
- backorderLeadTimeDays?: number | string | null
39
- preorderReleaseAt?: string | null
40
- lowStockThreshold?: number | string | null
41
- minOrderQuantity?: number | string | null
42
- maxOrderQuantity?: number | string | null
43
- quantityIncrement?: number | string | null
44
- hideWhenOutOfStock?: boolean
45
- isActive?: boolean
46
- }
47
-
48
- function toNullableInt(value: number | string | null | undefined): number | null {
49
- if (value === null || value === undefined || value === '') return null
50
- const parsed = typeof value === 'number' ? value : parseInt(value, 10)
51
- return Number.isFinite(parsed) ? parsed : null
52
- }
53
-
54
- export default function AvailabilityPolicyEditPage({ params }: { params?: { id?: string } }) {
55
- const t = useT()
56
- const router = useRouter()
57
- const id = params?.id ?? ''
58
- const { payload } = useBackendChrome()
59
- const canManage = hasFeature(payload?.grantedFeatures, 'availability.policies.manage')
60
-
61
- const [policy, setPolicy] = React.useState<PolicyRecord | null>(null)
62
- const [isLoading, setIsLoading] = React.useState(true)
63
- const [error, setError] = React.useState<string | null>(null)
64
- const [scope, setScope] = React.useState<PolicyScope>({ productId: '', variantId: '', storeId: '' })
65
-
66
- React.useEffect(() => {
67
- if (!id) return
68
- let cancelled = false
69
- async function load() {
70
- setIsLoading(true)
71
- setError(null)
72
- try {
73
- const call = await apiCall<{ items: PolicyRecord[] }>(`/api/availability/policies?id=${encodeURIComponent(id)}`, undefined, {
74
- fallback: { items: [] },
75
- })
76
- if (cancelled) return
77
- const record = call.ok ? call.result?.items?.[0] ?? null : null
78
- if (!record) {
79
- setError(t('availability.policies.edit.notFound'))
80
- return
81
- }
82
- setPolicy(record)
83
- setScope({ productId: record.productId ?? '', variantId: record.variantId ?? '', storeId: record.storeId ?? '' })
84
- } catch (e) {
85
- if (!cancelled) setError(e instanceof Error ? e.message : t('availability.policies.list.error.loadFailed'))
86
- } finally {
87
- if (!cancelled) setIsLoading(false)
88
- }
89
- }
90
- load()
91
- return () => { cancelled = true }
92
- }, [id, t])
93
-
94
- const groups = React.useMemo(() => buildPolicyFieldGroups(t), [t])
95
-
96
- if (isLoading) return <LoadingMessage label={t('common.loading')} />
97
- if (error || !policy) return <ErrorMessage label={error ?? t('availability.policies.edit.notFound')} />
98
-
99
- return (
100
- <CrudForm<FormValues>
101
- title={t('availability.policies.edit.title')}
102
- titleHeadingLevel={1}
103
- backHref="/backend/availability/policies"
104
- fields={[]}
105
- groups={groups}
106
- readOnly={!canManage}
107
- submitLabel={t('availability.policies.form.action.save')}
108
- cancelHref="/backend/availability/policies"
109
- optimisticLockUpdatedAt={policy.updatedAt ?? null}
110
- initialValues={{
111
- isStockManaged: policy.isStockManaged,
112
- allowBackorder: policy.allowBackorder,
113
- backorderLeadTimeDays: policy.backorderLeadTimeDays,
114
- preorderReleaseAt: policy.preorderReleaseAt,
115
- lowStockThreshold: policy.lowStockThreshold,
116
- minOrderQuantity: policy.minOrderQuantity,
117
- maxOrderQuantity: policy.maxOrderQuantity,
118
- quantityIncrement: policy.quantityIncrement,
119
- hideWhenOutOfStock: policy.hideWhenOutOfStock,
120
- isActive: policy.isActive,
121
- }}
122
- contentHeader={(
123
- <div className="space-y-4">
124
- <ScopeFields value={scope} onChange={setScope} disabled={!canManage} />
125
- <ResolutionPreviewPanel
126
- productId={scope.productId}
127
- variantId={scope.variantId || null}
128
- storeId={scope.storeId || null}
129
- />
130
- </div>
131
- )}
132
- deleteVisible={canManage}
133
- onDelete={async () => {
134
- await deleteCrud('availability/policies', id)
135
- flash(t('availability.policies.flash.deleted'), 'success')
136
- router.push('/backend/availability/policies')
137
- }}
138
- onSubmit={async (values) => {
139
- if (scope.variantId && !scope.productId) {
140
- throw createCrudFormError(
141
- t('availability.policies.errors.variantRequiresProduct'),
142
- { productId: t('availability.policies.errors.variantRequiresProduct') },
143
- )
144
- }
145
- if (values.allowBackorder && toNullableInt(values.backorderLeadTimeDays) == null) {
146
- throw createCrudFormError(
147
- t('availability.policies.errors.backorderRequiresLeadTime'),
148
- { backorderLeadTimeDays: t('availability.policies.errors.backorderRequiresLeadTime') },
149
- )
150
- }
151
- const min = toNullableInt(values.minOrderQuantity)
152
- const max = toNullableInt(values.maxOrderQuantity)
153
- if (min != null && max != null && max < min) {
154
- throw createCrudFormError(
155
- t('availability.policies.errors.maxBelowMin'),
156
- { maxOrderQuantity: t('availability.policies.errors.maxBelowMin') },
157
- )
158
- }
159
-
160
- const payloadBody = {
161
- id,
162
- productId: scope.productId || null,
163
- variantId: scope.variantId || null,
164
- storeId: scope.storeId || null,
165
- isStockManaged: !!values.isStockManaged,
166
- allowBackorder: !!values.allowBackorder,
167
- backorderLeadTimeDays: toNullableInt(values.backorderLeadTimeDays),
168
- preorderReleaseAt: values.preorderReleaseAt || null,
169
- lowStockThreshold: toNullableInt(values.lowStockThreshold),
170
- minOrderQuantity: min,
171
- maxOrderQuantity: max,
172
- quantityIncrement: toNullableInt(values.quantityIncrement),
173
- hideWhenOutOfStock: !!values.hideWhenOutOfStock,
174
- isActive: values.isActive !== false,
175
- }
176
- await updateCrud('availability/policies', payloadBody)
177
- flash(t('availability.policies.flash.updated'), 'success')
178
- router.push('/backend/availability/policies')
179
- }}
180
- />
181
- )
182
- }
@@ -1,8 +0,0 @@
1
- export const metadata = {
2
- requireAuth: true,
3
- requireFeatures: ['availability.policies.manage'],
4
- breadcrumb: [
5
- { label: 'Availability Policies', labelKey: 'availability.policies.nav.title', href: '/backend/availability/policies' },
6
- { label: 'Create', labelKey: 'availability.policies.create.title' },
7
- ],
8
- }
@@ -1,105 +0,0 @@
1
- "use client"
2
- import * as React from 'react'
3
- import { useRouter } from 'next/navigation'
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 { flash } from '@open-mercato/ui/backend/FlashMessages'
8
- import { useT } from '@open-mercato/shared/lib/i18n/context'
9
- import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
10
- import { ScopeFields, type PolicyScope } from '../ScopeFields'
11
- import { ResolutionPreviewPanel } from '../ResolutionPreviewPanel'
12
- import { buildPolicyFieldGroups } from '../formGroups'
13
-
14
- type FormValues = {
15
- isStockManaged?: boolean
16
- allowBackorder?: boolean
17
- backorderLeadTimeDays?: number | string | null
18
- preorderReleaseAt?: string | null
19
- lowStockThreshold?: number | string | null
20
- minOrderQuantity?: number | string | null
21
- maxOrderQuantity?: number | string | null
22
- quantityIncrement?: number | string | null
23
- hideWhenOutOfStock?: boolean
24
- isActive?: boolean
25
- }
26
-
27
- function toNullableInt(value: number | string | null | undefined): number | null {
28
- if (value === null || value === undefined || value === '') return null
29
- const parsed = typeof value === 'number' ? value : parseInt(value, 10)
30
- return Number.isFinite(parsed) ? parsed : null
31
- }
32
-
33
- export default function AvailabilityPolicyCreatePage() {
34
- const t = useT()
35
- const router = useRouter()
36
- const { organizationId, tenantId } = useOrganizationScopeDetail()
37
- const [scope, setScope] = React.useState<PolicyScope>({ productId: '', variantId: '', storeId: '' })
38
-
39
- const groups = React.useMemo(() => buildPolicyFieldGroups(t), [t])
40
-
41
- return (
42
- <CrudForm<FormValues>
43
- title={t('availability.policies.create.title')}
44
- titleHeadingLevel={1}
45
- backHref="/backend/availability/policies"
46
- fields={[]}
47
- groups={groups}
48
- submitLabel={t('availability.policies.form.action.create')}
49
- cancelHref="/backend/availability/policies"
50
- contentHeader={(
51
- <div className="space-y-4">
52
- <ScopeFields value={scope} onChange={setScope} />
53
- <ResolutionPreviewPanel
54
- productId={scope.productId}
55
- variantId={scope.variantId || null}
56
- storeId={scope.storeId || null}
57
- />
58
- </div>
59
- )}
60
- onSubmit={async (values) => {
61
- if (scope.variantId && !scope.productId) {
62
- throw createCrudFormError(
63
- t('availability.policies.errors.variantRequiresProduct'),
64
- { productId: t('availability.policies.errors.variantRequiresProduct') },
65
- )
66
- }
67
- if (values.allowBackorder && toNullableInt(values.backorderLeadTimeDays) == null) {
68
- throw createCrudFormError(
69
- t('availability.policies.errors.backorderRequiresLeadTime'),
70
- { backorderLeadTimeDays: t('availability.policies.errors.backorderRequiresLeadTime') },
71
- )
72
- }
73
- const min = toNullableInt(values.minOrderQuantity)
74
- const max = toNullableInt(values.maxOrderQuantity)
75
- if (min != null && max != null && max < min) {
76
- throw createCrudFormError(
77
- t('availability.policies.errors.maxBelowMin'),
78
- { maxOrderQuantity: t('availability.policies.errors.maxBelowMin') },
79
- )
80
- }
81
-
82
- const payload = {
83
- organizationId,
84
- tenantId,
85
- productId: scope.productId || null,
86
- variantId: scope.variantId || null,
87
- storeId: scope.storeId || null,
88
- isStockManaged: !!values.isStockManaged,
89
- allowBackorder: !!values.allowBackorder,
90
- backorderLeadTimeDays: toNullableInt(values.backorderLeadTimeDays),
91
- preorderReleaseAt: values.preorderReleaseAt || null,
92
- lowStockThreshold: toNullableInt(values.lowStockThreshold),
93
- minOrderQuantity: min,
94
- maxOrderQuantity: max,
95
- quantityIncrement: toNullableInt(values.quantityIncrement),
96
- hideWhenOutOfStock: !!values.hideWhenOutOfStock,
97
- isActive: values.isActive !== false,
98
- }
99
- await createCrud('availability/policies', payload)
100
- flash(t('availability.policies.flash.created'), 'success')
101
- router.push('/backend/availability/policies')
102
- }}
103
- />
104
- )
105
- }
@@ -1,77 +0,0 @@
1
- import type { CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
2
-
3
- /**
4
- * Shared field groups for the create and edit policy forms. Scope fields
5
- * (productId/variantId/storeId) live outside this list — see `ScopeFields`.
6
- */
7
- export function buildPolicyFieldGroups(t: (key: string) => string): CrudFormGroup[] {
8
- return [
9
- {
10
- id: 'sell-policy',
11
- column: 1,
12
- title: t('availability.policies.form.group.sellPolicy'),
13
- fields: [
14
- {
15
- id: 'isStockManaged',
16
- type: 'checkbox',
17
- label: t('availability.policies.form.field.isStockManaged'),
18
- description: t('availability.policies.form.field.isStockManaged.help'),
19
- },
20
- {
21
- id: 'allowBackorder',
22
- type: 'checkbox',
23
- label: t('availability.policies.form.field.allowBackorder'),
24
- },
25
- {
26
- id: 'backorderLeadTimeDays',
27
- type: 'number',
28
- label: t('availability.policies.form.field.backorderLeadTimeDays'),
29
- description: t('availability.policies.form.field.backorderLeadTimeDays.help'),
30
- },
31
- {
32
- id: 'preorderReleaseAt',
33
- type: 'datetime-local',
34
- label: t('availability.policies.form.field.preorderReleaseAt'),
35
- },
36
- ],
37
- },
38
- {
39
- id: 'thresholds',
40
- column: 2,
41
- title: t('availability.policies.form.group.thresholds'),
42
- fields: [
43
- {
44
- id: 'lowStockThreshold',
45
- type: 'number',
46
- label: t('availability.policies.form.field.lowStockThreshold'),
47
- },
48
- {
49
- id: 'minOrderQuantity',
50
- type: 'number',
51
- label: t('availability.policies.form.field.minOrderQuantity'),
52
- },
53
- {
54
- id: 'maxOrderQuantity',
55
- type: 'number',
56
- label: t('availability.policies.form.field.maxOrderQuantity'),
57
- },
58
- {
59
- id: 'quantityIncrement',
60
- type: 'number',
61
- label: t('availability.policies.form.field.quantityIncrement'),
62
- },
63
- {
64
- id: 'hideWhenOutOfStock',
65
- type: 'checkbox',
66
- label: t('availability.policies.form.field.hideWhenOutOfStock'),
67
- },
68
- {
69
- id: 'isActive',
70
- type: 'checkbox',
71
- label: t('availability.policies.form.field.isActive'),
72
- defaultValue: true,
73
- },
74
- ],
75
- },
76
- ]
77
- }
@@ -1,12 +0,0 @@
1
- export const metadata = {
2
- requireAuth: true,
3
- requireFeatures: ['availability.policies.view'],
4
- pageTitle: 'Availability Policies',
5
- pageTitleKey: 'availability.policies.nav.title',
6
- pageGroup: 'Availability',
7
- pageGroupKey: 'availability.nav.group',
8
- pageOrder: 10,
9
- pageContext: 'settings' as const,
10
- icon: 'boxes',
11
- breadcrumb: [{ label: 'Availability Policies', labelKey: 'availability.policies.nav.title' }],
12
- }
@@ -1,196 +0,0 @@
1
- "use client"
2
- import * as React from 'react'
3
- import Link from 'next/link'
4
- import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
- import { DataTable } from '@open-mercato/ui/backend/DataTable'
6
- import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
7
- import { Button } from '@open-mercato/ui/primitives/button'
8
- import { RowActions } from '@open-mercato/ui/backend/RowActions'
9
- import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
10
- import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
11
- import { flash } from '@open-mercato/ui/backend/FlashMessages'
12
- import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
13
- import { useT } from '@open-mercato/shared/lib/i18n/context'
14
- import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
15
- import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
16
-
17
- type Row = {
18
- id: string
19
- storeId: string | null
20
- productId: string | null
21
- variantId: string | null
22
- isStockManaged: boolean
23
- allowBackorder: boolean
24
- isActive: boolean
25
- updatedAt: string | null
26
- }
27
-
28
- type ResponsePayload = {
29
- items: Row[]
30
- total: number
31
- page?: number
32
- pageSize?: number
33
- totalPages: number
34
- totalIsCapped?: boolean
35
- }
36
-
37
- function ScopeCell({ value, allLabel }: { value: string | null; allLabel: string }) {
38
- if (!value) return <span className="text-muted-foreground">{allLabel}</span>
39
- return <code className="text-xs">{value}</code>
40
- }
41
-
42
- export default function AvailabilityPoliciesListPage() {
43
- const [rows, setRows] = React.useState<Row[]>([])
44
- const [page, setPage] = React.useState(1)
45
- const [total, setTotal] = React.useState(0)
46
- const [totalPages, setTotalPages] = React.useState(1)
47
- const [totalIsCapped, setTotalIsCapped] = React.useState(false)
48
- const [isLoading, setIsLoading] = React.useState(true)
49
- const [reloadToken, setReloadToken] = React.useState(0)
50
- const scopeVersion = useOrganizationScopeVersion()
51
- const t = useT()
52
- const { confirm, ConfirmDialogElement } = useConfirmDialog()
53
- const [filterValues, setFilterValues] = React.useState<FilterValues>({})
54
-
55
- const filters = React.useMemo<FilterDef[]>(() => [
56
- { id: 'productId', label: t('availability.policies.list.columns.product'), type: 'text' },
57
- { id: 'storeId', label: t('availability.policies.list.columns.store'), type: 'text' },
58
- {
59
- id: 'isActive',
60
- label: t('availability.policies.list.columns.active'),
61
- type: 'select',
62
- options: [
63
- { value: 'true', label: t('common.yes') },
64
- { value: 'false', label: t('common.no') },
65
- ],
66
- },
67
- ], [t])
68
-
69
- React.useEffect(() => {
70
- let cancelled = false
71
- async function load() {
72
- setIsLoading(true)
73
- try {
74
- const params = new URLSearchParams()
75
- params.set('page', String(page))
76
- params.set('pageSize', '50')
77
- const productId = typeof filterValues.productId === 'string' ? filterValues.productId.trim() : ''
78
- const storeId = typeof filterValues.storeId === 'string' ? filterValues.storeId.trim() : ''
79
- const isActive = typeof filterValues.isActive === 'string' ? filterValues.isActive : ''
80
- if (productId) params.set('productId', productId)
81
- if (storeId) params.set('storeId', storeId)
82
- if (isActive) params.set('isActive', isActive)
83
- const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }
84
- const call = await apiCall<ResponsePayload>(`/api/availability/policies?${params.toString()}`, undefined, { fallback })
85
- if (!call.ok) {
86
- const errorPayload = call.result as { error?: string } | undefined
87
- const message = typeof errorPayload?.error === 'string' ? errorPayload.error : t('availability.policies.list.error.loadFailed')
88
- flash(message, 'error')
89
- return
90
- }
91
- const payload = call.result ?? fallback
92
- if (!cancelled) {
93
- setRows(Array.isArray(payload.items) ? payload.items : [])
94
- setTotal(payload.total || 0)
95
- setTotalPages(payload.totalPages || 1)
96
- setTotalIsCapped(payload?.totalIsCapped === true)
97
- }
98
- } catch (error) {
99
- if (!cancelled) {
100
- const message = error instanceof Error ? error.message : t('availability.policies.list.error.loadFailed')
101
- flash(message, 'error')
102
- }
103
- } finally {
104
- if (!cancelled) setIsLoading(false)
105
- }
106
- }
107
- load()
108
- return () => { cancelled = true }
109
- }, [page, reloadToken, scopeVersion, filterValues, t])
110
-
111
- const handleDelete = React.useCallback(async (row: Row) => {
112
- const confirmed = await confirm({ title: t('availability.policies.list.confirmDelete'), variant: 'destructive' })
113
- if (!confirmed) return
114
- try {
115
- const call = await withScopedApiRequestHeaders(buildOptimisticLockHeader(row.updatedAt), () =>
116
- apiCall<{ error?: string }>(`/api/availability/policies?id=${encodeURIComponent(row.id)}`, { method: 'DELETE' }, { fallback: null }),
117
- )
118
- if (!call.ok) {
119
- const errorPayload = call.result as { error?: string } | undefined
120
- const message = typeof errorPayload?.error === 'string' ? errorPayload.error : t('availability.policies.list.error.deleteFailed')
121
- flash(message, 'error')
122
- return
123
- }
124
- flash(t('availability.policies.list.success.deleted'), 'success')
125
- setReloadToken((token) => token + 1)
126
- } catch (error) {
127
- const message = error instanceof Error ? error.message : t('availability.policies.list.error.deleteFailed')
128
- flash(message, 'error')
129
- }
130
- }, [confirm, t])
131
-
132
- const columns = React.useMemo<ColumnDef<Row>[]>(() => [
133
- {
134
- accessorKey: 'productId',
135
- header: t('availability.policies.list.columns.product'),
136
- cell: ({ row }) => <ScopeCell value={row.original.productId} allLabel={t('availability.policies.list.storeDefault')} />,
137
- },
138
- {
139
- accessorKey: 'variantId',
140
- header: t('availability.policies.list.columns.variant'),
141
- cell: ({ row }) => <ScopeCell value={row.original.variantId} allLabel={t('availability.policies.list.allVariants')} />,
142
- },
143
- {
144
- accessorKey: 'storeId',
145
- header: t('availability.policies.list.columns.store'),
146
- cell: ({ row }) => <ScopeCell value={row.original.storeId} allLabel={t('availability.policies.list.allStores')} />,
147
- },
148
- {
149
- accessorKey: 'isStockManaged',
150
- header: t('availability.policies.list.columns.stockManaged'),
151
- cell: ({ row }) => (row.original.isStockManaged ? t('common.yes') : t('common.no')),
152
- },
153
- {
154
- accessorKey: 'allowBackorder',
155
- header: t('availability.policies.list.columns.backorder'),
156
- cell: ({ row }) => (row.original.allowBackorder ? t('common.yes') : t('common.no')),
157
- },
158
- {
159
- accessorKey: 'isActive',
160
- header: t('availability.policies.list.columns.active'),
161
- cell: ({ row }) => (row.original.isActive ? t('common.yes') : t('common.no')),
162
- },
163
- ], [t])
164
-
165
- return (
166
- <Page>
167
- <PageBody>
168
- <DataTable
169
- title={t('availability.policies.list.title')}
170
- titleHeadingLevel={1}
171
- actions={(
172
- <Button asChild>
173
- <Link href="/backend/availability/policies/create">{t('availability.policies.list.actions.create')}</Link>
174
- </Button>
175
- )}
176
- columns={columns}
177
- data={rows}
178
- filters={filters}
179
- filterValues={filterValues}
180
- onFiltersApply={(values) => { setFilterValues(values); setPage(1) }}
181
- onFiltersClear={() => { setFilterValues({}); setPage(1) }}
182
- perspective={{ tableId: 'availability.policies.list' }}
183
- rowActions={(row) => (
184
- <RowActions items={[
185
- { id: 'edit', label: t('common.edit'), href: `/backend/availability/policies/${row.id}` },
186
- { id: 'delete', label: t('common.delete'), destructive: true, onSelect: () => { void handleDelete(row) } },
187
- ]} />
188
- )}
189
- pagination={{ page, pageSize: 50, total, totalPages, totalIsCapped, onPageChange: setPage }}
190
- isLoading={isLoading}
191
- />
192
- </PageBody>
193
- {ConfirmDialogElement}
194
- </Page>
195
- )
196
- }