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.
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/index.js +457 -63
- package/package.json +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
- package/template/apps/asas-backoffice/src/components.ts +35 -0
- package/template/apps/asas-backoffice/src/first-user.ts +14 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
- package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
- package/template/apps/asas-backoffice/src/redirect.ts +1 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/request-headers.ts +1 -1
- package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
- package/template/apps/asas-backoffice/src/router.tsx +18 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
- package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
- package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
- package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
- package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
- package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
- package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
- package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
- package/template/apps/asas-backoffice/src/server.ts +30 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
- package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
- package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
- package/template/apps/asas-tanstack/package.json +4 -2
- package/template/apps/asas-tanstack/src/api.ts +1 -1
- package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
- package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
- package/template/apps/asas-tanstack/src/components.ts +35 -0
- package/template/apps/asas-tanstack/src/copy.ts +416 -79
- package/template/apps/asas-tanstack/src/first-run.ts +16 -0
- package/template/apps/asas-tanstack/src/links.ts +1 -1
- package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
- package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
- package/template/apps/asas-tanstack/src/redirect.ts +20 -1
- package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
- package/template/apps/asas-tanstack/src/router.tsx +12 -1
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
- package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
- package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
- package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
- package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
- package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
- package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
- package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
- package/template/apps/asas-tanstack/src/screens.tsx +46 -26
- package/template/apps/asas-tanstack/src/server.ts +10 -0
- package/template/apps/asas-tanstack/src/slide.ts +1 -1
- package/template/apps/asas-tanstack/src/styles.css +7 -3
- package/template/apps/asas-tanstack/tsconfig.json +2 -2
- package/template/apps/asas-tanstack/vite.config.ts +7 -1
- package/template/package.json +3 -3
- package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
- package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
- package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
- package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
- package/template/templates/asas/.env.example +22 -5
- package/template/templates/asas/AGENTS.md +101 -0
- package/template/templates/asas/CLAUDE.md +1 -0
- package/template/templates/asas/README.md +1094 -0
- package/template/templates/asas/package.json +19 -4
- package/template/templates/asas/src/asas.ts +81 -56
- package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
- package/template/templates/asas/src/dev-setup.ts +112 -0
- package/template/templates/asas/src/local-database.ts +22 -0
- package/template/templates/asas/src/messages/account.json +129 -103
- package/template/templates/asas/src/messages/auditLog.json +223 -105
- package/template/templates/asas/src/messages/backofficeLog.json +132 -0
- package/template/templates/asas/src/messages/courses.json +256 -214
- package/template/templates/asas/src/messages/enrollments.json +45 -27
- package/template/templates/asas/src/messages/invitation.json +72 -56
- package/template/templates/asas/src/messages/localUsers.json +34 -28
- package/template/templates/asas/src/messages/media.json +31 -21
- package/template/templates/asas/src/messages/member.json +59 -39
- package/template/templates/asas/src/messages/memberRoles.json +37 -31
- package/template/templates/asas/src/messages/mithaq.json +206 -162
- package/template/templates/asas/src/messages/organization.json +63 -41
- package/template/templates/asas/src/messages/organizationRole.json +59 -47
- package/template/templates/asas/src/messages/roles.json +59 -45
- package/template/templates/asas/src/messages/session.json +99 -79
- package/template/templates/asas/src/messages/sessions.json +110 -80
- package/template/templates/asas/src/messages/settings.json +51 -53
- package/template/templates/asas/src/messages/tags.json +31 -21
- package/template/templates/asas/src/messages/user.json +109 -87
- package/template/templates/asas/src/messages/verification.json +59 -47
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
- package/template/templates/asas/src/migrations/schema.ts +1 -0
- package/template/templates/asas/src/mithaq.ts +4 -406
- package/template/templates/asas/src/organizations.ts +3 -0
- package/template/templates/asas/src/seed.ts +147 -45
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
- package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
- package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
- package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +637 -438
- package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
- package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
- package/template/templates/asas/src/ui-portal/home.tsx +9 -13
- package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
- package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
- package/template/templates/features/audit-log/index.ts +4 -0
- package/template/templates/features/audit-log/package.json +13 -0
- package/template/templates/features/auth/index.ts +11 -0
- package/template/templates/{asas/src → features/auth}/local-users/local-users.ts +7 -7
- package/template/templates/{auth → features/auth}/package.json +2 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
- package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
- package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
- package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
- package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
- package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
- package/template/templates/features/courses/courses/cover-reads.ts +33 -0
- package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
- package/template/templates/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
- package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
- package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
- package/template/templates/features/courses/domain/archive.ts +39 -0
- package/template/templates/features/courses/domain/on-offer.ts +6 -0
- package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
- package/template/templates/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
- package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
- package/template/templates/features/courses/index.ts +8 -0
- package/template/templates/features/courses/package.json +24 -0
- package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
- package/template/templates/features/courses/portal/ui/course.tsx +176 -0
- package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
- package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
- package/template/templates/features/courses/portal/ui/reads.ts +76 -0
- package/template/templates/features/courses/portal/ui/rules.ts +87 -0
- package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
- package/template/templates/features/courses/sessions/seats.ts +42 -0
- package/template/templates/{asas/src → features/courses}/sessions/sessions.ts +45 -14
- package/template/templates/features/media/index.ts +10 -0
- package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
- package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
- package/template/templates/features/media/package.json +19 -0
- package/template/templates/features/roles/index.ts +4 -0
- package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
- package/template/templates/features/roles/package.json +21 -0
- package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
- package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
- package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
- package/template/templates/features/settings/index.ts +3 -0
- package/template/templates/features/settings/package.json +13 -0
- package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
- package/template/templates/features/tags/index.ts +3 -0
- package/template/templates/features/tags/package.json +14 -0
- package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
- package/template/templates/mithaq/package.json +33 -0
- package/template/templates/mithaq/src/mail/index.ts +32 -0
- package/template/templates/mithaq/src/mail/mailers.ts +36 -0
- package/template/templates/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
- package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
- package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
- package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
- package/template/templates/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
- package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
- package/template/templates/mithaq/src/mithaq.ts +593 -0
- package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
- package/template/templates/{asas → mithaq}/src/roles/grants.ts +1 -1
- package/template/templates/mithaq/src/roles/member-access.ts +11 -0
- package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
- package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
- package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
- package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
- package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
- package/template/templates/mithaq/src/ui/activity.tsx +527 -0
- package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
- package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
- package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
- package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
- package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
- package/template/apps/asas-platform/src/redirect.ts +0 -1
- package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
- package/template/apps/asas-platform/src/router.tsx +0 -7
- package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
- package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
- package/template/apps/asas-platform/src/server.ts +0 -20
- package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
- package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
- package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
- package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
- package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
- package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
- package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
- package/template/templates/asas/src/rich-text-nodes.ts +0 -11
- package/template/templates/asas/src/roles.ts +0 -16
- package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
- package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
- package/template/templates/auth/src/index.ts +0 -690
- /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
- /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
- /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
- /package/template/templates/{asas → mithaq}/components.json +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
- /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 '
|
|
3
|
-
import { activity, appPage, danger,
|
|
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
|
-
*
|
|
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: {
|
|
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
|
-
|
|
44
|
+
backoffice: {
|
|
44
45
|
useAsTitle: 'startsAt',
|
|
45
46
|
pages: {
|
|
46
|
-
...
|
|
47
|
-
...
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
//
|
|
108
|
-
//
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import { defineKayan,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
|
13
|
-
* of
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
24
|
+
const imagesReadBy = (reads: readonly ImageRead[]) => async ({ user }: { user: unknown }) => {
|
|
28
25
|
if (readsPast(user, 'media.create')) return true
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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 `
|
|
57
|
-
* made; a
|
|
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:
|
|
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 '
|
|
14
|
-
import { AppShell } from '
|
|
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
|
+
}
|
|
@@ -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,
|
|
5
|
-
import {
|
|
6
|
-
import { tenantRoles } from '
|
|
7
|
-
import {
|
|
8
|
-
import { managesRoles, OWNER, OWNER_ROLE } from '
|
|
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:
|
|
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
|
|
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
|
-
|
|
149
|
+
backoffice: {
|
|
150
150
|
useAsTitle: 'user',
|
|
151
151
|
pages: {
|
|
152
|
-
...
|
|
153
|
-
...
|
|
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
|
|
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
|
|
181
|
-
// the
|
|
182
|
-
// the
|
|
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
|
-
...
|
|
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
|
+
}
|