create-mithaq 0.1.1 → 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 (348) hide show
  1. package/LICENSES/openstatus-time-picker.txt +21 -0
  2. package/index.js +457 -63
  3. package/package.json +1 -1
  4. package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
  5. package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
  6. package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
  7. package/template/apps/asas-backoffice/src/components.ts +35 -0
  8. package/template/apps/asas-backoffice/src/first-user.ts +14 -0
  9. package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
  10. package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
  11. package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
  12. package/template/apps/asas-backoffice/src/redirect.ts +1 -0
  13. package/template/apps/{asas-platform → asas-backoffice}/src/request-headers.ts +1 -1
  14. package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
  15. package/template/apps/asas-backoffice/src/router.tsx +18 -0
  16. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
  17. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
  18. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
  19. package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
  20. package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
  21. package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
  22. package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
  23. package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
  24. package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
  25. package/template/apps/{asas-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
  26. package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
  27. package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
  28. package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
  29. package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
  30. package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
  31. package/template/apps/asas-backoffice/src/server.ts +30 -0
  32. package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
  33. package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
  34. package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
  35. package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
  36. package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
  37. package/template/apps/asas-tanstack/package.json +4 -2
  38. package/template/apps/asas-tanstack/src/api.ts +1 -1
  39. package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
  40. package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
  41. package/template/apps/asas-tanstack/src/components.ts +35 -0
  42. package/template/apps/asas-tanstack/src/copy.ts +416 -79
  43. package/template/apps/asas-tanstack/src/first-run.ts +16 -0
  44. package/template/apps/asas-tanstack/src/links.ts +1 -1
  45. package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
  46. package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
  47. package/template/apps/asas-tanstack/src/redirect.ts +20 -1
  48. package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
  49. package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
  50. package/template/apps/asas-tanstack/src/router.tsx +12 -1
  51. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
  52. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
  53. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
  54. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
  55. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
  56. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
  57. package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
  58. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
  59. package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
  60. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
  61. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
  62. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
  63. package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
  64. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
  65. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
  66. package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
  67. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
  68. package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
  69. package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
  70. package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
  71. package/template/apps/asas-tanstack/src/screens.tsx +46 -26
  72. package/template/apps/asas-tanstack/src/server.ts +10 -0
  73. package/template/apps/asas-tanstack/src/slide.ts +1 -1
  74. package/template/apps/asas-tanstack/src/styles.css +7 -3
  75. package/template/apps/asas-tanstack/tsconfig.json +2 -2
  76. package/template/apps/asas-tanstack/vite.config.ts +7 -1
  77. package/template/package.json +3 -3
  78. package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
  79. package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
  80. package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
  81. package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
  82. package/template/templates/asas/.env.example +22 -5
  83. package/template/templates/asas/AGENTS.md +101 -0
  84. package/template/templates/asas/CLAUDE.md +1 -0
  85. package/template/templates/asas/README.md +1056 -0
  86. package/template/templates/asas/package.json +19 -4
  87. package/template/templates/asas/src/asas.ts +81 -56
  88. package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
  89. package/template/templates/asas/src/dev-setup.ts +112 -0
  90. package/template/templates/asas/src/local-database.ts +22 -0
  91. package/template/templates/asas/src/messages/account.json +129 -103
  92. package/template/templates/asas/src/messages/auditLog.json +223 -105
  93. package/template/templates/asas/src/messages/backofficeLog.json +132 -0
  94. package/template/templates/asas/src/messages/courses.json +256 -214
  95. package/template/templates/asas/src/messages/enrollments.json +45 -27
  96. package/template/templates/asas/src/messages/invitation.json +72 -56
  97. package/template/templates/asas/src/messages/localUsers.json +34 -28
  98. package/template/templates/asas/src/messages/media.json +31 -21
  99. package/template/templates/asas/src/messages/member.json +59 -39
  100. package/template/templates/asas/src/messages/memberRoles.json +37 -31
  101. package/template/templates/asas/src/messages/mithaq.json +206 -162
  102. package/template/templates/asas/src/messages/organization.json +63 -41
  103. package/template/templates/asas/src/messages/organizationRole.json +59 -47
  104. package/template/templates/asas/src/messages/roles.json +59 -45
  105. package/template/templates/asas/src/messages/session.json +99 -79
  106. package/template/templates/asas/src/messages/sessions.json +110 -80
  107. package/template/templates/asas/src/messages/settings.json +51 -53
  108. package/template/templates/asas/src/messages/tags.json +31 -21
  109. package/template/templates/asas/src/messages/user.json +109 -87
  110. package/template/templates/asas/src/messages/verification.json +59 -47
  111. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
  112. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
  113. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
  114. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
  115. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
  116. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
  117. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
  118. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
  119. package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
  120. package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
  121. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
  122. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
  123. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
  124. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
  125. package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
  126. package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
  127. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
  128. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
  129. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
  130. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
  131. package/template/templates/asas/src/migrations/schema.ts +1 -0
  132. package/template/templates/asas/src/mithaq.ts +4 -406
  133. package/template/templates/asas/src/organizations.ts +3 -0
  134. package/template/templates/asas/src/seed.ts +147 -45
  135. package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
  136. package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
  137. package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
  138. package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
  139. package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
  140. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
  141. package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
  142. package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
  143. package/template/templates/asas/src/ui-dashboard/members.tsx +637 -438
  144. package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
  145. package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
  146. package/template/templates/asas/src/ui-portal/home.tsx +9 -13
  147. package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
  148. package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
  149. package/template/templates/features/audit-log/index.ts +4 -0
  150. package/template/templates/features/audit-log/package.json +13 -0
  151. package/template/templates/features/auth/index.ts +11 -0
  152. package/template/templates/{asas/src → features/auth}/local-users/local-users.ts +7 -7
  153. package/template/templates/{auth → features/auth}/package.json +2 -3
  154. package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
  155. package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
  156. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
  157. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
  158. package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
  159. package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
  160. package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
  161. package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
  162. package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
  163. package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
  164. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
  165. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
  166. package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
  167. package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
  168. package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
  169. package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
  170. package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
  171. package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
  172. package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
  173. package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
  174. package/template/templates/features/courses/courses/cover-reads.ts +33 -0
  175. package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
  176. package/template/templates/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
  177. package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
  178. package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
  179. package/template/templates/features/courses/domain/archive.ts +39 -0
  180. package/template/templates/features/courses/domain/on-offer.ts +6 -0
  181. package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
  182. package/template/templates/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
  183. package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
  184. package/template/templates/features/courses/index.ts +8 -0
  185. package/template/templates/features/courses/package.json +24 -0
  186. package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
  187. package/template/templates/features/courses/portal/ui/course.tsx +176 -0
  188. package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
  189. package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
  190. package/template/templates/features/courses/portal/ui/reads.ts +76 -0
  191. package/template/templates/features/courses/portal/ui/rules.ts +87 -0
  192. package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
  193. package/template/templates/features/courses/sessions/seats.ts +42 -0
  194. package/template/templates/{asas/src → features/courses}/sessions/sessions.ts +45 -14
  195. package/template/templates/features/media/index.ts +10 -0
  196. package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
  197. package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
  198. package/template/templates/features/media/package.json +19 -0
  199. package/template/templates/features/roles/index.ts +4 -0
  200. package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
  201. package/template/templates/features/roles/package.json +21 -0
  202. package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
  203. package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
  204. package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
  205. package/template/templates/features/settings/index.ts +3 -0
  206. package/template/templates/features/settings/package.json +13 -0
  207. package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
  208. package/template/templates/features/tags/index.ts +3 -0
  209. package/template/templates/features/tags/package.json +14 -0
  210. package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
  211. package/template/templates/mithaq/package.json +33 -0
  212. package/template/templates/mithaq/src/mail/index.ts +32 -0
  213. package/template/templates/mithaq/src/mail/mailers.ts +36 -0
  214. package/template/templates/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
  215. package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
  216. package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
  217. package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
  218. package/template/templates/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
  219. package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
  220. package/template/templates/mithaq/src/mithaq.ts +593 -0
  221. package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
  222. package/template/templates/{asas → mithaq}/src/roles/grants.ts +1 -1
  223. package/template/templates/mithaq/src/roles/member-access.ts +11 -0
  224. package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
  225. package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
  226. package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
  227. package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
  228. package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
  229. package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
  230. package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
  231. package/template/templates/mithaq/src/ui/activity.tsx +527 -0
  232. package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
  233. package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
  234. package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
  235. package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
  236. package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
  237. package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
  238. package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
  239. package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
  240. package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
  241. package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
  242. package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
  243. package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
  244. package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
  245. package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
  246. package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
  247. package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
  248. package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
  249. package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
  250. package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
  251. package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
  252. package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
  253. package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
  254. package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
  255. package/template/apps/asas-platform/src/redirect.ts +0 -1
  256. package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
  257. package/template/apps/asas-platform/src/router.tsx +0 -7
  258. package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
  259. package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
  260. package/template/apps/asas-platform/src/server.ts +0 -20
  261. package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
  262. package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
  263. package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
  264. package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
  265. package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
  266. package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
  267. package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
  268. package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
  269. package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
  270. package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
  271. package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
  272. package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
  273. package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
  274. package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
  275. package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
  276. package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
  277. package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
  278. package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
  279. package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
  280. package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
  281. package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
  282. package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
  283. package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
  284. package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
  285. package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
  286. package/template/templates/asas/src/rich-text-nodes.ts +0 -11
  287. package/template/templates/asas/src/roles.ts +0 -16
  288. package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
  289. package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
  290. package/template/templates/auth/src/index.ts +0 -690
  291. /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
  292. /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
  293. /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
  294. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
  295. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
  296. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
  297. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
  298. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
  299. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
  300. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
  301. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
  302. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
  303. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
  304. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
  305. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
  306. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
  307. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
  308. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
  309. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
  310. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
  311. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
  312. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
  313. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
  314. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
  315. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
  316. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
  317. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
  318. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
  319. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
  320. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
  321. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
  322. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
  323. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
  324. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
  325. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
  326. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
  327. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
  328. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
  329. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
  330. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
  331. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
  332. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
  333. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
  334. /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
  335. /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
  336. /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
  337. /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
  338. /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
  339. /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
  340. /package/template/templates/{asas → mithaq}/components.json +0 -0
  341. /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
  342. /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
  343. /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
  344. /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
  345. /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
  346. /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
  347. /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
  348. /package/template/templates/{asas → mithaq}/src/ui/table-page.tsx +0 -0
@@ -1,26 +1,48 @@
1
1
  // Fills a database with a working organization's worth of rows, so the dashboard
2
2
  // can be tried with something in it.
3
3
  //
4
- // DATABASE_URL=postgres://... bun run seed
4
+ // DATABASE_URL=postgres://... npm run seed
5
5
  //
6
- // Every row is written through the built asas's own operations, as the owner of
7
- // the organization named "Admin org", so access, validation, option lists and
8
- // the organization the runner fills apply exactly as they do to a person in the dashboard.
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.
9
14
  // Running it twice adds nothing: each row is looked up by a key that does not
10
15
  // change between runs and is skipped where it is already there.
11
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
12
20
  import type { RichTextNode, RichTextValue } from 'mithaq'
21
+ // end add-on: courses
13
22
 
14
- const ORGANIZATION = 'Admin org'
15
- const OWNER = 'admin@admin.com'
23
+ function refuse(reason: string): never {
24
+ console.error(`Refusing to seed: ${reason}`)
25
+ process.exit(1)
26
+ }
16
27
 
17
28
  // The language falls back to the local development database when no URL is set,
18
29
  // which is right for the dev server and wrong here: a seed writes, so it runs
19
30
  // only against a database someone named. Checked before the build is imported,
20
31
  // since the import is what reads it.
21
- if (!process.env.DATABASE_URL) {
22
- console.error('Refusing to seed: set DATABASE_URL to the database to fill. Nothing is seeded by default.')
23
- process.exit(1)
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
+ }
24
46
  }
25
47
 
26
48
  const { asas } = await import('./asas.ts')
@@ -38,6 +60,7 @@ const pick = <T>(list: readonly T[]) => list[Math.floor(random() * list.length)]
38
60
  const DAY = 86_400_000
39
61
  const now = Date.now()
40
62
 
63
+ // add-on: courses
41
64
  /** One entry per course: the title it is found by on a later run, and a summary in the other locale. */
42
65
  const catalogue = [
43
66
  ['Introduction to Arabic calligraphy', 'مدخل إلى الخط العربي بأدواته وأساسياته', ['script']],
@@ -76,6 +99,7 @@ const courseDetails = (title: string, summary: string, topics: readonly string[]
76
99
  const topicNames: Record<string, string> = { script: 'الخط', grammar: 'النحو', recitation: 'التلاوة' }
77
100
  const sessionDetails = (format: 'workshop' | 'lecture') =>
78
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
79
103
 
80
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. */
81
105
  const tagNames = [
@@ -87,6 +111,7 @@ const tagNames = [
87
111
  { en: 'Children', ar: 'أطفال' },
88
112
  ]
89
113
 
114
+ // add-on: roles
90
115
  /** Three custom roles, each labelled in every locale and holding keys the build's own catalog lists. */
91
116
  const customRoles = {
92
117
  teacher: {
@@ -103,6 +128,7 @@ const customRoles = {
103
128
  holds: (key: string) => /^enrollments\./.test(key) || /^(courses|sessions)\.(find|findByID|count|fields\.)/.test(key) || key === 'screen.dashboard',
104
129
  },
105
130
  }
131
+ // end add-on: roles
106
132
 
107
133
  // The owner and the organization, read through the provider's own adapter:
108
134
  // the generated auth kayan answer no operation to anyone, so this is how a
@@ -112,7 +138,10 @@ type Row = Record<string, unknown> & { id: string }
112
138
  type AuthContext = {
113
139
  secret: string
114
140
  authCookies: { sessionToken: { name: string } }
115
- adapter: { findMany(query: { model: string; where: { field: string; value: string }[] }): Promise<Row[]> }
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
+ }
116
145
  internalAdapter: {
117
146
  findUserByEmail(email: string): Promise<{ user: Row } | null>
118
147
  createSession(userId: string, dontRememberMe?: boolean, override?: Record<string, unknown>): Promise<{ token: string }>
@@ -121,21 +150,27 @@ type AuthContext = {
121
150
  }
122
151
  const auth = await server.$context
123
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))
124
163
  // A name is not unique, so the organization is the one of that name the owner
125
164
  // owns, and there has to be exactly one.
126
- const owner = (await auth.internalAdapter.findUserByEmail(OWNER))?.user
127
- const owned = owner
128
- ? (await auth.adapter.findMany({ model: 'member', where: [{ field: 'userId', value: owner.id }] }))
129
- .filter((member) => String(member.role).split(',').includes('owner'))
130
- .map((member) => String(member.organizationId))
131
- : []
132
- const organizations = (await auth.adapter.findMany({ model: 'organization', where: [{ field: 'name', value: ORGANIZATION }] }))
165
+ const organizations = (await auth.adapter.findMany({ model: 'organization', where: [SEED_ORGANIZATION ? { field: 'name', value: SEED_ORGANIZATION } : { field: 'id', value: owned[0] ?? '' }] }))
133
166
  .filter((one) => owned.includes(one.id))
134
- if (!owner || organizations.length !== 1) {
135
- console.error(`Refusing to seed: expected one organization named "${ORGANIZATION}" owned by ${OWNER}, found ${organizations.length}.`)
136
- process.exit(1)
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}`)
137
171
  }
138
172
  const organization = organizations[0]!
173
+ console.log(`Seeding the organization "${organization.name}" that ${owner.email} owns, in ${database}.`)
139
174
  const members = await auth.adapter.findMany({ model: 'member', where: [{ field: 'organizationId', value: organization.id }] })
140
175
 
141
176
  // A session of the owner's own, in that organization, which the provider
@@ -146,9 +181,32 @@ const { token } = await auth.internalAdapter.createSession(owner.id, true, { act
146
181
  const signature = createHmac('sha256', auth.secret).update(token).digest('base64')
147
182
  const headers = new Headers({ cookie: `${auth.authCookies.sessionToken.name}=${encodeURIComponent(`${token}.${signature}`)}` })
148
183
 
149
- const created = { tags: 0, courses: 0, sessions: 0, enrollments: 0, roles: 0, memberRoles: 0, settings: 0 }
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
+ }
150
199
  /** The rows this run brought up to date rather than made. */
151
- const repaired = { roles: 0, tags: 0, tagColumns: 0, details: 0 }
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
+ }
152
210
  const client = (asas.targets.drizzle as unknown as { db: { $client: { query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> } } }).db.$client
153
211
 
154
212
  try {
@@ -175,9 +233,15 @@ try {
175
233
  repaired.tagColumns = 1
176
234
  }
177
235
  const actor = await asas.auth.actor(headers)
178
- if (!actor) throw new Error(`The session made for ${OWNER} did not authenticate.`)
236
+ if (!actor) throw new Error(`The session made for ${owner.email} did not authenticate.`)
179
237
  const options = { actor }
180
- const { courses, sessions, enrollments, tags, roles, memberRoles } = asas.kayan
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
181
245
 
182
246
  const tagIds: string[] = []
183
247
  for (const name of tagNames) {
@@ -196,10 +260,11 @@ try {
196
260
  tagIds.push(found.id)
197
261
  }
198
262
 
263
+ // add-on: courses
199
264
  // What this run made and when each is said to have been made, for the one
200
265
  // write below that goes around the operations.
201
266
  const backdate: { table: 'courses' | 'enrollments'; id: string; at: Date }[] = []
202
- const planned: { course: string; age: number; sessions: string[] }[] = []
267
+ const planned: { course: string; age: number; sessions: string[]; archived: boolean }[] = []
203
268
  for (const [index, [title, summary, topics]] of catalogue.entries()) {
204
269
  // Drawn whether or not the course is there, so every run draws the same plan.
205
270
  const age = 89 - index
@@ -218,15 +283,16 @@ try {
218
283
  notifications: random() < 0.7,
219
284
  opensOn: new Date(now - (age - 7) * DAY).toISOString().slice(0, 10),
220
285
  tags: tagIds.filter(() => random() < 0.35),
221
- // Two of the twenty are archived, with the reason the archive form asks for.
222
- ...(index % 9 === 8 && { archived: true, archiveReason: 'Replaced by a newer course.' }),
223
286
  }
224
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
225
290
  const found = (await courses.operations.find({ where: { title: { equals: title } }, limit: 1 }, options)).docs[0]
226
291
  const course = found ?? (await courses.operations.create(input, options))
227
292
  if (!found) {
228
293
  created.courses++
229
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)
230
296
  } else if (!found.details) {
231
297
  // A course seeded without details is given them, once.
232
298
  await courses.operations.update({ id: found.id, details: input.details }, options)
@@ -247,20 +313,40 @@ try {
247
313
  held.push((await sessions.operations.create({ ...session, details: sessionDetails(session.format) }, options)).id)
248
314
  created.sessions++
249
315
  }
250
- planned.push({ course: course.id, age, sessions: held })
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)
251
336
  }
252
337
 
253
338
  // Sixty enrollments, each on a session of its own drawn from every course's
254
- // list and taken by the organization's members in turn, so one session and
255
- // one student are the pair a later run finds it by. Each is said to have
256
- // been made after its course was, more of them lately than long ago.
257
- const pairs = planned.flatMap((one) => one.sessions.map((session) => ({ session, age: one.age })))
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 })))
258
344
  for (let at = pairs.length - 1; at > 0; at--) {
259
345
  const other = Math.floor(random() * (at + 1))
260
346
  ;[pairs[at], pairs[other]] = [pairs[other]!, pairs[at]!]
261
347
  }
262
348
  for (const [index, { session, age }] of pairs.slice(0, 60).entries()) {
263
- const student = String(members[index % members.length]!.userId)
349
+ const student = students[index % students.length]!
264
350
  const at = new Date(now - Math.floor(random() ** 2 * age) * DAY - random() * DAY)
265
351
  const found = (await enrollments.operations.find({ where: { session: { equals: session }, student: { equals: student } }, limit: 1 }, options)).docs[0]
266
352
  if (found) continue
@@ -269,6 +355,17 @@ try {
269
355
  backdate.push({ table: 'enrollments', id: made.id, at })
270
356
  }
271
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
272
369
  // A role already there is brought to the keys it would be made with, so a
273
370
  // database seeded earlier is repaired by running the seed again. A row that
274
371
  // already holds exactly these keys is left alone, so a second run still
@@ -287,38 +384,43 @@ try {
287
384
  }
288
385
  // Each custom role given to a member in turn, the owner included: the
289
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
290
392
  for (const [index, role] of Object.keys(customRoles).entries()) {
291
- const user = String(members[index % members.length]!.userId)
393
+ const user = String(turns[index % turns.length]!.userId)
292
394
  if ((await memberRoles.operations.find({ where: { user: { equals: user }, role: { equals: role } }, limit: 1 }, options)).docs.length) continue
293
395
  await memberRoles.operations.create({ user, role }, options)
294
396
  created.memberRoles++
295
397
  }
398
+ // end add-on: roles
296
399
 
400
+ // add-on: settings
297
401
  const { settings } = asas.global
298
402
  if (!(await settings.operations.findOne({}, options))) {
299
- await settings.operations.update({ siteTitle: `${ORGANIZATION} Academy`, contactEmail: OWNER, maintenance: false }, options)
403
+ await settings.operations.update({ contactEmail: String(owner.email), maintenance: false }, options)
300
404
  created.settings++
301
405
  }
302
-
303
- // `createdAt` is the application's, filled in by the create and refused from
304
- // a caller, so no operation can say a row was made in the past. The
305
- // dashboard's chart needs a past, so the rows this run just made, and only
306
- // those, get one here with a plain update through the drizzle target's own
307
- // connection, keyed on each row's id and this tenant.
308
- for (const { table, id, at } of backdate) {
309
- await client.query(`update "${table}" set "created_at" = $1 where "id" = $2 and "organization_id" = $3`, [at, id, organization.id])
310
- }
406
+ // end add-on: settings
311
407
 
312
408
  const totals = {
313
409
  tags: (await tags.operations.count({}, options)).totalDocs,
410
+ // add-on: courses
314
411
  courses: (await courses.operations.count({}, options)).totalDocs,
315
412
  sessions: (await sessions.operations.count({}, options)).totalDocs,
316
413
  enrollments: (await enrollments.operations.count({}, options)).totalDocs,
414
+ // end add-on: courses
415
+ // add-on: roles
317
416
  roles: (await roles.operations.count({}, options)).totalDocs,
318
417
  memberRoles: (await memberRoles.operations.count({}, options)).totalDocs,
418
+ // end add-on: roles
419
+ // add-on: settings
319
420
  settings: (await settings.operations.findOne({}, options)) ? 1 : 0,
421
+ // end add-on: settings
320
422
  }
321
- console.log(JSON.stringify({ organization: ORGANIZATION, created, repaired, totals }))
423
+ console.log(JSON.stringify({ organization: organization.name, created, repaired, totals }))
322
424
  } finally {
323
425
  await auth.internalAdapter.deleteSession(token)
324
426
  await asas.destroy()
@@ -1,4 +1,4 @@
1
- // What `/platform` itself draws: where staff start, inside the platform's
1
+ // What `/backoffice` itself draws: where staff start, inside the backoffice's
2
2
  // shell, with the figure card the dashboard and the portal homes draw for each
3
3
  // list the nav offers the viewer, with how many rows of it they may read across
4
4
  // every organization and a link to it. A list whose count the viewer may not
@@ -6,9 +6,9 @@
6
6
  import { ArrowRight } from 'lucide-react'
7
7
  import type { LoaderTools, PageProps } from 'mithaq/admin'
8
8
  import { useLocale, useScreen } from 'mithaq/admin/react'
9
- import { PlatformShell, iconOf } from '../ui/app-shell.tsx'
10
- import { KpiCard } from '../ui/kpi-card.tsx'
11
- import { navLabel } from '../ui/screen-nav.tsx'
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
12
 
13
13
  /** How many rows of each kayan the viewer may count, read as the viewer, keyed by its kayan. */
14
14
  export const loader = async ({ client, access }: LoaderTools): Promise<Record<string, number | undefined>> =>
@@ -24,14 +24,14 @@ export function Home({ loaderData: counts }: PageProps<Record<string, number | u
24
24
  const { locale, t } = useLocale()
25
25
  const screen = useScreen()
26
26
  return (
27
- <PlatformShell
27
+ <BackofficeShell
28
28
  regions={{
29
- header: <h1>{t('platform.title', 'Platform')}</h1>,
29
+ header: <h1>{t('backoffice.title', 'Backoffice')}</h1>,
30
30
  main: (
31
- <section data-dashboard="platform" className="@container grid gap-4">
32
- <p className="text-muted-foreground">{t('platform.about', 'Every organization and every person on the platform, for its staff.')}</p>
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
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 platform's style, which a count and one link do not need. */}
34
+ are the roomy ones of the backoffice's style, which a count and one link do not need. */}
35
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
36
  {screen.nav.flatMap((entry) => {
37
37
  if (!entry.kayan) return []
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- // The platform's record page: the shared one under a link back to the list the
1
+ // The backoffice's record page: the shared one under a link back to the list the
2
2
  // record belongs to, so a person's page, an organization's and a role's are never
3
3
  // a dead end. The link is named as the nav names the list, which is already the
4
4
  // viewer's own word for it.
@@ -6,12 +6,12 @@ import type { ReactNode } from 'react'
6
6
  import { ArrowLeft } from 'lucide-react'
7
7
  import type { LayoutProps } from 'mithaq/admin'
8
8
  import { useKayan, useLocale, useScreen } from 'mithaq/admin/react'
9
- import { RecordPage } from '../ui/record-page.tsx'
10
- import { navLabel } from '../ui/screen-nav.tsx'
9
+ import { RecordPage } from '@asas/mithaq/ui/record-page.tsx'
10
+ import { navLabel } from '@asas/mithaq/ui/screen-nav.tsx'
11
11
 
12
12
  export const regions = ['main', 'side', 'danger'] as const
13
13
 
14
- export function PlatformRecordPage(props: LayoutProps<readonly ['main', 'side', 'danger'], ReactNode>) {
14
+ export function BackofficeRecordPage(props: LayoutProps<readonly ['main', 'side', 'danger'], ReactNode>) {
15
15
  const { locale, t } = useLocale()
16
16
  const screen = useScreen()
17
17
  const kayan = useKayan()
@@ -1,13 +1,18 @@
1
- // The two role columns the platform lists, each named in the viewer's locale
1
+ // The two role columns the backoffice lists, each named in the viewer's locale
2
2
  // where the column holds a slug, so an Arabic page reads no English word and
3
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
4
8
  import { use } from 'react'
5
9
  import { useQuery } from '@tanstack/react-query'
6
- import type { CellProps, Row } from 'mithaq/admin'
7
- import { ListContext, useClient, useLocale, useScreen } from 'mithaq/admin/react'
10
+ import type { Row } from 'mithaq/admin'
11
+ import { ListContext, useClient, useScreen } from 'mithaq/admin/react'
8
12
  import type { asas } from '../asas.ts'
9
- import { listed, refusalKeyOf } from '@/components/mithaq/controls.tsx'
10
- import { systemRoles } from '../roles.ts'
13
+ import { refusalKeyOf } from 'mithaq-admin/refusal.ts'
14
+ import { systemRoles } from '@asas/mithaq/roles/system-roles.ts'
15
+ // end add-on: roles
11
16
 
12
17
  /**
13
18
  * A membership's role, which is the provider's own governance role, `owner`,
@@ -16,9 +21,10 @@ import { systemRoles } from '../roles.ts'
16
21
  export function MembershipRole({ value }: CellProps) {
17
22
  const { locale, t } = useLocale()
18
23
  if (typeof value !== 'string' || !value) return null
19
- return listed(value.split(',').map((one) => t(`platform.membership.${one.trim()}`) ?? one.trim()), locale)
24
+ return listed(value.split(',').map((one) => t(`backoffice.membership.${one.trim()}`) ?? one.trim()), locale)
20
25
  }
21
26
 
27
+ // add-on: roles
22
28
  /**
23
29
  * A business role an assignment names by its slug, by its label: a fixed
24
30
  * role's from its code, and a custom role's from its row of the assignment's own tenant, one read for
@@ -50,3 +56,4 @@ export function RoleName({ value, row }: CellProps) {
50
56
  : (custom?.find((one) => one.name === value && one.organization === row.organization)?.label as Record<string, string> | undefined)
51
57
  return label?.[locale] ?? value
52
58
  }
59
+ // end add-on: roles