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,11 @@
1
+ // Written by `mithaq migrate`. Commit it.
2
+ import { fileURLToPath } from 'node:url'
3
+ import { asas } from '../asas.ts'
4
+
5
+ export default {
6
+ dialect: 'postgresql',
7
+ schema: fileURLToPath(new URL('schema.ts', import.meta.url)),
8
+ out: fileURLToPath(new URL('.', import.meta.url)),
9
+ dbCredentials: { url: asas.targets.drizzle.connectionString },
10
+ migrations: { schema: 'drizzle', table: '__mithaq_migrations' },
11
+ }
@@ -0,0 +1,30 @@
1
+ // Written by `mithaq migrate`. Commit it.
2
+ // Every table the language built, named one per line. drizzle-kit reads named
3
+ // exports alone: an object export contributes no table and reports no changes
4
+ // forever, and a partial export still emits foreign keys to the tables it left
5
+ // out. `mithaq migrate` rewrites this file when a kayan is added or removed.
6
+ import { asas } from '../asas.ts'
7
+
8
+ export const {
9
+ user,
10
+ session,
11
+ account,
12
+ verification,
13
+ organization,
14
+ organizationRole,
15
+ member,
16
+ invitation,
17
+ localUsers,
18
+ roles,
19
+ memberRoles,
20
+ tags,
21
+ media,
22
+ courses,
23
+ sessions,
24
+ enrollments,
25
+ auditLog,
26
+ backofficeLog,
27
+ settings,
28
+ courses_tags,
29
+ _courses_versions,
30
+ } = asas.targets.drizzle.tables
@@ -0,0 +1,4 @@
1
+ // The `mithaq` command runs from this package and reads the language from
2
+ // `src/mithaq.ts` beside `src/asas.ts`: the lock, the generators and `migrate`
3
+ // read these four, wherever the language's own package sits.
4
+ export { defineKayan, generate, locales, targets } from '@asas/mithaq'
@@ -0,0 +1,3 @@
1
+ // The organization mode and who may sign up, for the apps' pages, which import the asas alone. It
2
+ // imports nothing of the provider, so a page bundles a few constants and no server.
3
+ export { defaultLocale, nameIn, organizations, showOrganization, signUp, verifyEmail } from '@asas/mithaq/auth/organizations'
@@ -0,0 +1,427 @@
1
+ // Fills a database with a working organization's worth of rows, so the dashboard
2
+ // can be tried with something in it.
3
+ //
4
+ // DATABASE_URL=postgres://... npm run seed
5
+ //
6
+ // It fills the organization named SEED_ORGANIZATION that the user signing in as
7
+ // SEED_EMAIL owns. SEED_EMAIL unset is the first user to sign up, and
8
+ // SEED_ORGANIZATION unset the first organization that user owns. A development
9
+ // tool only: nothing in production reads it.
10
+ //
11
+ // Every row is written through the built asas's own operations, as that
12
+ // organization's owner, so access, validation, option lists and the
13
+ // organization the runner fills apply exactly as they do to a person in the dashboard.
14
+ // Running it twice adds nothing: each row is looked up by a key that does not
15
+ // change between runs and is skipped where it is already there.
16
+ import { createHmac } from 'node:crypto'
17
+ import { organizations as mode } from '@asas/mithaq/auth/organizations'
18
+ import { databaseOf } from './local-database.ts'
19
+ // add-on: courses
20
+ import type { RichTextNode, RichTextValue } from 'mithaq'
21
+ // end add-on: courses
22
+
23
+ function refuse(reason: string): never {
24
+ console.error(`Refusing to seed: ${reason}`)
25
+ process.exit(1)
26
+ }
27
+
28
+ // The language falls back to the local development database when no URL is set,
29
+ // which is right for the dev server and wrong here: a seed writes, so it runs
30
+ // only against a database someone named. Checked before the build is imported,
31
+ // since the import is what reads it.
32
+ if (!process.env.DATABASE_URL) refuse('set DATABASE_URL to the database to fill. Nothing is seeded by default.')
33
+ if (process.env.NODE_ENV === 'production') refuse('NODE_ENV is production, and the seed fills a development database only.')
34
+ const { SEED_EMAIL, SEED_ORGANIZATION } = process.env
35
+ // Without a name the seed picks whoever signed up first, which on a shared
36
+ // database is someone's real organization. So it fills only this machine's
37
+ // database by itself, and another only when told whose and told twice.
38
+ const at = databaseOf(process.env.DATABASE_URL)
39
+ if (!at) refuse('DATABASE_URL is not a connection string pg reads.')
40
+ const { database, local } = at
41
+ if (!local) {
42
+ if (!SEED_EMAIL) refuse(`DATABASE_URL names ${database}, which is not this machine. Name the owner whose organization to fill there with SEED_EMAIL, and set SEED_ALLOW_REMOTE=1.`)
43
+ if (process.env.SEED_ALLOW_REMOTE !== '1') {
44
+ refuse(`DATABASE_URL names ${database}, which is not this machine. Set SEED_ALLOW_REMOTE=1 to fill ${SEED_ORGANIZATION ? `the organization "${SEED_ORGANIZATION}"` : 'the first organization'} that ${SEED_EMAIL} owns there.`)
45
+ }
46
+ }
47
+
48
+ const { asas } = await import('./asas.ts')
49
+
50
+ /** A generator of the same numbers on every run, so the plan a second run makes is the plan the first one wrote. */
51
+ const seeded = (state: number) => () => {
52
+ state = (state + 0x6d2b79f5) | 0
53
+ let t = Math.imul(state ^ (state >>> 15), 1 | state)
54
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
55
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
56
+ }
57
+ const random = seeded(20260927)
58
+ const pick = <T>(list: readonly T[]) => list[Math.floor(random() * list.length)]!
59
+
60
+ const DAY = 86_400_000
61
+ const now = Date.now()
62
+
63
+ // add-on: courses
64
+ /** One entry per course: the title it is found by on a later run, and a summary in the other locale. */
65
+ const catalogue = [
66
+ ['Introduction to Arabic calligraphy', 'مدخل إلى الخط العربي بأدواته وأساسياته', ['script']],
67
+ ['Naskh script from first strokes', 'خط النسخ من الحروف المفردة إلى الكلمات', ['script']],
68
+ ['Thuluth for advanced students', 'خط الثلث للمتقدمين وتركيب اللوحات', ['script']],
69
+ ['Diwani and decorative lettering', 'الخط الديواني والكتابة الزخرفية', ['script']],
70
+ ['Arabic grammar essentials', 'أساسيات النحو العربي للمبتدئين', ['grammar']],
71
+ ['Morphology and verb patterns', 'علم الصرف وأوزان الأفعال', ['grammar']],
72
+ ['Rhetoric in classical texts', 'البلاغة في النصوص الكلاسيكية', ['grammar']],
73
+ ['Reading classical poetry', 'قراءة الشعر العربي القديم وتذوقه', ['grammar', 'recitation']],
74
+ ['Tajweed rules for beginners', 'أحكام التجويد للمبتدئين', ['recitation']],
75
+ ['Quran recitation practice circle', 'حلقة تدريب على التلاوة', ['recitation']],
76
+ ['Makharij al-huruf in depth', 'مخارج الحروف وصفاتها بالتفصيل', ['recitation']],
77
+ ['Memorisation techniques', 'أساليب الحفظ والمراجعة', ['recitation']],
78
+ ['Modern Standard Arabic conversation', 'المحادثة بالعربية الفصحى المعاصرة', ['grammar']],
79
+ ['Arabic for business correspondence', 'العربية في المراسلات التجارية', ['grammar', 'script']],
80
+ ['Children\'s handwriting workshop', 'ورشة تحسين الخط للأطفال', ['script']],
81
+ ['Illumination and manuscript art', 'فن التذهيب وزخرفة المخطوطات', ['script']],
82
+ ['Prosody and poetic metres', 'علم العروض وبحور الشعر', ['grammar', 'recitation']],
83
+ ['Dictation and spelling', 'الإملاء وقواعد الكتابة', ['grammar', 'script']],
84
+ ['Public speaking in Arabic', 'فن الخطابة والإلقاء بالعربية', ['recitation']],
85
+ ['Teaching Arabic to non-native speakers', 'تعليم العربية للناطقين بغيرها', ['grammar']],
86
+ ] as const
87
+
88
+ /** Lexical's editor state holding these blocks, the shape a `richText` field stores. */
89
+ const richText = (...children: RichTextNode[]): RichTextValue => ({ root: { type: 'root', version: 1, direction: null, format: '', indent: 0, children } })
90
+ const words = (text: string, format = 0): RichTextNode => ({ type: 'text', version: 1, text, format, detail: 0, mode: 'normal', style: '' })
91
+ const block = (type: string, children: RichTextNode[], more: Record<string, unknown> = {}): RichTextNode => ({ type, version: 1, direction: null, format: '', indent: 0, children, ...more })
92
+ const courseDetails = (title: string, summary: string, topics: readonly string[]) =>
93
+ richText(
94
+ block('heading', [words(title)], { tag: 'h2' }),
95
+ block('paragraph', [words(summary)], { textFormat: 0, textStyle: '' }),
96
+ block('paragraph', [words('Covers ', 0), words(topics.join(', '), 1), words(' · يغطي: ', 0), words(topics.map((topic) => topicNames[topic] ?? topic).join('، '), 1)], { textFormat: 0, textStyle: '' }),
97
+ block('list', [block('listitem', [words('Bring a notebook · أحضر دفترا')], { value: 1 }), block('listitem', [words('Arrive ten minutes early · احضر قبل الموعد بعشر دقائق')], { value: 2 })], { listType: 'bullet', start: 1, tag: 'ul' }),
98
+ )
99
+ const topicNames: Record<string, string> = { script: 'الخط', grammar: 'النحو', recitation: 'التلاوة' }
100
+ const sessionDetails = (format: 'workshop' | 'lecture') =>
101
+ richText(block('paragraph', [words(format === 'workshop' ? 'A hands-on workshop.' : 'A lecture with questions at the end.')], { textFormat: 0, textStyle: '' }), block('paragraph', [words(format === 'workshop' ? 'ورشة عمل تطبيقية.' : 'محاضرة تتبعها أسئلة.')], { textFormat: 0, textStyle: '' }))
102
+ // end add-on: courses
103
+
104
+ /** A tag's name is copy a screen shows, so it is one string per locale, and a later run finds it by its English. */
105
+ const tagNames = [
106
+ { en: 'Beginner friendly', ar: 'مناسب للمبتدئين' },
107
+ { en: 'Weekend', ar: 'عطلة نهاية الأسبوع' },
108
+ { en: 'Online', ar: 'عبر الإنترنت' },
109
+ { en: 'Certificate', ar: 'شهادة' },
110
+ { en: 'Intensive', ar: 'مكثف' },
111
+ { en: 'Children', ar: 'أطفال' },
112
+ ]
113
+
114
+ // add-on: roles
115
+ /** Three custom roles, each labelled in every locale and holding keys the build's own catalog lists. */
116
+ const customRoles = {
117
+ teacher: {
118
+ label: { en: 'Teacher', ar: 'معلم' },
119
+ holds: (key: string) => /^(sessions|enrollments)\./.test(key) || /^courses\.(find|findByID|count|fields\.)/.test(key) || key === 'screen.dashboard',
120
+ },
121
+ accountant: {
122
+ label: { en: 'Accountant', ar: 'محاسب' },
123
+ // Every seat of the organization, not only its own.
124
+ holds: (key: string) => /^(courses|enrollments|sessions)\.(find|findByID|count|fields\.)/.test(key) || key === 'enrollments.others' || key === 'screen.dashboard',
125
+ },
126
+ receptionist: {
127
+ label: { en: 'Receptionist', ar: 'موظف استقبال' },
128
+ holds: (key: string) => /^enrollments\./.test(key) || /^(courses|sessions)\.(find|findByID|count|fields\.)/.test(key) || key === 'screen.dashboard',
129
+ },
130
+ }
131
+ // end add-on: roles
132
+
133
+ // The owner and the organization, read through the provider's own adapter:
134
+ // the generated auth kayan answer no operation to anyone, so this is how a
135
+ // script asks who someone is.
136
+ const server = (asas.auth.strategies.find((strategy) => strategy.name === 'user') as unknown as { server: { $context: Promise<AuthContext> } }).server
137
+ type Row = Record<string, unknown> & { id: string }
138
+ type AuthContext = {
139
+ secret: string
140
+ authCookies: { sessionToken: { name: string } }
141
+ adapter: {
142
+ findMany(query: { model: string; where?: { field: string; value: string }[]; limit?: number; sortBy?: { field: string; direction: 'asc' } }): Promise<Row[]>
143
+ create(query: { model: string; data: Record<string, unknown> }): Promise<Row>
144
+ }
145
+ internalAdapter: {
146
+ findUserByEmail(email: string): Promise<{ user: Row } | null>
147
+ createSession(userId: string, dontRememberMe?: boolean, override?: Record<string, unknown>): Promise<{ token: string }>
148
+ deleteSession(token: string): Promise<void>
149
+ }
150
+ }
151
+ const auth = await server.$context
152
+
153
+ const oldestFirst = { field: 'createdAt', direction: 'asc' } as const
154
+ const owner = SEED_EMAIL
155
+ ? (await auth.internalAdapter.findUserByEmail(SEED_EMAIL))?.user
156
+ : (await auth.adapter.findMany({ model: 'user', limit: 1, sortBy: oldestFirst }))[0]
157
+ // Where the first owner comes from: the first sign-up with many organizations, first-run otherwise.
158
+ const makeOne = mode === 'many' ? 'Sign up in the tenant app and create an organization first.' : 'Set the app up on the tenant app\'s /auth/first-run first.'
159
+ if (!owner) refuse(SEED_EMAIL ? `no user signs in as ${SEED_EMAIL}.` : `the database holds no user yet. ${makeOne}`)
160
+ const owned = (await auth.adapter.findMany({ model: 'member', where: [{ field: 'userId', value: owner.id }], sortBy: oldestFirst }))
161
+ .filter((member) => String(member.role).split(',').includes('owner'))
162
+ .map((member) => String(member.organizationId))
163
+ // A name is not unique, so the organization is the one of that name the owner
164
+ // owns, and there has to be exactly one.
165
+ const organizations = (await auth.adapter.findMany({ model: 'organization', where: [SEED_ORGANIZATION ? { field: 'name', value: SEED_ORGANIZATION } : { field: 'id', value: owned[0] ?? '' }] }))
166
+ .filter((one) => owned.includes(one.id))
167
+ if (organizations.length !== 1) {
168
+ refuse(SEED_ORGANIZATION
169
+ ? `${owner.email} owns ${organizations.length ? 'more than one' : 'no'} organization named "${SEED_ORGANIZATION}".`
170
+ : `${owner.email} owns no organization. ${makeOne}`)
171
+ }
172
+ const organization = organizations[0]!
173
+ console.log(`Seeding the organization "${organization.name}" that ${owner.email} owns, in ${database}.`)
174
+ const members = await auth.adapter.findMany({ model: 'member', where: [{ field: 'organizationId', value: organization.id }] })
175
+
176
+ // A session of the owner's own, in that organization, which the provider
177
+ // signs and reads back like any browser's. It is deleted when the seed ends,
178
+ // so the run leaves no way in behind it. The cookie is signed the way the
179
+ // provider's router signs it.
180
+ const { token } = await auth.internalAdapter.createSession(owner.id, true, { activeOrganizationId: organization.id })
181
+ const signature = createHmac('sha256', auth.secret).update(token).digest('base64')
182
+ const headers = new Headers({ cookie: `${auth.authCookies.sessionToken.name}=${encodeURIComponent(`${token}.${signature}`)}` })
183
+
184
+ const created = {
185
+ tags: 0,
186
+ // add-on: courses
187
+ courses: 0,
188
+ sessions: 0,
189
+ enrollments: 0,
190
+ // end add-on: courses
191
+ // add-on: roles
192
+ roles: 0,
193
+ memberRoles: 0,
194
+ // end add-on: roles
195
+ // add-on: settings
196
+ settings: 0,
197
+ // end add-on: settings
198
+ }
199
+ /** The rows this run brought up to date rather than made. */
200
+ const repaired = {
201
+ tags: 0,
202
+ tagColumns: 0,
203
+ // add-on: courses
204
+ details: 0,
205
+ // end add-on: courses
206
+ // add-on: roles
207
+ roles: 0,
208
+ // end add-on: roles
209
+ }
210
+ const client = (asas.targets.drizzle as unknown as { db: { $client: { query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> } } }).db.$client
211
+
212
+ try {
213
+ // A dev database seeded before a tag's name was localized holds one `name`
214
+ // column where the kayan reads `name_en` and `name_ar`, so every read below
215
+ // would fail. A migrated database gets the change from the committed
216
+ // migration; a dev database is pushed by `connect()` instead, and the push
217
+ // cannot tell a rename from a drop and an add, so it stops on this table.
218
+ // This block makes the change the way the migration does, and a push after it
219
+ // runs clean. `name_ar` starts as a copy of the English because a row the
220
+ // read cannot parse is refused whole; a tag a person made reads in English
221
+ // under Arabic until someone writes its Arabic, which the run reports as
222
+ // `repaired.tagColumns`. The five statements are one query, so postgres runs
223
+ // them as one transaction and a failure leaves the table as it was.
224
+ const columns = new Set((await client.query(`select column_name from information_schema.columns where table_schema = 'public' and table_name = 'tags'`)).rows.map((row) => row.column_name))
225
+ if (columns.has('name') && !columns.has('name_en')) {
226
+ await client.query([
227
+ 'alter table "tags" rename column "name" to "name_en"',
228
+ 'alter table "tags" add column "name_ar" varchar(60)',
229
+ 'update "tags" set "name_ar" = "name_en"',
230
+ 'alter table "tags" alter column "name_ar" set not null',
231
+ 'create index "tags_name_ar_index" on "tags" ("name_ar")',
232
+ ].join('; '))
233
+ repaired.tagColumns = 1
234
+ }
235
+ const actor = await asas.auth.actor(headers)
236
+ if (!actor) throw new Error(`The session made for ${owner.email} did not authenticate.`)
237
+ const options = { actor }
238
+ const { tags } = asas.kayan
239
+ // add-on: courses
240
+ const { courses, sessions, enrollments } = asas.kayan
241
+ // end add-on: courses
242
+ // add-on: roles
243
+ const { roles, memberRoles } = asas.kayan
244
+ // end add-on: roles
245
+
246
+ const tagIds: string[] = []
247
+ for (const name of tagNames) {
248
+ const found = (await tags.operations.find({ where: { 'name.en': { equals: name.en } }, limit: 1 }, options)).docs[0]
249
+ if (!found) {
250
+ created.tags++
251
+ tagIds.push((await tags.operations.create({ name }, options)).id)
252
+ continue
253
+ }
254
+ // A tag made before its name was localized carries no Arabic, and is
255
+ // brought to both here; one that already holds both is left alone.
256
+ if (found.name?.ar !== name.ar) {
257
+ await tags.operations.update({ id: found.id, name }, options)
258
+ repaired.tags++
259
+ }
260
+ tagIds.push(found.id)
261
+ }
262
+
263
+ // add-on: courses
264
+ // What this run made and when each is said to have been made, for the one
265
+ // write below that goes around the operations.
266
+ const backdate: { table: 'courses' | 'enrollments'; id: string; at: Date }[] = []
267
+ const planned: { course: string; age: number; sessions: string[]; archived: boolean }[] = []
268
+ for (const [index, [title, summary, topics]] of catalogue.entries()) {
269
+ // Drawn whether or not the course is there, so every run draws the same plan.
270
+ const age = 89 - index
271
+ const delivery = pick(['live', 'recorded'] as const)
272
+ const input = {
273
+ title,
274
+ summary,
275
+ details: courseDetails(title, summary, topics),
276
+ topics: [...topics],
277
+ fee: pick([0, 150, 250, 300, 450, 600, 900]),
278
+ currency: pick(['SAR', 'USD'] as const),
279
+ level: 1 + Math.floor(random() * 5),
280
+ audience: pick(['beginner', 'advanced'] as const),
281
+ delivery,
282
+ ...(delivery === 'live' && { accessCode: String(100000 + Math.floor(random() * 900000)) }),
283
+ notifications: random() < 0.7,
284
+ opensOn: new Date(now - (age - 7) * DAY).toISOString().slice(0, 10),
285
+ tags: tagIds.filter(() => random() < 0.35),
286
+ }
287
+ const sessionCount = 2 + Math.floor(random() * 5)
288
+ // Two of the twenty are archived, with the reason the archive form asks for.
289
+ const archived = index % 9 === 8
290
+ const found = (await courses.operations.find({ where: { title: { equals: title } }, limit: 1 }, options)).docs[0]
291
+ const course = found ?? (await courses.operations.create(input, options))
292
+ if (!found) {
293
+ created.courses++
294
+ backdate.push({ table: 'courses', id: course.id, at: new Date(now - age * DAY) })
295
+ if (archived) await courses.operations.archive({ id: course.id, reason: 'Replaced by a newer course.' }, options)
296
+ } else if (!found.details) {
297
+ // A course seeded without details is given them, once.
298
+ await courses.operations.update({ id: found.id, details: input.details }, options)
299
+ repaired.details++
300
+ }
301
+ // A course's sessions are keyed by their place in its list, which a later
302
+ // run reads back in the order of their start.
303
+ const held = (await sessions.operations.find({ where: { course: { equals: course.id } }, sort: 'startsAt', limit: 0 }, options)).docs.map((one) => one.id)
304
+ for (let at = 0; at < sessionCount; at++) {
305
+ const session = {
306
+ course: course.id,
307
+ startsAt: new Date(now - (60 - index * 3 - at * 7) * DAY + (random() < 0.5 ? 9 : 18) * 3_600_000 - (now % DAY)),
308
+ capacity: 10 + Math.floor(random() * 21),
309
+ venue: pick(['online', 'campus'] as const),
310
+ format: pick(['workshop', 'lecture'] as const),
311
+ }
312
+ if (held[at]) continue
313
+ held.push((await sessions.operations.create({ ...session, details: sessionDetails(session.format) }, options)).id)
314
+ created.sessions++
315
+ }
316
+ planned.push({ course: course.id, age, sessions: held, archived })
317
+ }
318
+
319
+ // Six students, made once by address, since a seat is held by someone holding
320
+ // the student role and an organization the seed starts from may hold none.
321
+ // They have no password, so nobody signs in as one, and are written past the
322
+ // sign-up hooks, since nobody signed up.
323
+ const students: string[] = []
324
+ for (let at = 1; at <= 6; at++) {
325
+ const email = `seed-student-${at}@example.invalid`
326
+ const user = (await auth.internalAdapter.findUserByEmail(email))?.user ?? await auth.adapter.create({ model: 'user', data: { email, name: `Student ${at}`, emailVerified: true, createdAt: new Date(), updatedAt: new Date() } })
327
+ const id = String(user.id)
328
+ if (!(await auth.adapter.findMany({ model: 'member', where: [{ field: 'organizationId', value: organization.id }, { field: 'userId', value: id }] })).length) {
329
+ await auth.adapter.create({ model: 'member', data: { organizationId: organization.id, userId: id, role: 'member', createdAt: new Date() } })
330
+ }
331
+ if (!(await memberRoles.operations.find({ where: { user: { equals: id }, role: { equals: 'student' } }, limit: 1 }, options)).docs.length) {
332
+ await memberRoles.operations.create({ user: id, role: 'student' }, options)
333
+ created.memberRoles++
334
+ }
335
+ students.push(id)
336
+ }
337
+
338
+ // Sixty enrollments, each on a session of its own drawn from every course's
339
+ // list and taken by the students in turn, so one session and one student are
340
+ // the pair a later run finds it by. Each is said to have been made after its
341
+ // course was, more of them lately than long ago.
342
+ // An archived course's sessions take no new seat, so seats are drawn from the open courses alone.
343
+ const pairs = planned.filter((one) => !one.archived).flatMap((one) => one.sessions.map((session) => ({ session, age: one.age })))
344
+ for (let at = pairs.length - 1; at > 0; at--) {
345
+ const other = Math.floor(random() * (at + 1))
346
+ ;[pairs[at], pairs[other]] = [pairs[other]!, pairs[at]!]
347
+ }
348
+ for (const [index, { session, age }] of pairs.slice(0, 60).entries()) {
349
+ const student = students[index % students.length]!
350
+ const at = new Date(now - Math.floor(random() ** 2 * age) * DAY - random() * DAY)
351
+ const found = (await enrollments.operations.find({ where: { session: { equals: session }, student: { equals: student } }, limit: 1 }, options)).docs[0]
352
+ if (found) continue
353
+ const made = await enrollments.operations.create({ session, student }, options)
354
+ created.enrollments++
355
+ backdate.push({ table: 'enrollments', id: made.id, at })
356
+ }
357
+
358
+ // `createdAt` is the application's, filled in by the create and refused from
359
+ // a caller, so no operation can say a row was made in the past. The
360
+ // dashboard's chart needs a past, so the rows this run just made, and only
361
+ // those, get one here with a plain update through the drizzle target's own
362
+ // connection, keyed on each row's id and this tenant.
363
+ for (const { table, id, at } of backdate) {
364
+ await client.query(`update "${table}" set "created_at" = $1 where "id" = $2 and "organization_id" = $3`, [at, id, organization.id])
365
+ }
366
+ // end add-on: courses
367
+
368
+ // add-on: roles
369
+ // A role already there is brought to the keys it would be made with, so a
370
+ // database seeded earlier is repaired by running the seed again. A row that
371
+ // already holds exactly these keys is left alone, so a second run still
372
+ // writes nothing. A row whose label is its slug is brought to its real label
373
+ // the same way.
374
+ for (const [name, { label, holds }] of Object.entries(customRoles)) {
375
+ const permissions = asas.catalog.filter(holds)
376
+ const [found] = (await roles.operations.find({ where: { name: { equals: name } }, limit: 1 }, options)).docs
377
+ if (!found) {
378
+ await roles.operations.create({ name, label, permissions }, options)
379
+ created.roles++
380
+ } else if (JSON.stringify(found.permissions) !== JSON.stringify(permissions) || found.label?.en !== label.en || found.label?.ar !== label.ar) {
381
+ await roles.operations.update({ id: found.id, label, permissions }, options)
382
+ repaired.roles++
383
+ }
384
+ }
385
+ // Each custom role given to a member in turn, the owner included: the
386
+ // owner's own `administrator` already holds every key, so this widens nothing.
387
+ // A student takes none, so the students a run makes leave a later run's turns
388
+ // where the first run's were.
389
+ const studying = new Set((await memberRoles.operations.find({ where: { role: { equals: 'student' } }, limit: 0 }, options)).docs.map((one) => String(one.user)))
390
+ const staff = members.filter((one) => !studying.has(String(one.userId)))
391
+ const turns = staff.length ? staff : members
392
+ for (const [index, role] of Object.keys(customRoles).entries()) {
393
+ const user = String(turns[index % turns.length]!.userId)
394
+ if ((await memberRoles.operations.find({ where: { user: { equals: user }, role: { equals: role } }, limit: 1 }, options)).docs.length) continue
395
+ await memberRoles.operations.create({ user, role }, options)
396
+ created.memberRoles++
397
+ }
398
+ // end add-on: roles
399
+
400
+ // add-on: settings
401
+ const { settings } = asas.global
402
+ if (!(await settings.operations.findOne({}, options))) {
403
+ await settings.operations.update({ contactEmail: String(owner.email), maintenance: false }, options)
404
+ created.settings++
405
+ }
406
+ // end add-on: settings
407
+
408
+ const totals = {
409
+ tags: (await tags.operations.count({}, options)).totalDocs,
410
+ // add-on: courses
411
+ courses: (await courses.operations.count({}, options)).totalDocs,
412
+ sessions: (await sessions.operations.count({}, options)).totalDocs,
413
+ enrollments: (await enrollments.operations.count({}, options)).totalDocs,
414
+ // end add-on: courses
415
+ // add-on: roles
416
+ roles: (await roles.operations.count({}, options)).totalDocs,
417
+ memberRoles: (await memberRoles.operations.count({}, options)).totalDocs,
418
+ // end add-on: roles
419
+ // add-on: settings
420
+ settings: (await settings.operations.findOne({}, options)) ? 1 : 0,
421
+ // end add-on: settings
422
+ }
423
+ console.log(JSON.stringify({ organization: organization.name, created, repaired, totals }))
424
+ } finally {
425
+ await auth.internalAdapter.deleteSession(token)
426
+ await asas.destroy()
427
+ }
@@ -0,0 +1,63 @@
1
+ // What `/backoffice` itself draws: where staff start, inside the backoffice's
2
+ // shell, with the figure card the dashboard and the portal homes draw for each
3
+ // list the nav offers the viewer, with how many rows of it they may read across
4
+ // every organization and a link to it. A list whose count the viewer may not
5
+ // read, or whose count failed, shows a dash.
6
+ import { ArrowRight } from 'lucide-react'
7
+ import type { LoaderTools, PageProps } from 'mithaq/admin'
8
+ import { useLocale, useScreen } from 'mithaq/admin/react'
9
+ import { BackofficeShell, iconOf } from '@asas/mithaq/ui/app-shell.tsx'
10
+ import { KpiCard } from '@asas/mithaq/ui/kpi-card.tsx'
11
+ import { navLabel } from '@asas/mithaq/ui/screen-nav.tsx'
12
+
13
+ /** How many rows of each kayan the viewer may count, read as the viewer, keyed by its kayan. */
14
+ export const loader = async ({ client, access }: LoaderTools): Promise<Record<string, number | undefined>> =>
15
+ Object.fromEntries(
16
+ await Promise.all(
17
+ Object.entries(access.collections ?? {})
18
+ .filter(([, one]) => one?.operations?.count === true)
19
+ .map(async ([kayan]) => [kayan, await (client({ kayan, operation: 'count', input: {} }) as Promise<{ totalDocs: number }>).then((found) => found.totalDocs).catch(() => undefined)] as const),
20
+ ),
21
+ )
22
+
23
+ export function Home({ loaderData: counts }: PageProps<Record<string, number | undefined>>) {
24
+ const { locale, t } = useLocale()
25
+ const screen = useScreen()
26
+ return (
27
+ <BackofficeShell
28
+ regions={{
29
+ header: <h1>{t('backoffice.title', 'Backoffice')}</h1>,
30
+ main: (
31
+ <section data-dashboard="backoffice" className="@container grid gap-4">
32
+ <p className="text-muted-foreground">{t('backoffice.about', 'Every organization and every person on the backoffice, for its staff.')}</p>
33
+ {/* Cards as tall as their content, the figure and the link: the card's own padding and gap
34
+ are the roomy ones of the backoffice's style, which a count and one link do not need. */}
35
+ <div className="grid grid-cols-2 items-start gap-4 @2xl:grid-cols-3 @5xl:grid-cols-4 [&_[data-slot=card]]:[--card-spacing:--spacing(4)]">
36
+ {screen.nav.flatMap((entry) => {
37
+ if (!entry.kayan) return []
38
+ const Icon = iconOf(entry.path)
39
+ const counted = counts?.[entry.kayan]
40
+ return [
41
+ <KpiCard
42
+ key={entry.kayan}
43
+ kpi={entry.kayan}
44
+ icon={<Icon className="size-4" />}
45
+ title={navLabel({ entry, screen, locale, t })}
46
+ value={counted === undefined ? '—' : new Intl.NumberFormat(locale).format(counted)}
47
+ // Underlined, with an arrow that follows the reading direction, so it reads as a link.
48
+ footer={
49
+ <a href={`${screen.path}${entry.path}`} className="text-foreground inline-flex items-center gap-1 underline underline-offset-4 max-sm:min-h-11">
50
+ {t('portal.open', 'Open the list')}
51
+ <ArrowRight className="size-3.5 rtl:-scale-x-100" aria-hidden="true" />
52
+ </a>
53
+ }
54
+ />,
55
+ ]
56
+ })}
57
+ </div>
58
+ </section>
59
+ ),
60
+ }}
61
+ />
62
+ )
63
+ }