@open-mercato/core 0.6.6-develop.6094.1.28b081ea16 → 0.6.6-develop.6126.1.0b9392fdae

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 (317) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/catalog_product/index.js +52 -0
  3. package/dist/generated/entities/catalog_product/index.js.map +2 -2
  4. package/dist/generated/entities/catalog_product_variant/index.js +4 -0
  5. package/dist/generated/entities/catalog_product_variant/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +28 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/api/products/route.js +103 -33
  9. package/dist/modules/catalog/api/products/route.js.map +2 -2
  10. package/dist/modules/catalog/api/variants/route.js +4 -0
  11. package/dist/modules/catalog/api/variants/route.js.map +2 -2
  12. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +46 -2
  13. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  14. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +4 -0
  15. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  16. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +2 -0
  17. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
  18. package/dist/modules/catalog/backend/catalog/products/create/page.js +42 -2
  19. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  20. package/dist/modules/catalog/commands/products.js +251 -14
  21. package/dist/modules/catalog/commands/products.js.map +2 -2
  22. package/dist/modules/catalog/commands/variants.js +72 -1
  23. package/dist/modules/catalog/commands/variants.js.map +2 -2
  24. package/dist/modules/catalog/components/products/ProductComplianceSection.js +500 -0
  25. package/dist/modules/catalog/components/products/ProductComplianceSection.js.map +7 -0
  26. package/dist/modules/catalog/components/products/VariantBuilder.js +39 -1
  27. package/dist/modules/catalog/components/products/VariantBuilder.js.map +2 -2
  28. package/dist/modules/catalog/components/products/productForm.js +164 -2
  29. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  30. package/dist/modules/catalog/components/products/variantForm.js +2 -0
  31. package/dist/modules/catalog/components/products/variantForm.js.map +2 -2
  32. package/dist/modules/catalog/data/entities.js +93 -0
  33. package/dist/modules/catalog/data/entities.js.map +2 -2
  34. package/dist/modules/catalog/data/types.js +28 -0
  35. package/dist/modules/catalog/data/types.js.map +2 -2
  36. package/dist/modules/catalog/data/validators.js +100 -5
  37. package/dist/modules/catalog/data/validators.js.map +2 -2
  38. package/dist/modules/catalog/lib/gtin.js +61 -0
  39. package/dist/modules/catalog/lib/gtin.js.map +7 -0
  40. package/dist/modules/catalog/migrations/Migration20260611090000.js +17 -0
  41. package/dist/modules/catalog/migrations/Migration20260611090000.js.map +7 -0
  42. package/dist/modules/catalog/translations.js +1 -1
  43. package/dist/modules/catalog/translations.js.map +2 -2
  44. package/dist/modules/configs/api/cache/route.js +60 -10
  45. package/dist/modules/configs/api/cache/route.js.map +3 -3
  46. package/dist/modules/configs/components/CachePanel.js +52 -29
  47. package/dist/modules/configs/components/CachePanel.js.map +2 -2
  48. package/dist/modules/currencies/api/currencies/options/route.js +18 -4
  49. package/dist/modules/currencies/api/currencies/options/route.js.map +2 -2
  50. package/dist/modules/currencies/services/rateFetchingService.js +35 -18
  51. package/dist/modules/currencies/services/rateFetchingService.js.map +2 -2
  52. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +16 -16
  53. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
  54. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +1 -1
  55. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  56. package/dist/modules/customers/backend/calendar/page.js +10 -0
  57. package/dist/modules/customers/backend/calendar/page.js.map +7 -0
  58. package/dist/modules/customers/backend/calendar/page.meta.js +25 -0
  59. package/dist/modules/customers/backend/calendar/page.meta.js.map +7 -0
  60. package/dist/modules/customers/cli.js +1 -0
  61. package/dist/modules/customers/cli.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/AgendaList.js +182 -0
  63. package/dist/modules/customers/components/calendar/AgendaList.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +331 -0
  65. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +7 -0
  66. package/dist/modules/customers/components/calendar/CalendarFooter.js +48 -0
  67. package/dist/modules/customers/components/calendar/CalendarFooter.js.map +7 -0
  68. package/dist/modules/customers/components/calendar/CalendarHeader.js +36 -0
  69. package/dist/modules/customers/components/calendar/CalendarHeader.js.map +7 -0
  70. package/dist/modules/customers/components/calendar/CalendarScreen.js +516 -0
  71. package/dist/modules/customers/components/calendar/CalendarScreen.js.map +7 -0
  72. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +163 -0
  73. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +7 -0
  74. package/dist/modules/customers/components/calendar/CalendarTabs.js +60 -0
  75. package/dist/modules/customers/components/calendar/CalendarTabs.js.map +7 -0
  76. package/dist/modules/customers/components/calendar/CalendarToolbar.js +262 -0
  77. package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +7 -0
  78. package/dist/modules/customers/components/calendar/EventBlock.js +149 -0
  79. package/dist/modules/customers/components/calendar/EventBlock.js.map +7 -0
  80. package/dist/modules/customers/components/calendar/EventPeekPopover.js +72 -0
  81. package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +7 -0
  82. package/dist/modules/customers/components/calendar/MonthGrid.js +214 -0
  83. package/dist/modules/customers/components/calendar/MonthGrid.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/ShortcutsDialog.js +44 -0
  85. package/dist/modules/customers/components/calendar/ShortcutsDialog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/TimeGrid.js +410 -0
  87. package/dist/modules/customers/components/calendar/TimeGrid.js.map +7 -0
  88. package/dist/modules/customers/components/calendar/UpcomingCards.js +175 -0
  89. package/dist/modules/customers/components/calendar/UpcomingCards.js.map +7 -0
  90. package/dist/modules/customers/components/calendar/editor/CategoryField.js +75 -0
  91. package/dist/modules/customers/components/calendar/editor/CategoryField.js.map +7 -0
  92. package/dist/modules/customers/components/calendar/editor/LocationField.js +30 -0
  93. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +7 -0
  94. package/dist/modules/customers/components/calendar/editor/PeopleField.js +135 -0
  95. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +7 -0
  96. package/dist/modules/customers/components/calendar/editor/RelatedToField.js +218 -0
  97. package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +7 -0
  98. package/dist/modules/customers/components/calendar/editor/RepeatField.js +119 -0
  99. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +7 -0
  100. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +83 -0
  101. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +7 -0
  102. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +44 -0
  103. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +7 -0
  104. package/dist/modules/customers/components/calendar/editor/hooks.js +132 -0
  105. package/dist/modules/customers/components/calendar/editor/hooks.js.map +7 -0
  106. package/dist/modules/customers/components/calendar/editor/inputs.js +160 -0
  107. package/dist/modules/customers/components/calendar/editor/inputs.js.map +7 -0
  108. package/dist/modules/customers/components/calendar/editor/lookups.js +107 -0
  109. package/dist/modules/customers/components/calendar/editor/lookups.js.map +7 -0
  110. package/dist/modules/customers/components/calendar/types.js +30 -0
  111. package/dist/modules/customers/components/calendar/types.js.map +7 -0
  112. package/dist/modules/customers/components/calendar/useCalendarItems.js +131 -0
  113. package/dist/modules/customers/components/calendar/useCalendarItems.js.map +7 -0
  114. package/dist/modules/customers/components/calendar/useCalendarPreferences.js +41 -0
  115. package/dist/modules/customers/components/calendar/useCalendarPreferences.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/categories.js +28 -0
  117. package/dist/modules/customers/lib/calendar/categories.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/conflicts.js +72 -0
  119. package/dist/modules/customers/lib/calendar/conflicts.js.map +7 -0
  120. package/dist/modules/customers/lib/calendar/editorPayload.js +271 -0
  121. package/dist/modules/customers/lib/calendar/editorPayload.js.map +7 -0
  122. package/dist/modules/customers/lib/calendar/grid.js +48 -0
  123. package/dist/modules/customers/lib/calendar/grid.js.map +7 -0
  124. package/dist/modules/customers/lib/calendar/layout.js +39 -0
  125. package/dist/modules/customers/lib/calendar/layout.js.map +7 -0
  126. package/dist/modules/customers/lib/calendar/mapItem.js +79 -0
  127. package/dist/modules/customers/lib/calendar/mapItem.js.map +7 -0
  128. package/dist/modules/customers/lib/calendar/preferences.js +71 -0
  129. package/dist/modules/customers/lib/calendar/preferences.js.map +7 -0
  130. package/dist/modules/customers/lib/calendar/range.js +46 -0
  131. package/dist/modules/customers/lib/calendar/range.js.map +7 -0
  132. package/dist/modules/customers/lib/calendar/recurrence.js +113 -0
  133. package/dist/modules/customers/lib/calendar/recurrence.js.map +7 -0
  134. package/dist/modules/dashboards/services/widgetDataService.js +5 -5
  135. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  136. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +61 -1
  137. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  138. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +33 -1
  139. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
  140. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +61 -1
  141. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  142. package/dist/modules/dictionaries/api/route.js +33 -1
  143. package/dist/modules/dictionaries/api/route.js.map +2 -2
  144. package/dist/modules/feature_toggles/lib/queries.js +36 -52
  145. package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
  146. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js +32 -1
  147. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js.map +2 -2
  148. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js +32 -1
  149. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js.map +2 -2
  150. package/dist/modules/perspectives/api/[tableId]/route.js +73 -2
  151. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  152. package/dist/modules/perspectives/services/perspectiveService.js +5 -3
  153. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  154. package/dist/modules/planner/api/access.js +9 -0
  155. package/dist/modules/planner/api/access.js.map +2 -2
  156. package/dist/modules/planner/api/availability-date-specific.js +22 -1
  157. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  158. package/dist/modules/planner/api/availability-weekly.js +22 -1
  159. package/dist/modules/planner/api/availability-weekly.js.map +2 -2
  160. package/dist/modules/resources/api/resources/tags/assign/route.js +39 -0
  161. package/dist/modules/resources/api/resources/tags/assign/route.js.map +2 -2
  162. package/dist/modules/resources/api/resources/tags/unassign/route.js +39 -0
  163. package/dist/modules/resources/api/resources/tags/unassign/route.js.map +2 -2
  164. package/dist/modules/sales/api/settings/document-numbers/route.js +33 -1
  165. package/dist/modules/sales/api/settings/document-numbers/route.js.map +2 -2
  166. package/dist/modules/sales/api/settings/order-editing/route.js +35 -3
  167. package/dist/modules/sales/api/settings/order-editing/route.js.map +2 -2
  168. package/dist/modules/staff/api/leave-requests/accept/route.js +41 -0
  169. package/dist/modules/staff/api/leave-requests/accept/route.js.map +2 -2
  170. package/dist/modules/staff/api/leave-requests/reject/route.js +41 -0
  171. package/dist/modules/staff/api/leave-requests/reject/route.js.map +2 -2
  172. package/dist/modules/staff/api/team-members/self/route.js +40 -0
  173. package/dist/modules/staff/api/team-members/self/route.js.map +2 -2
  174. package/dist/modules/staff/api/team-members/tags/assign/route.js +41 -0
  175. package/dist/modules/staff/api/team-members/tags/assign/route.js.map +2 -2
  176. package/dist/modules/staff/api/team-members/tags/unassign/route.js +41 -0
  177. package/dist/modules/staff/api/team-members/tags/unassign/route.js.map +2 -2
  178. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +61 -1
  179. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  180. package/dist/modules/translations/api/context.js +7 -0
  181. package/dist/modules/translations/api/context.js.map +2 -2
  182. package/dist/modules/translations/api/put/locales.js +33 -1
  183. package/dist/modules/translations/api/put/locales.js.map +2 -2
  184. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +6 -7
  185. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  186. package/dist/modules/workflows/backend/instances/[id]/page.js.map +1 -1
  187. package/dist/modules/workflows/components/EdgeEditDialog.js.map +1 -1
  188. package/dist/modules/workflows/components/EdgeEditDialogCrudForm.js.map +1 -1
  189. package/dist/modules/workflows/components/NodeEditDialog.js.map +1 -1
  190. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +1 -1
  191. package/dist/modules/workflows/components/WorkflowGraph.js.map +1 -1
  192. package/dist/modules/workflows/components/WorkflowGraphImpl.js +17 -9
  193. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  194. package/dist/modules/workflows/components/mobile/MobileVisualEditor.js.map +1 -1
  195. package/dist/modules/workflows/frontend/checkout-demo/deriveCurrentStep.js +8 -0
  196. package/dist/modules/workflows/frontend/checkout-demo/deriveCurrentStep.js.map +7 -0
  197. package/dist/modules/workflows/frontend/checkout-demo/page.js +2 -20
  198. package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
  199. package/dist/modules/workflows/lib/graph-utils.js +8 -0
  200. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  201. package/generated/entities/catalog_product/index.ts +26 -0
  202. package/generated/entities/catalog_product_variant/index.ts +2 -0
  203. package/generated/entity-fields-registry.ts +28 -0
  204. package/package.json +7 -7
  205. package/src/modules/catalog/api/products/route.ts +123 -39
  206. package/src/modules/catalog/api/variants/route.ts +4 -0
  207. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +43 -0
  208. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +14 -0
  209. package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +2 -0
  210. package/src/modules/catalog/backend/catalog/products/create/page.tsx +41 -0
  211. package/src/modules/catalog/commands/products.ts +307 -2
  212. package/src/modules/catalog/commands/variants.ts +90 -1
  213. package/src/modules/catalog/components/products/ProductComplianceSection.tsx +497 -0
  214. package/src/modules/catalog/components/products/VariantBuilder.tsx +43 -0
  215. package/src/modules/catalog/components/products/productForm.ts +262 -0
  216. package/src/modules/catalog/components/products/variantForm.ts +4 -0
  217. package/src/modules/catalog/data/entities.ts +97 -0
  218. package/src/modules/catalog/data/types.ts +36 -0
  219. package/src/modules/catalog/data/validators.ts +159 -3
  220. package/src/modules/catalog/i18n/de.json +64 -0
  221. package/src/modules/catalog/i18n/en.json +64 -0
  222. package/src/modules/catalog/i18n/es.json +64 -0
  223. package/src/modules/catalog/i18n/pl.json +64 -0
  224. package/src/modules/catalog/lib/gtin.ts +66 -0
  225. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +457 -0
  226. package/src/modules/catalog/migrations/Migration20260611090000.ts +17 -0
  227. package/src/modules/catalog/translations.ts +1 -1
  228. package/src/modules/configs/api/cache/route.ts +72 -10
  229. package/src/modules/configs/components/CachePanel.tsx +59 -29
  230. package/src/modules/currencies/api/currencies/options/route.ts +22 -4
  231. package/src/modules/currencies/services/rateFetchingService.ts +63 -26
  232. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +18 -18
  233. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +1 -1
  234. package/src/modules/customer_accounts/i18n/de.json +1 -1
  235. package/src/modules/customer_accounts/i18n/en.json +1 -1
  236. package/src/modules/customer_accounts/i18n/es.json +1 -1
  237. package/src/modules/customer_accounts/i18n/pl.json +1 -1
  238. package/src/modules/customers/backend/calendar/page.meta.ts +23 -0
  239. package/src/modules/customers/backend/calendar/page.tsx +12 -0
  240. package/src/modules/customers/cli.ts +1 -0
  241. package/src/modules/customers/components/calendar/AgendaList.tsx +219 -0
  242. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +386 -0
  243. package/src/modules/customers/components/calendar/CalendarFooter.tsx +61 -0
  244. package/src/modules/customers/components/calendar/CalendarHeader.tsx +40 -0
  245. package/src/modules/customers/components/calendar/CalendarScreen.tsx +590 -0
  246. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +207 -0
  247. package/src/modules/customers/components/calendar/CalendarTabs.tsx +63 -0
  248. package/src/modules/customers/components/calendar/CalendarToolbar.tsx +295 -0
  249. package/src/modules/customers/components/calendar/EventBlock.tsx +205 -0
  250. package/src/modules/customers/components/calendar/EventPeekPopover.tsx +100 -0
  251. package/src/modules/customers/components/calendar/MonthGrid.tsx +236 -0
  252. package/src/modules/customers/components/calendar/ShortcutsDialog.tsx +57 -0
  253. package/src/modules/customers/components/calendar/TimeGrid.tsx +510 -0
  254. package/src/modules/customers/components/calendar/UpcomingCards.tsx +220 -0
  255. package/src/modules/customers/components/calendar/editor/CategoryField.tsx +79 -0
  256. package/src/modules/customers/components/calendar/editor/LocationField.tsx +37 -0
  257. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +148 -0
  258. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +242 -0
  259. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +132 -0
  260. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +110 -0
  261. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +50 -0
  262. package/src/modules/customers/components/calendar/editor/hooks.ts +162 -0
  263. package/src/modules/customers/components/calendar/editor/inputs.tsx +158 -0
  264. package/src/modules/customers/components/calendar/editor/lookups.ts +160 -0
  265. package/src/modules/customers/components/calendar/types.ts +163 -0
  266. package/src/modules/customers/components/calendar/useCalendarItems.ts +162 -0
  267. package/src/modules/customers/components/calendar/useCalendarPreferences.ts +52 -0
  268. package/src/modules/customers/i18n/de.json +195 -0
  269. package/src/modules/customers/i18n/en.json +195 -0
  270. package/src/modules/customers/i18n/es.json +195 -0
  271. package/src/modules/customers/i18n/pl.json +195 -0
  272. package/src/modules/customers/lib/calendar/categories.ts +26 -0
  273. package/src/modules/customers/lib/calendar/conflicts.ts +117 -0
  274. package/src/modules/customers/lib/calendar/editorPayload.ts +388 -0
  275. package/src/modules/customers/lib/calendar/grid.ts +50 -0
  276. package/src/modules/customers/lib/calendar/layout.ts +47 -0
  277. package/src/modules/customers/lib/calendar/mapItem.ts +94 -0
  278. package/src/modules/customers/lib/calendar/preferences.ts +95 -0
  279. package/src/modules/customers/lib/calendar/range.ts +45 -0
  280. package/src/modules/customers/lib/calendar/recurrence.ts +137 -0
  281. package/src/modules/dashboards/services/widgetDataService.ts +8 -6
  282. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +61 -1
  283. package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +33 -1
  284. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +65 -1
  285. package/src/modules/dictionaries/api/route.ts +36 -1
  286. package/src/modules/feature_toggles/lib/queries.ts +41 -60
  287. package/src/modules/perspectives/api/[tableId]/[perspectiveId]/route.ts +34 -1
  288. package/src/modules/perspectives/api/[tableId]/roles/[roleId]/route.ts +34 -1
  289. package/src/modules/perspectives/api/[tableId]/route.ts +107 -32
  290. package/src/modules/perspectives/services/perspectiveService.ts +9 -5
  291. package/src/modules/planner/api/access.ts +9 -0
  292. package/src/modules/planner/api/availability-date-specific.ts +22 -1
  293. package/src/modules/planner/api/availability-weekly.ts +22 -1
  294. package/src/modules/resources/api/resources/tags/assign/route.ts +42 -0
  295. package/src/modules/resources/api/resources/tags/unassign/route.ts +42 -0
  296. package/src/modules/sales/api/settings/document-numbers/route.ts +35 -1
  297. package/src/modules/sales/api/settings/order-editing/route.ts +37 -3
  298. package/src/modules/staff/api/leave-requests/accept/route.ts +45 -0
  299. package/src/modules/staff/api/leave-requests/reject/route.ts +45 -0
  300. package/src/modules/staff/api/team-members/self/route.ts +43 -0
  301. package/src/modules/staff/api/team-members/tags/assign/route.ts +45 -0
  302. package/src/modules/staff/api/team-members/tags/unassign/route.ts +45 -0
  303. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +65 -1
  304. package/src/modules/translations/api/context.ts +7 -0
  305. package/src/modules/translations/api/put/locales.ts +35 -1
  306. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +11 -9
  307. package/src/modules/workflows/backend/instances/[id]/page.tsx +1 -1
  308. package/src/modules/workflows/components/EdgeEditDialog.tsx +1 -1
  309. package/src/modules/workflows/components/EdgeEditDialogCrudForm.tsx +1 -1
  310. package/src/modules/workflows/components/NodeEditDialog.tsx +1 -1
  311. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +1 -1
  312. package/src/modules/workflows/components/WorkflowGraph.tsx +2 -2
  313. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +27 -13
  314. package/src/modules/workflows/components/mobile/MobileVisualEditor.tsx +3 -3
  315. package/src/modules/workflows/frontend/checkout-demo/deriveCurrentStep.ts +12 -0
  316. package/src/modules/workflows/frontend/checkout-demo/page.tsx +3 -23
  317. package/src/modules/workflows/lib/graph-utils.ts +21 -1
@@ -0,0 +1,219 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { addDays } from 'date-fns/addDays'
5
+ import { format } from 'date-fns/format'
6
+ import { isToday } from 'date-fns/isToday'
7
+ import { isTomorrow } from 'date-fns/isTomorrow'
8
+ import { startOfDay } from 'date-fns/startOfDay'
9
+ import { CalendarClock } from 'lucide-react'
10
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
11
+ import { cn } from '@open-mercato/shared/lib/utils'
12
+ import { Avatar } from '@open-mercato/ui/primitives/avatar'
13
+ import { Button } from '@open-mercato/ui/primitives/button'
14
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
15
+ import type { AgendaListProps, CalendarCategory, CalendarItem } from './types'
16
+
17
+ const MAX_AVATARS_PER_ROW = 2
18
+
19
+ const CATEGORY_BADGE_CLASS: Record<CalendarCategory, string> = {
20
+ meeting: 'bg-status-info-bg text-status-info-text',
21
+ event: 'bg-status-warning-bg text-status-warning-text',
22
+ task: 'bg-primary text-primary-foreground',
23
+ other: 'bg-status-neutral-bg text-status-neutral-text',
24
+ }
25
+
26
+ const CATEGORY_LABEL: Record<CalendarCategory, { key: string; fallback: string }> = {
27
+ meeting: { key: 'customers.calendar.category.meeting', fallback: 'Meeting' },
28
+ event: { key: 'customers.calendar.category.event', fallback: 'Event' },
29
+ task: { key: 'customers.calendar.category.task', fallback: 'Task' },
30
+ other: { key: 'customers.calendar.category.other', fallback: 'Other' },
31
+ }
32
+
33
+ type AgendaDayGroup = { date: Date; items: CalendarItem[] }
34
+
35
+ function dayKeyOf(date: Date): string {
36
+ return format(date, 'yyyy-MM-dd')
37
+ }
38
+
39
+ function formatUrlHost(location: string): string {
40
+ try {
41
+ const url = new URL(location.startsWith('http') ? location : `https://${location}`)
42
+ return url.hostname
43
+ } catch {
44
+ return location
45
+ }
46
+ }
47
+
48
+ function formatTime(date: Date): string {
49
+ return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
50
+ }
51
+
52
+ function groupLabelOf(date: Date): string {
53
+ return date.toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric' })
54
+ }
55
+
56
+ function deriveTypeLabel(interactionType: string): string {
57
+ const normalized = interactionType.replace(/[-_]+/g, ' ').trim()
58
+ if (!normalized) return interactionType
59
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1)
60
+ }
61
+
62
+ function participantLabel(participant: CalendarItem['participants'][number]): string {
63
+ return participant.name ?? participant.email ?? participant.userId
64
+ }
65
+
66
+ function buildDayGroups(anchor: Date, horizonDays: number, items: CalendarItem[]): AgendaDayGroup[] {
67
+ const sorted = [...items].sort((first, second) => first.start.getTime() - second.start.getTime())
68
+ const byDay = new Map<string, CalendarItem[]>()
69
+ for (const item of sorted) {
70
+ const key = dayKeyOf(item.start)
71
+ const bucket = byDay.get(key)
72
+ if (bucket) bucket.push(item)
73
+ else byDay.set(key, [item])
74
+ }
75
+ const firstDay = startOfDay(anchor)
76
+ const groups: AgendaDayGroup[] = []
77
+ for (let offset = 0; offset <= horizonDays; offset += 1) {
78
+ const day = addDays(firstDay, offset)
79
+ const dayItems = byDay.get(dayKeyOf(day))
80
+ if (dayItems && dayItems.length > 0) groups.push({ date: day, items: dayItems })
81
+ }
82
+ return groups
83
+ }
84
+
85
+ function AgendaDayHeader({ date, count }: { date: Date; count: number }) {
86
+ const t = useT()
87
+ const todayMarker = isToday(date)
88
+ const tomorrowMarker = !todayMarker && isTomorrow(date)
89
+ return (
90
+ <div className="flex w-full items-center gap-2 bg-muted/50 px-3 py-2.5 sm:px-5">
91
+ <span className="text-sm font-semibold text-foreground">{groupLabelOf(date)}</span>
92
+ {todayMarker || tomorrowMarker ? (
93
+ <span className={cn('text-xs font-medium', todayMarker ? 'text-foreground' : 'text-muted-foreground')}>
94
+ {`· ${todayMarker ? t('customers.calendar.agenda.today', 'Today') : t('customers.calendar.agenda.tomorrow', 'Tomorrow')}`}
95
+ </span>
96
+ ) : null}
97
+ <span aria-hidden="true" className="w-2.5 shrink-0" />
98
+ <span className="text-xs font-medium text-muted-foreground">
99
+ {count === 1
100
+ ? t('customers.calendar.agenda.eventCount', '1 event')
101
+ : t('customers.calendar.agenda.eventsCount', '{count} events', { count })}
102
+ </span>
103
+ </div>
104
+ )
105
+ }
106
+
107
+ function AgendaRow({
108
+ item,
109
+ typeLabels,
110
+ onItemClick,
111
+ }: {
112
+ item: CalendarItem
113
+ typeLabels?: Record<string, string>
114
+ onItemClick: (item: CalendarItem) => void
115
+ }) {
116
+ const t = useT()
117
+ const canceled = item.status === 'canceled'
118
+ const done = item.status === 'done'
119
+ const typeLabel = typeLabels?.[item.interactionType] ?? deriveTypeLabel(item.interactionType)
120
+ const platformLabels: Record<string, string> = {
121
+ zoom: t('customers.calendar.platformFull.zoom', 'Zoom'),
122
+ meet: t('customers.calendar.platformFull.meet', 'Google Meet'),
123
+ slack: t('customers.calendar.platformFull.slack', 'Slack'),
124
+ teams: t('customers.calendar.platformFull.teams', 'Microsoft Teams'),
125
+ }
126
+ const locationLabel = item.platform
127
+ ? platformLabels[item.platform]
128
+ : item.locationKind === 'url' && item.location
129
+ ? formatUrlHost(item.location)
130
+ : item.location
131
+ const startLabel = item.allDay ? t('customers.calendar.grid.allDay', 'All day') : formatTime(item.start)
132
+ const endLabel = item.allDay ? null : formatTime(item.end)
133
+ const ariaTime = item.allDay ? startLabel : `${startLabel} – ${endLabel}`
134
+ return (
135
+ <Button
136
+ type="button"
137
+ variant="ghost"
138
+ aria-label={`${item.title} · ${ariaTime}`}
139
+ onClick={() => onItemClick(item)}
140
+ className={cn(
141
+ 'h-auto w-full justify-start whitespace-normal rounded-none bg-background px-3 py-3 text-left transition-colors hover:bg-muted/30 sm:gap-3.5 sm:px-5',
142
+ canceled && 'opacity-60',
143
+ )}
144
+ >
145
+ <div className="flex w-16 shrink-0 flex-col gap-0.5 sm:w-[86px]">
146
+ <span className={cn('truncate text-sm font-semibold leading-4', done || canceled ? 'text-muted-foreground' : 'text-foreground')}>
147
+ {startLabel}
148
+ </span>
149
+ {endLabel ? <span className="truncate text-xs text-muted-foreground">{endLabel}</span> : null}
150
+ </div>
151
+ <span
152
+ aria-hidden="true"
153
+ className={cn('h-9 w-1 shrink-0 rounded-full', !item.color && 'bg-muted-foreground/40')}
154
+ style={item.color ? { backgroundColor: item.color } : undefined}
155
+ />
156
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
157
+ <span
158
+ className={cn(
159
+ 'truncate text-sm font-semibold',
160
+ done || canceled ? 'text-muted-foreground' : 'text-foreground',
161
+ canceled && 'line-through',
162
+ )}
163
+ >
164
+ {item.title}
165
+ </span>
166
+ <span className="truncate text-xs text-muted-foreground">
167
+ {locationLabel ? `${typeLabel} · ${locationLabel}` : typeLabel}
168
+ </span>
169
+ </div>
170
+ <div className="flex shrink-0 items-center gap-2">
171
+ {item.participants.length > 0 ? (
172
+ <span className="hidden items-center gap-0.5 sm:flex">
173
+ {item.participants.slice(0, MAX_AVATARS_PER_ROW).map((participant) => (
174
+ <Avatar key={participant.userId} size="xs" label={participantLabel(participant)} />
175
+ ))}
176
+ {item.participants.length > MAX_AVATARS_PER_ROW ? (
177
+ <span className="ps-0.5 text-xs font-medium text-muted-foreground">
178
+ +{item.participants.length - MAX_AVATARS_PER_ROW}
179
+ </span>
180
+ ) : null}
181
+ </span>
182
+ ) : null}
183
+ <span
184
+ className={cn(
185
+ 'inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-overline font-medium uppercase tracking-wide',
186
+ CATEGORY_BADGE_CLASS[item.category],
187
+ )}
188
+ >
189
+ {t(CATEGORY_LABEL[item.category].key, CATEGORY_LABEL[item.category].fallback)}
190
+ </span>
191
+ </div>
192
+ </Button>
193
+ )
194
+ }
195
+
196
+ export function AgendaList({ anchor, horizonDays, items, typeLabels, onItemClick }: AgendaListProps) {
197
+ const t = useT()
198
+ const groups = React.useMemo(() => buildDayGroups(anchor, horizonDays, items), [anchor, horizonDays, items])
199
+ if (groups.length === 0) {
200
+ return (
201
+ <EmptyState
202
+ icon={<CalendarClock className="h-6 w-6" aria-hidden="true" />}
203
+ title={t('customers.calendar.empty.agenda', 'Nothing scheduled in this period')}
204
+ />
205
+ )
206
+ }
207
+ return (
208
+ <div className="flex w-full flex-col divide-y divide-border overflow-hidden border border-border bg-background">
209
+ {groups.map((group) => (
210
+ <React.Fragment key={dayKeyOf(group.date)}>
211
+ <AgendaDayHeader date={group.date} count={group.items.length} />
212
+ {group.items.map((item) => (
213
+ <AgendaRow key={item.id} item={item} typeLabels={typeLabels} onItemClick={onItemClick} />
214
+ ))}
215
+ </React.Fragment>
216
+ ))}
217
+ </div>
218
+ )
219
+ }
@@ -0,0 +1,386 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Calendar, X } from 'lucide-react'
5
+ import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
6
+ import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
8
+ import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
9
+ import { mapCrudServerErrorToFormErrors } from '@open-mercato/ui/backend/utils/serverErrors'
10
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
11
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
12
+ import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
13
+ import { Button } from '@open-mercato/ui/primitives/button'
14
+ import { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'
15
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
16
+ import { Input } from '@open-mercato/ui/primitives/input'
17
+ import { Textarea } from '@open-mercato/ui/primitives/textarea'
18
+ import { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'
19
+ import {
20
+ buildEditorCategoryOptions,
21
+ buildInteractionPayload,
22
+ computeDurationMinutes,
23
+ createDefaultFormState,
24
+ defaultRepeatDaysForDateInput,
25
+ EDITOR_KINDS,
26
+ KIND_CONFIG,
27
+ parseItemToFormState,
28
+ resolveSavedOwnerUserId,
29
+ type EditorFormState,
30
+ type EditorKind,
31
+ type EditorPriority,
32
+ } from '../../lib/calendar/editorPayload'
33
+ import type { ConflictScope } from '../../lib/calendar/preferences'
34
+ import type { CalendarItem } from './types'
35
+ import { Field } from './editor/inputs'
36
+ import { SegmentGroup } from './editor/SegmentGroup'
37
+ import { RelatedToField } from './editor/RelatedToField'
38
+ import { CategoryField } from './editor/CategoryField'
39
+ import { RepeatField } from './editor/RepeatField'
40
+ import { PeopleField } from './editor/PeopleField'
41
+ import { ScheduleSection } from './editor/ScheduleSection'
42
+ import { LocationField } from './editor/LocationField'
43
+ import { useConflictProbe, useEditorLabelResolution } from './editor/hooks'
44
+
45
+ export interface CalendarEventEditorProps {
46
+ open: boolean
47
+ mode: 'create' | 'edit'
48
+ item?: CalendarItem | null
49
+ defaultDate?: Date
50
+ defaultRange?: { start: Date; end: Date } | null
51
+ typeLabels: Record<string, string>
52
+ typeColors: Record<string, string | null>
53
+ surfacedTypes?: string[]
54
+ eventCategories?: string[]
55
+ conflictScope?: ConflictScope
56
+ currentUserId?: string | null
57
+ onOpenChange(open: boolean): void
58
+ onSaved(): void
59
+ }
60
+
61
+ type FieldErrors = { title?: string; relatedTo?: string; ends?: string }
62
+
63
+ const PEOPLE_FIELD_TEXT = {
64
+ attendees: { labelKey: 'customers.calendar.editor.attendees', label: 'Attendees', placeholderKey: 'customers.calendar.editor.addPeoplePlaceholder', placeholder: 'Add staff or customer…' },
65
+ participants: { labelKey: 'customers.calendar.editor.participants', label: 'Participants', placeholderKey: 'customers.calendar.editor.addPeoplePlaceholder', placeholder: 'Add staff or customer…' },
66
+ to: { labelKey: 'customers.calendar.editor.to', label: 'To', placeholderKey: 'customers.calendar.editor.addRecipientPlaceholder', placeholder: 'Add recipient…' },
67
+ } as const
68
+
69
+ export function CalendarEventEditor({ open, mode, item, defaultDate, defaultRange, typeLabels, typeColors, surfacedTypes, eventCategories, conflictScope, currentUserId, onOpenChange, onSaved }: CalendarEventEditorProps) {
70
+ const t = useT()
71
+ const locale = useLocale()
72
+ const [form, setForm] = React.useState<EditorFormState>(() => createDefaultFormState(defaultDate ?? null, undefined, defaultRange ?? null))
73
+ const [fieldErrors, setFieldErrors] = React.useState<FieldErrors>({})
74
+ const [saving, setSaving] = React.useState(false)
75
+ const openKeyRef = React.useRef<string | null>(null)
76
+ const isEdit = mode === 'edit' && Boolean(item?.id)
77
+ const config = KIND_CONFIG[form.kind]
78
+
79
+ const update = React.useCallback((patch: Partial<EditorFormState>) => {
80
+ setForm((previous) => ({ ...previous, ...patch }))
81
+ }, [])
82
+
83
+ React.useEffect(() => {
84
+ if (!open) {
85
+ openKeyRef.current = null
86
+ return
87
+ }
88
+ const key = `${mode}:${item?.id ?? 'new'}`
89
+ if (openKeyRef.current === key) return
90
+ openKeyRef.current = key
91
+ setForm(
92
+ mode === 'edit' && item
93
+ ? parseItemToFormState(item)
94
+ : createDefaultFormState(defaultDate ?? null, undefined, defaultRange ?? null),
95
+ )
96
+ setFieldErrors({})
97
+ }, [open, mode, item, defaultDate, defaultRange])
98
+
99
+ useEditorLabelResolution(open, form, update)
100
+ // Probe against the owner the interaction will actually be SAVED with so the
101
+ // editor warning matches the grid's post-save conflict rings: tasks own via
102
+ // their assignee, other kinds stay ownerless on create / keep the existing
103
+ // owner on edit (conflicts then come from shared participants).
104
+ const draftOwnerUserId = resolveSavedOwnerUserId(config, form, isEdit, item?.ownerUserId ?? null)
105
+ // Self-exclude by the underlying interaction id (raw.id): findEditorConflictItems
106
+ // drops candidates by raw.id, and every expanded occurrence of a recurring series
107
+ // shares it — so the edited record never conflicts with itself. (asEditableItem
108
+ // already normalizes a clicked occurrence's id to raw.id, so these match today;
109
+ // raw.id keeps it correct even if a future edit path skips that normalization.)
110
+ const conflict = useConflictProbe(open, form, config, isEdit && item ? item.raw.id : null, draftOwnerUserId, conflictScope ?? 'all', currentUserId ?? null)
111
+
112
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
113
+ formId: string
114
+ resourceKind: string
115
+ resourceId: string
116
+ retryLastMutation: () => Promise<boolean>
117
+ }>({
118
+ contextId: 'customers-calendar-event-editor',
119
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
120
+ })
121
+
122
+ const handleSave = React.useCallback(async () => {
123
+ const errors: FieldErrors = {}
124
+ if (!form.title.trim()) errors.title = t('customers.calendar.editor.validation.titleRequired', 'Title is required')
125
+ if (!form.relatedTo) errors.relatedTo = t('customers.calendar.editor.validation.relatedToRequired', 'Select a person or company to link this event')
126
+ if (config.hasEnd && !form.allDay && computeDurationMinutes(form) === null) {
127
+ errors.ends = t('customers.calendar.editor.validation.endsBeforeStarts', 'End must be after start')
128
+ }
129
+ setFieldErrors(errors)
130
+ if (Object.keys(errors).length > 0) return
131
+ setSaving(true)
132
+ try {
133
+ const payload = buildInteractionPayload(form, { mode, id: item?.id })
134
+ await runMutation({
135
+ operation: () => {
136
+ const call = () =>
137
+ apiCallOrThrow('/api/customers/interactions', {
138
+ method: isEdit ? 'PUT' : 'POST',
139
+ headers: { 'content-type': 'application/json' },
140
+ body: JSON.stringify(payload),
141
+ })
142
+ return isEdit ? withScopedApiRequestHeaders(buildOptimisticLockHeader(item?.updatedAt), call) : call()
143
+ },
144
+ mutationPayload: {
145
+ operation: isEdit ? 'updateCalendarEvent' : 'createCalendarEvent',
146
+ interactionId: item?.id ?? null,
147
+ interactionType: form.category ?? form.kind,
148
+ },
149
+ context: {
150
+ formId: 'customers-calendar-event-editor',
151
+ resourceKind: 'customers.interaction',
152
+ resourceId: item?.id ?? 'new',
153
+ retryLastMutation,
154
+ },
155
+ })
156
+ flash(t('customers.calendar.editor.saved', 'Event saved'), 'success')
157
+ onOpenChange(false)
158
+ requestAnimationFrame(() => { onSaved() })
159
+ } catch (err) {
160
+ // An optimistic-lock 409 is surfaced as the persistent conflict bar by
161
+ // useGuardedMutation (surfaceRecordConflict) — close without re-flashing.
162
+ if (extractOptimisticLockConflict(err)) {
163
+ onOpenChange(false)
164
+ return
165
+ }
166
+ const { message } = mapCrudServerErrorToFormErrors(err)
167
+ const key = typeof message === 'string' ? message.trim() : ''
168
+ flash(key ? t(key, key) : t('customers.calendar.editor.error', 'Failed to save event'), 'error')
169
+ } finally {
170
+ setSaving(false)
171
+ }
172
+ }, [config.hasEnd, form, isEdit, item?.id, mode, onOpenChange, onSaved, retryLastMutation, runMutation, t])
173
+
174
+ const handleKeyDown = useDialogKeyHandler({ onConfirm: handleSave, disabled: saving })
175
+
176
+ const categoryOptions = React.useMemo(
177
+ () =>
178
+ buildEditorCategoryOptions({
179
+ typeLabels,
180
+ surfacedTypes: surfacedTypes ?? [],
181
+ eventCategories: eventCategories ?? [],
182
+ selectedValue: form.category ?? form.kind,
183
+ selectedFallbackLabel: t(`customers.calendar.editor.types.${form.kind}`, form.kind),
184
+ }),
185
+ [typeLabels, surfacedTypes, eventCategories, form.category, form.kind, t],
186
+ )
187
+
188
+ const titleLabel = form.kind === 'email'
189
+ ? t('customers.calendar.editor.titleLabel.email', 'Subject')
190
+ : form.kind === 'note'
191
+ ? t('customers.calendar.editor.titleLabel.note', 'Note')
192
+ : t('customers.calendar.editor.titleLabel.generic', 'Title')
193
+ const dialogTitle = isEdit ? t('customers.calendar.editor.title.edit', 'Edit event') : t('customers.calendar.editor.title.create', 'New event')
194
+
195
+ return (
196
+ <Dialog open={open} onOpenChange={onOpenChange}>
197
+ <DialogContent
198
+ onKeyDown={handleKeyDown}
199
+ aria-describedby={undefined}
200
+ dismissible={false}
201
+ className="flex h-dvh max-h-dvh w-screen max-w-none flex-col gap-0 overflow-hidden rounded-none border-0 bg-background p-0 shadow-xl sm:h-auto sm:max-h-screen sm:w-full sm:max-w-md sm:rounded-2xl sm:border-0"
202
+ >
203
+ <VisuallyHidden>
204
+ <DialogTitle>{dialogTitle}</DialogTitle>
205
+ </VisuallyHidden>
206
+ <div className="flex shrink-0 items-center gap-3 border-b border-border bg-background py-4 pl-5 pr-4">
207
+ <Calendar aria-hidden className="size-6 shrink-0 text-foreground" strokeWidth={1.75} />
208
+ <p className="min-w-0 flex-1 text-sm font-medium leading-5 text-foreground">{dialogTitle}</p>
209
+ <IconButton
210
+ variant="ghost"
211
+ size="sm"
212
+ onClick={() => onOpenChange(false)}
213
+ aria-label={t('customers.calendar.editor.close', 'Close')}
214
+ className="shrink-0 text-muted-foreground"
215
+ >
216
+ <X aria-hidden className="size-5" />
217
+ </IconButton>
218
+ </div>
219
+ <div className="flex-1 overflow-y-auto">
220
+ <div className="flex flex-col items-start gap-5 px-4 py-5 sm:px-6 sm:py-6">
221
+ {conflict ? (
222
+ <Alert variant="warning" className="rounded-lg">
223
+ <AlertTitle>{t('customers.calendar.editor.conflictTitle', 'Calendar conflict')}</AlertTitle>
224
+ <AlertDescription>{conflict}</AlertDescription>
225
+ </Alert>
226
+ ) : null}
227
+ <SegmentGroup<EditorKind>
228
+ ariaLabel={t('customers.calendar.editor.typeSwitcher', 'Event type')}
229
+ value={form.kind}
230
+ onChange={(kind) => update({ kind, category: null })}
231
+ options={EDITOR_KINDS.map((kind) => ({ value: kind, label: t(`customers.calendar.editor.types.${kind}`, kind) }))}
232
+ />
233
+ <Field label={titleLabel} error={fieldErrors.title}>
234
+ <Input
235
+ type="text"
236
+ value={form.title}
237
+ onChange={(event) => update({ title: event.target.value })}
238
+ placeholder={t('customers.calendar.editor.titlePlaceholder', 'Add a title…')}
239
+ aria-label={titleLabel}
240
+ autoFocus
241
+ size="lg"
242
+ />
243
+ </Field>
244
+ <Field label={t('customers.calendar.editor.relatedTo', 'Related to')} error={fieldErrors.relatedTo}>
245
+ <RelatedToField
246
+ label={t('customers.calendar.editor.relatedTo', 'Related to')}
247
+ value={form.relatedTo}
248
+ deal={form.dealId && form.dealLabel ? { id: form.dealId, label: form.dealLabel } : null}
249
+ onChange={(relatedTo) => update({ relatedTo })}
250
+ onDealChange={(deal) => update({ dealId: deal?.id ?? null, dealLabel: deal?.label ?? null })}
251
+ error={fieldErrors.relatedTo}
252
+ />
253
+ </Field>
254
+ <ScheduleSection
255
+ dateLabel={config.dateLabel}
256
+ hasAllDay={config.hasAllDay}
257
+ hasEnd={config.hasEnd}
258
+ allDay={form.allDay}
259
+ date={form.date}
260
+ startTime={form.startTime}
261
+ endDate={form.endDate}
262
+ endTime={form.endTime}
263
+ locale={locale}
264
+ endsError={fieldErrors.ends}
265
+ onAllDayChange={(allDay) => update({ allDay })}
266
+ onDateChange={(date) => {
267
+ const untouchedDefault =
268
+ JSON.stringify(form.repeatDays) === JSON.stringify(defaultRepeatDaysForDateInput(form.date))
269
+ update(
270
+ untouchedDefault
271
+ ? { date, repeatDays: defaultRepeatDaysForDateInput(date) }
272
+ : { date },
273
+ )
274
+ }}
275
+ onStartTimeChange={(startTime) => update({ startTime })}
276
+ onEndDateChange={(endDate) => update({ endDate })}
277
+ onEndTimeChange={(endTime) => update({ endTime })}
278
+ />
279
+ {config.hasRepeat ? (
280
+ <RepeatField
281
+ freq={form.repeatFreq}
282
+ days={form.repeatDays}
283
+ endType={form.repeatEndType}
284
+ count={form.repeatCount}
285
+ untilDate={form.repeatUntilDate}
286
+ locale={locale}
287
+ onFreqChange={(repeatFreq) =>
288
+ update(
289
+ repeatFreq === 'weekly'
290
+ ? { repeatFreq, repeatDays: defaultRepeatDaysForDateInput(form.date) }
291
+ : { repeatFreq },
292
+ )}
293
+ onToggleDay={(index) =>
294
+ update({ repeatDays: form.repeatDays.map((active, dayIndex) => (dayIndex === index ? !active : active)) })}
295
+ onEndTypeChange={(repeatEndType) => update({ repeatEndType })}
296
+ onCountChange={(repeatCount) => update({ repeatCount })}
297
+ onUntilDateChange={(repeatUntilDate) => update({ repeatUntilDate })}
298
+ />
299
+ ) : null}
300
+ <Field label={t('customers.calendar.editor.category', 'Category')}>
301
+ <CategoryField
302
+ label={t('customers.calendar.editor.category', 'Category')}
303
+ value={form.category ?? form.kind}
304
+ options={categoryOptions}
305
+ colors={typeColors}
306
+ onChange={(category) => update({ category })}
307
+ />
308
+ </Field>
309
+ {config.location ? (
310
+ <LocationField variant={config.location} value={form.location} onChange={(location) => update({ location })} />
311
+ ) : null}
312
+ {config.people && config.people !== 'assignee' ? (
313
+ <Field label={t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label)}>
314
+ <PeopleField
315
+ mode="multi"
316
+ includeCustomers
317
+ placeholder={t(PEOPLE_FIELD_TEXT[config.people].placeholderKey, PEOPLE_FIELD_TEXT[config.people].placeholder)}
318
+ ariaLabel={t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label)}
319
+ value={form.participants}
320
+ onChange={(participants) => update({ participants })}
321
+ />
322
+ </Field>
323
+ ) : null}
324
+ {config.hasPriority ? (
325
+ <Field label={t('customers.calendar.editor.priority.label', 'Priority')}>
326
+ <SegmentGroup<EditorPriority>
327
+ size="md"
328
+ ariaLabel={t('customers.calendar.editor.priority.label', 'Priority')}
329
+ value={form.priority}
330
+ onChange={(priority) => update({ priority })}
331
+ options={[
332
+ { value: 'low', label: t('customers.calendar.editor.priority.low', 'Low') },
333
+ { value: 'medium', label: t('customers.calendar.editor.priority.medium', 'Medium') },
334
+ { value: 'high', label: t('customers.calendar.editor.priority.high', 'High') },
335
+ ]}
336
+ />
337
+ </Field>
338
+ ) : null}
339
+ {config.people === 'assignee' ? (
340
+ <Field label={t('customers.calendar.editor.assignee', 'Assignee')}>
341
+ <PeopleField
342
+ mode="single"
343
+ includeCustomers={false}
344
+ placeholder={t('customers.calendar.editor.assigneePlaceholder', 'Assign to a team member…')}
345
+ ariaLabel={t('customers.calendar.editor.assignee', 'Assignee')}
346
+ value={form.assigneeUserId
347
+ ? [{ userId: form.assigneeUserId, name: form.assigneeName ?? form.assigneeUserId, isCustomer: false }]
348
+ : []}
349
+ onChange={(entries) => {
350
+ const next = entries[entries.length - 1] ?? null
351
+ update({ assigneeUserId: next?.userId ?? null, assigneeName: next?.name ?? null })
352
+ }}
353
+ />
354
+ </Field>
355
+ ) : null}
356
+ <Field label={t('customers.calendar.editor.description', 'Description')}>
357
+ <Textarea
358
+ value={form.description}
359
+ onChange={(event) => update({ description: event.target.value })}
360
+ placeholder={t('customers.calendar.editor.descriptionPlaceholder', 'Add details…')}
361
+ aria-label={t('customers.calendar.editor.description', 'Description')}
362
+ className="h-24 resize-none"
363
+ />
364
+ </Field>
365
+ </div>
366
+ </div>
367
+ <div className="flex shrink-0 items-center justify-end gap-3 border-t border-border bg-background px-5 py-4">
368
+ <Button
369
+ type="button"
370
+ variant="outline"
371
+ onClick={() => onOpenChange(false)}
372
+ >
373
+ {t('customers.calendar.editor.cancel', 'Cancel')}
374
+ </Button>
375
+ <Button
376
+ type="button"
377
+ onClick={handleSave}
378
+ disabled={saving}
379
+ >
380
+ {saving ? t('customers.calendar.editor.saving', 'Saving…') : t('customers.calendar.editor.save', 'Save event')}
381
+ </Button>
382
+ </div>
383
+ </DialogContent>
384
+ </Dialog>
385
+ )
386
+ }
@@ -0,0 +1,61 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Button } from '@open-mercato/ui/primitives/button'
5
+ import { Kbd } from '@open-mercato/ui/primitives/kbd'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import type { CalendarFooterProps } from './types'
8
+
9
+ export const CALENDAR_SHORTCUTS: ReadonlyArray<{
10
+ key: string
11
+ labelKey: string
12
+ fallback: string
13
+ }> = [
14
+ { key: 'T', labelKey: 'customers.calendar.shortcuts.today', fallback: 'Today' },
15
+ { key: 'D', labelKey: 'customers.calendar.shortcuts.dayView', fallback: 'Day view' },
16
+ { key: 'W', labelKey: 'customers.calendar.shortcuts.week', fallback: 'Week' },
17
+ { key: 'M', labelKey: 'customers.calendar.shortcuts.month', fallback: 'Month' },
18
+ { key: 'A', labelKey: 'customers.calendar.shortcuts.agenda', fallback: 'Agenda' },
19
+ { key: 'N', labelKey: 'customers.calendar.shortcuts.newEvent', fallback: 'New event' },
20
+ { key: '/', labelKey: 'customers.calendar.shortcuts.search', fallback: 'Search' },
21
+ { key: '?', labelKey: 'customers.calendar.shortcuts.help', fallback: 'Shortcuts' },
22
+ ]
23
+
24
+ export function CalendarFooter({ timezoneLabel, onOpenShortcuts }: CalendarFooterProps) {
25
+ const t = useT()
26
+ return (
27
+ <footer className="flex flex-wrap items-center gap-x-4 gap-y-1.5 border-t bg-muted/40 px-4 py-2">
28
+ {CALENDAR_SHORTCUTS.map((shortcut) => {
29
+ const legend = (
30
+ <>
31
+ <Kbd>{shortcut.key}</Kbd>
32
+ <span className="text-xs text-muted-foreground">
33
+ {t(shortcut.labelKey, shortcut.fallback)}
34
+ </span>
35
+ </>
36
+ )
37
+ if (shortcut.key === '?' && onOpenShortcuts) {
38
+ return (
39
+ <Button
40
+ key={shortcut.key}
41
+ type="button"
42
+ variant="ghost"
43
+ size="sm"
44
+ className="h-auto gap-1.5 p-0 font-normal hover:bg-transparent"
45
+ aria-label={t('customers.calendar.shortcuts.title', 'Keyboard shortcuts')}
46
+ onClick={onOpenShortcuts}
47
+ >
48
+ {legend}
49
+ </Button>
50
+ )
51
+ }
52
+ return (
53
+ <span key={shortcut.key} className="inline-flex items-center gap-1.5">
54
+ {legend}
55
+ </span>
56
+ )
57
+ })}
58
+ <span className="ml-auto text-xs text-muted-foreground">{timezoneLabel}</span>
59
+ </footer>
60
+ )
61
+ }