@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/audit_logs/components/AuditLogsActions.js +80 -50
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +3 -3
- package/dist/modules/audit_logs/lib/rowPresentation.js +82 -0
- package/dist/modules/audit_logs/lib/rowPresentation.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +89 -16
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +7 -0
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +2 -2
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +10 -21
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260825140000_customers.js +17 -10
- package/dist/modules/customers/migrations/Migration20260825140000_customers.js.map +2 -2
- package/dist/modules/entities/cli.js +138 -66
- package/dist/modules/entities/cli.js.map +3 -3
- package/dist/modules/promotions/commands/codes.js +18 -3
- package/dist/modules/promotions/commands/codes.js.map +2 -2
- package/dist/modules/query_index/cli.js +17 -1
- package/dist/modules/query_index/cli.js.map +2 -2
- package/dist/modules/query_index/lib/reindexer.js +23 -5
- package/dist/modules/query_index/lib/reindexer.js.map +2 -2
- package/dist/modules/sales/commands/configuration.js +30 -16
- package/dist/modules/sales/commands/configuration.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +36 -15
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +35 -6
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +85 -48
- package/src/modules/audit_logs/i18n/de.json +16 -0
- package/src/modules/audit_logs/i18n/en.json +16 -0
- package/src/modules/audit_logs/i18n/es.json +16 -0
- package/src/modules/audit_logs/i18n/ko.json +16 -0
- package/src/modules/audit_logs/i18n/pl.json +16 -0
- package/src/modules/audit_logs/lib/rowPresentation.ts +92 -0
- package/src/modules/catalog/commands/variants.ts +96 -17
- package/src/modules/catalog/seed/examples.ts +7 -0
- package/src/modules/customers/api/interactions/route.ts +2 -2
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +9 -12
- package/src/modules/customers/i18n/de.json +2 -1
- package/src/modules/customers/i18n/en.json +2 -1
- package/src/modules/customers/i18n/es.json +2 -1
- package/src/modules/customers/i18n/ko.json +2 -1
- package/src/modules/customers/i18n/pl.json +2 -1
- package/src/modules/customers/migrations/Migration20260825140000_customers.ts +21 -10
- package/src/modules/entities/cli.ts +144 -65
- package/src/modules/promotions/commands/codes.ts +19 -4
- package/src/modules/query_index/cli.ts +17 -1
- package/src/modules/query_index/lib/reindexer.ts +55 -6
- package/src/modules/sales/commands/configuration.ts +35 -16
- package/src/modules/sales/components/documents/AddressesSection.tsx +51 -20
- package/src/modules/sales/i18n/de.json +3 -0
- package/src/modules/sales/i18n/en.json +3 -0
- package/src/modules/sales/i18n/es.json +3 -0
- package/src/modules/sales/i18n/ko.json +3 -0
- package/src/modules/sales/i18n/pl.json +4 -1
- 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,
|
|
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) =>
|
|
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) =>
|
|
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.
|
|
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.
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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: '
|
|
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 {
|
|
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
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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}`)
|