@open-mercato/core 0.6.8-develop.6983.1.e4f794099e → 0.6.8-develop.6985.1.fb93574faa

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 (597) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/notification/index.js +6 -0
  3. package/dist/generated/entities/notification/index.js.map +2 -2
  4. package/dist/generated/entities/notification_preference/index.js +19 -0
  5. package/dist/generated/entities/notification_preference/index.js.map +7 -0
  6. package/dist/generated/entities/notification_type/index.js +21 -0
  7. package/dist/generated/entities/notification_type/index.js.map +7 -0
  8. package/dist/generated/entities/notification_type_override/index.js +17 -0
  9. package/dist/generated/entities/notification_type_override/index.js.map +7 -0
  10. package/dist/generated/entities/push_notification_delivery/index.js +41 -0
  11. package/dist/generated/entities/push_notification_delivery/index.js.map +7 -0
  12. package/dist/generated/entities/user_device/index.js +35 -0
  13. package/dist/generated/entities/user_device/index.js.map +7 -0
  14. package/dist/generated/entities.ids.generated.js +12 -1
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +72 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/api.js +2 -1
  19. package/dist/helpers/integration/api.js.map +2 -2
  20. package/dist/helpers/integration/appRoot.js +9 -0
  21. package/dist/helpers/integration/appRoot.js.map +7 -0
  22. package/dist/helpers/integration/dbFixtures.js +1 -4
  23. package/dist/helpers/integration/dbFixtures.js.map +2 -2
  24. package/dist/helpers/integration/pushFake.js +126 -0
  25. package/dist/helpers/integration/pushFake.js.map +7 -0
  26. package/dist/modules/auth/notifications.js +16 -0
  27. package/dist/modules/auth/notifications.js.map +2 -2
  28. package/dist/modules/business_rules/notifications.js +2 -0
  29. package/dist/modules/business_rules/notifications.js.map +2 -2
  30. package/dist/modules/catalog/notifications.js +2 -0
  31. package/dist/modules/catalog/notifications.js.map +2 -2
  32. package/dist/modules/communication_channels/acl.js +9 -0
  33. package/dist/modules/communication_channels/acl.js.map +2 -2
  34. package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js +133 -0
  35. package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js.map +7 -0
  36. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +6 -2
  37. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +2 -2
  38. package/dist/modules/communication_channels/api/get/channels/[id]/route.js +6 -2
  39. package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +2 -2
  40. package/dist/modules/communication_channels/api/get/channels/route.js +2 -1
  41. package/dist/modules/communication_channels/api/get/channels/route.js.map +2 -2
  42. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +10 -5
  43. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
  44. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +29 -1
  45. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  46. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +157 -0
  47. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +7 -0
  48. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +8 -1
  49. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
  50. package/dist/modules/communication_channels/commands/admin-delete-channel.js +141 -0
  51. package/dist/modules/communication_channels/commands/admin-delete-channel.js.map +7 -0
  52. package/dist/modules/communication_channels/commands/connect-credential-channel.js +25 -8
  53. package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
  54. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +2 -2
  55. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  56. package/dist/modules/communication_channels/commands/push-register.js +1 -1
  57. package/dist/modules/communication_channels/commands/push-register.js.map +2 -2
  58. package/dist/modules/communication_channels/commands/push-unregister.js +4 -3
  59. package/dist/modules/communication_channels/commands/push-unregister.js.map +2 -2
  60. package/dist/modules/communication_channels/data/entities.js +4 -0
  61. package/dist/modules/communication_channels/data/entities.js.map +2 -2
  62. package/dist/modules/communication_channels/data/validators.js +2 -1
  63. package/dist/modules/communication_channels/data/validators.js.map +2 -2
  64. package/dist/modules/communication_channels/lib/access-control.js +11 -1
  65. package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
  66. package/dist/modules/communication_channels/lib/connect-channel.js +20 -12
  67. package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
  68. package/dist/modules/communication_channels/lib/push-adapter.js +68 -0
  69. package/dist/modules/communication_channels/lib/push-adapter.js.map +7 -0
  70. package/dist/modules/communication_channels/lib/push-capabilities.js +31 -0
  71. package/dist/modules/communication_channels/lib/push-capabilities.js.map +7 -0
  72. package/dist/modules/communication_channels/lib/push-connect-error.js +20 -0
  73. package/dist/modules/communication_channels/lib/push-connect-error.js.map +7 -0
  74. package/dist/modules/communication_channels/lib/push-credential-errors.js +41 -0
  75. package/dist/modules/communication_channels/lib/push-credential-errors.js.map +7 -0
  76. package/dist/modules/communication_channels/lib/push-envelope.js +46 -0
  77. package/dist/modules/communication_channels/lib/push-envelope.js.map +7 -0
  78. package/dist/modules/communication_channels/lib/refcounted-client-cache.js +64 -0
  79. package/dist/modules/communication_channels/lib/refcounted-client-cache.js.map +7 -0
  80. package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js +13 -0
  81. package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js.map +7 -0
  82. package/dist/modules/communication_channels/notifications.js +4 -0
  83. package/dist/modules/communication_channels/notifications.js.map +2 -2
  84. package/dist/modules/communication_channels/setup.js +2 -0
  85. package/dist/modules/communication_channels/setup.js.map +2 -2
  86. package/dist/modules/configs/lib/upgrade-actions.js +21 -0
  87. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  88. package/dist/modules/customer_accounts/notifications.js +12 -0
  89. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  90. package/dist/modules/customers/notifications.js +4 -0
  91. package/dist/modules/customers/notifications.js.map +2 -2
  92. package/dist/modules/devices/acl.js +11 -0
  93. package/dist/modules/devices/acl.js.map +7 -0
  94. package/dist/modules/devices/api/[id]/route.js +167 -0
  95. package/dist/modules/devices/api/[id]/route.js.map +7 -0
  96. package/dist/modules/devices/api/admin/devices/[id]/route.js +219 -0
  97. package/dist/modules/devices/api/admin/devices/[id]/route.js.map +7 -0
  98. package/dist/modules/devices/api/admin/devices/route.js +140 -0
  99. package/dist/modules/devices/api/admin/devices/route.js.map +7 -0
  100. package/dist/modules/devices/api/auth.js +9 -0
  101. package/dist/modules/devices/api/auth.js.map +7 -0
  102. package/dist/modules/devices/api/deviceList.js +53 -0
  103. package/dist/modules/devices/api/deviceList.js.map +7 -0
  104. package/dist/modules/devices/api/deviceOps.js +118 -0
  105. package/dist/modules/devices/api/deviceOps.js.map +7 -0
  106. package/dist/modules/devices/api/openapi.js +19 -0
  107. package/dist/modules/devices/api/openapi.js.map +7 -0
  108. package/dist/modules/devices/api/route.js +143 -0
  109. package/dist/modules/devices/api/route.js.map +7 -0
  110. package/dist/modules/devices/backend/devices/[id]/page.js +135 -0
  111. package/dist/modules/devices/backend/devices/[id]/page.js.map +7 -0
  112. package/dist/modules/devices/backend/devices/[id]/page.meta.js +16 -0
  113. package/dist/modules/devices/backend/devices/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/devices/backend/devices/create/page.js +68 -0
  115. package/dist/modules/devices/backend/devices/create/page.js.map +7 -0
  116. package/dist/modules/devices/backend/devices/create/page.meta.js +16 -0
  117. package/dist/modules/devices/backend/devices/create/page.meta.js.map +7 -0
  118. package/dist/modules/devices/backend/devices/page.js +235 -0
  119. package/dist/modules/devices/backend/devices/page.js.map +7 -0
  120. package/dist/modules/devices/backend/devices/page.meta.js +23 -0
  121. package/dist/modules/devices/backend/devices/page.meta.js.map +7 -0
  122. package/dist/modules/devices/commands/deactivate.js +107 -0
  123. package/dist/modules/devices/commands/deactivate.js.map +7 -0
  124. package/dist/modules/devices/commands/index.js +4 -0
  125. package/dist/modules/devices/commands/index.js.map +7 -0
  126. package/dist/modules/devices/commands/register.js +183 -0
  127. package/dist/modules/devices/commands/register.js.map +7 -0
  128. package/dist/modules/devices/commands/shared.js +74 -0
  129. package/dist/modules/devices/commands/shared.js.map +7 -0
  130. package/dist/modules/devices/commands/update.js +126 -0
  131. package/dist/modules/devices/commands/update.js.map +7 -0
  132. package/dist/modules/devices/data/entities.js +83 -0
  133. package/dist/modules/devices/data/entities.js.map +7 -0
  134. package/dist/modules/devices/data/validators.js +49 -0
  135. package/dist/modules/devices/data/validators.js.map +7 -0
  136. package/dist/modules/devices/di.js +11 -0
  137. package/dist/modules/devices/di.js.map +7 -0
  138. package/dist/modules/devices/encryption.js +12 -0
  139. package/dist/modules/devices/encryption.js.map +7 -0
  140. package/dist/modules/devices/events.js +19 -0
  141. package/dist/modules/devices/events.js.map +7 -0
  142. package/dist/modules/devices/index.js +17 -0
  143. package/dist/modules/devices/index.js.map +7 -0
  144. package/dist/modules/devices/lib/operationMetadata.js +20 -0
  145. package/dist/modules/devices/lib/operationMetadata.js.map +7 -0
  146. package/dist/modules/devices/migrations/Migration20260602120000.js +15 -0
  147. package/dist/modules/devices/migrations/Migration20260602120000.js.map +7 -0
  148. package/dist/modules/devices/migrations/Migration20260630120000.js +15 -0
  149. package/dist/modules/devices/migrations/Migration20260630120000.js.map +7 -0
  150. package/dist/modules/devices/migrations/Migration20260701120000.js +13 -0
  151. package/dist/modules/devices/migrations/Migration20260701120000.js.map +7 -0
  152. package/dist/modules/devices/migrations/Migration20260721120000.js +15 -0
  153. package/dist/modules/devices/migrations/Migration20260721120000.js.map +7 -0
  154. package/dist/modules/devices/setup.js +13 -0
  155. package/dist/modules/devices/setup.js.map +7 -0
  156. package/dist/modules/entities/cli.js +8 -3
  157. package/dist/modules/entities/cli.js.map +2 -2
  158. package/dist/modules/entities/migrations/Migration20260722120000.js +28 -0
  159. package/dist/modules/entities/migrations/Migration20260722120000.js.map +7 -0
  160. package/dist/modules/inbox_ops/notifications.js +2 -0
  161. package/dist/modules/inbox_ops/notifications.js.map +2 -2
  162. package/dist/modules/messages/notifications.js +2 -0
  163. package/dist/modules/messages/notifications.js.map +2 -2
  164. package/dist/modules/notifications/acl.js +3 -1
  165. package/dist/modules/notifications/acl.js.map +2 -2
  166. package/dist/modules/notifications/api/admin/preferences/route.js +167 -0
  167. package/dist/modules/notifications/api/admin/preferences/route.js.map +7 -0
  168. package/dist/modules/notifications/api/channels/route.js +54 -0
  169. package/dist/modules/notifications/api/channels/route.js.map +7 -0
  170. package/dist/modules/notifications/api/preferences/route.js +126 -0
  171. package/dist/modules/notifications/api/preferences/route.js.map +7 -0
  172. package/dist/modules/notifications/api/route.js +8 -1
  173. package/dist/modules/notifications/api/route.js.map +2 -2
  174. package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js +187 -0
  175. package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js.map +7 -0
  176. package/dist/modules/notifications/api/types/route.js +225 -0
  177. package/dist/modules/notifications/api/types/route.js.map +7 -0
  178. package/dist/modules/notifications/api/unread-count/route.js +5 -1
  179. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  180. package/dist/modules/notifications/backend/notifications/user-preferences/page.js +10 -0
  181. package/dist/modules/notifications/backend/notifications/user-preferences/page.js.map +7 -0
  182. package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js +28 -0
  183. package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js.map +7 -0
  184. package/dist/modules/notifications/backend/profile/notification-preferences/page.js +10 -0
  185. package/dist/modules/notifications/backend/profile/notification-preferences/page.js.map +7 -0
  186. package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +33 -0
  187. package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +7 -0
  188. package/dist/modules/notifications/data/entities.js +128 -1
  189. package/dist/modules/notifications/data/entities.js.map +2 -2
  190. package/dist/modules/notifications/data/validators.js +95 -2
  191. package/dist/modules/notifications/data/validators.js.map +2 -2
  192. package/dist/modules/notifications/di.js +7 -0
  193. package/dist/modules/notifications/di.js.map +2 -2
  194. package/dist/modules/notifications/events.js +17 -0
  195. package/dist/modules/notifications/events.js.map +2 -2
  196. package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js +190 -0
  197. package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js.map +7 -0
  198. package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js +124 -0
  199. package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js.map +7 -0
  200. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +166 -1
  201. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  202. package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js +172 -0
  203. package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js.map +7 -0
  204. package/dist/modules/notifications/generators.js +63 -0
  205. package/dist/modules/notifications/generators.js.map +7 -0
  206. package/dist/modules/notifications/lib/delivery-strategies-registry.js +19 -0
  207. package/dist/modules/notifications/lib/delivery-strategies-registry.js.map +7 -0
  208. package/dist/modules/notifications/lib/deliveryStrategies.js.map +2 -2
  209. package/dist/modules/notifications/lib/derive-category.js +8 -0
  210. package/dist/modules/notifications/lib/derive-category.js.map +7 -0
  211. package/dist/modules/notifications/lib/notification-channel-registry.js +35 -0
  212. package/dist/modules/notifications/lib/notification-channel-registry.js.map +7 -0
  213. package/dist/modules/notifications/lib/notification-type-registry.js +87 -0
  214. package/dist/modules/notifications/lib/notification-type-registry.js.map +7 -0
  215. package/dist/modules/notifications/lib/notificationCopy.js +14 -0
  216. package/dist/modules/notifications/lib/notificationCopy.js.map +7 -0
  217. package/dist/modules/notifications/lib/notificationFactory.js +4 -1
  218. package/dist/modules/notifications/lib/notificationFactory.js.map +2 -2
  219. package/dist/modules/notifications/lib/notificationMapper.js +2 -0
  220. package/dist/modules/notifications/lib/notificationMapper.js.map +2 -2
  221. package/dist/modules/notifications/lib/notificationPreferenceService.js +83 -0
  222. package/dist/modules/notifications/lib/notificationPreferenceService.js.map +7 -0
  223. package/dist/modules/notifications/lib/notificationService.js +83 -28
  224. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  225. package/dist/modules/notifications/lib/notificationVisibility.js +24 -0
  226. package/dist/modules/notifications/lib/notificationVisibility.js.map +7 -0
  227. package/dist/modules/notifications/lib/routeHelpers.js +11 -1
  228. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  229. package/dist/modules/notifications/lib/shouldDeliver.js +46 -0
  230. package/dist/modules/notifications/lib/shouldDeliver.js.map +7 -0
  231. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +54 -0
  232. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +7 -0
  233. package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js +12 -0
  234. package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js.map +7 -0
  235. package/dist/modules/notifications/lib/typeCatalogue.js +71 -0
  236. package/dist/modules/notifications/lib/typeCatalogue.js.map +7 -0
  237. package/dist/modules/notifications/lib/typeChannelSettings.js +12 -0
  238. package/dist/modules/notifications/lib/typeChannelSettings.js.map +7 -0
  239. package/dist/modules/notifications/lib/typeOverrides.js +19 -0
  240. package/dist/modules/notifications/lib/typeOverrides.js.map +7 -0
  241. package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js +14 -0
  242. package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js.map +7 -0
  243. package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js +15 -0
  244. package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js.map +7 -0
  245. package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js +13 -0
  246. package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js.map +7 -0
  247. package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js +15 -0
  248. package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js.map +7 -0
  249. package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js +13 -0
  250. package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js.map +7 -0
  251. package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js +14 -0
  252. package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js.map +7 -0
  253. package/dist/modules/notifications/notification-channels.js +26 -0
  254. package/dist/modules/notifications/notification-channels.js.map +7 -0
  255. package/dist/modules/notifications/notifications.delivery-strategies.js +12 -0
  256. package/dist/modules/notifications/notifications.delivery-strategies.js.map +7 -0
  257. package/dist/modules/notifications/setup.js +16 -1
  258. package/dist/modules/notifications/setup.js.map +2 -2
  259. package/dist/modules/notifications/subscribers/deliver-notification.js +66 -57
  260. package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
  261. package/dist/modules/notifications/subscribers/sync-notification-types.js +16 -0
  262. package/dist/modules/notifications/subscribers/sync-notification-types.js.map +7 -0
  263. package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js +23 -0
  264. package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js.map +7 -0
  265. package/dist/modules/notifications/widgets/injection-table.js +14 -0
  266. package/dist/modules/notifications/widgets/injection-table.js.map +7 -0
  267. package/dist/modules/push_notifications/acl.js +13 -0
  268. package/dist/modules/push_notifications/acl.js.map +7 -0
  269. package/dist/modules/push_notifications/api/custom-send/route.js +141 -0
  270. package/dist/modules/push_notifications/api/custom-send/route.js.map +7 -0
  271. package/dist/modules/push_notifications/api/deliveries/[id]/route.js +110 -0
  272. package/dist/modules/push_notifications/api/deliveries/[id]/route.js.map +7 -0
  273. package/dist/modules/push_notifications/api/deliveries/route.js +56 -0
  274. package/dist/modules/push_notifications/api/deliveries/route.js.map +7 -0
  275. package/dist/modules/push_notifications/api/openapi.js +19 -0
  276. package/dist/modules/push_notifications/api/openapi.js.map +7 -0
  277. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js +103 -0
  278. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js.map +7 -0
  279. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js +13 -0
  280. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js.map +7 -0
  281. package/dist/modules/push_notifications/backend/push_notifications/page.js +213 -0
  282. package/dist/modules/push_notifications/backend/push_notifications/page.js.map +7 -0
  283. package/dist/modules/push_notifications/backend/push_notifications/page.meta.js +23 -0
  284. package/dist/modules/push_notifications/backend/push_notifications/page.meta.js.map +7 -0
  285. package/dist/modules/push_notifications/backend/push_notifications/send/page.js +245 -0
  286. package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +7 -0
  287. package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js +26 -0
  288. package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js.map +7 -0
  289. package/dist/modules/push_notifications/data/entities.js +92 -0
  290. package/dist/modules/push_notifications/data/entities.js.map +7 -0
  291. package/dist/modules/push_notifications/data/extensions.js +23 -0
  292. package/dist/modules/push_notifications/data/extensions.js.map +7 -0
  293. package/dist/modules/push_notifications/data/validators.js +89 -0
  294. package/dist/modules/push_notifications/data/validators.js.map +7 -0
  295. package/dist/modules/push_notifications/di.js +17 -0
  296. package/dist/modules/push_notifications/di.js.map +7 -0
  297. package/dist/modules/push_notifications/events.js +24 -0
  298. package/dist/modules/push_notifications/events.js.map +7 -0
  299. package/dist/modules/push_notifications/index.js +16 -0
  300. package/dist/modules/push_notifications/index.js.map +7 -0
  301. package/dist/modules/push_notifications/lib/fake-provider-recorder.js +80 -0
  302. package/dist/modules/push_notifications/lib/fake-provider-recorder.js.map +7 -0
  303. package/dist/modules/push_notifications/lib/push-delivery-strategy.js +54 -0
  304. package/dist/modules/push_notifications/lib/push-delivery-strategy.js.map +7 -0
  305. package/dist/modules/push_notifications/lib/push-delivery.js +297 -0
  306. package/dist/modules/push_notifications/lib/push-delivery.js.map +7 -0
  307. package/dist/modules/push_notifications/lib/push-fanout.js +138 -0
  308. package/dist/modules/push_notifications/lib/push-fanout.js.map +7 -0
  309. package/dist/modules/push_notifications/lib/push-health.js +32 -0
  310. package/dist/modules/push_notifications/lib/push-health.js.map +7 -0
  311. package/dist/modules/push_notifications/lib/push-reaper.js +87 -0
  312. package/dist/modules/push_notifications/lib/push-reaper.js.map +7 -0
  313. package/dist/modules/push_notifications/lib/push-receipt-reaper.js +136 -0
  314. package/dist/modules/push_notifications/lib/push-receipt-reaper.js.map +7 -0
  315. package/dist/modules/push_notifications/lib/push-stub-adapter.js +63 -0
  316. package/dist/modules/push_notifications/lib/push-stub-adapter.js.map +7 -0
  317. package/dist/modules/push_notifications/lib/queue.js +53 -0
  318. package/dist/modules/push_notifications/lib/queue.js.map +7 -0
  319. package/dist/modules/push_notifications/lib/reclaim-window.js +13 -0
  320. package/dist/modules/push_notifications/lib/reclaim-window.js.map +7 -0
  321. package/dist/modules/push_notifications/lib/send-custom-push.js +48 -0
  322. package/dist/modules/push_notifications/lib/send-custom-push.js.map +7 -0
  323. package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js +16 -0
  324. package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js.map +7 -0
  325. package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js +13 -0
  326. package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js.map +7 -0
  327. package/dist/modules/push_notifications/notifications.delivery-strategies.js +8 -0
  328. package/dist/modules/push_notifications/notifications.delivery-strategies.js.map +7 -0
  329. package/dist/modules/push_notifications/notifications.js +40 -0
  330. package/dist/modules/push_notifications/notifications.js.map +7 -0
  331. package/dist/modules/push_notifications/setup.js +52 -0
  332. package/dist/modules/push_notifications/setup.js.map +7 -0
  333. package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js +57 -0
  334. package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js.map +7 -0
  335. package/dist/modules/push_notifications/workers/send-push.worker.js +27 -0
  336. package/dist/modules/push_notifications/workers/send-push.worker.js.map +7 -0
  337. package/dist/modules/sales/notifications.js +8 -0
  338. package/dist/modules/sales/notifications.js.map +2 -2
  339. package/dist/modules/staff/notifications.js +6 -0
  340. package/dist/modules/staff/notifications.js.map +2 -2
  341. package/dist/modules/workflows/notifications.js +2 -0
  342. package/dist/modules/workflows/notifications.js.map +2 -2
  343. package/generated/entities/notification/index.ts +3 -0
  344. package/generated/entities/notification_preference/index.ts +8 -0
  345. package/generated/entities/notification_type/index.ts +9 -0
  346. package/generated/entities/notification_type_override/index.ts +7 -0
  347. package/generated/entities/push_notification_delivery/index.ts +19 -0
  348. package/generated/entities/user_device/index.ts +16 -0
  349. package/generated/entities.ids.generated.ts +12 -1
  350. package/generated/entity-fields-registry.ts +72 -0
  351. package/package.json +7 -7
  352. package/src/helpers/integration/api.ts +2 -1
  353. package/src/helpers/integration/appRoot.ts +13 -0
  354. package/src/helpers/integration/dbFixtures.ts +1 -5
  355. package/src/helpers/integration/pushFake.ts +219 -0
  356. package/src/modules/auth/i18n/de.json +1 -0
  357. package/src/modules/auth/i18n/en.json +1 -0
  358. package/src/modules/auth/i18n/es.json +1 -0
  359. package/src/modules/auth/i18n/ko.json +1 -0
  360. package/src/modules/auth/i18n/pl.json +1 -0
  361. package/src/modules/auth/notifications.ts +16 -0
  362. package/src/modules/business_rules/i18n/de.json +2 -1
  363. package/src/modules/business_rules/i18n/en.json +2 -1
  364. package/src/modules/business_rules/i18n/es.json +2 -1
  365. package/src/modules/business_rules/i18n/ko.json +2 -1
  366. package/src/modules/business_rules/i18n/pl.json +2 -1
  367. package/src/modules/business_rules/notifications.ts +2 -0
  368. package/src/modules/catalog/i18n/de.json +2 -1
  369. package/src/modules/catalog/i18n/en.json +2 -1
  370. package/src/modules/catalog/i18n/es.json +2 -1
  371. package/src/modules/catalog/i18n/ko.json +2 -1
  372. package/src/modules/catalog/i18n/pl.json +2 -1
  373. package/src/modules/catalog/notifications.ts +2 -0
  374. package/src/modules/communication_channels/acl.ts +9 -0
  375. package/src/modules/communication_channels/api/delete/admin/channels/[id]/route.ts +168 -0
  376. package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +6 -2
  377. package/src/modules/communication_channels/api/get/channels/[id]/route.ts +6 -2
  378. package/src/modules/communication_channels/api/get/channels/route.ts +2 -1
  379. package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +16 -5
  380. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +37 -1
  381. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +175 -0
  382. package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +11 -1
  383. package/src/modules/communication_channels/commands/admin-delete-channel.ts +203 -0
  384. package/src/modules/communication_channels/commands/connect-credential-channel.ts +63 -15
  385. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +10 -2
  386. package/src/modules/communication_channels/commands/push-register.ts +7 -1
  387. package/src/modules/communication_channels/commands/push-unregister.ts +14 -3
  388. package/src/modules/communication_channels/data/entities.ts +10 -0
  389. package/src/modules/communication_channels/data/validators.ts +1 -0
  390. package/src/modules/communication_channels/i18n/de.json +35 -1
  391. package/src/modules/communication_channels/i18n/en.json +35 -1
  392. package/src/modules/communication_channels/i18n/es.json +35 -1
  393. package/src/modules/communication_channels/i18n/ko.json +35 -1
  394. package/src/modules/communication_channels/i18n/pl.json +35 -1
  395. package/src/modules/communication_channels/lib/access-control.ts +42 -0
  396. package/src/modules/communication_channels/lib/adapter.ts +25 -0
  397. package/src/modules/communication_channels/lib/connect-channel.ts +61 -26
  398. package/src/modules/communication_channels/lib/push-adapter.ts +139 -0
  399. package/src/modules/communication_channels/lib/push-capabilities.ts +42 -0
  400. package/src/modules/communication_channels/lib/push-connect-error.ts +52 -0
  401. package/src/modules/communication_channels/lib/push-credential-errors.ts +58 -0
  402. package/src/modules/communication_channels/lib/push-envelope.ts +104 -0
  403. package/src/modules/communication_channels/lib/refcounted-client-cache.ts +123 -0
  404. package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +9 -0
  405. package/src/modules/communication_channels/migrations/Migration20260703123630_communication_channels.ts +20 -0
  406. package/src/modules/communication_channels/notifications.ts +4 -0
  407. package/src/modules/communication_channels/setup.ts +2 -0
  408. package/src/modules/configs/i18n/de.json +5 -1
  409. package/src/modules/configs/i18n/en.json +5 -1
  410. package/src/modules/configs/i18n/es.json +5 -1
  411. package/src/modules/configs/i18n/ko.json +5 -1
  412. package/src/modules/configs/i18n/pl.json +5 -1
  413. package/src/modules/configs/lib/upgrade-actions.ts +21 -0
  414. package/src/modules/customer_accounts/i18n/de.json +2 -1
  415. package/src/modules/customer_accounts/i18n/en.json +2 -1
  416. package/src/modules/customer_accounts/i18n/es.json +2 -1
  417. package/src/modules/customer_accounts/i18n/ko.json +2 -1
  418. package/src/modules/customer_accounts/i18n/pl.json +2 -1
  419. package/src/modules/customer_accounts/notifications.ts +12 -0
  420. package/src/modules/customers/i18n/de.json +2 -1
  421. package/src/modules/customers/i18n/en.json +2 -1
  422. package/src/modules/customers/i18n/es.json +2 -1
  423. package/src/modules/customers/i18n/ko.json +2 -1
  424. package/src/modules/customers/i18n/pl.json +2 -1
  425. package/src/modules/customers/notifications.ts +4 -0
  426. package/src/modules/devices/AGENTS.md +101 -0
  427. package/src/modules/devices/acl.ts +7 -0
  428. package/src/modules/devices/api/[id]/route.ts +180 -0
  429. package/src/modules/devices/api/admin/devices/[id]/route.ts +235 -0
  430. package/src/modules/devices/api/admin/devices/route.ts +160 -0
  431. package/src/modules/devices/api/auth.ts +13 -0
  432. package/src/modules/devices/api/deviceList.ts +54 -0
  433. package/src/modules/devices/api/deviceOps.ts +186 -0
  434. package/src/modules/devices/api/openapi.ts +23 -0
  435. package/src/modules/devices/api/route.ts +165 -0
  436. package/src/modules/devices/backend/devices/[id]/page.meta.ts +12 -0
  437. package/src/modules/devices/backend/devices/[id]/page.tsx +164 -0
  438. package/src/modules/devices/backend/devices/create/page.meta.ts +12 -0
  439. package/src/modules/devices/backend/devices/create/page.tsx +81 -0
  440. package/src/modules/devices/backend/devices/page.meta.ts +21 -0
  441. package/src/modules/devices/backend/devices/page.tsx +259 -0
  442. package/src/modules/devices/commands/deactivate.ts +126 -0
  443. package/src/modules/devices/commands/index.ts +3 -0
  444. package/src/modules/devices/commands/register.ts +199 -0
  445. package/src/modules/devices/commands/shared.ts +119 -0
  446. package/src/modules/devices/commands/update.ts +142 -0
  447. package/src/modules/devices/data/entities.ts +79 -0
  448. package/src/modules/devices/data/validators.ts +65 -0
  449. package/src/modules/devices/di.ts +9 -0
  450. package/src/modules/devices/encryption.ts +15 -0
  451. package/src/modules/devices/events.ts +16 -0
  452. package/src/modules/devices/i18n/de.json +45 -0
  453. package/src/modules/devices/i18n/en.json +45 -0
  454. package/src/modules/devices/i18n/es.json +45 -0
  455. package/src/modules/devices/i18n/ko.json +45 -0
  456. package/src/modules/devices/i18n/pl.json +45 -0
  457. package/src/modules/devices/index.ts +15 -0
  458. package/src/modules/devices/lib/operationMetadata.ts +35 -0
  459. package/src/modules/devices/migrations/.snapshot-open-mercato.json +200 -0
  460. package/src/modules/devices/migrations/Migration20260602120000.ts +15 -0
  461. package/src/modules/devices/migrations/Migration20260630120000.ts +20 -0
  462. package/src/modules/devices/migrations/Migration20260701120000.ts +13 -0
  463. package/src/modules/devices/migrations/Migration20260721120000.ts +18 -0
  464. package/src/modules/devices/setup.ts +11 -0
  465. package/src/modules/entities/cli.ts +16 -2
  466. package/src/modules/entities/migrations/Migration20260722120000.ts +38 -0
  467. package/src/modules/inbox_ops/i18n/de.json +2 -1
  468. package/src/modules/inbox_ops/i18n/en.json +2 -1
  469. package/src/modules/inbox_ops/i18n/es.json +2 -1
  470. package/src/modules/inbox_ops/i18n/ko.json +2 -1
  471. package/src/modules/inbox_ops/i18n/pl.json +2 -1
  472. package/src/modules/inbox_ops/notifications.ts +2 -0
  473. package/src/modules/messages/i18n/de.json +2 -1
  474. package/src/modules/messages/i18n/en.json +2 -1
  475. package/src/modules/messages/i18n/es.json +2 -1
  476. package/src/modules/messages/i18n/ko.json +2 -1
  477. package/src/modules/messages/i18n/pl.json +2 -1
  478. package/src/modules/messages/notifications.ts +2 -0
  479. package/src/modules/notifications/AGENTS.md +54 -0
  480. package/src/modules/notifications/acl.ts +2 -0
  481. package/src/modules/notifications/api/admin/preferences/route.ts +193 -0
  482. package/src/modules/notifications/api/channels/route.ts +54 -0
  483. package/src/modules/notifications/api/preferences/route.ts +131 -0
  484. package/src/modules/notifications/api/route.ts +8 -1
  485. package/src/modules/notifications/api/types/[id]/channels/[channel]/route.ts +226 -0
  486. package/src/modules/notifications/api/types/route.ts +239 -0
  487. package/src/modules/notifications/api/unread-count/route.ts +5 -1
  488. package/src/modules/notifications/backend/notifications/user-preferences/page.meta.ts +26 -0
  489. package/src/modules/notifications/backend/notifications/user-preferences/page.tsx +12 -0
  490. package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +31 -0
  491. package/src/modules/notifications/backend/profile/notification-preferences/page.tsx +12 -0
  492. package/src/modules/notifications/data/entities.ts +144 -0
  493. package/src/modules/notifications/data/validators.ts +116 -0
  494. package/src/modules/notifications/di.ts +7 -0
  495. package/src/modules/notifications/events.ts +23 -0
  496. package/src/modules/notifications/frontend/NotificationPreferenceMatrix.tsx +287 -0
  497. package/src/modules/notifications/frontend/NotificationPreferencesPageClient.tsx +155 -0
  498. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +273 -2
  499. package/src/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.tsx +214 -0
  500. package/src/modules/notifications/generators.ts +82 -0
  501. package/src/modules/notifications/i18n/de.json +43 -0
  502. package/src/modules/notifications/i18n/en.json +43 -0
  503. package/src/modules/notifications/i18n/es.json +43 -0
  504. package/src/modules/notifications/i18n/ko.json +43 -0
  505. package/src/modules/notifications/i18n/pl.json +44 -1
  506. package/src/modules/notifications/lib/delivery-strategies-registry.ts +30 -0
  507. package/src/modules/notifications/lib/deliveryStrategies.ts +40 -4
  508. package/src/modules/notifications/lib/derive-category.ts +15 -0
  509. package/src/modules/notifications/lib/notification-channel-registry.ts +59 -0
  510. package/src/modules/notifications/lib/notification-type-registry.ts +182 -0
  511. package/src/modules/notifications/lib/notificationCopy.ts +34 -0
  512. package/src/modules/notifications/lib/notificationFactory.ts +5 -1
  513. package/src/modules/notifications/lib/notificationMapper.ts +2 -0
  514. package/src/modules/notifications/lib/notificationPreferenceService.ts +138 -0
  515. package/src/modules/notifications/lib/notificationService.ts +129 -25
  516. package/src/modules/notifications/lib/notificationVisibility.ts +48 -0
  517. package/src/modules/notifications/lib/routeHelpers.ts +14 -1
  518. package/src/modules/notifications/lib/shouldDeliver.ts +112 -0
  519. package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +63 -0
  520. package/src/modules/notifications/lib/strategies/in-app-delivery-strategy.ts +21 -0
  521. package/src/modules/notifications/lib/typeCatalogue.ts +141 -0
  522. package/src/modules/notifications/lib/typeChannelSettings.ts +29 -0
  523. package/src/modules/notifications/lib/typeOverrides.ts +46 -0
  524. package/src/modules/notifications/migrations/.snapshot-open-mercato.json +874 -100
  525. package/src/modules/notifications/migrations/Migration20260625122947_notifications.ts +14 -0
  526. package/src/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.ts +15 -0
  527. package/src/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.ts +13 -0
  528. package/src/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.ts +15 -0
  529. package/src/modules/notifications/migrations/Migration20260706120000_notifications_channels.ts +13 -0
  530. package/src/modules/notifications/migrations/Migration20260716123121_notifications.ts +14 -0
  531. package/src/modules/notifications/notification-channels.ts +30 -0
  532. package/src/modules/notifications/notifications.delivery-strategies.ts +15 -0
  533. package/src/modules/notifications/setup.ts +23 -1
  534. package/src/modules/notifications/subscribers/deliver-notification.ts +105 -71
  535. package/src/modules/notifications/subscribers/sync-notification-types.ts +18 -0
  536. package/src/modules/notifications/widgets/injection/profile-preferences-menu/widget.ts +35 -0
  537. package/src/modules/notifications/widgets/injection-table.ts +18 -0
  538. package/src/modules/push_notifications/AGENTS.md +92 -0
  539. package/src/modules/push_notifications/acl.ts +9 -0
  540. package/src/modules/push_notifications/api/custom-send/route.ts +175 -0
  541. package/src/modules/push_notifications/api/deliveries/[id]/route.ts +122 -0
  542. package/src/modules/push_notifications/api/deliveries/route.ts +58 -0
  543. package/src/modules/push_notifications/api/openapi.ts +22 -0
  544. package/src/modules/push_notifications/backend/push_notifications/[id]/page.meta.ts +9 -0
  545. package/src/modules/push_notifications/backend/push_notifications/[id]/page.tsx +149 -0
  546. package/src/modules/push_notifications/backend/push_notifications/page.meta.ts +21 -0
  547. package/src/modules/push_notifications/backend/push_notifications/page.tsx +239 -0
  548. package/src/modules/push_notifications/backend/push_notifications/send/page.meta.ts +24 -0
  549. package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +327 -0
  550. package/src/modules/push_notifications/data/entities.ts +111 -0
  551. package/src/modules/push_notifications/data/extensions.ts +29 -0
  552. package/src/modules/push_notifications/data/validators.ts +102 -0
  553. package/src/modules/push_notifications/di.ts +20 -0
  554. package/src/modules/push_notifications/events.ts +21 -0
  555. package/src/modules/push_notifications/i18n/de.json +79 -0
  556. package/src/modules/push_notifications/i18n/en.json +79 -0
  557. package/src/modules/push_notifications/i18n/es.json +79 -0
  558. package/src/modules/push_notifications/i18n/ko.json +79 -0
  559. package/src/modules/push_notifications/i18n/pl.json +79 -0
  560. package/src/modules/push_notifications/index.ts +15 -0
  561. package/src/modules/push_notifications/lib/fake-provider-recorder.ts +154 -0
  562. package/src/modules/push_notifications/lib/push-delivery-strategy.ts +90 -0
  563. package/src/modules/push_notifications/lib/push-delivery.ts +455 -0
  564. package/src/modules/push_notifications/lib/push-fanout.ts +237 -0
  565. package/src/modules/push_notifications/lib/push-health.ts +47 -0
  566. package/src/modules/push_notifications/lib/push-reaper.ts +140 -0
  567. package/src/modules/push_notifications/lib/push-receipt-reaper.ts +210 -0
  568. package/src/modules/push_notifications/lib/push-stub-adapter.ts +80 -0
  569. package/src/modules/push_notifications/lib/queue.ts +74 -0
  570. package/src/modules/push_notifications/lib/reclaim-window.ts +16 -0
  571. package/src/modules/push_notifications/lib/send-custom-push.ts +100 -0
  572. package/src/modules/push_notifications/migrations/.snapshot-open-mercato.json +386 -0
  573. package/src/modules/push_notifications/migrations/Migration20260625150049_push_notifications.ts +16 -0
  574. package/src/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.ts +13 -0
  575. package/src/modules/push_notifications/notifications.delivery-strategies.ts +8 -0
  576. package/src/modules/push_notifications/notifications.ts +50 -0
  577. package/src/modules/push_notifications/setup.ts +92 -0
  578. package/src/modules/push_notifications/workers/reclaim-stuck.worker.ts +78 -0
  579. package/src/modules/push_notifications/workers/send-push.worker.ts +34 -0
  580. package/src/modules/sales/i18n/de.json +1 -0
  581. package/src/modules/sales/i18n/en.json +1 -0
  582. package/src/modules/sales/i18n/es.json +1 -0
  583. package/src/modules/sales/i18n/ko.json +1 -0
  584. package/src/modules/sales/i18n/pl.json +1 -0
  585. package/src/modules/sales/notifications.ts +8 -0
  586. package/src/modules/staff/i18n/de.json +1 -0
  587. package/src/modules/staff/i18n/en.json +1 -0
  588. package/src/modules/staff/i18n/es.json +1 -0
  589. package/src/modules/staff/i18n/ko.json +1 -0
  590. package/src/modules/staff/i18n/pl.json +1 -0
  591. package/src/modules/staff/notifications.ts +6 -0
  592. package/src/modules/workflows/i18n/de.json +1 -0
  593. package/src/modules/workflows/i18n/en.json +1 -0
  594. package/src/modules/workflows/i18n/es.json +1 -0
  595. package/src/modules/workflows/i18n/ko.json +1 -0
  596. package/src/modules/workflows/i18n/pl.json +1 -0
  597. package/src/modules/workflows/notifications.ts +2 -0
@@ -0,0 +1,190 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { Switch } from "@open-mercato/ui/primitives/switch";
6
+ import { SimpleTooltip } from "@open-mercato/ui/primitives/tooltip";
7
+ import { FilterBar } from "@open-mercato/ui/backend/FilterBar";
8
+ import { FilteredEmptyResults } from "@open-mercato/ui/backend/filters/FilteredEmptyResults";
9
+ const PREFERENCE_CHANNELS = [
10
+ {
11
+ key: "in_app",
12
+ labelKey: "notifications.preferences.channels.inApp",
13
+ labelFallback: "In-app",
14
+ hintKey: "notifications.preferences.channels.inAppHint",
15
+ hintFallback: "Notification center and bell."
16
+ },
17
+ {
18
+ key: "email",
19
+ labelKey: "notifications.preferences.channels.email",
20
+ labelFallback: "Email",
21
+ hintKey: "notifications.preferences.channels.emailHint",
22
+ hintFallback: "Sent to your account email address."
23
+ },
24
+ {
25
+ key: "push",
26
+ labelKey: "notifications.preferences.channels.push",
27
+ labelFallback: "Push",
28
+ hintKey: "notifications.preferences.channels.pushHint",
29
+ hintFallback: "Mobile push (active once a push channel is connected)."
30
+ }
31
+ ];
32
+ function toChannelDef(item) {
33
+ return {
34
+ key: item.id,
35
+ labelKey: item.labelKey,
36
+ labelFallback: item.id,
37
+ hintKey: item.descriptionKey ?? "",
38
+ hintFallback: ""
39
+ };
40
+ }
41
+ function preferenceKey(typeId, channel) {
42
+ return `${typeId}::${channel}`;
43
+ }
44
+ function isChannelDisabledForType(type, channel) {
45
+ return Array.isArray(type.channels) && !type.channels.includes(channel);
46
+ }
47
+ function buildPreferenceMap(types, stored, channels = PREFERENCE_CHANNELS) {
48
+ const storedMap = new Map(stored.map((item) => [preferenceKey(item.notificationTypeId, item.channel), item.enabled]));
49
+ const next = {};
50
+ for (const type of types) {
51
+ for (const channel of channels) {
52
+ const key = preferenceKey(type.id, channel.key);
53
+ next[key] = isChannelDisabledForType(type, channel.key) ? false : storedMap.get(key) ?? true;
54
+ }
55
+ }
56
+ return next;
57
+ }
58
+ function diffPreferenceItems(types, initial, current, channels = PREFERENCE_CHANNELS) {
59
+ const items = [];
60
+ for (const type of types) {
61
+ for (const channel of channels) {
62
+ if (isChannelDisabledForType(type, channel.key)) continue;
63
+ const key = preferenceKey(type.id, channel.key);
64
+ const before = initial[key] ?? true;
65
+ const after = current[key] ?? true;
66
+ if (before !== after) {
67
+ items.push({ notificationTypeId: type.id, channel: channel.key, enabled: after });
68
+ }
69
+ }
70
+ }
71
+ return items;
72
+ }
73
+ function NotificationPreferenceMatrix({
74
+ types,
75
+ prefs,
76
+ onToggle,
77
+ disabled,
78
+ channels = PREFERENCE_CHANNELS
79
+ }) {
80
+ const t = useT();
81
+ const [filterValues, setFilterValues] = React.useState({});
82
+ const categoryOptions = React.useMemo(() => {
83
+ const byCategory = /* @__PURE__ */ new Map();
84
+ for (const type of types) {
85
+ if (!type.category) continue;
86
+ if (!byCategory.has(type.category)) byCategory.set(type.category, type.categoryLabel ?? type.category);
87
+ }
88
+ return Array.from(byCategory, ([value, label]) => ({ value, label })).sort(
89
+ (left, right) => left.label.localeCompare(right.label)
90
+ );
91
+ }, [types]);
92
+ const filters = React.useMemo(
93
+ () => categoryOptions.length > 1 ? [
94
+ {
95
+ id: "category",
96
+ label: t("notifications.preferences.filters.category", "Category"),
97
+ // `tags` (searchable chips), not a `select` checkbox list: the catalogue grows a
98
+ // category per module, so the list gets long enough that scanning checkboxes stops
99
+ // scaling. Mirrors the customer/tag filters in SalesDocumentsTable.
100
+ type: "tags",
101
+ options: categoryOptions,
102
+ placeholder: t("notifications.preferences.filters.categoryPlaceholder", "Search categories"),
103
+ formatValue: (value) => categoryOptions.find((option) => option.value === value)?.label ?? value
104
+ }
105
+ ] : [],
106
+ [categoryOptions, t]
107
+ );
108
+ const selectedCategories = React.useMemo(() => {
109
+ const raw = filterValues.category;
110
+ if (Array.isArray(raw)) return raw.filter((value) => typeof value === "string" && value !== "");
111
+ return typeof raw === "string" && raw !== "" ? [raw] : [];
112
+ }, [filterValues]);
113
+ const visibleTypes = React.useMemo(
114
+ () => selectedCategories.length === 0 ? types : types.filter((type) => type.category != null && selectedCategories.includes(type.category)),
115
+ [types, selectedCategories]
116
+ );
117
+ if (types.length === 0) {
118
+ return /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("notifications.preferences.empty", "No notification types are registered yet.") });
119
+ }
120
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
121
+ filters.length > 0 ? /* @__PURE__ */ jsx(
122
+ FilterBar,
123
+ {
124
+ filters,
125
+ values: filterValues,
126
+ onApply: setFilterValues,
127
+ onClear: () => setFilterValues({})
128
+ }
129
+ ) : null,
130
+ visibleTypes.length === 0 ? /* @__PURE__ */ jsx(
131
+ FilteredEmptyResults,
132
+ {
133
+ entityNamePlural: t("notifications.preferences.entityNamePlural", "notification types"),
134
+ canRemoveLast: selectedCategories.length > 0,
135
+ onClearAll: () => setFilterValues({}),
136
+ onRemoveLast: () => setFilterValues({})
137
+ }
138
+ ) : /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-lg border border-border", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
139
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b border-border bg-muted/50 text-left", children: [
140
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 font-medium", children: t("notifications.preferences.columns.type", "Notification type") }),
141
+ channels.map((channel) => /* @__PURE__ */ jsxs("th", { className: "px-4 py-3 font-medium", children: [
142
+ /* @__PURE__ */ jsx("div", { children: t(channel.labelKey, channel.labelFallback) }),
143
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-normal text-muted-foreground", children: t(channel.hintKey, channel.hintFallback) })
144
+ ] }, channel.key))
145
+ ] }) }),
146
+ /* @__PURE__ */ jsx("tbody", { children: visibleTypes.map((type) => /* @__PURE__ */ jsxs("tr", { className: "border-b border-border last:border-0", children: [
147
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-3", children: [
148
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: t(type.labelKey, type.id) }),
149
+ type.descriptionKey ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: t(type.descriptionKey, "") }) : null
150
+ ] }),
151
+ channels.map((channel) => {
152
+ const channelDisabled = isChannelDisabledForType(type, channel.key);
153
+ const locked = !channelDisabled && type.nonOptOut === true;
154
+ const cellLabel = `${t(type.labelKey, type.id)} \u2013 ${t(channel.labelKey, channel.labelFallback)}`;
155
+ const requiredHint = t(
156
+ "notifications.preferences.requiredHint",
157
+ "This notification is required and cannot be turned off."
158
+ );
159
+ const channelDisabledHint = t(
160
+ "notifications.preferences.channelDisabledHint",
161
+ "This channel is turned off for this notification by your administrator."
162
+ );
163
+ const lockHint = channelDisabled ? channelDisabledHint : requiredHint;
164
+ const switchEl = /* @__PURE__ */ jsx(
165
+ Switch,
166
+ {
167
+ checked: channelDisabled ? false : locked ? true : prefs[preferenceKey(type.id, channel.key)] ?? true,
168
+ disabled: disabled || locked || channelDisabled,
169
+ onCheckedChange: (checked) => onToggle(type.id, channel.key, checked),
170
+ "aria-label": locked || channelDisabled ? `${cellLabel} (${lockHint})` : cellLabel
171
+ }
172
+ );
173
+ return /* @__PURE__ */ jsx("td", { className: "px-4 py-3", children: locked || channelDisabled ? /* @__PURE__ */ jsx(SimpleTooltip, { content: lockHint, children: /* @__PURE__ */ jsx("span", { className: "inline-flex", children: switchEl }) }) : switchEl }, channel.key);
174
+ })
175
+ ] }, type.id)) })
176
+ ] }) })
177
+ ] });
178
+ }
179
+ var NotificationPreferenceMatrix_default = NotificationPreferenceMatrix;
180
+ export {
181
+ NotificationPreferenceMatrix,
182
+ PREFERENCE_CHANNELS,
183
+ buildPreferenceMap,
184
+ NotificationPreferenceMatrix_default as default,
185
+ diffPreferenceItems,
186
+ isChannelDisabledForType,
187
+ preferenceKey,
188
+ toChannelDef
189
+ };
190
+ //# sourceMappingURL=NotificationPreferenceMatrix.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/notifications/frontend/NotificationPreferenceMatrix.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { FilterBar, type FilterDef, type FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { FilteredEmptyResults } from '@open-mercato/ui/backend/filters/FilteredEmptyResults'\n\nexport type NotificationTypeItem = {\n id: string\n labelKey: string\n descriptionKey?: string | null\n // Stable grouping key from /api/notifications/types (the declared category, else the type-id\n // prefix). Filter on this \u2014 never on `categoryLabel`, which changes with the locale.\n category?: string | null\n // Server-resolved heading for `category`. Equals `category` when the owning module ships no\n // `notifications.categories.<key>` translation.\n categoryLabel?: string | null\n // When true the type cannot be opted out of; the matrix locks its cells ON (the server drops\n // opt-out writes for these, so a toggleable switch would silently lie on reload).\n nonOptOut?: boolean\n // Effective channel eligibility from /api/notifications/types (operator override, else the\n // code-declared set; null/absent = every channel). A channel outside the set renders locked\n // OFF and toggles are refused server-side too, so the lock is enforcement, not presentation.\n channels?: string[] | null\n}\nexport type PreferenceItem = { notificationTypeId: string; channel: string; enabled: boolean }\n\nexport type ChannelDef = { key: string; labelKey: string; labelFallback: string; hintKey: string; hintFallback: string }\n\n/**\n * Resilient fallback channel list used only while the `/api/notifications/channels` catalogue is\n * loading or when the fetch fails. The authoritative source is the module-registered channel\n * catalogue (see `notification-channels.ts`); callers pass the fetched channels into the helpers\n * and the component. Keep this list minimal \u2014 it is a safety net, not the source of truth.\n */\nexport const PREFERENCE_CHANNELS: ChannelDef[] = [\n {\n key: 'in_app',\n labelKey: 'notifications.preferences.channels.inApp',\n labelFallback: 'In-app',\n hintKey: 'notifications.preferences.channels.inAppHint',\n hintFallback: 'Notification center and bell.',\n },\n {\n key: 'email',\n labelKey: 'notifications.preferences.channels.email',\n labelFallback: 'Email',\n hintKey: 'notifications.preferences.channels.emailHint',\n hintFallback: 'Sent to your account email address.',\n },\n {\n key: 'push',\n labelKey: 'notifications.preferences.channels.push',\n labelFallback: 'Push',\n hintKey: 'notifications.preferences.channels.pushHint',\n hintFallback: 'Mobile push (active once a push channel is connected).',\n },\n]\n\n/** Map a `/api/notifications/channels` item to the UI channel shape. */\nexport function toChannelDef(item: { id: string; labelKey: string; descriptionKey?: string | null }): ChannelDef {\n return {\n key: item.id,\n labelKey: item.labelKey,\n labelFallback: item.id,\n hintKey: item.descriptionKey ?? '',\n hintFallback: '',\n }\n}\n\nexport function preferenceKey(typeId: string, channel: string): string {\n return `${typeId}::${channel}`\n}\n\n/** Whether the channel is outside the type's effective eligibility (cell locked off, tenant-wide). */\nexport function isChannelDisabledForType(type: NotificationTypeItem, channel: string): boolean {\n return Array.isArray(type.channels) && !type.channels.includes(channel)\n}\n\n/** Build the default-on preference map for a catalogue + stored rows; ineligible cells are forced off. */\nexport function buildPreferenceMap(\n types: NotificationTypeItem[],\n stored: PreferenceItem[],\n channels: ChannelDef[] = PREFERENCE_CHANNELS,\n): Record<string, boolean> {\n const storedMap = new Map(stored.map((item) => [preferenceKey(item.notificationTypeId, item.channel), item.enabled]))\n const next: Record<string, boolean> = {}\n for (const type of types) {\n for (const channel of channels) {\n const key = preferenceKey(type.id, channel.key)\n next[key] = isChannelDisabledForType(type, channel.key) ? false : storedMap.get(key) ?? true\n }\n }\n return next\n}\n\n/**\n * Return only the entries whose value differs from the originally-loaded state. The server upserts\n * exactly what it receives (last-write-wins), so sending the diff keeps the payload bounded by the\n * number of toggles \u2014 not the catalogue size \u2014 and avoids materializing redundant default-on rows.\n */\nexport function diffPreferenceItems(\n types: NotificationTypeItem[],\n initial: Record<string, boolean>,\n current: Record<string, boolean>,\n channels: ChannelDef[] = PREFERENCE_CHANNELS,\n): PreferenceItem[] {\n const items: PreferenceItem[] = []\n for (const type of types) {\n for (const channel of channels) {\n if (isChannelDisabledForType(type, channel.key)) continue\n const key = preferenceKey(type.id, channel.key)\n const before = initial[key] ?? true\n const after = current[key] ?? true\n if (before !== after) {\n items.push({ notificationTypeId: type.id, channel: channel.key, enabled: after })\n }\n }\n }\n return items\n}\n\nexport type NotificationPreferenceMatrixProps = {\n types: NotificationTypeItem[]\n prefs: Record<string, boolean>\n onToggle: (typeId: string, channel: string, enabled: boolean) => void\n disabled?: boolean\n channels?: ChannelDef[]\n}\n\nexport function NotificationPreferenceMatrix({\n types,\n prefs,\n onToggle,\n disabled,\n channels = PREFERENCE_CHANNELS,\n}: NotificationPreferenceMatrixProps) {\n const t = useT()\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n\n // Options come from the catalogue itself, so a module introducing a new category shows up\n // without touching this component. Sorted by the localized label, keyed by the stable slug.\n const categoryOptions = React.useMemo(() => {\n const byCategory = new Map<string, string>()\n for (const type of types) {\n if (!type.category) continue\n if (!byCategory.has(type.category)) byCategory.set(type.category, type.categoryLabel ?? type.category)\n }\n return Array.from(byCategory, ([value, label]) => ({ value, label })).sort((left, right) =>\n left.label.localeCompare(right.label),\n )\n }, [types])\n\n const filters = React.useMemo<FilterDef[]>(\n () =>\n categoryOptions.length > 1\n ? [\n {\n id: 'category',\n label: t('notifications.preferences.filters.category', 'Category'),\n // `tags` (searchable chips), not a `select` checkbox list: the catalogue grows a\n // category per module, so the list gets long enough that scanning checkboxes stops\n // scaling. Mirrors the customer/tag filters in SalesDocumentsTable.\n type: 'tags',\n options: categoryOptions,\n placeholder: t('notifications.preferences.filters.categoryPlaceholder', 'Search categories'),\n formatValue: (value: string) =>\n categoryOptions.find((option) => option.value === value)?.label ?? value,\n },\n ]\n : [],\n [categoryOptions, t],\n )\n\n const selectedCategories = React.useMemo(() => {\n const raw = filterValues.category\n if (Array.isArray(raw)) return raw.filter((value): value is string => typeof value === 'string' && value !== '')\n return typeof raw === 'string' && raw !== '' ? [raw] : []\n }, [filterValues])\n\n // Filtering happens at render only. The parent keeps the full `types` array, which\n // `diffPreferenceItems` walks on save \u2014 narrowing it upstream would silently drop pending\n // toggles for rows the filter hides.\n const visibleTypes = React.useMemo(\n () =>\n selectedCategories.length === 0\n ? types\n : types.filter((type) => type.category != null && selectedCategories.includes(type.category)),\n [types, selectedCategories],\n )\n\n if (types.length === 0) {\n return (\n <p className=\"text-sm text-muted-foreground\">\n {t('notifications.preferences.empty', 'No notification types are registered yet.')}\n </p>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-3\">\n {filters.length > 0 ? (\n <FilterBar\n filters={filters}\n values={filterValues}\n onApply={setFilterValues}\n onClear={() => setFilterValues({})}\n />\n ) : null}\n {visibleTypes.length === 0 ? (\n <FilteredEmptyResults\n entityNamePlural={t('notifications.preferences.entityNamePlural', 'notification types')}\n canRemoveLast={selectedCategories.length > 0}\n onClearAll={() => setFilterValues({})}\n onRemoveLast={() => setFilterValues({})}\n />\n ) : (\n <div className=\"overflow-x-auto rounded-lg border border-border\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border bg-muted/50 text-left\">\n <th className=\"px-4 py-3 font-medium\">{t('notifications.preferences.columns.type', 'Notification type')}</th>\n {channels.map((channel) => (\n <th key={channel.key} className=\"px-4 py-3 font-medium\">\n <div>{t(channel.labelKey, channel.labelFallback)}</div>\n <div className=\"text-xs font-normal text-muted-foreground\">{t(channel.hintKey, channel.hintFallback)}</div>\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {visibleTypes.map((type) => (\n <tr key={type.id} className=\"border-b border-border last:border-0\">\n <td className=\"px-4 py-3\">\n <div className=\"font-medium\">{t(type.labelKey, type.id)}</div>\n {type.descriptionKey ? (\n <div className=\"text-xs text-muted-foreground\">{t(type.descriptionKey, '')}</div>\n ) : null}\n </td>\n {channels.map((channel) => {\n // Operator hard-off wins over the nonOptOut forced-on lock \u2014 mirrors the gate,\n // where the tenant-wide channel block runs before the nonOptOut bypass.\n const channelDisabled = isChannelDisabledForType(type, channel.key)\n const locked = !channelDisabled && type.nonOptOut === true\n const cellLabel = `${t(type.labelKey, type.id)} \u2013 ${t(channel.labelKey, channel.labelFallback)}`\n const requiredHint = t(\n 'notifications.preferences.requiredHint',\n 'This notification is required and cannot be turned off.',\n )\n const channelDisabledHint = t(\n 'notifications.preferences.channelDisabledHint',\n 'This channel is turned off for this notification by your administrator.',\n )\n const lockHint = channelDisabled ? channelDisabledHint : requiredHint\n const switchEl = (\n <Switch\n checked={channelDisabled ? false : locked ? true : prefs[preferenceKey(type.id, channel.key)] ?? true}\n disabled={disabled || locked || channelDisabled}\n onCheckedChange={(checked) => onToggle(type.id, channel.key, checked)}\n aria-label={locked || channelDisabled ? `${cellLabel} (${lockHint})` : cellLabel}\n />\n )\n return (\n <td key={channel.key} className=\"px-4 py-3\">\n {locked || channelDisabled ? (\n <SimpleTooltip content={lockHint}>\n <span className=\"inline-flex\">{switchEl}</span>\n </SimpleTooltip>\n ) : (\n switchEl\n )}\n </td>\n )\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n )\n}\n\nexport default NotificationPreferenceMatrix\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { apiCall, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
6
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
7
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
8
+ import { Button } from "@open-mercato/ui/primitives/button";
9
+ import { LoadingMessage, ErrorMessage } from "@open-mercato/ui/backend/detail";
10
+ import {
11
+ NotificationPreferenceMatrix,
12
+ PREFERENCE_CHANNELS,
13
+ buildPreferenceMap,
14
+ diffPreferenceItems,
15
+ preferenceKey,
16
+ toChannelDef
17
+ } from "./NotificationPreferenceMatrix.js";
18
+ const PREFERENCES_CONTEXT_ID = "notifications-preferences";
19
+ function NotificationPreferencesPageClient() {
20
+ const t = useT();
21
+ const [types, setTypes] = React.useState(null);
22
+ const [channels, setChannels] = React.useState(PREFERENCE_CHANNELS);
23
+ const [prefs, setPrefs] = React.useState({});
24
+ const initialPrefs = React.useRef({});
25
+ const [loading, setLoading] = React.useState(true);
26
+ const [saving, setSaving] = React.useState(false);
27
+ const [error, setError] = React.useState(null);
28
+ const { runMutation, retryLastMutation } = useGuardedMutation({
29
+ contextId: PREFERENCES_CONTEXT_ID,
30
+ blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
31
+ });
32
+ const fetchData = React.useCallback(async () => {
33
+ setLoading(true);
34
+ setError(null);
35
+ try {
36
+ const [typesBody, prefsBody, channelsBody] = await Promise.all([
37
+ readApiResultOrThrow("/api/notifications/types", void 0, {
38
+ errorMessage: t("notifications.preferences.loadError", "Failed to load notification preferences"),
39
+ allowNullResult: true
40
+ }),
41
+ readApiResultOrThrow("/api/notifications/preferences", void 0, {
42
+ errorMessage: t("notifications.preferences.loadError", "Failed to load notification preferences"),
43
+ allowNullResult: true
44
+ }),
45
+ readApiResultOrThrow("/api/notifications/channels", void 0, {
46
+ errorMessage: t("notifications.preferences.loadError", "Failed to load notification preferences"),
47
+ allowNullResult: true
48
+ })
49
+ ]);
50
+ const typeItems = typesBody?.items ?? [];
51
+ setTypes(typeItems);
52
+ const channelDefs = channelsBody?.items?.length ? channelsBody.items.map(toChannelDef) : PREFERENCE_CHANNELS;
53
+ setChannels(channelDefs);
54
+ const map = buildPreferenceMap(typeItems, prefsBody?.items ?? [], channelDefs);
55
+ initialPrefs.current = map;
56
+ setPrefs(map);
57
+ } catch (err) {
58
+ const message = err instanceof Error ? err.message : t("notifications.preferences.loadError", "Failed to load notification preferences");
59
+ setError(message);
60
+ flash(message, "error");
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ }, [t]);
65
+ React.useEffect(() => {
66
+ fetchData();
67
+ }, [fetchData]);
68
+ const togglePref = (typeId, channel, enabled) => {
69
+ setPrefs((prev) => ({ ...prev, [preferenceKey(typeId, channel)]: enabled }));
70
+ };
71
+ const handleSave = async () => {
72
+ if (!types) return;
73
+ setSaving(true);
74
+ try {
75
+ const preferences = diffPreferenceItems(types, initialPrefs.current, prefs, channels);
76
+ if (preferences.length === 0) {
77
+ flash(t("notifications.preferences.saveSuccess", "Notification preferences saved"), "success");
78
+ return;
79
+ }
80
+ const response = await runMutation({
81
+ // optimistic-lock-exempt: the preference matrix is an idempotent per-cell toggle grid.
82
+ // The client sends only changed (type, channel) cells (diffPreferenceItems) and the server
83
+ // upserts them last-write-wins per cell, so there is no lost-update hazard on a shared
84
+ // aggregate that version-locking would guard.
85
+ operation: () => apiCall("/api/notifications/preferences", {
86
+ method: "PUT",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({ preferences })
89
+ }),
90
+ context: { formId: PREFERENCES_CONTEXT_ID, resourceKind: "notifications.preference", retryLastMutation },
91
+ mutationPayload: { preferences }
92
+ });
93
+ if (!response.ok) {
94
+ const message = response.result?.error || t("notifications.preferences.saveError", "Failed to save notification preferences");
95
+ throw new Error(message);
96
+ }
97
+ initialPrefs.current = prefs;
98
+ flash(t("notifications.preferences.saveSuccess", "Notification preferences saved"), "success");
99
+ } catch (err) {
100
+ const message = err instanceof Error ? err.message : t("notifications.preferences.saveError", "Failed to save notification preferences");
101
+ flash(message, "error");
102
+ } finally {
103
+ setSaving(false);
104
+ }
105
+ };
106
+ if (loading || !types) {
107
+ return /* @__PURE__ */ jsx(LoadingMessage, { label: t("notifications.preferences.loading", "Loading notification preferences...") });
108
+ }
109
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6", children: [
110
+ /* @__PURE__ */ jsxs("div", { children: [
111
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: t("notifications.preferences.pageTitle", "Notification Preferences") }),
112
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: t("notifications.preferences.pageDescription", "Choose which channels deliver each notification type. Unset choices stay enabled by default.") })
113
+ ] }),
114
+ /* @__PURE__ */ jsx(NotificationPreferenceMatrix, { types, prefs, onToggle: togglePref, channels }),
115
+ error && /* @__PURE__ */ jsx(ErrorMessage, { label: error }),
116
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleSave, disabled: saving, children: saving ? t("notifications.preferences.saving", "Saving...") : t("notifications.preferences.save", "Save preferences") }) })
117
+ ] });
118
+ }
119
+ var NotificationPreferencesPageClient_default = NotificationPreferencesPageClient;
120
+ export {
121
+ NotificationPreferencesPageClient,
122
+ NotificationPreferencesPageClient_default as default
123
+ };
124
+ //# sourceMappingURL=NotificationPreferencesPageClient.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/notifications/frontend/NotificationPreferencesPageClient.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport {\n NotificationPreferenceMatrix,\n PREFERENCE_CHANNELS,\n buildPreferenceMap,\n diffPreferenceItems,\n preferenceKey,\n toChannelDef,\n type ChannelDef,\n type NotificationTypeItem,\n type PreferenceItem,\n} from './NotificationPreferenceMatrix'\n\ntype TypesResponse = { items?: NotificationTypeItem[] }\ntype PreferencesResponse = { items?: PreferenceItem[] }\ntype ChannelsResponse = { items?: Array<{ id: string; labelKey: string; descriptionKey?: string | null }> }\ntype SaveResponse = { ok?: boolean; error?: string }\n\nconst PREFERENCES_CONTEXT_ID = 'notifications-preferences'\n\nexport function NotificationPreferencesPageClient() {\n const t = useT()\n const [types, setTypes] = React.useState<NotificationTypeItem[] | null>(null)\n const [channels, setChannels] = React.useState<ChannelDef[]>(PREFERENCE_CHANNELS)\n const [prefs, setPrefs] = React.useState<Record<string, boolean>>({})\n // The state as last loaded/saved; saves send only the entries that differ from this.\n const initialPrefs = React.useRef<Record<string, boolean>>({})\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: PREFERENCES_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const fetchData = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const [typesBody, prefsBody, channelsBody] = await Promise.all([\n readApiResultOrThrow<TypesResponse>('/api/notifications/types', undefined, {\n errorMessage: t('notifications.preferences.loadError', 'Failed to load notification preferences'),\n allowNullResult: true,\n }),\n readApiResultOrThrow<PreferencesResponse>('/api/notifications/preferences', undefined, {\n errorMessage: t('notifications.preferences.loadError', 'Failed to load notification preferences'),\n allowNullResult: true,\n }),\n readApiResultOrThrow<ChannelsResponse>('/api/notifications/channels', undefined, {\n errorMessage: t('notifications.preferences.loadError', 'Failed to load notification preferences'),\n allowNullResult: true,\n }),\n ])\n const typeItems = typesBody?.items ?? []\n setTypes(typeItems)\n const channelDefs = channelsBody?.items?.length ? channelsBody.items.map(toChannelDef) : PREFERENCE_CHANNELS\n setChannels(channelDefs)\n const map = buildPreferenceMap(typeItems, prefsBody?.items ?? [], channelDefs)\n initialPrefs.current = map\n setPrefs(map)\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.preferences.loadError', 'Failed to load notification preferences')\n setError(message)\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n fetchData()\n }, [fetchData])\n\n const togglePref = (typeId: string, channel: string, enabled: boolean) => {\n setPrefs((prev) => ({ ...prev, [preferenceKey(typeId, channel)]: enabled }))\n }\n\n const handleSave = async () => {\n if (!types) return\n setSaving(true)\n try {\n const preferences = diffPreferenceItems(types, initialPrefs.current, prefs, channels)\n // Nothing changed since load/last save \u2014 skip the no-op write entirely.\n if (preferences.length === 0) {\n flash(t('notifications.preferences.saveSuccess', 'Notification preferences saved'), 'success')\n return\n }\n const response = await runMutation({\n // optimistic-lock-exempt: the preference matrix is an idempotent per-cell toggle grid.\n // The client sends only changed (type, channel) cells (diffPreferenceItems) and the server\n // upserts them last-write-wins per cell, so there is no lost-update hazard on a shared\n // aggregate that version-locking would guard.\n operation: () =>\n apiCall<SaveResponse>('/api/notifications/preferences', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ preferences }),\n }),\n context: { formId: PREFERENCES_CONTEXT_ID, resourceKind: 'notifications.preference', retryLastMutation },\n mutationPayload: { preferences },\n })\n if (!response.ok) {\n const message = response.result?.error || t('notifications.preferences.saveError', 'Failed to save notification preferences')\n throw new Error(message)\n }\n initialPrefs.current = prefs\n flash(t('notifications.preferences.saveSuccess', 'Notification preferences saved'), 'success')\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.preferences.saveError', 'Failed to save notification preferences')\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }\n\n if (loading || !types) {\n return <LoadingMessage label={t('notifications.preferences.loading', 'Loading notification preferences...')} />\n }\n\n return (\n <div className=\"flex flex-col gap-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{t('notifications.preferences.pageTitle', 'Notification Preferences')}</h1>\n <p className=\"text-muted-foreground text-sm\">\n {t('notifications.preferences.pageDescription', 'Choose which channels deliver each notification type. Unset choices stay enabled by default.')}\n </p>\n </div>\n\n <NotificationPreferenceMatrix types={types} prefs={prefs} onToggle={togglePref} channels={channels} />\n\n {error && <ErrorMessage label={error} />}\n\n <div className=\"flex items-center gap-3\">\n <Button type=\"button\" onClick={handleSave} disabled={saving}>\n {saving ? t('notifications.preferences.saving', 'Saving...') : t('notifications.preferences.save', 'Save preferences')}\n </Button>\n </div>\n </div>\n )\n}\n\nexport default NotificationPreferencesPageClient\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -2,7 +2,9 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useT } from "@open-mercato/shared/lib/i18n/context";
5
- import { apiCall, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
6
+ import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
7
+ import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
6
8
  import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
7
9
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
8
10
  import { Button } from "@open-mercato/ui/primitives/button";
@@ -30,6 +32,116 @@ function NotificationSettingsPageClient() {
30
32
  contextId: SETTINGS_CONTEXT_ID,
31
33
  blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
32
34
  });
35
+ const [types, setTypes] = React.useState([]);
36
+ const [channels, setChannels] = React.useState([]);
37
+ const [savingTypeCell, setSavingTypeCell] = React.useState(null);
38
+ const fetchCatalogue = React.useCallback(async () => {
39
+ try {
40
+ const [typesBody, channelsBody] = await Promise.all([
41
+ readApiResultOrThrow("/api/notifications/types", void 0, {
42
+ errorMessage: t("notifications.settings.types.loadError", "Failed to load notification types"),
43
+ allowNullResult: true
44
+ }),
45
+ readApiResultOrThrow("/api/notifications/channels", void 0, {
46
+ errorMessage: t("notifications.settings.types.loadError", "Failed to load notification types"),
47
+ allowNullResult: true
48
+ })
49
+ ]);
50
+ setTypes(typesBody?.items ?? []);
51
+ setChannels((channelsBody?.items ?? []).map((item) => ({ id: item.id, labelKey: item.labelKey })));
52
+ } catch (err) {
53
+ const message = err instanceof Error ? err.message : t("notifications.settings.types.loadError", "Failed to load notification types");
54
+ flash(message, "error");
55
+ }
56
+ }, [t]);
57
+ React.useEffect(() => {
58
+ fetchCatalogue();
59
+ }, [fetchCatalogue]);
60
+ const saveTypeCell = async (type, cellKey, mutationPayload, operation, resolveSavedFlash) => {
61
+ setSavingTypeCell(cellKey);
62
+ try {
63
+ const response = await runMutation({
64
+ operation,
65
+ context: {
66
+ formId: SETTINGS_CONTEXT_ID,
67
+ resourceKind: "notifications.settings",
68
+ resourceId: type.id,
69
+ retryLastMutation
70
+ },
71
+ mutationPayload
72
+ });
73
+ if (!response.ok || !response.result?.ok) {
74
+ if (surfaceRecordConflict({ status: response.status, body: response.result }, t, { onRefresh: fetchCatalogue })) {
75
+ await fetchCatalogue();
76
+ return;
77
+ }
78
+ const message = response.result?.error || t("notifications.settings.types.saveError", "Failed to save notification type settings");
79
+ throw new Error(message);
80
+ }
81
+ const saved = response.result.item;
82
+ setTypes(
83
+ (prev) => prev.map(
84
+ (item) => item.id === type.id && saved ? {
85
+ ...item,
86
+ channels: saved.channels,
87
+ storedChannels: saved.storedChannels,
88
+ nonOptOut: saved.nonOptOut,
89
+ storedNonOptOut: saved.storedNonOptOut,
90
+ updatedAt: saved.updatedAt
91
+ } : item
92
+ )
93
+ );
94
+ const savedFlash = saved ? resolveSavedFlash?.(saved) ?? null : null;
95
+ flash(
96
+ savedFlash?.message ?? t("notifications.settings.types.saveSuccess", "Notification type settings saved"),
97
+ savedFlash?.kind ?? "success"
98
+ );
99
+ } catch (err) {
100
+ const message = err instanceof Error ? err.message : t("notifications.settings.types.saveError", "Failed to save notification type settings");
101
+ flash(message, "error");
102
+ } finally {
103
+ setSavingTypeCell(null);
104
+ }
105
+ };
106
+ const patchType = (type, cellKey, payload) => saveTypeCell(
107
+ type,
108
+ cellKey,
109
+ { id: type.id, ...payload },
110
+ () => withScopedApiRequestHeaders(
111
+ buildOptimisticLockHeader(type.updatedAt),
112
+ () => apiCall("/api/notifications/types", {
113
+ method: "PATCH",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({ id: type.id, ...payload })
116
+ })
117
+ )
118
+ );
119
+ const toggleTypeChannel = (type, cellKey, channel, enabled) => saveTypeCell(
120
+ type,
121
+ cellKey,
122
+ { id: type.id, channel, enabled },
123
+ () => apiCall(
124
+ `/api/notifications/types/${encodeURIComponent(type.id)}/channels/${encodeURIComponent(channel)}`,
125
+ { method: enabled ? "PUT" : "DELETE" }
126
+ ),
127
+ // Unchecking the last channel clears the override instead of storing an empty set (which would
128
+ // black-hole the type), so the code-declared default reapplies and the box the operator just
129
+ // unticked can come back ticked. The write succeeded, so say what happened rather than letting a
130
+ // green "saved" sit next to a checkbox that visibly ignored the click.
131
+ (saved) => !enabled && saved.storedChannels === null && (saved.channels === null || saved.channels.includes(channel)) ? {
132
+ message: t(
133
+ "notifications.settings.types.lastChannelRestored",
134
+ "A notification type must keep at least one channel, so the module default was restored."
135
+ ),
136
+ kind: "warning"
137
+ } : null
138
+ );
139
+ const handleTypeChannelToggle = async (type, channelId, enabled) => {
140
+ await toggleTypeChannel(type, `${type.id}::${channelId}`, channelId, enabled);
141
+ };
142
+ const handleTypeNonOptOutToggle = async (type, required) => {
143
+ await patchType(type, `${type.id}::nonOptOut`, { nonOptOut: required });
144
+ };
33
145
  const fetchSettings = React.useCallback(async () => {
34
146
  setLoading(true);
35
147
  setError(null);
@@ -216,6 +328,59 @@ function NotificationSettingsPageClient() {
216
328
  ] })
217
329
  ] })
218
330
  ] }),
331
+ /* @__PURE__ */ jsxs(Card, { children: [
332
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
333
+ /* @__PURE__ */ jsx(CardTitle, { children: t("notifications.settings.types.title", "Delivery channels per type") }),
334
+ /* @__PURE__ */ jsx(CardDescription, { children: t(
335
+ "notifications.settings.types.description",
336
+ "Delivery channels per notification type. Turning a channel off disables it completely for that type in your tenant \u2014 it never delivers and users cannot enable it in their preferences. Turning it on lets users opt in or out per type; required (non-opt-out) types always deliver on enabled channels. Your setting overrides the module default."
337
+ ) })
338
+ ] }),
339
+ /* @__PURE__ */ jsx(CardContent, { children: types.length === 0 || channels.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("notifications.settings.types.empty", "No notification types are registered yet.") }) : /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-lg border border-border", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
340
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b border-border bg-muted/50 text-left", children: [
341
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 font-medium", children: t("notifications.settings.types.typeColumn", "Notification type") }),
342
+ channels.map((channel) => /* @__PURE__ */ jsx("th", { className: "px-4 py-3 font-medium", children: t(channel.labelKey, channel.id) }, channel.id)),
343
+ /* @__PURE__ */ jsxs("th", { className: "px-4 py-3 font-medium", children: [
344
+ /* @__PURE__ */ jsx("div", { children: t("notifications.settings.types.requiredColumn", "Required") }),
345
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-normal text-muted-foreground", children: t("notifications.settings.types.requiredHint", "Users cannot opt out when on.") })
346
+ ] })
347
+ ] }) }),
348
+ /* @__PURE__ */ jsx("tbody", { children: types.map((type) => /* @__PURE__ */ jsxs("tr", { className: "border-b border-border last:border-0", children: [
349
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-3", children: [
350
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: t(type.labelKey, type.id) }),
351
+ type.descriptionKey ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: t(type.descriptionKey, "") }) : null
352
+ ] }),
353
+ channels.map((channel) => {
354
+ const cellKey = `${type.id}::${channel.id}`;
355
+ const channelEnabled = type.channels === null || type.channels.includes(channel.id);
356
+ return /* @__PURE__ */ jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("span", { className: "relative inline-flex items-center", children: [
357
+ /* @__PURE__ */ jsx(
358
+ Switch,
359
+ {
360
+ checked: channelEnabled,
361
+ disabled: savingTypeCell !== null,
362
+ "aria-label": `${t(type.labelKey, type.id)} \u2013 ${t(channel.labelKey, channel.id)}`,
363
+ onCheckedChange: (checked) => handleTypeChannelToggle(type, channel.id, checked)
364
+ }
365
+ ),
366
+ savingTypeCell === cellKey ? /* @__PURE__ */ jsx(Spinner, { size: "sm", className: "absolute left-full ml-2" }) : null
367
+ ] }) }, channel.id);
368
+ }),
369
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("span", { className: "relative inline-flex", children: [
370
+ /* @__PURE__ */ jsx(
371
+ Switch,
372
+ {
373
+ checked: type.nonOptOut === true,
374
+ disabled: savingTypeCell !== null,
375
+ "aria-label": `${t(type.labelKey, type.id)} \u2013 ${t("notifications.settings.types.requiredColumn", "Required")}`,
376
+ onCheckedChange: (checked) => handleTypeNonOptOutToggle(type, checked)
377
+ }
378
+ ),
379
+ savingTypeCell === `${type.id}::nonOptOut` ? /* @__PURE__ */ jsx(Spinner, { size: "sm", className: "absolute left-full ml-2" }) : null
380
+ ] }) })
381
+ ] }, type.id)) })
382
+ ] }) }) })
383
+ ] }),
219
384
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
220
385
  /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleSave, disabled: saving, children: saving ? t("notifications.settings.saving", "Saving...") : t("notifications.settings.save", "Save settings") }),
221
386
  error && /* @__PURE__ */ jsx("span", { className: "text-sm text-destructive", children: error })