create-mithaq 0.1.1 → 0.2.1

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 +1094 -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
@@ -0,0 +1,194 @@
1
+ // The portal's frame, its own look apart from the dashboard's: the organization's name at its head, which is the
2
+ // site's name in every locale, the portal's three places as pills across the top from md up and as a tab bar at
3
+ // the foot of a phone, and one column between them. Its colors are the shadcn tokens the primitives read, set
4
+ // here to the portal's teal and amber for each theme, so a Button or a Card inside it is the portal's without a
5
+ // class of its own.
6
+ import '@fontsource-variable/bricolage-grotesque'
7
+ import { use, useState, type ReactNode } from 'react'
8
+ import { BookOpen, ClipboardCheck, House, Wrench, type LucideIcon } from 'lucide-react'
9
+ // add-on: settings
10
+ import { useQuery } from '@tanstack/react-query'
11
+ import type { Row } from 'mithaq/admin'
12
+ import { useClient } from 'mithaq/admin/react'
13
+ // end add-on: settings
14
+ import type { NavEntry, ShellProps } from 'mithaq/admin'
15
+ import { ScreenContext, useAccess, useLocale, useNav, useScreen, useUser } from 'mithaq/admin/react'
16
+ import { holds } from 'mithaq/admin'
17
+ import { currentOf } from 'mithaq-admin/nav-links.tsx'
18
+ import { DiscardDialog } from 'mithaq-admin/discard-dialog.tsx'
19
+ import { SidebarProvider } from '@/components/ui/sidebar'
20
+ import { locales, switchLocale } from '@/copy.ts'
21
+ import { frame, UserMenu } from '@asas/mithaq/ui/app-shell.tsx'
22
+ import { navLabel } from '@asas/mithaq/ui/screen-nav.tsx'
23
+ import { nameIn } from '@asas/mithaq/mithaq/auth/organizations.ts'
24
+ // add-on: settings
25
+ import { closedTo } from './rules.ts'
26
+ // end add-on: settings
27
+
28
+ export const regions = ['header', 'main'] as const
29
+
30
+ /**
31
+ * The portal's colors over shadcn's tokens, light and dark. Teal is the brand, amber its accent; each pair is
32
+ * 4.5:1 or more, text on its ground.
33
+ */
34
+ const tokens = [
35
+ '[--background:#F5F5F0] [--foreground:#1B1F24] [--card:#FFFFFF] [--card-foreground:#1B1F24]',
36
+ '[--primary:#0F6E6E] [--primary-foreground:#FFFFFF] [--ring:#0F6E6E] [--muted-foreground:#555A60]',
37
+ '[--accent:#EAF3F2] [--accent-foreground:#0F6E6E] [--border:#E4E4DC] [--input:#D9D9D0] [--portal-amber:#E8A33D]',
38
+ 'dark:[--background:#0E1414] dark:[--foreground:#ECEFEE] dark:[--card:#172020] dark:[--card-foreground:#ECEFEE]',
39
+ 'dark:[--primary:#5FB8B3] dark:[--primary-foreground:#0B2524] dark:[--ring:#5FB8B3] dark:[--muted-foreground:#A4B0AE]',
40
+ 'dark:[--accent:#16312F] dark:[--accent-foreground:#9CD9D4] dark:[--border:#26302F] dark:[--input:#34403F] dark:[--portal-amber:#E8A33D]',
41
+ ].join(' ')
42
+
43
+ /** The display face of the portal's headings. Its Latin is Bricolage Grotesque; Arabic falls to IBM Plex Sans Arabic. */
44
+ export const display = "font-['Bricolage_Grotesque_Variable','IBM_Plex_Sans_Arabic',sans-serif] font-bold tracking-tight"
45
+
46
+ const icons: Record<string, LucideIcon> = { '/': House, '/courses': BookOpen, '/enrollments': ClipboardCheck }
47
+
48
+ // add-on: settings
49
+ /** The organization's settings as the viewer reads them, or `null` where it may not or none are written yet. */
50
+ const useSettings = () => {
51
+ const client = useClient()
52
+ return useQuery({
53
+ queryKey: ['portal-settings'],
54
+ queryFn: () => (client({ kayan: 'settings', operation: 'findOne', input: {} }) as Promise<Row | null>).catch(() => null),
55
+ staleTime: 60_000,
56
+ }).data
57
+ }
58
+ // end add-on: settings
59
+
60
+ /** One place of the portal: its link, its label, and whether the viewer is on it. */
61
+ const usePlaces = () => {
62
+ const { locale, t } = useLocale()
63
+ const screen = useScreen()
64
+ const access = useAccess()
65
+ const state = use(ScreenContext)
66
+ // A seat of anyone's is no longer the viewer's own, so the list is "My enrollments" only to a student.
67
+ const label = (entry: NavEntry) =>
68
+ entry.kayan === 'enrollments' && !holds(access, 'enrollments.others') ? t('portal.myEnrollments', 'My enrollments')! : navLabel({ entry, screen, locale, t })!
69
+ return [
70
+ { path: '/', href: screen.path, label: t('portal.home', 'Home')!, current: state?.path === screen.path },
71
+ ...useNav().map((entry) => ({ path: entry.path, href: `${screen.path}${entry.path}`, label: label(entry), current: currentOf(entry, state?.kayan?.name) === 'page' })),
72
+ ]
73
+ }
74
+
75
+ /** The other language, offered as its own name, so a reader who cannot read this one still finds theirs. */
76
+ function LocaleButton() {
77
+ const { locale } = useLocale()
78
+ const other = locales.find((one) => one !== locale) ?? locale
79
+ // Asked first, in the app's discard dialog, where a form holds a change nobody saved.
80
+ const [asked, setAsked] = useState(false)
81
+ const { t } = useLocale()
82
+ return (
83
+ <>
84
+ <button
85
+ type="button"
86
+ lang={other}
87
+ aria-label={other === 'ar' ? 'العربية' : 'English'}
88
+ onClick={() => (window.onbeforeunload ? setAsked(true) : switchLocale({ to: other }))}
89
+ className="border-input bg-card text-foreground focus-visible:ring-ring inline-flex h-11 min-w-11 items-center justify-center rounded-full border px-3 text-sm focus-visible:ring-2 focus-visible:outline-none"
90
+ >
91
+ {other === 'ar' ? 'ع' : 'EN'}
92
+ </button>
93
+ <DiscardDialog open={asked} onOpenChange={setAsked} onDiscard={() => switchLocale({ to: other })} t={t} />
94
+ </>
95
+ )
96
+ }
97
+
98
+ /** What a student meets while the portal is closed for maintenance: why, and where to write meanwhile. */
99
+ function Closed({ contact }: { contact?: string }) {
100
+ const { t } = useLocale()
101
+ return (
102
+ <section data-portal-closed="" className="bg-card mx-auto grid max-w-md justify-items-center gap-3 rounded-3xl px-6 py-12 text-center">
103
+ <Wrench aria-hidden className="text-primary size-10" />
104
+ <h1 className={`${display} text-2xl`}>{t('portal.closed', 'Closed for maintenance')}</h1>
105
+ <p className="text-muted-foreground">{t('portal.closedBody', 'The school is working on the portal. Please come back soon.')}</p>
106
+ {contact ? <Contact email={contact} /> : null}
107
+ </section>
108
+ )
109
+ }
110
+
111
+ function Contact({ email }: { email: string }) {
112
+ const { t } = useLocale()
113
+ return (
114
+ <p className="text-muted-foreground text-sm">
115
+ {t('portal.contact', 'Questions? Write to')}{' '}
116
+ <a href={`mailto:${email}`} dir="ltr" className="text-primary font-medium underline-offset-4 hover:underline">{email}</a>
117
+ </p>
118
+ )
119
+ }
120
+
121
+ export function PortalShell({ regions }: ShellProps<readonly ['header', 'main'], ReactNode>) {
122
+ const { locale, t } = useLocale()
123
+ const user = useUser()
124
+ const places = usePlaces()
125
+ // add-on: settings
126
+ const access = useAccess()
127
+ const settings = useSettings()
128
+ // The page answers what the viewer reads either way; this only draws the closed page in its place.
129
+ const closed = closedTo({ maintenance: settings?.maintenance, access })
130
+ const contact = typeof settings?.contactEmail === 'string' && settings.contactEmail ? settings.contactEmail : undefined
131
+ // end add-on: settings
132
+ // without add-on: settings
133
+ // const closed = false
134
+ // const contact: string | undefined = undefined
135
+ // end without add-on: settings
136
+ const active = user.organizations?.find((one) => one.id === user.activeOrganizationId)
137
+ const site = active ? nameIn(active, locale) : t('portal.title', 'Portal')!
138
+ const nav = t('portal.navigation', 'Portal')
139
+ return (
140
+ <SidebarProvider data-shell="top" data-portal="" style={frame} className={`bg-background text-foreground min-h-svh flex-col ${tokens}`}>
141
+ <header className="bg-card border-b">
142
+ <div className="mx-auto flex w-full max-w-6xl items-center justify-between gap-3 px-4 py-3 sm:px-6">
143
+ <a href={places[0]!.href} className="flex min-w-0 items-center gap-2.5 font-semibold">
144
+ <span aria-hidden className="bg-primary text-primary-foreground grid size-9 shrink-0 place-items-center rounded-[10px]">{[...site][0]}</span>
145
+ <span className="truncate">{site}</span>
146
+ </a>
147
+ <nav aria-label={nav} className="hidden gap-1 md:flex">
148
+ {places.map((place) => (
149
+ <a
150
+ key={place.path}
151
+ href={place.href}
152
+ aria-current={place.current ? 'page' : undefined}
153
+ className="text-foreground/80 hover:text-foreground aria-[current=page]:bg-accent aria-[current=page]:text-accent-foreground inline-flex h-11 items-center rounded-full px-4 text-[15px] aria-[current=page]:font-semibold"
154
+ >
155
+ {place.label}
156
+ </a>
157
+ ))}
158
+ </nav>
159
+ <div className="flex shrink-0 items-center gap-2">
160
+ <LocaleButton />
161
+ {/* The avatar alone: the menu signs out and switches locale too. */}
162
+ <div className="w-12 [&_[data-slot=avatar]]:grayscale-0 [&_[data-slot=avatar-fallback]]:bg-(--portal-amber) [&_[data-slot=avatar-fallback]]:text-[#1B1F24]">
163
+ <UserMenu />
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </header>
168
+ <main data-region="main" className="mx-auto grid w-full max-w-6xl grid-cols-[minmax(0,1fr)] content-start gap-6 px-4 pt-6 pb-28 sm:px-6 md:pb-12">
169
+ {closed ? <Closed contact={contact} /> : <><div data-region="header">{regions.header}</div>{regions.main}</>}
170
+ </main>
171
+ {contact && !closed ? (
172
+ <footer className="mx-auto w-full max-w-6xl px-4 pb-28 sm:px-6 md:pb-8">
173
+ <Contact email={contact} />
174
+ </footer>
175
+ ) : null}
176
+ <nav aria-label={nav} className="bg-card fixed inset-x-0 bottom-0 z-10 grid grid-cols-3 border-t px-2 pt-1.5 pb-[max(env(safe-area-inset-bottom),0.75rem)] md:hidden">
177
+ {places.map((place) => {
178
+ const Icon = icons[place.path] ?? BookOpen
179
+ return (
180
+ <a
181
+ key={place.path}
182
+ href={place.href}
183
+ aria-current={place.current ? 'page' : undefined}
184
+ className="text-muted-foreground aria-[current=page]:text-primary flex min-h-13 flex-col items-center justify-center gap-1 text-xs aria-[current=page]:font-semibold"
185
+ >
186
+ <Icon aria-hidden className="size-[22px]" />
187
+ {place.label}
188
+ </a>
189
+ )
190
+ })}
191
+ </nav>
192
+ </SidebarProvider>
193
+ )
194
+ }
@@ -0,0 +1,42 @@
1
+ import { sql } from 'mithaq/drizzle'
2
+ import type { NodePgDatabase } from 'mithaq/drizzle/node-postgres'
3
+ import { serverTargets } from '@asas/mithaq'
4
+
5
+ /** A write's transaction, which is drizzle's here. */
6
+ const txOf = (tx: unknown) => tx as NodePgDatabase
7
+
8
+ /**
9
+ * How many seats of a session are held, a withdrawn one not among them. Counted through the store, past the
10
+ * reader's own access, which a student's narrows to its own seat: a caller reads the session under that access first.
11
+ */
12
+ export const seatsTaken = async ({ session, tx }: { session: string; tx?: unknown }) =>
13
+ (await serverTargets.drizzle.find({
14
+ tx,
15
+ kayan: 'enrollments',
16
+ args: { limit: 1, page: 1, sort: { field: 'id', direction: 'asc' }, where: { and: [{ session: { equals: session } }, { deletedAt: { exists: false } }] } },
17
+ })).totalDocs
18
+
19
+ /**
20
+ * Serializes every write that takes a seat in one session until its transaction ends: a second write waits here,
21
+ * and under Read Committed its count, a statement after the lock, sees what the first one committed, so two
22
+ * writes never both take the last seat.
23
+ */
24
+ export const holdSeats = async ({ tx, session }: { tx: unknown; session: string }) => {
25
+ await txOf(tx).execute(sql`select pg_advisory_xact_lock(hashtext('mithaq.seats'), hashtext(${session}))`)
26
+ }
27
+
28
+ /**
29
+ * What a seat is judged by, read off the columns in the write's transaction rather than through the actor's
30
+ * projection: a role shown no capacity, start or archived mark would otherwise read none and skip each rule. A
31
+ * caller reads the session under the actor's access first, so this reads only a session the actor may reach. Its
32
+ * `id` is the stored one, the same in every case the caller wrote it. Both rows stay locked before the
33
+ * enrollment's reference check, so an archive or capacity change cannot pass the decision, and the later
34
+ * `FOR KEY SHARE` does not require a lock upgrade. `FOR SHARE` lets enrollments share those rows while
35
+ * holding off their changes; `holdSeats` serializes the seat count for a session with a capacity.
36
+ *
37
+ * The tables and columns are written out, as the drizzle target names them; a rename fails R7a, R7b and R7f.
38
+ */
39
+ export const sessionHeld = async ({ tx, session }: { tx: unknown; session: string }) =>
40
+ (await txOf(tx).execute<{ id: string; capacity: number | null; startsAt: Date; archived: boolean | null; courseArchived: boolean | null; selfEnroll: boolean | null }>(sql`
41
+ select s.id, s.capacity::float8 as capacity, s.starts_at as "startsAt", s.archived, c.archived as "courseArchived", c.self_enroll as "selfEnroll"
42
+ from course_sessions s join courses c on c.id = s.course_id where s.id = ${session} for share of s, c`)).rows[0]
@@ -1,13 +1,14 @@
1
1
  import { defaultFields, defaultOperations, type Ctx, type Where } from 'mithaq'
2
- import { defineKayan, organization } from '../mithaq.ts'
3
- import { activity, appPage, danger, platformList, platformRecord } from '../pages.ts'
2
+ import { defineKayan, organization } from '@asas/mithaq'
3
+ import { activity, appPage, danger, backofficeList, backofficeRecord } from '@asas/mithaq/mithaq/pages.ts'
4
4
  import { courses, runsCatalogue } from '../courses/courses.ts'
5
+ import { archivedRow, marks, reason, reasonInput, unarchived } from '../domain/archive.ts'
5
6
 
6
7
  /**
7
- * Writes every matched session `archived: true` and nothing else, whatever the call sent as `data`,
8
- * so the course page's bulk Archive can only archive, never un-archive.
8
+ * Hands each matched session's `archive` no reason and nothing else, whatever the call sent as
9
+ * `data`, so the course page's bulk Archive can only archive, never un-archive.
9
10
  */
10
- const archiveAll = (ctx: Ctx): Ctx => ({ ...ctx, data: { ...ctx.data, data: { archived: true } } })
11
+ const archiveAll = (ctx: Ctx): Ctx => ({ ...ctx, data: { ...ctx.data, data: { reason: '' } } })
11
12
 
12
13
  /**
13
14
  * What a session's create and edit pages both draw, written once so the two cannot drift. The edit
@@ -40,11 +41,11 @@ export const sessions = defineKayan({
40
41
  // one schema is a trap.
41
42
  dbName: 'course_sessions',
42
43
  targets: {
43
- platform: {
44
+ backoffice: {
44
45
  useAsTitle: 'startsAt',
45
46
  pages: {
46
- ...platformList('/sessions', { columns: ['startsAt', 'organization', 'course', 'capacity', 'venue'], filters: ['organization', 'course', 'venue', 'archived'], search: ['venue', 'course.title'], sort: '-startsAt' }),
47
- ...platformRecord({
47
+ ...backofficeList('/sessions', { columns: ['startsAt', 'organization', 'course', 'capacity', 'venue'], filters: ['organization', 'course', 'venue', 'archived'], search: ['venue', 'course.title'], sort: '-startsAt' }),
48
+ ...backofficeRecord({
48
49
  path: '/sessions/:id',
49
50
  main: [{ tabs: [
50
51
  { tab: 'details', label: { en: 'Details', ar: 'التفاصيل' }, fields: [
@@ -95,8 +96,24 @@ export const sessions = defineKayan({
95
96
  columns: ['student', 'createdAt'],
96
97
  }, when: { '$user.collections.enrollments.operations.find': { equals: true } } }] },
97
98
  ] }],
98
- side: [course, activity],
99
- danger: danger(),
99
+ // Whether it is archived and why, read only: a session is archived through its own operations.
100
+ side: [{ ...course, fields: [...course.fields, { field: 'archived', readOnly: true }, { field: 'archiveReason', readOnly: true }] }, activity],
101
+ // Archived and brought back as a course is, each with its reason, before the delete.
102
+ danger: danger({
103
+ button: 'archive',
104
+ when: unarchived,
105
+ label: { en: 'Archive', ar: 'أرشفة' },
106
+ fields: ['reason'],
107
+ reload: true,
108
+ confirm: { title: { en: 'Archive this session?', ar: 'أرشفة هذه الجلسة؟' }, body: { en: 'It stays on the course, marked archived.', ar: 'تبقى في الدورة، معلمة بأنها مؤرشفة.' } },
109
+ }, {
110
+ button: 'unarchive',
111
+ when: archivedRow,
112
+ label: { en: 'Unarchive', ar: 'إلغاء الأرشفة' },
113
+ fields: ['reason'],
114
+ reload: true,
115
+ confirm: { title: { en: 'Unarchive this session?', ar: 'إلغاء أرشفة هذه الجلسة؟' }, body: { en: 'It is open again as it was.', ar: 'تعود مفتوحة كما كانت.' } },
116
+ }),
100
117
  }]),
101
118
  },
102
119
  },
@@ -104,9 +121,19 @@ export const sessions = defineKayan({
104
121
  service: {
105
122
  operations: {
106
123
  ...defaultOperations,
107
- // Runs as `updateMany` does, one `update` per matched row under the same access, and writes
108
- // nothing but the mark.
109
- archiveMany: { steps: ({ steps }) => [archiveAll, steps.updateMany] },
124
+ // One `archive` per matched row under the same access, so the mark is written by an
125
+ // archive alone.
126
+ archiveMany: { each: 'archive', steps: ({ steps }) => [archiveAll, steps.updateMany] },
127
+ archive: {
128
+ targets: { dashboard: { fields: { reason } } },
129
+ input: () => reasonInput('sessions.operations.archive'),
130
+ steps: ({ steps }) => [marks(true), steps.update],
131
+ },
132
+ unarchive: {
133
+ targets: { dashboard: { fields: { reason } } },
134
+ input: () => reasonInput('sessions.operations.unarchive'),
135
+ steps: ({ steps }) => [marks(false), steps.update],
136
+ },
110
137
  },
111
138
  },
112
139
  },
@@ -138,7 +165,8 @@ export const sessions = defineKayan({
138
165
  ],
139
166
  targets: { dashboard: { component: 'combobox' } },
140
167
  },
141
- archived: { kind: 'checkbox', label: { en: 'Archived', ar: 'مؤرشفة' } },
168
+ // Written by `archive` and `unarchive` alone, as a course's is, and `archiveMany` runs an archive per row.
169
+ archived: { kind: 'checkbox', label: { en: 'Archived', ar: 'مؤرشفة' }, rules: { providedBy: { create: 'never', update: 'application' } } },
142
170
  details: { kind: 'richText', label: { en: 'Details', ar: 'التفاصيل' } },
143
171
  /**
144
172
  * The enrollments that point at this session: the one kind that builds no column. `enrollments.session`
@@ -151,5 +179,8 @@ export const sessions = defineKayan({
151
179
  on: 'session',
152
180
  label: { en: 'Enrollments', ar: 'التسجيلات' },
153
181
  },
182
+ // Why it was last archived or brought back. Declared last, so a pushed table and a migrated one
183
+ // hold the column in the same place.
184
+ archiveReason: { kind: 'text', label: { en: 'Archive reason', ar: 'سبب الأرشفة' }, rules: { maxLength: 200, providedBy: { create: 'never', update: 'application' } } },
154
185
  },
155
186
  })
@@ -0,0 +1,10 @@
1
+ import { media, type ImageRead } from './media/media.ts'
2
+
3
+ export type { ImageRead }
4
+
5
+ /**
6
+ * The feature's kayan, built for the reads beside `media.create` that open an
7
+ * image, which each feature whose rows point at images brings and the asas
8
+ * hands in. With none, only a holder of `media.create` reads an image.
9
+ */
10
+ export const kayan = ({ reads }: { reads: readonly ImageRead[] }) => [media({ reads })] as const
@@ -1,50 +1,44 @@
1
- import { actorPermissions, defaultFields, defaultOperations, folderOf, uploadFields, withUploadReference, type Where } from 'mithaq'
2
- import { defineKayan, serverTargets, organization } from '../mithaq.ts'
3
- import { courses } from '../courses/courses.ts'
4
- import { currentOrganization, readsPast } from '../tenant-access.ts'
5
- import { appPage, danger } from '../pages.ts'
6
- import { storageConfig } from '../storage/index.ts'
1
+ import { defaultFields, defaultOperations, folderOf, uploadFields, withUploadReference, type Where } from 'mithaq'
2
+ import { defineKayan, organization } from '@asas/mithaq'
3
+ import { readsPast } from '@asas/mithaq/roles/tenant-access.ts'
4
+ import { appPage, danger } from '@asas/mithaq/mithaq/pages.ts'
5
+ import { storageConfig } from '@asas/mithaq/storage/index.ts'
7
6
 
8
7
  const ui = folderOf(new URL('./ui/', import.meta.url))
9
8
 
9
+ /**
10
+ * One way an actor that holds no `media.create` reads an image: the images it
11
+ * may read, or `false` for none. Never `true`, which would open every image of
12
+ * the organization to whoever the read answered it for.
13
+ */
14
+ export type ImageRead = (args: { user: unknown }) => Promise<Where | false>
15
+
10
16
  /**
11
17
  * The images an actor reads, inside its active organization: every image of
12
- * its organization where it holds `media.create`, and otherwise only the cover
13
- * of a course it reads. `media.create` is the line
14
- * because a role that adds images has to find one before any course names it,
15
- * in the cover's picker. So a student or an instructor reads no image of a
16
- * draft, of an archived course or of none, through find, findByID and
17
- * `GET /api/media/file/<id>`, and the administrator and the registrar read all of them.
18
- *
19
- * "A course it reads" is a published course of the organization the actor's session has
20
- * active, read off the trusted snapshot, under the courses kayan's own read
21
- * access for this actor. The courses are read straight through the store,
22
- * past the runner's scope, so the organization is named here and the read
23
- * never reaches past it whatever the access answers.
24
- * An image has no field that points at a course, so a row filter cannot reach
25
- * one, and the covers are read first and named by id.
18
+ * its organization where it holds `media.create`, and otherwise only those one
19
+ * of `reads` answers. `media.create` is the line because a role that adds
20
+ * images has to find one before anything names it, in an upload field's
21
+ * picker. A file at `GET /api/media/file/<id>` is read through this kayan's
22
+ * own `findByID`, so the same answer decides who downloads it.
26
23
  */
27
- const coversOfReadableCourses = async ({ user }: { user: unknown }) => {
24
+ const imagesReadBy = (reads: readonly ImageRead[]) => async ({ user }: { user: unknown }) => {
28
25
  if (readsPast(user, 'media.create')) return true
29
- const held = actorPermissions(user)
30
- if (held !== true && !held?.has('courses.find')) return false
31
- const active = currentOrganization(user)
32
- const readable = active && (await courses.access!.read!({ user }))
33
- if (!readable) return false
34
- const published: Where = { and: [{ organization: { equals: active } }, { _status: { equals: 'published' } }] }
35
- const found = await serverTargets.drizzle.find({ kayan: 'courses', args: {
36
- limit: 0,
37
- page: 1,
38
- sort: { field: 'id', direction: 'asc' },
39
- where: readable === true ? published : { and: [readable, published] },
40
- } })
41
- const ids = [...new Set(found.docs.map((row) => row.cover).filter((id): id is string => typeof id === 'string'))]
42
- return ids.length ? { id: { in: ids } } : false
26
+ const answered: unknown[] = await Promise.all(reads.map((read) => read({ user })))
27
+ // The type forbids it, but a cast `true` or a filter of nothing would open every image of the
28
+ // organization, so either throws and the actor reads none.
29
+ for (const [at, answer] of answered.entries()) {
30
+ if (answer !== false && (!answer || typeof answer !== 'object' || !Object.keys(answer).length)) {
31
+ throw new Error(`The image read ${reads[at]!.name || `at ${at}`} answered ${JSON.stringify(answer)}, which would open every image; a read answers false or a filter.`)
32
+ }
33
+ }
34
+ const answers = answered.filter((one): one is Where => one !== false)
35
+ if (!answers.length) return false
36
+ return answers.length === 1 ? answers[0]! : { or: answers }
43
37
  }
44
38
 
45
39
  /**
46
40
  * The organization's images: a row per file, whose object is kept where the
47
- * project's storage puts it. A course points at one as its cover.
41
+ * project's storage puts it. An upload field points at one, as a course's cover does.
48
42
  *
49
43
  * It spreads `uploadFields`, the columns the create writes from the upload it
50
44
  * verified and nobody writes after, and declares its own create and delete:
@@ -53,13 +47,13 @@ const coversOfReadableCourses = async ({ user }: { user: unknown }) => {
53
47
  * `uploaded`, and the delete runs its `removed` after the commit. `buildAsas`
54
48
  * refuses a create that skips the configured `uploaded`. A file is
55
49
  * read at `GET /api/media/file/<id>` through this kayan's own `findByID`, so
56
- * its organization and `coversOfReadableCourses` decide who reads it. It keeps no versions, since a file never changes once it is
57
- * made; a course keeps the versions of its cover instead.
50
+ * its organization and `reads` decide who reads it. It keeps no versions, since a file never changes once it is
51
+ * made; a kayan pointing at one keeps the versions of that pointer instead.
58
52
  */
59
- export const media = defineKayan({
53
+ export const media = ({ reads }: { reads: readonly ImageRead[] }) => defineKayan({
60
54
  name: 'media',
61
55
  // ANDed with the inherited rows, so it narrows and never widens.
62
- access: { read: coversOfReadableCourses },
56
+ access: { read: imagesReadBy(reads) },
63
57
  messages: {
64
58
  singular: { en: 'Image', ar: 'صورة' },
65
59
  plural: { en: 'Images', ar: 'الصور' },
@@ -10,8 +10,8 @@ import type { CellProps, ComponentProps, Row } from 'mithaq/admin'
10
10
  import { useAccess, useClient, useLocale, useRecord, useScreen } from 'mithaq/admin/react'
11
11
  import { locales } from '@/copy.ts'
12
12
  import { Button, buttonVariants } from '@/components/ui/button'
13
- import { FileThumb, FileValue, UploadDialog, sizeOf } from '@/components/mithaq/upload.tsx'
14
- import { AppShell } from '../../ui/app-shell.tsx'
13
+ import { FileThumb, FileValue, UploadDialog, sizeOf } from 'mithaq-admin/upload.tsx'
14
+ import { AppShell } from '@asas/mithaq/ui/app-shell.tsx'
15
15
 
16
16
  /** The `filesize` column: the size as a person reads it, never the raw bytes, in the upload dialog's own units. */
17
17
  export function FileSize({ value }: CellProps) {
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@asas/media",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "dependencies": {
10
+ "@asas/mithaq": "*",
11
+ "mithaq": "*",
12
+ "mithaq-admin": "*"
13
+ },
14
+ "devDependencies": {
15
+ "@types/react": "^19.3.0",
16
+ "lucide-react": "^1.16.0",
17
+ "react": "^19.3.0"
18
+ }
19
+ }
@@ -0,0 +1,4 @@
1
+ import { memberRoles } from './member-roles/member-roles.ts'
2
+ import { roles } from './roles/roles.ts'
3
+
4
+ export const kayan = [roles, memberRoles] as const
@@ -1,13 +1,13 @@
1
1
  import { sql } from 'mithaq/drizzle'
2
2
  import type { NodePgDatabase } from 'mithaq/drizzle/node-postgres'
3
3
  import { assignableRole, conflict, defaultFields, defaultOperations, denied, type Ctx, type WriteTools } from 'mithaq'
4
- import { defineKayan, platformUi, rows, organization } from '../mithaq.ts'
5
- import { platformList, platformRecord } from '../pages.ts'
6
- import { tenantRoles } from '../roles.ts'
7
- import { platformReads } from '../tenant-access.ts'
8
- import { managesRoles, OWNER, OWNER_ROLE } from '../roles/grants.ts'
4
+ import { defineKayan, backofficeUi, rows, organization } from '@asas/mithaq'
5
+ import { backofficeList, backofficeRecord } from '@asas/mithaq/mithaq/pages.ts'
6
+ import { tenantRoles } from '@asas/mithaq/roles/system-roles.ts'
7
+ import { backofficeReads } from '@asas/mithaq/roles/tenant-access.ts'
8
+ import { managesRoles, OWNER, OWNER_ROLE } from '@asas/mithaq/roles/grants.ts'
9
9
 
10
- const role = { field: 'role', cell: platformUi('role-cells.tsx#RoleName') } as const
10
+ const role = { field: 'role', cell: backofficeUi('role-cells.tsx#RoleName') } as const
11
11
 
12
12
  /**
13
13
  * Nobody takes from themselves the role that lets them manage roles: their own
@@ -143,14 +143,14 @@ export const memberRoles = defineKayan({
143
143
  name: 'memberRoles',
144
144
  dbName: 'member_roles',
145
145
  targets: {
146
- // Every organization's rows on the platform, read-only, and each row's
146
+ // Every organization's rows on the backoffice, read-only, and each row's
147
147
  // record. An assignment is looked for by its person, is titled by them, and
148
148
  // reads its role by the role's label, never its slug.
149
- platform: {
149
+ backoffice: {
150
150
  useAsTitle: 'user',
151
151
  pages: {
152
- ...platformList('/member-roles', { columns: ['user', role, 'organization'], filters: ['organization', 'user', 'role'], search: ['user.name', 'user.email'] }),
153
- ...platformRecord({ path: '/member-roles/:id', main: [{ section: 'assignment', label: { en: 'Assignment', ar: 'الإسناد' }, fields: ['user', role] }] }),
152
+ ...backofficeList('/member-roles', { columns: ['user', role, 'organization'], filters: ['organization', 'user', 'role'], search: ['user.name', 'user.email'] }),
153
+ ...backofficeRecord({ path: '/member-roles/:id', main: [{ section: 'assignment', label: { en: 'Assignment', ar: 'الإسناد' }, fields: ['user', role] }] }),
154
154
  },
155
155
  },
156
156
  // No dashboard page and so no nav entry: a member's roles are changed on the
@@ -163,7 +163,7 @@ export const memberRoles = defineKayan({
163
163
  // the column is a string, and the write has not happened, so a refused
164
164
  // assignment leaves no row. Without it a custom role holding
165
165
  // `memberRoles.create` assigns itself `administrator`. It is handed the
166
- // tenant roles alone, so a platform role such as `support` reads as an
166
+ // tenant roles alone, so a backoffice role such as `support` reads as an
167
167
  // unknown custom role and is refused 400. `'roles'` is the kayan it reads
168
168
  // the tenant's custom roles from.
169
169
  create: { steps: ({ steps }) => [assignableRole(tenantRoles, 'roles'), steps.insert] },
@@ -177,12 +177,12 @@ export const memberRoles = defineKayan({
177
177
  },
178
178
  },
179
179
  // Its own rows, since the inherited table would close the instructor's
180
- // delete here. Every platform role reads the assignments of every tenant in
181
- // the platform app and changes none; in the tenant app `assignedRoles` omits
182
- // the platform slugs.
180
+ // delete here. Every backoffice role reads the assignments of every tenant in
181
+ // the backoffice app and changes none; in the tenant app `assignedRoles` omits
182
+ // the backoffice slugs.
183
183
  access: rows({
184
184
  '*': { create: true, read: true, update: true, delete: true },
185
- ...platformReads,
185
+ ...backofficeReads,
186
186
  }),
187
187
  /** A member holds a role once per tenant. A second assignment answers 409 `error.conflict.unique`. */
188
188
  indexes: [{ within: 'organization', unique: [['user', 'role']] }],
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@asas/roles",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts",
8
+ "./roles/ui/*": "./roles/ui/*"
9
+ },
10
+ "dependencies": {
11
+ "@asas/auth": "*",
12
+ "@asas/mithaq": "*",
13
+ "mithaq": "*",
14
+ "mithaq-admin": "*"
15
+ },
16
+ "devDependencies": {
17
+ "@types/react": "^19.3.0",
18
+ "lucide-react": "^1.16.0",
19
+ "react": "^19.3.0"
20
+ }
21
+ }