@open-mercato/core 0.10.1-develop.8004.1.a8a79ee43f → 0.10.1-develop.8022.1.b6753de892

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 (59) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/audit_logs/components/AuditLogsActions.js +80 -50
  3. package/dist/modules/audit_logs/components/AuditLogsActions.js.map +3 -3
  4. package/dist/modules/audit_logs/lib/rowPresentation.js +82 -0
  5. package/dist/modules/audit_logs/lib/rowPresentation.js.map +7 -0
  6. package/dist/modules/catalog/commands/variants.js +89 -16
  7. package/dist/modules/catalog/commands/variants.js.map +2 -2
  8. package/dist/modules/catalog/seed/examples.js +7 -0
  9. package/dist/modules/catalog/seed/examples.js.map +2 -2
  10. package/dist/modules/customers/api/interactions/route.js +2 -2
  11. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  12. package/dist/modules/customers/components/detail/AssignRoleDialog.js +10 -21
  13. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  14. package/dist/modules/customers/migrations/Migration20260825140000_customers.js +17 -10
  15. package/dist/modules/customers/migrations/Migration20260825140000_customers.js.map +2 -2
  16. package/dist/modules/entities/cli.js +138 -66
  17. package/dist/modules/entities/cli.js.map +3 -3
  18. package/dist/modules/promotions/commands/codes.js +18 -3
  19. package/dist/modules/promotions/commands/codes.js.map +2 -2
  20. package/dist/modules/query_index/cli.js +17 -1
  21. package/dist/modules/query_index/cli.js.map +2 -2
  22. package/dist/modules/query_index/lib/reindexer.js +23 -5
  23. package/dist/modules/query_index/lib/reindexer.js.map +2 -2
  24. package/dist/modules/sales/commands/configuration.js +30 -16
  25. package/dist/modules/sales/commands/configuration.js.map +2 -2
  26. package/dist/modules/sales/components/documents/AddressesSection.js +36 -15
  27. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  28. package/dist/modules/sales/seed/examples.js +35 -6
  29. package/dist/modules/sales/seed/examples.js.map +2 -2
  30. package/package.json +7 -7
  31. package/src/modules/audit_logs/components/AuditLogsActions.tsx +85 -48
  32. package/src/modules/audit_logs/i18n/de.json +16 -0
  33. package/src/modules/audit_logs/i18n/en.json +16 -0
  34. package/src/modules/audit_logs/i18n/es.json +16 -0
  35. package/src/modules/audit_logs/i18n/ko.json +16 -0
  36. package/src/modules/audit_logs/i18n/pl.json +16 -0
  37. package/src/modules/audit_logs/lib/rowPresentation.ts +92 -0
  38. package/src/modules/catalog/commands/variants.ts +96 -17
  39. package/src/modules/catalog/seed/examples.ts +7 -0
  40. package/src/modules/customers/api/interactions/route.ts +2 -2
  41. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +9 -12
  42. package/src/modules/customers/i18n/de.json +2 -1
  43. package/src/modules/customers/i18n/en.json +2 -1
  44. package/src/modules/customers/i18n/es.json +2 -1
  45. package/src/modules/customers/i18n/ko.json +2 -1
  46. package/src/modules/customers/i18n/pl.json +2 -1
  47. package/src/modules/customers/migrations/Migration20260825140000_customers.ts +21 -10
  48. package/src/modules/entities/cli.ts +144 -65
  49. package/src/modules/promotions/commands/codes.ts +19 -4
  50. package/src/modules/query_index/cli.ts +17 -1
  51. package/src/modules/query_index/lib/reindexer.ts +55 -6
  52. package/src/modules/sales/commands/configuration.ts +35 -16
  53. package/src/modules/sales/components/documents/AddressesSection.tsx +51 -20
  54. package/src/modules/sales/i18n/de.json +3 -0
  55. package/src/modules/sales/i18n/en.json +3 -0
  56. package/src/modules/sales/i18n/es.json +3 -0
  57. package/src/modules/sales/i18n/ko.json +3 -0
  58. package/src/modules/sales/i18n/pl.json +4 -1
  59. package/src/modules/sales/seed/examples.ts +41 -6
@@ -4,7 +4,10 @@ import * as React from 'react'
4
4
  import { extensionPoints } from '@open-mercato/core/modules/audit_logs/extension-points'
5
5
  import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
6
6
  import { DataTable, type PaginationProps } from '@open-mercato/ui/backend/DataTable'
7
+ import type { RowActionItem } from '@open-mercato/ui/backend/RowActions'
7
8
  import { Button } from '@open-mercato/ui/primitives/button'
9
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
10
+ import { auditRecordName, auditChangeSummary, auditExecutionState } from '../lib/rowPresentation'
8
11
  import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
9
12
  import { useT } from '@open-mercato/shared/lib/i18n/context'
10
13
  import { ActionLogDetailsDialog } from './ActionLogDetailsDialog'
@@ -16,6 +19,10 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
16
19
 
17
20
  const logger = createLogger('audit_logs').child({ component: 'AuditLogsActions' })
18
21
 
22
+ function AuditLogRowActions({ children }: { items: RowActionItem[]; children: React.ReactNode }) {
23
+ return <div className="flex justify-end gap-1" data-actions-cell>{children}</div>
24
+ }
25
+
19
26
  /**
20
27
  * `raiseCrudError` prefers the server's `error` body over the fallback message and stamps
21
28
  * `status` on anything it builds from an HTTP response, so a numeric `status` is what
@@ -150,16 +157,35 @@ export function AuditLogsActions({
150
157
  }
151
158
  }, [onRefresh, onRedoError, t])
152
159
 
153
- const columns = React.useMemo<ColumnDef<ActionLogItem, any>[]>(() => [
160
+ const columns = React.useMemo<ColumnDef<ActionLogItem, unknown>[]>(() => [
154
161
  {
155
162
  accessorKey: 'actionLabel',
156
163
  header: t('audit_logs.actions.columns.action'),
157
- cell: (info) => info.row.original.actionLabel || info.row.original.commandId,
164
+ cell: (info) => {
165
+ const item = info.row.original
166
+ const summary = auditChangeSummary(item, t)
167
+ return (
168
+ <div className="max-w-72">
169
+ <div>{item.actionLabel || item.commandId}</div>
170
+ <div className="truncate text-xs text-muted-foreground" title={summary}>{summary}</div>
171
+ </div>
172
+ )
173
+ },
158
174
  },
159
175
  {
160
176
  accessorKey: 'resourceKind',
161
177
  header: t('audit_logs.actions.columns.resource'),
162
- cell: (info) => formatResource(info.row.original, noneLabel),
178
+ cell: (info) => {
179
+ const item = info.row.original
180
+ const name = auditRecordName(item)
181
+ const resource = formatResource(item, noneLabel)
182
+ return (
183
+ <div className="max-w-72">
184
+ {name ? <div className="truncate font-medium" title={name}>{name}</div> : null}
185
+ <div className={name ? 'truncate text-xs text-muted-foreground' : 'truncate'} title={resource}>{resource}</div>
186
+ </div>
187
+ )
188
+ },
163
189
  },
164
190
  {
165
191
  accessorKey: 'actorUserId',
@@ -170,13 +196,13 @@ export function AuditLogsActions({
170
196
  {
171
197
  accessorKey: 'tenantId',
172
198
  header: t('audit_logs.actions.columns.tenant'),
173
- cell: (info) => info.row.original.tenantName || info.getValue() || noneLabel,
199
+ cell: (info) => info.row.original.tenantName || info.row.original.tenantId || noneLabel,
174
200
  meta: { priority: 4 },
175
201
  },
176
202
  {
177
203
  accessorKey: 'organizationId',
178
204
  header: t('audit_logs.actions.columns.organization'),
179
- cell: (info) => info.row.original.organizationName || info.getValue() || noneLabel,
205
+ cell: (info) => info.row.original.organizationName || info.row.original.organizationId || noneLabel,
180
206
  meta: { priority: 4 },
181
207
  },
182
208
  {
@@ -187,52 +213,61 @@ export function AuditLogsActions({
187
213
  {
188
214
  accessorKey: 'executionState',
189
215
  header: t('audit_logs.actions.columns.status'),
216
+ cell: (info) => auditExecutionState(info.row.original.executionState, t),
190
217
  },
191
- {
192
- id: 'controls',
193
- header: t('audit_logs.actions.columns.controls'),
194
- enableSorting: false,
195
- cell: (info) => {
196
- const item = info.row.original
197
- const itemCanUndo = canUndoEntry(permissions, item.actorUserId)
198
- const itemCanRedo = canRedoEntry(permissions, item.actorUserId)
199
- const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === 'done' && isLatestUndoableForItem(item)
200
- const showRedo = itemCanRedo && item.executionState === 'undone'
201
- const canRedo = showRedo && isRedoCandidate(item)
202
- if (!canUndo && !showRedo) return null
203
- return (
204
- <div className="flex justify-end gap-1" data-actions-cell>
205
- {canUndo ? (
206
- <Button
207
- variant="ghost"
208
- size="icon"
209
- aria-label={t('audit_logs.actions.undo')}
210
- onClick={() => { void handleUndo(item.undoToken) }}
211
- disabled={undoingToken === item.undoToken || Boolean(redoingId)}
212
- >
213
- <Undo2 className="size-4" aria-hidden="true" />
214
- </Button>
215
- ) : null}
216
- {showRedo ? (
217
- <Button
218
- variant="ghost"
219
- size="icon"
220
- aria-label={t('audit_logs.actions.redo')}
221
- onClick={() => { void handleRedo(item.id) }}
222
- disabled={!canRedo || redoingId === item.id || Boolean(undoingToken)}
223
- >
224
- <RotateCcw className="size-4" aria-hidden="true" />
225
- </Button>
226
- ) : null}
227
- </div>
228
- )
229
- },
230
- meta: { align: 'right' },
231
- },
232
- ], [t, noneLabel, handleUndo, handleRedo, isLatestUndoableForItem, isRedoCandidate, undoingToken, redoingId, permissions])
218
+ ], [t, noneLabel])
219
+
220
+ const rowActions = React.useCallback((item: ActionLogItem) => {
221
+ const itemCanUndo = canUndoEntry(permissions, item.actorUserId)
222
+ const itemCanRedo = canRedoEntry(permissions, item.actorUserId)
223
+ const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === 'done' && isLatestUndoableForItem(item)
224
+ const showRedo = itemCanRedo && item.executionState === 'undone'
225
+ const canRedo = showRedo && isRedoCandidate(item)
226
+ if (!canUndo && !showRedo) return null
227
+ const undoDisabled = undoingToken === item.undoToken || Boolean(redoingId)
228
+ const redoDisabled = !canRedo || redoingId === item.id || Boolean(undoingToken)
229
+ const undoLabel = t('audit_logs.actions.undo_this')
230
+ const redoLabel = t('audit_logs.actions.redo_this')
231
+ const onUndo = () => { void handleUndo(item.undoToken) }
232
+ const onRedo = () => { void handleRedo(item.id) }
233
+ const items: RowActionItem[] = []
234
+ if (canUndo && !undoDisabled) items.push({ id: 'undo', label: undoLabel, onSelect: onUndo })
235
+ if (canRedo && !redoDisabled) items.push({ id: 'redo', label: redoLabel, onSelect: onRedo })
236
+ return (
237
+ <AuditLogRowActions items={items}>
238
+ {canUndo ? (
239
+ <IconButton
240
+ variant="ghost"
241
+ size="lg"
242
+ type="button"
243
+ aria-label={undoLabel}
244
+ title={undoLabel}
245
+ onClick={onUndo}
246
+ disabled={undoDisabled}
247
+ >
248
+ <Undo2 className="size-4" aria-hidden="true" />
249
+ </IconButton>
250
+ ) : null}
251
+ {showRedo ? (
252
+ <IconButton
253
+ variant="ghost"
254
+ size="lg"
255
+ type="button"
256
+ aria-label={redoLabel}
257
+ title={redoLabel}
258
+ onClick={onRedo}
259
+ disabled={redoDisabled}
260
+ >
261
+ <RotateCcw className="size-4" aria-hidden="true" />
262
+ </IconButton>
263
+ ) : null}
264
+ </AuditLogRowActions>
265
+ )
266
+ }, [t, handleUndo, handleRedo, isLatestUndoableForItem, isRedoCandidate, undoingToken, redoingId, permissions])
233
267
 
234
268
  const undoButton = latestUndoable?.undoToken && canUndoEntry(permissions, latestUndoable.actorUserId) ? (
235
269
  <Button
270
+ type="button"
236
271
  variant="secondary"
237
272
  size="sm"
238
273
  onClick={() => { void handleUndo(latestUndoable.undoToken) }}
@@ -262,6 +297,8 @@ export function AuditLogsActions({
262
297
  titleHeadingLevel={2}
263
298
  data={actionItems}
264
299
  columns={columns}
300
+ rowActions={rowActions}
301
+ stickyActionsColumn
265
302
  actions={combinedActions}
266
303
  perspective={{ tableId: extensionPoints.hosts.actionsTable.tableId }}
267
304
  isLoading={Boolean(isLoading) || Boolean(undoingToken) || Boolean(redoingId)}
@@ -295,6 +332,6 @@ function formatDate(value: string) {
295
332
  if (Number.isNaN(date.getTime())) return value
296
333
  return new Intl.DateTimeFormat(undefined, {
297
334
  dateStyle: 'medium',
298
- timeStyle: 'short',
335
+ timeStyle: 'medium',
299
336
  }).format(date)
300
337
  }
@@ -26,8 +26,10 @@
26
26
  "audit_logs.actions.details.snapshot_before": "Snapshot vorher",
27
27
  "audit_logs.actions.details.title": "Aktionsdetails",
28
28
  "audit_logs.actions.redo": "Letzte Aktion wiederholen",
29
+ "audit_logs.actions.redo_this": "Diese Aktion wiederholen",
29
30
  "audit_logs.actions.title": "Aktionsprotokoll",
30
31
  "audit_logs.actions.undo": "Letzte Aktion rückgängig",
32
+ "audit_logs.actions.undo_this": "Diese Aktion rückgängig machen",
31
33
  "audit_logs.actions.undoing": "Wird rückgängig gemacht…",
32
34
  "audit_logs.actor.api_key": "API-Schlüssel: {name}",
33
35
  "audit_logs.actor.api_key_revoked": "API-Schlüssel: {name} (widerrufen)",
@@ -48,6 +50,20 @@
48
50
  "audit_logs.filters.undoable_only": "Nur rückgängig machbar",
49
51
  "audit_logs.hint.view_self_only": "Es werden nur Ihre eigenen Änderungen angezeigt. Wenden Sie sich an einen Administrator für erweiterten Zugriff.",
50
52
  "audit_logs.nav.title": "Audit-Logs",
53
+ "audit_logs.summary.active": "Aktiv",
54
+ "audit_logs.summary.change": "{field}: {from} → {to}",
55
+ "audit_logs.summary.count": "Geänderte Felder: {count}",
56
+ "audit_logs.summary.label": "Bezeichnung",
57
+ "audit_logs.summary.more": "{summary} (+{count})",
58
+ "audit_logs.summary.name": "Name",
59
+ "audit_logs.summary.no": "Nein",
60
+ "audit_logs.summary.parent": "Übergeordnetes Element",
61
+ "audit_logs.summary.parent_organization": "Übergeordnete Organisation",
62
+ "audit_logs.summary.root": "(Stammebene)",
63
+ "audit_logs.summary.slug": "Slug",
64
+ "audit_logs.summary.status": "Status",
65
+ "audit_logs.summary.title": "Titel",
66
+ "audit_logs.summary.yes": "Ja",
51
67
  "audit_logs.tabs.label": "Ansichten des Audit-Protokolls",
52
68
  "audit_logs.version_history.action.redo": "{action} erneut ausführen",
53
69
  "audit_logs.version_history.action.undo": "{action} rückgängig machen",
@@ -26,8 +26,10 @@
26
26
  "audit_logs.actions.details.snapshot_before": "Snapshot before",
27
27
  "audit_logs.actions.details.title": "Action details",
28
28
  "audit_logs.actions.redo": "Redo last action",
29
+ "audit_logs.actions.redo_this": "Redo this action",
29
30
  "audit_logs.actions.title": "Action Log",
30
31
  "audit_logs.actions.undo": "Undo last action",
32
+ "audit_logs.actions.undo_this": "Undo this action",
31
33
  "audit_logs.actions.undoing": "Undoing…",
32
34
  "audit_logs.actor.api_key": "API key: {name}",
33
35
  "audit_logs.actor.api_key_revoked": "API key: {name} (revoked)",
@@ -48,6 +50,20 @@
48
50
  "audit_logs.filters.undoable_only": "Undoable only",
49
51
  "audit_logs.hint.view_self_only": "Showing only your own changes. Contact an administrator for broader access.",
50
52
  "audit_logs.nav.title": "Audit Logs",
53
+ "audit_logs.summary.active": "Active",
54
+ "audit_logs.summary.change": "{field}: {from} → {to}",
55
+ "audit_logs.summary.count": "Changed fields: {count}",
56
+ "audit_logs.summary.label": "Label",
57
+ "audit_logs.summary.more": "{summary} (+{count})",
58
+ "audit_logs.summary.name": "Name",
59
+ "audit_logs.summary.no": "No",
60
+ "audit_logs.summary.parent": "Parent",
61
+ "audit_logs.summary.parent_organization": "Parent organization",
62
+ "audit_logs.summary.root": "(root level)",
63
+ "audit_logs.summary.slug": "Slug",
64
+ "audit_logs.summary.status": "Status",
65
+ "audit_logs.summary.title": "Title",
66
+ "audit_logs.summary.yes": "Yes",
51
67
  "audit_logs.tabs.label": "Audit log views",
52
68
  "audit_logs.version_history.action.redo": "Redo {action}",
53
69
  "audit_logs.version_history.action.undo": "Undo {action}",
@@ -26,8 +26,10 @@
26
26
  "audit_logs.actions.details.snapshot_before": "Captura anterior",
27
27
  "audit_logs.actions.details.title": "Detalles de la acción",
28
28
  "audit_logs.actions.redo": "Rehacer última acción",
29
+ "audit_logs.actions.redo_this": "Rehacer esta acción",
29
30
  "audit_logs.actions.title": "Registro de acciones",
30
31
  "audit_logs.actions.undo": "Deshacer última acción",
32
+ "audit_logs.actions.undo_this": "Deshacer esta acción",
31
33
  "audit_logs.actions.undoing": "Deshaciendo…",
32
34
  "audit_logs.actor.api_key": "Clave API: {name}",
33
35
  "audit_logs.actor.api_key_revoked": "Clave API: {name} (revocada)",
@@ -48,6 +50,20 @@
48
50
  "audit_logs.filters.undoable_only": "Solo deshacibles",
49
51
  "audit_logs.hint.view_self_only": "Solo se muestran sus propios cambios. Contacte a un administrador para un acceso más amplio.",
50
52
  "audit_logs.nav.title": "Registros de auditoría",
53
+ "audit_logs.summary.active": "Activo",
54
+ "audit_logs.summary.change": "{field}: {from} → {to}",
55
+ "audit_logs.summary.count": "Campos modificados: {count}",
56
+ "audit_logs.summary.label": "Etiqueta",
57
+ "audit_logs.summary.more": "{summary} (+{count})",
58
+ "audit_logs.summary.name": "Nombre",
59
+ "audit_logs.summary.no": "No",
60
+ "audit_logs.summary.parent": "Elemento superior",
61
+ "audit_logs.summary.parent_organization": "Organización superior",
62
+ "audit_logs.summary.root": "(nivel raíz)",
63
+ "audit_logs.summary.slug": "Slug",
64
+ "audit_logs.summary.status": "Estado",
65
+ "audit_logs.summary.title": "Título",
66
+ "audit_logs.summary.yes": "Sí",
51
67
  "audit_logs.tabs.label": "Vistas del registro de auditoría",
52
68
  "audit_logs.version_history.action.redo": "Rehacer {action}",
53
69
  "audit_logs.version_history.action.undo": "Deshacer {action}",
@@ -26,8 +26,10 @@
26
26
  "audit_logs.actions.details.snapshot_before": "변경 전 스냅샷",
27
27
  "audit_logs.actions.details.title": "작업 상세 정보",
28
28
  "audit_logs.actions.redo": "마지막 작업 다시 실행",
29
+ "audit_logs.actions.redo_this": "이 작업 다시 실행",
29
30
  "audit_logs.actions.title": "작업 로그",
30
31
  "audit_logs.actions.undo": "마지막 작업 실행 취소",
32
+ "audit_logs.actions.undo_this": "이 작업 실행 취소",
31
33
  "audit_logs.actions.undoing": "실행 취소 중…",
32
34
  "audit_logs.actor.api_key": "API 키: {name}",
33
35
  "audit_logs.actor.api_key_revoked": "API 키: {name} (해지됨)",
@@ -48,6 +50,20 @@
48
50
  "audit_logs.filters.undoable_only": "실행 취소 가능한 항목만",
49
51
  "audit_logs.hint.view_self_only": "본인의 변경 사항만 표시됩니다. 더 넓은 범위의 접근이 필요하면 관리자에게 문의하세요.",
50
52
  "audit_logs.nav.title": "감사 로그",
53
+ "audit_logs.summary.active": "활성",
54
+ "audit_logs.summary.change": "{field}: {from} → {to}",
55
+ "audit_logs.summary.count": "변경된 필드: {count}",
56
+ "audit_logs.summary.label": "레이블",
57
+ "audit_logs.summary.more": "{summary} (+{count})",
58
+ "audit_logs.summary.name": "이름",
59
+ "audit_logs.summary.no": "아니요",
60
+ "audit_logs.summary.parent": "상위 항목",
61
+ "audit_logs.summary.parent_organization": "상위 조직",
62
+ "audit_logs.summary.root": "(최상위)",
63
+ "audit_logs.summary.slug": "슬러그",
64
+ "audit_logs.summary.status": "상태",
65
+ "audit_logs.summary.title": "제목",
66
+ "audit_logs.summary.yes": "예",
51
67
  "audit_logs.tabs.label": "감사 로그 보기",
52
68
  "audit_logs.version_history.action.redo": "{action} 다시 실행",
53
69
  "audit_logs.version_history.action.undo": "{action} 실행 취소",
@@ -26,8 +26,10 @@
26
26
  "audit_logs.actions.details.snapshot_before": "Migawka przed",
27
27
  "audit_logs.actions.details.title": "Szczegóły działania",
28
28
  "audit_logs.actions.redo": "Przywróć ostatnie działanie",
29
+ "audit_logs.actions.redo_this": "Ponów to działanie",
29
30
  "audit_logs.actions.title": "Dziennik działań",
30
31
  "audit_logs.actions.undo": "Cofnij ostatnie działanie",
32
+ "audit_logs.actions.undo_this": "Cofnij to działanie",
31
33
  "audit_logs.actions.undoing": "Cofanie…",
32
34
  "audit_logs.actor.api_key": "Klucz API: {name}",
33
35
  "audit_logs.actor.api_key_revoked": "Klucz API: {name} (unieważniony)",
@@ -48,6 +50,20 @@
48
50
  "audit_logs.filters.undoable_only": "Tylko do cofnięcia",
49
51
  "audit_logs.hint.view_self_only": "Wyświetlane są tylko Twoje zmiany. Skontaktuj się z administratorem, aby uzyskać szerszy dostęp.",
50
52
  "audit_logs.nav.title": "Logi audytu",
53
+ "audit_logs.summary.active": "Aktywne",
54
+ "audit_logs.summary.change": "{field}: {from} → {to}",
55
+ "audit_logs.summary.count": "Zmienione pola: {count}",
56
+ "audit_logs.summary.label": "Etykieta",
57
+ "audit_logs.summary.more": "{summary} (+{count})",
58
+ "audit_logs.summary.name": "Nazwa",
59
+ "audit_logs.summary.no": "Nie",
60
+ "audit_logs.summary.parent": "Element nadrzędny",
61
+ "audit_logs.summary.parent_organization": "Organizacja nadrzędna",
62
+ "audit_logs.summary.root": "(poziom główny)",
63
+ "audit_logs.summary.slug": "Slug",
64
+ "audit_logs.summary.status": "Status",
65
+ "audit_logs.summary.title": "Tytuł",
66
+ "audit_logs.summary.yes": "Tak",
51
67
  "audit_logs.tabs.label": "Widoki dziennika audytu",
52
68
  "audit_logs.version_history.action.redo": "Ponów {action}",
53
69
  "audit_logs.version_history.action.undo": "Cofnij {action}",
@@ -0,0 +1,92 @@
1
+ import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
2
+ import { isRecord } from './changeRows'
3
+
4
+ type AuditRow = {
5
+ resourceKind?: string | null
6
+ snapshotBefore?: unknown
7
+ snapshotAfter?: unknown
8
+ changes?: Record<string, unknown> | null
9
+ }
10
+
11
+ const previewFields = [
12
+ ['displayName', 'audit_logs.summary.name'],
13
+ ['display_name', 'audit_logs.summary.name'],
14
+ ['name', 'audit_logs.summary.name'],
15
+ ['title', 'audit_logs.summary.title'],
16
+ ['label', 'audit_logs.summary.label'],
17
+ ['parentId', 'audit_logs.summary.parent'],
18
+ ['parent_id', 'audit_logs.summary.parent'],
19
+ ['status', 'audit_logs.summary.status'],
20
+ ['isActive', 'audit_logs.summary.active'],
21
+ ['is_active', 'audit_logs.summary.active'],
22
+ ['slug', 'audit_logs.summary.slug'],
23
+ ] as const
24
+
25
+ function previewText(value: unknown): string | undefined {
26
+ if (typeof value !== 'string') return undefined
27
+ const text = value.replace(/\s+/g, ' ').trim()
28
+ if (!text || text.endsWith(':v1')) return undefined
29
+ return text.length > 120 ? `${text.slice(0, 119)}…` : text
30
+ }
31
+
32
+ function snapshotRecord(snapshot: unknown): Record<string, unknown> | undefined {
33
+ if (!isRecord(snapshot)) return undefined
34
+ return isRecord(snapshot.view) ? snapshot.view : snapshot
35
+ }
36
+
37
+ export function auditRecordName(item: AuditRow): string | undefined {
38
+ for (const snapshot of [item.snapshotAfter, item.snapshotBefore]) {
39
+ const record = snapshotRecord(snapshot)
40
+ if (!record) continue
41
+ for (const field of ['displayName', 'display_name', 'name', 'title', 'label']) {
42
+ const name = previewText(record[field])
43
+ if (name) return name
44
+ }
45
+ }
46
+ return undefined
47
+ }
48
+
49
+ function summaryValue(value: unknown, field: string, t: TranslateFn): string | undefined {
50
+ if (value === null) {
51
+ return t(field === 'parentId' || field === 'parent_id' ? 'audit_logs.summary.root' : 'audit_logs.common.none')
52
+ }
53
+ if (typeof value === 'boolean') return t(value ? 'audit_logs.summary.yes' : 'audit_logs.summary.no')
54
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value)
55
+ return previewText(value)
56
+ }
57
+
58
+ export function auditChangeSummary(item: AuditRow, t: TranslateFn): string {
59
+ if (!isRecord(item.changes)) return t('audit_logs.actions.details.no_changes')
60
+ const count = Object.keys(item.changes).length
61
+ if (!count) return t('audit_logs.actions.details.no_changes')
62
+ for (const [field, labelKey] of previewFields) {
63
+ if (!Object.hasOwn(item.changes, field)) continue
64
+ const change = item.changes[field]
65
+ const before = snapshotRecord(item.snapshotBefore)?.[field]
66
+ const from = isRecord(change)
67
+ ? Object.hasOwn(change, 'from') ? change.from : Object.hasOwn(change, 'old') ? change.old : before
68
+ : before
69
+ const to = isRecord(change)
70
+ ? Object.hasOwn(change, 'to') ? change.to : change.new
71
+ : change
72
+ const fromText = summaryValue(from, field, t)
73
+ const toText = summaryValue(to, field, t)
74
+ if (fromText === undefined || toText === undefined) continue
75
+ const fieldLabel = labelKey === 'audit_logs.summary.parent' && item.resourceKind === 'directory.organization'
76
+ ? t('audit_logs.summary.parent_organization')
77
+ : t(labelKey)
78
+ const summary = t('audit_logs.summary.change', { field: fieldLabel, from: fromText, to: toText })
79
+ return count > 1 ? t('audit_logs.summary.more', { summary, count: count - 1 }) : summary
80
+ }
81
+ return t('audit_logs.summary.count', { count })
82
+ }
83
+
84
+ export function auditExecutionState(state: string, t: TranslateFn): string {
85
+ switch (state) {
86
+ case 'done': return t('audit_logs.version_history.status.done')
87
+ case 'undone': return t('audit_logs.version_history.status.undone')
88
+ case 'failed': return t('audit_logs.version_history.status.failed')
89
+ case 'redone': return t('audit_logs.version_history.status.redone')
90
+ default: return state
91
+ }
92
+ }
@@ -1,9 +1,17 @@
1
1
  import { registerCommand } from '@open-mercato/shared/lib/commands'
2
2
  import type { CommandHandler } from '@open-mercato/shared/lib/commands'
3
- import { buildChanges, requireId, parseWithCustomFields, setCustomFieldsIfAny, emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
3
+ import {
4
+ buildChanges,
5
+ requireId,
6
+ parseWithCustomFields,
7
+ setCustomFieldsIfAny,
8
+ emitCrudSideEffects,
9
+ emitCrudUndoSideEffects,
10
+ } from '@open-mercato/shared/lib/commands/helpers'
4
11
  import type { EntityManager } from '@mikro-orm/postgresql'
5
- import { UniqueConstraintViolationException } from '@mikro-orm/core'
12
+ import { LockMode, UniqueConstraintViolationException } from '@mikro-orm/core'
6
13
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
14
+ import { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'
7
15
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
16
  import { loadCustomFieldSnapshot, buildCustomFieldResetMap } from '@open-mercato/shared/lib/commands/customFieldSnapshots'
9
17
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
@@ -44,7 +52,7 @@ import {
44
52
  getErrorMessage,
45
53
  } from './shared'
46
54
  import { SalesTaxRate } from '@open-mercato/core/modules/sales/data/entities'
47
- import type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
55
+ import type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
48
56
 
49
57
  const variantCrudEvents: CrudEventsConfig = {
50
58
  module: 'catalog',
@@ -57,6 +65,21 @@ const variantCrudEvents: CrudEventsConfig = {
57
65
  }),
58
66
  }
59
67
 
68
+ const variantPriceCrudEvents: CrudEventsConfig = {
69
+ module: 'catalog',
70
+ entity: 'price',
71
+ persistent: true,
72
+ buildPayload: (ctx) => ({
73
+ id: ctx.identifiers.id,
74
+ organizationId: ctx.identifiers.organizationId,
75
+ tenantId: ctx.identifiers.tenantId,
76
+ }),
77
+ }
78
+
79
+ const variantPriceCrudIndexer: CrudIndexerConfig = {
80
+ entityType: E.catalog.catalog_product_price,
81
+ }
82
+
60
83
  type VariantSnapshot = {
61
84
  id: string
62
85
  productId: string
@@ -357,6 +380,24 @@ async function loadVariantPriceSnapshots(
357
380
  return snapshots
358
381
  }
359
382
 
383
+ async function resetVariantPriceCustomFields(
384
+ dataEngine: DataEngine,
385
+ snapshots: VariantPriceSnapshot[]
386
+ ): Promise<void> {
387
+ for (const snapshot of snapshots) {
388
+ const resetValues = buildCustomFieldResetMap(snapshot.custom ?? undefined, undefined)
389
+ if (!Object.keys(resetValues).length) continue
390
+ await setCustomFieldsIfAny({
391
+ dataEngine,
392
+ entityId: E.catalog.catalog_product_price,
393
+ recordId: snapshot.id,
394
+ organizationId: snapshot.organizationId,
395
+ tenantId: snapshot.tenantId,
396
+ values: resetValues,
397
+ })
398
+ }
399
+ }
400
+
360
401
  async function restoreVariantPricesFromSnapshots(
361
402
  em: EntityManager,
362
403
  variant: CatalogProductVariant,
@@ -728,8 +769,57 @@ const createVariantCommand: CommandHandler<VariantCreateInput, { variantId: stri
728
769
  if (!record) return
729
770
  ensureTenantScope(ctx, record.tenantId)
730
771
  ensureOrganizationScope(ctx, record.organizationId)
731
- em.remove(record)
732
- await em.flush()
772
+ const variantScope = { tenantId: record.tenantId, organizationId: record.organizationId }
773
+ let priceSnapshots: VariantPriceSnapshot[] = []
774
+ let variantLocked = false
775
+ await withAtomicFlush(
776
+ em,
777
+ [
778
+ async () => {
779
+ const locked = await findOneWithDecryption(
780
+ em,
781
+ CatalogProductVariant,
782
+ { id: record.id },
783
+ { lockMode: LockMode.PESSIMISTIC_WRITE },
784
+ variantScope,
785
+ )
786
+ if (!locked) return
787
+ variantLocked = true
788
+ priceSnapshots = await loadVariantPriceSnapshots(em.fork({ keepTransactionContext: true }), record.id, variantScope)
789
+ if (priceSnapshots.length) {
790
+ await em.nativeDelete(CatalogProductPrice, { id: { $in: priceSnapshots.map((price) => price.id) } })
791
+ }
792
+ },
793
+ () => {
794
+ if (variantLocked) em.remove(record)
795
+ },
796
+ ],
797
+ { transaction: true }
798
+ )
799
+ if (!variantLocked) return
800
+ await capturePriceHistoryEntries(em, priceSnapshots, 'delete', {
801
+ metadata: { undoneCommand: 'catalog.variants.create' },
802
+ cache: resolveOmnibusCache(ctx.container),
803
+ })
804
+ const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
805
+ await resetVariantPriceCustomFields(dataEngine, priceSnapshots)
806
+ for (const price of priceSnapshots) {
807
+ await emitCrudUndoSideEffects({
808
+ dataEngine,
809
+ action: 'deleted',
810
+ entity: price,
811
+ identifiers: { id: price.id, organizationId: price.organizationId, tenantId: price.tenantId },
812
+ events: variantPriceCrudEvents,
813
+ indexer: variantPriceCrudIndexer,
814
+ })
815
+ await invalidateCrudCache(
816
+ ctx.container,
817
+ 'catalog.price',
818
+ { id: price.id, organizationId: price.organizationId, tenantId: price.tenantId },
819
+ price.tenantId,
820
+ 'command:catalog.variants.create:undo'
821
+ )
822
+ }
733
823
  if (payload?.previousDefaultVariantId) {
734
824
  const previousDefault = await em.findOne(CatalogProductVariant, { id: payload.previousDefaultVariantId })
735
825
  if (previousDefault) {
@@ -1098,18 +1188,7 @@ const deleteVariantCommand: CommandHandler<
1098
1188
  em.remove(record)
1099
1189
  await em.flush()
1100
1190
  await capturePriceHistoryEntries(em, priceSnapshots, 'delete', { cache: resolveOmnibusCache(ctx.container) })
1101
- for (const priceSnapshot of priceSnapshots) {
1102
- const resetValues = buildCustomFieldResetMap(priceSnapshot.custom ?? undefined, undefined)
1103
- if (!Object.keys(resetValues).length) continue
1104
- await setCustomFieldsIfAny({
1105
- dataEngine,
1106
- entityId: E.catalog.catalog_product_price,
1107
- recordId: priceSnapshot.id,
1108
- organizationId: priceSnapshot.organizationId,
1109
- tenantId: priceSnapshot.tenantId,
1110
- values: resetValues,
1111
- })
1112
- }
1191
+ await resetVariantPriceCustomFields(dataEngine, priceSnapshots)
1113
1192
  if (snapshot?.custom && Object.keys(snapshot.custom).length) {
1114
1193
  const resetValues = buildCustomFieldResetMap(snapshot.custom, undefined)
1115
1194
  if (Object.keys(resetValues).length) {
@@ -824,6 +824,13 @@ async function loadPriceKinds(
824
824
  return map;
825
825
  }
826
826
 
827
+ export function listCatalogExampleKeys(): { productHandles: string[]; variantSkus: string[] } {
828
+ return {
829
+ productHandles: PRODUCT_SEEDS.map((seed) => seed.handle),
830
+ variantSkus: PRODUCT_SEEDS.flatMap((seed) => seed.variants.map((variant) => variant.sku)),
831
+ };
832
+ }
833
+
827
834
  export async function seedCatalogExamples(
828
835
  em: EntityManager,
829
836
  container: AwilixContainer,
@@ -372,12 +372,12 @@ function applyInteractionListFilters(
372
372
  // customer_activity / customer_comment. The returned page's title/body are
373
373
  // still decrypted for display further below.
374
374
  const searchTerm = `%${escapeLikePattern(query.search)}%`
375
- q = q.where(sql<boolean>`coalesce(title, '') ilike ${searchTerm} or coalesce(body, '') ilike ${searchTerm}`)
375
+ q = q.where(sql<boolean>`(coalesce(title, '') ilike ${searchTerm} or coalesce(body, '') ilike ${searchTerm})`)
376
376
  }
377
377
  if (query.recurrenceMasters === 'true') {
378
378
  q = q.where('recurrence_rule', 'is not', null)
379
379
  if (query.from) {
380
- q = q.where(sql<boolean>`recurrence_end is null or recurrence_end >= ${query.from}`)
380
+ q = q.where(sql<boolean>`(recurrence_end is null or recurrence_end >= ${query.from})`)
381
381
  }
382
382
  if (query.to) {
383
383
  q = q.where(sql<boolean>`coalesce(occurred_at, scheduled_at, created_at) <= ${query.to}`)