@open-mercato/core 0.6.6-develop.6317.1.b3be10ab84 → 0.6.6-develop.6330.1.a261878aa8

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 (67) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +7 -12
  3. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  4. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +5 -2
  5. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  6. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -1
  7. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  8. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +2 -1
  9. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js.map +2 -2
  10. package/dist/modules/customers/components/detail/EntityTagsDialog.js +76 -67
  11. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +3 -3
  12. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +4 -3
  13. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  14. package/dist/modules/data_sync/api/runs.js +1 -0
  15. package/dist/modules/data_sync/api/runs.js.map +2 -2
  16. package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
  17. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  18. package/dist/modules/data_sync/data/validators.js +1 -0
  19. package/dist/modules/data_sync/data/validators.js.map +2 -2
  20. package/dist/modules/data_sync/lib/sync-run-service.js +20 -0
  21. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  22. package/dist/modules/entities/api/records.js +11 -1
  23. package/dist/modules/entities/api/records.js.map +2 -2
  24. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +17 -10
  25. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  26. package/dist/modules/messages/api/unread-count/route.js +32 -0
  27. package/dist/modules/messages/api/unread-count/route.js.map +2 -2
  28. package/dist/modules/query_index/components/QueryIndexesTable.js +13 -5
  29. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  30. package/dist/modules/shipping_carriers/i18n/en.js +11 -1
  31. package/dist/modules/shipping_carriers/i18n/en.js.map +2 -2
  32. package/dist/modules/shipping_carriers/i18n/pl.js +11 -1
  33. package/dist/modules/shipping_carriers/i18n/pl.js.map +2 -2
  34. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js +25 -0
  35. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js.map +7 -0
  36. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js +3 -17
  37. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js.map +2 -2
  38. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +19 -14
  39. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  40. package/package.json +7 -7
  41. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +7 -10
  42. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -7
  43. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +3 -2
  44. package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +3 -2
  45. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +89 -71
  46. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +6 -3
  47. package/src/modules/data_sync/api/runs.ts +1 -0
  48. package/src/modules/data_sync/backend/data-sync/page.tsx +3 -1
  49. package/src/modules/data_sync/data/validators.ts +1 -0
  50. package/src/modules/data_sync/i18n/de.json +1 -0
  51. package/src/modules/data_sync/i18n/en.json +1 -0
  52. package/src/modules/data_sync/i18n/es.json +1 -0
  53. package/src/modules/data_sync/i18n/pl.json +1 -0
  54. package/src/modules/data_sync/lib/sync-run-service.ts +23 -0
  55. package/src/modules/entities/api/records.ts +17 -1
  56. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +25 -12
  57. package/src/modules/messages/api/unread-count/route.ts +40 -0
  58. package/src/modules/query_index/components/QueryIndexesTable.tsx +17 -15
  59. package/src/modules/shipping_carriers/i18n/de.json +11 -1
  60. package/src/modules/shipping_carriers/i18n/en.json +11 -1
  61. package/src/modules/shipping_carriers/i18n/en.ts +10 -0
  62. package/src/modules/shipping_carriers/i18n/es.json +11 -1
  63. package/src/modules/shipping_carriers/i18n/pl.json +11 -1
  64. package/src/modules/shipping_carriers/i18n/pl.ts +10 -0
  65. package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.tsx +35 -0
  66. package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx +3 -22
  67. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +30 -13
@@ -107,6 +107,8 @@ const listRecordsQuerySchema = z
107
107
  pageSize: z.coerce.number().int().min(1).max(100).optional(),
108
108
  sortField: z.string().optional(),
109
109
  sortDir: z.enum(['asc', 'desc']).optional(),
110
+ search: z.string().optional(),
111
+ searchFields: z.string().optional(),
110
112
  withDeleted: z.coerce.boolean().optional(),
111
113
  format: z.enum(['csv', 'json', 'xml', 'markdown']).optional(),
112
114
  exportScope: z.enum(['full']).optional(),
@@ -145,10 +147,15 @@ export async function GET(req: Request) {
145
147
  const sortField = url.searchParams.get('sortField') || 'id'
146
148
  const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'
147
149
  const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)
150
+ const searchTerm = (url.searchParams.get('search') || '').trim()
151
+ const searchFields = (url.searchParams.get('searchFields') || '')
152
+ .split(',')
153
+ .map((field) => field.trim())
154
+ .filter(Boolean)
148
155
 
149
156
  const qpEntries: Array<[string, string]> = []
150
157
  for (const [key, val] of url.searchParams.entries()) {
151
- if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full'].includes(key)) continue
158
+ if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full','search','searchFields'].includes(key)) continue
152
159
  qpEntries.push([key, val])
153
160
  }
154
161
 
@@ -246,6 +253,15 @@ export async function GET(req: Request) {
246
253
  // even before the first record exists.
247
254
  if (isCustomEntity) qopts.forceCustomEntityStorage = true
248
255
  for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)
256
+ // Server-side full-result search: match the term against the requested fields
257
+ // (defaults to `id`) before pagination so totals/exports stay consistent with
258
+ // the active search instead of filtering only the current client page (#3229).
259
+ if (searchTerm) {
260
+ const fields = searchFields.length ? searchFields : ['id']
261
+ const pattern = `%${searchTerm}%`
262
+ const orClauses = fields.map((field) => ({ [field]: { $ilike: pattern } }))
263
+ ;(filtersObj as any).$or = orClauses
264
+ }
249
265
  const res = await qe.query(entityId as any, qopts)
250
266
  const rawItems = res.items || []
251
267
  const viewPageItems = rawItems.map(mapRow)
@@ -97,7 +97,16 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
97
97
  keyExtras: [scopeVersion],
98
98
  })
99
99
 
100
- // Fetch records whenever paging/sorting/filters change (do NOT refetch on cfDefs/search changes)
100
+ // Fields searched server-side: every visible column plus the base `id`.
101
+ const searchableFields = React.useMemo(() => {
102
+ const fields = (columns || [])
103
+ .map((col) => (col as any).accessorKey)
104
+ .filter((key): key is string => typeof key === 'string' && key.length > 0)
105
+ return Array.from(new Set(['id', ...fields]))
106
+ }, [columns])
107
+
108
+ // Fetch records whenever paging/sorting/filters/search change. Search is applied
109
+ // server-side (before pagination) so totals and exports stay consistent (#3229).
101
110
  React.useEffect(() => {
102
111
  let cancelled = false
103
112
  const run = async () => {
@@ -107,6 +116,11 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
107
116
  params.set('entityId', entityId)
108
117
  params.set('page', String(page))
109
118
  params.set('pageSize', String(pageSize))
119
+ const trimmedSearch = search.trim()
120
+ if (trimmedSearch) {
121
+ params.set('search', trimmedSearch)
122
+ if (searchableFields.length) params.set('searchFields', searchableFields.join(','))
123
+ }
110
124
  const s = sorting?.[0]
111
125
  if (s?.id) {
112
126
  params.set('sortField', String(s.id))
@@ -154,7 +168,7 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
154
168
  }
155
169
  if (entityId) run()
156
170
  return () => { cancelled = true }
157
- }, [entityId, page, pageSize, sorting, filterValues, scopeVersion])
171
+ }, [entityId, page, pageSize, sorting, filterValues, scopeVersion, search, searchableFields])
158
172
 
159
173
  // Build columns from custom field definitions only (no data round-trip)
160
174
  React.useEffect(() => {
@@ -175,15 +189,9 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
175
189
  setColumns(cols)
176
190
  }, [cfDefs])
177
191
 
178
- // Client-side quick search filtering without triggering server refetch
179
- const data = React.useMemo(() => {
180
- if (!search.trim()) return rawData
181
- const q = search.trim().toLowerCase()
182
- return (rawData || []).filter((row: any) => {
183
- const values = Object.values(row || {})
184
- return values.some((v) => normalizeCell(v).toLowerCase().includes(q))
185
- })
186
- }, [rawData, search])
192
+ // Search is server-side (see fetch effect); render the fetched page as-is so
193
+ // pagination totals and exports stay consistent with the active search (#3229).
194
+ const data = rawData
187
195
 
188
196
  const viewExportColumns = React.useMemo(() => {
189
197
  return (columns || [])
@@ -213,8 +221,13 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
213
221
  qp.set('sortField', String(sort.id))
214
222
  qp.set('sortDir', sort.desc ? 'desc' : 'asc')
215
223
  }
224
+ const trimmedSearch = search.trim()
225
+ if (trimmedSearch) {
226
+ qp.set('search', trimmedSearch)
227
+ if (searchableFields.length) qp.set('searchFields', searchableFields.join(','))
228
+ }
216
229
  return `/api/entities/records?${qp.toString()}`
217
- }, [entityId, sorting])
230
+ }, [entityId, sorting, search, searchableFields])
218
231
 
219
232
  const exportConfig = React.useMemo(() => {
220
233
  const safeEntityId = entityId.replace(/[^a-z0-9_-]/gi, '_') || 'records'
@@ -1,5 +1,11 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { type Kysely, sql } from 'kysely'
3
+ import { runWithCacheTenant } from '@open-mercato/cache'
4
+ import {
5
+ buildCollectionTags,
6
+ isCrudCacheEnabled,
7
+ resolveCrudCache,
8
+ } from '@open-mercato/shared/lib/crud/cache'
3
9
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
4
10
  import { resolveMessageContext } from '../../lib/routeHelpers'
5
11
  import { unreadCountResponseSchema } from '../openapi'
@@ -8,6 +14,16 @@ export const metadata = {
8
14
  GET: { requireAuth: true },
9
15
  }
10
16
 
17
+ const UNREAD_COUNT_RESOURCE = 'messages.message'
18
+ const UNREAD_COUNT_TTL_MS = 10_000
19
+
20
+ function buildUnreadCountCacheKey(params: {
21
+ userId: string
22
+ orgId: string | null
23
+ }): string {
24
+ return `messages:unread-count:u=${params.userId}:org=${params.orgId ?? 'null'}`
25
+ }
26
+
11
27
  function getDb(em: EntityManager): Kysely<any> {
12
28
  return em.getKysely<any>()
13
29
  }
@@ -17,6 +33,19 @@ export async function GET(req: Request) {
17
33
  const em = ctx.container.resolve('em') as EntityManager
18
34
  const db = getDb(em) as any
19
35
 
36
+ const orgId = scope.organizationId ?? null
37
+ const cache = isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null
38
+ const cacheKey = cache
39
+ ? buildUnreadCountCacheKey({ userId: scope.userId, orgId })
40
+ : null
41
+
42
+ if (cache && cacheKey) {
43
+ const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))
44
+ if (typeof cached === 'number') {
45
+ return Response.json({ unreadCount: cached })
46
+ }
47
+ }
48
+
20
49
  let query = db
21
50
  .selectFrom('message_recipients as r')
22
51
  .innerJoin('messages as m', 'm.id', 'r.message_id')
@@ -38,6 +67,17 @@ export async function GET(req: Request) {
38
67
  .executeTakeFirst() as { count: string | number } | undefined
39
68
  const count = Number(row?.count ?? 0)
40
69
 
70
+ if (cache && cacheKey) {
71
+ try {
72
+ await runWithCacheTenant(scope.tenantId, () =>
73
+ cache.set(cacheKey, count, {
74
+ ttl: UNREAD_COUNT_TTL_MS,
75
+ tags: buildCollectionTags(UNREAD_COUNT_RESOURCE, scope.tenantId, [orgId]),
76
+ }),
77
+ )
78
+ } catch {}
79
+ }
80
+
41
81
  return Response.json({ unreadCount: count })
42
82
  }
43
83
 
@@ -11,6 +11,7 @@ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/u
11
11
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
12
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
13
13
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
14
+ import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
14
15
 
15
16
  const MUTATION_CONTEXT_ID = 'query_index.status.list:actions'
16
17
 
@@ -127,7 +128,7 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
127
128
  if (!record.vectorEnabled) return <span>—</span>
128
129
  const ok = record.vectorCount != null && record.baseCount != null && record.vectorCount === record.baseCount
129
130
  const display = formatCount(record.vectorCount)
130
- const className = ok ? 'text-green-600' : 'text-orange-600'
131
+ const className = ok ? 'text-status-success-text' : 'text-status-warning-text'
131
132
  return <span className={className}>{display}</span>
132
133
  },
133
134
  meta: { priority: 2 },
@@ -141,7 +142,7 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
141
142
  if (!record.fulltextEnabled) return <span>—</span>
142
143
  const ok = record.fulltextCount != null && record.baseCount != null && record.fulltextCount === record.baseCount
143
144
  const display = formatCount(record.fulltextCount)
144
- const className = ok ? 'text-green-600' : 'text-orange-600'
145
+ const className = ok ? 'text-status-success-text' : 'text-status-warning-text'
145
146
  return <span className={className}>{display}</span>
146
147
  },
147
148
  meta: { priority: 2 },
@@ -159,17 +160,14 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
159
160
  const label = jobProgress
160
161
  ? t('query_index.table.status.withProgress', { status: statusText, progress: jobProgress })
161
162
  : statusText
162
- const className = job
163
- ? job.status === 'stalled'
164
- ? 'text-red-600'
165
- : job.status === 'reindexing' || job.status === 'purging'
166
- ? 'text-orange-600'
167
- : ok
168
- ? 'text-green-600'
169
- : 'text-muted-foreground'
170
- : ok
171
- ? 'text-green-600'
172
- : 'text-muted-foreground'
163
+ let variant: StatusBadgeVariant = 'neutral'
164
+ if (job) {
165
+ if (job.status === 'stalled') variant = 'error'
166
+ else if (job.status === 'reindexing' || job.status === 'purging') variant = 'warning'
167
+ else variant = ok ? 'success' : 'neutral'
168
+ } else {
169
+ variant = ok ? 'success' : 'neutral'
170
+ }
173
171
 
174
172
  const lines: string[] = []
175
173
 
@@ -203,8 +201,12 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
203
201
  }
204
202
 
205
203
  return (
206
- <div className="space-y-1">
207
- <span className={className}>{label}</span>
204
+ <div className="space-y-1.5">
205
+ <div>
206
+ <StatusBadge variant={variant} dot>
207
+ {label}
208
+ </StatusBadge>
209
+ </div>
208
210
  {lines.length > 0 && (
209
211
  <div className="text-xs text-muted-foreground">
210
212
  {lines.map((line, idx) => (
@@ -70,5 +70,15 @@
70
70
  "shipping_carriers.create.summary.to": "Nach",
71
71
  "shipping_carriers.create.title": "Sendung beim Transporteur erstellen",
72
72
  "shipping_carriers.feature.manage": "Versanddienstleister-Vorgaenge verwalten",
73
- "shipping_carriers.feature.view": "Sendungen der Versanddienstleister anzeigen"
73
+ "shipping_carriers.feature.view": "Sendungen der Versanddienstleister anzeigen",
74
+ "shipping_carriers.status.cancelled": "Storniert",
75
+ "shipping_carriers.status.delivered": "Zugestellt",
76
+ "shipping_carriers.status.failed_delivery": "Zustellung fehlgeschlagen",
77
+ "shipping_carriers.status.in_transit": "Unterwegs",
78
+ "shipping_carriers.status.label_created": "Label erstellt",
79
+ "shipping_carriers.status.out_for_delivery": "In Zustellung",
80
+ "shipping_carriers.status.pending": "Ausstehend",
81
+ "shipping_carriers.status.picked_up": "Abgeholt",
82
+ "shipping_carriers.status.returned": "Zuruckgesendet",
83
+ "shipping_carriers.status.unknown": "Unbekannt"
74
84
  }
@@ -70,5 +70,15 @@
70
70
  "shipping_carriers.create.summary.to": "To",
71
71
  "shipping_carriers.create.title": "Create carrier shipment",
72
72
  "shipping_carriers.feature.manage": "Manage shipping carrier operations",
73
- "shipping_carriers.feature.view": "View shipping carrier shipments"
73
+ "shipping_carriers.feature.view": "View shipping carrier shipments",
74
+ "shipping_carriers.status.cancelled": "Cancelled",
75
+ "shipping_carriers.status.delivered": "Delivered",
76
+ "shipping_carriers.status.failed_delivery": "Delivery failed",
77
+ "shipping_carriers.status.in_transit": "In transit",
78
+ "shipping_carriers.status.label_created": "Label created",
79
+ "shipping_carriers.status.out_for_delivery": "Out for delivery",
80
+ "shipping_carriers.status.pending": "Pending",
81
+ "shipping_carriers.status.picked_up": "Picked up",
82
+ "shipping_carriers.status.returned": "Returned",
83
+ "shipping_carriers.status.unknown": "Unknown"
74
84
  }
@@ -4,4 +4,14 @@ export default {
4
4
  'shipping_carriers.column.trackingStatus': 'Tracking status',
5
5
  'shipping_carriers.column.shippingStatus': 'Shipping status',
6
6
  'shipping_carriers.action.createShipment': 'Create shipment',
7
+ 'shipping_carriers.status.cancelled': 'Cancelled',
8
+ 'shipping_carriers.status.delivered': 'Delivered',
9
+ 'shipping_carriers.status.failed_delivery': 'Delivery failed',
10
+ 'shipping_carriers.status.in_transit': 'In transit',
11
+ 'shipping_carriers.status.label_created': 'Label created',
12
+ 'shipping_carriers.status.out_for_delivery': 'Out for delivery',
13
+ 'shipping_carriers.status.pending': 'Pending',
14
+ 'shipping_carriers.status.picked_up': 'Picked up',
15
+ 'shipping_carriers.status.returned': 'Returned',
16
+ 'shipping_carriers.status.unknown': 'Unknown',
7
17
  }
@@ -70,5 +70,15 @@
70
70
  "shipping_carriers.create.summary.to": "Hasta",
71
71
  "shipping_carriers.create.title": "Crear envio con transportista",
72
72
  "shipping_carriers.feature.manage": "Gestionar operaciones de transportistas",
73
- "shipping_carriers.feature.view": "Ver envios de transportistas"
73
+ "shipping_carriers.feature.view": "Ver envios de transportistas",
74
+ "shipping_carriers.status.cancelled": "Cancelado",
75
+ "shipping_carriers.status.delivered": "Entregado",
76
+ "shipping_carriers.status.failed_delivery": "Entrega fallida",
77
+ "shipping_carriers.status.in_transit": "En transito",
78
+ "shipping_carriers.status.label_created": "Etiqueta creada",
79
+ "shipping_carriers.status.out_for_delivery": "En reparto",
80
+ "shipping_carriers.status.pending": "Pendiente",
81
+ "shipping_carriers.status.picked_up": "Recogido",
82
+ "shipping_carriers.status.returned": "Devuelto",
83
+ "shipping_carriers.status.unknown": "Desconocido"
74
84
  }
@@ -70,5 +70,15 @@
70
70
  "shipping_carriers.create.summary.to": "Odbiór",
71
71
  "shipping_carriers.create.title": "Utwórz przesyłkę u przewoźnika",
72
72
  "shipping_carriers.feature.manage": "Zarzadzanie operacjami przewoznikow",
73
- "shipping_carriers.feature.view": "Podglad przesylek przewoznikow"
73
+ "shipping_carriers.feature.view": "Podglad przesylek przewoznikow",
74
+ "shipping_carriers.status.cancelled": "Anulowano",
75
+ "shipping_carriers.status.delivered": "Doreczono",
76
+ "shipping_carriers.status.failed_delivery": "Doreczenie nieudane",
77
+ "shipping_carriers.status.in_transit": "W transporcie",
78
+ "shipping_carriers.status.label_created": "Etykieta utworzona",
79
+ "shipping_carriers.status.out_for_delivery": "W doreczeniu",
80
+ "shipping_carriers.status.pending": "Oczekuje",
81
+ "shipping_carriers.status.picked_up": "Odebrano",
82
+ "shipping_carriers.status.returned": "Zwrocono",
83
+ "shipping_carriers.status.unknown": "Nieznany"
74
84
  }
@@ -4,4 +4,14 @@ export default {
4
4
  'shipping_carriers.column.trackingStatus': 'Status śledzenia',
5
5
  'shipping_carriers.column.shippingStatus': 'Status wysyłki',
6
6
  'shipping_carriers.action.createShipment': 'Utwórz przesyłkę',
7
+ 'shipping_carriers.status.cancelled': 'Anulowano',
8
+ 'shipping_carriers.status.delivered': 'Doręczono',
9
+ 'shipping_carriers.status.failed_delivery': 'Doręczenie nieudane',
10
+ 'shipping_carriers.status.in_transit': 'W transporcie',
11
+ 'shipping_carriers.status.label_created': 'Etykieta utworzona',
12
+ 'shipping_carriers.status.out_for_delivery': 'W doręczeniu',
13
+ 'shipping_carriers.status.pending': 'Oczekuje',
14
+ 'shipping_carriers.status.picked_up': 'Odebrano',
15
+ 'shipping_carriers.status.returned': 'Zwrócono',
16
+ 'shipping_carriers.status.unknown': 'Nieznany',
7
17
  }
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
4
+ import { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'
5
+ import type { UnifiedShipmentStatus } from '../../../lib/adapter'
6
+
7
+ type ShippingStatus = UnifiedShipmentStatus | 'pending'
8
+
9
+ const shippingStatusVariantMap: StatusMap<ShippingStatus> = {
10
+ pending: 'neutral',
11
+ label_created: 'info',
12
+ picked_up: 'info',
13
+ in_transit: 'info',
14
+ out_for_delivery: 'info',
15
+ delivered: 'success',
16
+ failed_delivery: 'error',
17
+ returned: 'warning',
18
+ cancelled: 'error',
19
+ unknown: 'neutral',
20
+ }
21
+
22
+ export type ShippingStatusBadgeWidgetProps = {
23
+ status: string
24
+ }
25
+
26
+ export default function ShippingStatusBadgeWidget({ status }: ShippingStatusBadgeWidgetProps) {
27
+ const t = useT()
28
+ const variant = shippingStatusVariantMap[status as ShippingStatus] ?? 'neutral'
29
+
30
+ return (
31
+ <StatusBadge variant={variant} dot>
32
+ {t(`shipping_carriers.status.${status}`, status)}
33
+ </StatusBadge>
34
+ )
35
+ }
@@ -1,20 +1,6 @@
1
+ import * as React from 'react'
1
2
  import type { InjectionColumnWidget } from '@open-mercato/shared/modules/widgets/injection'
2
-
3
- const SHIPPING_STATUS_COLORS: Record<string, string> = {
4
- pending: 'bg-gray-100 text-gray-700',
5
- label_created: 'bg-slate-100 text-slate-700',
6
- picked_up: 'bg-sky-100 text-sky-700',
7
- in_transit: 'bg-blue-100 text-blue-700',
8
- out_for_delivery: 'bg-indigo-100 text-indigo-700',
9
- delivered: 'bg-green-100 text-green-700',
10
- returned: 'bg-amber-100 text-amber-700',
11
- cancelled: 'bg-red-100 text-red-700',
12
- failed_delivery: 'bg-orange-100 text-orange-700',
13
- }
14
-
15
- function formatShippingStatusLabel(value: string): string {
16
- return value.replace(/_/g, ' ')
17
- }
3
+ import ShippingStatusBadgeWidget from './widget.client'
18
4
 
19
5
  const widget: InjectionColumnWidget = {
20
6
  metadata: {
@@ -30,12 +16,7 @@ const widget: InjectionColumnWidget = {
30
16
  cell: ({ getValue }) => {
31
17
  const value = getValue()
32
18
  if (typeof value !== 'string' || value.length === 0) return null
33
- const colors = SHIPPING_STATUS_COLORS[value] ?? 'bg-gray-100 text-gray-700'
34
- return (
35
- <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${colors}`}>
36
- {formatShippingStatusLabel(value)}
37
- </span>
38
- )
19
+ return React.createElement(ShippingStatusBadgeWidget, { status: value })
39
20
  },
40
21
  },
41
22
  ],
@@ -3,9 +3,12 @@
3
3
  import * as React from 'react'
4
4
  import { useParams } from 'next/navigation'
5
5
  import Link from 'next/link'
6
+ import { useQuery } from '@tanstack/react-query'
6
7
  import { ExternalLink, Languages } from 'lucide-react'
7
8
  import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
8
9
  import { useT } from '@open-mercato/shared/lib/i18n/context'
10
+ import { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'
11
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
9
12
  import { Button } from '@open-mercato/ui/primitives/button'
10
13
  import {
11
14
  Drawer,
@@ -21,20 +24,34 @@ import { extractRecordId } from '../../../lib/extract-record-id'
21
24
  type WidgetContext = { entityId?: string; recordId?: string }
22
25
  type WidgetData = Record<string, unknown> & { id?: string | number }
23
26
 
27
+ type FeatureCheckResponse = {
28
+ ok: boolean
29
+ granted: string[]
30
+ userId: string
31
+ }
32
+
33
+ const TRANSLATION_ACCESS_FEATURES = ['translations.view']
34
+
35
+ // Probe access through the shared apiCall/React Query path so multiple injected
36
+ // widget instances dedupe a single request via the query key. feature-check
37
+ // always answers 200 with the granted feature list, so a user without
38
+ // translations.view is hidden gracefully — no login redirect, no forbidden flash.
24
39
  function useTranslationAccess(): boolean {
25
- const [hasAccess, setHasAccess] = React.useState(false)
26
- React.useEffect(() => {
27
- let mounted = true
28
- // Use the original fetch to bypass the global apiFetch wrapper
29
- // that redirects to login on 403. This lets us gracefully hide the widget
30
- // when the user lacks translations.view instead of crashing the page.
31
- const nativeFetch = ((window as any).__omOriginalFetch as typeof fetch) || fetch
32
- nativeFetch('/api/translations/locales', { credentials: 'include' })
33
- .then((res) => { if (mounted) setHasAccess(res.ok) })
34
- .catch(() => { if (mounted) setHasAccess(false) })
35
- return () => { mounted = false }
36
- }, [])
37
- return hasAccess
40
+ const { data } = useQuery<boolean>({
41
+ queryKey: ['translations', 'widget-access', ...TRANSLATION_ACCESS_FEATURES],
42
+ queryFn: async () => {
43
+ const res = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {
44
+ method: 'POST',
45
+ headers: { 'content-type': 'application/json' },
46
+ body: JSON.stringify({ features: TRANSLATION_ACCESS_FEATURES }),
47
+ })
48
+ if (!res.ok) return false
49
+ return hasAllFeatures(TRANSLATION_ACCESS_FEATURES, res.result?.granted ?? [])
50
+ },
51
+ staleTime: 5 * 60 * 1000,
52
+ retry: false,
53
+ })
54
+ return data === true
38
55
  }
39
56
 
40
57
  export default function TranslationWidget({ context, data }: InjectionWidgetComponentProps<WidgetContext, WidgetData>) {