@open-mercato/core 0.6.6-develop.6257.1.6d0af84d26 → 0.6.6-develop.6286.1.d985c5c374

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 (208) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/carrier_shipment_idempotency_key/index.js +19 -0
  3. package/dist/generated/entities/carrier_shipment_idempotency_key/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +2 -1
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +10 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/lib/productResolution.js +45 -0
  9. package/dist/modules/catalog/lib/productResolution.js.map +7 -0
  10. package/dist/modules/communication_channels/workers/channel-import-history.js +1 -1
  11. package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
  12. package/dist/modules/currencies/api/fetch-configs/route.js +34 -0
  13. package/dist/modules/currencies/api/fetch-configs/route.js.map +2 -2
  14. package/dist/modules/currencies/components/CurrencyFetchingConfig.js +17 -15
  15. package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
  16. package/dist/modules/customers/api/companies/[id]/route.js +176 -161
  17. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  18. package/dist/modules/customers/api/people/[id]/route.js +75 -59
  19. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  20. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +17 -10
  21. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
  22. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +6 -4
  23. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +2 -2
  24. package/dist/modules/customers/components/detail/AiActionChips.js +5 -2
  25. package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
  26. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +19 -21
  27. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
  28. package/dist/modules/data_sync/api/runs/[id]/cancel.js +1 -1
  29. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  30. package/dist/modules/data_sync/backend/data-sync/page.js +9 -45
  31. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  32. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +11 -17
  33. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  34. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +12 -10
  35. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  36. package/dist/modules/data_sync/lib/id-mapping.js +46 -1
  37. package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
  38. package/dist/modules/data_sync/lib/sync-engine.js +2 -2
  39. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  40. package/dist/modules/data_sync/lib/syncRunStatus.js +30 -0
  41. package/dist/modules/data_sync/lib/syncRunStatus.js.map +7 -0
  42. package/dist/modules/entities/api/records.js +6 -0
  43. package/dist/modules/entities/api/records.js.map +2 -2
  44. package/dist/modules/feature_toggles/api/global/route.js +17 -15
  45. package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
  46. package/dist/modules/feature_toggles/api/openapi.js +2 -2
  47. package/dist/modules/feature_toggles/api/openapi.js.map +2 -2
  48. package/dist/modules/feature_toggles/commands/global.js +14 -0
  49. package/dist/modules/feature_toggles/commands/global.js.map +2 -2
  50. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +6 -0
  51. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  52. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +3 -1
  53. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
  54. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +12 -5
  55. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  56. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js +10 -1
  57. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js.map +2 -2
  58. package/dist/modules/payment_gateways/api/cancel/route.js +5 -0
  59. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  60. package/dist/modules/payment_gateways/api/capture/route.js +5 -0
  61. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  62. package/dist/modules/payment_gateways/api/refund/route.js +5 -0
  63. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  64. package/dist/modules/payment_gateways/lib/gateway-service.js +52 -25
  65. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  66. package/dist/modules/payment_gateways/lib/status-machine.js +13 -0
  67. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  68. package/dist/modules/perspectives/api/[tableId]/route.js +8 -2
  69. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  70. package/dist/modules/perspectives/data/entities.js +33 -3
  71. package/dist/modules/perspectives/data/entities.js.map +2 -2
  72. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js +63 -0
  73. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js.map +7 -0
  74. package/dist/modules/perspectives/services/perspectiveService.js +17 -0
  75. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  76. package/dist/modules/planner/commands/availability-weekly.js +33 -1
  77. package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
  78. package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -2
  79. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  80. package/dist/modules/progress/api/active/route.js +1 -1
  81. package/dist/modules/progress/api/active/route.js.map +2 -2
  82. package/dist/modules/progress/lib/progressService.js.map +1 -1
  83. package/dist/modules/progress/lib/progressServiceImpl.js +33 -8
  84. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  85. package/dist/modules/query_index/api/status.js +19 -24
  86. package/dist/modules/query_index/api/status.js.map +3 -3
  87. package/dist/modules/query_index/lib/coverage.js +35 -0
  88. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  89. package/dist/modules/query_index/lib/engine.js +11 -6
  90. package/dist/modules/query_index/lib/engine.js.map +2 -2
  91. package/dist/modules/resources/api/resource-types.js +5 -37
  92. package/dist/modules/resources/api/resource-types.js.map +2 -2
  93. package/dist/modules/resources/api/resourceTypeCounts.js +35 -0
  94. package/dist/modules/resources/api/resourceTypeCounts.js.map +7 -0
  95. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  96. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +1 -1
  97. package/dist/modules/resources/backend/resources/resource-types/page.js +2 -1
  98. package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
  99. package/dist/modules/sales/components/documents/ReturnDialog.js +4 -1
  100. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  101. package/dist/modules/sales/components/documents/ShipmentDialog.js +17 -2
  102. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  103. package/dist/modules/sales/di.js +4 -0
  104. package/dist/modules/sales/di.js.map +2 -2
  105. package/dist/modules/sales/services/salesOrderService.js +26 -0
  106. package/dist/modules/sales/services/salesOrderService.js.map +7 -0
  107. package/dist/modules/shipping_carriers/api/shipments/route.js +8 -0
  108. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  109. package/dist/modules/shipping_carriers/data/entities.js +38 -0
  110. package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
  111. package/dist/modules/shipping_carriers/data/validators.js +2 -1
  112. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  113. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js +76 -0
  114. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js.map +7 -0
  115. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +2 -1
  116. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +2 -2
  117. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +20 -2
  118. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  119. package/dist/modules/shipping_carriers/lib/shipping-service.js +82 -43
  120. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  121. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js +14 -0
  122. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js.map +7 -0
  123. package/dist/modules/staff/lib/timesheets-ui/ListView.js +15 -6
  124. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
  125. package/dist/modules/translations/components/TranslationManager.js +9 -18
  126. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  127. package/generated/entities/carrier_shipment_idempotency_key/index.ts +8 -0
  128. package/generated/entities.ids.generated.ts +2 -1
  129. package/generated/entity-fields-registry.ts +10 -0
  130. package/package.json +9 -8
  131. package/src/modules/catalog/lib/productResolution.ts +75 -0
  132. package/src/modules/communication_channels/workers/channel-import-history.ts +1 -1
  133. package/src/modules/currencies/api/fetch-configs/route.ts +36 -0
  134. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +20 -17
  135. package/src/modules/currencies/i18n/de.json +2 -1
  136. package/src/modules/currencies/i18n/en.json +2 -1
  137. package/src/modules/currencies/i18n/es.json +2 -1
  138. package/src/modules/currencies/i18n/pl.json +2 -1
  139. package/src/modules/customers/api/companies/[id]/route.ts +214 -189
  140. package/src/modules/customers/api/people/[id]/route.ts +106 -80
  141. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +19 -12
  142. package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +8 -6
  143. package/src/modules/customers/components/detail/AiActionChips.tsx +6 -3
  144. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +16 -18
  145. package/src/modules/data_sync/api/runs/[id]/cancel.ts +1 -1
  146. package/src/modules/data_sync/backend/data-sync/page.tsx +11 -58
  147. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +13 -20
  148. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +10 -8
  149. package/src/modules/data_sync/lib/id-mapping.ts +58 -1
  150. package/src/modules/data_sync/lib/sync-engine.ts +2 -2
  151. package/src/modules/data_sync/lib/syncRunStatus.ts +39 -0
  152. package/src/modules/entities/api/records.ts +14 -0
  153. package/src/modules/feature_toggles/api/global/route.ts +16 -14
  154. package/src/modules/feature_toggles/api/openapi.ts +2 -2
  155. package/src/modules/feature_toggles/commands/global.ts +15 -0
  156. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +7 -0
  157. package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +2 -0
  158. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +14 -4
  159. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.ts +15 -1
  160. package/src/modules/payment_gateways/api/cancel/route.ts +5 -0
  161. package/src/modules/payment_gateways/api/capture/route.ts +5 -0
  162. package/src/modules/payment_gateways/api/refund/route.ts +5 -0
  163. package/src/modules/payment_gateways/lib/gateway-service.ts +58 -25
  164. package/src/modules/payment_gateways/lib/status-machine.ts +16 -0
  165. package/src/modules/perspectives/api/[tableId]/route.ts +8 -2
  166. package/src/modules/perspectives/data/entities.ts +43 -3
  167. package/src/modules/perspectives/migrations/.snapshot-open-mercato.json +69 -25
  168. package/src/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.ts +65 -0
  169. package/src/modules/perspectives/services/perspectiveService.ts +17 -0
  170. package/src/modules/planner/commands/availability-weekly.ts +48 -1
  171. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +31 -10
  172. package/src/modules/progress/api/active/route.ts +1 -1
  173. package/src/modules/progress/lib/progressService.ts +2 -2
  174. package/src/modules/progress/lib/progressServiceImpl.ts +33 -8
  175. package/src/modules/query_index/api/status.ts +28 -32
  176. package/src/modules/query_index/lib/coverage.ts +63 -0
  177. package/src/modules/query_index/lib/engine.ts +37 -24
  178. package/src/modules/resources/api/resource-types.ts +4 -44
  179. package/src/modules/resources/api/resourceTypeCounts.ts +74 -0
  180. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  181. package/src/modules/resources/backend/resources/resource-types/page.tsx +1 -0
  182. package/src/modules/sales/components/documents/ReturnDialog.tsx +4 -1
  183. package/src/modules/sales/components/documents/ShipmentDialog.tsx +20 -2
  184. package/src/modules/sales/di.ts +6 -0
  185. package/src/modules/sales/services/salesOrderService.ts +38 -0
  186. package/src/modules/shipping_carriers/api/shipments/route.ts +8 -0
  187. package/src/modules/shipping_carriers/data/entities.ts +33 -0
  188. package/src/modules/shipping_carriers/data/validators.ts +1 -0
  189. package/src/modules/shipping_carriers/i18n/de.json +1 -0
  190. package/src/modules/shipping_carriers/i18n/en.json +1 -0
  191. package/src/modules/shipping_carriers/i18n/es.json +1 -0
  192. package/src/modules/shipping_carriers/i18n/pl.json +1 -0
  193. package/src/modules/shipping_carriers/lib/shipment-idempotency.ts +119 -0
  194. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +3 -1
  195. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +24 -2
  196. package/src/modules/shipping_carriers/lib/shipping-service.ts +87 -43
  197. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +197 -0
  198. package/src/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.ts +14 -0
  199. package/src/modules/staff/i18n/de.json +1 -0
  200. package/src/modules/staff/i18n/en.json +1 -0
  201. package/src/modules/staff/i18n/es.json +1 -0
  202. package/src/modules/staff/i18n/pl.json +1 -0
  203. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +15 -7
  204. package/src/modules/translations/components/TranslationManager.tsx +17 -24
  205. package/src/modules/translations/i18n/de.json +1 -0
  206. package/src/modules/translations/i18n/en.json +1 -0
  207. package/src/modules/translations/i18n/es.json +1 -0
  208. package/src/modules/translations/i18n/pl.json +1 -0
@@ -2,6 +2,15 @@ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find"
2
2
  import { CarrierShipment } from "../data/entities.js";
3
3
  import { emitShippingEvent } from "../events.js";
4
4
  import { getShippingAdapter } from "./adapter-registry.js";
5
+ import {
6
+ claimShipmentIdempotency,
7
+ computeShipmentRequestHash,
8
+ findShipmentIdempotencyClaim,
9
+ isShipmentIdempotencyConflictError,
10
+ releaseShipmentIdempotency,
11
+ resolveShipmentIdempotency,
12
+ ShipmentIdempotencyConflictError
13
+ } from "./shipment-idempotency.js";
5
14
  import {
6
15
  getTerminalShippingEvent,
7
16
  isValidShippingTransition,
@@ -54,49 +63,79 @@ function createShippingCarrierService(deps) {
54
63
  });
55
64
  },
56
65
  async createShipment(input) {
57
- const { adapter, credentials } = await resolveAdapter(input.providerKey, {
58
- organizationId: input.organizationId,
59
- tenantId: input.tenantId
60
- });
61
- const mergedCredentials = {
62
- ...credentials,
63
- ...input.senderPhone !== void 0 ? { senderPhone: input.senderPhone } : {},
64
- ...input.senderEmail !== void 0 ? { senderEmail: input.senderEmail } : {},
65
- ...input.receiverPhone !== void 0 ? { receiverPhone: input.receiverPhone } : {},
66
- ...input.receiverEmail !== void 0 ? { receiverEmail: input.receiverEmail } : {},
67
- ...input.targetPoint !== void 0 ? { targetPoint: input.targetPoint } : {},
68
- ...input.c2cSendingMethod !== void 0 ? { c2cSendingMethod: input.c2cSendingMethod } : {}
69
- };
70
- const created = await adapter.createShipment({
71
- orderId: input.orderId,
72
- origin: input.origin,
73
- destination: input.destination,
74
- packages: input.packages,
75
- serviceCode: input.serviceCode,
76
- credentials: mergedCredentials,
77
- labelFormat: input.labelFormat
78
- });
79
- const shipment = em.create(CarrierShipment, {
80
- orderId: input.orderId,
81
- providerKey: input.providerKey,
82
- carrierShipmentId: created.shipmentId,
83
- trackingNumber: created.trackingNumber,
84
- unifiedStatus: "label_created",
85
- labelUrl: created.labelUrl ?? null,
86
- labelData: created.labelData ?? null,
87
- organizationId: input.organizationId,
88
- tenantId: input.tenantId
89
- });
90
- await em.persist(shipment).flush();
91
- await emitShippingEvent("shipping_carriers.shipment.created", {
92
- shipmentId: shipment.id,
93
- orderId: input.orderId,
94
- providerKey: input.providerKey,
95
- trackingNumber: created.trackingNumber,
96
- organizationId: input.organizationId,
97
- tenantId: input.tenantId
98
- });
99
- return shipment;
66
+ const scope = { organizationId: input.organizationId, tenantId: input.tenantId };
67
+ let claim = null;
68
+ if (input.idempotencyKey) {
69
+ const { idempotencyKey: _idempotencyKey, ...fingerprint } = input;
70
+ const requestHash = computeShipmentRequestHash(fingerprint);
71
+ const existing = await findShipmentIdempotencyClaim(em, input.idempotencyKey, input.providerKey, scope);
72
+ if (existing) {
73
+ if (existing.requestHash !== requestHash) {
74
+ throw new ShipmentIdempotencyConflictError(input.idempotencyKey);
75
+ }
76
+ if (existing.shipmentId) {
77
+ return findShipmentOrThrow(existing.shipmentId, scope);
78
+ }
79
+ throw new ShipmentIdempotencyConflictError(input.idempotencyKey);
80
+ }
81
+ claim = await claimShipmentIdempotency(em, input.idempotencyKey, input.providerKey, requestHash, scope);
82
+ if (!claim) {
83
+ throw new ShipmentIdempotencyConflictError(input.idempotencyKey);
84
+ }
85
+ }
86
+ let carrierShipmentCreated = false;
87
+ try {
88
+ const { adapter, credentials } = await resolveAdapter(input.providerKey, scope);
89
+ const mergedCredentials = {
90
+ ...credentials,
91
+ ...input.senderPhone !== void 0 ? { senderPhone: input.senderPhone } : {},
92
+ ...input.senderEmail !== void 0 ? { senderEmail: input.senderEmail } : {},
93
+ ...input.receiverPhone !== void 0 ? { receiverPhone: input.receiverPhone } : {},
94
+ ...input.receiverEmail !== void 0 ? { receiverEmail: input.receiverEmail } : {},
95
+ ...input.targetPoint !== void 0 ? { targetPoint: input.targetPoint } : {},
96
+ ...input.c2cSendingMethod !== void 0 ? { c2cSendingMethod: input.c2cSendingMethod } : {}
97
+ };
98
+ const created = await adapter.createShipment({
99
+ orderId: input.orderId,
100
+ origin: input.origin,
101
+ destination: input.destination,
102
+ packages: input.packages,
103
+ serviceCode: input.serviceCode,
104
+ credentials: mergedCredentials,
105
+ labelFormat: input.labelFormat
106
+ });
107
+ carrierShipmentCreated = true;
108
+ const shipment = em.create(CarrierShipment, {
109
+ orderId: input.orderId,
110
+ providerKey: input.providerKey,
111
+ carrierShipmentId: created.shipmentId,
112
+ trackingNumber: created.trackingNumber,
113
+ unifiedStatus: "label_created",
114
+ labelUrl: created.labelUrl ?? null,
115
+ labelData: created.labelData ?? null,
116
+ organizationId: input.organizationId,
117
+ tenantId: input.tenantId
118
+ });
119
+ await em.persist(shipment).flush();
120
+ if (claim) {
121
+ await resolveShipmentIdempotency(em, claim, shipment.id);
122
+ }
123
+ await emitShippingEvent("shipping_carriers.shipment.created", {
124
+ shipmentId: shipment.id,
125
+ orderId: input.orderId,
126
+ providerKey: input.providerKey,
127
+ trackingNumber: created.trackingNumber,
128
+ organizationId: input.organizationId,
129
+ tenantId: input.tenantId
130
+ });
131
+ return shipment;
132
+ } catch (error) {
133
+ if (claim && !carrierShipmentCreated && input.idempotencyKey && !isShipmentIdempotencyConflictError(error)) {
134
+ await releaseShipmentIdempotency(em, input.idempotencyKey, input.providerKey, scope).catch(() => {
135
+ });
136
+ }
137
+ throw error;
138
+ }
100
139
  },
101
140
  async getTracking(input) {
102
141
  const { adapter, credentials } = await resolveAdapter(input.providerKey, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/shipping_carriers/lib/shipping-service.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { CredentialsService } from '../../integrations/lib/credentials-service'\nimport { CarrierShipment } from '../data/entities'\nimport { emitShippingEvent } from '../events'\nimport { getShippingAdapter } from './adapter-registry'\nimport type { UnifiedShipmentStatus } from './adapter'\nimport {\n getTerminalShippingEvent,\n isValidShippingTransition,\n ShipmentCancelNotAllowedError,\n syncShipmentStatus,\n TERMINAL_SHIPPING_STATUSES,\n} from './status-sync'\n\nexport function createShippingCarrierService(deps: {\n em: EntityManager\n integrationCredentialsService: CredentialsService\n}) {\n const { em, integrationCredentialsService } = deps\n\n async function findShipmentOrThrow(\n shipmentId: string,\n scope: { organizationId: string; tenantId: string },\n ): Promise<CarrierShipment> {\n const shipment = await findOneWithDecryption(\n em,\n CarrierShipment,\n {\n id: shipmentId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!shipment) {\n throw new Error('Shipment not found')\n }\n return shipment\n }\n\n async function resolveAdapter(providerKey: string, scope: { organizationId: string; tenantId: string }) {\n const adapter = getShippingAdapter(providerKey)\n if (!adapter) throw new Error(`No shipping adapter registered for provider: ${providerKey}`)\n const credentials = await integrationCredentialsService.resolve(`carrier_${providerKey}`, scope) ?? {}\n return { adapter, credentials }\n }\n\n return {\n async calculateRates(input: {\n providerKey: string\n origin: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }\n destination: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }\n packages: Array<{ weightKg: number; lengthCm: number; widthCm: number; heightCm: number }>\n receiverPhone?: string\n receiverEmail?: string\n organizationId: string\n tenantId: string\n }) {\n const { adapter, credentials } = await resolveAdapter(input.providerKey, {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n const mergedCredentials = {\n ...credentials,\n ...(input.receiverPhone !== undefined ? { receiverPhone: input.receiverPhone } : {}),\n ...(input.receiverEmail !== undefined ? { receiverEmail: input.receiverEmail } : {}),\n }\n return adapter.calculateRates({\n origin: input.origin,\n destination: input.destination,\n packages: input.packages,\n credentials: mergedCredentials,\n })\n },\n\n async createShipment(input: {\n providerKey: string\n orderId: string\n origin: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }\n destination: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }\n packages: Array<{ weightKg: number; lengthCm: number; widthCm: number; heightCm: number }>\n serviceCode: string\n labelFormat?: 'pdf' | 'zpl' | 'png'\n senderPhone?: string\n senderEmail?: string\n receiverPhone?: string\n receiverEmail?: string\n targetPoint?: string\n c2cSendingMethod?: string\n organizationId: string\n tenantId: string\n }) {\n const { adapter, credentials } = await resolveAdapter(input.providerKey, {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n const mergedCredentials = {\n ...credentials,\n ...(input.senderPhone !== undefined ? { senderPhone: input.senderPhone } : {}),\n ...(input.senderEmail !== undefined ? { senderEmail: input.senderEmail } : {}),\n ...(input.receiverPhone !== undefined ? { receiverPhone: input.receiverPhone } : {}),\n ...(input.receiverEmail !== undefined ? { receiverEmail: input.receiverEmail } : {}),\n ...(input.targetPoint !== undefined ? { targetPoint: input.targetPoint } : {}),\n ...(input.c2cSendingMethod !== undefined ? { c2cSendingMethod: input.c2cSendingMethod } : {}),\n }\n const created = await adapter.createShipment({\n orderId: input.orderId,\n origin: input.origin,\n destination: input.destination,\n packages: input.packages,\n serviceCode: input.serviceCode,\n credentials: mergedCredentials,\n labelFormat: input.labelFormat,\n })\n const shipment = em.create(CarrierShipment, {\n orderId: input.orderId,\n providerKey: input.providerKey,\n carrierShipmentId: created.shipmentId,\n trackingNumber: created.trackingNumber,\n unifiedStatus: 'label_created',\n labelUrl: created.labelUrl ?? null,\n labelData: created.labelData ?? null,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n await em.persist(shipment).flush()\n await emitShippingEvent('shipping_carriers.shipment.created', {\n shipmentId: shipment.id,\n orderId: input.orderId,\n providerKey: input.providerKey,\n trackingNumber: created.trackingNumber,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n return shipment\n },\n\n async getTracking(input: {\n providerKey: string\n shipmentId?: string\n trackingNumber?: string\n organizationId: string\n tenantId: string\n }) {\n const { adapter, credentials } = await resolveAdapter(input.providerKey, {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n const shipment = input.shipmentId\n ? await findOneWithDecryption(\n em,\n CarrierShipment,\n {\n id: input.shipmentId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n deletedAt: null,\n },\n undefined,\n {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n },\n )\n : null\n return adapter.getTracking({\n shipmentId: shipment?.carrierShipmentId ?? input.shipmentId,\n trackingNumber: input.trackingNumber ?? shipment?.trackingNumber,\n credentials,\n })\n },\n\n async refreshTracking(input: {\n providerKey: string\n shipmentId: string\n organizationId: string\n tenantId: string\n }) {\n const scope = { organizationId: input.organizationId, tenantId: input.tenantId }\n const shipment = await findShipmentOrThrow(input.shipmentId, scope)\n const { adapter, credentials } = await resolveAdapter(input.providerKey, scope)\n const tracking = await adapter.getTracking({\n shipmentId: shipment.carrierShipmentId,\n trackingNumber: shipment.trackingNumber,\n credentials,\n })\n const previousStatus = shipment.unifiedStatus\n shipment.trackingEvents = tracking.events\n shipment.lastPolledAt = new Date()\n const transitionApplied = syncShipmentStatus(shipment, tracking.status)\n await em.flush()\n if (transitionApplied) {\n const eventPayload = {\n shipmentId: shipment.id,\n providerKey: input.providerKey,\n previousStatus,\n newStatus: tracking.status,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n }\n await emitShippingEvent('shipping_carriers.shipment.status_changed', eventPayload)\n if (TERMINAL_SHIPPING_STATUSES.has(tracking.status)) {\n const terminalEvent = getTerminalShippingEvent(tracking.status)\n if (terminalEvent) await emitShippingEvent(terminalEvent, eventPayload)\n }\n }\n return tracking\n },\n\n async cancelShipment(input: {\n providerKey: string\n shipmentId: string\n reason?: string\n organizationId: string\n tenantId: string\n }) {\n const scope = { organizationId: input.organizationId, tenantId: input.tenantId }\n const shipment = await findShipmentOrThrow(input.shipmentId, scope)\n if (!isValidShippingTransition(shipment.unifiedStatus as UnifiedShipmentStatus, 'cancelled')) {\n throw new ShipmentCancelNotAllowedError(shipment.unifiedStatus)\n }\n const { adapter, credentials } = await resolveAdapter(input.providerKey, {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n const result = await adapter.cancelShipment({\n shipmentId: shipment.carrierShipmentId,\n reason: input.reason,\n credentials,\n })\n shipment.unifiedStatus = result.status\n await em.flush()\n await emitShippingEvent('shipping_carriers.shipment.cancelled', {\n shipmentId: shipment.id,\n providerKey: input.providerKey,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n return result\n },\n\n async findShipmentByCarrierId(\n providerKey: string,\n carrierShipmentId: string,\n scope: { organizationId: string; tenantId: string },\n ) {\n return findOneWithDecryption(\n em,\n CarrierShipment,\n {\n providerKey,\n carrierShipmentId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async searchDropOffPoints(input: {\n providerKey: string\n query?: string\n type?: string\n postCode?: string\n organizationId: string\n tenantId: string\n }) {\n const { adapter, credentials } = await resolveAdapter(input.providerKey, {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n if (!adapter.searchDropOffPoints) {\n throw new Error(`Provider ${input.providerKey} does not support drop-off point search`)\n }\n return adapter.searchDropOffPoints({\n query: input.query,\n type: input.type,\n postCode: input.postCode,\n credentials,\n })\n },\n }\n}\n\nexport type ShippingCarrierService = ReturnType<typeof createShippingCarrierService>\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { CredentialsService } from '../../integrations/lib/credentials-service'\nimport { CarrierShipment, type CarrierShipmentIdempotencyKey } from '../data/entities'\nimport { emitShippingEvent } from '../events'\nimport { getShippingAdapter } from './adapter-registry'\nimport type { UnifiedShipmentStatus } from './adapter'\nimport {\n claimShipmentIdempotency,\n computeShipmentRequestHash,\n findShipmentIdempotencyClaim,\n isShipmentIdempotencyConflictError,\n releaseShipmentIdempotency,\n resolveShipmentIdempotency,\n ShipmentIdempotencyConflictError,\n} from './shipment-idempotency'\nimport {\n getTerminalShippingEvent,\n isValidShippingTransition,\n ShipmentCancelNotAllowedError,\n syncShipmentStatus,\n TERMINAL_SHIPPING_STATUSES,\n} from './status-sync'\n\nexport function createShippingCarrierService(deps: {\n em: EntityManager\n integrationCredentialsService: CredentialsService\n}) {\n const { em, integrationCredentialsService } = deps\n\n async function findShipmentOrThrow(\n shipmentId: string,\n scope: { organizationId: string; tenantId: string },\n ): Promise<CarrierShipment> {\n const shipment = await findOneWithDecryption(\n em,\n CarrierShipment,\n {\n id: shipmentId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!shipment) {\n throw new Error('Shipment not found')\n }\n return shipment\n }\n\n async function resolveAdapter(providerKey: string, scope: { organizationId: string; tenantId: string }) {\n const adapter = getShippingAdapter(providerKey)\n if (!adapter) throw new Error(`No shipping adapter registered for provider: ${providerKey}`)\n const credentials = await integrationCredentialsService.resolve(`carrier_${providerKey}`, scope) ?? {}\n return { adapter, credentials }\n }\n\n return {\n async calculateRates(input: {\n providerKey: string\n origin: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }\n destination: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }\n packages: Array<{ weightKg: number; lengthCm: number; widthCm: number; heightCm: number }>\n receiverPhone?: string\n receiverEmail?: string\n organizationId: string\n tenantId: string\n }) {\n const { adapter, credentials } = await resolveAdapter(input.providerKey, {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n const mergedCredentials = {\n ...credentials,\n ...(input.receiverPhone !== undefined ? { receiverPhone: input.receiverPhone } : {}),\n ...(input.receiverEmail !== undefined ? { receiverEmail: input.receiverEmail } : {}),\n }\n return adapter.calculateRates({\n origin: input.origin,\n destination: input.destination,\n packages: input.packages,\n credentials: mergedCredentials,\n })\n },\n\n async createShipment(input: {\n providerKey: string\n orderId: string\n origin: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }\n destination: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }\n packages: Array<{ weightKg: number; lengthCm: number; widthCm: number; heightCm: number }>\n serviceCode: string\n labelFormat?: 'pdf' | 'zpl' | 'png'\n senderPhone?: string\n senderEmail?: string\n receiverPhone?: string\n receiverEmail?: string\n targetPoint?: string\n c2cSendingMethod?: string\n idempotencyKey?: string\n organizationId: string\n tenantId: string\n }) {\n const scope = { organizationId: input.organizationId, tenantId: input.tenantId }\n let claim: CarrierShipmentIdempotencyKey | null = null\n if (input.idempotencyKey) {\n const { idempotencyKey: _idempotencyKey, ...fingerprint } = input\n const requestHash = computeShipmentRequestHash(fingerprint)\n const existing = await findShipmentIdempotencyClaim(em, input.idempotencyKey, input.providerKey, scope)\n if (existing) {\n if (existing.requestHash !== requestHash) {\n throw new ShipmentIdempotencyConflictError(input.idempotencyKey)\n }\n if (existing.shipmentId) {\n return findShipmentOrThrow(existing.shipmentId, scope)\n }\n // Claim exists but the original request has not resolved yet (concurrent in-flight).\n throw new ShipmentIdempotencyConflictError(input.idempotencyKey)\n }\n claim = await claimShipmentIdempotency(em, input.idempotencyKey, input.providerKey, requestHash, scope)\n if (!claim) {\n // Lost the claim race with a concurrent request creating the same shipment.\n throw new ShipmentIdempotencyConflictError(input.idempotencyKey)\n }\n }\n let carrierShipmentCreated = false\n try {\n const { adapter, credentials } = await resolveAdapter(input.providerKey, scope)\n const mergedCredentials = {\n ...credentials,\n ...(input.senderPhone !== undefined ? { senderPhone: input.senderPhone } : {}),\n ...(input.senderEmail !== undefined ? { senderEmail: input.senderEmail } : {}),\n ...(input.receiverPhone !== undefined ? { receiverPhone: input.receiverPhone } : {}),\n ...(input.receiverEmail !== undefined ? { receiverEmail: input.receiverEmail } : {}),\n ...(input.targetPoint !== undefined ? { targetPoint: input.targetPoint } : {}),\n ...(input.c2cSendingMethod !== undefined ? { c2cSendingMethod: input.c2cSendingMethod } : {}),\n }\n const created = await adapter.createShipment({\n orderId: input.orderId,\n origin: input.origin,\n destination: input.destination,\n packages: input.packages,\n serviceCode: input.serviceCode,\n credentials: mergedCredentials,\n labelFormat: input.labelFormat,\n })\n carrierShipmentCreated = true\n const shipment = em.create(CarrierShipment, {\n orderId: input.orderId,\n providerKey: input.providerKey,\n carrierShipmentId: created.shipmentId,\n trackingNumber: created.trackingNumber,\n unifiedStatus: 'label_created',\n labelUrl: created.labelUrl ?? null,\n labelData: created.labelData ?? null,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n await em.persist(shipment).flush()\n if (claim) {\n await resolveShipmentIdempotency(em, claim, shipment.id)\n }\n await emitShippingEvent('shipping_carriers.shipment.created', {\n shipmentId: shipment.id,\n orderId: input.orderId,\n providerKey: input.providerKey,\n trackingNumber: created.trackingNumber,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n return shipment\n } catch (error: unknown) {\n // Release the claim only when the carrier was NOT successfully called, so a retry can\n // proceed. If the carrier already created the shipment, keep the claim so a retry does\n // not produce a duplicate upstream shipment.\n if (claim && !carrierShipmentCreated && input.idempotencyKey && !isShipmentIdempotencyConflictError(error)) {\n await releaseShipmentIdempotency(em, input.idempotencyKey, input.providerKey, scope).catch(() => {})\n }\n throw error\n }\n },\n\n async getTracking(input: {\n providerKey: string\n shipmentId?: string\n trackingNumber?: string\n organizationId: string\n tenantId: string\n }) {\n const { adapter, credentials } = await resolveAdapter(input.providerKey, {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n const shipment = input.shipmentId\n ? await findOneWithDecryption(\n em,\n CarrierShipment,\n {\n id: input.shipmentId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n deletedAt: null,\n },\n undefined,\n {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n },\n )\n : null\n return adapter.getTracking({\n shipmentId: shipment?.carrierShipmentId ?? input.shipmentId,\n trackingNumber: input.trackingNumber ?? shipment?.trackingNumber,\n credentials,\n })\n },\n\n async refreshTracking(input: {\n providerKey: string\n shipmentId: string\n organizationId: string\n tenantId: string\n }) {\n const scope = { organizationId: input.organizationId, tenantId: input.tenantId }\n const shipment = await findShipmentOrThrow(input.shipmentId, scope)\n const { adapter, credentials } = await resolveAdapter(input.providerKey, scope)\n const tracking = await adapter.getTracking({\n shipmentId: shipment.carrierShipmentId,\n trackingNumber: shipment.trackingNumber,\n credentials,\n })\n const previousStatus = shipment.unifiedStatus\n shipment.trackingEvents = tracking.events\n shipment.lastPolledAt = new Date()\n const transitionApplied = syncShipmentStatus(shipment, tracking.status)\n await em.flush()\n if (transitionApplied) {\n const eventPayload = {\n shipmentId: shipment.id,\n providerKey: input.providerKey,\n previousStatus,\n newStatus: tracking.status,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n }\n await emitShippingEvent('shipping_carriers.shipment.status_changed', eventPayload)\n if (TERMINAL_SHIPPING_STATUSES.has(tracking.status)) {\n const terminalEvent = getTerminalShippingEvent(tracking.status)\n if (terminalEvent) await emitShippingEvent(terminalEvent, eventPayload)\n }\n }\n return tracking\n },\n\n async cancelShipment(input: {\n providerKey: string\n shipmentId: string\n reason?: string\n organizationId: string\n tenantId: string\n }) {\n const scope = { organizationId: input.organizationId, tenantId: input.tenantId }\n const shipment = await findShipmentOrThrow(input.shipmentId, scope)\n if (!isValidShippingTransition(shipment.unifiedStatus as UnifiedShipmentStatus, 'cancelled')) {\n throw new ShipmentCancelNotAllowedError(shipment.unifiedStatus)\n }\n const { adapter, credentials } = await resolveAdapter(input.providerKey, {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n const result = await adapter.cancelShipment({\n shipmentId: shipment.carrierShipmentId,\n reason: input.reason,\n credentials,\n })\n shipment.unifiedStatus = result.status\n await em.flush()\n await emitShippingEvent('shipping_carriers.shipment.cancelled', {\n shipmentId: shipment.id,\n providerKey: input.providerKey,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n return result\n },\n\n async findShipmentByCarrierId(\n providerKey: string,\n carrierShipmentId: string,\n scope: { organizationId: string; tenantId: string },\n ) {\n return findOneWithDecryption(\n em,\n CarrierShipment,\n {\n providerKey,\n carrierShipmentId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async searchDropOffPoints(input: {\n providerKey: string\n query?: string\n type?: string\n postCode?: string\n organizationId: string\n tenantId: string\n }) {\n const { adapter, credentials } = await resolveAdapter(input.providerKey, {\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n if (!adapter.searchDropOffPoints) {\n throw new Error(`Provider ${input.providerKey} does not support drop-off point search`)\n }\n return adapter.searchDropOffPoints({\n query: input.query,\n type: input.type,\n postCode: input.postCode,\n credentials,\n })\n },\n }\n}\n\nexport type ShippingCarrierService = ReturnType<typeof createShippingCarrierService>\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,14 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260618173616_shipping_carriers extends Migration {
3
+ up() {
4
+ this.addSql(`create table "carrier_shipment_idempotency_keys" ("id" uuid not null default gen_random_uuid(), "provider_key" text not null, "idempotency_key" text not null, "request_hash" text not null, "shipment_id" uuid null, "organization_id" uuid not null, "tenant_id" uuid not null, "created_at" timestamptz not null, primary key ("id"));`);
5
+ this.addSql(`alter table "carrier_shipment_idempotency_keys" add constraint "carrier_shipment_idempotency_keys_unique" unique ("idempotency_key", "provider_key", "organization_id", "tenant_id");`);
6
+ }
7
+ down() {
8
+ this.addSql(`drop table if exists "carrier_shipment_idempotency_keys";`);
9
+ }
10
+ }
11
+ export {
12
+ Migration20260618173616_shipping_carriers
13
+ };
14
+ //# sourceMappingURL=Migration20260618173616_shipping_carriers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260618173616_shipping_carriers extends Migration {\n\n override up(): void | Promise<void> {\n this.addSql(`create table \"carrier_shipment_idempotency_keys\" (\"id\" uuid not null default gen_random_uuid(), \"provider_key\" text not null, \"idempotency_key\" text not null, \"request_hash\" text not null, \"shipment_id\" uuid null, \"organization_id\" uuid not null, \"tenant_id\" uuid not null, \"created_at\" timestamptz not null, primary key (\"id\"));`);\n this.addSql(`alter table \"carrier_shipment_idempotency_keys\" add constraint \"carrier_shipment_idempotency_keys_unique\" unique (\"idempotency_key\", \"provider_key\", \"organization_id\", \"tenant_id\");`);\n }\n\n override down(): void | Promise<void> {\n this.addSql(`drop table if exists \"carrier_shipment_idempotency_keys\";`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,kDAAkD,UAAU;AAAA,EAE9D,KAA2B;AAClC,SAAK,OAAO,2UAA2U;AACvV,SAAK,OAAO,uLAAuL;AAAA,EACrM;AAAA,EAES,OAA6B;AACpC,SAAK,OAAO,2DAA2D;AAAA,EACzE;AAEF;",
6
+ "names": []
7
+ }
@@ -2,7 +2,9 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useT } from "@open-mercato/shared/lib/i18n/context";
5
- import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
6
+ import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
7
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
6
8
  import { ProjectColorDot } from "./ProjectColorDot.js";
7
9
  function getLocalDateStr(date) {
8
10
  const year = date.getFullYear();
@@ -47,6 +49,7 @@ function InlineDescription({
47
49
  placeholder,
48
50
  onSaved
49
51
  }) {
52
+ const t = useT();
50
53
  const [editing, setEditing] = React.useState(false);
51
54
  const [value, setValue] = React.useState(initialValue ?? "");
52
55
  const [saving, setSaving] = React.useState(false);
@@ -62,18 +65,24 @@ function InlineDescription({
62
65
  }
63
66
  setSaving(true);
64
67
  try {
65
- await apiCall(`/api/staff/timesheets/time-entries`, {
68
+ await apiCallOrThrow(`/api/staff/timesheets/time-entries`, {
66
69
  method: "PUT",
67
70
  headers: { "Content-Type": "application/json" },
68
71
  body: JSON.stringify({ id: entryId, notes: trimmed || null })
69
72
  });
70
- onSaved?.();
71
- } catch {
72
- } finally {
73
73
  setSaving(false);
74
74
  setEditing(false);
75
+ onSaved?.();
76
+ } catch (err) {
77
+ setSaving(false);
78
+ if (!surfaceRecordConflict(err, t)) {
79
+ flash(
80
+ t("staff.timesheets.my.list.saveError", "Failed to save description. Please try again."),
81
+ "error"
82
+ );
83
+ }
75
84
  }
76
- }, [entryId, value, initialValue, onSaved]);
85
+ }, [entryId, value, initialValue, onSaved, t]);
77
86
  if (!editing) {
78
87
  return /* @__PURE__ */ jsx(
79
88
  "button",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/staff/lib/timesheets-ui/ListView.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { ProjectColorDot } from './ProjectColorDot'\n\ntype TimeEntry = {\n id: string\n date: string\n durationMinutes: number\n projectId: string\n projectName: string\n projectCode: string | null\n projectColor: string | null\n notes: string | null\n source: string\n startedAt: string | null\n endedAt: string | null\n}\n\ntype ListViewProps = {\n entries: TimeEntry[]\n onEntryUpdated?: () => void\n}\n\nfunction getLocalDateStr(date: Date): string {\n const year = date.getFullYear()\n const month = String(date.getMonth() + 1).padStart(2, '0')\n const day = String(date.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\nfunction formatDayLabel(dateStr: string, translate: TranslateFn): string {\n const today = new Date()\n const todayStr = getLocalDateStr(today)\n\n const yesterday = new Date(today)\n yesterday.setDate(yesterday.getDate() - 1)\n const yesterdayStr = getLocalDateStr(yesterday)\n\n if (dateStr === todayStr) return translate('staff.timesheets.my.list.today', 'Today')\n if (dateStr === yesterdayStr) return translate('staff.timesheets.my.list.yesterday', 'Yesterday')\n\n const date = new Date(dateStr + 'T00:00:00')\n const dayName = date.toLocaleDateString(undefined, { weekday: 'short' })\n const day = date.getDate()\n const month = date.toLocaleDateString(undefined, { month: 'short' })\n\n return `${dayName}, ${day} ${month}`\n}\n\nfunction formatDuration(minutes: number): string {\n if (minutes < 60) return `${minutes}m`\n\n const hours = Math.floor(minutes / 60)\n const remainingMinutes = minutes % 60\n\n if (remainingMinutes === 0) return `${hours}h`\n return `${hours}h ${remainingMinutes}m`\n}\n\nfunction formatTimeRange(startedAt: string, endedAt: string): string {\n const start = new Date(startedAt)\n const end = new Date(endedAt)\n\n const format = (date: Date) =>\n date.toLocaleTimeString(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n hour12: true,\n })\n\n return `${format(start)} - ${format(end)}`\n}\n\nfunction InlineDescription({\n entryId,\n initialValue,\n placeholder,\n onSaved,\n}: {\n entryId: string\n initialValue: string | null\n placeholder: string\n onSaved?: () => void\n}) {\n const [editing, setEditing] = React.useState(false)\n const [value, setValue] = React.useState(initialValue ?? '')\n const [saving, setSaving] = React.useState(false)\n const inputRef = React.useRef<HTMLInputElement>(null)\n\n React.useEffect(() => {\n if (editing && inputRef.current) inputRef.current.focus()\n }, [editing])\n\n const save = React.useCallback(async () => {\n const trimmed = value.trim()\n if (trimmed === (initialValue ?? '')) {\n setEditing(false)\n return\n }\n setSaving(true)\n try {\n await apiCall(`/api/staff/timesheets/time-entries`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: entryId, notes: trimmed || null }),\n })\n onSaved?.()\n } catch {\n // silent\n } finally {\n setSaving(false)\n setEditing(false)\n }\n }, [entryId, value, initialValue, onSaved])\n\n if (!editing) {\n return (\n <button\n type=\"button\"\n onClick={() => setEditing(true)}\n className={`text-sm text-left cursor-pointer hover:underline ${\n initialValue ? '' : 'text-muted-foreground italic'\n }`}\n >\n {initialValue || placeholder}\n </button>\n )\n }\n\n return (\n <input\n ref={inputRef}\n type=\"text\"\n value={value}\n disabled={saving}\n onChange={(e) => setValue(e.target.value)}\n onBlur={() => { void save() }}\n onKeyDown={(e) => {\n if (e.key === 'Enter') { void save() }\n if (e.key === 'Escape') { setValue(initialValue ?? ''); setEditing(false) }\n }}\n placeholder={placeholder}\n className=\"text-sm w-full bg-transparent border-b border-primary/40 outline-none py-0.5 placeholder:text-muted-foreground/50\"\n />\n )\n}\n\nexport function ListView({ entries, onEntryUpdated }: ListViewProps) {\n const t = useT()\n\n if (entries.length === 0) {\n return (\n <div className=\"flex items-center justify-center py-12 text-sm text-muted-foreground\">\n {t('staff.timesheets.my.list.noEntries', 'No entries for this period.')}\n </div>\n )\n }\n\n const grouped = new Map<string, TimeEntry[]>()\n for (const entry of entries) {\n const existing = grouped.get(entry.date)\n if (existing) {\n existing.push(entry)\n } else {\n grouped.set(entry.date, [entry])\n }\n }\n\n const sortedDays = [...grouped.keys()].sort((a, b) => b.localeCompare(a))\n\n return (\n <div>\n {sortedDays.map((dateStr) => {\n const dayEntries = grouped.get(dateStr)!\n const dailyTotal = dayEntries.reduce(\n (sum, entry) => sum + entry.durationMinutes,\n 0,\n )\n\n return (\n <div key={dateStr} className=\"rounded-lg border mb-4\">\n <div className=\"flex justify-between items-center p-4 border-b bg-muted/30 font-medium text-sm\">\n <span>{formatDayLabel(dateStr, t)}</span>\n <span className=\"font-mono tabular-nums\">\n {formatDuration(dailyTotal)}\n </span>\n </div>\n\n {dayEntries.map((entry) => (\n <div\n key={entry.id}\n className=\"flex items-center justify-between p-3 border-b last:border-0 hover:bg-muted/20\"\n >\n <div className=\"flex flex-col gap-1 min-w-0 flex-1 mr-4\">\n <InlineDescription\n entryId={entry.id}\n initialValue={entry.notes}\n placeholder={t('staff.timesheets.my.list.addDescription', 'Add description')}\n onSaved={onEntryUpdated}\n />\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-muted-foreground\">\n <ProjectColorDot colorKey={entry.projectColor} projectName={entry.projectName} size=\"xs\" />\n {entry.projectName}\n </span>\n </div>\n\n <div className=\"flex flex-col items-end gap-1 shrink-0\">\n {entry.source === 'timer' &&\n entry.startedAt &&\n entry.endedAt ? (\n <span className=\"text-xs text-muted-foreground\">\n {formatTimeRange(entry.startedAt, entry.endedAt)}\n </span>\n ) : null}\n <span className=\"text-sm font-mono tabular-nums\">\n {formatDuration(entry.durationMinutes)}\n </span>\n </div>\n </div>\n ))}\n </div>\n )\n })}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { ProjectColorDot } from './ProjectColorDot'\n\ntype TimeEntry = {\n id: string\n date: string\n durationMinutes: number\n projectId: string\n projectName: string\n projectCode: string | null\n projectColor: string | null\n notes: string | null\n source: string\n startedAt: string | null\n endedAt: string | null\n}\n\ntype ListViewProps = {\n entries: TimeEntry[]\n onEntryUpdated?: () => void\n}\n\nfunction getLocalDateStr(date: Date): string {\n const year = date.getFullYear()\n const month = String(date.getMonth() + 1).padStart(2, '0')\n const day = String(date.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\nfunction formatDayLabel(dateStr: string, translate: TranslateFn): string {\n const today = new Date()\n const todayStr = getLocalDateStr(today)\n\n const yesterday = new Date(today)\n yesterday.setDate(yesterday.getDate() - 1)\n const yesterdayStr = getLocalDateStr(yesterday)\n\n if (dateStr === todayStr) return translate('staff.timesheets.my.list.today', 'Today')\n if (dateStr === yesterdayStr) return translate('staff.timesheets.my.list.yesterday', 'Yesterday')\n\n const date = new Date(dateStr + 'T00:00:00')\n const dayName = date.toLocaleDateString(undefined, { weekday: 'short' })\n const day = date.getDate()\n const month = date.toLocaleDateString(undefined, { month: 'short' })\n\n return `${dayName}, ${day} ${month}`\n}\n\nfunction formatDuration(minutes: number): string {\n if (minutes < 60) return `${minutes}m`\n\n const hours = Math.floor(minutes / 60)\n const remainingMinutes = minutes % 60\n\n if (remainingMinutes === 0) return `${hours}h`\n return `${hours}h ${remainingMinutes}m`\n}\n\nfunction formatTimeRange(startedAt: string, endedAt: string): string {\n const start = new Date(startedAt)\n const end = new Date(endedAt)\n\n const format = (date: Date) =>\n date.toLocaleTimeString(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n hour12: true,\n })\n\n return `${format(start)} - ${format(end)}`\n}\n\nfunction InlineDescription({\n entryId,\n initialValue,\n placeholder,\n onSaved,\n}: {\n entryId: string\n initialValue: string | null\n placeholder: string\n onSaved?: () => void\n}) {\n const t = useT()\n const [editing, setEditing] = React.useState(false)\n const [value, setValue] = React.useState(initialValue ?? '')\n const [saving, setSaving] = React.useState(false)\n const inputRef = React.useRef<HTMLInputElement>(null)\n\n React.useEffect(() => {\n if (editing && inputRef.current) inputRef.current.focus()\n }, [editing])\n\n const save = React.useCallback(async () => {\n const trimmed = value.trim()\n if (trimmed === (initialValue ?? '')) {\n setEditing(false)\n return\n }\n setSaving(true)\n try {\n await apiCallOrThrow(`/api/staff/timesheets/time-entries`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: entryId, notes: trimmed || null }),\n })\n setSaving(false)\n setEditing(false)\n onSaved?.()\n } catch (err) {\n setSaving(false)\n if (!surfaceRecordConflict(err, t)) {\n flash(\n t('staff.timesheets.my.list.saveError', 'Failed to save description. Please try again.'),\n 'error',\n )\n }\n }\n }, [entryId, value, initialValue, onSaved, t])\n\n if (!editing) {\n return (\n <button\n type=\"button\"\n onClick={() => setEditing(true)}\n className={`text-sm text-left cursor-pointer hover:underline ${\n initialValue ? '' : 'text-muted-foreground italic'\n }`}\n >\n {initialValue || placeholder}\n </button>\n )\n }\n\n return (\n <input\n ref={inputRef}\n type=\"text\"\n value={value}\n disabled={saving}\n onChange={(e) => setValue(e.target.value)}\n onBlur={() => { void save() }}\n onKeyDown={(e) => {\n if (e.key === 'Enter') { void save() }\n if (e.key === 'Escape') { setValue(initialValue ?? ''); setEditing(false) }\n }}\n placeholder={placeholder}\n className=\"text-sm w-full bg-transparent border-b border-primary/40 outline-none py-0.5 placeholder:text-muted-foreground/50\"\n />\n )\n}\n\nexport function ListView({ entries, onEntryUpdated }: ListViewProps) {\n const t = useT()\n\n if (entries.length === 0) {\n return (\n <div className=\"flex items-center justify-center py-12 text-sm text-muted-foreground\">\n {t('staff.timesheets.my.list.noEntries', 'No entries for this period.')}\n </div>\n )\n }\n\n const grouped = new Map<string, TimeEntry[]>()\n for (const entry of entries) {\n const existing = grouped.get(entry.date)\n if (existing) {\n existing.push(entry)\n } else {\n grouped.set(entry.date, [entry])\n }\n }\n\n const sortedDays = [...grouped.keys()].sort((a, b) => b.localeCompare(a))\n\n return (\n <div>\n {sortedDays.map((dateStr) => {\n const dayEntries = grouped.get(dateStr)!\n const dailyTotal = dayEntries.reduce(\n (sum, entry) => sum + entry.durationMinutes,\n 0,\n )\n\n return (\n <div key={dateStr} className=\"rounded-lg border mb-4\">\n <div className=\"flex justify-between items-center p-4 border-b bg-muted/30 font-medium text-sm\">\n <span>{formatDayLabel(dateStr, t)}</span>\n <span className=\"font-mono tabular-nums\">\n {formatDuration(dailyTotal)}\n </span>\n </div>\n\n {dayEntries.map((entry) => (\n <div\n key={entry.id}\n className=\"flex items-center justify-between p-3 border-b last:border-0 hover:bg-muted/20\"\n >\n <div className=\"flex flex-col gap-1 min-w-0 flex-1 mr-4\">\n <InlineDescription\n entryId={entry.id}\n initialValue={entry.notes}\n placeholder={t('staff.timesheets.my.list.addDescription', 'Add description')}\n onSaved={onEntryUpdated}\n />\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-muted-foreground\">\n <ProjectColorDot colorKey={entry.projectColor} projectName={entry.projectName} size=\"xs\" />\n {entry.projectName}\n </span>\n </div>\n\n <div className=\"flex flex-col items-end gap-1 shrink-0\">\n {entry.source === 'timer' &&\n entry.startedAt &&\n entry.endedAt ? (\n <span className=\"text-xs text-muted-foreground\">\n {formatTimeRange(entry.startedAt, entry.endedAt)}\n </span>\n ) : null}\n <span className=\"text-sm font-mono tabular-nums\">\n {formatDuration(entry.durationMinutes)}\n </span>\n </div>\n </div>\n ))}\n </div>\n )\n })}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
5
  import { Button } from "@open-mercato/ui/primitives/button";
6
+ import { IconButton } from "@open-mercato/ui/primitives/icon-button";
7
+ import { Tabs, TabsList, TabsTrigger } from "@open-mercato/ui/primitives/tabs";
6
8
  import { Input } from "@open-mercato/ui/primitives/input";
7
9
  import { ComboboxInput } from "@open-mercato/ui/backend/inputs";
8
10
  import { LoadingMessage, ErrorMessage } from "@open-mercato/ui/backend/detail";
@@ -272,21 +274,7 @@ function TranslationManager({
272
274
  )
273
275
  ] });
274
276
  };
275
- const renderLocaleTabs = () => /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b", children: locales.map((locale) => {
276
- const isActive = activeLocale === locale;
277
- return /* @__PURE__ */ jsx(
278
- "button",
279
- {
280
- type: "button",
281
- "data-state": isActive ? "active" : "inactive",
282
- "data-locale": locale,
283
- className: `px-3 py-1.5 text-sm font-medium transition-colors ${isActive ? "border-b-2 border-accent-indigo text-foreground" : "text-muted-foreground hover:text-foreground"}`,
284
- onClick: () => setActiveLocale(locale),
285
- children: locale.toUpperCase()
286
- },
287
- locale
288
- );
289
- }) });
277
+ const renderLocaleTabs = () => /* @__PURE__ */ jsx(Tabs, { variant: "underline", value: activeLocale, onValueChange: setActiveLocale, children: /* @__PURE__ */ jsx(TabsList, { children: locales.map((locale) => /* @__PURE__ */ jsx(TabsTrigger, { value: locale, children: locale.toUpperCase() }, locale)) }) });
290
278
  const renderFieldTable = () => {
291
279
  if (!entityType || !recordId) {
292
280
  return /* @__PURE__ */ jsx("div", { className: "rounded border bg-background/80 p-4 text-sm text-muted-foreground", children: t("translations.manager.selectFirst", "Select an entity and record to manage translations.") });
@@ -496,10 +484,13 @@ function LocaleManager() {
496
484
  locale.toUpperCase(),
497
485
  getIso639Label(locale) ? ` \u2014 ${getIso639Label(locale)}` : "",
498
486
  locales.length > 1 && /* @__PURE__ */ jsx(
499
- "button",
487
+ IconButton,
500
488
  {
501
- type: "button",
502
- className: "rounded-full p-0.5 text-muted-foreground hover:text-foreground transition-colors",
489
+ variant: "ghost",
490
+ size: "xs",
491
+ fullRadius: true,
492
+ "aria-label": t("translations.locales.remove", "Remove {{locale}}", { locale: getIso639Label(locale) ?? locale.toUpperCase() }),
493
+ title: t("translations.locales.remove", "Remove {{locale}}", { locale: getIso639Label(locale) ?? locale.toUpperCase() }),
503
494
  onClick: () => removeLocale(locale),
504
495
  disabled: mutation.isPending,
505
496
  children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })