@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
@@ -1,4 +1,4 @@
1
- [build:core] found 6298 entry points
1
+ [build:core] found 6308 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -4,6 +4,8 @@ import * as React from "react";
4
4
  import { extensionPoints } from "@open-mercato/core/modules/audit_logs/extension-points";
5
5
  import { DataTable } from "@open-mercato/ui/backend/DataTable";
6
6
  import { Button } from "@open-mercato/ui/primitives/button";
7
+ import { IconButton } from "@open-mercato/ui/primitives/icon-button";
8
+ import { auditRecordName, auditChangeSummary, auditExecutionState } from "../lib/rowPresentation.js";
7
9
  import { apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
8
10
  import { useT } from "@open-mercato/shared/lib/i18n/context";
9
11
  import { ActionLogDetailsDialog } from "./ActionLogDetailsDialog.js";
@@ -13,6 +15,9 @@ import { useAuditPermissions, canUndoEntry, canRedoEntry } from "@open-mercato/u
13
15
  import { Alert, AlertDescription } from "@open-mercato/ui/primitives/alert";
14
16
  import { createLogger } from "@open-mercato/shared/lib/logger";
15
17
  const logger = createLogger("audit_logs").child({ component: "AuditLogsActions" });
18
+ function AuditLogRowActions({ children }) {
19
+ return /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-1", "data-actions-cell": true, children });
20
+ }
16
21
  function resolveServerReason(err) {
17
22
  if (!(err instanceof Error)) return void 0;
18
23
  const status = err.status;
@@ -107,12 +112,27 @@ function AuditLogsActions({
107
112
  {
108
113
  accessorKey: "actionLabel",
109
114
  header: t("audit_logs.actions.columns.action"),
110
- cell: (info) => info.row.original.actionLabel || info.row.original.commandId
115
+ cell: (info) => {
116
+ const item = info.row.original;
117
+ const summary = auditChangeSummary(item, t);
118
+ return /* @__PURE__ */ jsxs("div", { className: "max-w-72", children: [
119
+ /* @__PURE__ */ jsx("div", { children: item.actionLabel || item.commandId }),
120
+ /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-muted-foreground", title: summary, children: summary })
121
+ ] });
122
+ }
111
123
  },
112
124
  {
113
125
  accessorKey: "resourceKind",
114
126
  header: t("audit_logs.actions.columns.resource"),
115
- cell: (info) => formatResource(info.row.original, noneLabel)
127
+ cell: (info) => {
128
+ const item = info.row.original;
129
+ const name = auditRecordName(item);
130
+ const resource = formatResource(item, noneLabel);
131
+ return /* @__PURE__ */ jsxs("div", { className: "max-w-72", children: [
132
+ name ? /* @__PURE__ */ jsx("div", { className: "truncate font-medium", title: name, children: name }) : null,
133
+ /* @__PURE__ */ jsx("div", { className: name ? "truncate text-xs text-muted-foreground" : "truncate", title: resource, children: resource })
134
+ ] });
135
+ }
116
136
  },
117
137
  {
118
138
  accessorKey: "actorUserId",
@@ -123,13 +143,13 @@ function AuditLogsActions({
123
143
  {
124
144
  accessorKey: "tenantId",
125
145
  header: t("audit_logs.actions.columns.tenant"),
126
- cell: (info) => info.row.original.tenantName || info.getValue() || noneLabel,
146
+ cell: (info) => info.row.original.tenantName || info.row.original.tenantId || noneLabel,
127
147
  meta: { priority: 4 }
128
148
  },
129
149
  {
130
150
  accessorKey: "organizationId",
131
151
  header: t("audit_logs.actions.columns.organization"),
132
- cell: (info) => info.row.original.organizationName || info.getValue() || noneLabel,
152
+ cell: (info) => info.row.original.organizationName || info.row.original.organizationId || noneLabel,
133
153
  meta: { priority: 4 }
134
154
  },
135
155
  {
@@ -139,55 +159,63 @@ function AuditLogsActions({
139
159
  },
140
160
  {
141
161
  accessorKey: "executionState",
142
- header: t("audit_logs.actions.columns.status")
143
- },
144
- {
145
- id: "controls",
146
- header: t("audit_logs.actions.columns.controls"),
147
- enableSorting: false,
148
- cell: (info) => {
149
- const item = info.row.original;
150
- const itemCanUndo = canUndoEntry(permissions, item.actorUserId);
151
- const itemCanRedo = canRedoEntry(permissions, item.actorUserId);
152
- const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === "done" && isLatestUndoableForItem(item);
153
- const showRedo = itemCanRedo && item.executionState === "undone";
154
- const canRedo = showRedo && isRedoCandidate(item);
155
- if (!canUndo && !showRedo) return null;
156
- return /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1", "data-actions-cell": true, children: [
157
- canUndo ? /* @__PURE__ */ jsx(
158
- Button,
159
- {
160
- variant: "ghost",
161
- size: "icon",
162
- "aria-label": t("audit_logs.actions.undo"),
163
- onClick: () => {
164
- void handleUndo(item.undoToken);
165
- },
166
- disabled: undoingToken === item.undoToken || Boolean(redoingId),
167
- children: /* @__PURE__ */ jsx(Undo2, { className: "size-4", "aria-hidden": "true" })
168
- }
169
- ) : null,
170
- showRedo ? /* @__PURE__ */ jsx(
171
- Button,
172
- {
173
- variant: "ghost",
174
- size: "icon",
175
- "aria-label": t("audit_logs.actions.redo"),
176
- onClick: () => {
177
- void handleRedo(item.id);
178
- },
179
- disabled: !canRedo || redoingId === item.id || Boolean(undoingToken),
180
- children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4", "aria-hidden": "true" })
181
- }
182
- ) : null
183
- ] });
184
- },
185
- meta: { align: "right" }
162
+ header: t("audit_logs.actions.columns.status"),
163
+ cell: (info) => auditExecutionState(info.row.original.executionState, t)
186
164
  }
187
- ], [t, noneLabel, handleUndo, handleRedo, isLatestUndoableForItem, isRedoCandidate, undoingToken, redoingId, permissions]);
165
+ ], [t, noneLabel]);
166
+ const rowActions = React.useCallback((item) => {
167
+ const itemCanUndo = canUndoEntry(permissions, item.actorUserId);
168
+ const itemCanRedo = canRedoEntry(permissions, item.actorUserId);
169
+ const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === "done" && isLatestUndoableForItem(item);
170
+ const showRedo = itemCanRedo && item.executionState === "undone";
171
+ const canRedo = showRedo && isRedoCandidate(item);
172
+ if (!canUndo && !showRedo) return null;
173
+ const undoDisabled = undoingToken === item.undoToken || Boolean(redoingId);
174
+ const redoDisabled = !canRedo || redoingId === item.id || Boolean(undoingToken);
175
+ const undoLabel = t("audit_logs.actions.undo_this");
176
+ const redoLabel = t("audit_logs.actions.redo_this");
177
+ const onUndo = () => {
178
+ void handleUndo(item.undoToken);
179
+ };
180
+ const onRedo = () => {
181
+ void handleRedo(item.id);
182
+ };
183
+ const items2 = [];
184
+ if (canUndo && !undoDisabled) items2.push({ id: "undo", label: undoLabel, onSelect: onUndo });
185
+ if (canRedo && !redoDisabled) items2.push({ id: "redo", label: redoLabel, onSelect: onRedo });
186
+ return /* @__PURE__ */ jsxs(AuditLogRowActions, { items: items2, children: [
187
+ canUndo ? /* @__PURE__ */ jsx(
188
+ IconButton,
189
+ {
190
+ variant: "ghost",
191
+ size: "lg",
192
+ type: "button",
193
+ "aria-label": undoLabel,
194
+ title: undoLabel,
195
+ onClick: onUndo,
196
+ disabled: undoDisabled,
197
+ children: /* @__PURE__ */ jsx(Undo2, { className: "size-4", "aria-hidden": "true" })
198
+ }
199
+ ) : null,
200
+ showRedo ? /* @__PURE__ */ jsx(
201
+ IconButton,
202
+ {
203
+ variant: "ghost",
204
+ size: "lg",
205
+ type: "button",
206
+ "aria-label": redoLabel,
207
+ title: redoLabel,
208
+ onClick: onRedo,
209
+ disabled: redoDisabled,
210
+ children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4", "aria-hidden": "true" })
211
+ }
212
+ ) : null
213
+ ] });
214
+ }, [t, handleUndo, handleRedo, isLatestUndoableForItem, isRedoCandidate, undoingToken, redoingId, permissions]);
188
215
  const undoButton = latestUndoable?.undoToken && canUndoEntry(permissions, latestUndoable.actorUserId) ? /* @__PURE__ */ jsx(
189
216
  Button,
190
217
  {
218
+ type: "button",
191
219
  variant: "secondary",
192
220
  size: "sm",
193
221
  onClick: () => {
@@ -211,6 +239,8 @@ function AuditLogsActions({
211
239
  titleHeadingLevel: 2,
212
240
  data: actionItems,
213
241
  columns,
242
+ rowActions,
243
+ stickyActionsColumn: true,
214
244
  actions: combinedActions,
215
245
  perspective: { tableId: extensionPoints.hosts.actionsTable.tableId },
216
246
  isLoading: Boolean(isLoading) || Boolean(undoingToken) || Boolean(redoingId),
@@ -242,7 +272,7 @@ function formatDate(value) {
242
272
  if (Number.isNaN(date.getTime())) return value;
243
273
  return new Intl.DateTimeFormat(void 0, {
244
274
  dateStyle: "medium",
245
- timeStyle: "short"
275
+ timeStyle: "medium"
246
276
  }).format(date);
247
277
  }
248
278
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/audit_logs/components/AuditLogsActions.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/audit_logs/extension-points'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable, type PaginationProps } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ActionLogDetailsDialog } from './ActionLogDetailsDialog'\nimport { Undo2, RotateCcw } from 'lucide-react'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from '@open-mercato/ui/backend/version-history'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'AuditLogsActions' })\n\n/**\n * `raiseCrudError` prefers the server's `error` body over the fallback message and stamps\n * `status` on anything it builds from an HTTP response, so a numeric `status` is what\n * distinguishes a route's refusal from a client-side failure. Without that check an\n * offline or aborted request would put a raw \"Failed to fetch\" into the operator's\n * banner; those keep the generic fallback instead.\n */\nfunction resolveServerReason(err: unknown): string | undefined {\n if (!(err instanceof Error)) return undefined\n const status = (err as Error & { status?: unknown }).status\n if (typeof status !== 'number') return undefined\n const message = err.message?.trim()\n return message ? message : undefined\n}\n\nexport type ActionLogItem = {\n id: string\n commandId: string\n actionLabel: string | null\n executionState: string\n actorUserId: string | null\n actorUserName: string | null\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n resourceKind: string | null\n resourceId: string | null\n undoToken: string | null\n createdAt: string\n updatedAt: string\n snapshotBefore?: unknown | null\n snapshotAfter?: unknown | null\n changes?: Record<string, unknown> | null\n context?: Record<string, unknown> | null\n}\n\nexport function AuditLogsActions({\n items,\n onRefresh,\n isLoading,\n headerExtras,\n onUndoError,\n onRedoError,\n pagination,\n}: {\n items: ActionLogItem[] | undefined\n onRefresh: () => Promise<void>\n isLoading?: boolean\n headerExtras?: React.ReactNode\n /** Receives the server's reason when it sent one, so the page can show why the undo was refused. */\n onUndoError?: (reason?: string) => void\n onRedoError?: (reason?: string) => void\n pagination?: PaginationProps\n}) {\n const t = useT()\n const permissions = useAuditPermissions(true)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const [selected, setSelected] = React.useState<ActionLogItem | null>(null)\n const actionItems = Array.isArray(items) ? items : []\n const latestUndoable = React.useMemo(() => actionItems.find((item) => !!item.undoToken && item.executionState === 'done'), [actionItems])\n const noneLabel = t('audit_logs.common.none')\n const latestPerResource = React.useMemo(() => {\n const map = new Map<string, string>()\n let fallback: string | null = null\n for (const item of actionItems) {\n if (!item.undoToken || item.executionState !== 'done') continue\n const key = buildResourceKey(item)\n if (key) {\n if (!map.has(key)) map.set(key, item.id)\n } else if (!fallback) {\n fallback = item.id\n }\n }\n return { map, fallback }\n }, [actionItems])\n const latestUndoneId = React.useMemo(() => {\n const undone = actionItems.filter((item) => item.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [actionItems])\n\n const isLatestUndoableForItem = React.useCallback((item: ActionLogItem) => {\n const key = buildResourceKey(item)\n if (key) return latestPerResource.map.get(key) === item.id\n return latestPerResource.fallback === item.id\n }, [latestPerResource])\n\n const isRedoCandidate = React.useCallback((item: ActionLogItem) => item.executionState === 'undone' && latestUndoneId === item.id, [latestUndoneId])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n await onRefresh()\n } catch (err) {\n logger.error('Undo action failed', { err })\n onUndoError?.(resolveServerReason(err))\n } finally {\n setUndoingToken(null)\n }\n }, [onRefresh, onUndoError, t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n await onRefresh()\n } catch (err) {\n logger.error('Redo action failed', { err })\n onRedoError?.(resolveServerReason(err))\n } finally {\n setRedoingId(null)\n }\n }, [onRefresh, onRedoError, t])\n\n const columns = React.useMemo<ColumnDef<ActionLogItem, any>[]>(() => [\n {\n accessorKey: 'actionLabel',\n header: t('audit_logs.actions.columns.action'),\n cell: (info) => info.row.original.actionLabel || info.row.original.commandId,\n },\n {\n accessorKey: 'resourceKind',\n header: t('audit_logs.actions.columns.resource'),\n cell: (info) => formatResource(info.row.original, noneLabel),\n },\n {\n accessorKey: 'actorUserId',\n header: t('audit_logs.actions.columns.user'),\n cell: (info) => info.row.original.actorUserName || noneLabel,\n meta: { priority: 3 },\n },\n {\n accessorKey: 'tenantId',\n header: t('audit_logs.actions.columns.tenant'),\n cell: (info) => info.row.original.tenantName || info.getValue() || noneLabel,\n meta: { priority: 4 },\n },\n {\n accessorKey: 'organizationId',\n header: t('audit_logs.actions.columns.organization'),\n cell: (info) => info.row.original.organizationName || info.getValue() || noneLabel,\n meta: { priority: 4 },\n },\n {\n accessorKey: 'createdAt',\n header: t('audit_logs.actions.columns.when'),\n cell: (info) => formatDate(info.getValue() as string),\n },\n {\n accessorKey: 'executionState',\n header: t('audit_logs.actions.columns.status'),\n },\n {\n id: 'controls',\n header: t('audit_logs.actions.columns.controls'),\n enableSorting: false,\n cell: (info) => {\n const item = info.row.original\n const itemCanUndo = canUndoEntry(permissions, item.actorUserId)\n const itemCanRedo = canRedoEntry(permissions, item.actorUserId)\n const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === 'done' && isLatestUndoableForItem(item)\n const showRedo = itemCanRedo && item.executionState === 'undone'\n const canRedo = showRedo && isRedoCandidate(item)\n if (!canUndo && !showRedo) return null\n return (\n <div className=\"flex justify-end gap-1\" data-actions-cell>\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={() => { void handleUndo(item.undoToken) }}\n disabled={undoingToken === item.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={() => { void handleRedo(item.id) }}\n disabled={!canRedo || redoingId === item.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n </Button>\n ) : null}\n </div>\n )\n },\n meta: { align: 'right' },\n },\n ], [t, noneLabel, handleUndo, handleRedo, isLatestUndoableForItem, isRedoCandidate, undoingToken, redoingId, permissions])\n\n const undoButton = latestUndoable?.undoToken && canUndoEntry(permissions, latestUndoable.actorUserId) ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => { void handleUndo(latestUndoable.undoToken) }}\n disabled={Boolean(undoingToken) || Boolean(redoingId)}\n >\n {undoingToken ? t('audit_logs.actions.undoing') : t('audit_logs.actions.undo')}\n </Button>\n ) : null\n\n const combinedActions = undoButton || headerExtras\n ? <div className=\"flex items-center gap-2\">{headerExtras}{undoButton}</div>\n : undefined\n\n const showSelfOnlyHint = !permissions.isLoading && !permissions.canViewTenant && !!permissions.currentUserId\n\n return (\n <>\n {showSelfOnlyHint ? (\n <Alert status=\"information\" className=\"mb-4\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n <DataTable<ActionLogItem>\n title={t('audit_logs.actions.title')}\n titleHeadingLevel={2}\n data={actionItems}\n columns={columns}\n actions={combinedActions}\n perspective={{ tableId: extensionPoints.hosts.actionsTable.tableId }}\n isLoading={Boolean(isLoading) || Boolean(undoingToken) || Boolean(redoingId)}\n onRowClick={(item) => setSelected(item)}\n pagination={pagination}\n />\n {selected ? (\n <ActionLogDetailsDialog\n item={selected}\n onClose={() => setSelected(null)}\n />\n ) : null}\n </>\n )\n}\n\nfunction formatResource(item: { resourceKind?: string | null; resourceId?: string | null }, fallback: string) {\n if (!item.resourceKind && !item.resourceId) return fallback\n return [item.resourceKind, item.resourceId].filter(Boolean).join(' \u00B7 ')\n}\n\nfunction buildResourceKey(item: { resourceKind?: string | null; resourceId?: string | null }) {\n const kind = typeof item.resourceKind === 'string' ? item.resourceKind.trim() : ''\n const id = typeof item.resourceId === 'string' ? item.resourceId.trim() : ''\n if (!kind && !id) return null\n return `${kind}::${id}`\n}\n\nfunction formatDate(value: string) {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return value\n return new Intl.DateTimeFormat(undefined, {\n dateStyle: 'medium',\n timeStyle: 'short',\n }).format(date)\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/audit_logs/extension-points'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable, type PaginationProps } from '@open-mercato/ui/backend/DataTable'\nimport type { RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { auditRecordName, auditChangeSummary, auditExecutionState } from '../lib/rowPresentation'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ActionLogDetailsDialog } from './ActionLogDetailsDialog'\nimport { Undo2, RotateCcw } from 'lucide-react'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from '@open-mercato/ui/backend/version-history'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'AuditLogsActions' })\n\nfunction AuditLogRowActions({ children }: { items: RowActionItem[]; children: React.ReactNode }) {\n return <div className=\"flex justify-end gap-1\" data-actions-cell>{children}</div>\n}\n\n/**\n * `raiseCrudError` prefers the server's `error` body over the fallback message and stamps\n * `status` on anything it builds from an HTTP response, so a numeric `status` is what\n * distinguishes a route's refusal from a client-side failure. Without that check an\n * offline or aborted request would put a raw \"Failed to fetch\" into the operator's\n * banner; those keep the generic fallback instead.\n */\nfunction resolveServerReason(err: unknown): string | undefined {\n if (!(err instanceof Error)) return undefined\n const status = (err as Error & { status?: unknown }).status\n if (typeof status !== 'number') return undefined\n const message = err.message?.trim()\n return message ? message : undefined\n}\n\nexport type ActionLogItem = {\n id: string\n commandId: string\n actionLabel: string | null\n executionState: string\n actorUserId: string | null\n actorUserName: string | null\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n resourceKind: string | null\n resourceId: string | null\n undoToken: string | null\n createdAt: string\n updatedAt: string\n snapshotBefore?: unknown | null\n snapshotAfter?: unknown | null\n changes?: Record<string, unknown> | null\n context?: Record<string, unknown> | null\n}\n\nexport function AuditLogsActions({\n items,\n onRefresh,\n isLoading,\n headerExtras,\n onUndoError,\n onRedoError,\n pagination,\n}: {\n items: ActionLogItem[] | undefined\n onRefresh: () => Promise<void>\n isLoading?: boolean\n headerExtras?: React.ReactNode\n /** Receives the server's reason when it sent one, so the page can show why the undo was refused. */\n onUndoError?: (reason?: string) => void\n onRedoError?: (reason?: string) => void\n pagination?: PaginationProps\n}) {\n const t = useT()\n const permissions = useAuditPermissions(true)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const [selected, setSelected] = React.useState<ActionLogItem | null>(null)\n const actionItems = Array.isArray(items) ? items : []\n const latestUndoable = React.useMemo(() => actionItems.find((item) => !!item.undoToken && item.executionState === 'done'), [actionItems])\n const noneLabel = t('audit_logs.common.none')\n const latestPerResource = React.useMemo(() => {\n const map = new Map<string, string>()\n let fallback: string | null = null\n for (const item of actionItems) {\n if (!item.undoToken || item.executionState !== 'done') continue\n const key = buildResourceKey(item)\n if (key) {\n if (!map.has(key)) map.set(key, item.id)\n } else if (!fallback) {\n fallback = item.id\n }\n }\n return { map, fallback }\n }, [actionItems])\n const latestUndoneId = React.useMemo(() => {\n const undone = actionItems.filter((item) => item.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [actionItems])\n\n const isLatestUndoableForItem = React.useCallback((item: ActionLogItem) => {\n const key = buildResourceKey(item)\n if (key) return latestPerResource.map.get(key) === item.id\n return latestPerResource.fallback === item.id\n }, [latestPerResource])\n\n const isRedoCandidate = React.useCallback((item: ActionLogItem) => item.executionState === 'undone' && latestUndoneId === item.id, [latestUndoneId])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n await onRefresh()\n } catch (err) {\n logger.error('Undo action failed', { err })\n onUndoError?.(resolveServerReason(err))\n } finally {\n setUndoingToken(null)\n }\n }, [onRefresh, onUndoError, t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n await onRefresh()\n } catch (err) {\n logger.error('Redo action failed', { err })\n onRedoError?.(resolveServerReason(err))\n } finally {\n setRedoingId(null)\n }\n }, [onRefresh, onRedoError, t])\n\n const columns = React.useMemo<ColumnDef<ActionLogItem, unknown>[]>(() => [\n {\n accessorKey: 'actionLabel',\n header: t('audit_logs.actions.columns.action'),\n cell: (info) => {\n const item = info.row.original\n const summary = auditChangeSummary(item, t)\n return (\n <div className=\"max-w-72\">\n <div>{item.actionLabel || item.commandId}</div>\n <div className=\"truncate text-xs text-muted-foreground\" title={summary}>{summary}</div>\n </div>\n )\n },\n },\n {\n accessorKey: 'resourceKind',\n header: t('audit_logs.actions.columns.resource'),\n cell: (info) => {\n const item = info.row.original\n const name = auditRecordName(item)\n const resource = formatResource(item, noneLabel)\n return (\n <div className=\"max-w-72\">\n {name ? <div className=\"truncate font-medium\" title={name}>{name}</div> : null}\n <div className={name ? 'truncate text-xs text-muted-foreground' : 'truncate'} title={resource}>{resource}</div>\n </div>\n )\n },\n },\n {\n accessorKey: 'actorUserId',\n header: t('audit_logs.actions.columns.user'),\n cell: (info) => info.row.original.actorUserName || noneLabel,\n meta: { priority: 3 },\n },\n {\n accessorKey: 'tenantId',\n header: t('audit_logs.actions.columns.tenant'),\n cell: (info) => info.row.original.tenantName || info.row.original.tenantId || noneLabel,\n meta: { priority: 4 },\n },\n {\n accessorKey: 'organizationId',\n header: t('audit_logs.actions.columns.organization'),\n cell: (info) => info.row.original.organizationName || info.row.original.organizationId || noneLabel,\n meta: { priority: 4 },\n },\n {\n accessorKey: 'createdAt',\n header: t('audit_logs.actions.columns.when'),\n cell: (info) => formatDate(info.getValue() as string),\n },\n {\n accessorKey: 'executionState',\n header: t('audit_logs.actions.columns.status'),\n cell: (info) => auditExecutionState(info.row.original.executionState, t),\n },\n ], [t, noneLabel])\n\n const rowActions = React.useCallback((item: ActionLogItem) => {\n const itemCanUndo = canUndoEntry(permissions, item.actorUserId)\n const itemCanRedo = canRedoEntry(permissions, item.actorUserId)\n const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === 'done' && isLatestUndoableForItem(item)\n const showRedo = itemCanRedo && item.executionState === 'undone'\n const canRedo = showRedo && isRedoCandidate(item)\n if (!canUndo && !showRedo) return null\n const undoDisabled = undoingToken === item.undoToken || Boolean(redoingId)\n const redoDisabled = !canRedo || redoingId === item.id || Boolean(undoingToken)\n const undoLabel = t('audit_logs.actions.undo_this')\n const redoLabel = t('audit_logs.actions.redo_this')\n const onUndo = () => { void handleUndo(item.undoToken) }\n const onRedo = () => { void handleRedo(item.id) }\n const items: RowActionItem[] = []\n if (canUndo && !undoDisabled) items.push({ id: 'undo', label: undoLabel, onSelect: onUndo })\n if (canRedo && !redoDisabled) items.push({ id: 'redo', label: redoLabel, onSelect: onRedo })\n return (\n <AuditLogRowActions items={items}>\n {canUndo ? (\n <IconButton\n variant=\"ghost\"\n size=\"lg\"\n type=\"button\"\n aria-label={undoLabel}\n title={undoLabel}\n onClick={onUndo}\n disabled={undoDisabled}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n </IconButton>\n ) : null}\n {showRedo ? (\n <IconButton\n variant=\"ghost\"\n size=\"lg\"\n type=\"button\"\n aria-label={redoLabel}\n title={redoLabel}\n onClick={onRedo}\n disabled={redoDisabled}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n </IconButton>\n ) : null}\n </AuditLogRowActions>\n )\n }, [t, handleUndo, handleRedo, isLatestUndoableForItem, isRedoCandidate, undoingToken, redoingId, permissions])\n\n const undoButton = latestUndoable?.undoToken && canUndoEntry(permissions, latestUndoable.actorUserId) ? (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => { void handleUndo(latestUndoable.undoToken) }}\n disabled={Boolean(undoingToken) || Boolean(redoingId)}\n >\n {undoingToken ? t('audit_logs.actions.undoing') : t('audit_logs.actions.undo')}\n </Button>\n ) : null\n\n const combinedActions = undoButton || headerExtras\n ? <div className=\"flex items-center gap-2\">{headerExtras}{undoButton}</div>\n : undefined\n\n const showSelfOnlyHint = !permissions.isLoading && !permissions.canViewTenant && !!permissions.currentUserId\n\n return (\n <>\n {showSelfOnlyHint ? (\n <Alert status=\"information\" className=\"mb-4\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n <DataTable<ActionLogItem>\n title={t('audit_logs.actions.title')}\n titleHeadingLevel={2}\n data={actionItems}\n columns={columns}\n rowActions={rowActions}\n stickyActionsColumn\n actions={combinedActions}\n perspective={{ tableId: extensionPoints.hosts.actionsTable.tableId }}\n isLoading={Boolean(isLoading) || Boolean(undoingToken) || Boolean(redoingId)}\n onRowClick={(item) => setSelected(item)}\n pagination={pagination}\n />\n {selected ? (\n <ActionLogDetailsDialog\n item={selected}\n onClose={() => setSelected(null)}\n />\n ) : null}\n </>\n )\n}\n\nfunction formatResource(item: { resourceKind?: string | null; resourceId?: string | null }, fallback: string) {\n if (!item.resourceKind && !item.resourceId) return fallback\n return [item.resourceKind, item.resourceId].filter(Boolean).join(' \u00B7 ')\n}\n\nfunction buildResourceKey(item: { resourceKind?: string | null; resourceId?: string | null }) {\n const kind = typeof item.resourceKind === 'string' ? item.resourceKind.trim() : ''\n const id = typeof item.resourceId === 'string' ? item.resourceId.trim() : ''\n if (!kind && !id) return null\n return `${kind}::${id}`\n}\n\nfunction formatDate(value: string) {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return value\n return new Intl.DateTimeFormat(undefined, {\n dateStyle: 'medium',\n timeStyle: 'medium',\n }).format(date)\n}\n"],
5
+ "mappings": ";AAsBS,SAwQL,UAxQK,KAiJC,YAjJD;AApBT,YAAY,WAAW;AACvB,SAAS,uBAAuB;AAEhC,SAAS,iBAAuC;AAEhD,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB,oBAAoB,2BAA2B;AACzE,SAAS,sBAAsB;AAC/B,SAAS,YAAY;AACrB,SAAS,8BAA8B;AACvC,SAAS,OAAO,iBAAiB;AACjC,SAAS,kBAAkB,uBAAuB;AAClD,SAAS,qBAAqB,cAAc,oBAAoB;AAChE,SAAS,OAAO,wBAAwB;AACxC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,YAAY,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAEjF,SAAS,mBAAmB,EAAE,SAAS,GAA0D;AAC/F,SAAO,oBAAC,SAAI,WAAU,0BAAyB,qBAAiB,MAAE,UAAS;AAC7E;AASA,SAAS,oBAAoB,KAAkC;AAC7D,MAAI,EAAE,eAAe,OAAQ,QAAO;AACpC,QAAM,SAAU,IAAqC;AACrD,MAAI,OAAO,WAAW,SAAU,QAAO;AACvC,QAAM,UAAU,IAAI,SAAS,KAAK;AAClC,SAAO,UAAU,UAAU;AAC7B;AAwBO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASG;AACD,QAAM,IAAI,KAAK;AACf,QAAM,cAAc,oBAAoB,IAAI;AAC5C,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAwB,IAAI;AAC1E,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAA+B,IAAI;AACzE,QAAM,cAAc,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC;AACpD,QAAM,iBAAiB,MAAM,QAAQ,MAAM,YAAY,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,aAAa,KAAK,mBAAmB,MAAM,GAAG,CAAC,WAAW,CAAC;AACxI,QAAM,YAAY,EAAE,wBAAwB;AAC5C,QAAM,oBAAoB,MAAM,QAAQ,MAAM;AAC5C,UAAM,MAAM,oBAAI,IAAoB;AACpC,QAAI,WAA0B;AAC9B,eAAW,QAAQ,aAAa;AAC9B,UAAI,CAAC,KAAK,aAAa,KAAK,mBAAmB,OAAQ;AACvD,YAAM,MAAM,iBAAiB,IAAI;AACjC,UAAI,KAAK;AACP,YAAI,CAAC,IAAI,IAAI,GAAG,EAAG,KAAI,IAAI,KAAK,KAAK,EAAE;AAAA,MACzC,WAAW,CAAC,UAAU;AACpB,mBAAW,KAAK;AAAA,MAClB;AAAA,IACF;AACA,WAAO,EAAE,KAAK,SAAS;AAAA,EACzB,GAAG,CAAC,WAAW,CAAC;AAChB,QAAM,iBAAiB,MAAM,QAAQ,MAAM;AACzC,UAAM,SAAS,YAAY,OAAO,CAAC,SAAS,KAAK,mBAAmB,QAAQ;AAC5E,QAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,UAAM,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM;AACxC,YAAM,MAAM,KAAK,MAAM,EAAE,SAAS;AAClC,YAAM,MAAM,KAAK,MAAM,EAAE,SAAS;AAClC,cAAQ,OAAO,SAAS,GAAG,IAAI,MAAM,MAAM,OAAO,SAAS,GAAG,IAAI,MAAM;AAAA,IAC1E,CAAC;AACD,WAAO,OAAO,CAAC,GAAG,MAAM;AAAA,EAC1B,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,0BAA0B,MAAM,YAAY,CAAC,SAAwB;AACzE,UAAM,MAAM,iBAAiB,IAAI;AACjC,QAAI,IAAK,QAAO,kBAAkB,IAAI,IAAI,GAAG,MAAM,KAAK;AACxD,WAAO,kBAAkB,aAAa,KAAK;AAAA,EAC7C,GAAG,CAAC,iBAAiB,CAAC;AAEtB,QAAM,kBAAkB,MAAM,YAAY,CAAC,SAAwB,KAAK,mBAAmB,YAAY,mBAAmB,KAAK,IAAI,CAAC,cAAc,CAAC;AAEnJ,QAAM,aAAa,MAAM,YAAY,OAAO,UAAyB;AACnE,QAAI,CAAC,MAAO;AACZ,oBAAgB,KAAK;AACrB,QAAI;AACF,YAAM,eAAe,2CAA2C;AAAA,QAC9D,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,WAAW,MAAM,CAAC;AAAA,MAC3C,GAAG,EAAE,cAAc,EAAE,uBAAuB,EAAE,CAAC;AAC/C,sBAAgB,KAAK;AACrB,YAAM,UAAU;AAAA,IAClB,SAAS,KAAK;AACZ,aAAO,MAAM,sBAAsB,EAAE,IAAI,CAAC;AAC1C,oBAAc,oBAAoB,GAAG,CAAC;AAAA,IACxC,UAAE;AACA,sBAAgB,IAAI;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,WAAW,aAAa,CAAC,CAAC;AAE9B,QAAM,aAAa,MAAM,YAAY,OAAO,UAAyB;AACnE,QAAI,CAAC,MAAO;AACZ,iBAAa,KAAK;AAClB,QAAI;AACF,YAAM,eAAe,2CAA2C;AAAA,QAC9D,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,MAAM,CAAC;AAAA,MAChC,GAAG,EAAE,cAAc,EAAE,uBAAuB,EAAE,CAAC;AAC/C,uBAAiB,KAAK;AACtB,YAAM,UAAU;AAAA,IAClB,SAAS,KAAK;AACZ,aAAO,MAAM,sBAAsB,EAAE,IAAI,CAAC;AAC1C,oBAAc,oBAAoB,GAAG,CAAC;AAAA,IACxC,UAAE;AACA,mBAAa,IAAI;AAAA,IACnB;AAAA,EACF,GAAG,CAAC,WAAW,aAAa,CAAC,CAAC;AAE9B,QAAM,UAAU,MAAM,QAA6C,MAAM;AAAA,IACvE;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,mCAAmC;AAAA,MAC7C,MAAM,CAAC,SAAS;AACd,cAAM,OAAO,KAAK,IAAI;AACtB,cAAM,UAAU,mBAAmB,MAAM,CAAC;AAC1C,eACE,qBAAC,SAAI,WAAU,YACb;AAAA,8BAAC,SAAK,eAAK,eAAe,KAAK,WAAU;AAAA,UACzC,oBAAC,SAAI,WAAU,0CAAyC,OAAO,SAAU,mBAAQ;AAAA,WACnF;AAAA,MAEJ;AAAA,IACF;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,qCAAqC;AAAA,MAC/C,MAAM,CAAC,SAAS;AACd,cAAM,OAAO,KAAK,IAAI;AACtB,cAAM,OAAO,gBAAgB,IAAI;AACjC,cAAM,WAAW,eAAe,MAAM,SAAS;AAC/C,eACE,qBAAC,SAAI,WAAU,YACZ;AAAA,iBAAO,oBAAC,SAAI,WAAU,wBAAuB,OAAO,MAAO,gBAAK,IAAS;AAAA,UAC1E,oBAAC,SAAI,WAAW,OAAO,2CAA2C,YAAY,OAAO,UAAW,oBAAS;AAAA,WAC3G;AAAA,MAEJ;AAAA,IACF;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,iCAAiC;AAAA,MAC3C,MAAM,CAAC,SAAS,KAAK,IAAI,SAAS,iBAAiB;AAAA,MACnD,MAAM,EAAE,UAAU,EAAE;AAAA,IACtB;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,mCAAmC;AAAA,MAC7C,MAAM,CAAC,SAAS,KAAK,IAAI,SAAS,cAAc,KAAK,IAAI,SAAS,YAAY;AAAA,MAC9E,MAAM,EAAE,UAAU,EAAE;AAAA,IACtB;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,yCAAyC;AAAA,MACnD,MAAM,CAAC,SAAS,KAAK,IAAI,SAAS,oBAAoB,KAAK,IAAI,SAAS,kBAAkB;AAAA,MAC1F,MAAM,EAAE,UAAU,EAAE;AAAA,IACtB;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,iCAAiC;AAAA,MAC3C,MAAM,CAAC,SAAS,WAAW,KAAK,SAAS,CAAW;AAAA,IACtD;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,mCAAmC;AAAA,MAC7C,MAAM,CAAC,SAAS,oBAAoB,KAAK,IAAI,SAAS,gBAAgB,CAAC;AAAA,IACzE;AAAA,EACF,GAAG,CAAC,GAAG,SAAS,CAAC;AAEjB,QAAM,aAAa,MAAM,YAAY,CAAC,SAAwB;AAC5D,UAAM,cAAc,aAAa,aAAa,KAAK,WAAW;AAC9D,UAAM,cAAc,aAAa,aAAa,KAAK,WAAW;AAC9D,UAAM,UAAU,eAAe,QAAQ,KAAK,SAAS,KAAK,KAAK,mBAAmB,UAAU,wBAAwB,IAAI;AACxH,UAAM,WAAW,eAAe,KAAK,mBAAmB;AACxD,UAAM,UAAU,YAAY,gBAAgB,IAAI;AAChD,QAAI,CAAC,WAAW,CAAC,SAAU,QAAO;AAClC,UAAM,eAAe,iBAAiB,KAAK,aAAa,QAAQ,SAAS;AACzE,UAAM,eAAe,CAAC,WAAW,cAAc,KAAK,MAAM,QAAQ,YAAY;AAC9E,UAAM,YAAY,EAAE,8BAA8B;AAClD,UAAM,YAAY,EAAE,8BAA8B;AAClD,UAAM,SAAS,MAAM;AAAE,WAAK,WAAW,KAAK,SAAS;AAAA,IAAE;AACvD,UAAM,SAAS,MAAM;AAAE,WAAK,WAAW,KAAK,EAAE;AAAA,IAAE;AAChD,UAAMA,SAAyB,CAAC;AAChC,QAAI,WAAW,CAAC,aAAc,CAAAA,OAAM,KAAK,EAAE,IAAI,QAAQ,OAAO,WAAW,UAAU,OAAO,CAAC;AAC3F,QAAI,WAAW,CAAC,aAAc,CAAAA,OAAM,KAAK,EAAE,IAAI,QAAQ,OAAO,WAAW,UAAU,OAAO,CAAC;AAC3F,WACE,qBAAC,sBAAmB,OAAOA,QACxB;AAAA,gBACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,MAAK;AAAA,UACL,cAAY;AAAA,UACZ,OAAO;AAAA,UACP,SAAS;AAAA,UACT,UAAU;AAAA,UAEV,8BAAC,SAAM,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,MAC/C,IACE;AAAA,MACH,WACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,MAAK;AAAA,UACL,cAAY;AAAA,UACZ,OAAO;AAAA,UACP,SAAS;AAAA,UACT,UAAU;AAAA,UAEV,8BAAC,aAAU,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,MACnD,IACE;AAAA,OACN;AAAA,EAEJ,GAAG,CAAC,GAAG,YAAY,YAAY,yBAAyB,iBAAiB,cAAc,WAAW,WAAW,CAAC;AAE9G,QAAM,aAAa,gBAAgB,aAAa,aAAa,aAAa,eAAe,WAAW,IAClG;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,SAAS,MAAM;AAAE,aAAK,WAAW,eAAe,SAAS;AAAA,MAAE;AAAA,MAC3D,UAAU,QAAQ,YAAY,KAAK,QAAQ,SAAS;AAAA,MAEnD,yBAAe,EAAE,4BAA4B,IAAI,EAAE,yBAAyB;AAAA;AAAA,EAC/E,IACE;AAEJ,QAAM,kBAAkB,cAAc,eAClC,qBAAC,SAAI,WAAU,2BAA2B;AAAA;AAAA,IAAc;AAAA,KAAW,IACnE;AAEJ,QAAM,mBAAmB,CAAC,YAAY,aAAa,CAAC,YAAY,iBAAiB,CAAC,CAAC,YAAY;AAE/F,SACE,iCACG;AAAA,uBACC,oBAAC,SAAM,QAAO,eAAc,WAAU,QACpC,8BAAC,oBACE,YAAE,kCAAkC,6EAA6E,GACpH,GACF,IACE;AAAA,IACJ;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,0BAA0B;AAAA,QACnC,mBAAmB;AAAA,QACnB,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,qBAAmB;AAAA,QACnB,SAAS;AAAA,QACT,aAAa,EAAE,SAAS,gBAAgB,MAAM,aAAa,QAAQ;AAAA,QACnE,WAAW,QAAQ,SAAS,KAAK,QAAQ,YAAY,KAAK,QAAQ,SAAS;AAAA,QAC3E,YAAY,CAAC,SAAS,YAAY,IAAI;AAAA,QACtC;AAAA;AAAA,IACF;AAAA,IACC,WACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,SAAS,MAAM,YAAY,IAAI;AAAA;AAAA,IACjC,IACE;AAAA,KACN;AAEJ;AAEA,SAAS,eAAe,MAAoE,UAAkB;AAC5G,MAAI,CAAC,KAAK,gBAAgB,CAAC,KAAK,WAAY,QAAO;AACnD,SAAO,CAAC,KAAK,cAAc,KAAK,UAAU,EAAE,OAAO,OAAO,EAAE,KAAK,QAAK;AACxE;AAEA,SAAS,iBAAiB,MAAoE;AAC5F,QAAM,OAAO,OAAO,KAAK,iBAAiB,WAAW,KAAK,aAAa,KAAK,IAAI;AAChF,QAAM,KAAK,OAAO,KAAK,eAAe,WAAW,KAAK,WAAW,KAAK,IAAI;AAC1E,MAAI,CAAC,QAAQ,CAAC,GAAI,QAAO;AACzB,SAAO,GAAG,IAAI,KAAK,EAAE;AACvB;AAEA,SAAS,WAAW,OAAe;AACjC,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AACzC,SAAO,IAAI,KAAK,eAAe,QAAW;AAAA,IACxC,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC,EAAE,OAAO,IAAI;AAChB;",
6
+ "names": ["items"]
7
7
  }
@@ -0,0 +1,82 @@
1
+ import { isRecord } from "./changeRows.js";
2
+ const previewFields = [
3
+ ["displayName", "audit_logs.summary.name"],
4
+ ["display_name", "audit_logs.summary.name"],
5
+ ["name", "audit_logs.summary.name"],
6
+ ["title", "audit_logs.summary.title"],
7
+ ["label", "audit_logs.summary.label"],
8
+ ["parentId", "audit_logs.summary.parent"],
9
+ ["parent_id", "audit_logs.summary.parent"],
10
+ ["status", "audit_logs.summary.status"],
11
+ ["isActive", "audit_logs.summary.active"],
12
+ ["is_active", "audit_logs.summary.active"],
13
+ ["slug", "audit_logs.summary.slug"]
14
+ ];
15
+ function previewText(value) {
16
+ if (typeof value !== "string") return void 0;
17
+ const text = value.replace(/\s+/g, " ").trim();
18
+ if (!text || text.endsWith(":v1")) return void 0;
19
+ return text.length > 120 ? `${text.slice(0, 119)}\u2026` : text;
20
+ }
21
+ function snapshotRecord(snapshot) {
22
+ if (!isRecord(snapshot)) return void 0;
23
+ return isRecord(snapshot.view) ? snapshot.view : snapshot;
24
+ }
25
+ function auditRecordName(item) {
26
+ for (const snapshot of [item.snapshotAfter, item.snapshotBefore]) {
27
+ const record = snapshotRecord(snapshot);
28
+ if (!record) continue;
29
+ for (const field of ["displayName", "display_name", "name", "title", "label"]) {
30
+ const name = previewText(record[field]);
31
+ if (name) return name;
32
+ }
33
+ }
34
+ return void 0;
35
+ }
36
+ function summaryValue(value, field, t) {
37
+ if (value === null) {
38
+ return t(field === "parentId" || field === "parent_id" ? "audit_logs.summary.root" : "audit_logs.common.none");
39
+ }
40
+ if (typeof value === "boolean") return t(value ? "audit_logs.summary.yes" : "audit_logs.summary.no");
41
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
42
+ return previewText(value);
43
+ }
44
+ function auditChangeSummary(item, t) {
45
+ if (!isRecord(item.changes)) return t("audit_logs.actions.details.no_changes");
46
+ const count = Object.keys(item.changes).length;
47
+ if (!count) return t("audit_logs.actions.details.no_changes");
48
+ for (const [field, labelKey] of previewFields) {
49
+ if (!Object.hasOwn(item.changes, field)) continue;
50
+ const change = item.changes[field];
51
+ const before = snapshotRecord(item.snapshotBefore)?.[field];
52
+ const from = isRecord(change) ? Object.hasOwn(change, "from") ? change.from : Object.hasOwn(change, "old") ? change.old : before : before;
53
+ const to = isRecord(change) ? Object.hasOwn(change, "to") ? change.to : change.new : change;
54
+ const fromText = summaryValue(from, field, t);
55
+ const toText = summaryValue(to, field, t);
56
+ if (fromText === void 0 || toText === void 0) continue;
57
+ const fieldLabel = labelKey === "audit_logs.summary.parent" && item.resourceKind === "directory.organization" ? t("audit_logs.summary.parent_organization") : t(labelKey);
58
+ const summary = t("audit_logs.summary.change", { field: fieldLabel, from: fromText, to: toText });
59
+ return count > 1 ? t("audit_logs.summary.more", { summary, count: count - 1 }) : summary;
60
+ }
61
+ return t("audit_logs.summary.count", { count });
62
+ }
63
+ function auditExecutionState(state, t) {
64
+ switch (state) {
65
+ case "done":
66
+ return t("audit_logs.version_history.status.done");
67
+ case "undone":
68
+ return t("audit_logs.version_history.status.undone");
69
+ case "failed":
70
+ return t("audit_logs.version_history.status.failed");
71
+ case "redone":
72
+ return t("audit_logs.version_history.status.redone");
73
+ default:
74
+ return state;
75
+ }
76
+ }
77
+ export {
78
+ auditChangeSummary,
79
+ auditExecutionState,
80
+ auditRecordName
81
+ };
82
+ //# sourceMappingURL=rowPresentation.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/audit_logs/lib/rowPresentation.ts"],
4
+ "sourcesContent": ["import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { isRecord } from './changeRows'\n\ntype AuditRow = {\n resourceKind?: string | null\n snapshotBefore?: unknown\n snapshotAfter?: unknown\n changes?: Record<string, unknown> | null\n}\n\nconst previewFields = [\n ['displayName', 'audit_logs.summary.name'],\n ['display_name', 'audit_logs.summary.name'],\n ['name', 'audit_logs.summary.name'],\n ['title', 'audit_logs.summary.title'],\n ['label', 'audit_logs.summary.label'],\n ['parentId', 'audit_logs.summary.parent'],\n ['parent_id', 'audit_logs.summary.parent'],\n ['status', 'audit_logs.summary.status'],\n ['isActive', 'audit_logs.summary.active'],\n ['is_active', 'audit_logs.summary.active'],\n ['slug', 'audit_logs.summary.slug'],\n] as const\n\nfunction previewText(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const text = value.replace(/\\s+/g, ' ').trim()\n if (!text || text.endsWith(':v1')) return undefined\n return text.length > 120 ? `${text.slice(0, 119)}\u2026` : text\n}\n\nfunction snapshotRecord(snapshot: unknown): Record<string, unknown> | undefined {\n if (!isRecord(snapshot)) return undefined\n return isRecord(snapshot.view) ? snapshot.view : snapshot\n}\n\nexport function auditRecordName(item: AuditRow): string | undefined {\n for (const snapshot of [item.snapshotAfter, item.snapshotBefore]) {\n const record = snapshotRecord(snapshot)\n if (!record) continue\n for (const field of ['displayName', 'display_name', 'name', 'title', 'label']) {\n const name = previewText(record[field])\n if (name) return name\n }\n }\n return undefined\n}\n\nfunction summaryValue(value: unknown, field: string, t: TranslateFn): string | undefined {\n if (value === null) {\n return t(field === 'parentId' || field === 'parent_id' ? 'audit_logs.summary.root' : 'audit_logs.common.none')\n }\n if (typeof value === 'boolean') return t(value ? 'audit_logs.summary.yes' : 'audit_logs.summary.no')\n if (typeof value === 'number' && Number.isFinite(value)) return String(value)\n return previewText(value)\n}\n\nexport function auditChangeSummary(item: AuditRow, t: TranslateFn): string {\n if (!isRecord(item.changes)) return t('audit_logs.actions.details.no_changes')\n const count = Object.keys(item.changes).length\n if (!count) return t('audit_logs.actions.details.no_changes')\n for (const [field, labelKey] of previewFields) {\n if (!Object.hasOwn(item.changes, field)) continue\n const change = item.changes[field]\n const before = snapshotRecord(item.snapshotBefore)?.[field]\n const from = isRecord(change)\n ? Object.hasOwn(change, 'from') ? change.from : Object.hasOwn(change, 'old') ? change.old : before\n : before\n const to = isRecord(change)\n ? Object.hasOwn(change, 'to') ? change.to : change.new\n : change\n const fromText = summaryValue(from, field, t)\n const toText = summaryValue(to, field, t)\n if (fromText === undefined || toText === undefined) continue\n const fieldLabel = labelKey === 'audit_logs.summary.parent' && item.resourceKind === 'directory.organization'\n ? t('audit_logs.summary.parent_organization')\n : t(labelKey)\n const summary = t('audit_logs.summary.change', { field: fieldLabel, from: fromText, to: toText })\n return count > 1 ? t('audit_logs.summary.more', { summary, count: count - 1 }) : summary\n }\n return t('audit_logs.summary.count', { count })\n}\n\nexport function auditExecutionState(state: string, t: TranslateFn): string {\n switch (state) {\n case 'done': return t('audit_logs.version_history.status.done')\n case 'undone': return t('audit_logs.version_history.status.undone')\n case 'failed': return t('audit_logs.version_history.status.failed')\n case 'redone': return t('audit_logs.version_history.status.redone')\n default: return state\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,7 +1,15 @@
1
1
  import { registerCommand } from "@open-mercato/shared/lib/commands";
2
- import { buildChanges, requireId, parseWithCustomFields, setCustomFieldsIfAny, emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
3
- import { UniqueConstraintViolationException } from "@mikro-orm/core";
2
+ import {
3
+ buildChanges,
4
+ requireId,
5
+ parseWithCustomFields,
6
+ setCustomFieldsIfAny,
7
+ emitCrudSideEffects,
8
+ emitCrudUndoSideEffects
9
+ } from "@open-mercato/shared/lib/commands/helpers";
10
+ import { LockMode, UniqueConstraintViolationException } from "@mikro-orm/core";
4
11
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
12
+ import { invalidateCrudCache } from "@open-mercato/shared/lib/crud/cache";
5
13
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
6
14
  import { loadCustomFieldSnapshot, buildCustomFieldResetMap } from "@open-mercato/shared/lib/commands/customFieldSnapshots";
7
15
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
@@ -48,6 +56,19 @@ const variantCrudEvents = {
48
56
  tenantId: ctx.identifiers.tenantId
49
57
  })
50
58
  };
59
+ const variantPriceCrudEvents = {
60
+ module: "catalog",
61
+ entity: "price",
62
+ persistent: true,
63
+ buildPayload: (ctx) => ({
64
+ id: ctx.identifiers.id,
65
+ organizationId: ctx.identifiers.organizationId,
66
+ tenantId: ctx.identifiers.tenantId
67
+ })
68
+ };
69
+ const variantPriceCrudIndexer = {
70
+ entityType: E.catalog.catalog_product_price
71
+ };
51
72
  const VARIANT_CHANGE_KEYS = [
52
73
  "name",
53
74
  "sku",
@@ -241,6 +262,20 @@ async function loadVariantPriceSnapshots(em, variantId, scope) {
241
262
  }
242
263
  return snapshots;
243
264
  }
265
+ async function resetVariantPriceCustomFields(dataEngine, snapshots) {
266
+ for (const snapshot of snapshots) {
267
+ const resetValues = buildCustomFieldResetMap(snapshot.custom ?? void 0, void 0);
268
+ if (!Object.keys(resetValues).length) continue;
269
+ await setCustomFieldsIfAny({
270
+ dataEngine,
271
+ entityId: E.catalog.catalog_product_price,
272
+ recordId: snapshot.id,
273
+ organizationId: snapshot.organizationId,
274
+ tenantId: snapshot.tenantId,
275
+ values: resetValues
276
+ });
277
+ }
278
+ }
244
279
  async function restoreVariantPricesFromSnapshots(em, variant, snapshots, dataEngine) {
245
280
  if (!snapshots.length) return;
246
281
  const productRef = typeof variant.product === "string" ? await requireProduct(em, variant.product, {
@@ -566,8 +601,57 @@ const createVariantCommand = {
566
601
  if (!record) return;
567
602
  ensureTenantScope(ctx, record.tenantId);
568
603
  ensureOrganizationScope(ctx, record.organizationId);
569
- em.remove(record);
570
- await em.flush();
604
+ const variantScope = { tenantId: record.tenantId, organizationId: record.organizationId };
605
+ let priceSnapshots = [];
606
+ let variantLocked = false;
607
+ await withAtomicFlush(
608
+ em,
609
+ [
610
+ async () => {
611
+ const locked = await findOneWithDecryption(
612
+ em,
613
+ CatalogProductVariant,
614
+ { id: record.id },
615
+ { lockMode: LockMode.PESSIMISTIC_WRITE },
616
+ variantScope
617
+ );
618
+ if (!locked) return;
619
+ variantLocked = true;
620
+ priceSnapshots = await loadVariantPriceSnapshots(em.fork({ keepTransactionContext: true }), record.id, variantScope);
621
+ if (priceSnapshots.length) {
622
+ await em.nativeDelete(CatalogProductPrice, { id: { $in: priceSnapshots.map((price) => price.id) } });
623
+ }
624
+ },
625
+ () => {
626
+ if (variantLocked) em.remove(record);
627
+ }
628
+ ],
629
+ { transaction: true }
630
+ );
631
+ if (!variantLocked) return;
632
+ await capturePriceHistoryEntries(em, priceSnapshots, "delete", {
633
+ metadata: { undoneCommand: "catalog.variants.create" },
634
+ cache: resolveOmnibusCache(ctx.container)
635
+ });
636
+ const dataEngine = ctx.container.resolve("dataEngine");
637
+ await resetVariantPriceCustomFields(dataEngine, priceSnapshots);
638
+ for (const price of priceSnapshots) {
639
+ await emitCrudUndoSideEffects({
640
+ dataEngine,
641
+ action: "deleted",
642
+ entity: price,
643
+ identifiers: { id: price.id, organizationId: price.organizationId, tenantId: price.tenantId },
644
+ events: variantPriceCrudEvents,
645
+ indexer: variantPriceCrudIndexer
646
+ });
647
+ await invalidateCrudCache(
648
+ ctx.container,
649
+ "catalog.price",
650
+ { id: price.id, organizationId: price.organizationId, tenantId: price.tenantId },
651
+ price.tenantId,
652
+ "command:catalog.variants.create:undo"
653
+ );
654
+ }
571
655
  if (payload?.previousDefaultVariantId) {
572
656
  const previousDefault = await em.findOne(CatalogProductVariant, { id: payload.previousDefaultVariantId });
573
657
  if (previousDefault) {
@@ -915,18 +999,7 @@ const deleteVariantCommand = {
915
999
  em.remove(record);
916
1000
  await em.flush();
917
1001
  await capturePriceHistoryEntries(em, priceSnapshots, "delete", { cache: resolveOmnibusCache(ctx.container) });
918
- for (const priceSnapshot of priceSnapshots) {
919
- const resetValues = buildCustomFieldResetMap(priceSnapshot.custom ?? void 0, void 0);
920
- if (!Object.keys(resetValues).length) continue;
921
- await setCustomFieldsIfAny({
922
- dataEngine,
923
- entityId: E.catalog.catalog_product_price,
924
- recordId: priceSnapshot.id,
925
- organizationId: priceSnapshot.organizationId,
926
- tenantId: priceSnapshot.tenantId,
927
- values: resetValues
928
- });
929
- }
1002
+ await resetVariantPriceCustomFields(dataEngine, priceSnapshots);
930
1003
  if (snapshot?.custom && Object.keys(snapshot.custom).length) {
931
1004
  const resetValues = buildCustomFieldResetMap(snapshot.custom, void 0);
932
1005
  if (Object.keys(resetValues).length) {