create-mithaq 0.0.0-stage → 0.2.0

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 (396) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/openstatus-time-picker.txt +21 -0
  3. package/LICENSES/payload.txt +22 -0
  4. package/LICENSES/ponytail.txt +21 -0
  5. package/index.js +521 -0
  6. package/package.json +19 -4
  7. package/template/apps/asas-backoffice/components.json +25 -0
  8. package/template/apps/asas-backoffice/gitignore +3 -0
  9. package/template/apps/asas-backoffice/package.json +57 -0
  10. package/template/apps/asas-backoffice/public/favicon.svg +1 -0
  11. package/template/apps/asas-backoffice/src/api.ts +147 -0
  12. package/template/apps/asas-backoffice/src/auth-frame.tsx +150 -0
  13. package/template/apps/asas-backoffice/src/components/ui/accordion.tsx +74 -0
  14. package/template/apps/asas-backoffice/src/components/ui/alert-dialog.tsx +185 -0
  15. package/template/apps/asas-backoffice/src/components/ui/avatar.tsx +106 -0
  16. package/template/apps/asas-backoffice/src/components/ui/badge.tsx +48 -0
  17. package/template/apps/asas-backoffice/src/components/ui/breadcrumb.tsx +124 -0
  18. package/template/apps/asas-backoffice/src/components/ui/button-group.tsx +87 -0
  19. package/template/apps/asas-backoffice/src/components/ui/button.tsx +55 -0
  20. package/template/apps/asas-backoffice/src/components/ui/calendar.tsx +223 -0
  21. package/template/apps/asas-backoffice/src/components/ui/card.tsx +102 -0
  22. package/template/apps/asas-backoffice/src/components/ui/chart.tsx +371 -0
  23. package/template/apps/asas-backoffice/src/components/ui/checkbox.tsx +28 -0
  24. package/template/apps/asas-backoffice/src/components/ui/collapsible.tsx +19 -0
  25. package/template/apps/asas-backoffice/src/components/ui/combobox.tsx +308 -0
  26. package/template/apps/asas-backoffice/src/components/ui/dialog.tsx +158 -0
  27. package/template/apps/asas-backoffice/src/components/ui/direction.tsx +6 -0
  28. package/template/apps/asas-backoffice/src/components/ui/directional-icon.tsx +20 -0
  29. package/template/apps/asas-backoffice/src/components/ui/dropdown-menu.tsx +267 -0
  30. package/template/apps/asas-backoffice/src/components/ui/field.tsx +236 -0
  31. package/template/apps/asas-backoffice/src/components/ui/input-group.tsx +155 -0
  32. package/template/apps/asas-backoffice/src/components/ui/input-otp.tsx +84 -0
  33. package/template/apps/asas-backoffice/src/components/ui/input.tsx +19 -0
  34. package/template/apps/asas-backoffice/src/components/ui/label.tsx +19 -0
  35. package/template/apps/asas-backoffice/src/components/ui/popover.tsx +90 -0
  36. package/template/apps/asas-backoffice/src/components/ui/radio-group.tsx +37 -0
  37. package/template/apps/asas-backoffice/src/components/ui/resizable.tsx +47 -0
  38. package/template/apps/asas-backoffice/src/components/ui/scroll-area.tsx +54 -0
  39. package/template/apps/asas-backoffice/src/components/ui/select.tsx +204 -0
  40. package/template/apps/asas-backoffice/src/components/ui/separator.tsx +24 -0
  41. package/template/apps/asas-backoffice/src/components/ui/sheet.tsx +139 -0
  42. package/template/apps/asas-backoffice/src/components/ui/sidebar.tsx +714 -0
  43. package/template/apps/asas-backoffice/src/components/ui/skeleton.tsx +13 -0
  44. package/template/apps/asas-backoffice/src/components/ui/slider.tsx +55 -0
  45. package/template/apps/asas-backoffice/src/components/ui/spinner.tsx +10 -0
  46. package/template/apps/asas-backoffice/src/components/ui/switch.tsx +29 -0
  47. package/template/apps/asas-backoffice/src/components/ui/table.tsx +115 -0
  48. package/template/apps/asas-backoffice/src/components/ui/tabs.tsx +79 -0
  49. package/template/apps/asas-backoffice/src/components/ui/textarea.tsx +17 -0
  50. package/template/apps/asas-backoffice/src/components/ui/toggle-group.tsx +89 -0
  51. package/template/apps/asas-backoffice/src/components/ui/toggle.tsx +44 -0
  52. package/template/apps/asas-backoffice/src/components/ui/tooltip.tsx +63 -0
  53. package/template/apps/asas-backoffice/src/components.ts +35 -0
  54. package/template/apps/asas-backoffice/src/copy.ts +1 -0
  55. package/template/apps/asas-backoffice/src/first-user.ts +14 -0
  56. package/template/apps/asas-backoffice/src/hooks/use-mobile.ts +19 -0
  57. package/template/apps/asas-backoffice/src/lib/utils.ts +1 -0
  58. package/template/apps/asas-backoffice/src/links.ts +38 -0
  59. package/template/apps/asas-backoffice/src/locale-switch.tsx +28 -0
  60. package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
  61. package/template/apps/asas-backoffice/src/redirect.ts +1 -0
  62. package/template/apps/asas-backoffice/src/request-headers.ts +11 -0
  63. package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
  64. package/template/apps/asas-backoffice/src/router.tsx +18 -0
  65. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
  66. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/screen.json +3971 -0
  67. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/types.ts +163 -0
  68. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.$.tsx +17 -0
  69. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.index.tsx +10 -0
  70. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.tsx +13 -0
  71. package/template/apps/asas-backoffice/src/routes/__root.tsx +95 -0
  72. package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
  73. package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
  74. package/template/apps/asas-backoffice/src/routes/auth.sign-in.tsx +77 -0
  75. package/template/apps/asas-backoffice/src/routes/unauthorized.tsx +134 -0
  76. package/template/apps/asas-backoffice/src/screen-form.tsx +36 -0
  77. package/template/apps/asas-backoffice/src/screen-list.tsx +140 -0
  78. package/template/apps/asas-backoffice/src/screen-names.ts +10 -0
  79. package/template/apps/asas-backoffice/src/screens.tsx +888 -0
  80. package/template/apps/asas-backoffice/src/server.ts +30 -0
  81. package/template/apps/asas-backoffice/src/slide.ts +22 -0
  82. package/template/apps/asas-backoffice/src/start.ts +42 -0
  83. package/template/apps/asas-backoffice/src/styles.css +202 -0
  84. package/template/apps/asas-backoffice/src/theme.ts +2 -0
  85. package/template/apps/asas-backoffice/tsconfig.json +19 -0
  86. package/template/apps/asas-backoffice/vite.config.ts +32 -0
  87. package/template/apps/asas-tanstack/components.json +25 -0
  88. package/template/apps/asas-tanstack/gitignore +3 -0
  89. package/template/apps/asas-tanstack/package.json +58 -0
  90. package/template/apps/asas-tanstack/public/favicon.svg +1 -0
  91. package/template/apps/asas-tanstack/src/api.ts +147 -0
  92. package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
  93. package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
  94. package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
  95. package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
  96. package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
  97. package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
  98. package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
  99. package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
  100. package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
  101. package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
  102. package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
  103. package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
  104. package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
  105. package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
  106. package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
  107. package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
  108. package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
  109. package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
  110. package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
  111. package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
  112. package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
  113. package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
  114. package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
  115. package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
  116. package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
  117. package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
  118. package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
  119. package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
  120. package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
  121. package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
  122. package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
  123. package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
  124. package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
  125. package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
  126. package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
  127. package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
  128. package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
  129. package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
  130. package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
  131. package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
  132. package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
  133. package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
  134. package/template/apps/asas-tanstack/src/components.ts +35 -0
  135. package/template/apps/asas-tanstack/src/copy.ts +1207 -0
  136. package/template/apps/asas-tanstack/src/first-run.ts +16 -0
  137. package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
  138. package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
  139. package/template/apps/asas-tanstack/src/links.ts +38 -0
  140. package/template/apps/asas-tanstack/src/locale-switch.tsx +28 -0
  141. package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
  142. package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
  143. package/template/apps/asas-tanstack/src/redirect.ts +39 -0
  144. package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
  145. package/template/apps/asas-tanstack/src/routeTree.gen.ts +424 -0
  146. package/template/apps/asas-tanstack/src/router.tsx +18 -0
  147. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +11 -0
  148. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +4242 -0
  149. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +160 -0
  150. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
  151. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
  152. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
  153. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +9 -0
  154. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1564 -0
  155. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +94 -0
  156. package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
  157. package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
  158. package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
  159. package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
  160. package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
  161. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +108 -0
  162. package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
  163. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
  164. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
  165. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +16 -0
  166. package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
  167. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +98 -0
  168. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +143 -0
  169. package/template/apps/asas-tanstack/src/routes/index.tsx +146 -0
  170. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +134 -0
  171. package/template/apps/asas-tanstack/src/screen-form.tsx +36 -0
  172. package/template/apps/asas-tanstack/src/screen-list.tsx +140 -0
  173. package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
  174. package/template/apps/asas-tanstack/src/screens.tsx +888 -0
  175. package/template/apps/asas-tanstack/src/server.ts +26 -0
  176. package/template/apps/asas-tanstack/src/slide.ts +22 -0
  177. package/template/apps/asas-tanstack/src/start.ts +57 -0
  178. package/template/apps/asas-tanstack/src/styles.css +163 -0
  179. package/template/apps/asas-tanstack/src/theme.ts +5 -0
  180. package/template/apps/asas-tanstack/tsconfig.json +19 -0
  181. package/template/apps/asas-tanstack/vite.config.ts +34 -0
  182. package/template/package.json +16 -0
  183. package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
  184. package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
  185. package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
  186. package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
  187. package/template/templates/asas/.env.example +33 -0
  188. package/template/templates/asas/AGENTS.md +101 -0
  189. package/template/templates/asas/CLAUDE.md +1 -0
  190. package/template/templates/asas/README.md +1056 -0
  191. package/template/templates/asas/mithaq.hints.json +25 -0
  192. package/template/templates/asas/package.json +53 -0
  193. package/template/templates/asas/src/asas.ts +118 -0
  194. package/template/templates/asas/src/backoffice-http.ts +117 -0
  195. package/template/templates/asas/src/dev-setup.ts +112 -0
  196. package/template/templates/asas/src/local-database.ts +22 -0
  197. package/template/templates/asas/src/messages/account.json +132 -0
  198. package/template/templates/asas/src/messages/auditLog.json +304 -0
  199. package/template/templates/asas/src/messages/backofficeLog.json +132 -0
  200. package/template/templates/asas/src/messages/courses.json +312 -0
  201. package/template/templates/asas/src/messages/enrollments.json +60 -0
  202. package/template/templates/asas/src/messages/invitation.json +82 -0
  203. package/template/templates/asas/src/messages/localUsers.json +48 -0
  204. package/template/templates/asas/src/messages/media.json +76 -0
  205. package/template/templates/asas/src/messages/member.json +62 -0
  206. package/template/templates/asas/src/messages/memberRoles.json +48 -0
  207. package/template/templates/asas/src/messages/mithaq.json +214 -0
  208. package/template/templates/asas/src/messages/organization.json +72 -0
  209. package/template/templates/asas/src/messages/organizationRole.json +62 -0
  210. package/template/templates/asas/src/messages/roles.json +72 -0
  211. package/template/templates/asas/src/messages/session.json +102 -0
  212. package/template/templates/asas/src/messages/sessions.json +136 -0
  213. package/template/templates/asas/src/messages/settings.json +64 -0
  214. package/template/templates/asas/src/messages/tags.json +48 -0
  215. package/template/templates/asas/src/messages/user.json +112 -0
  216. package/template/templates/asas/src/messages/verification.json +62 -0
  217. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
  218. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
  219. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
  220. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
  221. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
  222. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
  223. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
  224. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
  225. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
  226. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
  227. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
  228. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
  229. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
  230. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
  231. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
  232. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
  233. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
  234. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
  235. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
  236. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
  237. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
  238. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
  239. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
  240. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
  241. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
  242. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
  243. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
  244. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
  245. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
  246. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
  247. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
  248. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
  249. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
  250. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
  251. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
  252. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
  253. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
  254. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
  255. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
  256. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
  257. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
  258. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
  259. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
  260. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
  261. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
  262. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
  263. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
  264. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
  265. package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
  266. package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
  267. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
  268. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
  269. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
  270. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
  271. package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
  272. package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
  273. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
  274. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
  275. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
  276. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
  277. package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
  278. package/template/templates/asas/src/migrations/schema.ts +30 -0
  279. package/template/templates/asas/src/mithaq.ts +4 -0
  280. package/template/templates/asas/src/organizations.ts +3 -0
  281. package/template/templates/asas/src/seed.ts +427 -0
  282. package/template/templates/asas/src/ui-backoffice/home.tsx +63 -0
  283. package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
  284. package/template/templates/asas/src/ui-backoffice/record-page.tsx +30 -0
  285. package/template/templates/asas/src/ui-backoffice/role-cells.tsx +59 -0
  286. package/template/templates/asas/src/ui-backoffice/user-actions.tsx +249 -0
  287. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +300 -0
  288. package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
  289. package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
  290. package/template/templates/asas/src/ui-dashboard/members.tsx +829 -0
  291. package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
  292. package/template/templates/asas/src/ui-dashboard/versions.tsx +446 -0
  293. package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
  294. package/template/templates/asas/src/ui-portal/home.tsx +73 -0
  295. package/template/templates/asas/src/ui-portal/page.tsx +11 -0
  296. package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
  297. package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
  298. package/template/templates/features/audit-log/index.ts +4 -0
  299. package/template/templates/features/audit-log/package.json +13 -0
  300. package/template/templates/features/auth/index.ts +11 -0
  301. package/template/templates/features/auth/local-users/local-users.ts +143 -0
  302. package/template/templates/features/auth/package.json +13 -0
  303. package/template/templates/features/auth/user/account.access.ts +8 -0
  304. package/template/templates/features/auth/user/account.ts +31 -0
  305. package/template/templates/features/auth/user/index.ts +18 -0
  306. package/template/templates/features/auth/user/invitation.access.ts +4 -0
  307. package/template/templates/features/auth/user/invitation.ts +27 -0
  308. package/template/templates/features/auth/user/member.access.ts +10 -0
  309. package/template/templates/features/auth/user/member.screens.ts +37 -0
  310. package/template/templates/features/auth/user/member.ts +26 -0
  311. package/template/templates/features/auth/user/organization.access.ts +20 -0
  312. package/template/templates/features/auth/user/organization.screens.ts +37 -0
  313. package/template/templates/features/auth/user/organization.ts +25 -0
  314. package/template/templates/features/auth/user/organizationRole.access.ts +8 -0
  315. package/template/templates/features/auth/user/organizationRole.ts +25 -0
  316. package/template/templates/features/auth/user/session.access.ts +8 -0
  317. package/template/templates/features/auth/user/session.ts +28 -0
  318. package/template/templates/features/auth/user/user.access.ts +30 -0
  319. package/template/templates/features/auth/user/user.screens.ts +45 -0
  320. package/template/templates/features/auth/user/user.ts +31 -0
  321. package/template/templates/features/auth/user/verification.access.ts +8 -0
  322. package/template/templates/features/auth/user/verification.ts +23 -0
  323. package/template/templates/features/courses/courses/courses.ts +601 -0
  324. package/template/templates/features/courses/courses/cover-reads.ts +33 -0
  325. package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
  326. package/template/templates/features/courses/courses/ui/fee-cell.tsx +19 -0
  327. package/template/templates/features/courses/courses/ui/no-courses.tsx +13 -0
  328. package/template/templates/features/courses/courses/ui/status-cell.tsx +33 -0
  329. package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
  330. package/template/templates/features/courses/domain/archive.ts +39 -0
  331. package/template/templates/features/courses/domain/on-offer.ts +6 -0
  332. package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
  333. package/template/templates/features/courses/enrollments/ui/session-cell.tsx +75 -0
  334. package/template/templates/features/courses/enrollments/ui/session-labels.tsx +36 -0
  335. package/template/templates/features/courses/index.ts +8 -0
  336. package/template/templates/features/courses/package.json +24 -0
  337. package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
  338. package/template/templates/features/courses/portal/ui/course.tsx +176 -0
  339. package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
  340. package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
  341. package/template/templates/features/courses/portal/ui/reads.ts +76 -0
  342. package/template/templates/features/courses/portal/ui/rules.ts +87 -0
  343. package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
  344. package/template/templates/features/courses/sessions/seats.ts +42 -0
  345. package/template/templates/features/courses/sessions/sessions.ts +186 -0
  346. package/template/templates/features/media/index.ts +10 -0
  347. package/template/templates/features/media/media/media.ts +131 -0
  348. package/template/templates/features/media/media/ui/preview.tsx +143 -0
  349. package/template/templates/features/media/package.json +19 -0
  350. package/template/templates/features/roles/index.ts +4 -0
  351. package/template/templates/features/roles/member-roles/member-roles.ts +207 -0
  352. package/template/templates/features/roles/package.json +21 -0
  353. package/template/templates/features/roles/roles/roles.ts +184 -0
  354. package/template/templates/features/roles/roles/ui/role-form.tsx +267 -0
  355. package/template/templates/features/roles/roles/ui/roles.tsx +190 -0
  356. package/template/templates/features/settings/index.ts +3 -0
  357. package/template/templates/features/settings/package.json +13 -0
  358. package/template/templates/features/settings/settings/settings.ts +64 -0
  359. package/template/templates/features/tags/index.ts +3 -0
  360. package/template/templates/features/tags/package.json +14 -0
  361. package/template/templates/features/tags/tags/tags.ts +80 -0
  362. package/template/templates/mithaq/components.json +22 -0
  363. package/template/templates/mithaq/package.json +33 -0
  364. package/template/templates/mithaq/src/mail/index.ts +32 -0
  365. package/template/templates/mithaq/src/mail/mailers.ts +36 -0
  366. package/template/templates/mithaq/src/mithaq/audit-sink.ts +111 -0
  367. package/template/templates/mithaq/src/mithaq/auth/backoffice-roles.ts +108 -0
  368. package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
  369. package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
  370. package/template/templates/mithaq/src/mithaq/pages.ts +83 -0
  371. package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
  372. package/template/templates/mithaq/src/mithaq.ts +593 -0
  373. package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
  374. package/template/templates/mithaq/src/roles/grants.ts +47 -0
  375. package/template/templates/mithaq/src/roles/member-access.ts +11 -0
  376. package/template/templates/mithaq/src/roles/statements.ts +13 -0
  377. package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
  378. package/template/templates/mithaq/src/roles/tenant-access.ts +260 -0
  379. package/template/templates/mithaq/src/roles/tenant-roles.ts +321 -0
  380. package/template/templates/mithaq/src/steps/expected-updated-at.ts +41 -0
  381. package/template/templates/mithaq/src/storage/defaults.ts +156 -0
  382. package/template/templates/mithaq/src/storage/index.ts +97 -0
  383. package/template/templates/mithaq/src/storage/local.ts +143 -0
  384. package/template/templates/mithaq/src/storage/r2.ts +5 -0
  385. package/template/templates/mithaq/src/storage/s3.ts +91 -0
  386. package/template/templates/mithaq/src/storage/uploadthing.ts +59 -0
  387. package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
  388. package/template/templates/mithaq/src/ui/activity.tsx +527 -0
  389. package/template/templates/mithaq/src/ui/app-shell.tsx +268 -0
  390. package/template/templates/mithaq/src/ui/kpi-card.tsx +25 -0
  391. package/template/templates/mithaq/src/ui/record-page.tsx +50 -0
  392. package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
  393. package/template/templates/mithaq/src/ui/screen-nav.tsx +28 -0
  394. package/template/templates/mithaq/src/ui/table-page.tsx +9 -0
  395. package/template/templates/mithaq/src/ui/top-shell.tsx +60 -0
  396. package/README.md +0 -3
@@ -0,0 +1,121 @@
1
+ // The list's cell for the `slug` column. Rename is drawn only where this actor holds
2
+ // `organization.rename`, with many organizations alone (with one, the name lives in code), and
3
+ // members only where this actor reads them; the route refuses a rename without the key. A rename that lands asks the list behind it to be read again through
4
+ // `mithaq:refresh`, so the list keeps its scroll.
5
+ import { useState, type FormEvent } from 'react'
6
+ import { holds, type CellProps } from 'mithaq/admin'
7
+ import { useAccess, useLocale, useScreen } from 'mithaq/admin/react'
8
+ import { writeQuery } from 'mithaq-admin/list-query.ts'
9
+ import { endonym } from 'mithaq-admin/locale-menu.ts'
10
+ import { defaultLocale, nameIn, organizationName, organizations } from '@asas/mithaq/auth/organizations'
11
+ import { toast } from 'mithaq-admin/toast.tsx'
12
+ import { Button } from '@/components/ui/button'
13
+ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
14
+ import { Input } from '@/components/ui/input'
15
+ import { Label } from '@/components/ui/label'
16
+
17
+ const slugShape = /^[a-z0-9]+(-[a-z0-9]+)*$/
18
+
19
+ export function OrganizationActions({ value, row }: CellProps) {
20
+ const { t } = useLocale()
21
+ const access = useAccess()
22
+ const screen = useScreen()
23
+ const renames = organizations === 'many' && holds(access, 'organization.rename')
24
+ // The default locale's name is the required `name` box, so only the others get optional boxes of their own.
25
+ const others = Object.keys(organizationName).filter((one) => one !== defaultLocale)
26
+ const members = holds(access, 'member.find') && screen.kayan.member?.pages.list !== undefined
27
+ const [open, setOpen] = useState(false)
28
+ const [refusal, setRefusal] = useState<string>()
29
+ /** Said here rather than by the browser's own bubble, which is in the browser's language. */
30
+ const [wrong, setWrong] = useState<{ name?: string; slug?: string }>({})
31
+ const id = String(row.id ?? '')
32
+ const save = async (event: FormEvent<HTMLFormElement>) => {
33
+ event.preventDefault()
34
+ const box = (key: string) => (event.currentTarget.elements.namedItem(key) as HTMLInputElement | null)?.value ?? ''
35
+ const name = box('name')
36
+ const slug = box('slug')
37
+ // An empty one falls back to `name`, so it is left out.
38
+ const names = { [defaultLocale]: name.trim(), ...Object.fromEntries(others.map((one) => [one, box(`name-${one}`).trim()]).filter(([, one]) => one)) }
39
+ const found = {
40
+ name: name.trim() ? undefined : t('organization.name.required', 'This field is required.'),
41
+ slug: !slug ? t('organization.slug.required', 'This field is required.') : slugShape.test(slug) ? undefined : t('backoffice.slugPattern', 'A slug is lowercase letters and digits joined by single dashes, such as admin-org.'),
42
+ }
43
+ setWrong(found)
44
+ if (found.name || found.slug) return
45
+ const response = await fetch('/api/backoffice/organizations/update', {
46
+ method: 'POST',
47
+ headers: { 'content-type': 'application/json' },
48
+ body: JSON.stringify({ id, names, slug }),
49
+ })
50
+ if (!response.ok) {
51
+ const key = ((await response.json().catch(() => null)) as { data?: { key?: string } } | null)?.data?.key
52
+ return setRefusal((key && t(key)) ?? t('backoffice.refused', 'The backoffice refused that action.'))
53
+ }
54
+ window.dispatchEvent(new Event('mithaq:refresh'))
55
+ // A function, so a name holding `$&` is put in as written.
56
+ toast.success((t('backoffice.renamed', 'The organization is now named {name}.') ?? '').replace('{name}', () => name))
57
+ setOpen(false)
58
+ }
59
+ return (
60
+ <div data-organization={id} className="flex items-center gap-2 whitespace-nowrap">
61
+ {/* An identifier, read left to right in either locale. */}
62
+ <span dir="ltr">{String(value ?? '')}</span>
63
+ {renames ? (
64
+ <Button size="sm" variant="outline" className="max-sm:h-11" data-action="rename" onClick={() => setOpen(true)}>
65
+ {t('backoffice.rename', 'Rename')}
66
+ </Button>
67
+ ) : null}
68
+ {members ? (
69
+ <Button
70
+ size="sm"
71
+ variant="outline"
72
+ className="max-sm:h-11"
73
+ data-action="members"
74
+ nativeButton={false}
75
+ render={<a href={`${screen.path}${screen.kayan.member!.pages.list!.path}?${new URLSearchParams(writeQuery({ where: { or: [{ and: [{ organizationId: { equals: id } }] }] } }))}`} />}
76
+ >
77
+ {t('backoffice.members', 'Members')}
78
+ </Button>
79
+ ) : null}
80
+ {open ? (
81
+ <Dialog open onOpenChange={setOpen}>
82
+ {/* Cancel is the way out, so the primitive's own close, named in
83
+ English alone, is not drawn beside it. */}
84
+ <DialogContent showCloseButton={false}>
85
+ <DialogHeader>
86
+ <DialogTitle>{t('backoffice.rename', 'Rename')}</DialogTitle>
87
+ <DialogDescription>{t('backoffice.renameAbout', 'The new name and slug take the place of the old ones for everyone in this organization.')}</DialogDescription>
88
+ </DialogHeader>
89
+ <form noValidate onSubmit={(event) => void save(event)} className="grid gap-4">
90
+ <div className="grid gap-2">
91
+ <Label htmlFor={`organization-name-${id}`}>{t('organization.name', 'Name')}</Label>
92
+ <Input id={`organization-name-${id}`} name="name" required aria-required dir="auto" defaultValue={String(row.name ?? '')} aria-invalid={wrong.name ? true : undefined} aria-describedby={wrong.name ? `organization-name-${id}-error` : undefined} />
93
+ {wrong.name ? <p id={`organization-name-${id}-error`} role="alert" className="text-sm text-destructive">{wrong.name}</p> : null}
94
+ </div>
95
+ {others.map((one) => (
96
+ <div key={one} className="grid gap-2">
97
+ <Label htmlFor={`organization-name-${one}-${id}`}>{(t('backoffice.nameIn', 'Name in {language}, if it has one') ?? '').replace('{language}', () => endonym(one))}</Label>
98
+ <Input id={`organization-name-${one}-${id}`} name={`name-${one}`} lang={one} dir="auto" defaultValue={nameIn({ name: '', names: row.names }, one)} />
99
+ </div>
100
+ ))}
101
+ <div className="grid gap-2">
102
+ <Label htmlFor={`organization-slug-${id}`}>{t('organization.slug', 'Slug')}</Label>
103
+ <Input id={`organization-slug-${id}`} name="slug" required aria-required dir="ltr" pattern={slugShape.source.slice(1, -1)} defaultValue={String(value ?? '')} aria-invalid={wrong.slug ? true : undefined} aria-describedby={wrong.slug ? `organization-slug-${id}-error` : undefined} />
104
+ {wrong.slug ? <p id={`organization-slug-${id}-error`} role="alert" className="text-sm text-destructive">{wrong.slug}</p> : null}
105
+ </div>
106
+ {refusal ? (
107
+ <p role="alert" className="text-sm text-destructive">
108
+ {refusal}
109
+ </p>
110
+ ) : null}
111
+ <DialogFooter>
112
+ <DialogClose render={<Button type="button" variant="outline" />}>{t('admin.cancel', 'Cancel')}</DialogClose>
113
+ <Button type="submit">{t('admin.save', 'Save')}</Button>
114
+ </DialogFooter>
115
+ </form>
116
+ </DialogContent>
117
+ </Dialog>
118
+ ) : null}
119
+ </div>
120
+ )
121
+ }
@@ -0,0 +1,30 @@
1
+ // The backoffice's record page: the shared one under a link back to the list the
2
+ // record belongs to, so a person's page, an organization's and a role's are never
3
+ // a dead end. The link is named as the nav names the list, which is already the
4
+ // viewer's own word for it.
5
+ import type { ReactNode } from 'react'
6
+ import { ArrowLeft } from 'lucide-react'
7
+ import type { LayoutProps } from 'mithaq/admin'
8
+ import { useKayan, useLocale, useScreen } from 'mithaq/admin/react'
9
+ import { RecordPage } from '@asas/mithaq/ui/record-page.tsx'
10
+ import { navLabel } from '@asas/mithaq/ui/screen-nav.tsx'
11
+
12
+ export const regions = ['main', 'side', 'danger'] as const
13
+
14
+ export function BackofficeRecordPage(props: LayoutProps<readonly ['main', 'side', 'danger'], ReactNode>) {
15
+ const { locale, t } = useLocale()
16
+ const screen = useScreen()
17
+ const kayan = useKayan()
18
+ const list = screen.nav.find((entry) => entry.kayan === kayan.name)
19
+ return (
20
+ <div className="grid gap-4">
21
+ {list ? (
22
+ <a href={`${screen.path}${list.path}`} data-back="" className="inline-flex w-fit items-center gap-1.5 text-sm underline underline-offset-4 max-sm:min-h-11">
23
+ <ArrowLeft className="size-4 rtl:-scale-x-100" aria-hidden="true" />
24
+ <span>{navLabel({ entry: list, screen, locale, t })}</span>
25
+ </a>
26
+ ) : null}
27
+ <RecordPage {...props} />
28
+ </div>
29
+ )
30
+ }
@@ -0,0 +1,59 @@
1
+ // The two role columns the backoffice lists, each named in the viewer's locale
2
+ // where the column holds a slug, so an Arabic page reads no English word and
3
+ // no slug for them.
4
+ import type { CellProps } from 'mithaq/admin'
5
+ import { useLocale } from 'mithaq/admin/react'
6
+ import { listed } from 'mithaq-admin/format.tsx'
7
+ // add-on: roles
8
+ import { use } from 'react'
9
+ import { useQuery } from '@tanstack/react-query'
10
+ import type { Row } from 'mithaq/admin'
11
+ import { ListContext, useClient, useScreen } from 'mithaq/admin/react'
12
+ import type { asas } from '../asas.ts'
13
+ import { refusalKeyOf } from 'mithaq-admin/refusal.ts'
14
+ import { systemRoles } from '@asas/mithaq/roles/system-roles.ts'
15
+ // end add-on: roles
16
+
17
+ /**
18
+ * A membership's role, which is the provider's own governance role, `owner`,
19
+ * `admin` or `member`, several of them comma-joined, each by the project's word for it.
20
+ */
21
+ export function MembershipRole({ value }: CellProps) {
22
+ const { locale, t } = useLocale()
23
+ if (typeof value !== 'string' || !value) return null
24
+ return listed(value.split(',').map((one) => t(`backoffice.membership.${one.trim()}`) ?? one.trim()), locale)
25
+ }
26
+
27
+ // add-on: roles
28
+ /**
29
+ * A business role an assignment names by its slug, by its label: a fixed
30
+ * role's from its code, and a custom role's from its row of the assignment's own tenant, one read for
31
+ * every custom slug the page's rows name, shared through the query client as
32
+ * the session column shares its own. A role the viewer may not read keeps its
33
+ * slug, the one thing the column holds.
34
+ */
35
+ export function RoleName({ value, row }: CellProps) {
36
+ const client = useClient<typeof asas.router>()
37
+ const { locale } = useLocale()
38
+ const { maxLimit } = useScreen()
39
+ const slugs = [...new Set((use(ListContext)?.page.docs ?? [row]).flatMap((one: Row) => (typeof one.role === 'string' && !Object.hasOwn(systemRoles, one.role) ? [one.role] : [])))].sort()
40
+ const { data: custom } = useQuery({
41
+ queryKey: ['role-name', slugs],
42
+ queryFn: () =>
43
+ client({ kayan: 'roles', operation: 'find', input: { where: { name: { in: slugs } }, limit: maxLimit } }).then(
44
+ (found) => found.docs,
45
+ (thrown: unknown) => {
46
+ if (refusalKeyOf(thrown)) return []
47
+ throw thrown
48
+ },
49
+ ),
50
+ enabled: slugs.length > 0,
51
+ staleTime: 30_000,
52
+ })
53
+ if (typeof value !== 'string') return null
54
+ const label: Record<string, string> | undefined = Object.hasOwn(systemRoles, value)
55
+ ? systemRoles[value as keyof typeof systemRoles].label
56
+ : (custom?.find((one) => one.name === value && one.organization === row.organization)?.label as Record<string, string> | undefined)
57
+ return label?.[locale] ?? value
58
+ }
59
+ // end add-on: roles
@@ -0,0 +1,249 @@
1
+ // Each action is the admin plugin's own route, drawn only where this actor holds its key
2
+ // (`user.set-role`, `user.ban` or `user.impersonate`) and refused by the provider without it. None is
3
+ // drawn on the viewer's own row, and on a staff member's, a person holding a backoffice role, only for a
4
+ // super admin; the server refuses both whatever is drawn. An action that lands asks the page behind
5
+ // it to be read again through `mithaq:refresh`, so the page keeps its scroll.
6
+ import { useState, type ReactNode } from 'react'
7
+ import { holds, type CellProps, type MenuProps, type Row } from 'mithaq/admin'
8
+ import { useAccess, useLocale, useRecord, useUser } from 'mithaq/admin/react'
9
+ import { backofficeRoles } from '@asas/mithaq/auth/backoffice-roles'
10
+ import { authArm } from '@/api.ts'
11
+ import { toast } from 'mithaq-admin/toast.tsx'
12
+ import {
13
+ AlertDialog,
14
+ AlertDialogAction,
15
+ AlertDialogCancel,
16
+ AlertDialogContent,
17
+ AlertDialogDescription,
18
+ AlertDialogFooter,
19
+ AlertDialogHeader,
20
+ AlertDialogTitle,
21
+ } from '@/components/ui/alert-dialog'
22
+ import { Badge } from '@/components/ui/badge'
23
+ import { Button } from '@/components/ui/button'
24
+ import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
25
+ import { Input } from '@/components/ui/input'
26
+ import { Label } from '@/components/ui/label'
27
+
28
+ /** The names `set-role` accepts: the backoffice roles and the plugin's own `user`, which is none of them. */
29
+ const roleNames = ['user', ...Object.keys(backofficeRoles)] as const
30
+
31
+ /** How long a ban lasts, in the seconds the plugin's `banExpiresIn` takes, or until it is lifted. */
32
+ const banEnds = [
33
+ ['', 'backoffice.banUntilLifted', 'Until it is lifted'],
34
+ ['86400', 'backoffice.banDay', 'One day'],
35
+ ['604800', 'backoffice.banWeek', 'Seven days'],
36
+ ['2592000', 'backoffice.banMonth', 'Thirty days'],
37
+ ] as const
38
+
39
+ type Action = 'set-role' | 'ban' | 'unban' | 'impersonate'
40
+
41
+ /** What a native choice looks like beside the underline `Input`: no box, no corners, the line under it. */
42
+ const choiceClass =
43
+ 'h-10 w-full rounded-none border border-transparent border-b-input bg-transparent px-0 text-sm outline-none focus-visible:border-b-ring [&>option]:bg-popover [&>option]:text-popover-foreground'
44
+
45
+ const refusalsByCode: Record<string, [key: string, fallback: string]> = {
46
+ BANNED_USER: ['backoffice.refusedBanned', 'This person is banned, so they cannot be impersonated.'],
47
+ }
48
+
49
+ const useBackofficeRoleLabel = () => {
50
+ const { locale, t } = useLocale()
51
+ return (one: string) =>
52
+ Object.hasOwn(backofficeRoles, one) ? backofficeRoles[one as keyof typeof backofficeRoles].label[locale === 'ar' ? 'ar' : 'en'] : t('backoffice.noRole', 'No backoffice role')
53
+ }
54
+
55
+ /** A person's backoffice roles as badges, each by its label, which a record page draws where the column holds the slugs. */
56
+ export function BackofficeRole({ value }: Pick<CellProps, 'value'>) {
57
+ const labelOf = useBackofficeRoleLabel()
58
+ const role = typeof value === 'string' && value ? value : 'user'
59
+ return (
60
+ <span className="inline-flex flex-wrap gap-1">
61
+ {role.split(',').map((one) => (
62
+ <Badge key={one} variant="secondary">
63
+ {labelOf(one.trim())}
64
+ </Badge>
65
+ ))}
66
+ </span>
67
+ )
68
+ }
69
+
70
+ const usePerson = (row: Row) => {
71
+ const access = useAccess()
72
+ const viewer = useUser()
73
+ const role = typeof row.role === 'string' && row.role ? row.role : 'user'
74
+ const own = typeof row.email === 'string' && row.email === viewer.email
75
+ const staff = role.split(',').some((one) => Object.hasOwn(backofficeRoles, one.trim()))
76
+ // None on the viewer's own row, and on a staff member's only for a super
77
+ // admin; the server refuses both whatever is drawn.
78
+ const acts = !own && (!staff || access.permissions === '*')
79
+ const actions: Action[] = [
80
+ ...(acts && holds(access, 'user.set-role') ? ['set-role' as const] : []),
81
+ ...(acts && holds(access, 'user.ban') ? [row.banned === true ? ('unban' as const) : ('ban' as const)] : []),
82
+ // A banned account is refused a session, so impersonating it cannot work.
83
+ ...(acts && row.banned !== true && holds(access, 'user.impersonate') ? ['impersonate' as const] : []),
84
+ ]
85
+ return { role, actions }
86
+ }
87
+
88
+ const useNames = () => {
89
+ const { t } = useLocale()
90
+ return { 'set-role': t('backoffice.setRole', 'Set role'), ban: t('backoffice.ban', 'Ban'), unban: t('backoffice.unban', 'Unban'), impersonate: t('backoffice.impersonate', 'Impersonate') }
91
+ }
92
+
93
+ function PersonDialog({ row, action, onClose }: { row: Row; action: Action; onClose: () => void }) {
94
+ const { t } = useLocale()
95
+ const names = useNames()
96
+ const labelOf = useBackofficeRoleLabel()
97
+ const { role } = usePerson(row)
98
+ const [chosen, setChosen] = useState(role)
99
+ const [reason, setReason] = useState('')
100
+ const [ends, setEnds] = useState('')
101
+ const [refusal, setRefusal] = useState<string>()
102
+ const [busy, setBusy] = useState(false)
103
+ const userId = String(row.id ?? '')
104
+ const name = String(row.name || row.email || '')
105
+ const send = async () => {
106
+ const [path, body] = {
107
+ 'set-role': ['admin/set-role', { userId, role: chosen }],
108
+ ban: ['admin/ban-user', { userId, banReason: reason.trim(), ...(ends ? { banExpiresIn: Number(ends) } : {}) }],
109
+ unban: ['admin/unban-user', { userId }],
110
+ impersonate: ['admin/impersonate-user', { userId }],
111
+ }[action] as [string, Record<string, unknown>]
112
+ setBusy(true)
113
+ const answered = await authArm(path, body).finally(() => setBusy(false))
114
+ // The provider's own sentence is English alone, so the dialog says the
115
+ // refusal in the viewer's locale; the detail stays in the answer's body.
116
+ if (!answered.ok) {
117
+ const known = refusalsByCode[String(answered.body?.code)]
118
+ return setRefusal(known ? t(known[0], known[1]) : t('backoffice.refused', 'The backoffice refused that action.'))
119
+ }
120
+ // The backoffice answers a one-time link into the tenant app, which trades it for the impersonated
121
+ // session under its own cookie.
122
+ if (action === 'impersonate') {
123
+ onClose()
124
+ return window.location.assign(String(answered.body?.url))
125
+ }
126
+ window.dispatchEvent(new Event('mithaq:refresh'))
127
+ toast.success(landed[action])
128
+ onClose()
129
+ }
130
+ // Functions, so a name holding `$&` is put in as written and never read as a pattern.
131
+ const fill = (text: string | undefined) => (text ?? '').replace('{name}', () => name).replace('{role}', () => labelOf(chosen) ?? chosen)
132
+ const said = {
133
+ // Asked only once another role is picked: the dialog opens on the role
134
+ // the person holds, which there is nothing to give.
135
+ 'set-role': fill(chosen === role ? t('backoffice.pickRole', 'Pick another backoffice role for {name}.') : t('backoffice.confirmRole', 'Give {name} the backoffice role "{role}"?')),
136
+ ban: fill(t('backoffice.confirmBan', 'Ban {name}? They are signed out of both apps and cannot sign in until the ban ends.')),
137
+ unban: fill(t('backoffice.confirmUnban', 'Lift the ban on {name}? They can sign in again.')),
138
+ impersonate: fill(t('backoffice.confirmImpersonate', 'Impersonate {name}? You act as them in the tenant app until you stop.')),
139
+ }[action]
140
+ const landed = {
141
+ 'set-role': fill(t('backoffice.roleSet', '{name} now holds the backoffice role "{role}".')),
142
+ ban: fill(t('backoffice.banned', '{name} is banned.')),
143
+ unban: fill(t('backoffice.unbanned', 'The ban on {name} is lifted.')),
144
+ }
145
+ return (
146
+ <AlertDialog open onOpenChange={(open) => !open && onClose()}>
147
+ <AlertDialogContent data-person={userId}>
148
+ <AlertDialogHeader>
149
+ <AlertDialogTitle>{names[action]}</AlertDialogTitle>
150
+ <AlertDialogDescription>{said}</AlertDialogDescription>
151
+ </AlertDialogHeader>
152
+ {action === 'set-role' ? (
153
+ <div className="grid gap-2">
154
+ <Label htmlFor={`role-${userId}`}>{t('backoffice.role', 'Backoffice role')}</Label>
155
+ <select
156
+ id={`role-${userId}`}
157
+ data-role="chosen"
158
+ className={choiceClass}
159
+ value={chosen}
160
+ onChange={(event) => setChosen(event.target.value)}
161
+ >
162
+ {roleNames.map((one) => (
163
+ <option key={one} value={one}>
164
+ {labelOf(one)}
165
+ </option>
166
+ ))}
167
+ </select>
168
+ </div>
169
+ ) : null}
170
+ {action === 'ban' ? (
171
+ <div className="grid gap-3">
172
+ <div className="grid gap-2">
173
+ <Label htmlFor={`ban-reason-${userId}`}>{t('backoffice.banReason', 'Reason')}</Label>
174
+ {/* Required, and said so while it is empty, since Ban stays off until a reason is given. */}
175
+ <Input id={`ban-reason-${userId}`} data-ban="reason" required aria-required maxLength={200} aria-describedby={reason.trim() ? undefined : `ban-reason-${userId}-needed`} value={reason} onChange={(event) => setReason(event.target.value)} />
176
+ {reason.trim() ? null : (
177
+ <p id={`ban-reason-${userId}-needed`} className="text-muted-foreground text-sm">
178
+ {t('backoffice.banReasonNeeded', 'A ban needs a reason.')}
179
+ </p>
180
+ )}
181
+ </div>
182
+ <div className="grid gap-2">
183
+ <Label htmlFor={`ban-ends-${userId}`}>{t('backoffice.banEnds', 'Ban lasts')}</Label>
184
+ <select
185
+ id={`ban-ends-${userId}`}
186
+ data-ban="ends"
187
+ className={choiceClass}
188
+ value={ends}
189
+ onChange={(event) => setEnds(event.target.value)}
190
+ >
191
+ {banEnds.map(([seconds, key, fallback]) => (
192
+ <option key={seconds} value={seconds}>
193
+ {t(key, fallback)}
194
+ </option>
195
+ ))}
196
+ </select>
197
+ </div>
198
+ </div>
199
+ ) : null}
200
+ {refusal ? (
201
+ <p role="alert" className="text-sm text-destructive">
202
+ {refusal}
203
+ </p>
204
+ ) : null}
205
+ <AlertDialogFooter className="max-sm:[&_button]:h-11">
206
+ <AlertDialogCancel data-button="close">{t('admin.cancel', 'Cancel')}</AlertDialogCancel>
207
+ <AlertDialogAction
208
+ data-button="confirm"
209
+ variant={action === 'ban' ? 'destructive' : 'default'}
210
+ disabled={busy || (action === 'ban' && !reason.trim()) || (action === 'set-role' && chosen === role)}
211
+ onClick={() => void send()}
212
+ >
213
+ {names[action]}
214
+ </AlertDialogAction>
215
+ </AlertDialogFooter>
216
+ </AlertDialogContent>
217
+ </AlertDialog>
218
+ )
219
+ }
220
+
221
+ /** The actions in a row's menu of the users list, after the list's own View; each opens its dialog below the table, which outlives the menu. */
222
+ export function UserMenu({ row, open }: MenuProps<ReactNode>) {
223
+ const names = useNames()
224
+ const { actions } = usePerson(row)
225
+ return actions.map((action) => (
226
+ <DropdownMenuItem key={action} data-action={action} variant={action === 'ban' ? 'destructive' : 'default'} onClick={() => open(<PersonDialog row={row} action={action} onClose={() => open(null)} />)}>
227
+ {names[action]}
228
+ </DropdownMenuItem>
229
+ ))
230
+ }
231
+
232
+ /** The same actions as buttons on the person's own page, which a row opens. */
233
+ export function UserButtons() {
234
+ const row = useRecord()
235
+ const names = useNames()
236
+ const { actions } = usePerson(row ?? {})
237
+ const [asking, setAsking] = useState<Action>()
238
+ if (!row || !actions.length) return null
239
+ return (
240
+ <div data-person-actions={actions.join(' ')} className="flex flex-wrap gap-2">
241
+ {actions.map((action) => (
242
+ <Button key={action} type="button" size="sm" variant={action === 'ban' ? 'destructive' : 'outline'} data-action={action} onClick={() => setAsking(action)}>
243
+ {names[action]}
244
+ </Button>
245
+ ))}
246
+ {asking ? <PersonDialog row={row} action={asking} onClose={() => setAsking(undefined)} /> : null}
247
+ </div>
248
+ )
249
+ }