@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6245.1.2be3b151f8

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 (137) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/commands/users.js +1 -0
  3. package/dist/modules/auth/commands/users.js.map +2 -2
  4. package/dist/modules/communication_channels/data/enrichers.js +9 -1
  5. package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
  6. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
  7. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
  8. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
  9. package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
  10. package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
  11. package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
  12. package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
  13. package/dist/modules/customers/api/pipelines/route.js +107 -12
  14. package/dist/modules/customers/api/pipelines/route.js.map +2 -2
  15. package/dist/modules/customers/api/settings/address-format/route.js +33 -1
  16. package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
  17. package/dist/modules/customers/api/tags/assign/route.js +37 -0
  18. package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
  19. package/dist/modules/customers/api/tags/unassign/route.js +37 -0
  20. package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
  21. package/dist/modules/customers/components/calendar/CalendarToolbar.js +4 -6
  22. package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
  23. package/dist/modules/customers/components/calendar/EventBlock.js +2 -6
  24. package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
  25. package/dist/modules/customers/components/calendar/UpcomingCards.js +9 -7
  26. package/dist/modules/customers/components/calendar/UpcomingCards.js.map +2 -2
  27. package/dist/modules/customers/lib/calendar/format.js +27 -0
  28. package/dist/modules/customers/lib/calendar/format.js.map +7 -0
  29. package/dist/modules/entities/api/encryption.js +109 -38
  30. package/dist/modules/entities/api/encryption.js.map +2 -2
  31. package/dist/modules/entities/components/EncryptionManager.js +22 -9
  32. package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
  33. package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
  34. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  35. package/dist/modules/integrations/api/[id]/route.js +10 -3
  36. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  37. package/dist/modules/integrations/api/[id]/state/route.js +20 -5
  38. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  39. package/dist/modules/integrations/api/[id]/version/route.js +17 -1
  40. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  41. package/dist/modules/integrations/api/route.js +2 -0
  42. package/dist/modules/integrations/api/route.js.map +2 -2
  43. package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
  44. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  45. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
  46. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  47. package/dist/modules/integrations/backend/integrations/page.js +4 -3
  48. package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
  49. package/dist/modules/integrations/data/entities.js +2 -2
  50. package/dist/modules/integrations/data/entities.js.map +2 -2
  51. package/dist/modules/integrations/lib/credentials-service.js +32 -0
  52. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  53. package/dist/modules/integrations/lib/state-service.js +4 -2
  54. package/dist/modules/integrations/lib/state-service.js.map +2 -2
  55. package/dist/modules/notifications/lib/notificationService.js +42 -10
  56. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  57. package/dist/modules/payment_gateways/api/status/route.js +91 -6
  58. package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
  59. package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
  60. package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
  61. package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
  62. package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
  63. package/dist/modules/sales/backend/sales/channels/page.js +25 -7
  64. package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
  65. package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
  66. package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
  67. package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
  68. package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
  69. package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
  70. package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
  71. package/dist/modules/sales/components/StatusSettings.js +66 -31
  72. package/dist/modules/sales/components/StatusSettings.js.map +2 -2
  73. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
  74. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
  75. package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
  76. package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
  77. package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
  78. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  79. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +46 -13
  80. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
  81. package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
  82. package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
  83. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +43 -8
  84. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
  85. package/dist/modules/workflows/backend/definitions/page.js +19 -10
  86. package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
  87. package/package.json +7 -7
  88. package/src/modules/auth/commands/users.ts +1 -0
  89. package/src/modules/communication_channels/data/enrichers.ts +25 -0
  90. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
  91. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
  92. package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
  93. package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
  94. package/src/modules/customers/api/pipelines/route.ts +112 -13
  95. package/src/modules/customers/api/settings/address-format/route.ts +36 -1
  96. package/src/modules/customers/api/tags/assign/route.ts +39 -0
  97. package/src/modules/customers/api/tags/unassign/route.ts +39 -0
  98. package/src/modules/customers/components/calendar/CalendarToolbar.tsx +4 -7
  99. package/src/modules/customers/components/calendar/EventBlock.tsx +2 -6
  100. package/src/modules/customers/components/calendar/UpcomingCards.tsx +9 -7
  101. package/src/modules/customers/i18n/de.json +1 -0
  102. package/src/modules/customers/i18n/en.json +1 -0
  103. package/src/modules/customers/i18n/es.json +1 -0
  104. package/src/modules/customers/i18n/pl.json +1 -0
  105. package/src/modules/customers/lib/calendar/format.ts +25 -0
  106. package/src/modules/entities/api/encryption.ts +122 -39
  107. package/src/modules/entities/components/EncryptionManager.tsx +28 -9
  108. package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
  109. package/src/modules/integrations/api/[id]/route.ts +8 -1
  110. package/src/modules/integrations/api/[id]/state/route.ts +20 -4
  111. package/src/modules/integrations/api/[id]/version/route.ts +18 -1
  112. package/src/modules/integrations/api/route.ts +3 -0
  113. package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
  114. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
  115. package/src/modules/integrations/backend/integrations/page.tsx +8 -5
  116. package/src/modules/integrations/data/entities.ts +2 -2
  117. package/src/modules/integrations/lib/credentials-service.ts +35 -0
  118. package/src/modules/integrations/lib/state-service.ts +3 -0
  119. package/src/modules/notifications/lib/notificationService.ts +74 -11
  120. package/src/modules/payment_gateways/api/status/route.ts +97 -5
  121. package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
  122. package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
  123. package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
  124. package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
  125. package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
  126. package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
  127. package/src/modules/sales/components/StatusSettings.tsx +74 -33
  128. package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
  129. package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
  130. package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
  131. package/src/modules/staff/i18n/de.json +5 -5
  132. package/src/modules/staff/i18n/es.json +5 -5
  133. package/src/modules/staff/i18n/pl.json +5 -5
  134. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +57 -13
  135. package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
  136. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +57 -9
  137. package/src/modules/workflows/backend/definitions/page.tsx +19 -10
@@ -10,6 +10,7 @@ import {
10
10
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
11
11
  import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
12
12
  import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
13
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
13
14
  import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
14
15
  import { useT } from "@open-mercato/shared/lib/i18n/context";
15
16
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
@@ -27,6 +28,7 @@ const DEFAULT_FORM_VALUES = {
27
28
  color: null,
28
29
  icon: null
29
30
  };
31
+ const SAVE_CONTEXT_ID = "sales-status-settings";
30
32
  function StatusSettings() {
31
33
  const t = useT();
32
34
  const { confirm, ConfirmDialogElement } = useConfirmDialog();
@@ -34,6 +36,18 @@ function StatusSettings() {
34
36
  const value = t(key);
35
37
  return value === key ? fallback : value;
36
38
  }, [t]);
39
+ const { runMutation, retryLastMutation } = useGuardedMutation({
40
+ contextId: SAVE_CONTEXT_ID,
41
+ blockedMessage: translate("ui.forms.flash.saveBlocked", "Save blocked by validation")
42
+ });
43
+ const mutationContext = React.useMemo(
44
+ () => ({
45
+ formId: SAVE_CONTEXT_ID,
46
+ resourceKind: "sales.statuses",
47
+ retryLastMutation
48
+ }),
49
+ [retryLastMutation]
50
+ );
37
51
  const sections = React.useMemo(() => [
38
52
  {
39
53
  kind: "order-statuses",
@@ -171,17 +185,24 @@ function StatusSettings() {
171
185
  });
172
186
  if (!confirmed) return;
173
187
  try {
174
- const call = await withScopedApiRequestHeaders(
175
- buildOptimisticLockHeader(entry.updatedAt),
176
- () => apiCall(apiPaths[kind], {
177
- method: "DELETE",
178
- headers: { "content-type": "application/json" },
179
- body: JSON.stringify({ id: entry.id })
180
- })
181
- );
182
- if (!call.ok) {
183
- await raiseCrudError(call.response, translate("sales.config.statuses.error.delete", "Failed to delete status."));
184
- }
188
+ await runMutation({
189
+ operation: async () => {
190
+ const call = await withScopedApiRequestHeaders(
191
+ buildOptimisticLockHeader(entry.updatedAt),
192
+ () => apiCall(apiPaths[kind], {
193
+ method: "DELETE",
194
+ headers: { "content-type": "application/json" },
195
+ body: JSON.stringify({ id: entry.id })
196
+ })
197
+ );
198
+ if (!call.ok) {
199
+ await raiseCrudError(call.response, translate("sales.config.statuses.error.delete", "Failed to delete status."));
200
+ }
201
+ return call;
202
+ },
203
+ context: mutationContext,
204
+ mutationPayload: { action: "delete", kind, id: entry.id }
205
+ });
185
206
  flash(translate("sales.config.statuses.success.delete", "Status deleted."), "success");
186
207
  await loadEntries(kind);
187
208
  } catch (err) {
@@ -189,21 +210,28 @@ function StatusSettings() {
189
210
  const message2 = err instanceof Error ? err.message : translate("sales.config.statuses.error.delete", "Failed to delete status.");
190
211
  flash(message2, "error");
191
212
  }
192
- }, [apiPaths, confirm, loadEntries, translate]);
213
+ }, [apiPaths, confirm, loadEntries, mutationContext, runMutation, translate]);
193
214
  const submitForm = React.useCallback(async (values) => {
194
215
  if (!dialog) return;
195
216
  const path = apiPaths[dialog.kind];
196
217
  setSubmitting(true);
197
218
  try {
198
219
  if (dialog.mode === "create") {
199
- const call = await apiCall(path, {
200
- method: "POST",
201
- headers: { "content-type": "application/json" },
202
- body: JSON.stringify(values)
220
+ await runMutation({
221
+ operation: async () => {
222
+ const call = await apiCall(path, {
223
+ method: "POST",
224
+ headers: { "content-type": "application/json" },
225
+ body: JSON.stringify(values)
226
+ });
227
+ if (!call.ok) {
228
+ await raiseCrudError(call.response, translate("sales.config.statuses.error.save", "Failed to save status."));
229
+ }
230
+ return call;
231
+ },
232
+ context: mutationContext,
233
+ mutationPayload: { action: "create", kind: dialog.kind, ...values }
203
234
  });
204
- if (!call.ok) {
205
- await raiseCrudError(call.response, translate("sales.config.statuses.error.save", "Failed to save status."));
206
- }
207
235
  flash(translate("sales.config.statuses.success.save", "Status saved."), "success");
208
236
  } else if (dialog.mode === "edit") {
209
237
  const entry = dialog.entry;
@@ -214,17 +242,24 @@ function StatusSettings() {
214
242
  if (nextColor !== (entry.color ?? null)) body.color = nextColor;
215
243
  const nextIcon = values.icon ?? null;
216
244
  if (nextIcon !== (entry.icon ?? null)) body.icon = nextIcon;
217
- const call = await withScopedApiRequestHeaders(
218
- buildOptimisticLockHeader(entry.updatedAt),
219
- () => apiCall(path, {
220
- method: "PUT",
221
- headers: { "content-type": "application/json" },
222
- body: JSON.stringify(body)
223
- })
224
- );
225
- if (!call.ok) {
226
- await raiseCrudError(call.response, translate("sales.config.statuses.error.save", "Failed to save status."));
227
- }
245
+ await runMutation({
246
+ operation: async () => {
247
+ const call = await withScopedApiRequestHeaders(
248
+ buildOptimisticLockHeader(entry.updatedAt),
249
+ () => apiCall(path, {
250
+ method: "PUT",
251
+ headers: { "content-type": "application/json" },
252
+ body: JSON.stringify(body)
253
+ })
254
+ );
255
+ if (!call.ok) {
256
+ await raiseCrudError(call.response, translate("sales.config.statuses.error.save", "Failed to save status."));
257
+ }
258
+ return call;
259
+ },
260
+ context: mutationContext,
261
+ mutationPayload: { action: "update", kind: dialog.kind, ...body }
262
+ });
228
263
  flash(translate("sales.config.statuses.success.save", "Status saved."), "success");
229
264
  }
230
265
  closeDialog();
@@ -237,7 +272,7 @@ function StatusSettings() {
237
272
  } finally {
238
273
  setSubmitting(false);
239
274
  }
240
- }, [apiPaths, closeDialog, dialog, loadEntries, translate]);
275
+ }, [apiPaths, closeDialog, dialog, loadEntries, mutationContext, runMutation, translate]);
241
276
  const currentValues = React.useMemo(() => {
242
277
  if (dialog && dialog.mode === "edit") {
243
278
  return {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sales/components/StatusSettings.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { ICON_SUGGESTIONS } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport {\n DictionaryForm,\n type DictionaryFormValues,\n} from '@open-mercato/core/modules/dictionaries/components/DictionaryForm'\nimport {\n DictionaryTable,\n type DictionaryTableEntry,\n} from '@open-mercato/core/modules/dictionaries/components/DictionaryTable'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\n\ntype SalesStatusKind = 'order-statuses' | 'order-line-statuses' | 'shipment-statuses' | 'payment-statuses'\n\ntype SectionDefinition = {\n kind: SalesStatusKind\n title: string\n description: string\n}\n\ntype DialogState =\n | { mode: 'create'; kind: SalesStatusKind }\n | { mode: 'edit'; kind: SalesStatusKind; entry: DictionaryTableEntry }\n\nconst DEFAULT_FORM_VALUES: DictionaryFormValues = {\n value: '',\n label: '',\n color: null,\n icon: null,\n}\n\nexport function StatusSettings() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const translate = React.useCallback((key: string, fallback: string) => {\n const value = t(key)\n return value === key ? fallback : value\n }, [t])\n\n const sections = React.useMemo<SectionDefinition[]>(() => [\n {\n kind: 'order-statuses',\n title: translate('sales.config.statuses.orders.title', 'Order statuses'),\n description: translate('sales.config.statuses.orders.description', 'Configure the status values available for sales orders.'),\n },\n {\n kind: 'order-line-statuses',\n title: translate('sales.config.statuses.lines.title', 'Order line statuses'),\n description: translate('sales.config.statuses.lines.description', 'Configure the status values available for sales order lines.'),\n },\n {\n kind: 'shipment-statuses',\n title: translate('sales.config.statuses.shipments.title', 'Shipment statuses'),\n description: translate('sales.config.statuses.shipments.description', 'Configure the status values available for shipments.'),\n },\n {\n kind: 'payment-statuses',\n title: translate('sales.config.statuses.payments.title', 'Payment statuses'),\n description: translate('sales.config.statuses.payments.description', 'Configure the status values available for payments.'),\n },\n ], [translate])\n\n const [entriesByKind, setEntriesByKind] = React.useState<Record<SalesStatusKind, DictionaryTableEntry[]>>({\n 'order-statuses': [],\n 'order-line-statuses': [],\n 'shipment-statuses': [],\n 'payment-statuses': [],\n })\n const [loadingKind, setLoadingKind] = React.useState<Record<SalesStatusKind, boolean>>({\n 'order-statuses': false,\n 'order-line-statuses': false,\n 'shipment-statuses': false,\n 'payment-statuses': false,\n })\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [submitting, setSubmitting] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n\n const apiPaths = React.useMemo<Record<SalesStatusKind, string>>(\n () => ({\n 'order-statuses': '/api/sales/order-statuses',\n 'order-line-statuses': '/api/sales/order-line-statuses',\n 'shipment-statuses': '/api/sales/shipment-statuses',\n 'payment-statuses': '/api/sales/payment-statuses',\n }),\n []\n )\n\n const loadEntries = React.useCallback(async (kind: SalesStatusKind) => {\n setLoadingKind((prev) => ({ ...prev, [kind]: true }))\n try {\n const data = await readApiResultOrThrow<{ items?: any[] }>(\n apiPaths[kind],\n undefined,\n {\n errorMessage: translate('sales.config.statuses.error.load', 'Failed to load status entries.'),\n fallback: { items: [] },\n },\n )\n if (!data || typeof data !== 'object' || !Array.isArray((data as any).items)) {\n throw new Error(translate('sales.config.statuses.error.load', 'Failed to load status entries.'))\n }\n const items: DictionaryTableEntry[] = (data as { items: any[] }).items.map((item) => ({\n id: String(item.id),\n value: String(item.value ?? ''),\n label: typeof item.label === 'string' ? item.label : '',\n color: typeof item.color === 'string' ? item.color : null,\n icon: typeof item.icon === 'string' ? item.icon : null,\n organizationId: typeof item.organizationId === 'string' ? item.organizationId : null,\n tenantId: typeof item.tenantId === 'string' ? item.tenantId : null,\n isInherited: false,\n createdAt: typeof item.createdAt === 'string' ? item.createdAt : null,\n updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : null,\n }))\n setEntriesByKind((prev) => ({ ...prev, [kind]: items }))\n } catch (err) {\n console.error('sales.statuses.list failed', err)\n flash(translate('sales.config.statuses.error.load', 'Failed to load status entries.'), 'error')\n } finally {\n setLoadingKind((prev) => ({ ...prev, [kind]: false }))\n }\n }, [apiPaths, translate])\n\n React.useEffect(() => {\n void loadEntries('order-statuses')\n void loadEntries('order-line-statuses')\n void loadEntries('shipment-statuses')\n void loadEntries('payment-statuses')\n }, [loadEntries, scopeVersion])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n }, [])\n\n const tableTranslations = React.useMemo(() => ({\n valueColumn: translate('sales.config.statuses.columns.value', 'Value'),\n labelColumn: translate('sales.config.statuses.columns.label', 'Label'),\n appearanceColumn: translate('sales.config.statuses.columns.appearance', 'Appearance'),\n addLabel: translate('sales.config.statuses.actions.add', 'Add status'),\n editLabel: translate('sales.config.statuses.actions.edit', 'Edit'),\n deleteLabel: translate('sales.config.statuses.actions.delete', 'Delete'),\n refreshLabel: translate('sales.config.statuses.actions.refresh', 'Refresh'),\n inheritedLabel: '',\n inheritedTooltip: '',\n emptyLabel: translate('sales.config.statuses.empty', 'No statuses yet.'),\n searchPlaceholder: translate('sales.config.statuses.search', 'Search statuses\u2026'),\n }), [translate])\n\n const formTranslations = React.useMemo(() => ({\n createTitle: translate('sales.config.statuses.dialog.addTitle', 'Add status'),\n editTitle: translate('sales.config.statuses.dialog.editTitle', 'Edit status'),\n valueLabel: translate('sales.config.statuses.dialog.valueLabel', 'Value'),\n labelLabel: translate('sales.config.statuses.dialog.labelLabel', 'Label'),\n saveLabel: translate('sales.config.statuses.dialog.save', 'Save'),\n cancelLabel: translate('sales.config.statuses.dialog.cancel', 'Cancel'),\n appearance: {\n colorLabel: translate('sales.config.statuses.dialog.colorLabel', 'Color'),\n colorHelp: translate('sales.config.statuses.dialog.colorHelp', 'Pick a highlight color for this status.'),\n colorClearLabel: translate('sales.config.statuses.dialog.colorClear', 'Remove color'),\n iconLabel: translate('sales.config.statuses.dialog.iconLabel', 'Icon'),\n iconPlaceholder: translate('sales.config.statuses.dialog.iconPlaceholder', 'Type an emoji or pick one of the suggestions.'),\n iconPickerTriggerLabel: translate('sales.config.statuses.dialog.iconBrowse', 'Browse icons and emojis'),\n iconSearchPlaceholder: translate('sales.config.statuses.dialog.iconSearchPlaceholder', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: translate('sales.config.statuses.dialog.iconSearchEmpty', 'No icons match your search.'),\n iconSuggestionsLabel: translate('sales.config.statuses.dialog.iconSuggestions', 'Suggestions'),\n iconClearLabel: translate('sales.config.statuses.dialog.iconClear', 'Remove icon'),\n previewEmptyLabel: translate('sales.config.statuses.appearance.empty', 'None'),\n },\n }), [translate])\n\n const startCreate = React.useCallback((kind: SalesStatusKind) => {\n setDialog({ mode: 'create', kind })\n }, [])\n\n const startEdit = React.useCallback((kind: SalesStatusKind, entry: DictionaryTableEntry) => {\n setDialog({ mode: 'edit', kind, entry })\n }, [])\n\n const deleteEntry = React.useCallback(async (kind: SalesStatusKind, entry: DictionaryTableEntry) => {\n const message = translate('sales.config.statuses.deleteConfirm', 'Delete status \"{{value}}\"?').replace('{{value}}', entry.label || entry.value)\n const confirmed = await confirm({\n title: message,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(entry.updatedAt),\n () =>\n apiCall(apiPaths[kind], {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id }),\n })\n )\n if (!call.ok) {\n await raiseCrudError(call.response, translate('sales.config.statuses.error.delete', 'Failed to delete status.'))\n }\n flash(translate('sales.config.statuses.success.delete', 'Status deleted.'), 'success')\n await loadEntries(kind)\n } catch (err) {\n console.error('sales.statuses.delete failed', err)\n const message = err instanceof Error ? err.message : translate('sales.config.statuses.error.delete', 'Failed to delete status.')\n flash(message, 'error')\n }\n }, [apiPaths, confirm, loadEntries, translate])\n\n const submitForm = React.useCallback(async (values: DictionaryFormValues) => {\n if (!dialog) return\n const path = apiPaths[dialog.kind]\n setSubmitting(true)\n try {\n if (dialog.mode === 'create') {\n const call = await apiCall(path, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(values),\n })\n if (!call.ok) {\n await raiseCrudError(call.response, translate('sales.config.statuses.error.save', 'Failed to save status.'))\n }\n flash(translate('sales.config.statuses.success.save', 'Status saved.'), 'success')\n } else if (dialog.mode === 'edit') {\n const entry = dialog.entry\n const body: Record<string, unknown> = { id: entry.id }\n if (values.value !== entry.value) body.value = values.value\n if (values.label !== entry.label) body.label = values.label\n const nextColor = values.color ?? null\n if (nextColor !== (entry.color ?? null)) body.color = nextColor\n const nextIcon = values.icon ?? null\n if (nextIcon !== (entry.icon ?? null)) body.icon = nextIcon\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(entry.updatedAt),\n () =>\n apiCall(path, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n )\n if (!call.ok) {\n await raiseCrudError(call.response, translate('sales.config.statuses.error.save', 'Failed to save status.'))\n }\n flash(translate('sales.config.statuses.success.save', 'Status saved.'), 'success')\n }\n closeDialog()\n await loadEntries(dialog.kind)\n } catch (err) {\n console.error('sales.statuses.submit failed', err)\n const message = err instanceof Error ? err.message : translate('sales.config.statuses.error.save', 'Failed to save status.')\n flash(message, 'error')\n throw err instanceof Error ? err : new Error(message)\n } finally {\n setSubmitting(false)\n }\n }, [apiPaths, closeDialog, dialog, loadEntries, translate])\n\n const currentValues = React.useMemo<DictionaryFormValues>(() => {\n if (dialog && dialog.mode === 'edit') {\n return {\n value: dialog.entry.value,\n label: dialog.entry.label,\n color: dialog.entry.color,\n icon: dialog.entry.icon,\n }\n }\n return DEFAULT_FORM_VALUES\n }, [dialog])\n\n return (\n <div className=\"space-y-6\">\n {sections.map((section) => {\n const items = entriesByKind[section.kind] ?? []\n const loading = loadingKind[section.kind] ?? false\n return (\n <section key={section.kind} className=\"rounded border bg-card text-card-foreground shadow-sm\">\n <div className=\"border-b px-6 py-4 space-y-1\">\n <h2 className=\"text-lg font-medium\">{section.title}</h2>\n <p className=\"text-sm text-muted-foreground\">{section.description}</p>\n </div>\n <div className=\"px-2 py-4 sm:px-4\">\n <DictionaryTable\n entries={items}\n loading={loading}\n canManage\n onCreate={() => startCreate(section.kind)}\n onEdit={(entry) => startEdit(section.kind, entry)}\n onDelete={(entry) => deleteEntry(section.kind, entry)}\n onRefresh={() => loadEntries(section.kind)}\n translations={{ ...tableTranslations, title: section.title }}\n />\n </div>\n </section>\n )\n })}\n\n <Dialog open={dialog !== null} onOpenChange={(open) => { if (!open) closeDialog() }}>\n <DialogContent className=\"max-w-lg\">\n <DialogHeader>\n <DialogTitle>\n {dialog?.mode === 'edit' ? formTranslations.editTitle : formTranslations.createTitle}\n </DialogTitle>\n </DialogHeader>\n <DictionaryForm\n mode={dialog?.mode === 'edit' ? 'edit' : 'create'}\n initialValues={currentValues}\n onSubmit={submitForm}\n onCancel={closeDialog}\n submitting={submitting}\n translations={{\n title: dialog?.mode === 'edit' ? formTranslations.editTitle : formTranslations.createTitle,\n valueLabel: formTranslations.valueLabel,\n labelLabel: formTranslations.labelLabel,\n saveLabel: formTranslations.saveLabel,\n cancelLabel: formTranslations.cancelLabel,\n appearance: formTranslations.appearance,\n }}\n iconSuggestions={ICON_SUGGESTIONS}\n />\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
5
- "mappings": ";AAkSY,SACE,KADF;AAhSZ,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,SAAS,sBAAsB,mCAAmC;AAC3E,SAAS,iCAAiC;AAC1C,SAAS,mCAAmC;AAC5C,SAAS,YAAY;AACrB,SAAS,wBAAwB;AACjC,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,OAEK;AACP;AAAA,EACE;AAAA,OAEK;AACP,SAAS,sBAAsB;AAc/B,MAAM,sBAA4C;AAAA,EAChD,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AACR;AAEO,SAAS,iBAAiB;AAC/B,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,SAAS,qBAAqB,IAAI,iBAAiB;AAC3D,QAAM,YAAY,MAAM,YAAY,CAAC,KAAa,aAAqB;AACrE,UAAM,QAAQ,EAAE,GAAG;AACnB,WAAO,UAAU,MAAM,WAAW;AAAA,EACpC,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,WAAW,MAAM,QAA6B,MAAM;AAAA,IACxD;AAAA,MACE,MAAM;AAAA,MACN,OAAO,UAAU,sCAAsC,gBAAgB;AAAA,MACvE,aAAa,UAAU,4CAA4C,yDAAyD;AAAA,IAC9H;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,OAAO,UAAU,qCAAqC,qBAAqB;AAAA,MAC3E,aAAa,UAAU,2CAA2C,8DAA8D;AAAA,IAClI;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,OAAO,UAAU,yCAAyC,mBAAmB;AAAA,MAC7E,aAAa,UAAU,+CAA+C,sDAAsD;AAAA,IAC9H;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,OAAO,UAAU,wCAAwC,kBAAkB;AAAA,MAC3E,aAAa,UAAU,8CAA8C,qDAAqD;AAAA,IAC5H;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAA0D;AAAA,IACxG,kBAAkB,CAAC;AAAA,IACnB,uBAAuB,CAAC;AAAA,IACxB,qBAAqB,CAAC;AAAA,IACtB,oBAAoB,CAAC;AAAA,EACvB,CAAC;AACD,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAA2C;AAAA,IACrF,kBAAkB;AAAA,IAClB,uBAAuB;AAAA,IACvB,qBAAqB;AAAA,IACrB,oBAAoB;AAAA,EACtB,CAAC;AACD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA6B,IAAI;AACnE,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,eAAe,4BAA4B;AAEjD,QAAM,WAAW,MAAM;AAAA,IACrB,OAAO;AAAA,MACL,kBAAkB;AAAA,MAClB,uBAAuB;AAAA,MACvB,qBAAqB;AAAA,MACrB,oBAAoB;AAAA,IACtB;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,cAAc,MAAM,YAAY,OAAO,SAA0B;AACrE,mBAAe,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,IAAI,GAAG,KAAK,EAAE;AACpD,QAAI;AACF,YAAM,OAAO,MAAM;AAAA,QACjB,SAAS,IAAI;AAAA,QACb;AAAA,QACA;AAAA,UACE,cAAc,UAAU,oCAAoC,gCAAgC;AAAA,UAC5F,UAAU,EAAE,OAAO,CAAC,EAAE;AAAA,QACxB;AAAA,MACF;AACA,UAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAS,KAAa,KAAK,GAAG;AAC5E,cAAM,IAAI,MAAM,UAAU,oCAAoC,gCAAgC,CAAC;AAAA,MACjG;AACA,YAAM,QAAiC,KAA0B,MAAM,IAAI,CAAC,UAAU;AAAA,QACpF,IAAI,OAAO,KAAK,EAAE;AAAA,QAClB,OAAO,OAAO,KAAK,SAAS,EAAE;AAAA,QAC9B,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,QACrD,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,QACrD,MAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AAAA,QAClD,gBAAgB,OAAO,KAAK,mBAAmB,WAAW,KAAK,iBAAiB;AAAA,QAChF,UAAU,OAAO,KAAK,aAAa,WAAW,KAAK,WAAW;AAAA,QAC9D,aAAa;AAAA,QACb,WAAW,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY;AAAA,QACjE,WAAW,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY;AAAA,MACnE,EAAE;AACF,uBAAiB,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,IAAI,GAAG,MAAM,EAAE;AAAA,IACzD,SAAS,KAAK;AACZ,cAAQ,MAAM,8BAA8B,GAAG;AAC/C,YAAM,UAAU,oCAAoC,gCAAgC,GAAG,OAAO;AAAA,IAChG,UAAE;AACA,qBAAe,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,IAAI,GAAG,MAAM,EAAE;AAAA,IACvD;AAAA,EACF,GAAG,CAAC,UAAU,SAAS,CAAC;AAExB,QAAM,UAAU,MAAM;AACpB,SAAK,YAAY,gBAAgB;AACjC,SAAK,YAAY,qBAAqB;AACtC,SAAK,YAAY,mBAAmB;AACpC,SAAK,YAAY,kBAAkB;AAAA,EACrC,GAAG,CAAC,aAAa,YAAY,CAAC;AAE9B,QAAM,cAAc,MAAM,YAAY,MAAM;AAC1C,cAAU,IAAI;AAAA,EAChB,GAAG,CAAC,CAAC;AAEL,QAAM,oBAAoB,MAAM,QAAQ,OAAO;AAAA,IAC7C,aAAa,UAAU,uCAAuC,OAAO;AAAA,IACrE,aAAa,UAAU,uCAAuC,OAAO;AAAA,IACrE,kBAAkB,UAAU,4CAA4C,YAAY;AAAA,IACpF,UAAU,UAAU,qCAAqC,YAAY;AAAA,IACrE,WAAW,UAAU,sCAAsC,MAAM;AAAA,IACjE,aAAa,UAAU,wCAAwC,QAAQ;AAAA,IACvE,cAAc,UAAU,yCAAyC,SAAS;AAAA,IAC1E,gBAAgB;AAAA,IAChB,kBAAkB;AAAA,IAClB,YAAY,UAAU,+BAA+B,kBAAkB;AAAA,IACvE,mBAAmB,UAAU,gCAAgC,uBAAkB;AAAA,EACjF,IAAI,CAAC,SAAS,CAAC;AAEf,QAAM,mBAAmB,MAAM,QAAQ,OAAO;AAAA,IAC5C,aAAa,UAAU,yCAAyC,YAAY;AAAA,IAC5E,WAAW,UAAU,0CAA0C,aAAa;AAAA,IAC5E,YAAY,UAAU,2CAA2C,OAAO;AAAA,IACxE,YAAY,UAAU,2CAA2C,OAAO;AAAA,IACxE,WAAW,UAAU,qCAAqC,MAAM;AAAA,IAChE,aAAa,UAAU,uCAAuC,QAAQ;AAAA,IACtE,YAAY;AAAA,MACV,YAAY,UAAU,2CAA2C,OAAO;AAAA,MACxE,WAAW,UAAU,0CAA0C,yCAAyC;AAAA,MACxG,iBAAiB,UAAU,2CAA2C,cAAc;AAAA,MACpF,WAAW,UAAU,0CAA0C,MAAM;AAAA,MACrE,iBAAiB,UAAU,gDAAgD,+CAA+C;AAAA,MAC1H,wBAAwB,UAAU,2CAA2C,yBAAyB;AAAA,MACtG,uBAAuB,UAAU,sDAAsD,8BAAyB;AAAA,MAChH,sBAAsB,UAAU,gDAAgD,6BAA6B;AAAA,MAC7G,sBAAsB,UAAU,gDAAgD,aAAa;AAAA,MAC7F,gBAAgB,UAAU,0CAA0C,aAAa;AAAA,MACjF,mBAAmB,UAAU,0CAA0C,MAAM;AAAA,IAC/E;AAAA,EACF,IAAI,CAAC,SAAS,CAAC;AAEf,QAAM,cAAc,MAAM,YAAY,CAAC,SAA0B;AAC/D,cAAU,EAAE,MAAM,UAAU,KAAK,CAAC;AAAA,EACpC,GAAG,CAAC,CAAC;AAEL,QAAM,YAAY,MAAM,YAAY,CAAC,MAAuB,UAAgC;AAC1F,cAAU,EAAE,MAAM,QAAQ,MAAM,MAAM,CAAC;AAAA,EACzC,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc,MAAM,YAAY,OAAO,MAAuB,UAAgC;AAClG,UAAM,UAAU,UAAU,uCAAuC,4BAA4B,EAAE,QAAQ,aAAa,MAAM,SAAS,MAAM,KAAK;AAC9I,UAAM,YAAY,MAAM,QAAQ;AAAA,MAC9B,OAAO;AAAA,MACP,SAAS;AAAA,IACX,CAAC;AACD,QAAI,CAAC,UAAW;AAChB,QAAI;AACF,YAAM,OAAO,MAAM;AAAA,QACjB,0BAA0B,MAAM,SAAS;AAAA,QACzC,MACE,QAAQ,SAAS,IAAI,GAAG;AAAA,UACtB,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,IAAI,MAAM,GAAG,CAAC;AAAA,QACvC,CAAC;AAAA,MACL;AACA,UAAI,CAAC,KAAK,IAAI;AACZ,cAAM,eAAe,KAAK,UAAU,UAAU,sCAAsC,0BAA0B,CAAC;AAAA,MACjH;AACA,YAAM,UAAU,wCAAwC,iBAAiB,GAAG,SAAS;AACrF,YAAM,YAAY,IAAI;AAAA,IACxB,SAAS,KAAK;AACZ,cAAQ,MAAM,gCAAgC,GAAG;AACjD,YAAMA,WAAU,eAAe,QAAQ,IAAI,UAAU,UAAU,sCAAsC,0BAA0B;AAC/H,YAAMA,UAAS,OAAO;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,UAAU,SAAS,aAAa,SAAS,CAAC;AAE9C,QAAM,aAAa,MAAM,YAAY,OAAO,WAAiC;AAC3E,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,SAAS,OAAO,IAAI;AACjC,kBAAc,IAAI;AAClB,QAAI;AACF,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,OAAO,MAAM,QAAQ,MAAM;AAAA,UAC/B,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,MAAM;AAAA,QAC7B,CAAC;AACD,YAAI,CAAC,KAAK,IAAI;AACZ,gBAAM,eAAe,KAAK,UAAU,UAAU,oCAAoC,wBAAwB,CAAC;AAAA,QAC7G;AACA,cAAM,UAAU,sCAAsC,eAAe,GAAG,SAAS;AAAA,MACnF,WAAW,OAAO,SAAS,QAAQ;AACjC,cAAM,QAAQ,OAAO;AACrB,cAAM,OAAgC,EAAE,IAAI,MAAM,GAAG;AACrD,YAAI,OAAO,UAAU,MAAM,MAAO,MAAK,QAAQ,OAAO;AACtD,YAAI,OAAO,UAAU,MAAM,MAAO,MAAK,QAAQ,OAAO;AACtD,cAAM,YAAY,OAAO,SAAS;AAClC,YAAI,eAAe,MAAM,SAAS,MAAO,MAAK,QAAQ;AACtD,cAAM,WAAW,OAAO,QAAQ;AAChC,YAAI,cAAc,MAAM,QAAQ,MAAO,MAAK,OAAO;AACnD,cAAM,OAAO,MAAM;AAAA,UACjB,0BAA0B,MAAM,SAAS;AAAA,UACzC,MACE,QAAQ,MAAM;AAAA,YACZ,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,MAAM,KAAK,UAAU,IAAI;AAAA,UAC3B,CAAC;AAAA,QACL;AACA,YAAI,CAAC,KAAK,IAAI;AACZ,gBAAM,eAAe,KAAK,UAAU,UAAU,oCAAoC,wBAAwB,CAAC;AAAA,QAC7G;AACA,cAAM,UAAU,sCAAsC,eAAe,GAAG,SAAS;AAAA,MACnF;AACA,kBAAY;AACZ,YAAM,YAAY,OAAO,IAAI;AAAA,IAC/B,SAAS,KAAK;AACZ,cAAQ,MAAM,gCAAgC,GAAG;AACjD,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,UAAU,oCAAoC,wBAAwB;AAC3H,YAAM,SAAS,OAAO;AACtB,YAAM,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO;AAAA,IACtD,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,UAAU,aAAa,QAAQ,aAAa,SAAS,CAAC;AAE1D,QAAM,gBAAgB,MAAM,QAA8B,MAAM;AAC9D,QAAI,UAAU,OAAO,SAAS,QAAQ;AACpC,aAAO;AAAA,QACL,OAAO,OAAO,MAAM;AAAA,QACpB,OAAO,OAAO,MAAM;AAAA,QACpB,OAAO,OAAO,MAAM;AAAA,QACpB,MAAM,OAAO,MAAM;AAAA,MACrB;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,SACE,qBAAC,SAAI,WAAU,aACZ;AAAA,aAAS,IAAI,CAAC,YAAY;AACzB,YAAM,QAAQ,cAAc,QAAQ,IAAI,KAAK,CAAC;AAC9C,YAAM,UAAU,YAAY,QAAQ,IAAI,KAAK;AAC7C,aACE,qBAAC,aAA2B,WAAU,yDACpC;AAAA,6BAAC,SAAI,WAAU,gCACb;AAAA,8BAAC,QAAG,WAAU,uBAAuB,kBAAQ,OAAM;AAAA,UACnD,oBAAC,OAAE,WAAU,iCAAiC,kBAAQ,aAAY;AAAA,WACpE;AAAA,QACA,oBAAC,SAAI,WAAU,qBACb;AAAA,UAAC;AAAA;AAAA,YACC,SAAS;AAAA,YACT;AAAA,YACA,WAAS;AAAA,YACT,UAAU,MAAM,YAAY,QAAQ,IAAI;AAAA,YACxC,QAAQ,CAAC,UAAU,UAAU,QAAQ,MAAM,KAAK;AAAA,YAChD,UAAU,CAAC,UAAU,YAAY,QAAQ,MAAM,KAAK;AAAA,YACpD,WAAW,MAAM,YAAY,QAAQ,IAAI;AAAA,YACzC,cAAc,EAAE,GAAG,mBAAmB,OAAO,QAAQ,MAAM;AAAA;AAAA,QAC7D,GACF;AAAA,WAhBY,QAAQ,IAiBtB;AAAA,IAEJ,CAAC;AAAA,IAED,oBAAC,UAAO,MAAM,WAAW,MAAM,cAAc,CAAC,SAAS;AAAE,UAAI,CAAC,KAAM,aAAY;AAAA,IAAE,GAChF,+BAAC,iBAAc,WAAU,YACvB;AAAA,0BAAC,gBACC,8BAAC,eACE,kBAAQ,SAAS,SAAS,iBAAiB,YAAY,iBAAiB,aAC3E,GACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,QAAQ,SAAS,SAAS,SAAS;AAAA,UACzC,eAAe;AAAA,UACf,UAAU;AAAA,UACV,UAAU;AAAA,UACV;AAAA,UACA,cAAc;AAAA,YACZ,OAAO,QAAQ,SAAS,SAAS,iBAAiB,YAAY,iBAAiB;AAAA,YAC/E,YAAY,iBAAiB;AAAA,YAC7B,YAAY,iBAAiB;AAAA,YAC7B,WAAW,iBAAiB;AAAA,YAC5B,aAAa,iBAAiB;AAAA,YAC9B,YAAY,iBAAiB;AAAA,UAC/B;AAAA,UACA,iBAAiB;AAAA;AAAA,MACnB;AAAA,OACF,GACF;AAAA,IACC;AAAA,KACH;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { ICON_SUGGESTIONS } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport {\n DictionaryForm,\n type DictionaryFormValues,\n} from '@open-mercato/core/modules/dictionaries/components/DictionaryForm'\nimport {\n DictionaryTable,\n type DictionaryTableEntry,\n} from '@open-mercato/core/modules/dictionaries/components/DictionaryTable'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\n\ntype SalesStatusKind = 'order-statuses' | 'order-line-statuses' | 'shipment-statuses' | 'payment-statuses'\n\ntype SectionDefinition = {\n kind: SalesStatusKind\n title: string\n description: string\n}\n\ntype DialogState =\n | { mode: 'create'; kind: SalesStatusKind }\n | { mode: 'edit'; kind: SalesStatusKind; entry: DictionaryTableEntry }\n\nconst DEFAULT_FORM_VALUES: DictionaryFormValues = {\n value: '',\n label: '',\n color: null,\n icon: null,\n}\n\nconst SAVE_CONTEXT_ID = 'sales-status-settings'\n\nexport function StatusSettings() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const translate = React.useCallback((key: string, fallback: string) => {\n const value = t(key)\n return value === key ? fallback : value\n }, [t])\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: SAVE_CONTEXT_ID,\n blockedMessage: translate('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({\n formId: SAVE_CONTEXT_ID,\n resourceKind: 'sales.statuses',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n\n const sections = React.useMemo<SectionDefinition[]>(() => [\n {\n kind: 'order-statuses',\n title: translate('sales.config.statuses.orders.title', 'Order statuses'),\n description: translate('sales.config.statuses.orders.description', 'Configure the status values available for sales orders.'),\n },\n {\n kind: 'order-line-statuses',\n title: translate('sales.config.statuses.lines.title', 'Order line statuses'),\n description: translate('sales.config.statuses.lines.description', 'Configure the status values available for sales order lines.'),\n },\n {\n kind: 'shipment-statuses',\n title: translate('sales.config.statuses.shipments.title', 'Shipment statuses'),\n description: translate('sales.config.statuses.shipments.description', 'Configure the status values available for shipments.'),\n },\n {\n kind: 'payment-statuses',\n title: translate('sales.config.statuses.payments.title', 'Payment statuses'),\n description: translate('sales.config.statuses.payments.description', 'Configure the status values available for payments.'),\n },\n ], [translate])\n\n const [entriesByKind, setEntriesByKind] = React.useState<Record<SalesStatusKind, DictionaryTableEntry[]>>({\n 'order-statuses': [],\n 'order-line-statuses': [],\n 'shipment-statuses': [],\n 'payment-statuses': [],\n })\n const [loadingKind, setLoadingKind] = React.useState<Record<SalesStatusKind, boolean>>({\n 'order-statuses': false,\n 'order-line-statuses': false,\n 'shipment-statuses': false,\n 'payment-statuses': false,\n })\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [submitting, setSubmitting] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n\n const apiPaths = React.useMemo<Record<SalesStatusKind, string>>(\n () => ({\n 'order-statuses': '/api/sales/order-statuses',\n 'order-line-statuses': '/api/sales/order-line-statuses',\n 'shipment-statuses': '/api/sales/shipment-statuses',\n 'payment-statuses': '/api/sales/payment-statuses',\n }),\n []\n )\n\n const loadEntries = React.useCallback(async (kind: SalesStatusKind) => {\n setLoadingKind((prev) => ({ ...prev, [kind]: true }))\n try {\n const data = await readApiResultOrThrow<{ items?: any[] }>(\n apiPaths[kind],\n undefined,\n {\n errorMessage: translate('sales.config.statuses.error.load', 'Failed to load status entries.'),\n fallback: { items: [] },\n },\n )\n if (!data || typeof data !== 'object' || !Array.isArray((data as any).items)) {\n throw new Error(translate('sales.config.statuses.error.load', 'Failed to load status entries.'))\n }\n const items: DictionaryTableEntry[] = (data as { items: any[] }).items.map((item) => ({\n id: String(item.id),\n value: String(item.value ?? ''),\n label: typeof item.label === 'string' ? item.label : '',\n color: typeof item.color === 'string' ? item.color : null,\n icon: typeof item.icon === 'string' ? item.icon : null,\n organizationId: typeof item.organizationId === 'string' ? item.organizationId : null,\n tenantId: typeof item.tenantId === 'string' ? item.tenantId : null,\n isInherited: false,\n createdAt: typeof item.createdAt === 'string' ? item.createdAt : null,\n updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : null,\n }))\n setEntriesByKind((prev) => ({ ...prev, [kind]: items }))\n } catch (err) {\n console.error('sales.statuses.list failed', err)\n flash(translate('sales.config.statuses.error.load', 'Failed to load status entries.'), 'error')\n } finally {\n setLoadingKind((prev) => ({ ...prev, [kind]: false }))\n }\n }, [apiPaths, translate])\n\n React.useEffect(() => {\n void loadEntries('order-statuses')\n void loadEntries('order-line-statuses')\n void loadEntries('shipment-statuses')\n void loadEntries('payment-statuses')\n }, [loadEntries, scopeVersion])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n }, [])\n\n const tableTranslations = React.useMemo(() => ({\n valueColumn: translate('sales.config.statuses.columns.value', 'Value'),\n labelColumn: translate('sales.config.statuses.columns.label', 'Label'),\n appearanceColumn: translate('sales.config.statuses.columns.appearance', 'Appearance'),\n addLabel: translate('sales.config.statuses.actions.add', 'Add status'),\n editLabel: translate('sales.config.statuses.actions.edit', 'Edit'),\n deleteLabel: translate('sales.config.statuses.actions.delete', 'Delete'),\n refreshLabel: translate('sales.config.statuses.actions.refresh', 'Refresh'),\n inheritedLabel: '',\n inheritedTooltip: '',\n emptyLabel: translate('sales.config.statuses.empty', 'No statuses yet.'),\n searchPlaceholder: translate('sales.config.statuses.search', 'Search statuses\u2026'),\n }), [translate])\n\n const formTranslations = React.useMemo(() => ({\n createTitle: translate('sales.config.statuses.dialog.addTitle', 'Add status'),\n editTitle: translate('sales.config.statuses.dialog.editTitle', 'Edit status'),\n valueLabel: translate('sales.config.statuses.dialog.valueLabel', 'Value'),\n labelLabel: translate('sales.config.statuses.dialog.labelLabel', 'Label'),\n saveLabel: translate('sales.config.statuses.dialog.save', 'Save'),\n cancelLabel: translate('sales.config.statuses.dialog.cancel', 'Cancel'),\n appearance: {\n colorLabel: translate('sales.config.statuses.dialog.colorLabel', 'Color'),\n colorHelp: translate('sales.config.statuses.dialog.colorHelp', 'Pick a highlight color for this status.'),\n colorClearLabel: translate('sales.config.statuses.dialog.colorClear', 'Remove color'),\n iconLabel: translate('sales.config.statuses.dialog.iconLabel', 'Icon'),\n iconPlaceholder: translate('sales.config.statuses.dialog.iconPlaceholder', 'Type an emoji or pick one of the suggestions.'),\n iconPickerTriggerLabel: translate('sales.config.statuses.dialog.iconBrowse', 'Browse icons and emojis'),\n iconSearchPlaceholder: translate('sales.config.statuses.dialog.iconSearchPlaceholder', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: translate('sales.config.statuses.dialog.iconSearchEmpty', 'No icons match your search.'),\n iconSuggestionsLabel: translate('sales.config.statuses.dialog.iconSuggestions', 'Suggestions'),\n iconClearLabel: translate('sales.config.statuses.dialog.iconClear', 'Remove icon'),\n previewEmptyLabel: translate('sales.config.statuses.appearance.empty', 'None'),\n },\n }), [translate])\n\n const startCreate = React.useCallback((kind: SalesStatusKind) => {\n setDialog({ mode: 'create', kind })\n }, [])\n\n const startEdit = React.useCallback((kind: SalesStatusKind, entry: DictionaryTableEntry) => {\n setDialog({ mode: 'edit', kind, entry })\n }, [])\n\n const deleteEntry = React.useCallback(async (kind: SalesStatusKind, entry: DictionaryTableEntry) => {\n const message = translate('sales.config.statuses.deleteConfirm', 'Delete status \"{{value}}\"?').replace('{{value}}', entry.label || entry.value)\n const confirmed = await confirm({\n title: message,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(entry.updatedAt),\n () =>\n apiCall(apiPaths[kind], {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id }),\n })\n )\n if (!call.ok) {\n await raiseCrudError(call.response, translate('sales.config.statuses.error.delete', 'Failed to delete status.'))\n }\n return call\n },\n context: mutationContext,\n mutationPayload: { action: 'delete', kind, id: entry.id },\n })\n flash(translate('sales.config.statuses.success.delete', 'Status deleted.'), 'success')\n await loadEntries(kind)\n } catch (err) {\n console.error('sales.statuses.delete failed', err)\n const message = err instanceof Error ? err.message : translate('sales.config.statuses.error.delete', 'Failed to delete status.')\n flash(message, 'error')\n }\n }, [apiPaths, confirm, loadEntries, mutationContext, runMutation, translate])\n\n const submitForm = React.useCallback(async (values: DictionaryFormValues) => {\n if (!dialog) return\n const path = apiPaths[dialog.kind]\n setSubmitting(true)\n try {\n if (dialog.mode === 'create') {\n await runMutation({\n operation: async () => {\n const call = await apiCall(path, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(values),\n })\n if (!call.ok) {\n await raiseCrudError(call.response, translate('sales.config.statuses.error.save', 'Failed to save status.'))\n }\n return call\n },\n context: mutationContext,\n mutationPayload: { action: 'create', kind: dialog.kind, ...values },\n })\n flash(translate('sales.config.statuses.success.save', 'Status saved.'), 'success')\n } else if (dialog.mode === 'edit') {\n const entry = dialog.entry\n const body: Record<string, unknown> = { id: entry.id }\n if (values.value !== entry.value) body.value = values.value\n if (values.label !== entry.label) body.label = values.label\n const nextColor = values.color ?? null\n if (nextColor !== (entry.color ?? null)) body.color = nextColor\n const nextIcon = values.icon ?? null\n if (nextIcon !== (entry.icon ?? null)) body.icon = nextIcon\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(entry.updatedAt),\n () =>\n apiCall(path, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n )\n if (!call.ok) {\n await raiseCrudError(call.response, translate('sales.config.statuses.error.save', 'Failed to save status.'))\n }\n return call\n },\n context: mutationContext,\n mutationPayload: { action: 'update', kind: dialog.kind, ...body },\n })\n flash(translate('sales.config.statuses.success.save', 'Status saved.'), 'success')\n }\n closeDialog()\n await loadEntries(dialog.kind)\n } catch (err) {\n console.error('sales.statuses.submit failed', err)\n const message = err instanceof Error ? err.message : translate('sales.config.statuses.error.save', 'Failed to save status.')\n flash(message, 'error')\n throw err instanceof Error ? err : new Error(message)\n } finally {\n setSubmitting(false)\n }\n }, [apiPaths, closeDialog, dialog, loadEntries, mutationContext, runMutation, translate])\n\n const currentValues = React.useMemo<DictionaryFormValues>(() => {\n if (dialog && dialog.mode === 'edit') {\n return {\n value: dialog.entry.value,\n label: dialog.entry.label,\n color: dialog.entry.color,\n icon: dialog.entry.icon,\n }\n }\n return DEFAULT_FORM_VALUES\n }, [dialog])\n\n return (\n <div className=\"space-y-6\">\n {sections.map((section) => {\n const items = entriesByKind[section.kind] ?? []\n const loading = loadingKind[section.kind] ?? false\n return (\n <section key={section.kind} className=\"rounded border bg-card text-card-foreground shadow-sm\">\n <div className=\"border-b px-6 py-4 space-y-1\">\n <h2 className=\"text-lg font-medium\">{section.title}</h2>\n <p className=\"text-sm text-muted-foreground\">{section.description}</p>\n </div>\n <div className=\"px-2 py-4 sm:px-4\">\n <DictionaryTable\n entries={items}\n loading={loading}\n canManage\n onCreate={() => startCreate(section.kind)}\n onEdit={(entry) => startEdit(section.kind, entry)}\n onDelete={(entry) => deleteEntry(section.kind, entry)}\n onRefresh={() => loadEntries(section.kind)}\n translations={{ ...tableTranslations, title: section.title }}\n />\n </div>\n </section>\n )\n })}\n\n <Dialog open={dialog !== null} onOpenChange={(open) => { if (!open) closeDialog() }}>\n <DialogContent className=\"max-w-lg\">\n <DialogHeader>\n <DialogTitle>\n {dialog?.mode === 'edit' ? formTranslations.editTitle : formTranslations.createTitle}\n </DialogTitle>\n </DialogHeader>\n <DictionaryForm\n mode={dialog?.mode === 'edit' ? 'edit' : 'create'}\n initialValues={currentValues}\n onSubmit={submitForm}\n onCancel={closeDialog}\n submitting={submitting}\n translations={{\n title: dialog?.mode === 'edit' ? formTranslations.editTitle : formTranslations.createTitle,\n valueLabel: formTranslations.valueLabel,\n labelLabel: formTranslations.labelLabel,\n saveLabel: formTranslations.saveLabel,\n cancelLabel: formTranslations.cancelLabel,\n appearance: formTranslations.appearance,\n }}\n iconSuggestions={ICON_SUGGESTIONS}\n />\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["message"]
7
7
  }
@@ -12,12 +12,26 @@ import { readApiResultOrThrow, withScopedApiRequestHeaders } from "@open-mercato
12
12
  import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
13
13
  import { deleteCrud } from "@open-mercato/ui/backend/utils/crud";
14
14
  import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
15
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
15
16
  import { useT } from "@open-mercato/shared/lib/i18n/context";
16
17
  import { mapOfferRow, renderOfferPriceSummary } from "./offerTableUtils.js";
17
18
  const PAGE_SIZE = 25;
19
+ const SAVE_CONTEXT_ID = "sales-channel-offers-panel";
18
20
  function SalesChannelOffersPanel({ channelId, channelName }) {
19
21
  const t = useT();
20
22
  const router = useRouter();
23
+ const { runMutation, retryLastMutation } = useGuardedMutation({
24
+ contextId: SAVE_CONTEXT_ID,
25
+ blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
26
+ });
27
+ const mutationContext = React.useMemo(
28
+ () => ({
29
+ formId: SAVE_CONTEXT_ID,
30
+ resourceKind: "sales.channels.offers",
31
+ retryLastMutation
32
+ }),
33
+ [retryLastMutation]
34
+ );
21
35
  const [rows, setRows] = React.useState([]);
22
36
  const [page, setPage] = React.useState(1);
23
37
  const [total, setTotal] = React.useState(0);
@@ -100,12 +114,16 @@ function SalesChannelOffersPanel({ channelId, channelName }) {
100
114
  }, [loadOffers, reloadToken]);
101
115
  const handleDelete = React.useCallback(async (row) => {
102
116
  try {
103
- await withScopedApiRequestHeaders(
104
- buildOptimisticLockHeader(row.updatedAt),
105
- () => deleteCrud("catalog/offers", row.id, {
106
- errorMessage: t("sales.channels.offers.errors.delete", "Failed to delete offer.")
107
- })
108
- );
117
+ await runMutation({
118
+ operation: () => withScopedApiRequestHeaders(
119
+ buildOptimisticLockHeader(row.updatedAt),
120
+ () => deleteCrud("catalog/offers", row.id, {
121
+ errorMessage: t("sales.channels.offers.errors.delete", "Failed to delete offer.")
122
+ })
123
+ ),
124
+ context: mutationContext,
125
+ mutationPayload: { action: "delete", id: row.id }
126
+ });
109
127
  flash(t("sales.channels.offers.messages.deleted", "Offer deleted."), "success");
110
128
  setReloadToken((token) => token + 1);
111
129
  } catch (err) {
@@ -113,7 +131,7 @@ function SalesChannelOffersPanel({ channelId, channelName }) {
113
131
  console.error("sales.channels.offers.delete", err);
114
132
  flash(t("sales.channels.offers.errors.delete", "Failed to delete offer."), "error");
115
133
  }
116
- }, [t]);
134
+ }, [mutationContext, runMutation, t]);
117
135
  const handleSearchChange = React.useCallback((value) => {
118
136
  setSearch(value);
119
137
  setPage(1);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sales/components/channels/SalesChannelOffersPanel.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { mapOfferRow, renderOfferPriceSummary, type OfferRow } from './offerTableUtils'\n\ntype OffersResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n totalPages?: number\n}\n\nconst PAGE_SIZE = 25\n\nexport function SalesChannelOffersPanel({ channelId, channelName }: { channelId: string; channelName?: string }) {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<OfferRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [isLoading, setLoading] = React.useState(true)\n const [sorting, setSorting] = React.useState<SortingState>([])\n const [reloadToken, setReloadToken] = React.useState(0)\n\n const columns = React.useMemo<ColumnDef<OfferRow>[]>(() => [\n {\n accessorKey: 'title',\n header: t('sales.channels.offers.table.offer', 'Offer'),\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n {row.original.productMediaUrl ? (\n <img\n src={row.original.productMediaUrl}\n alt={row.original.productTitle ?? row.original.title}\n className=\"h-12 w-12 rounded border object-cover\"\n />\n ) : (\n <div className=\"h-12 w-12 rounded border bg-muted\" />\n )}\n <div className=\"flex flex-col gap-1\">\n <div>\n <span className=\"font-medium\">{row.original.title}</span>\n <div className=\"text-xs text-muted-foreground\">\n {row.original.productTitle ?? t('sales.channels.offers.table.emptyProduct', 'Unlinked product')}\n </div>\n </div>\n </div>\n </div>\n ),\n meta: { sticky: true },\n },\n {\n accessorKey: 'pricing',\n header: t('sales.channels.offers.table.pricing', 'Prices'),\n cell: ({ row }) => (\n <div className=\"text-sm\">{renderOfferPriceSummary(row.original, t as any)}</div>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('sales.channels.offers.table.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isActive} />,\n },\n {\n accessorKey: 'updatedAt',\n header: t('sales.channels.offers.table.updated', 'Updated'),\n cell: ({ row }) =>\n row.original.updatedAt\n ? <span className=\"text-xs text-muted-foreground\">{new Date(row.original.updatedAt).toLocaleDateString()}</span>\n : <span className=\"text-xs text-muted-foreground\">\u2014</span>,\n },\n ], [t])\n\n const loadOffers = React.useCallback(async () => {\n if (!channelId) return\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(PAGE_SIZE),\n channelId,\n })\n if (search.trim().length) {\n params.set('search', search.trim())\n }\n const sort = sorting[0]\n if (sort?.id) {\n params.set('sortField', sort.id)\n params.set('sortDir', sort.desc ? 'desc' : 'asc')\n }\n const payload = await readApiResultOrThrow<OffersResponse>(\n `/api/catalog/offers?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.offers.errors.load', 'Failed to load offers.') },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items.map(mapOfferRow))\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))\n } catch (err) {\n console.error('sales.channels.offers', err)\n flash(t('sales.channels.offers.errors.load', 'Failed to load offers.'), 'error')\n } finally {\n setLoading(false)\n }\n }, [channelId, page, search, sorting, t])\n\n React.useEffect(() => {\n void loadOffers()\n }, [loadOffers, reloadToken])\n\n const handleDelete = React.useCallback(async (row: OfferRow) => {\n try {\n await withScopedApiRequestHeaders(buildOptimisticLockHeader(row.updatedAt), () =>\n deleteCrud('catalog/offers', row.id, {\n errorMessage: t('sales.channels.offers.errors.delete', 'Failed to delete offer.'),\n }),\n )\n flash(t('sales.channels.offers.messages.deleted', 'Offer deleted.'), 'success')\n setReloadToken((token) => token + 1)\n } catch (err) {\n if (surfaceRecordConflict(err, t)) return\n console.error('sales.channels.offers.delete', err)\n flash(t('sales.channels.offers.errors.delete', 'Failed to delete offer.'), 'error')\n }\n }, [t])\n\n const handleSearchChange = React.useCallback((value: string) => {\n setSearch(value)\n setPage(1)\n }, [])\n\n const tableTitle = (\n <div>\n <div className=\"text-lg font-semibold\">\n {t('sales.channels.offers.heading', 'Offers for {{name}}', { name: channelName || t('sales.channels.nav.title', 'Sales channels') })}\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t('sales.channels.offers.subtitle', 'Override product presentation and pricing per channel.')}\n </p>\n </div>\n )\n\n const actions = (\n <Button asChild>\n <Link href={`/backend/sales/channels/${channelId}/offers/create`}>\n {t('sales.channels.offers.actions.create', 'Add offer')}\n </Link>\n </Button>\n )\n\n return (\n <DataTable<OfferRow>\n title={tableTitle}\n actions={actions}\n columns={columns}\n data={rows}\n isLoading={isLoading}\n sorting={sorting}\n onSortingChange={setSorting}\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('sales.channels.offers.table.search', 'Search offers\u2026')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n refreshButton={{\n label: t('sales.channels.offers.table.refresh', 'Refresh'),\n onRefresh: () => setReloadToken((token) => token + 1),\n isRefreshing: isLoading,\n }}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('sales.channels.offers.actions.edit', 'Edit'),\n href: `/backend/sales/channels/${channelId}/offers/${row.id}/edit`,\n },\n {\n id: 'delete',\n label: t('sales.channels.offers.actions.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n destructive: true,\n },\n ]}\n />\n )}\n onRowClick={(row) => router.push(`/backend/sales/channels/${channelId}/offers/${row.id}/edit`)}\n emptyState={\n <div className=\"py-10 text-center text-sm text-muted-foreground\">\n {t('sales.channels.offers.table.empty', 'No offers for this channel yet.')}\n </div>\n }\n />\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { mapOfferRow, renderOfferPriceSummary, type OfferRow } from './offerTableUtils'\n\ntype OffersResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n totalPages?: number\n}\n\nconst PAGE_SIZE = 25\n\nconst SAVE_CONTEXT_ID = 'sales-channel-offers-panel'\n\nexport function SalesChannelOffersPanel({ channelId, channelName }: { channelId: string; channelName?: string }) {\n const t = useT()\n const router = useRouter()\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: SAVE_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({\n formId: SAVE_CONTEXT_ID,\n resourceKind: 'sales.channels.offers',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n const [rows, setRows] = React.useState<OfferRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [isLoading, setLoading] = React.useState(true)\n const [sorting, setSorting] = React.useState<SortingState>([])\n const [reloadToken, setReloadToken] = React.useState(0)\n\n const columns = React.useMemo<ColumnDef<OfferRow>[]>(() => [\n {\n accessorKey: 'title',\n header: t('sales.channels.offers.table.offer', 'Offer'),\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n {row.original.productMediaUrl ? (\n <img\n src={row.original.productMediaUrl}\n alt={row.original.productTitle ?? row.original.title}\n className=\"h-12 w-12 rounded border object-cover\"\n />\n ) : (\n <div className=\"h-12 w-12 rounded border bg-muted\" />\n )}\n <div className=\"flex flex-col gap-1\">\n <div>\n <span className=\"font-medium\">{row.original.title}</span>\n <div className=\"text-xs text-muted-foreground\">\n {row.original.productTitle ?? t('sales.channels.offers.table.emptyProduct', 'Unlinked product')}\n </div>\n </div>\n </div>\n </div>\n ),\n meta: { sticky: true },\n },\n {\n accessorKey: 'pricing',\n header: t('sales.channels.offers.table.pricing', 'Prices'),\n cell: ({ row }) => (\n <div className=\"text-sm\">{renderOfferPriceSummary(row.original, t as any)}</div>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('sales.channels.offers.table.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isActive} />,\n },\n {\n accessorKey: 'updatedAt',\n header: t('sales.channels.offers.table.updated', 'Updated'),\n cell: ({ row }) =>\n row.original.updatedAt\n ? <span className=\"text-xs text-muted-foreground\">{new Date(row.original.updatedAt).toLocaleDateString()}</span>\n : <span className=\"text-xs text-muted-foreground\">\u2014</span>,\n },\n ], [t])\n\n const loadOffers = React.useCallback(async () => {\n if (!channelId) return\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(PAGE_SIZE),\n channelId,\n })\n if (search.trim().length) {\n params.set('search', search.trim())\n }\n const sort = sorting[0]\n if (sort?.id) {\n params.set('sortField', sort.id)\n params.set('sortDir', sort.desc ? 'desc' : 'asc')\n }\n const payload = await readApiResultOrThrow<OffersResponse>(\n `/api/catalog/offers?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.offers.errors.load', 'Failed to load offers.') },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items.map(mapOfferRow))\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))\n } catch (err) {\n console.error('sales.channels.offers', err)\n flash(t('sales.channels.offers.errors.load', 'Failed to load offers.'), 'error')\n } finally {\n setLoading(false)\n }\n }, [channelId, page, search, sorting, t])\n\n React.useEffect(() => {\n void loadOffers()\n }, [loadOffers, reloadToken])\n\n const handleDelete = React.useCallback(async (row: OfferRow) => {\n try {\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(buildOptimisticLockHeader(row.updatedAt), () =>\n deleteCrud('catalog/offers', row.id, {\n errorMessage: t('sales.channels.offers.errors.delete', 'Failed to delete offer.'),\n }),\n ),\n context: mutationContext,\n mutationPayload: { action: 'delete', id: row.id },\n })\n flash(t('sales.channels.offers.messages.deleted', 'Offer deleted.'), 'success')\n setReloadToken((token) => token + 1)\n } catch (err) {\n if (surfaceRecordConflict(err, t)) return\n console.error('sales.channels.offers.delete', err)\n flash(t('sales.channels.offers.errors.delete', 'Failed to delete offer.'), 'error')\n }\n }, [mutationContext, runMutation, t])\n\n const handleSearchChange = React.useCallback((value: string) => {\n setSearch(value)\n setPage(1)\n }, [])\n\n const tableTitle = (\n <div>\n <div className=\"text-lg font-semibold\">\n {t('sales.channels.offers.heading', 'Offers for {{name}}', { name: channelName || t('sales.channels.nav.title', 'Sales channels') })}\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t('sales.channels.offers.subtitle', 'Override product presentation and pricing per channel.')}\n </p>\n </div>\n )\n\n const actions = (\n <Button asChild>\n <Link href={`/backend/sales/channels/${channelId}/offers/create`}>\n {t('sales.channels.offers.actions.create', 'Add offer')}\n </Link>\n </Button>\n )\n\n return (\n <DataTable<OfferRow>\n title={tableTitle}\n actions={actions}\n columns={columns}\n data={rows}\n isLoading={isLoading}\n sorting={sorting}\n onSortingChange={setSorting}\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('sales.channels.offers.table.search', 'Search offers\u2026')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n refreshButton={{\n label: t('sales.channels.offers.table.refresh', 'Refresh'),\n onRefresh: () => setReloadToken((token) => token + 1),\n isRefreshing: isLoading,\n }}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('sales.channels.offers.actions.edit', 'Edit'),\n href: `/backend/sales/channels/${channelId}/offers/${row.id}/edit`,\n },\n {\n id: 'delete',\n label: t('sales.channels.offers.actions.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n destructive: true,\n },\n ]}\n />\n )}\n onRowClick={(row) => router.push(`/backend/sales/channels/${channelId}/offers/${row.id}/edit`)}\n emptyState={\n <div className=\"py-10 text-center text-sm text-muted-foreground\">\n {t('sales.channels.offers.table.empty', 'No offers for this channel yet.')}\n </div>\n }\n />\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,9 +2,19 @@ import { NextResponse } from "next/server";
2
2
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
3
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
5
+ import {
6
+ runCrudMutationGuardAfterSuccess,
7
+ validateCrudMutationGuard
8
+ } from "@open-mercato/shared/lib/crud/mutation-guard";
5
9
  import { isShipmentCancelNotAllowedError } from "../../lib/status-sync.js";
6
10
  import { cancelShipmentSchema } from "../../data/validators.js";
7
11
  import { shippingCarriersTag } from "../openapi.js";
12
+ function resolveGuardUserId(auth) {
13
+ if (typeof auth.sub === "string" && auth.sub.trim().length > 0) return auth.sub;
14
+ if (typeof auth.userId === "string" && auth.userId.trim().length > 0) return auth.userId;
15
+ if (typeof auth.keyId === "string" && auth.keyId.trim().length > 0) return auth.keyId;
16
+ return "system";
17
+ }
8
18
  const metadata = {
9
19
  path: "/shipping-carriers/cancel",
10
20
  POST: { requireAuth: true, requireFeatures: ["shipping_carriers.manage"] }
@@ -20,6 +30,21 @@ async function POST(req) {
20
30
  return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 422 });
21
31
  }
22
32
  const container = await createRequestContainer();
33
+ const guardUserId = resolveGuardUserId(auth);
34
+ const guardResult = await validateCrudMutationGuard(container, {
35
+ tenantId: auth.tenantId,
36
+ organizationId: auth.orgId,
37
+ userId: guardUserId,
38
+ resourceKind: "shipping_carriers.shipment",
39
+ resourceId: parsed.data.shipmentId,
40
+ operation: "custom",
41
+ requestMethod: req.method,
42
+ requestHeaders: req.headers,
43
+ mutationPayload: parsed.data
44
+ });
45
+ if (guardResult && !guardResult.ok) {
46
+ return NextResponse.json(guardResult.body, { status: guardResult.status });
47
+ }
23
48
  const service = container.resolve("shippingCarrierService");
24
49
  try {
25
50
  const result = await service.cancelShipment({
@@ -27,6 +52,19 @@ async function POST(req) {
27
52
  organizationId: auth.orgId,
28
53
  tenantId: auth.tenantId
29
54
  });
55
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
56
+ await runCrudMutationGuardAfterSuccess(container, {
57
+ tenantId: auth.tenantId,
58
+ organizationId: auth.orgId,
59
+ userId: guardUserId,
60
+ resourceKind: "shipping_carriers.shipment",
61
+ resourceId: parsed.data.shipmentId,
62
+ operation: "custom",
63
+ requestMethod: req.method,
64
+ requestHeaders: req.headers,
65
+ metadata: guardResult.metadata ?? null
66
+ });
67
+ }
30
68
  return NextResponse.json(result);
31
69
  } catch (error) {
32
70
  if (isShipmentCancelNotAllowedError(error)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/shipping_carriers/api/cancel/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { ShippingCarrierService } from '../../lib/shipping-service'\nimport { isShipmentCancelNotAllowedError } from '../../lib/status-sync'\nimport { cancelShipmentSchema } from '../../data/validators'\nimport { shippingCarriersTag } from '../openapi'\n\nexport const metadata = {\n path: '/shipping-carriers/cancel',\n POST: { requireAuth: true, requireFeatures: ['shipping_carriers.manage'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const payload = await readJsonSafe<unknown>(req)\n const parsed = cancelShipmentSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n const container = await createRequestContainer()\n const service = container.resolve('shippingCarrierService') as ShippingCarrierService\n try {\n const result = await service.cancelShipment({\n ...parsed.data,\n organizationId: auth.orgId as string,\n tenantId: auth.tenantId,\n })\n return NextResponse.json(result)\n } catch (error: unknown) {\n if (isShipmentCancelNotAllowedError(error)) {\n return NextResponse.json({ error: error.message }, { status: 422 })\n }\n const message = error instanceof Error ? error.message : 'Failed to cancel shipment'\n return NextResponse.json({ error: message }, { status: 502 })\n }\n}\n\nexport const openApi = {\n tags: [shippingCarriersTag],\n summary: 'Cancel shipment',\n methods: {\n POST: {\n summary: 'Cancel shipment',\n tags: [shippingCarriersTag],\n responses: [\n { status: 200, description: 'Shipment cancelled' },\n { status: 422, description: 'Validation failed or shipment cannot be cancelled in its current status' },\n { status: 502, description: 'Provider upstream error' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAE7B,SAAS,uCAAuC;AAChD,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AAE7B,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC3E;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AACA,QAAM,UAAU,MAAM,aAAsB,GAAG;AAC/C,QAAM,SAAS,qBAAqB,UAAU,OAAO;AACrD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,SAAS,OAAO,MAAM,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzG;AACA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,UAAU,UAAU,QAAQ,wBAAwB;AAC1D,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,eAAe;AAAA,MAC1C,GAAG,OAAO;AAAA,MACV,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,IACjB,CAAC;AACD,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,OAAgB;AACvB,QAAI,gCAAgC,KAAK,GAAG;AAC1C,aAAO,aAAa,KAAK,EAAE,OAAO,MAAM,QAAQ,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpE;AACA,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,WAAO,aAAa,KAAK,EAAE,OAAO,QAAQ,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9D;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,mBAAmB;AAAA,EAC1B,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,MAAM,CAAC,mBAAmB;AAAA,MAC1B,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,QACjD,EAAE,QAAQ,KAAK,aAAa,0EAA0E;AAAA,QACtG,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { ShippingCarrierService } from '../../lib/shipping-service'\nimport { isShipmentCancelNotAllowedError } from '../../lib/status-sync'\nimport { cancelShipmentSchema } from '../../data/validators'\nimport { shippingCarriersTag } from '../openapi'\n\nfunction resolveGuardUserId(auth: {\n sub?: string | null\n userId?: string | null\n keyId?: string | null\n}): string {\n if (typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub\n if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId\n if (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId\n return 'system'\n}\n\nexport const metadata = {\n path: '/shipping-carriers/cancel',\n POST: { requireAuth: true, requireFeatures: ['shipping_carriers.manage'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const payload = await readJsonSafe<unknown>(req)\n const parsed = cancelShipmentSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n const container = await createRequestContainer()\n const guardUserId = resolveGuardUserId(auth)\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: guardUserId,\n resourceKind: 'shipping_carriers.shipment',\n resourceId: parsed.data.shipmentId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as Record<string, unknown>,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const service = container.resolve('shippingCarrierService') as ShippingCarrierService\n try {\n const result = await service.cancelShipment({\n ...parsed.data,\n organizationId: auth.orgId as string,\n tenantId: auth.tenantId,\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: guardUserId,\n resourceKind: 'shipping_carriers.shipment',\n resourceId: parsed.data.shipmentId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json(result)\n } catch (error: unknown) {\n if (isShipmentCancelNotAllowedError(error)) {\n return NextResponse.json({ error: error.message }, { status: 422 })\n }\n const message = error instanceof Error ? error.message : 'Failed to cancel shipment'\n return NextResponse.json({ error: message }, { status: 502 })\n }\n}\n\nexport const openApi = {\n tags: [shippingCarriersTag],\n summary: 'Cancel shipment',\n methods: {\n POST: {\n summary: 'Cancel shipment',\n tags: [shippingCarriersTag],\n responses: [\n { status: 200, description: 'Shipment cancelled' },\n { status: 422, description: 'Validation failed or shipment cannot be cancelled in its current status' },\n { status: 502, description: 'Provider upstream error' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,12 @@ import {
8
8
  } from "@open-mercato/shared/lib/crud/mutation-guard";
9
9
  import { createShipmentSchema } from "../../data/validators.js";
10
10
  import { shippingCarriersTag } from "../openapi.js";
11
+ function resolveGuardUserId(auth) {
12
+ if (typeof auth.sub === "string" && auth.sub.trim().length > 0) return auth.sub;
13
+ if (typeof auth.userId === "string" && auth.userId.trim().length > 0) return auth.userId;
14
+ if (typeof auth.keyId === "string" && auth.keyId.trim().length > 0) return auth.keyId;
15
+ return "system";
16
+ }
11
17
  const metadata = {
12
18
  path: "/shipping-carriers/shipments",
13
19
  POST: { requireAuth: true, requireFeatures: ["shipping_carriers.manage"] }
@@ -23,11 +29,11 @@ async function POST(req) {
23
29
  return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 422 });
24
30
  }
25
31
  const container = await createRequestContainer();
26
- const service = container.resolve("shippingCarrierService");
32
+ const guardUserId = resolveGuardUserId(auth);
27
33
  const guardResult = await validateCrudMutationGuard(container, {
28
34
  tenantId: auth.tenantId,
29
35
  organizationId: auth.orgId,
30
- userId: auth.sub ?? "",
36
+ userId: guardUserId,
31
37
  resourceKind: "shipping_carriers.shipment",
32
38
  resourceId: parsed.data.orderId,
33
39
  operation: "create",
@@ -38,19 +44,20 @@ async function POST(req) {
38
44
  if (guardResult && !guardResult.ok) {
39
45
  return NextResponse.json(guardResult.body, { status: guardResult.status });
40
46
  }
47
+ const service = container.resolve("shippingCarrierService");
41
48
  try {
42
49
  const shipment = await service.createShipment({
43
50
  ...parsed.data,
44
51
  organizationId: auth.orgId,
45
52
  tenantId: auth.tenantId
46
53
  });
47
- if (guardResult?.shouldRunAfterSuccess) {
54
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
48
55
  await runCrudMutationGuardAfterSuccess(container, {
49
56
  tenantId: auth.tenantId,
50
57
  organizationId: auth.orgId,
51
- userId: auth.sub ?? "",
58
+ userId: guardUserId,
52
59
  resourceKind: "shipping_carriers.shipment",
53
- resourceId: shipment.id,
60
+ resourceId: parsed.data.orderId,
54
61
  operation: "create",
55
62
  requestMethod: req.method,
56
63
  requestHeaders: req.headers,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/shipping_carriers/api/shipments/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { ShippingCarrierService } from '../../lib/shipping-service'\nimport { createShipmentSchema } from '../../data/validators'\nimport { shippingCarriersTag } from '../openapi'\n\nexport const metadata = {\n path: '/shipping-carriers/shipments',\n POST: { requireAuth: true, requireFeatures: ['shipping_carriers.manage'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const payload = await readJsonSafe<unknown>(req)\n const parsed = createShipmentSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n const container = await createRequestContainer()\n const service = container.resolve('shippingCarrierService') as ShippingCarrierService\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: 'shipping_carriers.shipment',\n resourceId: parsed.data.orderId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as unknown as Record<string, unknown>,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n try {\n const shipment = await service.createShipment({\n ...parsed.data,\n organizationId: auth.orgId as string,\n tenantId: auth.tenantId,\n })\n if (guardResult?.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: 'shipping_carriers.shipment',\n resourceId: shipment.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return NextResponse.json({\n shipmentId: shipment.id,\n carrierShipmentId: shipment.carrierShipmentId,\n trackingNumber: shipment.trackingNumber,\n status: shipment.unifiedStatus,\n labelUrl: shipment.labelUrl,\n }, { status: 201 })\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : 'Failed to create shipment'\n return NextResponse.json({ error: message }, { status: 502 })\n }\n}\n\nexport const openApi = {\n tags: [shippingCarriersTag],\n summary: 'Create shipment',\n methods: {\n POST: {\n summary: 'Create shipment',\n tags: [shippingCarriersTag],\n responses: [\n { status: 201, description: 'Shipment created' },\n { status: 422, description: 'Validation failed' },\n { status: 502, description: 'Provider upstream error' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AAE7B,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC3E;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AACA,QAAM,UAAU,MAAM,aAAsB,GAAG;AAC/C,QAAM,SAAS,qBAAqB,UAAU,OAAO;AACrD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,SAAS,OAAO,MAAM,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzG;AACA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,UAAU,UAAU,QAAQ,wBAAwB;AAC1D,QAAM,cAAc,MAAM,0BAA0B,WAAW;AAAA,IAC7D,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,IACrB,QAAQ,KAAK,OAAO;AAAA,IACpB,cAAc;AAAA,IACd,YAAY,OAAO,KAAK;AAAA,IACxB,WAAW;AAAA,IACX,eAAe,IAAI;AAAA,IACnB,gBAAgB,IAAI;AAAA,IACpB,iBAAiB,OAAO;AAAA,EAC1B,CAAC;AACD,MAAI,eAAe,CAAC,YAAY,IAAI;AAClC,WAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,EAC3E;AACA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,eAAe;AAAA,MAC5C,GAAG,OAAO;AAAA,MACV,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,IACjB,CAAC;AACD,QAAI,aAAa,uBAAuB;AACtC,YAAM,iCAAiC,WAAW;AAAA,QAChD,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK;AAAA,QACrB,QAAQ,KAAK,OAAO;AAAA,QACpB,cAAc;AAAA,QACd,YAAY,SAAS;AAAA,QACrB,WAAW;AAAA,QACX,eAAe,IAAI;AAAA,QACnB,gBAAgB,IAAI;AAAA,QACpB,UAAU,YAAY,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AACA,WAAO,aAAa,KAAK;AAAA,MACvB,YAAY,SAAS;AAAA,MACrB,mBAAmB,SAAS;AAAA,MAC5B,gBAAgB,SAAS;AAAA,MACzB,QAAQ,SAAS;AAAA,MACjB,UAAU,SAAS;AAAA,IACrB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpB,SAAS,OAAgB;AACvB,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,WAAO,aAAa,KAAK,EAAE,OAAO,QAAQ,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9D;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,mBAAmB;AAAA,EAC1B,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,MAAM,CAAC,mBAAmB;AAAA,MAC1B,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,oBAAoB;AAAA,QAChD,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { ShippingCarrierService } from '../../lib/shipping-service'\nimport { createShipmentSchema } from '../../data/validators'\nimport { shippingCarriersTag } from '../openapi'\n\nfunction resolveGuardUserId(auth: {\n sub?: string | null\n userId?: string | null\n keyId?: string | null\n}): string {\n if (typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub\n if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId\n if (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId\n return 'system'\n}\n\nexport const metadata = {\n path: '/shipping-carriers/shipments',\n POST: { requireAuth: true, requireFeatures: ['shipping_carriers.manage'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const payload = await readJsonSafe<unknown>(req)\n const parsed = createShipmentSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n const container = await createRequestContainer()\n const guardUserId = resolveGuardUserId(auth)\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: guardUserId,\n resourceKind: 'shipping_carriers.shipment',\n resourceId: parsed.data.orderId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as Record<string, unknown>,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const service = container.resolve('shippingCarrierService') as ShippingCarrierService\n try {\n const shipment = await service.createShipment({\n ...parsed.data,\n organizationId: auth.orgId as string,\n tenantId: auth.tenantId,\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: guardUserId,\n resourceKind: 'shipping_carriers.shipment',\n resourceId: parsed.data.orderId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n shipmentId: shipment.id,\n carrierShipmentId: shipment.carrierShipmentId,\n trackingNumber: shipment.trackingNumber,\n status: shipment.unifiedStatus,\n labelUrl: shipment.labelUrl,\n }, { status: 201 })\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : 'Failed to create shipment'\n return NextResponse.json({ error: message }, { status: 502 })\n }\n}\n\nexport const openApi = {\n tags: [shippingCarriersTag],\n summary: 'Create shipment',\n methods: {\n POST: {\n summary: 'Create shipment',\n tags: [shippingCarriersTag],\n responses: [\n { status: 201, description: 'Shipment created' },\n { status: 422, description: 'Validation failed' },\n { status: 502, description: 'Provider upstream error' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }