@open-mercato/core 0.8.1-develop.7266.1.8e520bbe03 → 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 (124) 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/wms/di.js +0 -3
  7. package/dist/modules/wms/di.js.map +2 -2
  8. package/generated/entities.ids.generated.ts +0 -4
  9. package/generated/entity-fields-registry.ts +0 -21
  10. package/package.json +7 -7
  11. package/src/modules/auth/i18n/de.json +0 -3
  12. package/src/modules/auth/i18n/en.json +0 -3
  13. package/src/modules/auth/i18n/es.json +0 -3
  14. package/src/modules/auth/i18n/ko.json +0 -3
  15. package/src/modules/auth/i18n/pl.json +0 -3
  16. package/src/modules/wms/di.ts +0 -6
  17. package/dist/generated/entities/availability_policy/index.js +0 -41
  18. package/dist/generated/entities/availability_policy/index.js.map +0 -7
  19. package/dist/modules/availability/acl.js +0 -16
  20. package/dist/modules/availability/acl.js.map +0 -7
  21. package/dist/modules/availability/api/check/route.js +0 -95
  22. package/dist/modules/availability/api/check/route.js.map +0 -7
  23. package/dist/modules/availability/api/openapi.js +0 -27
  24. package/dist/modules/availability/api/openapi.js.map +0 -7
  25. package/dist/modules/availability/api/policies/resolve-preview/route.js +0 -58
  26. package/dist/modules/availability/api/policies/resolve-preview/route.js.map +0 -7
  27. package/dist/modules/availability/api/policies/route.js +0 -188
  28. package/dist/modules/availability/api/policies/route.js.map +0 -7
  29. package/dist/modules/availability/backend/availability/check/page.js +0 -185
  30. package/dist/modules/availability/backend/availability/check/page.js.map +0 -7
  31. package/dist/modules/availability/backend/availability/check/page.meta.js +0 -16
  32. package/dist/modules/availability/backend/availability/check/page.meta.js.map +0 -7
  33. package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js +0 -79
  34. package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js.map +0 -7
  35. package/dist/modules/availability/backend/availability/policies/ScopeFields.js +0 -53
  36. package/dist/modules/availability/backend/availability/policies/ScopeFields.js.map +0 -7
  37. package/dist/modules/availability/backend/availability/policies/[id]/page.js +0 -152
  38. package/dist/modules/availability/backend/availability/policies/[id]/page.js.map +0 -7
  39. package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js +0 -12
  40. package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js.map +0 -7
  41. package/dist/modules/availability/backend/availability/policies/create/page.js +0 -94
  42. package/dist/modules/availability/backend/availability/policies/create/page.js.map +0 -7
  43. package/dist/modules/availability/backend/availability/policies/create/page.meta.js +0 -12
  44. package/dist/modules/availability/backend/availability/policies/create/page.meta.js.map +0 -7
  45. package/dist/modules/availability/backend/availability/policies/formGroups.js +0 -75
  46. package/dist/modules/availability/backend/availability/policies/formGroups.js.map +0 -7
  47. package/dist/modules/availability/backend/availability/policies/page.js +0 -176
  48. package/dist/modules/availability/backend/availability/policies/page.js.map +0 -7
  49. package/dist/modules/availability/backend/availability/policies/page.meta.js +0 -16
  50. package/dist/modules/availability/backend/availability/policies/page.meta.js.map +0 -7
  51. package/dist/modules/availability/commands/policies.js +0 -312
  52. package/dist/modules/availability/commands/policies.js.map +0 -7
  53. package/dist/modules/availability/commands/scope.js +0 -19
  54. package/dist/modules/availability/commands/scope.js.map +0 -7
  55. package/dist/modules/availability/data/entities.js +0 -95
  56. package/dist/modules/availability/data/entities.js.map +0 -7
  57. package/dist/modules/availability/data/validators.js +0 -53
  58. package/dist/modules/availability/data/validators.js.map +0 -7
  59. package/dist/modules/availability/di.js +0 -41
  60. package/dist/modules/availability/di.js.map +0 -7
  61. package/dist/modules/availability/events.js +0 -18
  62. package/dist/modules/availability/events.js.map +0 -7
  63. package/dist/modules/availability/index.js +0 -15
  64. package/dist/modules/availability/index.js.map +0 -7
  65. package/dist/modules/availability/lib/policyResolution.js +0 -155
  66. package/dist/modules/availability/lib/policyResolution.js.map +0 -7
  67. package/dist/modules/availability/lib/tryResolve.js +0 -11
  68. package/dist/modules/availability/lib/tryResolve.js.map +0 -7
  69. package/dist/modules/availability/migrations/Migration20260922095844_availability.js +0 -16
  70. package/dist/modules/availability/migrations/Migration20260922095844_availability.js.map +0 -7
  71. package/dist/modules/availability/setup.js +0 -12
  72. package/dist/modules/availability/setup.js.map +0 -7
  73. package/dist/modules/wms/lib/availabilityCache.js +0 -85
  74. package/dist/modules/wms/lib/availabilityCache.js.map +0 -7
  75. package/dist/modules/wms/lib/availabilityCalculation.js +0 -178
  76. package/dist/modules/wms/lib/availabilityCalculation.js.map +0 -7
  77. package/dist/modules/wms/lib/availabilityProvider.js +0 -14
  78. package/dist/modules/wms/lib/availabilityProvider.js.map +0 -7
  79. package/dist/modules/wms/lib/sqlInClause.js +0 -9
  80. package/dist/modules/wms/lib/sqlInClause.js.map +0 -7
  81. package/dist/modules/wms/lib/tryResolve.js +0 -11
  82. package/dist/modules/wms/lib/tryResolve.js.map +0 -7
  83. package/dist/modules/wms/subscribers/invalidate-availability-cache.js +0 -16
  84. package/dist/modules/wms/subscribers/invalidate-availability-cache.js.map +0 -7
  85. package/generated/entities/availability_policy/index.ts +0 -19
  86. package/src/modules/availability/acl.ts +0 -12
  87. package/src/modules/availability/api/check/route.ts +0 -114
  88. package/src/modules/availability/api/openapi.ts +0 -28
  89. package/src/modules/availability/api/policies/resolve-preview/route.ts +0 -67
  90. package/src/modules/availability/api/policies/route.ts +0 -224
  91. package/src/modules/availability/backend/availability/check/page.meta.ts +0 -12
  92. package/src/modules/availability/backend/availability/check/page.tsx +0 -223
  93. package/src/modules/availability/backend/availability/policies/ResolutionPreviewPanel.tsx +0 -106
  94. package/src/modules/availability/backend/availability/policies/ScopeFields.tsx +0 -55
  95. package/src/modules/availability/backend/availability/policies/[id]/page.meta.ts +0 -8
  96. package/src/modules/availability/backend/availability/policies/[id]/page.tsx +0 -182
  97. package/src/modules/availability/backend/availability/policies/create/page.meta.ts +0 -8
  98. package/src/modules/availability/backend/availability/policies/create/page.tsx +0 -105
  99. package/src/modules/availability/backend/availability/policies/formGroups.ts +0 -77
  100. package/src/modules/availability/backend/availability/policies/page.meta.ts +0 -12
  101. package/src/modules/availability/backend/availability/policies/page.tsx +0 -196
  102. package/src/modules/availability/commands/policies.ts +0 -358
  103. package/src/modules/availability/commands/scope.ts +0 -26
  104. package/src/modules/availability/data/entities.ts +0 -92
  105. package/src/modules/availability/data/validators.ts +0 -70
  106. package/src/modules/availability/di.ts +0 -54
  107. package/src/modules/availability/events.ts +0 -17
  108. package/src/modules/availability/i18n/de.json +0 -81
  109. package/src/modules/availability/i18n/en.json +0 -81
  110. package/src/modules/availability/i18n/es.json +0 -81
  111. package/src/modules/availability/i18n/ko.json +0 -81
  112. package/src/modules/availability/i18n/pl.json +0 -81
  113. package/src/modules/availability/index.ts +0 -13
  114. package/src/modules/availability/lib/policyResolution.ts +0 -264
  115. package/src/modules/availability/lib/tryResolve.ts +0 -8
  116. package/src/modules/availability/migrations/.snapshot-open-mercato.json +0 -375
  117. package/src/modules/availability/migrations/Migration20260922095844_availability.ts +0 -13
  118. package/src/modules/availability/setup.ts +0 -10
  119. package/src/modules/wms/lib/availabilityCache.ts +0 -136
  120. package/src/modules/wms/lib/availabilityCalculation.ts +0 -289
  121. package/src/modules/wms/lib/availabilityProvider.ts +0 -29
  122. package/src/modules/wms/lib/sqlInClause.ts +0 -21
  123. package/src/modules/wms/lib/tryResolve.ts +0 -8
  124. package/src/modules/wms/subscribers/invalidate-availability-cache.ts +0 -30
@@ -1,224 +0,0 @@
1
- import { NextResponse } from 'next/server'
2
- import { z } from 'zod'
3
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
4
- import { AvailabilityPolicy } from '../../data/entities'
5
- import type { EntityManager } from '@mikro-orm/postgresql'
6
- import type { FilterQuery } from '@mikro-orm/core'
7
- import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
8
- import { resolveActiveOrganizationId, organizationScopeRequiredResponse } from '@open-mercato/shared/lib/auth/organizationScope'
9
- import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
10
- import { availabilityPolicyCreateSchema, availabilityPolicyUpdateSchema } from '../../data/validators'
11
- import {
12
- createAvailabilityCrudOpenApi,
13
- createPagedListResponseSchema,
14
- defaultOkResponseSchema,
15
- } from '../openapi'
16
-
17
- const routeMetadata = {
18
- GET: { requireAuth: true, requireFeatures: ['availability.policies.view'] },
19
- POST: { requireAuth: true, requireFeatures: ['availability.policies.manage'] },
20
- PUT: { requireAuth: true, requireFeatures: ['availability.policies.manage'] },
21
- DELETE: { requireAuth: true, requireFeatures: ['availability.policies.manage'] },
22
- }
23
-
24
- export const metadata = routeMetadata
25
-
26
- const rawBodySchema = z.object({}).loose()
27
- type CrudInput = Record<string, unknown>
28
-
29
- const crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({
30
- metadata: routeMetadata,
31
- orm: {
32
- entity: AvailabilityPolicy,
33
- idField: 'id',
34
- orgField: 'organizationId',
35
- tenantField: 'tenantId',
36
- softDeleteField: 'deletedAt',
37
- },
38
- events: {
39
- module: 'availability',
40
- entity: 'policy',
41
- persistent: true,
42
- },
43
- actions: {
44
- create: {
45
- commandId: 'availability.policies.create',
46
- schema: rawBodySchema,
47
- mapInput: ({ parsed }) => parsed,
48
- response: ({ result }) => ({ id: String(result.policyId) }),
49
- status: 201,
50
- },
51
- update: {
52
- commandId: 'availability.policies.update',
53
- schema: rawBodySchema,
54
- mapInput: ({ parsed }) => parsed,
55
- response: () => ({ ok: true }),
56
- },
57
- delete: {
58
- commandId: 'availability.policies.delete',
59
- schema: rawBodySchema,
60
- mapInput: ({ raw, ctx }) => ({
61
- id: ((raw as Record<string, unknown>).query as Record<string, unknown> | undefined)?.id as string | undefined,
62
- organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? undefined,
63
- tenantId: ctx.auth?.tenantId ?? undefined,
64
- }),
65
- response: () => ({ ok: true }),
66
- },
67
- },
68
- })
69
-
70
- const listQuerySchema = z.object({
71
- id: z.uuid().optional(),
72
- page: z.coerce.number().min(1).default(1),
73
- pageSize: z.coerce.number().min(1).max(100).default(50),
74
- storeId: z.uuid().optional(),
75
- productId: z.uuid().optional(),
76
- variantId: z.uuid().optional(),
77
- isActive: z.enum(['true', 'false']).optional(),
78
- sortField: z.enum(['createdAt', 'updatedAt']).optional(),
79
- sortDir: z.enum(['asc', 'desc']).optional(),
80
- }).loose()
81
-
82
- type AvailabilityPolicyRow = {
83
- id: string
84
- organizationId: string
85
- tenantId: string
86
- storeId: string | null
87
- productId: string | null
88
- variantId: string | null
89
- isStockManaged: boolean
90
- allowBackorder: boolean
91
- backorderLeadTimeDays: number | null
92
- preorderReleaseAt: string | null
93
- lowStockThreshold: number | null
94
- minOrderQuantity: number | null
95
- maxOrderQuantity: number | null
96
- quantityIncrement: number | null
97
- hideWhenOutOfStock: boolean
98
- isActive: boolean
99
- createdAt: string | null
100
- updatedAt: string | null
101
- }
102
-
103
- const toRow = (policy: AvailabilityPolicy): AvailabilityPolicyRow => ({
104
- id: String(policy.id),
105
- organizationId: String(policy.organizationId),
106
- tenantId: String(policy.tenantId),
107
- storeId: policy.storeId ?? null,
108
- productId: policy.productId ?? null,
109
- variantId: policy.variantId ?? null,
110
- isStockManaged: !!policy.isStockManaged,
111
- allowBackorder: !!policy.allowBackorder,
112
- backorderLeadTimeDays: policy.backorderLeadTimeDays ?? null,
113
- preorderReleaseAt: policy.preorderReleaseAt ? policy.preorderReleaseAt.toISOString() : null,
114
- lowStockThreshold: policy.lowStockThreshold ?? null,
115
- minOrderQuantity: policy.minOrderQuantity ?? null,
116
- maxOrderQuantity: policy.maxOrderQuantity ?? null,
117
- quantityIncrement: policy.quantityIncrement ?? null,
118
- hideWhenOutOfStock: !!policy.hideWhenOutOfStock,
119
- isActive: !!policy.isActive,
120
- createdAt: policy.createdAt ? policy.createdAt.toISOString() : null,
121
- updatedAt: policy.updatedAt ? policy.updatedAt.toISOString() : null,
122
- })
123
-
124
- export async function GET(req: Request) {
125
- const auth = await getAuthFromRequest(req)
126
- if (!auth || !auth.tenantId) {
127
- return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 401 })
128
- }
129
- const organizationId = resolveActiveOrganizationId(auth)
130
- // A superadmin with no organization selected legitimately sees every
131
- // organization in the tenant (om_selected_org=__all__); anyone else with
132
- // an unresolved scope gets the standard 400, never a 401 (that reads as an
133
- // expired session to the client and redirect-loops through session
134
- // refresh — see organizationScope.ts).
135
- if (!organizationId && !auth.isSuperAdmin) return organizationScopeRequiredResponse()
136
-
137
- const url = new URL(req.url)
138
- const parsed = listQuerySchema.safeParse({
139
- id: url.searchParams.get('id') ?? undefined,
140
- page: url.searchParams.get('page') ?? undefined,
141
- pageSize: url.searchParams.get('pageSize') ?? undefined,
142
- storeId: url.searchParams.get('storeId') ?? undefined,
143
- productId: url.searchParams.get('productId') ?? undefined,
144
- variantId: url.searchParams.get('variantId') ?? undefined,
145
- isActive: url.searchParams.get('isActive') ?? undefined,
146
- sortField: url.searchParams.get('sortField') ?? undefined,
147
- sortDir: url.searchParams.get('sortDir') ?? undefined,
148
- })
149
- if (!parsed.success) {
150
- return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 400 })
151
- }
152
-
153
- const container = await createRequestContainer()
154
- const em = container.resolve('em') as EntityManager
155
-
156
- const { id, page, pageSize, storeId, productId, variantId, isActive, sortField, sortDir } = parsed.data
157
- const filter: FilterQuery<AvailabilityPolicy> = {
158
- tenantId: auth.tenantId,
159
- deletedAt: null,
160
- }
161
- if (organizationId) filter.organizationId = organizationId
162
- if (id) filter.id = id
163
- if (storeId) filter.storeId = storeId
164
- if (productId) filter.productId = productId
165
- if (variantId) filter.variantId = variantId
166
- if (isActive === 'true') filter.isActive = true
167
- if (isActive === 'false') filter.isActive = false
168
-
169
- const orderBy: Record<string, 'ASC' | 'DESC'> = {}
170
- orderBy[sortField ?? 'createdAt'] = sortDir === 'asc' ? 'ASC' : 'DESC'
171
-
172
- const offset = (page - 1) * pageSize
173
- const [rows, total] = await em.findAndCount(AvailabilityPolicy, filter, { orderBy, limit: pageSize, offset })
174
- const items = rows.map(toRow)
175
- const totalPages = Math.max(1, Math.ceil(total / pageSize))
176
-
177
- return NextResponse.json({ items, total, page, pageSize, totalPages })
178
- }
179
-
180
- export const POST = crud.POST
181
- export const PUT = crud.PUT
182
- export const DELETE = crud.DELETE
183
-
184
- const availabilityPolicyListItemSchema = z.object({
185
- id: z.uuid(),
186
- organizationId: z.uuid(),
187
- tenantId: z.uuid(),
188
- storeId: z.uuid().nullable(),
189
- productId: z.uuid().nullable(),
190
- variantId: z.uuid().nullable(),
191
- isStockManaged: z.boolean(),
192
- allowBackorder: z.boolean(),
193
- backorderLeadTimeDays: z.number().nullable(),
194
- preorderReleaseAt: z.string().nullable(),
195
- lowStockThreshold: z.number().nullable(),
196
- minOrderQuantity: z.number().nullable(),
197
- maxOrderQuantity: z.number().nullable(),
198
- quantityIncrement: z.number().nullable(),
199
- hideWhenOutOfStock: z.boolean(),
200
- isActive: z.boolean(),
201
- createdAt: z.string().nullable(),
202
- updatedAt: z.string().nullable(),
203
- })
204
-
205
- export const openApi = createAvailabilityCrudOpenApi({
206
- resourceName: 'AvailabilityPolicy',
207
- pluralName: 'AvailabilityPolicies',
208
- querySchema: listQuerySchema,
209
- listResponseSchema: createPagedListResponseSchema(availabilityPolicyListItemSchema),
210
- create: {
211
- schema: availabilityPolicyCreateSchema,
212
- description: 'Creates a new availability policy.',
213
- },
214
- update: {
215
- schema: availabilityPolicyUpdateSchema,
216
- responseSchema: defaultOkResponseSchema,
217
- description: 'Updates an existing availability policy by id.',
218
- },
219
- del: {
220
- schema: z.object({ id: z.string().uuid() }),
221
- responseSchema: defaultOkResponseSchema,
222
- description: 'Deletes an availability policy by id.',
223
- },
224
- })
@@ -1,12 +0,0 @@
1
- export const metadata = {
2
- requireAuth: true,
3
- requireFeatures: ['availability.check'],
4
- pageTitle: 'Availability Check',
5
- pageTitleKey: 'availability.check.nav.title',
6
- pageGroup: 'Availability',
7
- pageGroupKey: 'availability.nav.group',
8
- pageOrder: 20,
9
- pageContext: 'settings' as const,
10
- icon: 'search',
11
- breadcrumb: [{ label: 'Availability Check', labelKey: 'availability.check.nav.title' }],
12
- }
@@ -1,223 +0,0 @@
1
- "use client"
2
- import * as React from 'react'
3
- import { Page, PageBody } from '@open-mercato/ui/backend/Page'
4
- import { Input } from '@open-mercato/ui/primitives/input'
5
- import { Label } from '@open-mercato/ui/primitives/label'
6
- import { Button } from '@open-mercato/ui/primitives/button'
7
- import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
8
- import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
9
- import { useT } from '@open-mercato/shared/lib/i18n/context'
10
-
11
- type AvailabilityState = 'in_stock' | 'low_stock' | 'out_of_stock' | 'backorder' | 'preorder' | 'not_tracked'
12
-
13
- type AvailabilityItemResult = {
14
- state: AvailabilityState
15
- availableQuantity: number | null
16
- canFulfil: boolean
17
- leadTimeDays: number | null
18
- releaseAt: string | null
19
- isAuthoritative: boolean
20
- policySourceId: string | null
21
- }
22
-
23
- type FieldTrace = { value: unknown; policySourceId: string | null }
24
- type PolicyTrace = Record<string, FieldTrace>
25
-
26
- type CheckResponse = {
27
- availability?: AvailabilityItemResult | null
28
- policyTrace?: PolicyTrace | null
29
- error?: string
30
- }
31
-
32
- const STATE_VARIANTS: Record<AvailabilityState, StatusBadgeVariant> = {
33
- in_stock: 'success',
34
- low_stock: 'warning',
35
- out_of_stock: 'error',
36
- backorder: 'info',
37
- preorder: 'info',
38
- not_tracked: 'neutral',
39
- }
40
-
41
- const TRACE_FIELDS = [
42
- 'isStockManaged',
43
- 'allowBackorder',
44
- 'backorderLeadTimeDays',
45
- 'preorderReleaseAt',
46
- 'lowStockThreshold',
47
- 'minOrderQuantity',
48
- 'maxOrderQuantity',
49
- 'quantityIncrement',
50
- 'hideWhenOutOfStock',
51
- 'isActive',
52
- ]
53
-
54
- function formatTraceValue(value: unknown, t: (key: string) => string): string {
55
- if (value === null || value === undefined) return t('availability.policies.form.preview.notSet')
56
- if (typeof value === 'boolean') return value ? t('common.yes') : t('common.no')
57
- return String(value)
58
- }
59
-
60
- export default function AvailabilityCheckPage() {
61
- const t = useT()
62
- const [productId, setProductId] = React.useState('')
63
- const [variantId, setVariantId] = React.useState('')
64
- const [storeId, setStoreId] = React.useState('')
65
- const [quantity, setQuantity] = React.useState('1')
66
- const [isRunning, setIsRunning] = React.useState(false)
67
- const [error, setError] = React.useState<string | null>(null)
68
- const [result, setResult] = React.useState<CheckResponse | null>(null)
69
-
70
- const runCheck = React.useCallback(async () => {
71
- if (!productId.trim()) {
72
- setError(t('availability.check.errors.productRequired'))
73
- return
74
- }
75
- setIsRunning(true)
76
- setError(null)
77
- setResult(null)
78
- try {
79
- const call = await apiCall<CheckResponse>(
80
- '/api/availability/check',
81
- {
82
- method: 'POST',
83
- body: JSON.stringify({
84
- productId: productId.trim(),
85
- variantId: variantId.trim() || null,
86
- storeId: storeId.trim() || null,
87
- quantity: Number(quantity) > 0 ? Number(quantity) : 1,
88
- }),
89
- },
90
- { fallback: null },
91
- )
92
- if (!call.ok) {
93
- const errorPayload = call.result as { error?: string } | undefined
94
- setError(typeof errorPayload?.error === 'string' ? errorPayload.error : t('availability.check.errors.failed'))
95
- return
96
- }
97
- setResult(call.result ?? null)
98
- } catch (e) {
99
- setError(e instanceof Error ? e.message : t('availability.check.errors.failed'))
100
- } finally {
101
- setIsRunning(false)
102
- }
103
- }, [productId, variantId, storeId, quantity, t])
104
-
105
- const availability = result?.availability ?? null
106
- const policyTrace = result?.policyTrace ?? null
107
-
108
- return (
109
- <Page>
110
- <PageBody>
111
- <div
112
- className="space-y-6"
113
- onKeyDown={(event) => {
114
- if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
115
- event.preventDefault()
116
- void runCheck()
117
- }
118
- }}
119
- >
120
- <h1 className="text-xl font-semibold">{t('availability.check.title')}</h1>
121
- <div className="grid grid-cols-1 gap-4 rounded-md border p-4 sm:grid-cols-4">
122
- <div className="space-y-1.5">
123
- <Label htmlFor="availability-check-product-id">{t('availability.policies.form.field.productId')}</Label>
124
- <Input
125
- id="availability-check-product-id"
126
- value={productId}
127
- onChange={(e) => setProductId(e.target.value)}
128
- autoFocus
129
- />
130
- </div>
131
- <div className="space-y-1.5">
132
- <Label htmlFor="availability-check-variant-id">{t('availability.policies.form.field.variantId')}</Label>
133
- <Input id="availability-check-variant-id" value={variantId} onChange={(e) => setVariantId(e.target.value)} />
134
- </div>
135
- <div className="space-y-1.5">
136
- <Label htmlFor="availability-check-store-id">{t('availability.policies.form.field.storeId')}</Label>
137
- <Input id="availability-check-store-id" value={storeId} onChange={(e) => setStoreId(e.target.value)} />
138
- </div>
139
- <div className="space-y-1.5">
140
- <Label htmlFor="availability-check-quantity">{t('availability.check.field.quantity')}</Label>
141
- <Input
142
- id="availability-check-quantity"
143
- type="number"
144
- min={1}
145
- value={quantity}
146
- onChange={(e) => setQuantity(e.target.value)}
147
- />
148
- </div>
149
- </div>
150
- <Button onClick={() => void runCheck()} disabled={isRunning}>
151
- {isRunning ? t('availability.check.action.running') : t('availability.check.action.run')}
152
- </Button>
153
-
154
- {error && (
155
- <div className="rounded-md border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive" data-testid="availability-check-error">
156
- {error}
157
- </div>
158
- )}
159
-
160
- {availability && (
161
- <div className="space-y-4 rounded-md border p-4" data-testid="availability-check-result">
162
- <div className="flex items-center gap-3">
163
- <StatusBadge variant={STATE_VARIANTS[availability.state]}>{t(`availability.states.${availability.state}`)}</StatusBadge>
164
- {!availability.isAuthoritative && (
165
- <span className="text-xs text-muted-foreground">{t('availability.check.advisoryNote')}</span>
166
- )}
167
- </div>
168
- <dl className="grid grid-cols-1 gap-2 sm:grid-cols-3">
169
- <div>
170
- <dt className="text-xs text-muted-foreground">{t('availability.check.result.availableQuantity')}</dt>
171
- <dd className="text-sm">{availability.availableQuantity ?? t('availability.policies.form.preview.notSet')}</dd>
172
- </div>
173
- <div>
174
- <dt className="text-xs text-muted-foreground">{t('availability.check.result.canFulfil')}</dt>
175
- <dd className="text-sm">{availability.canFulfil ? t('common.yes') : t('common.no')}</dd>
176
- </div>
177
- <div>
178
- <dt className="text-xs text-muted-foreground">{t('availability.check.result.policySourceId')}</dt>
179
- <dd className="text-sm">{availability.policySourceId ?? t('availability.policies.form.preview.moduleDefault')}</dd>
180
- </div>
181
- {availability.leadTimeDays != null && (
182
- <div>
183
- <dt className="text-xs text-muted-foreground">{t('availability.policies.form.field.backorderLeadTimeDays')}</dt>
184
- <dd className="text-sm">{availability.leadTimeDays}</dd>
185
- </div>
186
- )}
187
- {availability.releaseAt && (
188
- <div>
189
- <dt className="text-xs text-muted-foreground">{t('availability.policies.form.field.preorderReleaseAt')}</dt>
190
- <dd className="text-sm">{availability.releaseAt}</dd>
191
- </div>
192
- )}
193
- </dl>
194
- {availability.state === 'not_tracked' && (
195
- <p className="text-xs text-muted-foreground">{t('availability.check.notTrackedExplanation')}</p>
196
- )}
197
- </div>
198
- )}
199
-
200
- {policyTrace && (
201
- <div className="rounded-md border bg-muted p-4" data-testid="availability-check-policy-trace">
202
- <div className="mb-2 text-sm font-medium">{t('availability.policies.form.preview.title')}</div>
203
- <dl className="grid grid-cols-1 gap-2 sm:grid-cols-2">
204
- {TRACE_FIELDS.map((key) => {
205
- const field = policyTrace[key]
206
- const source = field?.policySourceId ?? t('availability.policies.form.preview.moduleDefault')
207
- return (
208
- <div key={key}>
209
- <dt className="text-xs text-muted-foreground">{t(`availability.policies.form.field.${key}`)}</dt>
210
- <dd className="text-xs">
211
- {formatTraceValue(field?.value, t)} <span className="text-muted-foreground">({source})</span>
212
- </dd>
213
- </div>
214
- )
215
- })}
216
- </dl>
217
- </div>
218
- )}
219
- </div>
220
- </PageBody>
221
- </Page>
222
- )
223
- }
@@ -1,106 +0,0 @@
1
- "use client"
2
- import * as React from 'react'
3
- import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
4
- import { useT } from '@open-mercato/shared/lib/i18n/context'
5
-
6
- type FieldTrace = { value: unknown; policySourceId: string | null }
7
- type PolicyTrace = Record<string, FieldTrace>
8
-
9
- type ResponsePayload = { policyTrace?: PolicyTrace }
10
-
11
- const TRACE_FIELDS: Array<{ key: string; labelKey: string }> = [
12
- { key: 'isStockManaged', labelKey: 'availability.policies.form.field.isStockManaged' },
13
- { key: 'allowBackorder', labelKey: 'availability.policies.form.field.allowBackorder' },
14
- { key: 'backorderLeadTimeDays', labelKey: 'availability.policies.form.field.backorderLeadTimeDays' },
15
- { key: 'preorderReleaseAt', labelKey: 'availability.policies.form.field.preorderReleaseAt' },
16
- { key: 'lowStockThreshold', labelKey: 'availability.policies.form.field.lowStockThreshold' },
17
- { key: 'minOrderQuantity', labelKey: 'availability.policies.form.field.minOrderQuantity' },
18
- { key: 'maxOrderQuantity', labelKey: 'availability.policies.form.field.maxOrderQuantity' },
19
- { key: 'quantityIncrement', labelKey: 'availability.policies.form.field.quantityIncrement' },
20
- { key: 'hideWhenOutOfStock', labelKey: 'availability.policies.form.field.hideWhenOutOfStock' },
21
- { key: 'isActive', labelKey: 'availability.policies.form.field.isActive' },
22
- ]
23
-
24
- function formatValue(value: unknown, t: (key: string) => string): string {
25
- if (value === null || value === undefined) return t('availability.policies.form.preview.notSet')
26
- if (typeof value === 'boolean') return value ? t('common.yes') : t('common.no')
27
- return String(value)
28
- }
29
-
30
- /**
31
- * Live "resolution-chain preview" — US-A2. Shows the currently PERSISTED
32
- * resolution for the (product, variant, store) target the admin is editing,
33
- * refetched (debounced) whenever the target changes. Does not simulate the
34
- * in-progress, unsaved field edits — see PLAN.md § Key design decisions.
35
- */
36
- export function ResolutionPreviewPanel({
37
- productId,
38
- variantId,
39
- storeId,
40
- }: {
41
- productId: string
42
- variantId: string | null
43
- storeId: string | null
44
- }) {
45
- const t = useT()
46
- const [trace, setTrace] = React.useState<PolicyTrace | null>(null)
47
- const [isLoading, setIsLoading] = React.useState(false)
48
-
49
- React.useEffect(() => {
50
- if (!productId) {
51
- setTrace(null)
52
- return
53
- }
54
- let cancelled = false
55
- const handle = setTimeout(() => {
56
- setIsLoading(true)
57
- const params = new URLSearchParams({ productId })
58
- if (variantId) params.set('variantId', variantId)
59
- if (storeId) params.set('storeId', storeId)
60
- apiCall<ResponsePayload>(`/api/availability/policies/resolve-preview?${params.toString()}`, undefined, { fallback: null })
61
- .then((call) => {
62
- if (!cancelled && call.ok) setTrace(call.result?.policyTrace ?? null)
63
- })
64
- .finally(() => {
65
- if (!cancelled) setIsLoading(false)
66
- })
67
- }, 300)
68
- return () => {
69
- cancelled = true
70
- clearTimeout(handle)
71
- }
72
- }, [productId, variantId, storeId])
73
-
74
- if (!productId) {
75
- return (
76
- <div className="rounded-md border bg-muted p-4 text-sm text-muted-foreground" data-testid="availability-resolution-preview">
77
- {t('availability.policies.form.preview.enterProduct')}
78
- </div>
79
- )
80
- }
81
-
82
- return (
83
- <div className="rounded-md border bg-muted p-4 text-sm" data-testid="availability-resolution-preview">
84
- <div className="mb-2 font-medium">{t('availability.policies.form.preview.title')}</div>
85
- {isLoading && !trace ? (
86
- <div className="text-muted-foreground">{t('common.loading')}</div>
87
- ) : (
88
- <dl className="grid grid-cols-1 gap-2 sm:grid-cols-2">
89
- {TRACE_FIELDS.map(({ key, labelKey }) => {
90
- const field = trace?.[key]
91
- const source = field?.policySourceId ?? t('availability.policies.form.preview.moduleDefault')
92
- return (
93
- <div key={key}>
94
- <dt className="text-xs text-muted-foreground">{t(labelKey)}</dt>
95
- <dd className="text-xs">
96
- {formatValue(field?.value, t)}{' '}
97
- <span className="text-muted-foreground">({source})</span>
98
- </dd>
99
- </div>
100
- )
101
- })}
102
- </dl>
103
- )}
104
- </div>
105
- )
106
- }
@@ -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
- }