create-mithaq 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1056 -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
|
@@ -1,57 +1,36 @@
|
|
|
1
|
-
import { defaultFields, draftFields, folderOf, invalid,
|
|
1
|
+
import { defaultFields, draftFields, folderOf, invalid, type Ctx, type ReadTools, type WriteTools } from 'mithaq'
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
-
import { defineKayan, organization } from '
|
|
4
|
-
import { activity, appPage, danger,
|
|
5
|
-
import { readsPast } from '
|
|
3
|
+
import { defineKayan, organization } from '@asas/mithaq'
|
|
4
|
+
import { activity, activityTab, appPage, danger, backofficeList, backofficeRecord } from '@asas/mithaq/mithaq/pages.ts'
|
|
5
|
+
import { readsPast } from '@asas/mithaq/roles/tenant-access.ts'
|
|
6
6
|
import { sessions } from '../sessions/sessions.ts'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { seatsTaken } from '../sessions/seats.ts'
|
|
8
|
+
import { expectedUpdatedAt } from '@asas/mithaq/steps/expected-updated-at.ts'
|
|
9
|
+
import { archivedRow, marks, notArchived, reason, reasonInput, unarchived } from '../domain/archive.ts'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Page pieces only this kayan draws with. What both screens share is reached
|
|
13
13
|
* through `ui`, `dashboardUi` and `portalUi` from `../mithaq.ts`.
|
|
14
14
|
*/
|
|
15
15
|
const ui = folderOf(new URL('./ui/', import.meta.url))
|
|
16
|
+
/** The portal's pages, which the courses feature draws for its students. */
|
|
17
|
+
const portalUi = folderOf(new URL('../portal/ui/', import.meta.url))
|
|
16
18
|
/** What a fee column draws on every screen. */
|
|
17
19
|
const feeCell = ui('fee-cell.tsx#FeeCell')
|
|
18
20
|
/** What the list's Status column draws. */
|
|
19
21
|
const statusCell = ui('status-cell.tsx#StatusCell')
|
|
20
22
|
|
|
21
|
-
/**
|
|
22
|
-
* What `archive` parses. The error messages are this project's own copy keys: a
|
|
23
|
-
* refusal's message is the key the form resolves. Without them the form draws what zod
|
|
24
|
-
* wrote, in English.
|
|
25
|
-
*/
|
|
26
|
-
const archiveInput = z.object({
|
|
27
|
-
id: z.guid(),
|
|
28
|
-
reason: z
|
|
29
|
-
.string({ error: 'courses.operations.archive.reason.required' })
|
|
30
|
-
.max(200, { error: 'courses.operations.archive.reason.maxLength' }),
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
/** The box `archive`'s dialog asks for on every screen that draws its button. */
|
|
34
|
-
const reason = {
|
|
35
|
-
kind: 'text',
|
|
36
|
-
label: { en: 'Reason', ar: 'السبب' },
|
|
37
|
-
rules: { required: true, maxLength: 200 },
|
|
38
|
-
} as const
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The one step `archive` adds: it turns the operation's `reason` into the two
|
|
42
|
-
* columns the kayan has, so `reason` never has to be a field.
|
|
43
|
-
*/
|
|
44
|
-
const markArchived = (ctx: Ctx<z.infer<typeof archiveInput>>): Ctx => {
|
|
45
|
-
const { reason } = ctx.data
|
|
46
|
-
// Spread `ctx.data` to keep what the runner filled before this step, such as
|
|
47
|
-
// `updatedAt`. `reason` reaches no column: the store step writes the kayan's own fields alone.
|
|
48
|
-
return { ...ctx, data: { ...ctx.data, archived: true, archiveReason: reason } }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
23
|
/** When the access code is drawn: while the form holds a live delivery, redrawn as the delivery changes. */
|
|
52
24
|
const live = { delivery: { equals: 'live' } }
|
|
53
|
-
/**
|
|
54
|
-
const
|
|
25
|
+
/** The edit page's header pill for an archived course, beside its title and draft status. */
|
|
26
|
+
const archivedBadge = { component: ui('archived-badge.tsx#ArchivedBadge'), when: archivedRow }
|
|
27
|
+
/** Active, Drafts, Archived and All: Active is what a list opens on, so an archived course is out of the way. */
|
|
28
|
+
const stateTabs = [
|
|
29
|
+
{ tab: 'active', label: { en: 'Active', ar: 'النشطة' }, where: notArchived },
|
|
30
|
+
{ tab: 'drafts', label: { en: 'Drafts', ar: 'المسودات' }, where: { and: [notArchived, { _status: { equals: 'draft' } }] } },
|
|
31
|
+
{ tab: 'archived', label: { en: 'Archived', ar: 'المؤرشفة' }, where: { archived: { equals: true } } },
|
|
32
|
+
{ tab: 'all', label: { en: 'All', ar: 'الكل' } },
|
|
33
|
+
] as const
|
|
55
34
|
|
|
56
35
|
/** Whether the actor runs the catalogue and so reads every course and every session. */
|
|
57
36
|
export const runsCatalogue = (user: unknown): boolean => readsPast(user, 'courses.archive') || readsPast(user, 'screen.dashboard')
|
|
@@ -73,7 +52,7 @@ const archivedToArchivers = ({ user }: { user: unknown }) =>
|
|
|
73
52
|
const details = [
|
|
74
53
|
{ section: 'about', label: { en: 'About the course', ar: 'عن الدورة' }, fields: ['title', 'summary', 'cover', 'details'] },
|
|
75
54
|
{ section: 'pricing', label: { en: 'Pricing', ar: 'التسعير' }, fields: [{ row: ['fee', 'currency'] }] },
|
|
76
|
-
{ section: 'teaching', label: { en: 'Teaching', ar: 'التدريس' }, fields: ['delivery', { field: 'accessCode', when: live }, 'notifications'] },
|
|
55
|
+
{ section: 'teaching', label: { en: 'Teaching', ar: 'التدريس' }, fields: ['delivery', { field: 'accessCode', when: live }, 'notifications', { field: 'selfEnroll', component: 'switch' }] },
|
|
77
56
|
] as const
|
|
78
57
|
const catalogue = { section: 'catalogue', label: { en: 'Catalogue', ar: 'الدليل' }, fields: ['tags'] } as const
|
|
79
58
|
/**
|
|
@@ -119,11 +98,19 @@ const createSessions = async (
|
|
|
119
98
|
return ctx
|
|
120
99
|
}
|
|
121
100
|
|
|
122
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* What `findWithSessions` puts on the course, one entry per session it may
|
|
103
|
+
* read, with the seats it has left: `null` where it holds any number. A reader
|
|
104
|
+
* who sees only its own seats, a student, could not count the others'.
|
|
105
|
+
*/
|
|
123
106
|
const sessionRow = z.object({
|
|
124
107
|
id: z.guid(),
|
|
125
108
|
startsAt: z.date(),
|
|
126
109
|
capacity: z.number().nullable(),
|
|
110
|
+
venue: z.string().nullish(),
|
|
111
|
+
format: z.string().nullish(),
|
|
112
|
+
archived: z.boolean().nullish(),
|
|
113
|
+
seatsLeft: z.number().nullable(),
|
|
127
114
|
})
|
|
128
115
|
|
|
129
116
|
/**
|
|
@@ -141,7 +128,13 @@ const attachSessions = async (ctx: Ctx, tools: ReadTools): Promise<Ctx> => {
|
|
|
141
128
|
where: { course: { equals: course.id } },
|
|
142
129
|
limit: 0,
|
|
143
130
|
})
|
|
144
|
-
|
|
131
|
+
// Each session was read under the reader's access, so only the seats of a session it may read are counted.
|
|
132
|
+
// ponytail: one count per session, a grouped count when a course holds many sessions.
|
|
133
|
+
const counted = await Promise.all(found.docs.map(async (one) => ({
|
|
134
|
+
...one,
|
|
135
|
+
seatsLeft: typeof one.capacity === 'number' ? Math.max(0, one.capacity - (await seatsTaken({ session: one.id }))) : null,
|
|
136
|
+
})))
|
|
137
|
+
return { ...ctx, data: { ...course, sessions: counted } }
|
|
145
138
|
}
|
|
146
139
|
|
|
147
140
|
export const courses = defineKayan({
|
|
@@ -166,18 +159,18 @@ export const courses = defineKayan({
|
|
|
166
159
|
},
|
|
167
160
|
},
|
|
168
161
|
targets: {
|
|
169
|
-
/** Every organization's rows on the
|
|
170
|
-
|
|
162
|
+
/** Every organization's rows on the backoffice, read-only, with each row's record page and its sessions. */
|
|
163
|
+
backoffice: {
|
|
171
164
|
useAsTitle: 'title',
|
|
172
165
|
pages: {
|
|
173
|
-
...
|
|
174
|
-
...
|
|
166
|
+
...backofficeList('/courses', { columns: ['title', 'organization', 'fee', 'archived', 'createdAt'], filters: ['organization', 'title', 'fee', 'archived'], search: ['title'], sort: '-createdAt' }),
|
|
167
|
+
...backofficeRecord({
|
|
175
168
|
path: '/courses/:id',
|
|
176
169
|
main: [{ tabs: [
|
|
177
170
|
{ tab: 'details', label: { en: 'Details', ar: 'التفاصيل' }, fields: [
|
|
178
171
|
{ section: 'about', label: { en: 'About the course', ar: 'عن الدورة' }, fields: ['title', 'summary', 'details'] },
|
|
179
172
|
{ section: 'pricing', label: { en: 'Pricing', ar: 'التسعير' }, fields: [{ row: ['fee', 'currency'] }, 'audience', 'topics'] },
|
|
180
|
-
{ section: 'teaching', label: { en: 'Teaching', ar: 'التدريس' }, fields: ['delivery', 'accessCode', 'level', 'opensOn'] },
|
|
173
|
+
{ section: 'teaching', label: { en: 'Teaching', ar: 'التدريس' }, fields: ['delivery', 'accessCode', 'level', 'opensOn', 'selfEnroll'] },
|
|
181
174
|
{ section: 'notes', label: { en: 'Notes', ar: 'ملاحظات' }, fields: ['archiveReason'] },
|
|
182
175
|
] },
|
|
183
176
|
{ tab: 'sessions', label: { en: 'Sessions', ar: 'الجلسات' }, fields: [{ list: {
|
|
@@ -204,10 +197,9 @@ export const courses = defineKayan({
|
|
|
204
197
|
* through are the service target's own.
|
|
205
198
|
*/
|
|
206
199
|
service: {
|
|
207
|
-
operations
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
...versionOperations,
|
|
200
|
+
// The defaults hold versions' three operations too, since a course keeps its versions.
|
|
201
|
+
operations: ({ defaults }) => ({
|
|
202
|
+
...defaults,
|
|
211
203
|
/**
|
|
212
204
|
* The shipped create list with one step added and one shape widened:
|
|
213
205
|
* `sessions` is part of the create signature without being a column, since
|
|
@@ -254,13 +246,26 @@ export const courses = defineKayan({
|
|
|
254
246
|
// What a screen's button asks for beyond the row's id, said as fields: the
|
|
255
247
|
// dialog draws `reason` through the control every text field uses and keys
|
|
256
248
|
// its label `courses.operations.archive.reason`. `buildAsas` refuses a
|
|
257
|
-
// field it names no key for.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
249
|
+
// field it names no key for. A screen that declares none has no dialog.
|
|
250
|
+
targets: { dashboard: { fields: { reason } } },
|
|
251
|
+
input: () => reasonInput('courses.operations.archive'),
|
|
252
|
+
steps: ({ steps }) => [marks(true), steps.update],
|
|
253
|
+
},
|
|
254
|
+
/** Brings an archived course back into the catalogue, with the reason, which the activity keeps. */
|
|
255
|
+
unarchive: {
|
|
256
|
+
targets: { dashboard: { fields: { reason } } },
|
|
257
|
+
input: () => reasonInput('courses.operations.unarchive'),
|
|
258
|
+
steps: ({ steps }) => [marks(false), steps.update],
|
|
259
|
+
},
|
|
260
|
+
/**
|
|
261
|
+
* Payload's Unpublish: the course goes back to a draft, kept as it is and
|
|
262
|
+
* read by draft readers alone until it is published again. The caller
|
|
263
|
+
* sends the status it asks for, since an operation of its own keeps the
|
|
264
|
+
* row's status unless the call names one.
|
|
265
|
+
*/
|
|
266
|
+
unpublish: {
|
|
267
|
+
input: ({ id }) => id.extend({ _status: z.literal('draft') }),
|
|
268
|
+
steps: ({ steps }) => [steps.update],
|
|
264
269
|
},
|
|
265
270
|
/**
|
|
266
271
|
* A read list with one step of its own and an `output` written the way
|
|
@@ -273,13 +278,15 @@ export const courses = defineKayan({
|
|
|
273
278
|
// puts that refusal back.
|
|
274
279
|
input: ({ row }) => row.pick({ id: true }).strict(),
|
|
275
280
|
steps: ({ steps }) => [steps.findOne, attachSessions],
|
|
276
|
-
|
|
281
|
+
// Every column optional but the id: the output is read after the reader's projection, which leaves out a
|
|
282
|
+
// column the reader holds no key for, as a student's leaves out the access code.
|
|
283
|
+
output: ({ row }) => row.partial().required({ id: true }).extend({ sessions: z.array(sessionRow) }),
|
|
277
284
|
},
|
|
278
|
-
},
|
|
285
|
+
}),
|
|
279
286
|
},
|
|
280
287
|
dashboard: {
|
|
281
288
|
// What this kayan is for, drawn under the list heading and the form title.
|
|
282
|
-
// One string per declared locale, keyed `courses.description`.
|
|
289
|
+
// One string per declared locale, keyed `courses.page.description`.
|
|
283
290
|
description: {
|
|
284
291
|
en: 'Everything on offer in the catalogue, and what a seat on each costs.',
|
|
285
292
|
ar: 'كل ما هو معروض في الدليل، وما يكلفه مقعد في كل منها.',
|
|
@@ -297,12 +304,12 @@ export const courses = defineKayan({
|
|
|
297
304
|
page: appPage([{ layout: 'table', main: [{ list: {
|
|
298
305
|
// `_status` says whether each row is a draft or published, since this
|
|
299
306
|
// list reads every row's latest draft.
|
|
300
|
-
columns: ['title', 'fee', '
|
|
307
|
+
columns: ['title', 'fee', 'opensOn', '_status', 'details', 'cover'],
|
|
301
308
|
filters: ['title', 'fee', 'archived'],
|
|
302
309
|
// `details` is searched because this list names it: a rich text field
|
|
303
310
|
// offers itself to no search box on its own.
|
|
304
311
|
search: ['title', 'summary', 'details'],
|
|
305
|
-
empty: '
|
|
312
|
+
empty: ui('no-courses.tsx#NoCourses'),
|
|
306
313
|
// Over the ticked rows: a delete behind its question, and one dialog that
|
|
307
314
|
// writes the fields it names over every one of them.
|
|
308
315
|
bulk: [
|
|
@@ -314,16 +321,15 @@ export const courses = defineKayan({
|
|
|
314
321
|
body: { en: 'This cannot be undone.', ar: 'لا يمكن التراجع عن هذا.' },
|
|
315
322
|
},
|
|
316
323
|
},
|
|
317
|
-
|
|
324
|
+
// Archived is not among them: a course leaves and comes back through its own operations.
|
|
325
|
+
{ button: 'updateMany', label: { en: 'Update', ar: 'تحديث' }, fields: ['fee', 'notifications'] },
|
|
326
|
+
],
|
|
327
|
+
// On each row, the edit page's own archive or unarchive, asking for its reason.
|
|
328
|
+
row: [
|
|
329
|
+
{ button: 'archive', when: unarchived, label: { en: 'Archive', ar: 'أرشفة' }, fields: ['reason'] },
|
|
330
|
+
{ button: 'unarchive', when: archivedRow, label: { en: 'Unarchive', ar: 'إلغاء الأرشفة' }, fields: ['reason'] },
|
|
318
331
|
],
|
|
319
|
-
|
|
320
|
-
// asking for its reason.
|
|
321
|
-
row: [{
|
|
322
|
-
button: 'archive',
|
|
323
|
-
when: unarchived,
|
|
324
|
-
label: { en: 'Archive', ar: 'أرشفة' },
|
|
325
|
-
fields: ['reason'],
|
|
326
|
-
}],
|
|
332
|
+
tabs: stateTabs,
|
|
327
333
|
} }] }]),
|
|
328
334
|
},
|
|
329
335
|
// The edit page's Details and Catalogue, and nothing that needs a row yet:
|
|
@@ -339,16 +345,27 @@ export const courses = defineKayan({
|
|
|
339
345
|
// field it draws, and no id, tenant or timestamp.
|
|
340
346
|
edit: {
|
|
341
347
|
path: '/courses/:id',
|
|
342
|
-
page: appPage([{ tabs: [
|
|
348
|
+
page: { ...appPage([{ tabs: [
|
|
343
349
|
{ tab: 'details', label: { en: 'Details', ar: 'التفاصيل' }, fields: [{
|
|
344
350
|
layout: 'record',
|
|
345
351
|
main: [{ form: details }],
|
|
346
352
|
// Beside the form, what was done to this row, by whom and when.
|
|
347
353
|
side: [archivedCatalogue, activity],
|
|
348
|
-
// Where a button sits is where it is drawn:
|
|
349
|
-
//
|
|
350
|
-
//
|
|
354
|
+
// Where a button sits is where it is drawn: in the record layout's danger
|
|
355
|
+
// zone, Unpublish over a published course, Archive over one not archived and
|
|
356
|
+
// Unarchive over one that is, each asking before it runs, before the delete
|
|
357
|
+
// every edit page ends its danger zone with.
|
|
351
358
|
danger: danger({
|
|
359
|
+
button: 'unpublish',
|
|
360
|
+
when: { 'record._status': { equals: 'published' } },
|
|
361
|
+
label: { en: 'Unpublish', ar: 'إلغاء النشر' },
|
|
362
|
+
component: ui('unpublish-button.tsx#UnpublishButton'),
|
|
363
|
+
reload: true,
|
|
364
|
+
confirm: {
|
|
365
|
+
title: { en: 'Unpublish this course?', ar: 'إلغاء نشر هذه الدورة؟' },
|
|
366
|
+
body: { en: 'It goes back to a draft that no student sees, until it is published again.', ar: 'تعود مسودة لا يراها أي طالب حتى تُنشر مجددا.' },
|
|
367
|
+
},
|
|
368
|
+
}, {
|
|
352
369
|
button: 'archive',
|
|
353
370
|
when: unarchived,
|
|
354
371
|
label: { en: 'Archive', ar: 'أرشفة' },
|
|
@@ -358,6 +375,16 @@ export const courses = defineKayan({
|
|
|
358
375
|
title: { en: 'Archive this course?', ar: 'أرشفة هذه الدورة؟' },
|
|
359
376
|
body: { en: 'It leaves the catalogue for every student.', ar: 'تخرج من الدليل لكل الطلاب.' },
|
|
360
377
|
},
|
|
378
|
+
}, {
|
|
379
|
+
button: 'unarchive',
|
|
380
|
+
when: archivedRow,
|
|
381
|
+
label: { en: 'Unarchive', ar: 'إلغاء الأرشفة' },
|
|
382
|
+
fields: ['reason'],
|
|
383
|
+
reload: true,
|
|
384
|
+
confirm: {
|
|
385
|
+
title: { en: 'Unarchive this course?', ar: 'إلغاء أرشفة هذه الدورة؟' },
|
|
386
|
+
body: { en: 'It is back in the catalogue as it was.', ar: 'تعود إلى الدليل كما كانت.' },
|
|
387
|
+
},
|
|
361
388
|
}),
|
|
362
389
|
}] },
|
|
363
390
|
// The sessions are rows of another kayan pointing at this one: a list of
|
|
@@ -386,7 +413,9 @@ export const courses = defineKayan({
|
|
|
386
413
|
{ tab: 'versions', label: { en: 'Versions', ar: 'الإصدارات' }, fields: [
|
|
387
414
|
{ component: 'versions', when: { '$user.collections.courses.operations.findVersions': { equals: true } } },
|
|
388
415
|
] },
|
|
389
|
-
|
|
416
|
+
// Beside them, the row's whole activity, searched and filtered; the side column keeps the last few.
|
|
417
|
+
activityTab,
|
|
418
|
+
] }]), header: [{ title: {} }, archivedBadge, { controls: {} }] },
|
|
390
419
|
},
|
|
391
420
|
board: {
|
|
392
421
|
path: '/courses/board',
|
|
@@ -395,62 +424,16 @@ export const courses = defineKayan({
|
|
|
395
424
|
},
|
|
396
425
|
},
|
|
397
426
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
* dashboard's does, and its edit page's archive is a component of the
|
|
402
|
-
* portal's own. It lists the courses on offer alone: an archived course is
|
|
403
|
-
* left out of its list and its home's count, and its page opens for no
|
|
404
|
-
* student or instructor.
|
|
427
|
+
* The students' own pages, each drawn whole by the courses feature's portal: the catalogue of what is on
|
|
428
|
+
* offer, and a course's page with its sessions and, where the course lets students enroll themselves, the
|
|
429
|
+
* seats they take. Both read the courses that are published and not archived alone.
|
|
405
430
|
*/
|
|
406
431
|
portal: {
|
|
407
432
|
useAsTitle: 'title',
|
|
408
|
-
baseFilter: onOffer,
|
|
409
433
|
description: { en: 'The courses on offer.', ar: 'الدورات المعروضة.' },
|
|
410
434
|
pages: {
|
|
411
|
-
list: {
|
|
412
|
-
|
|
413
|
-
// Opens by title and 25 to a page, which the URL overrides, with
|
|
414
|
-
// the same archive on each row not archived yet.
|
|
415
|
-
page: topPage([{ layout: 'page', main: [{ list: {
|
|
416
|
-
columns: ['title', 'fee'],
|
|
417
|
-
filters: ['title'],
|
|
418
|
-
search: ['title'],
|
|
419
|
-
empty: ui('no-courses-on-offer.tsx#NoCourses'),
|
|
420
|
-
sort: 'title',
|
|
421
|
-
limit: 25,
|
|
422
|
-
row: [{
|
|
423
|
-
button: 'archive',
|
|
424
|
-
when: unarchived,
|
|
425
|
-
label: { en: 'Archive', ar: 'أرشفة' },
|
|
426
|
-
fields: ['reason'],
|
|
427
|
-
}],
|
|
428
|
-
} }] }]),
|
|
429
|
-
},
|
|
430
|
-
// The dashboard's record layout: the form's fields in sections, and the
|
|
431
|
-
// archive in the danger zone under it, only over a row not archived yet,
|
|
432
|
-
// drawn by the portal's own button, which writes the reason in the viewer's locale.
|
|
433
|
-
edit: {
|
|
434
|
-
path: '/courses/:id',
|
|
435
|
-
page: topPage([{
|
|
436
|
-
layout: 'record',
|
|
437
|
-
main: [{ form: [
|
|
438
|
-
{ section: 'about', label: { en: 'About the course', ar: 'عن الدورة' }, fields: ['title', 'summary', 'cover', 'details'] },
|
|
439
|
-
{ section: 'pricing', label: { en: 'Pricing', ar: 'التسعير' }, fields: [{ row: ['fee', 'currency'] }, { field: 'audience', component: 'buttonGroup' }] },
|
|
440
|
-
{ section: 'teaching', label: { en: 'Teaching', ar: 'التدريس' }, fields: ['delivery', { field: 'accessCode', when: live }, 'opensOn'] },
|
|
441
|
-
] }],
|
|
442
|
-
// Drawn only for a viewer who may archive, so a danger zone with
|
|
443
|
-
// nothing in it is not drawn at all.
|
|
444
|
-
danger: [{
|
|
445
|
-
button: 'archive',
|
|
446
|
-
when: { and: [unarchived, { '$user.collections.courses.operations.archive': { equals: true } }] },
|
|
447
|
-
label: { en: 'Archive', ar: 'أرشفة' },
|
|
448
|
-
reload: true,
|
|
449
|
-
component: 'archive',
|
|
450
|
-
confirm: { title: { en: 'Archive this course?', ar: 'أرشفة هذه الدورة؟' }, body: { en: 'Students stop seeing it.', ar: 'يتوقف الطلاب عن رؤيتها.' } },
|
|
451
|
-
}],
|
|
452
|
-
}]),
|
|
453
|
-
},
|
|
435
|
+
list: { path: '/courses', component: portalUi('catalogue.tsx#Catalogue') },
|
|
436
|
+
view: { path: '/courses/:id', component: portalUi('course.tsx#Course') },
|
|
454
437
|
},
|
|
455
438
|
},
|
|
456
439
|
},
|
|
@@ -513,7 +496,7 @@ export const courses = defineKayan({
|
|
|
513
496
|
zod: z.number({ error: 'courses.fee.invalid' }).nonnegative({ error: 'courses.fee.min' }).nullish(),
|
|
514
497
|
dashboard: { filter: true, cell: feeCell },
|
|
515
498
|
portal: { cell: feeCell },
|
|
516
|
-
|
|
499
|
+
backoffice: { cell: feeCell },
|
|
517
500
|
},
|
|
518
501
|
},
|
|
519
502
|
currency: {
|
|
@@ -558,7 +541,9 @@ export const courses = defineKayan({
|
|
|
558
541
|
kind: 'text', label: { en: 'Access code', ar: 'رمز الدخول' }, rules: { maxLength: 6 },
|
|
559
542
|
targets: { dashboard: { component: 'otp' } },
|
|
560
543
|
},
|
|
561
|
-
|
|
544
|
+
// Written by `archive` and `unarchive` alone, which record why: no create or
|
|
545
|
+
// plain update names it, so no key past theirs archives a course.
|
|
546
|
+
archived: { kind: 'checkbox', label: { en: 'Archived', ar: 'مؤرشف' }, rules: { providedBy: { create: 'never', update: 'application' } }, targets: { dashboard: { filter: true } } },
|
|
562
547
|
// What a course covers, several at once out of a closed list. A course names
|
|
563
548
|
// at most two, so a third box ticked is refused before the save. No floor: an
|
|
564
549
|
// optional list left empty passes any `minRows`, and a list that is there
|
|
@@ -574,12 +559,12 @@ export const courses = defineKayan({
|
|
|
574
559
|
],
|
|
575
560
|
},
|
|
576
561
|
// The day enrolment opens, a calendar day with no time of its own, which
|
|
577
|
-
// the portal draws as the
|
|
562
|
+
// the portal draws as the backoffice's own day picker.
|
|
578
563
|
opensOn: { kind: 'date', label: { en: 'Opens on', ar: 'يفتح في' } },
|
|
579
564
|
archiveReason: {
|
|
580
565
|
kind: 'text',
|
|
581
566
|
label: { en: 'Archive reason', ar: 'سبب الأرشفة' },
|
|
582
|
-
rules: { maxLength: 200 },
|
|
567
|
+
rules: { maxLength: 200, providedBy: { create: 'never', update: 'application' } },
|
|
583
568
|
},
|
|
584
569
|
// The many-to-many: no column on this table at all, and a join table
|
|
585
570
|
// `courses_tags` the store writes in the same transaction the course is
|
|
@@ -607,6 +592,10 @@ export const courses = defineKayan({
|
|
|
607
592
|
// The course's cover, one of the organization's images. Its row cannot be
|
|
608
593
|
// deleted while a course points at it, since an upload restricts. Declared
|
|
609
594
|
// last, for the same column order.
|
|
610
|
-
cover: { kind: 'upload', relationTo:
|
|
595
|
+
cover: { kind: 'upload', relationTo: 'media', label: { en: 'Cover image', ar: 'صورة الغلاف' } },
|
|
596
|
+
// Whether a student takes a seat in this course's sessions from the portal: only where it is true, so a course
|
|
597
|
+
// that never says is one whose staff enroll each student. No default, which every create would then write, and
|
|
598
|
+
// a role without the column's key would be refused. Declared last, for the same column order.
|
|
599
|
+
selfEnroll: { kind: 'checkbox', label: { en: 'Students may enroll themselves', ar: 'يسجّل الطلاب أنفسهم' } },
|
|
611
600
|
},
|
|
612
601
|
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { actorPermissions, type Where } from 'mithaq'
|
|
2
|
+
import { serverTargets } from '@asas/mithaq'
|
|
3
|
+
import { currentOrganization } from '@asas/mithaq/roles/tenant-access.ts'
|
|
4
|
+
import { courses } from './courses.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The images an actor reads as covers: the cover of a published course it
|
|
8
|
+
* reads, of the organization its session has active, under the courses kayan's
|
|
9
|
+
* own read access for this actor. So a student or an instructor reads no image
|
|
10
|
+
* of a draft, of an archived course or of none. The media feature asks it of
|
|
11
|
+
* an actor that holds no `media.create`, and holds no import of this file.
|
|
12
|
+
*
|
|
13
|
+
* The organization is read off the trusted snapshot and named here, since the
|
|
14
|
+
* courses are read straight through the store, past the runner's scope. An
|
|
15
|
+
* image has no field that points at a course, so a row filter cannot reach
|
|
16
|
+
* one, and the covers are read first and named by id.
|
|
17
|
+
*/
|
|
18
|
+
export const coverReads = async ({ user }: { user: unknown }): Promise<Where | false> => {
|
|
19
|
+
const held = actorPermissions(user)
|
|
20
|
+
if (held !== true && !held?.has('courses.find')) return false
|
|
21
|
+
const active = currentOrganization(user)
|
|
22
|
+
const readable = active && (await courses.access!.read!({ user }))
|
|
23
|
+
if (!readable) return false
|
|
24
|
+
const published: Where = { and: [{ organization: { equals: active } }, { _status: { equals: 'published' } }] }
|
|
25
|
+
const found = await serverTargets.drizzle.find({ kayan: 'courses', args: {
|
|
26
|
+
limit: 0,
|
|
27
|
+
page: 1,
|
|
28
|
+
sort: { field: 'id', direction: 'asc' },
|
|
29
|
+
where: readable === true ? published : { and: [readable, published] },
|
|
30
|
+
} })
|
|
31
|
+
const ids = [...new Set(found.docs.map((row) => row.cover).filter((id): id is string => typeof id === 'string'))]
|
|
32
|
+
return ids.length ? { id: { in: ids } } : false
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The edit page's header pill for an archived course, beside the title and the
|
|
2
|
+
// draft status, so a course out of the catalogue says so on every tab.
|
|
3
|
+
import { useLocale } from 'mithaq/admin/react'
|
|
4
|
+
import { Badge } from '@/components/ui/badge'
|
|
5
|
+
|
|
6
|
+
export function ArchivedBadge() {
|
|
7
|
+
const { t } = useLocale()
|
|
8
|
+
return <Badge data-status="archived" className="bg-destructive/10 text-destructive">{t('version.archived', 'Archived')}</Badge>
|
|
9
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the dot of "ر.س." on the wrong side.
|
|
4
4
|
import type { CellProps } from 'mithaq/admin'
|
|
5
5
|
import { useLocale } from 'mithaq/admin/react'
|
|
6
|
-
import { noValue } from '
|
|
6
|
+
import { noValue } from 'mithaq-admin/format.tsx'
|
|
7
7
|
|
|
8
8
|
const amount = (text: string) => <bdi dir="ltr">{text}</bdi>
|
|
9
9
|
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
// The list reads each row's latest draft, so a published course whose newest version is a draft
|
|
2
2
|
// and a never-published course both arrive as `draft`. The plain read finds only courses with a
|
|
3
|
-
// published copy, which tells the two apart.
|
|
3
|
+
// published copy, which tells the two apart. An archived course says so over any of the three.
|
|
4
4
|
import { use } from 'react'
|
|
5
5
|
import { useQuery } from '@tanstack/react-query'
|
|
6
6
|
import type { CellProps, Row } from 'mithaq/admin'
|
|
7
7
|
import { ListContext, useClient, useLocale, useScreen } from 'mithaq/admin/react'
|
|
8
8
|
import { Badge } from '@/components/ui/badge'
|
|
9
|
-
import { noValue } from '
|
|
9
|
+
import { noValue } from 'mithaq-admin/format.tsx'
|
|
10
10
|
import { coursesOf } from '../../enrollments/ui/session-cell.tsx'
|
|
11
11
|
|
|
12
12
|
const pills = {
|
|
13
13
|
draft: 'bg-muted text-muted-foreground',
|
|
14
14
|
published: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400',
|
|
15
15
|
changed: 'bg-amber-500/15 text-amber-700 dark:text-amber-400',
|
|
16
|
+
archived: 'bg-destructive/10 text-destructive',
|
|
16
17
|
}
|
|
17
|
-
const words = { draft: 'Draft', published: 'Published', changed: 'Changed' }
|
|
18
|
+
const words = { draft: 'Draft', published: 'Published', changed: 'Changed', archived: 'Archived' }
|
|
18
19
|
|
|
19
20
|
export function StatusCell({ value, row }: CellProps) {
|
|
20
21
|
const client = useClient()
|
|
@@ -24,6 +25,7 @@ export function StatusCell({ value, row }: CellProps) {
|
|
|
24
25
|
const ids = [...new Set((use(ListContext)?.page.docs ?? [row]).flatMap((one: Row) => (one._status === 'draft' ? [String(one.id)] : [])))].sort()
|
|
25
26
|
const { data: published, isError } = useQuery({ queryKey: ['status-cell', ids], queryFn: () => coursesOf({ client, ids, maxLimit }), enabled: ids.length > 0, staleTime: 30_000 })
|
|
26
27
|
if (value !== 'draft' && value !== 'published') return noValue
|
|
28
|
+
if (row.archived === true) return <Badge data-status="archived" className={pills.archived}>{t('version.archived', words.archived)}</Badge>
|
|
27
29
|
// Loading until the read answers, and a draft where it failed, which is what the row itself holds.
|
|
28
30
|
if (value === 'draft' && !published && !isError) return t('admin.loading', 'Loading…')
|
|
29
31
|
const status = value === 'draft' && published?.has(String(row.id)) ? 'changed' : value
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The danger zone's Unpublish: one click, behind the page's confirm, sending the
|
|
2
|
+
// draft status `unpublish` takes, as Payload's Unpublish does. The course stays
|
|
3
|
+
// as it is, read by draft readers alone, until it is published again.
|
|
4
|
+
import type { ButtonProps } from 'mithaq/admin'
|
|
5
|
+
import { useLocale } from 'mithaq/admin/react'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
|
|
8
|
+
export function UnpublishButton({ busy, operation, run }: ButtonProps) {
|
|
9
|
+
const { t } = useLocale()
|
|
10
|
+
return (
|
|
11
|
+
<Button type="button" variant="outline" data-button={operation} disabled={busy} onClick={() => void run({ _status: 'draft' })}>
|
|
12
|
+
{t('courses.operations.unpublish', 'Unpublish')}
|
|
13
|
+
</Button>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { Ctx } from 'mithaq'
|
|
3
|
+
|
|
4
|
+
/** The box an archive's or an unarchive's dialog asks for on every screen that draws its button. */
|
|
5
|
+
export const reason = {
|
|
6
|
+
kind: 'text',
|
|
7
|
+
label: { en: 'Reason', ar: 'السبب' },
|
|
8
|
+
rules: { required: true, maxLength: 200 },
|
|
9
|
+
} as const
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What `archive` and `unarchive` parse: the row and why. The error messages are
|
|
13
|
+
* the kayan's own copy keys, which the form resolves; without them it draws
|
|
14
|
+
* what zod wrote, in English.
|
|
15
|
+
*/
|
|
16
|
+
export const reasonInput = (key: `${string}.operations.${'archive' | 'unarchive'}`) => z.object({
|
|
17
|
+
id: z.guid(),
|
|
18
|
+
reason: z
|
|
19
|
+
.string({ error: `${key}.reason.required` })
|
|
20
|
+
.max(200, { error: `${key}.reason.maxLength` }),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The one step each adds: it turns the operation's `reason` into the two
|
|
25
|
+
* columns the kayan has, so `reason` never has to be a field. `ctx.data` is
|
|
26
|
+
* spread to keep what the runner filled before it, such as `updatedAt`, and
|
|
27
|
+
* `reason` reaches no column: the store step writes the kayan's own fields alone.
|
|
28
|
+
* An empty reason, which a bulk archive sends, keeps none.
|
|
29
|
+
*/
|
|
30
|
+
export const marks = (archived: boolean) => (ctx: Ctx<{ reason: string }>): Ctx => ({ ...ctx, data: { ...ctx.data, archived, archiveReason: ctx.data.reason || null } })
|
|
31
|
+
|
|
32
|
+
/** Over a row not archived yet, where an archive button is drawn. Both arms, since a row from before the column holds `null`. */
|
|
33
|
+
export const unarchived = { or: [{ 'record.archived': { exists: false } }, { 'record.archived': { equals: false } }] } as const
|
|
34
|
+
|
|
35
|
+
/** Over an archived row, where an unarchive button and the Archived badge are drawn. */
|
|
36
|
+
export const archivedRow = { 'record.archived': { equals: true } } as const
|
|
37
|
+
|
|
38
|
+
/** A list's tabs over the same column: what is running, what was archived, and everything. */
|
|
39
|
+
export const notArchived = { or: [{ archived: { exists: false } }, { archived: { equals: false } }] } as const
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The courses the portal offers: every course not archived, of those a plain
|
|
2
|
+
// read answers, which are the published ones. The portal's catalogue, its home
|
|
3
|
+
// and its course page read these alone. Plain data in a file of its own, so a
|
|
4
|
+
// page reads it without the kayan's declaration. A course with no `archived`
|
|
5
|
+
// value is not archived either.
|
|
6
|
+
export const onOffer = { or: [{ archived: { exists: false } }, { archived: { equals: false } }] }
|