@open-mercato/core 0.6.4-develop.4236.1.9fa6806b34 → 0.6.4-develop.4239.1.4a264a5828

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 (39) hide show
  1. package/dist/modules/business_rules/backend/logs/[id]/page.js +24 -5
  2. package/dist/modules/business_rules/backend/logs/[id]/page.js.map +2 -2
  3. package/dist/modules/catalog/api/offers/route.js +15 -5
  4. package/dist/modules/catalog/api/offers/route.js.map +2 -2
  5. package/dist/modules/currencies/backend/currencies/[id]/page.js +19 -2
  6. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  7. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +27 -7
  8. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  9. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +27 -7
  10. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
  11. package/dist/modules/customers/backend/customers/people/[id]/page.js +29 -8
  12. package/dist/modules/customers/backend/customers/people/[id]/page.js.map +2 -2
  13. package/dist/modules/progress/acl.js +8 -4
  14. package/dist/modules/progress/acl.js.map +2 -2
  15. package/dist/modules/workflows/backend/events/[id]/page.js +24 -6
  16. package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
  17. package/dist/modules/workflows/backend/instances/[id]/page.js +27 -5
  18. package/dist/modules/workflows/backend/instances/[id]/page.js.map +2 -2
  19. package/dist/modules/workflows/backend/tasks/[id]/page.js +25 -6
  20. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  21. package/package.json +7 -7
  22. package/src/modules/business_rules/backend/logs/[id]/page.tsx +32 -7
  23. package/src/modules/catalog/api/offers/route.ts +20 -5
  24. package/src/modules/currencies/backend/currencies/[id]/page.tsx +21 -2
  25. package/src/modules/currencies/i18n/de.json +1 -0
  26. package/src/modules/currencies/i18n/en.json +1 -0
  27. package/src/modules/currencies/i18n/es.json +1 -0
  28. package/src/modules/currencies/i18n/pl.json +1 -0
  29. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +34 -11
  30. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +34 -11
  31. package/src/modules/customers/backend/customers/people/[id]/page.tsx +35 -11
  32. package/src/modules/progress/acl.ts +4 -0
  33. package/src/modules/workflows/backend/events/[id]/page.tsx +32 -10
  34. package/src/modules/workflows/backend/instances/[id]/page.tsx +33 -9
  35. package/src/modules/workflows/backend/tasks/[id]/page.tsx +33 -10
  36. package/src/modules/workflows/i18n/de.json +1 -0
  37. package/src/modules/workflows/i18n/en.json +1 -0
  38. package/src/modules/workflows/i18n/es.json +1 -0
  39. package/src/modules/workflows/i18n/pl.json +1 -0
@@ -94,11 +94,25 @@ export async function decorateOffersWithDetails(
94
94
  .filter((value): value is string => !!value)
95
95
  if (!offerIds.length && !productIds.length) return
96
96
  const em = ctx.container.resolve('em') as EntityManager
97
+ const scopeTenantId = ctx.auth?.tenantId ?? null
98
+ if (!scopeTenantId) {
99
+ throw new CrudHttpError(403, '[internal] Missing tenant scope for offer decoration')
100
+ }
101
+ const scopeOrgIds =
102
+ Array.isArray(ctx.organizationIds) && ctx.organizationIds.length
103
+ ? Array.from(new Set(ctx.organizationIds))
104
+ : (ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null)
105
+ ? [(ctx.selectedOrganizationId ?? ctx.auth?.orgId) as string]
106
+ : []
107
+ const scopeWhere: Record<string, unknown> = { tenantId: scopeTenantId }
108
+ if (scopeOrgIds.length === 1) scopeWhere.organizationId = scopeOrgIds[0]
109
+ else if (scopeOrgIds.length > 1) scopeWhere.organizationId = { $in: scopeOrgIds }
110
+ const scope = { tenantId: scopeTenantId, organizationId: scopeOrgIds.length === 1 ? scopeOrgIds[0] : null }
97
111
  const [products, prices, defaultVariants] = await Promise.all([
98
112
  productIds.length
99
113
  ? em.find(
100
114
  CatalogProduct,
101
- { id: { $in: productIds } },
115
+ { id: { $in: productIds }, ...scopeWhere },
102
116
  {
103
117
  fields: ['id', 'title', 'description', 'defaultMediaId', 'defaultMediaUrl', 'sku'],
104
118
  },
@@ -108,15 +122,15 @@ export async function decorateOffersWithDetails(
108
122
  ? findWithDecryption(
109
123
  em,
110
124
  CatalogProductPrice,
111
- { offer: { $in: offerIds } },
125
+ { offer: { $in: offerIds }, ...scopeWhere },
112
126
  { populate: ['priceKind'] },
113
- { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
127
+ scope,
114
128
  )
115
129
  : [],
116
130
  productIds.length
117
131
  ? em.find(
118
132
  CatalogProductVariant,
119
- { product: { $in: productIds }, isDefault: true },
133
+ { product: { $in: productIds }, isDefault: true, ...scopeWhere },
120
134
  { fields: ['id', 'product'] },
121
135
  )
122
136
  : [],
@@ -227,6 +241,7 @@ export async function decorateOffersWithDetails(
227
241
  CatalogProductPrice,
228
242
  {
229
243
  offer: null,
244
+ ...scopeWhere,
230
245
  $and: [
231
246
  { $or: fallbackTargets },
232
247
  channelFilterValues.includes(null)
@@ -240,7 +255,7 @@ export async function decorateOffersWithDetails(
240
255
  ],
241
256
  },
242
257
  { populate: ['priceKind'] },
243
- { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
258
+ scope,
244
259
  )
245
260
  : []
246
261
  fallbackEntries.forEach((entry) => {
@@ -12,6 +12,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
12
12
  import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
13
13
  import { DataLoader } from '@open-mercato/ui/primitives/DataLoader'
14
14
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
15
+ import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
15
16
 
16
17
  type CurrencyData = {
17
18
  id: string
@@ -35,6 +36,7 @@ export default function EditCurrencyPage({ params }: { params?: { id?: string }
35
36
  const [currency, setCurrency] = React.useState<CurrencyData | null>(null)
36
37
  const [loading, setLoading] = React.useState(true)
37
38
  const [error, setError] = React.useState<string | null>(null)
39
+ const [isNotFound, setIsNotFound] = React.useState(false)
38
40
 
39
41
  React.useEffect(() => {
40
42
  async function loadCurrency() {
@@ -42,8 +44,10 @@ export default function EditCurrencyPage({ params }: { params?: { id?: string }
42
44
  const response = await apiCall<{ items: CurrencyData[] }>(`/api/currencies/currencies?id=${params?.id}`)
43
45
  if (response.ok && response.result && response.result.items.length > 0) {
44
46
  setCurrency(response.result.items[0])
47
+ } else if (!response.ok) {
48
+ setError(t('currencies.form.errors.load'))
45
49
  } else {
46
- setError(t('currencies.form.errors.notFound'))
50
+ setIsNotFound(true)
47
51
  }
48
52
  } catch (err) {
49
53
  setError(t('currencies.form.errors.load'))
@@ -163,11 +167,26 @@ export default function EditCurrencyPage({ params }: { params?: { id?: string }
163
167
  )
164
168
  }
165
169
 
170
+ if (isNotFound) {
171
+ return (
172
+ <Page>
173
+ <PageBody>
174
+ <RecordNotFoundState
175
+ label={t('currencies.form.errors.notFound', 'Currency not found.')}
176
+ backHref="/backend/currencies"
177
+ backLabel={t('currencies.form.actions.backToList', 'Back to currencies')}
178
+ />
179
+ </PageBody>
180
+ {ConfirmDialogElement}
181
+ </Page>
182
+ )
183
+ }
184
+
166
185
  if (error || !currency) {
167
186
  return (
168
187
  <Page>
169
188
  <PageBody>
170
- <div className="text-destructive">{error || t('currencies.form.errors.notFound')}</div>
189
+ <ErrorMessage label={error ?? t('currencies.form.errors.notFound', 'Currency not found.')} />
171
190
  </PageBody>
172
191
  {ConfirmDialogElement}
173
192
  </Page>
@@ -41,6 +41,7 @@
41
41
  "currencies.flash.updated": "Währung erfolgreich aktualisiert",
42
42
  "currencies.form.action.create": "Währung erstellen",
43
43
  "currencies.form.action.save": "Änderungen speichern",
44
+ "currencies.form.actions.backToList": "Zurück zu Währungen",
44
45
  "currencies.form.errors.codeFormat": "Währungscode muss genau 3 Großbuchstaben sein (z.B. USD)",
45
46
  "currencies.form.errors.delete": "Währung konnte nicht gelöscht werden",
46
47
  "currencies.form.errors.load": "Währung konnte nicht geladen werden",
@@ -41,6 +41,7 @@
41
41
  "currencies.flash.updated": "Currency updated successfully",
42
42
  "currencies.form.action.create": "Create Currency",
43
43
  "currencies.form.action.save": "Save Changes",
44
+ "currencies.form.actions.backToList": "Back to currencies",
44
45
  "currencies.form.errors.codeFormat": "Currency code must be exactly 3 uppercase letters (e.g., USD)",
45
46
  "currencies.form.errors.delete": "Failed to delete currency",
46
47
  "currencies.form.errors.load": "Failed to load currency",
@@ -41,6 +41,7 @@
41
41
  "currencies.flash.updated": "Moneda actualizada correctamente",
42
42
  "currencies.form.action.create": "Crear Moneda",
43
43
  "currencies.form.action.save": "Guardar Cambios",
44
+ "currencies.form.actions.backToList": "Volver a divisas",
44
45
  "currencies.form.errors.codeFormat": "El código de moneda debe ser exactamente 3 letras mayúsculas (ej. USD)",
45
46
  "currencies.form.errors.delete": "Error al eliminar moneda",
46
47
  "currencies.form.errors.load": "Error al cargar moneda",
@@ -41,6 +41,7 @@
41
41
  "currencies.flash.updated": "Waluta zaktualizowana pomyślnie",
42
42
  "currencies.form.action.create": "Utwórz Walutę",
43
43
  "currencies.form.action.save": "Zapisz Zmiany",
44
+ "currencies.form.actions.backToList": "Wróć do walut",
44
45
  "currencies.form.errors.codeFormat": "Kod waluty musi składać się z dokładnie 3 wielkich liter (np. USD)",
45
46
  "currencies.form.errors.delete": "Nie udało się usunąć waluty",
46
47
  "currencies.form.errors.load": "Nie udało się załadować waluty",
@@ -11,6 +11,7 @@ import { Spinner } from '@open-mercato/ui/primitives/spinner'
11
11
  import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
12
12
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
13
13
  import { useT } from '@open-mercato/shared/lib/i18n/context'
14
+ import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
14
15
 
15
16
  type RoleDetail = {
16
17
  id: string
@@ -146,10 +147,11 @@ export default function CustomerRoleDetailPage({ params }: { params?: { id?: str
146
147
  const [data, setData] = React.useState<RoleDetail | null>(null)
147
148
  const [isLoading, setIsLoading] = React.useState(true)
148
149
  const [error, setError] = React.useState<string | null>(null)
150
+ const [isNotFound, setIsNotFound] = React.useState(false)
149
151
 
150
152
  React.useEffect(() => {
151
153
  if (!id) {
152
- setError(t('customer_accounts.admin.roleDetail.error.notFound', 'Role not found'))
154
+ setIsNotFound(true)
153
155
  setIsLoading(false)
154
156
  return
155
157
  }
@@ -157,6 +159,7 @@ export default function CustomerRoleDetailPage({ params }: { params?: { id?: str
157
159
  async function load() {
158
160
  setIsLoading(true)
159
161
  setError(null)
162
+ setIsNotFound(false)
160
163
  try {
161
164
  const payload = await readApiResultOrThrow<RoleDetail>(
162
165
  `/api/customer_accounts/admin/roles/${encodeURIComponent(id!)}`,
@@ -167,8 +170,12 @@ export default function CustomerRoleDetailPage({ params }: { params?: { id?: str
167
170
  setData(payload)
168
171
  } catch (err) {
169
172
  if (cancelled) return
170
- const message = err instanceof Error ? err.message : t('customer_accounts.admin.roleDetail.error.load', 'Failed to load role')
171
- setError(message)
173
+ if ((err as { status?: number }).status === 404) {
174
+ setIsNotFound(true)
175
+ } else {
176
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.roleDetail.error.load', 'Failed to load role')
177
+ setError(message)
178
+ }
172
179
  } finally {
173
180
  if (!cancelled) setIsLoading(false)
174
181
  }
@@ -300,18 +307,34 @@ export default function CustomerRoleDetailPage({ params }: { params?: { id?: str
300
307
  )
301
308
  }
302
309
 
310
+ if (isNotFound) {
311
+ return (
312
+ <Page>
313
+ <PageBody>
314
+ <RecordNotFoundState
315
+ label={t('customer_accounts.admin.roleDetail.error.notFound', 'Role not found')}
316
+ backHref="/backend/customer_accounts/roles"
317
+ backLabel={t('customer_accounts.admin.roleDetail.actions.backToList', 'Back to roles')}
318
+ />
319
+ </PageBody>
320
+ </Page>
321
+ )
322
+ }
323
+
303
324
  if (error || !data) {
304
325
  return (
305
326
  <Page>
306
327
  <PageBody>
307
- <div className="flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground">
308
- <p>{error || t('customer_accounts.admin.roleDetail.error.notFound', 'Role not found')}</p>
309
- <Button asChild variant="outline">
310
- <Link href="/backend/customer_accounts/roles">
311
- {t('customer_accounts.admin.roleDetail.actions.backToList', 'Back to roles')}
312
- </Link>
313
- </Button>
314
- </div>
328
+ <ErrorMessage
329
+ label={error ?? t('customer_accounts.admin.roleDetail.error.notFound', 'Role not found')}
330
+ action={
331
+ <Button asChild variant="outline" size="sm">
332
+ <Link href="/backend/customer_accounts/roles">
333
+ {t('customer_accounts.admin.roleDetail.actions.backToList', 'Back to roles')}
334
+ </Link>
335
+ </Button>
336
+ }
337
+ />
315
338
  </PageBody>
316
339
  </Page>
317
340
  )
@@ -16,6 +16,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
16
16
  import { useT } from '@open-mercato/shared/lib/i18n/context'
17
17
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
18
18
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
19
+ import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
19
20
 
20
21
  type UserDetail = {
21
22
  id: string
@@ -147,6 +148,7 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
147
148
  const [data, setData] = React.useState<UserDetail | null>(null)
148
149
  const [isLoading, setIsLoading] = React.useState(true)
149
150
  const [error, setError] = React.useState<string | null>(null)
151
+ const [isNotFound, setIsNotFound] = React.useState(false)
150
152
  const [isSaving, setIsSaving] = React.useState(false)
151
153
  const [editActive, setEditActive] = React.useState<boolean | null>(null)
152
154
  const [editDisplayName, setEditDisplayName] = React.useState('')
@@ -186,7 +188,7 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
186
188
 
187
189
  React.useEffect(() => {
188
190
  if (!id) {
189
- setError(t('customer_accounts.admin.detail.error.notFound', 'User not found'))
191
+ setIsNotFound(true)
190
192
  setIsLoading(false)
191
193
  return
192
194
  }
@@ -194,6 +196,7 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
194
196
  async function load() {
195
197
  setIsLoading(true)
196
198
  setError(null)
199
+ setIsNotFound(false)
197
200
  try {
198
201
  const payload = await readApiResultOrThrow<UserDetail>(
199
202
  `/api/customer_accounts/admin/users/${encodeURIComponent(id!)}`,
@@ -209,8 +212,12 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
209
212
  setEditCustomerEntityId(payload.customerEntityId)
210
213
  } catch (err) {
211
214
  if (cancelled) return
212
- const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.error.load', 'Failed to load user')
213
- setError(message)
215
+ if ((err as { status?: number }).status === 404) {
216
+ setIsNotFound(true)
217
+ } else {
218
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.error.load', 'Failed to load user')
219
+ setError(message)
220
+ }
214
221
  } finally {
215
222
  if (!cancelled) setIsLoading(false)
216
223
  }
@@ -463,18 +470,34 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
463
470
  )
464
471
  }
465
472
 
473
+ if (isNotFound) {
474
+ return (
475
+ <Page>
476
+ <PageBody>
477
+ <RecordNotFoundState
478
+ label={t('customer_accounts.admin.detail.error.notFound', 'User not found')}
479
+ backHref="/backend/customer_accounts/users"
480
+ backLabel={t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
481
+ />
482
+ </PageBody>
483
+ </Page>
484
+ )
485
+ }
486
+
466
487
  if (error || !data) {
467
488
  return (
468
489
  <Page>
469
490
  <PageBody>
470
- <div className="flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground">
471
- <p>{error || t('customer_accounts.admin.detail.error.notFound', 'User not found')}</p>
472
- <Button asChild variant="outline">
473
- <Link href="/backend/customer_accounts/users">
474
- {t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
475
- </Link>
476
- </Button>
477
- </div>
491
+ <ErrorMessage
492
+ label={error ?? t('customer_accounts.admin.detail.error.notFound', 'User not found')}
493
+ action={
494
+ <Button asChild variant="outline" size="sm">
495
+ <Link href="/backend/customer_accounts/users">
496
+ {t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
497
+ </Link>
498
+ </Button>
499
+ }
500
+ />
478
501
  </PageBody>
479
502
  </Page>
480
503
  )
@@ -20,6 +20,8 @@ import {
20
20
  NotesSection,
21
21
  type CommentSummary,
22
22
  type SectionAction,
23
+ RecordNotFoundState,
24
+ ErrorMessage,
23
25
  } from '@open-mercato/ui/backend/detail'
24
26
  import {
25
27
  TagsSection,
@@ -116,6 +118,7 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
116
118
  const [data, setData] = React.useState<PersonOverview | null>(null)
117
119
  const [isLoading, setIsLoading] = React.useState(true)
118
120
  const [error, setError] = React.useState<string | null>(null)
121
+ const [isNotFound, setIsNotFound] = React.useState(false)
119
122
  const [activeTab, setActiveTab] = React.useState<SectionKey>(initialTab)
120
123
  const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
121
124
  const [isDeleting, setIsDeleting] = React.useState(false)
@@ -276,7 +279,7 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
276
279
  const initialLoadDoneRef = React.useRef(false)
277
280
  const loadData = React.useCallback(async () => {
278
281
  if (!id) {
279
- setError(t('customers.people.detail.error.notFound'))
282
+ setIsNotFound(true)
280
283
  setIsLoading(false)
281
284
  return
282
285
  }
@@ -284,6 +287,7 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
284
287
  setIsLoading(true)
285
288
  }
286
289
  setError(null)
290
+ setIsNotFound(false)
287
291
  try {
288
292
  const payload = await readApiResultOrThrow<PersonOverview>(
289
293
  `/api/customers/people/${encodeURIComponent(id)}?include=todos`,
@@ -292,8 +296,12 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
292
296
  )
293
297
  setData(payload as PersonOverview)
294
298
  } catch (err) {
295
- const message = err instanceof Error ? err.message : t('customers.people.detail.error.load')
296
- setError(message)
299
+ if ((err as { status?: number }).status === 404) {
300
+ setIsNotFound(true)
301
+ } else {
302
+ const message = err instanceof Error ? err.message : t('customers.people.detail.error.load')
303
+ setError(message)
304
+ }
297
305
  if (!initialLoadDoneRef.current) setData(null)
298
306
  } finally {
299
307
  setIsLoading(false)
@@ -469,18 +477,34 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
469
477
  )
470
478
  }
471
479
 
480
+ if (isNotFound) {
481
+ return (
482
+ <Page>
483
+ <PageBody>
484
+ <RecordNotFoundState
485
+ label={t('customers.people.detail.error.notFound', 'Person not found.')}
486
+ backHref="/backend/customers/people"
487
+ backLabel={t('customers.people.detail.actions.backToList', 'Back to people')}
488
+ />
489
+ </PageBody>
490
+ </Page>
491
+ )
492
+ }
493
+
472
494
  if (error || !data || !personId) {
473
495
  return (
474
496
  <Page>
475
497
  <PageBody>
476
- <div className="flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground">
477
- <p>{error || t('customers.people.detail.error.notFound')}</p>
478
- <Button asChild variant="outline">
479
- <Link href="/backend/customers/people">
480
- {t('customers.people.detail.actions.backToList')}
481
- </Link>
482
- </Button>
483
- </div>
498
+ <ErrorMessage
499
+ label={error ?? t('customers.people.detail.error.notFound', 'Person not found.')}
500
+ action={
501
+ <Button asChild variant="outline" size="sm">
502
+ <Link href="/backend/customers/people">
503
+ {t('customers.people.detail.actions.backToList', 'Back to people')}
504
+ </Link>
505
+ </Button>
506
+ }
507
+ />
484
508
  </PageBody>
485
509
  </Page>
486
510
  )
@@ -8,21 +8,25 @@ export const features = [
8
8
  id: 'progress.create',
9
9
  title: 'Create progress jobs',
10
10
  module: 'progress',
11
+ dependsOn: ['progress.view'],
11
12
  },
12
13
  {
13
14
  id: 'progress.update',
14
15
  title: 'Update progress jobs',
15
16
  module: 'progress',
17
+ dependsOn: ['progress.view'],
16
18
  },
17
19
  {
18
20
  id: 'progress.cancel',
19
21
  title: 'Cancel progress jobs',
20
22
  module: 'progress',
23
+ dependsOn: ['progress.view'],
21
24
  },
22
25
  {
23
26
  id: 'progress.manage',
24
27
  title: 'Manage all progress jobs',
25
28
  module: 'progress',
29
+ dependsOn: ['progress.view'],
26
30
  },
27
31
  ]
28
32
 
@@ -11,6 +11,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
11
11
  import { FormHeader } from '@open-mercato/ui/backend/forms'
12
12
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
13
13
  import { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'
14
+ import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
14
15
 
15
16
  type WorkflowEvent = {
16
17
  id: string
@@ -55,8 +56,13 @@ export default function WorkflowEventDetailPage() {
55
56
  const response = await apiFetch(`/api/workflows/events/${eventId}`)
56
57
 
57
58
  if (!response.ok) {
58
- const errorData = await response.json().catch(() => ({}))
59
- throw new Error(t('workflows.events.messages.loadFailed'))
59
+ const httpErr = new Error(
60
+ response.status === 404
61
+ ? t('workflows.events.notFound', 'Event not found.')
62
+ : t('workflows.events.messages.loadFailed')
63
+ ) as Error & { status: number }
64
+ httpErr.status = response.status
65
+ throw httpErr
60
66
  }
61
67
  const result = await response.json()
62
68
  return result as WorkflowEvent
@@ -77,18 +83,34 @@ export default function WorkflowEventDetailPage() {
77
83
  )
78
84
  }
79
85
 
86
+ const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404
87
+
88
+ if (isNotFound) {
89
+ return (
90
+ <Page>
91
+ <PageBody>
92
+ <RecordNotFoundState
93
+ label={t('workflows.events.notFound', 'Event not found.')}
94
+ backHref="/backend/events"
95
+ backLabel={t('workflows.events.backToList', 'Back to Events')}
96
+ />
97
+ </PageBody>
98
+ </Page>
99
+ )
100
+ }
101
+
80
102
  if (error || !event) {
81
103
  return (
82
104
  <Page>
83
105
  <PageBody>
84
- <div className="flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground">
85
- <p>{error ? t('workflows.events.messages.loadFailed') : t('workflows.events.notFound')}</p>
86
- <Button asChild variant="outline">
87
- <Link href="/backend/events">
88
- {t('workflows.events.backToList')}
89
- </Link>
90
- </Button>
91
- </div>
106
+ <ErrorMessage
107
+ label={(error as Error | null)?.message ?? t('workflows.events.messages.loadFailed')}
108
+ action={
109
+ <Button asChild variant="outline" size="sm">
110
+ <Link href="/backend/events">{t('workflows.events.backToList', 'Back to Events')}</Link>
111
+ </Button>
112
+ }
113
+ />
92
114
  </PageBody>
93
115
  </Page>
94
116
  )
@@ -19,6 +19,7 @@ import { MobileInstanceOverview } from '../../../components/mobile/MobileInstanc
19
19
  import { useIsMobile } from '@open-mercato/ui/hooks/useIsMobile'
20
20
  import { definitionToGraph } from '../../../lib/graph-utils'
21
21
  import { Node } from '@xyflow/react'
22
+ import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
22
23
 
23
24
  export default function WorkflowInstanceDetailPage({ params }: { params?: { id?: string } }) {
24
25
  const id = params?.id
@@ -32,7 +33,13 @@ export default function WorkflowInstanceDetailPage({ params }: { params?: { id?:
32
33
  queryFn: async () => {
33
34
  const response = await apiFetch(`/api/workflows/instances/${id}`)
34
35
  if (!response.ok) {
35
- throw new Error(t('workflows.instances.notFound') || 'Instance not found')
36
+ const httpErr = new Error(
37
+ response.status === 404
38
+ ? t('workflows.instances.detail.notFound', 'Workflow instance not found.')
39
+ : t('workflows.instances.loadFailed', 'Failed to load workflow instance.')
40
+ ) as Error & { status: number }
41
+ httpErr.status = response.status
42
+ throw httpErr
36
43
  }
37
44
  const data = await response.json()
38
45
  return data.data as WorkflowInstance
@@ -368,18 +375,35 @@ export default function WorkflowInstanceDetailPage({ params }: { params?: { id?:
368
375
  )
369
376
  }
370
377
 
378
+ const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404
379
+
380
+ if (isNotFound) {
381
+ return (
382
+ <Page>
383
+ <PageBody>
384
+ <RecordNotFoundState
385
+ label={t('workflows.instances.detail.notFound', 'Workflow instance not found.')}
386
+ backHref="/backend/instances"
387
+ backLabel={t('workflows.instances.actions.backToList', 'Back to instances')}
388
+ />
389
+ </PageBody>
390
+ {ConfirmDialogElement}
391
+ </Page>
392
+ )
393
+ }
394
+
371
395
  if (error || !instance) {
372
396
  return (
373
397
  <Page>
374
398
  <PageBody>
375
- <div className="flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground">
376
- <p>{error ? t('workflows.instances.loadFailed') : t('workflows.instances.detail.notFound') || 'Workflow instance not found.'}</p>
377
- <Button asChild variant="outline">
378
- <Link href="/backend/instances">
379
- {t('workflows.instances.actions.backToList') || 'Back to instances'}
380
- </Link>
381
- </Button>
382
- </div>
399
+ <ErrorMessage
400
+ label={(error as Error | null)?.message ?? t('workflows.instances.loadFailed', 'Failed to load workflow instance.')}
401
+ action={
402
+ <Button asChild variant="outline" size="sm">
403
+ <Link href="/backend/instances">{t('workflows.instances.actions.backToList', 'Back to instances')}</Link>
404
+ </Button>
405
+ }
406
+ />
383
407
  </PageBody>
384
408
  {ConfirmDialogElement}
385
409
  </Page>
@@ -25,6 +25,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
25
25
  import { MobileTaskForm } from '../../../components/mobile/MobileTaskForm'
26
26
  import { useIsMobile } from '@open-mercato/ui/hooks/useIsMobile'
27
27
  import type { UserTaskResponse, UserTaskStatus } from '../../../data/types'
28
+ import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
28
29
 
29
30
  export default function UserTaskDetailPage({ params }: { params: { id: string } }) {
30
31
  const router = useRouter()
@@ -44,12 +45,16 @@ export default function UserTaskDetailPage({ params }: { params: { id: string }
44
45
  const result = await apiCall<{ data: UserTaskResponse }>(
45
46
  `/api/workflows/tasks/${params.id}`
46
47
  )
47
-
48
48
  if (!result.ok) {
49
- throw new Error('Failed to fetch task')
49
+ const httpErr = new Error(
50
+ result.status === 404
51
+ ? t('workflows.tasks.detail.notFound', 'Task not found')
52
+ : t('workflows.tasks.detail.loadFailed', 'Failed to load task')
53
+ ) as Error & { status: number }
54
+ httpErr.status = result.status
55
+ throw httpErr
50
56
  }
51
-
52
- return result.result?.data || null
57
+ return result.result?.data ?? null
53
58
  },
54
59
  })
55
60
 
@@ -347,16 +352,34 @@ export default function UserTaskDetailPage({ params }: { params: { id: string }
347
352
  )
348
353
  }
349
354
 
355
+ const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404
356
+
357
+ if (isNotFound) {
358
+ return (
359
+ <Page>
360
+ <PageBody>
361
+ <RecordNotFoundState
362
+ label={t('workflows.tasks.detail.notFound', 'Task not found')}
363
+ backHref="/backend/tasks"
364
+ backLabel={t('workflows.tasks.detail.backToList', 'Back to Tasks')}
365
+ />
366
+ </PageBody>
367
+ </Page>
368
+ )
369
+ }
370
+
350
371
  if (error || !task) {
351
372
  return (
352
373
  <Page>
353
374
  <PageBody>
354
- <div className="p-8 text-center">
355
- <p className="text-status-error-text">{t('workflows.tasks.detail.notFound')}</p>
356
- <Button onClick={() => router.push('/backend/tasks')} className="mt-4">
357
- {t('workflows.tasks.detail.backToList')}
358
- </Button>
359
- </div>
375
+ <ErrorMessage
376
+ label={(error as Error | null)?.message ?? t('workflows.tasks.detail.loadFailed', 'Failed to load task')}
377
+ action={
378
+ <Button asChild variant="outline" size="sm">
379
+ <Link href="/backend/tasks">{t('workflows.tasks.detail.backToList', 'Back to Tasks')}</Link>
380
+ </Button>
381
+ }
382
+ />
360
383
  </PageBody>
361
384
  </Page>
362
385
  )