@open-mercato/core 0.6.6-develop.6257.1.6d0af84d26 → 0.6.6-develop.6290.1.4bb5a8ba3f

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 (220) 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/data_sync/workers/sync-scheduled.js +13 -14
  43. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  44. package/dist/modules/directory/components/TenantSelect.js +29 -16
  45. package/dist/modules/directory/components/TenantSelect.js.map +2 -2
  46. package/dist/modules/entities/api/records.js +6 -0
  47. package/dist/modules/entities/api/records.js.map +2 -2
  48. package/dist/modules/feature_toggles/api/global/route.js +17 -15
  49. package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
  50. package/dist/modules/feature_toggles/api/openapi.js +2 -2
  51. package/dist/modules/feature_toggles/api/openapi.js.map +2 -2
  52. package/dist/modules/feature_toggles/commands/global.js +14 -0
  53. package/dist/modules/feature_toggles/commands/global.js.map +2 -2
  54. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +6 -0
  55. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  56. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +3 -1
  57. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
  58. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +12 -5
  59. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  60. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js +10 -1
  61. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js.map +2 -2
  62. package/dist/modules/payment_gateways/api/cancel/route.js +5 -0
  63. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  64. package/dist/modules/payment_gateways/api/capture/route.js +5 -0
  65. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  66. package/dist/modules/payment_gateways/api/refund/route.js +5 -0
  67. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  68. package/dist/modules/payment_gateways/lib/gateway-service.js +52 -25
  69. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  70. package/dist/modules/payment_gateways/lib/status-machine.js +13 -0
  71. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  72. package/dist/modules/perspectives/api/[tableId]/route.js +8 -2
  73. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  74. package/dist/modules/perspectives/data/entities.js +33 -3
  75. package/dist/modules/perspectives/data/entities.js.map +2 -2
  76. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js +63 -0
  77. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js.map +7 -0
  78. package/dist/modules/perspectives/services/perspectiveService.js +17 -0
  79. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  80. package/dist/modules/planner/commands/availability-weekly.js +33 -1
  81. package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
  82. package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -2
  83. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  84. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js +35 -0
  85. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js.map +7 -0
  86. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +18 -19
  87. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
  88. package/dist/modules/progress/api/active/route.js +1 -1
  89. package/dist/modules/progress/api/active/route.js.map +2 -2
  90. package/dist/modules/progress/lib/progressService.js.map +1 -1
  91. package/dist/modules/progress/lib/progressServiceImpl.js +33 -8
  92. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  93. package/dist/modules/query_index/api/status.js +19 -24
  94. package/dist/modules/query_index/api/status.js.map +3 -3
  95. package/dist/modules/query_index/lib/coverage.js +35 -0
  96. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  97. package/dist/modules/query_index/lib/engine.js +11 -6
  98. package/dist/modules/query_index/lib/engine.js.map +2 -2
  99. package/dist/modules/resources/api/resource-types.js +5 -37
  100. package/dist/modules/resources/api/resource-types.js.map +2 -2
  101. package/dist/modules/resources/api/resourceTypeCounts.js +35 -0
  102. package/dist/modules/resources/api/resourceTypeCounts.js.map +7 -0
  103. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  104. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +1 -1
  105. package/dist/modules/resources/backend/resources/resource-types/page.js +2 -1
  106. package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
  107. package/dist/modules/sales/components/documents/ReturnDialog.js +4 -1
  108. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  109. package/dist/modules/sales/components/documents/ShipmentDialog.js +17 -2
  110. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  111. package/dist/modules/sales/di.js +4 -0
  112. package/dist/modules/sales/di.js.map +2 -2
  113. package/dist/modules/sales/services/salesOrderService.js +26 -0
  114. package/dist/modules/sales/services/salesOrderService.js.map +7 -0
  115. package/dist/modules/shipping_carriers/api/shipments/route.js +8 -0
  116. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  117. package/dist/modules/shipping_carriers/data/entities.js +38 -0
  118. package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
  119. package/dist/modules/shipping_carriers/data/validators.js +2 -1
  120. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  121. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js +76 -0
  122. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js.map +7 -0
  123. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +2 -1
  124. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +2 -2
  125. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +20 -2
  126. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  127. package/dist/modules/shipping_carriers/lib/shipping-service.js +82 -43
  128. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  129. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js +14 -0
  130. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js.map +7 -0
  131. package/dist/modules/staff/lib/timesheets-ui/ListView.js +15 -6
  132. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
  133. package/dist/modules/translations/components/TranslationManager.js +9 -18
  134. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  135. package/generated/entities/carrier_shipment_idempotency_key/index.ts +8 -0
  136. package/generated/entities.ids.generated.ts +2 -1
  137. package/generated/entity-fields-registry.ts +10 -0
  138. package/package.json +9 -8
  139. package/src/modules/catalog/lib/productResolution.ts +75 -0
  140. package/src/modules/communication_channels/workers/channel-import-history.ts +1 -1
  141. package/src/modules/currencies/api/fetch-configs/route.ts +36 -0
  142. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +20 -17
  143. package/src/modules/currencies/i18n/de.json +2 -1
  144. package/src/modules/currencies/i18n/en.json +2 -1
  145. package/src/modules/currencies/i18n/es.json +2 -1
  146. package/src/modules/currencies/i18n/pl.json +2 -1
  147. package/src/modules/customers/api/companies/[id]/route.ts +214 -189
  148. package/src/modules/customers/api/people/[id]/route.ts +106 -80
  149. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +19 -12
  150. package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +8 -6
  151. package/src/modules/customers/components/detail/AiActionChips.tsx +6 -3
  152. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +16 -18
  153. package/src/modules/data_sync/api/runs/[id]/cancel.ts +1 -1
  154. package/src/modules/data_sync/backend/data-sync/page.tsx +11 -58
  155. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +13 -20
  156. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +10 -8
  157. package/src/modules/data_sync/lib/id-mapping.ts +58 -1
  158. package/src/modules/data_sync/lib/sync-engine.ts +2 -2
  159. package/src/modules/data_sync/lib/syncRunStatus.ts +39 -0
  160. package/src/modules/data_sync/workers/sync-scheduled.ts +13 -14
  161. package/src/modules/directory/components/TenantSelect.tsx +33 -16
  162. package/src/modules/entities/api/records.ts +14 -0
  163. package/src/modules/feature_toggles/api/global/route.ts +16 -14
  164. package/src/modules/feature_toggles/api/openapi.ts +2 -2
  165. package/src/modules/feature_toggles/commands/global.ts +15 -0
  166. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +7 -0
  167. package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +2 -0
  168. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +14 -4
  169. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.ts +15 -1
  170. package/src/modules/payment_gateways/api/cancel/route.ts +5 -0
  171. package/src/modules/payment_gateways/api/capture/route.ts +5 -0
  172. package/src/modules/payment_gateways/api/refund/route.ts +5 -0
  173. package/src/modules/payment_gateways/lib/gateway-service.ts +58 -25
  174. package/src/modules/payment_gateways/lib/status-machine.ts +16 -0
  175. package/src/modules/perspectives/api/[tableId]/route.ts +8 -2
  176. package/src/modules/perspectives/data/entities.ts +43 -3
  177. package/src/modules/perspectives/migrations/.snapshot-open-mercato.json +69 -25
  178. package/src/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.ts +65 -0
  179. package/src/modules/perspectives/services/perspectiveService.ts +17 -0
  180. package/src/modules/planner/commands/availability-weekly.ts +48 -1
  181. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +31 -10
  182. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.ts +36 -0
  183. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +22 -23
  184. package/src/modules/progress/api/active/route.ts +1 -1
  185. package/src/modules/progress/lib/progressService.ts +2 -2
  186. package/src/modules/progress/lib/progressServiceImpl.ts +33 -8
  187. package/src/modules/query_index/api/status.ts +28 -32
  188. package/src/modules/query_index/lib/coverage.ts +63 -0
  189. package/src/modules/query_index/lib/engine.ts +37 -24
  190. package/src/modules/resources/api/resource-types.ts +4 -44
  191. package/src/modules/resources/api/resourceTypeCounts.ts +74 -0
  192. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  193. package/src/modules/resources/backend/resources/resource-types/page.tsx +1 -0
  194. package/src/modules/sales/components/documents/ReturnDialog.tsx +4 -1
  195. package/src/modules/sales/components/documents/ShipmentDialog.tsx +20 -2
  196. package/src/modules/sales/di.ts +6 -0
  197. package/src/modules/sales/services/salesOrderService.ts +38 -0
  198. package/src/modules/shipping_carriers/api/shipments/route.ts +8 -0
  199. package/src/modules/shipping_carriers/data/entities.ts +33 -0
  200. package/src/modules/shipping_carriers/data/validators.ts +1 -0
  201. package/src/modules/shipping_carriers/i18n/de.json +1 -0
  202. package/src/modules/shipping_carriers/i18n/en.json +1 -0
  203. package/src/modules/shipping_carriers/i18n/es.json +1 -0
  204. package/src/modules/shipping_carriers/i18n/pl.json +1 -0
  205. package/src/modules/shipping_carriers/lib/shipment-idempotency.ts +119 -0
  206. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +3 -1
  207. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +24 -2
  208. package/src/modules/shipping_carriers/lib/shipping-service.ts +87 -43
  209. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +197 -0
  210. package/src/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.ts +14 -0
  211. package/src/modules/staff/i18n/de.json +1 -0
  212. package/src/modules/staff/i18n/en.json +1 -0
  213. package/src/modules/staff/i18n/es.json +1 -0
  214. package/src/modules/staff/i18n/pl.json +1 -0
  215. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +15 -7
  216. package/src/modules/translations/components/TranslationManager.tsx +17 -24
  217. package/src/modules/translations/i18n/de.json +1 -0
  218. package/src/modules/translations/i18n/en.json +1 -0
  219. package/src/modules/translations/i18n/es.json +1 -0
  220. package/src/modules/translations/i18n/pl.json +1 -0
@@ -485,37 +485,13 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
485
485
  return forbidden('Access denied')
486
486
  }
487
487
 
488
- profile = await findOneWithDecryption(em, CustomerPersonProfile, { entity: person }, { populate: ['company'] }, { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null })
488
+ const [profileResult, personCompanyLinks] = await Promise.all([
489
+ findOneWithDecryption(em, CustomerPersonProfile, { entity: person }, { populate: ['company'] }, { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null }),
490
+ loadPersonCompanyLinks(em, person),
491
+ ])
492
+ profile = profileResult
489
493
  profiler.mark('profile_loaded', { found: !!profile })
490
- companies = summarizePersonCompanies(profile, await loadPersonCompanyLinks(em, person))
491
-
492
- if (includeAddresses) {
493
- addresses = await findWithDecryption(em, CustomerAddress, { entity: person.id }, { orderBy: { isPrimary: 'desc', createdAt: 'desc' } }, { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null })
494
- profiler.mark('addresses_loaded', { count: addresses.length })
495
- }
496
-
497
- tagAssignments = await findWithDecryption(
498
- em,
499
- CustomerTagAssignment,
500
- { entity: person.id },
501
- { populate: ['tag'] },
502
- { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null },
503
- )
504
- profiler.mark('tags_loaded', { count: tagAssignments.length })
505
-
506
- const labelAssignments = await findWithDecryption(
507
- em,
508
- CustomerLabelAssignment,
509
- { entity: person.id },
510
- { populate: ['label'] },
511
- { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null },
512
- )
513
- profiler.mark('labels_loaded', { count: labelAssignments.length })
514
-
515
- if (includeComments) {
516
- comments = await findWithDecryption(em, CustomerComment, { entity: person.id }, { orderBy: { createdAt: 'desc' }, limit: 50 }, { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null })
517
- profiler.mark('comments_loaded', { count: comments.length })
518
- }
494
+ companies = summarizePersonCompanies(profile, personCompanyLinks)
519
495
 
520
496
  // Per-user email privacy (CRM email integration spec, "Layer 1 — DB filter"):
521
497
  // exclude private email interactions owned by other users from every read
@@ -528,18 +504,46 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
528
504
  userFeatures: undefined,
529
505
  })
530
506
 
507
+ const personScope = { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null }
531
508
  const shouldLoadCanonicalInteractions = includeInteractions || includeActivities || includeTodos
532
- const canonicalInteractionRows = shouldLoadCanonicalInteractions
533
- ? await findWithDecryption(
534
- em,
535
- CustomerInteraction,
536
- interactionFlags.unified
537
- ? { entity: person.id, deletedAt: null, ...emailVisibilityFilter }
538
- : { entity: person.id, ...emailVisibilityFilter },
539
- { orderBy: { scheduledAt: 'asc', createdAt: 'desc' }, limit: 100 },
540
- { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null },
541
- )
542
- : []
509
+
510
+ // These reads only depend on the resolved person + scope + email visibility
511
+ // filter, so dispatch them together to avoid a server-side request waterfall
512
+ // before the detail surface can render (issue #3203).
513
+ const [
514
+ addressesResult,
515
+ tagAssignmentsResult,
516
+ labelAssignments,
517
+ commentsResult,
518
+ canonicalInteractionRows,
519
+ ] = await Promise.all([
520
+ includeAddresses
521
+ ? findWithDecryption(em, CustomerAddress, { entity: person.id }, { orderBy: { isPrimary: 'desc', createdAt: 'desc' } }, personScope)
522
+ : Promise.resolve<CustomerAddress[]>([]),
523
+ findWithDecryption(em, CustomerTagAssignment, { entity: person.id }, { populate: ['tag'] }, personScope),
524
+ findWithDecryption(em, CustomerLabelAssignment, { entity: person.id }, { populate: ['label'] }, personScope),
525
+ includeComments
526
+ ? findWithDecryption(em, CustomerComment, { entity: person.id }, { orderBy: { createdAt: 'desc' }, limit: 50 }, personScope)
527
+ : Promise.resolve<CustomerComment[]>([]),
528
+ shouldLoadCanonicalInteractions
529
+ ? findWithDecryption(
530
+ em,
531
+ CustomerInteraction,
532
+ interactionFlags.unified
533
+ ? { entity: person.id, deletedAt: null, ...emailVisibilityFilter }
534
+ : { entity: person.id, ...emailVisibilityFilter },
535
+ { orderBy: { scheduledAt: 'asc', createdAt: 'desc' }, limit: 100 },
536
+ personScope,
537
+ )
538
+ : Promise.resolve<CustomerInteraction[]>([]),
539
+ ])
540
+ addresses = addressesResult
541
+ tagAssignments = tagAssignmentsResult
542
+ comments = commentsResult
543
+ profiler.mark('addresses_loaded', { count: addresses.length })
544
+ profiler.mark('tags_loaded', { count: tagAssignments.length })
545
+ profiler.mark('labels_loaded', { count: labelAssignments.length })
546
+ profiler.mark('comments_loaded', { count: comments.length })
543
547
  profiler.mark('canonical_interactions_loaded', { count: canonicalInteractionRows.length })
544
548
  const canonicalActiveInteractions = canonicalInteractionRows.filter((interaction) => !interaction.deletedAt)
545
549
  const canonicalInteractions = shouldLoadCanonicalInteractions
@@ -676,35 +680,39 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
676
680
  profiler.mark('deals_loaded', { count: deals.length })
677
681
  }
678
682
 
679
- const entityCustomFieldValues = await loadCustomFieldValues({
680
- em,
681
- entityId: E.customers.customer_entity,
682
- recordIds: [person.id],
683
- tenantIdByRecord: { [person.id]: person.tenantId ?? null },
684
- organizationIdByRecord: { [person.id]: person.organizationId ?? null },
685
- tenantFallbacks: [
686
- person.tenantId ?? auth.tenantId ?? null,
687
- ].filter((v): v is string => !!v),
688
- })
689
- profiler.mark('entity_custom_fields_loaded', { keys: Object.keys(entityCustomFieldValues?.[person.id] ?? {}).length })
690
-
691
- let profileCustomFieldValues: Record<string, Record<string, unknown>> = {}
683
+ // Entity custom fields, profile custom fields, and the routing lookup do not
684
+ // depend on each other (profileId is already resolved above), so load them in
685
+ // parallel instead of three sequential awaits (issue #3203).
692
686
  const profileId = profile?.id ?? null
693
- if (profileId) {
694
- profileCustomFieldValues = await loadCustomFieldValues({
687
+ const [entityCustomFieldValues, profileCustomFieldValues, routing] = await Promise.all([
688
+ loadCustomFieldValues({
695
689
  em,
696
- entityId: E.customers.customer_person_profile,
697
- recordIds: [profileId],
698
- tenantIdByRecord: { [profileId]: profile?.tenantId ?? null },
699
- organizationIdByRecord: { [profileId]: profile?.organizationId ?? null },
690
+ entityId: E.customers.customer_entity,
691
+ recordIds: [person.id],
692
+ tenantIdByRecord: { [person.id]: person.tenantId ?? null },
693
+ organizationIdByRecord: { [person.id]: person.organizationId ?? null },
700
694
  tenantFallbacks: [
701
- profile?.tenantId ?? person.tenantId ?? auth.tenantId ?? null,
695
+ person.tenantId ?? auth.tenantId ?? null,
702
696
  ].filter((v): v is string => !!v),
703
- })
697
+ }),
698
+ profileId
699
+ ? loadCustomFieldValues({
700
+ em,
701
+ entityId: E.customers.customer_person_profile,
702
+ recordIds: [profileId],
703
+ tenantIdByRecord: { [profileId]: profile?.tenantId ?? null },
704
+ organizationIdByRecord: { [profileId]: profile?.organizationId ?? null },
705
+ tenantFallbacks: [
706
+ profile?.tenantId ?? person.tenantId ?? auth.tenantId ?? null,
707
+ ].filter((v): v is string => !!v),
708
+ })
709
+ : Promise.resolve<Record<string, Record<string, unknown>>>({}),
710
+ resolvePersonCustomFieldRouting(em, person.tenantId ?? null, person.organizationId ?? null),
711
+ ])
712
+ profiler.mark('entity_custom_fields_loaded', { keys: Object.keys(entityCustomFieldValues?.[person.id] ?? {}).length })
713
+ if (profileId) {
704
714
  profiler.mark('profile_custom_fields_loaded', { keys: Object.keys(profileCustomFieldValues?.[profileId] ?? {}).length })
705
715
  }
706
-
707
- const routing = await resolvePersonCustomFieldRouting(em, person.tenantId ?? null, person.organizationId ?? null)
708
716
  profiler.mark('custom_field_routing_resolved', { keys: routing.size })
709
717
  customFields = normalizeCustomerDetailCustomFields(
710
718
  mergePersonCustomFieldValues(
@@ -716,16 +724,25 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
716
724
  profiler.mark('custom_fields_merged', { keys: Object.keys(customFields).length })
717
725
 
718
726
  const viewerUserIdFinal = viewerUserId
719
- const counts = {
720
- tags: tagAssignments.length + labelAssignments.length,
721
- comments: includeComments
722
- ? comments.length
723
- : await em.count(CustomerComment, {
727
+ // The count fields are independent of each other; dispatch them together so
728
+ // the response counts object is assembled in one round of parallel queries
729
+ // instead of an inline await waterfall (issue #3203).
730
+ const [
731
+ commentsCount,
732
+ activitiesCount,
733
+ interactionsCount,
734
+ todosCount,
735
+ addressesCount,
736
+ dealsCount,
737
+ ] = await Promise.all([
738
+ includeComments
739
+ ? Promise.resolve(comments.length)
740
+ : em.count(CustomerComment, {
724
741
  entity: person.id,
725
742
  organizationId: person.organizationId,
726
743
  tenantId: person.tenantId,
727
744
  }),
728
- activities: await em.count(CustomerInteraction, {
745
+ em.count(CustomerInteraction, {
729
746
  entity: person.id,
730
747
  organizationId: person.organizationId,
731
748
  tenantId: person.tenantId,
@@ -733,38 +750,47 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
733
750
  interactionType: { $ne: 'task' },
734
751
  ...emailVisibilityFilter,
735
752
  }),
736
- interactions: await em.count(CustomerInteraction, {
753
+ em.count(CustomerInteraction, {
737
754
  entity: person.id,
738
755
  organizationId: person.organizationId,
739
756
  tenantId: person.tenantId,
740
757
  deletedAt: null,
741
758
  ...emailVisibilityFilter,
742
759
  }),
743
- todos: interactionFlags.unified
744
- ? await em.count(CustomerInteraction, {
760
+ interactionFlags.unified
761
+ ? em.count(CustomerInteraction, {
745
762
  entity: person.id,
746
763
  organizationId: person.organizationId,
747
764
  tenantId: person.tenantId,
748
765
  deletedAt: null,
749
766
  interactionType: 'task',
750
767
  })
751
- : await em.count(CustomerTodoLink, {
768
+ : em.count(CustomerTodoLink, {
752
769
  entity: person.id,
753
770
  organizationId: person.organizationId,
754
771
  tenantId: person.tenantId,
755
772
  }),
756
- addresses: includeAddresses
757
- ? addresses.length
758
- : await em.count(CustomerAddress, {
773
+ includeAddresses
774
+ ? Promise.resolve(addresses.length)
775
+ : em.count(CustomerAddress, {
759
776
  entity: person.id,
760
777
  organizationId: person.organizationId,
761
778
  tenantId: person.tenantId,
762
779
  }),
763
- deals: includeDeals
764
- ? deals.length
765
- : await em.count(CustomerDealPersonLink, {
780
+ includeDeals
781
+ ? Promise.resolve(deals.length)
782
+ : em.count(CustomerDealPersonLink, {
766
783
  person: person.id,
767
784
  }),
785
+ ])
786
+ const counts = {
787
+ tags: tagAssignments.length + labelAssignments.length,
788
+ comments: commentsCount,
789
+ activities: activitiesCount,
790
+ interactions: interactionsCount,
791
+ todos: todosCount,
792
+ addresses: addressesCount,
793
+ deals: dealsCount,
768
794
  companies: companies.length,
769
795
  }
770
796
 
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
+ import { ChevronDown, ChevronUp } from 'lucide-react'
4
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
7
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
@@ -8,6 +9,8 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
8
9
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
9
10
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
11
  import { Button } from '@open-mercato/ui/primitives/button'
12
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
13
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
11
14
  import { Input } from '@open-mercato/ui/primitives/input'
12
15
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@open-mercato/ui/primitives/dialog'
13
16
  import {
@@ -516,24 +519,28 @@ export default function PipelineStagesPage() {
516
519
  {stages.map((stage, index) => (
517
520
  <div key={stage.id} className="flex items-center gap-3 px-4 py-3">
518
521
  <div className="flex flex-col gap-1">
519
- <button
522
+ <IconButton
520
523
  type="button"
521
- className="text-muted-foreground hover:text-foreground disabled:opacity-50"
524
+ variant="ghost"
525
+ size="xs"
526
+ className="text-muted-foreground hover:text-foreground"
522
527
  onClick={() => moveStage(index, 'up')}
523
528
  disabled={index === 0}
524
529
  aria-label={t('customers.config.pipelineStages.moveUp', 'Move up')}
525
530
  >
526
-
527
- </button>
528
- <button
531
+ <ChevronUp className="size-4" />
532
+ </IconButton>
533
+ <IconButton
529
534
  type="button"
530
- className="text-muted-foreground hover:text-foreground disabled:opacity-50"
535
+ variant="ghost"
536
+ size="xs"
537
+ className="text-muted-foreground hover:text-foreground"
531
538
  onClick={() => moveStage(index, 'down')}
532
539
  disabled={index === stages.length - 1}
533
540
  aria-label={t('customers.config.pipelineStages.moveDown', 'Move down')}
534
541
  >
535
-
536
- </button>
542
+ <ChevronDown className="size-4" />
543
+ </IconButton>
537
544
  </div>
538
545
  <span className="flex-1 text-sm flex items-center gap-2">
539
546
  {stage.color ? renderDictionaryColor(stage.color) : null}
@@ -583,11 +590,11 @@ export default function PipelineStagesPage() {
583
590
  autoFocus
584
591
  />
585
592
  </div>
586
- <label className="flex items-center gap-2 text-sm cursor-pointer">
587
- <input
588
- type="checkbox"
593
+ <label htmlFor="pipeline-is-default" className="flex items-center gap-2 text-sm cursor-pointer">
594
+ <Checkbox
595
+ id="pipeline-is-default"
589
596
  checked={pipelineIsDefault}
590
- onChange={(e) => setPipelineIsDefault(e.target.checked)}
597
+ onCheckedChange={(checked) => setPipelineIsDefault(checked === true)}
591
598
  />
592
599
  {t('customers.config.pipelineStages.setAsDefault', 'Set as default pipeline')}
593
600
  </label>
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { Check, Phone, Mail, Users, CheckSquare } from 'lucide-react'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { Button } from '@open-mercato/ui/primitives/button'
6
7
  import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
7
8
 
8
9
  export type ActivityKind = 'meeting' | 'call' | 'task' | 'email'
@@ -34,28 +35,29 @@ export function ActivitiesAddNewMenu({ onSelect, disabled }: ActivitiesAddNewMen
34
35
  return (
35
36
  <Popover open={open} onOpenChange={setOpen}>
36
37
  <PopoverTrigger asChild>
37
- <button
38
+ <Button
38
39
  type="button"
39
40
  disabled={disabled}
40
41
  aria-label={t('customers.activities.addNew', 'Add new')}
41
- className="inline-flex items-center gap-1.5 overflow-hidden rounded-md bg-foreground pl-3 pr-3.5 py-2 text-xs font-semibold text-background transition-colors hover:bg-foreground/90 disabled:opacity-60"
42
+ className="gap-1.5 overflow-hidden rounded-md bg-foreground pl-3 pr-3.5 py-2 text-xs font-semibold text-background hover:bg-foreground/90 disabled:opacity-60"
42
43
  >
43
44
  <Check className="size-3.5" />
44
45
  {t('customers.activities.addNew', 'Add new')}
45
- </button>
46
+ </Button>
46
47
  </PopoverTrigger>
47
48
  <PopoverContent align="end" className="w-[180px] p-1">
48
49
  <ul className="flex flex-col">
49
50
  {MENU_ITEMS.map(({ kind, icon: Icon, key, fallback }) => (
50
51
  <li key={kind}>
51
- <button
52
+ <Button
52
53
  type="button"
54
+ variant="ghost"
53
55
  onClick={() => handleSelect(kind)}
54
- className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-accent/40"
56
+ className="h-auto w-full justify-start gap-2 rounded-md px-2 py-1.5 text-sm font-normal text-foreground hover:bg-accent/40"
55
57
  >
56
58
  <Icon className="size-4 text-muted-foreground" />
57
59
  {t(key, fallback)}
58
- </button>
60
+ </Button>
59
61
  </li>
60
62
  ))}
61
63
  </ul>
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { useT } from '@open-mercato/shared/lib/i18n/context'
5
5
  import { AiIcon } from '@open-mercato/ui/ai/AiIcon'
6
+ import { Button } from '@open-mercato/ui/primitives/button'
6
7
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@open-mercato/ui/primitives/tooltip'
7
8
  import { AI_TIMELINE_ACTIONS_BY_TYPE, resolveAiActions } from './aiActionCatalog'
8
9
 
@@ -23,13 +24,15 @@ export function AiActionChips({ activityType }: AiActionChipsProps) {
23
24
  {actions.map((action) => (
24
25
  <Tooltip key={action.key}>
25
26
  <TooltipTrigger asChild>
26
- <button
27
+ <Button
27
28
  type="button"
28
- className="inline-flex items-center gap-1 rounded-[4px] border border-dashed border-border bg-card pl-1.5 pr-[7px] py-[3px] text-[9px] font-medium text-muted-foreground/70 transition-colors hover:border-muted-foreground hover:text-foreground"
29
+ variant="outline"
30
+ size="sm"
31
+ className="h-auto gap-1 rounded-[4px] border-dashed bg-card pl-1.5 pr-[7px] py-[3px] text-[9px] font-medium text-muted-foreground/70 shadow-none hover:border-muted-foreground hover:text-foreground"
29
32
  >
30
33
  <AiIcon className="size-2.5 shrink-0" />
31
34
  {t(action.i18nKey, action.fallback)}
32
- </button>
35
+ </Button>
33
36
  </TooltipTrigger>
34
37
  <TooltipContent side="bottom" className="text-xs">
35
38
  {t('customers.ai.comingSoon', 'Coming soon')}
@@ -1,12 +1,14 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { Users, Search, X, Clock, CheckCircle2, XCircle } from 'lucide-react'
4
+ import { Users, X, Clock, CheckCircle2, XCircle } from 'lucide-react'
5
5
  import { cn } from '@open-mercato/shared/lib/utils'
6
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
8
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
8
9
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
9
10
  import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
11
+ import { SearchInput } from '@open-mercato/ui/primitives/search-input'
10
12
  import { fetchAssignableStaffMembersPage } from '../assignableStaff'
11
13
  import type { ActivityType, ScheduleFieldId } from './fieldConfig'
12
14
  import { isVisible, getFieldLabel } from './fieldConfig'
@@ -84,17 +86,13 @@ function ParticipantSearchPopover({
84
86
  </Button>
85
87
  </PopoverTrigger>
86
88
  <PopoverContent align="start" className="w-72 p-2">
87
- <div className="flex items-center gap-2 rounded-md border bg-background px-2 py-1.5 mb-2">
88
- <Search className="size-3.5 text-muted-foreground shrink-0" />
89
- <input
90
- type="text"
91
- value={query}
92
- onChange={(e) => setQuery(e.target.value)}
93
- placeholder={t('customers.schedule.searchParticipant', 'Search team members...')}
94
- className="flex-1 bg-transparent text-sm focus:outline-none"
95
- autoFocus
96
- />
97
- </div>
89
+ <SearchInput
90
+ value={query}
91
+ onChange={setQuery}
92
+ placeholder={t('customers.schedule.searchParticipant', 'Search team members...')}
93
+ className="mb-2"
94
+ autoFocus
95
+ />
98
96
  {selectableResults.length ? (
99
97
  <div className="mb-2">
100
98
  <Button
@@ -228,16 +226,16 @@ export function ParticipantsField({
228
226
  {participants.length > 0 && (
229
227
  <div className="mt-3 flex flex-wrap items-center gap-x-[16px] gap-y-[6px] text-xs">
230
228
  <span className="font-medium text-muted-foreground">{t('customers.schedule.guestPermissions', 'Guest permissions:')}</span>
231
- <label className="flex items-center gap-1.5 cursor-pointer">
232
- <input type="checkbox" checked={guestPermissions.canInviteOthers} onChange={(e) => setGuestPermissions((p) => ({ ...p, canInviteOthers: e.target.checked }))} className="rounded" />
229
+ <label htmlFor="guest-perm-invite" className="flex items-center gap-1.5 cursor-pointer">
230
+ <Checkbox id="guest-perm-invite" checked={guestPermissions.canInviteOthers} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canInviteOthers: checked === true }))} />
233
231
  {t('customers.schedule.guestPerm.invite', 'Invite others')}
234
232
  </label>
235
- <label className="flex items-center gap-1.5 cursor-pointer">
236
- <input type="checkbox" checked={guestPermissions.canModify} onChange={(e) => setGuestPermissions((p) => ({ ...p, canModify: e.target.checked }))} className="rounded" />
233
+ <label htmlFor="guest-perm-modify" className="flex items-center gap-1.5 cursor-pointer">
234
+ <Checkbox id="guest-perm-modify" checked={guestPermissions.canModify} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canModify: checked === true }))} />
237
235
  {t('customers.schedule.guestPerm.modify', 'Modify')}
238
236
  </label>
239
- <label className="flex items-center gap-1.5 cursor-pointer">
240
- <input type="checkbox" checked={guestPermissions.canSeeList} onChange={(e) => setGuestPermissions((p) => ({ ...p, canSeeList: e.target.checked }))} className="rounded" />
237
+ <label htmlFor="guest-perm-see-list" className="flex items-center gap-1.5 cursor-pointer">
238
+ <Checkbox id="guest-perm-see-list" checked={guestPermissions.canSeeList} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canSeeList: checked === true }))} />
241
239
  {t('customers.schedule.guestPerm.seeList', 'See list')}
242
240
  </label>
243
241
  </div>
@@ -79,7 +79,7 @@ export async function POST(req: Request, ctx: { params?: Promise<{ id?: string }
79
79
  } catch (error) {
80
80
  const job = await progressService.getJob(run.progressJobId, progressCtx)
81
81
  const cancelRequested = job && (job.status === 'running' || job.status === 'cancelled')
82
- ? await progressService.isCancellationRequested(run.progressJobId, progressCtx.tenantId)
82
+ ? await progressService.isCancellationRequested(run.progressJobId, progressCtx.tenantId, progressCtx.organizationId)
83
83
  : false
84
84
 
85
85
  if (job?.status !== 'cancelled' && !cancelRequested) {
@@ -7,7 +7,8 @@ import { DataTable } from '@open-mercato/ui/backend/DataTable'
7
7
  import type { ColumnDef } from '@tanstack/react-table'
8
8
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
9
9
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
10
- import { Badge, type BadgeProps } from '@open-mercato/ui/primitives/badge'
10
+ import { Badge } from '@open-mercato/ui/primitives/badge'
11
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
11
12
  import { Card, CardContent, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'
12
13
  import { Button } from '@open-mercato/ui/primitives/button'
13
14
  import { Input } from '@open-mercato/ui/primitives/input'
@@ -40,6 +41,7 @@ import {
40
41
  Settings2,
41
42
  ShieldCheck,
42
43
  } from 'lucide-react'
44
+ import { getSyncRunStatusVariant, getSyncSummaryVariant } from '../../lib/syncRunStatus'
43
45
 
44
46
  type SyncRunRow = {
45
47
  id: string
@@ -107,57 +109,8 @@ type SyncScheduleEditorState = {
107
109
  updatedAt?: string | null
108
110
  }
109
111
 
110
- const STATUS_STYLES: Record<string, string> = {
111
- pending: 'bg-gray-100 text-gray-800',
112
- running: 'bg-blue-100 text-blue-800',
113
- completed: 'bg-green-100 text-green-800',
114
- failed: 'bg-red-100 text-red-800',
115
- cancelled: 'bg-yellow-100 text-yellow-800',
116
- paused: 'bg-orange-100 text-orange-800',
117
- }
118
-
119
112
  const DEFAULT_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
120
113
 
121
- type SummaryBadgeStyle = {
122
- variant: BadgeProps['variant']
123
- className?: string
124
- }
125
-
126
- function getSummaryBadgeStyle(kind: 'enabled' | 'disabled' | 'ready' | 'missing' | 'scheduled' | 'paused' | 'none'): SummaryBadgeStyle {
127
- if (kind === 'enabled' || kind === 'ready') {
128
- return {
129
- variant: 'outline',
130
- className: 'border-emerald-500/30 bg-emerald-500/15 text-emerald-200',
131
- }
132
- }
133
-
134
- if (kind === 'disabled' || kind === 'missing') {
135
- return {
136
- variant: 'outline',
137
- className: 'border-red-500/30 bg-red-500/15 text-red-200',
138
- }
139
- }
140
-
141
- if (kind === 'paused') {
142
- return {
143
- variant: 'outline',
144
- className: 'border-amber-500/30 bg-amber-500/15 text-amber-200',
145
- }
146
- }
147
-
148
- if (kind === 'scheduled') {
149
- return {
150
- variant: 'outline',
151
- className: 'border-sky-500/30 bg-sky-500/15 text-sky-200',
152
- }
153
- }
154
-
155
- return {
156
- variant: 'outline',
157
- className: 'border-muted-foreground/20 bg-muted/50 text-muted-foreground',
158
- }
159
- }
160
-
161
114
  function formatEntityTypeLabel(entityType: string): string {
162
115
  return entityType
163
116
  .replace(/[_-]+/g, ' ')
@@ -589,9 +542,9 @@ export default function SyncRunsDashboardPage() {
589
542
  accessorKey: 'status',
590
543
  header: t('data_sync.dashboard.columns.status'),
591
544
  cell: ({ row }) => (
592
- <Badge variant="secondary" className={STATUS_STYLES[row.original.status] ?? ''}>
545
+ <StatusBadge variant={getSyncRunStatusVariant(row.original.status)}>
593
546
  {t(`data_sync.dashboard.status.${row.original.status}`)}
594
- </Badge>
547
+ </StatusBadge>
595
548
  ),
596
549
  },
597
550
  {
@@ -622,9 +575,9 @@ export default function SyncRunsDashboardPage() {
622
575
  )
623
576
  const hasSavedSchedule = Boolean(scheduleEditor.id)
624
577
  const selectedEntityLabel = selectedEntityType ? formatEntityTypeLabel(selectedEntityType) : t('data_sync.dashboard.columns.entityType')
625
- const integrationStateBadge = getSummaryBadgeStyle(selectedIntegration?.isEnabled ? 'enabled' : 'disabled')
626
- const credentialsBadge = getSummaryBadgeStyle(selectedIntegration?.hasCredentials ? 'ready' : 'missing')
627
- const scheduleBadge = getSummaryBadgeStyle(
578
+ const integrationStateVariant = getSyncSummaryVariant(selectedIntegration?.isEnabled ? 'enabled' : 'disabled')
579
+ const credentialsVariant = getSyncSummaryVariant(selectedIntegration?.hasCredentials ? 'ready' : 'missing')
580
+ const scheduleVariant = getSyncSummaryVariant(
628
581
  hasSavedSchedule
629
582
  ? (scheduleEditor.isEnabled ? 'scheduled' : 'paused')
630
583
  : 'none',
@@ -668,19 +621,19 @@ export default function SyncRunsDashboardPage() {
668
621
  <ArrowRightLeft className="size-3.5" />
669
622
  {t(`data_sync.dashboard.direction.${selectedDirection}`)}
670
623
  </Badge>
671
- <Badge variant={integrationStateBadge.variant} className={`gap-1.5 ${integrationStateBadge.className ?? ''}`}>
624
+ <Badge variant={integrationStateVariant} className="gap-1.5">
672
625
  <ShieldCheck className="size-3.5" />
673
626
  {selectedIntegration.isEnabled
674
627
  ? t('data_sync.dashboard.start.status.enabled', 'Integration enabled')
675
628
  : t('data_sync.dashboard.start.status.disabled', 'Integration disabled')}
676
629
  </Badge>
677
- <Badge variant={credentialsBadge.variant} className={`gap-1.5 ${credentialsBadge.className ?? ''}`}>
630
+ <Badge variant={credentialsVariant} className="gap-1.5">
678
631
  <PlugZap className="size-3.5" />
679
632
  {selectedIntegration.hasCredentials
680
633
  ? t('data_sync.dashboard.start.status.credentialsReady', 'Credentials ready')
681
634
  : t('data_sync.dashboard.start.status.credentialsMissing', 'Credentials missing')}
682
635
  </Badge>
683
- <Badge variant={scheduleBadge.variant} className={`gap-1.5 ${scheduleBadge.className ?? ''}`}>
636
+ <Badge variant={scheduleVariant} className="gap-1.5">
684
637
  <CalendarClock className="size-3.5" />
685
638
  {hasSavedSchedule
686
639
  ? (scheduleEditor.isEnabled