@open-mercato/core 0.6.6-develop.6256.1.9fc16aedc4 → 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 (234) 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/api/[id]/conversation/archive/route.js +25 -7
  53. package/dist/modules/messages/api/[id]/conversation/archive/route.js.map +2 -2
  54. package/dist/modules/messages/api/[id]/conversation/read/route.js +24 -6
  55. package/dist/modules/messages/api/[id]/conversation/read/route.js.map +2 -2
  56. package/dist/modules/messages/api/[id]/route.js +13 -1
  57. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  58. package/dist/modules/messages/api/openapi.js +3 -1
  59. package/dist/modules/messages/api/openapi.js.map +2 -2
  60. package/dist/modules/messages/commands/conversation.js +77 -0
  61. package/dist/modules/messages/commands/conversation.js.map +2 -2
  62. package/dist/modules/messages/components/MessageDetailPageClient.js +16 -4
  63. package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
  64. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +3 -1
  65. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
  66. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +29 -4
  67. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  68. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js +10 -1
  69. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js.map +2 -2
  70. package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js +13 -11
  71. package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js.map +2 -2
  72. package/dist/modules/payment_gateways/api/cancel/route.js +5 -0
  73. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  74. package/dist/modules/payment_gateways/api/capture/route.js +5 -0
  75. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  76. package/dist/modules/payment_gateways/api/refund/route.js +5 -0
  77. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  78. package/dist/modules/payment_gateways/lib/gateway-service.js +52 -25
  79. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  80. package/dist/modules/payment_gateways/lib/status-machine.js +13 -0
  81. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  82. package/dist/modules/perspectives/api/[tableId]/route.js +8 -2
  83. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  84. package/dist/modules/perspectives/data/entities.js +33 -3
  85. package/dist/modules/perspectives/data/entities.js.map +2 -2
  86. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js +63 -0
  87. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js.map +7 -0
  88. package/dist/modules/perspectives/services/perspectiveService.js +17 -0
  89. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  90. package/dist/modules/planner/commands/availability-weekly.js +33 -1
  91. package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
  92. package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -2
  93. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  94. package/dist/modules/progress/api/active/route.js +1 -1
  95. package/dist/modules/progress/api/active/route.js.map +2 -2
  96. package/dist/modules/progress/lib/progressService.js.map +1 -1
  97. package/dist/modules/progress/lib/progressServiceImpl.js +33 -8
  98. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  99. package/dist/modules/query_index/api/status.js +19 -24
  100. package/dist/modules/query_index/api/status.js.map +3 -3
  101. package/dist/modules/query_index/lib/coverage.js +35 -0
  102. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  103. package/dist/modules/query_index/lib/engine.js +11 -6
  104. package/dist/modules/query_index/lib/engine.js.map +2 -2
  105. package/dist/modules/resources/api/resource-types.js +5 -37
  106. package/dist/modules/resources/api/resource-types.js.map +2 -2
  107. package/dist/modules/resources/api/resourceTypeCounts.js +35 -0
  108. package/dist/modules/resources/api/resourceTypeCounts.js.map +7 -0
  109. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  110. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +1 -1
  111. package/dist/modules/resources/backend/resources/resource-types/page.js +2 -1
  112. package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
  113. package/dist/modules/sales/components/documents/ReturnDialog.js +4 -1
  114. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  115. package/dist/modules/sales/components/documents/ShipmentDialog.js +17 -2
  116. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  117. package/dist/modules/sales/di.js +4 -0
  118. package/dist/modules/sales/di.js.map +2 -2
  119. package/dist/modules/sales/services/salesOrderService.js +26 -0
  120. package/dist/modules/sales/services/salesOrderService.js.map +7 -0
  121. package/dist/modules/shipping_carriers/api/shipments/route.js +8 -0
  122. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  123. package/dist/modules/shipping_carriers/data/entities.js +38 -0
  124. package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
  125. package/dist/modules/shipping_carriers/data/validators.js +2 -1
  126. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  127. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js +76 -0
  128. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js.map +7 -0
  129. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +2 -1
  130. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +2 -2
  131. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +20 -2
  132. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  133. package/dist/modules/shipping_carriers/lib/shipping-service.js +82 -43
  134. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  135. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js +14 -0
  136. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js.map +7 -0
  137. package/dist/modules/staff/lib/timesheets-ui/ListView.js +15 -6
  138. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
  139. package/dist/modules/translations/components/TranslationManager.js +9 -18
  140. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  141. package/generated/entities/carrier_shipment_idempotency_key/index.ts +8 -0
  142. package/generated/entities.ids.generated.ts +2 -1
  143. package/generated/entity-fields-registry.ts +10 -0
  144. package/package.json +9 -8
  145. package/src/modules/catalog/lib/productResolution.ts +75 -0
  146. package/src/modules/communication_channels/workers/channel-import-history.ts +1 -1
  147. package/src/modules/currencies/api/fetch-configs/route.ts +36 -0
  148. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +20 -17
  149. package/src/modules/currencies/i18n/de.json +2 -1
  150. package/src/modules/currencies/i18n/en.json +2 -1
  151. package/src/modules/currencies/i18n/es.json +2 -1
  152. package/src/modules/currencies/i18n/pl.json +2 -1
  153. package/src/modules/customers/api/companies/[id]/route.ts +214 -189
  154. package/src/modules/customers/api/people/[id]/route.ts +106 -80
  155. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +19 -12
  156. package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +8 -6
  157. package/src/modules/customers/components/detail/AiActionChips.tsx +6 -3
  158. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +16 -18
  159. package/src/modules/data_sync/api/runs/[id]/cancel.ts +1 -1
  160. package/src/modules/data_sync/backend/data-sync/page.tsx +11 -58
  161. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +13 -20
  162. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +10 -8
  163. package/src/modules/data_sync/lib/id-mapping.ts +58 -1
  164. package/src/modules/data_sync/lib/sync-engine.ts +2 -2
  165. package/src/modules/data_sync/lib/syncRunStatus.ts +39 -0
  166. package/src/modules/entities/api/records.ts +14 -0
  167. package/src/modules/feature_toggles/api/global/route.ts +16 -14
  168. package/src/modules/feature_toggles/api/openapi.ts +2 -2
  169. package/src/modules/feature_toggles/commands/global.ts +15 -0
  170. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +7 -0
  171. package/src/modules/messages/api/[id]/conversation/archive/route.ts +29 -6
  172. package/src/modules/messages/api/[id]/conversation/read/route.ts +29 -6
  173. package/src/modules/messages/api/[id]/route.ts +15 -0
  174. package/src/modules/messages/api/openapi.ts +2 -0
  175. package/src/modules/messages/commands/conversation.ts +87 -0
  176. package/src/modules/messages/components/MessageDetailPageClient.tsx +16 -4
  177. package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +2 -0
  178. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +39 -4
  179. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.ts +15 -1
  180. package/src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx +29 -14
  181. package/src/modules/messages/components/message-detail/types.ts +2 -0
  182. package/src/modules/messages/i18n/de.json +4 -0
  183. package/src/modules/messages/i18n/en.json +4 -0
  184. package/src/modules/messages/i18n/es.json +4 -0
  185. package/src/modules/messages/i18n/pl.json +4 -0
  186. package/src/modules/payment_gateways/api/cancel/route.ts +5 -0
  187. package/src/modules/payment_gateways/api/capture/route.ts +5 -0
  188. package/src/modules/payment_gateways/api/refund/route.ts +5 -0
  189. package/src/modules/payment_gateways/lib/gateway-service.ts +58 -25
  190. package/src/modules/payment_gateways/lib/status-machine.ts +16 -0
  191. package/src/modules/perspectives/api/[tableId]/route.ts +8 -2
  192. package/src/modules/perspectives/data/entities.ts +43 -3
  193. package/src/modules/perspectives/migrations/.snapshot-open-mercato.json +69 -25
  194. package/src/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.ts +65 -0
  195. package/src/modules/perspectives/services/perspectiveService.ts +17 -0
  196. package/src/modules/planner/commands/availability-weekly.ts +48 -1
  197. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +31 -10
  198. package/src/modules/progress/api/active/route.ts +1 -1
  199. package/src/modules/progress/lib/progressService.ts +2 -2
  200. package/src/modules/progress/lib/progressServiceImpl.ts +33 -8
  201. package/src/modules/query_index/api/status.ts +28 -32
  202. package/src/modules/query_index/lib/coverage.ts +63 -0
  203. package/src/modules/query_index/lib/engine.ts +37 -24
  204. package/src/modules/resources/api/resource-types.ts +4 -44
  205. package/src/modules/resources/api/resourceTypeCounts.ts +74 -0
  206. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  207. package/src/modules/resources/backend/resources/resource-types/page.tsx +1 -0
  208. package/src/modules/sales/components/documents/ReturnDialog.tsx +4 -1
  209. package/src/modules/sales/components/documents/ShipmentDialog.tsx +20 -2
  210. package/src/modules/sales/di.ts +6 -0
  211. package/src/modules/sales/services/salesOrderService.ts +38 -0
  212. package/src/modules/shipping_carriers/api/shipments/route.ts +8 -0
  213. package/src/modules/shipping_carriers/data/entities.ts +33 -0
  214. package/src/modules/shipping_carriers/data/validators.ts +1 -0
  215. package/src/modules/shipping_carriers/i18n/de.json +1 -0
  216. package/src/modules/shipping_carriers/i18n/en.json +1 -0
  217. package/src/modules/shipping_carriers/i18n/es.json +1 -0
  218. package/src/modules/shipping_carriers/i18n/pl.json +1 -0
  219. package/src/modules/shipping_carriers/lib/shipment-idempotency.ts +119 -0
  220. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +3 -1
  221. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +24 -2
  222. package/src/modules/shipping_carriers/lib/shipping-service.ts +87 -43
  223. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +197 -0
  224. package/src/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.ts +14 -0
  225. package/src/modules/staff/i18n/de.json +1 -0
  226. package/src/modules/staff/i18n/en.json +1 -0
  227. package/src/modules/staff/i18n/es.json +1 -0
  228. package/src/modules/staff/i18n/pl.json +1 -0
  229. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +15 -7
  230. package/src/modules/translations/components/TranslationManager.tsx +17 -24
  231. package/src/modules/translations/i18n/de.json +1 -0
  232. package/src/modules/translations/i18n/en.json +1 -0
  233. package/src/modules/translations/i18n/es.json +1 -0
  234. package/src/modules/translations/i18n/pl.json +1 -0
@@ -5,6 +5,7 @@ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
5
  import { FormHeader } from '@open-mercato/ui/backend/forms'
6
6
  import { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'
7
7
  import { Badge } from '@open-mercato/ui/primitives/badge'
8
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
8
9
  import { Button } from '@open-mercato/ui/primitives/button'
9
10
  import { LogList, type LogListEntry } from '@open-mercato/ui/backend/LogList'
10
11
  import { Progress } from '@open-mercato/ui/primitives/progress'
@@ -15,6 +16,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
15
16
  import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
16
17
  import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
17
18
  import { RotateCcw, XCircle } from 'lucide-react'
19
+ import { getSyncRunStatusVariant } from '../../../../lib/syncRunStatus'
18
20
 
19
21
  type SyncRunDetail = {
20
22
  id: string
@@ -69,15 +71,6 @@ function formatEtaSeconds(seconds: number): string {
69
71
  return `${hours}h ${minutes}m`
70
72
  }
71
73
 
72
- const STATUS_STYLES: Record<string, string> = {
73
- pending: 'bg-gray-100 text-gray-800',
74
- running: 'bg-blue-100 text-blue-800',
75
- completed: 'bg-green-100 text-green-800',
76
- failed: 'bg-red-100 text-red-800',
77
- cancelled: 'bg-yellow-100 text-yellow-800',
78
- paused: 'bg-orange-100 text-orange-800',
79
- }
80
-
81
74
  type SyncRunDetailPageProps = {
82
75
  params?: {
83
76
  id?: string | string[]
@@ -284,9 +277,9 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
284
277
  statusBadge={(
285
278
  <div className="mt-2 flex flex-wrap gap-2">
286
279
  <Badge variant="outline">{t(`data_sync.dashboard.direction.${run.direction}`)}</Badge>
287
- <Badge variant="secondary" className={STATUS_STYLES[run.status] ?? ''}>
280
+ <StatusBadge variant={getSyncRunStatusVariant(run.status)}>
288
281
  {t(`data_sync.dashboard.status.${run.status}`)}
289
- </Badge>
282
+ </StatusBadge>
290
283
  {run.triggeredBy ? <Badge variant="outline">{run.triggeredBy}</Badge> : null}
291
284
  </div>
292
285
  )}
@@ -312,9 +305,9 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
312
305
  <CardHeader>
313
306
  <div className="flex items-center justify-between gap-3">
314
307
  <CardTitle>{t('data_sync.runs.detail.progress')}</CardTitle>
315
- <Badge variant="secondary" className={STATUS_STYLES[progressStatus] ?? ''}>
308
+ <StatusBadge variant={getSyncRunStatusVariant(progressStatus)}>
316
309
  {t(`data_sync.dashboard.status.${progressStatus}`)}
317
- </Badge>
310
+ </StatusBadge>
318
311
  </div>
319
312
  </CardHeader>
320
313
  <CardContent className="space-y-3">
@@ -355,37 +348,37 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
355
348
  <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
356
349
  <Card>
357
350
  <CardContent className="pt-6 text-center">
358
- <div className="text-2xl font-bold text-green-600">{run.createdCount}</div>
351
+ <div className="text-2xl font-bold text-status-success-text">{run.createdCount}</div>
359
352
  <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.created')}</p>
360
353
  </CardContent>
361
354
  </Card>
362
355
  <Card>
363
356
  <CardContent className="pt-6 text-center">
364
- <div className="text-2xl font-bold text-blue-600">{run.updatedCount}</div>
357
+ <div className="text-2xl font-bold text-status-info-text">{run.updatedCount}</div>
365
358
  <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.updated')}</p>
366
359
  </CardContent>
367
360
  </Card>
368
361
  <Card>
369
362
  <CardContent className="pt-6 text-center">
370
- <div className="text-2xl font-bold text-gray-600">{run.skippedCount}</div>
363
+ <div className="text-2xl font-bold text-muted-foreground">{run.skippedCount}</div>
371
364
  <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.skipped')}</p>
372
365
  </CardContent>
373
366
  </Card>
374
367
  <Card>
375
368
  <CardContent className="pt-6 text-center">
376
- <div className="text-2xl font-bold text-red-600">{run.failedCount}</div>
369
+ <div className="text-2xl font-bold text-status-error-text">{run.failedCount}</div>
377
370
  <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.failed')}</p>
378
371
  </CardContent>
379
372
  </Card>
380
373
  </div>
381
374
 
382
375
  {run.lastError && (
383
- <Card className="border-red-200 bg-red-50">
376
+ <Card className="border-status-error-border bg-status-error-bg">
384
377
  <CardHeader>
385
- <CardTitle className="text-red-800">{t('data_sync.runs.detail.error')}</CardTitle>
378
+ <CardTitle className="text-status-error-text">{t('data_sync.runs.detail.error')}</CardTitle>
386
379
  </CardHeader>
387
380
  <CardContent>
388
- <pre className="text-sm text-red-700 whitespace-pre-wrap">{run.lastError}</pre>
381
+ <pre className="text-sm text-status-error-text whitespace-pre-wrap">{run.lastError}</pre>
389
382
  </CardContent>
390
383
  </Card>
391
384
  )}
@@ -18,8 +18,10 @@ import {
18
18
  SelectValue,
19
19
  } from '@open-mercato/ui/primitives/select'
20
20
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
21
+ import { Switch } from '@open-mercato/ui/primitives/switch'
21
22
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
22
23
  import { useT } from '@open-mercato/shared/lib/i18n/context'
24
+ import { getSyncSummaryVariant } from '../lib/syncRunStatus'
23
25
  import {
24
26
  CalendarClock,
25
27
  Play,
@@ -360,13 +362,13 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
360
362
  <div className="flex flex-wrap items-start justify-between gap-3">
361
363
  <div className="space-y-2">
362
364
  <div className="flex flex-wrap items-center gap-2">
363
- <Badge variant="outline" className={props.isEnabled ? 'border-emerald-300 text-emerald-700' : 'border-amber-300 text-amber-700'}>
365
+ <Badge variant={getSyncSummaryVariant(props.isEnabled ? 'enabled' : 'disabled')}>
364
366
  {props.isEnabled ? <ShieldCheck className="mr-2 h-3.5 w-3.5" /> : <ShieldAlert className="mr-2 h-3.5 w-3.5" />}
365
367
  {props.isEnabled
366
368
  ? t('data_sync.dashboard.start.status.enabled', 'Integration enabled')
367
369
  : t('data_sync.dashboard.start.status.disabled', 'Integration disabled')}
368
370
  </Badge>
369
- <Badge variant="outline" className={props.hasCredentials ? 'border-sky-300 text-sky-700' : 'border-amber-300 text-amber-700'}>
371
+ <Badge variant={getSyncSummaryVariant(props.hasCredentials ? 'ready' : 'missing')}>
370
372
  <CalendarClock className="mr-2 h-3.5 w-3.5" />
371
373
  {props.hasCredentials
372
374
  ? t('data_sync.dashboard.start.status.credentialsReady', 'Credentials ready')
@@ -477,22 +479,22 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
477
479
  </td>
478
480
  <td className="px-3 py-3">
479
481
  <label className="flex min-h-10 items-center gap-2 text-sm">
480
- <input
481
- type="checkbox"
482
+ <Switch
482
483
  checked={scheduleState.fullSync}
483
- onChange={(event) => updateScheduleEditor(row.key, { fullSync: event.target.checked }, row.entityType)}
484
+ onCheckedChange={(checked) => updateScheduleEditor(row.key, { fullSync: checked }, row.entityType)}
484
485
  disabled={controlsDisabled}
486
+ aria-label={t('data_sync.dashboard.start.fullSync', 'Run as full sync')}
485
487
  />
486
488
  <span>{t('data_sync.integrationTab.fullSyncShort', 'Full')}</span>
487
489
  </label>
488
490
  </td>
489
491
  <td className="px-3 py-3">
490
492
  <label className="flex min-h-10 items-center gap-2 text-sm">
491
- <input
492
- type="checkbox"
493
+ <Switch
493
494
  checked={scheduleState.isEnabled}
494
- onChange={(event) => updateScheduleEditor(row.key, { isEnabled: event.target.checked }, row.entityType)}
495
+ onCheckedChange={(checked) => updateScheduleEditor(row.key, { isEnabled: checked }, row.entityType)}
495
496
  disabled={controlsDisabled}
497
+ aria-label={t('data_sync.dashboard.schedule.enabled', 'Schedule enabled')}
496
498
  />
497
499
  <span>{scheduleState.isEnabled ? t('data_sync.dashboard.schedule.status.shortEnabled', 'Scheduled') : t('data_sync.dashboard.schedule.status.shortDisabled', 'Paused')}</span>
498
500
  </label>
@@ -1,5 +1,5 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
- import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
2
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
3
  import { SyncExternalIdMapping } from '../../integrations/data/entities'
4
4
 
5
5
  type MappingScope = {
@@ -119,6 +119,63 @@ export function createExternalIdMappingService(em: EntityManager) {
119
119
  await em.persist(created).flush()
120
120
  return created
121
121
  },
122
+
123
+ async deleteExternalIdMapping(
124
+ integrationId: string,
125
+ entityType: string,
126
+ localId: string,
127
+ scope: MappingScope,
128
+ ): Promise<boolean> {
129
+ const row = await findOneWithDecryption(
130
+ em,
131
+ SyncExternalIdMapping,
132
+ {
133
+ integrationId,
134
+ internalEntityType: entityType,
135
+ internalEntityId: localId,
136
+ organizationId: scope.organizationId,
137
+ tenantId: scope.tenantId,
138
+ deletedAt: null,
139
+ },
140
+ undefined,
141
+ scope,
142
+ )
143
+ if (!row) return false
144
+ row.deletedAt = new Date()
145
+ await em.flush()
146
+ return true
147
+ },
148
+
149
+ async deleteExternalIdMappings(
150
+ integrationId: string,
151
+ entityType: string,
152
+ localIds: string[],
153
+ scope: MappingScope,
154
+ ): Promise<number> {
155
+ const uniqueLocalIds = Array.from(new Set(localIds))
156
+ if (uniqueLocalIds.length === 0) return 0
157
+ const rows = await findWithDecryption(
158
+ em,
159
+ SyncExternalIdMapping,
160
+ {
161
+ integrationId,
162
+ internalEntityType: entityType,
163
+ internalEntityId: { $in: uniqueLocalIds },
164
+ organizationId: scope.organizationId,
165
+ tenantId: scope.tenantId,
166
+ deletedAt: null,
167
+ },
168
+ undefined,
169
+ scope,
170
+ )
171
+ if (rows.length === 0) return 0
172
+ const now = new Date()
173
+ for (const row of rows) {
174
+ row.deletedAt = now
175
+ }
176
+ await em.flush()
177
+ return rows.length
178
+ },
122
179
  }
123
180
  }
124
181
 
@@ -435,7 +435,7 @@ export function createSyncEngine(deps: EngineDeps) {
435
435
  scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
436
436
  runId: run.id,
437
437
  })) {
438
- if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId)) {
438
+ if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
439
439
  await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
440
440
  return
441
441
  }
@@ -579,7 +579,7 @@ export function createSyncEngine(deps: EngineDeps) {
579
579
  scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
580
580
  runId: run.id,
581
581
  })) {
582
- if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId)) {
582
+ if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
583
583
  await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
584
584
  return
585
585
  }
@@ -0,0 +1,39 @@
1
+ import type { StatusBadgeVariant, StatusMap } from '@open-mercato/ui/primitives/status-badge'
2
+
3
+ export type SyncRunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'
4
+
5
+ export const syncRunStatusVariants: StatusMap<SyncRunStatus> = {
6
+ pending: 'neutral',
7
+ running: 'info',
8
+ completed: 'success',
9
+ failed: 'error',
10
+ cancelled: 'warning',
11
+ paused: 'warning',
12
+ }
13
+
14
+ export function getSyncRunStatusVariant(status: string): StatusBadgeVariant {
15
+ return syncRunStatusVariants[status as SyncRunStatus] ?? 'neutral'
16
+ }
17
+
18
+ export type SyncSummaryKind =
19
+ | 'enabled'
20
+ | 'disabled'
21
+ | 'ready'
22
+ | 'missing'
23
+ | 'scheduled'
24
+ | 'paused'
25
+ | 'none'
26
+
27
+ export const syncSummaryVariants: Record<SyncSummaryKind, StatusBadgeVariant> = {
28
+ enabled: 'success',
29
+ ready: 'success',
30
+ disabled: 'neutral',
31
+ missing: 'warning',
32
+ scheduled: 'info',
33
+ paused: 'warning',
34
+ none: 'neutral',
35
+ }
36
+
37
+ export function getSyncSummaryVariant(kind: SyncSummaryKind): StatusBadgeVariant {
38
+ return syncSummaryVariants[kind] ?? 'neutral'
39
+ }
@@ -382,6 +382,13 @@ export async function POST(req: Request) {
382
382
  if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
383
383
  const isCustomEntity = entityKind === 'custom'
384
384
  await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
385
+ // Strip reserved record/system columns the edit form echoes back from the loaded record
386
+ // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom
387
+ // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are
388
+ // rejected as "Unknown custom field", which fails EVERY custom-entity edit-form save.
389
+ for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {
390
+ delete (values as any)[reservedKey]
391
+ }
385
392
  const norm = normalizeValues(values)
386
393
 
387
394
  // Validate against custom field definitions
@@ -448,6 +455,13 @@ export async function PUT(req: Request) {
448
455
  if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
449
456
  const isCustomEntity = entityKind === 'custom'
450
457
  await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
458
+ // Strip reserved record/system columns the edit form echoes back from the loaded record
459
+ // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom
460
+ // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are
461
+ // rejected as "Unknown custom field", which fails EVERY custom-entity edit-form save.
462
+ for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {
463
+ delete (values as any)[reservedKey]
464
+ }
451
465
  const norm = normalizeValues(values)
452
466
 
453
467
  // Validate against custom field definitions
@@ -49,6 +49,21 @@ const listFields = [
49
49
  'updated_at',
50
50
  ]
51
51
 
52
+ export const transformFeatureToggleListItem = (item: Record<string, unknown>) => {
53
+ if (!item) return item
54
+ return {
55
+ id: item.id,
56
+ identifier: item.identifier,
57
+ name: item.name,
58
+ description: item.description ?? null,
59
+ category: item.category ?? null,
60
+ type: item.type,
61
+ defaultValue: item.default_value,
62
+ createdAt: item.created_at,
63
+ updatedAt: item.updated_at,
64
+ }
65
+ }
66
+
52
67
  const buildFilters = (query: FeatureToggleListQuery): Record<string, unknown> => {
53
68
  const filters: Record<string, unknown> = {}
54
69
  const search = query.search?.trim()
@@ -105,20 +120,7 @@ const crud = makeCrudRoute({
105
120
  updatedAt: 'updated_at',
106
121
  type: 'type',
107
122
  },
108
- transformItem: (item: Record<string, unknown>) => {
109
- if (!item) return item
110
- return {
111
- id: item.id,
112
- identifier: item.identifier,
113
- name: item.name,
114
- description: item.description ?? null,
115
- category: item.category ?? null,
116
- type: item.type,
117
- defaultValue: item.default_value,
118
- created_at: item.created_at,
119
- updated_at: item.updated_at,
120
- }
121
- },
123
+ transformItem: transformFeatureToggleListItem,
122
124
  buildFilters: async (query) => buildFilters(query),
123
125
  },
124
126
  actions: {
@@ -28,8 +28,8 @@ export const featureToggleSchema = z
28
28
  category: z.string().nullable().optional(),
29
29
  type: toggleTypeSchema,
30
30
  defaultValue: z.any().nullable().optional(),
31
- created_at: z.string().optional(),
32
- updated_at: z.string().optional(),
31
+ createdAt: z.string().nullable().optional(),
32
+ updatedAt: z.string().nullable().optional(),
33
33
  })
34
34
  .passthrough()
35
35
 
@@ -6,6 +6,7 @@ import { ToggleCreateInput, toggleCreateSchema, ToggleUpdateInput, toggleUpdateS
6
6
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
7
7
  import { registerCommand } from '@open-mercato/shared/lib/commands'
8
8
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
9
+ import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
9
10
  import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects, requireId } from '@open-mercato/shared/lib/commands/helpers'
10
11
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
11
12
  import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
@@ -49,6 +50,8 @@ type ToggleUndoPayload = {
49
50
 
50
51
  const featureToggleCrudIndexer = { entityType: E.feature_toggles.feature_toggle }
51
52
 
53
+ const FEATURE_TOGGLE_LOCK_RESOURCE_KIND = 'feature_toggles.feature_toggle'
54
+
52
55
  function featureToggleIdentifiers(
53
56
  toggle: FeatureToggle | ToggleSnapshot,
54
57
  ctx: { auth?: { tenantId?: string | null } | null },
@@ -217,6 +220,12 @@ const updateToggleCommand: CommandHandler<ToggleUpdateInput, { toggleId: string
217
220
  const em = (ctx.container.resolve('em') as EntityManager).fork()
218
221
  const toggle = await em.findOne(FeatureToggle, { id: parsed.id, deletedAt: null })
219
222
  if (!toggle) throw new CrudHttpError(404, { error: 'Toggle not found' })
223
+ enforceCommandOptimisticLock({
224
+ resourceKind: FEATURE_TOGGLE_LOCK_RESOURCE_KIND,
225
+ resourceId: toggle.id,
226
+ current: toggle.updatedAt ?? null,
227
+ request: ctx.request ?? null,
228
+ })
220
229
  const previousIdentifier = toggle.identifier
221
230
  if (parsed.identifier && parsed.identifier !== toggle.identifier) {
222
231
  const existing = await em.findOne(FeatureToggle, { identifier: parsed.identifier })
@@ -344,6 +353,12 @@ const deleteToggleCommand: CommandHandler<{ body?: Record<string, unknown>; quer
344
353
  const em = (ctx.container.resolve('em') as EntityManager).fork()
345
354
  const toggle = await em.findOne(FeatureToggle, { id, deletedAt: null })
346
355
  if (!toggle) throw new CrudHttpError(404, { error: 'Feature toggle not found' })
356
+ enforceCommandOptimisticLock({
357
+ resourceKind: FEATURE_TOGGLE_LOCK_RESOURCE_KIND,
358
+ resourceId: toggle.id,
359
+ current: toggle.updatedAt ?? null,
360
+ request: ctx.request ?? null,
361
+ })
347
362
  const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService
348
363
  await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)
349
364
 
@@ -148,6 +148,13 @@ export default function InboxOpsProposalsPage() {
148
148
  if (initialLoadComplete) loadProposals()
149
149
  }, [page, statusFilter, categoryFilter, search, scopeVersion]) // eslint-disable-line react-hooks/exhaustive-deps
150
150
 
151
+ React.useEffect(() => {
152
+ if (initialLoadComplete) {
153
+ loadCounts()
154
+ loadSettings()
155
+ }
156
+ }, [scopeVersion]) // eslint-disable-line react-hooks/exhaustive-deps
157
+
151
158
  const handleCopyAddress = React.useCallback(() => {
152
159
  if (settings?.inboxAddress) {
153
160
  navigator.clipboard.writeText(settings.inboxAddress)
@@ -10,9 +10,14 @@ import {
10
10
 
11
11
  export const metadata = {
12
12
  PUT: { requireAuth: true, requireFeatures: ['messages.view'] },
13
+ DELETE: { requireAuth: true, requireFeatures: ['messages.view'] },
13
14
  }
14
15
 
15
- export async function PUT(req: Request, { params }: { params: { id: string } }) {
16
+ async function runConversationArchiveMutation(
17
+ req: Request,
18
+ id: string,
19
+ commandId: 'messages.conversation.archive_for_actor' | 'messages.conversation.unarchive_for_actor',
20
+ ) {
16
21
  const { ctx, scope } = await resolveMessageContext(req)
17
22
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
18
23
 
@@ -23,7 +28,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
23
28
  organizationId: scope.organizationId,
24
29
  userId: scope.userId,
25
30
  resourceKind: 'messages.conversation',
26
- resourceId: params.id,
31
+ resourceId: id,
27
32
  operation: 'update',
28
33
  requestMethod: req.method,
29
34
  requestHeaders: req.headers,
@@ -39,9 +44,9 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
39
44
  }
40
45
 
41
46
  try {
42
- const { result, logEntry } = await commandBus.execute('messages.conversation.archive_for_actor', {
47
+ const { result, logEntry } = await commandBus.execute(commandId, {
43
48
  input: {
44
- anchorMessageId: params.id,
49
+ anchorMessageId: id,
45
50
  tenantId: scope.tenantId,
46
51
  organizationId: scope.organizationId,
47
52
  userId: scope.userId,
@@ -59,14 +64,14 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
59
64
  const response = Response.json(result)
60
65
  attachOperationMetadataHeader(response, logEntry, {
61
66
  resourceKind: 'messages.conversation',
62
- resourceId: params.id,
67
+ resourceId: id,
63
68
  })
64
69
  await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
65
70
  tenantId: scope.tenantId,
66
71
  organizationId: scope.organizationId,
67
72
  userId: scope.userId,
68
73
  resourceKind: 'messages.conversation',
69
- resourceId: params.id,
74
+ resourceId: id,
70
75
  operation: 'update',
71
76
  requestMethod: req.method,
72
77
  requestHeaders: req.headers,
@@ -85,6 +90,14 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
85
90
  }
86
91
  }
87
92
 
93
+ export async function PUT(req: Request, { params }: { params: { id: string } }) {
94
+ return runConversationArchiveMutation(req, params.id, 'messages.conversation.archive_for_actor')
95
+ }
96
+
97
+ export async function DELETE(req: Request, { params }: { params: { id: string } }) {
98
+ return runConversationArchiveMutation(req, params.id, 'messages.conversation.unarchive_for_actor')
99
+ }
100
+
88
101
  export const openApi: OpenApiRouteDoc = {
89
102
  tag: 'Messages',
90
103
  methods: {
@@ -98,5 +111,15 @@ export const openApi: OpenApiRouteDoc = {
98
111
  { status: 404, description: 'Message not found', schema: errorResponseSchema },
99
112
  ],
100
113
  },
114
+ DELETE: {
115
+ summary: 'Unarchive conversation for current actor',
116
+ responses: [
117
+ { status: 200, description: 'Conversation unarchived', schema: conversationMutationResponseSchema },
118
+ ],
119
+ errors: [
120
+ { status: 403, description: 'Access denied', schema: errorResponseSchema },
121
+ { status: 404, description: 'Message not found', schema: errorResponseSchema },
122
+ ],
123
+ },
101
124
  },
102
125
  }
@@ -9,10 +9,15 @@ import {
9
9
  } from '../../../openapi'
10
10
 
11
11
  export const metadata = {
12
+ PUT: { requireAuth: true, requireFeatures: ['messages.view'] },
12
13
  DELETE: { requireAuth: true, requireFeatures: ['messages.view'] },
13
14
  }
14
15
 
15
- export async function DELETE(req: Request, { params }: { params: { id: string } }) {
16
+ async function runConversationReadMutation(
17
+ req: Request,
18
+ id: string,
19
+ commandId: 'messages.conversation.mark_read_for_actor' | 'messages.conversation.mark_unread_for_actor',
20
+ ) {
16
21
  const { ctx, scope } = await resolveMessageContext(req)
17
22
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
18
23
 
@@ -23,7 +28,7 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
23
28
  organizationId: scope.organizationId,
24
29
  userId: scope.userId,
25
30
  resourceKind: 'messages.conversation',
26
- resourceId: params.id,
31
+ resourceId: id,
27
32
  operation: 'update',
28
33
  requestMethod: req.method,
29
34
  requestHeaders: req.headers,
@@ -39,9 +44,9 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
39
44
  }
40
45
 
41
46
  try {
42
- const { result, logEntry } = await commandBus.execute('messages.conversation.mark_unread_for_actor', {
47
+ const { result, logEntry } = await commandBus.execute(commandId, {
43
48
  input: {
44
- anchorMessageId: params.id,
49
+ anchorMessageId: id,
45
50
  tenantId: scope.tenantId,
46
51
  organizationId: scope.organizationId,
47
52
  userId: scope.userId,
@@ -59,14 +64,14 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
59
64
  const response = Response.json(result)
60
65
  attachOperationMetadataHeader(response, logEntry, {
61
66
  resourceKind: 'messages.conversation',
62
- resourceId: params.id,
67
+ resourceId: id,
63
68
  })
64
69
  await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
65
70
  tenantId: scope.tenantId,
66
71
  organizationId: scope.organizationId,
67
72
  userId: scope.userId,
68
73
  resourceKind: 'messages.conversation',
69
- resourceId: params.id,
74
+ resourceId: id,
70
75
  operation: 'update',
71
76
  requestMethod: req.method,
72
77
  requestHeaders: req.headers,
@@ -85,9 +90,27 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
85
90
  }
86
91
  }
87
92
 
93
+ export async function PUT(req: Request, { params }: { params: { id: string } }) {
94
+ return runConversationReadMutation(req, params.id, 'messages.conversation.mark_read_for_actor')
95
+ }
96
+
97
+ export async function DELETE(req: Request, { params }: { params: { id: string } }) {
98
+ return runConversationReadMutation(req, params.id, 'messages.conversation.mark_unread_for_actor')
99
+ }
100
+
88
101
  export const openApi: OpenApiRouteDoc = {
89
102
  tag: 'Messages',
90
103
  methods: {
104
+ PUT: {
105
+ summary: 'Mark entire conversation as read for current actor',
106
+ responses: [
107
+ { status: 200, description: 'Conversation marked read', schema: conversationMutationResponseSchema },
108
+ ],
109
+ errors: [
110
+ { status: 403, description: 'Access denied', schema: errorResponseSchema },
111
+ { status: 404, description: 'Message not found', schema: errorResponseSchema },
112
+ ],
113
+ },
91
114
  DELETE: {
92
115
  summary: 'Mark entire conversation as unread for current actor',
93
116
  responses: [
@@ -122,6 +122,19 @@ export async function GET(req: Request, { params }: { params: { id: string } })
122
122
  threadMessage.senderUserId === scope.userId || visibleRecipientMessageIds.has(threadMessage.id)
123
123
  ))
124
124
 
125
+ const actorRecipientStatusByMessageId = new Map<string, string>()
126
+ for (const row of visibleRecipientRows) {
127
+ actorRecipientStatusByMessageId.set(row.messageId, row.status)
128
+ }
129
+ if (recipient) {
130
+ actorRecipientStatusByMessageId.set(params.id, autoMarkRead ? 'read' : recipient.status)
131
+ }
132
+ const actorRecipientStatuses = Array.from(actorRecipientStatusByMessageId.values())
133
+ const conversationArchived = actorRecipientStatuses.length > 0
134
+ && actorRecipientStatuses.every((status) => status === 'archived')
135
+ const conversationAllUnread = actorRecipientStatuses.length > 0
136
+ && actorRecipientStatuses.every((status) => status === 'unread')
137
+
125
138
  const threadSenderIds = actorVisibleThreadMessages
126
139
  .map((threadMessage) => threadMessage.senderUserId)
127
140
  .filter((value): value is string => typeof value === 'string' && value.length > 0)
@@ -244,6 +257,8 @@ export async function GET(req: Request, { params }: { params: { id: string } })
244
257
  }
245
258
  }),
246
259
  isRead: recipient ? (autoMarkRead || recipient.status !== 'unread') : true,
260
+ conversationArchived,
261
+ conversationAllUnread,
247
262
  })
248
263
  }
249
264
 
@@ -121,6 +121,8 @@ export const messageDetailResponseSchema = z.object({
121
121
  })),
122
122
  thread: z.array(messageThreadItemSchema),
123
123
  isRead: z.boolean(),
124
+ conversationArchived: z.boolean(),
125
+ conversationAllUnread: z.boolean(),
124
126
  })
125
127
 
126
128
  export const messageTokenDetailResponseSchema = z.object({