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,26 +1,48 @@
|
|
|
1
1
|
// Fills a database with a working organization's worth of rows, so the dashboard
|
|
2
2
|
// can be tried with something in it.
|
|
3
3
|
//
|
|
4
|
-
// DATABASE_URL=postgres://...
|
|
4
|
+
// DATABASE_URL=postgres://... npm run seed
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// It fills the organization named SEED_ORGANIZATION that the user signing in as
|
|
7
|
+
// SEED_EMAIL owns. SEED_EMAIL unset is the first user to sign up, and
|
|
8
|
+
// SEED_ORGANIZATION unset the first organization that user owns. A development
|
|
9
|
+
// tool only: nothing in production reads it.
|
|
10
|
+
//
|
|
11
|
+
// Every row is written through the built asas's own operations, as that
|
|
12
|
+
// organization's owner, so access, validation, option lists and the
|
|
13
|
+
// organization the runner fills apply exactly as they do to a person in the dashboard.
|
|
9
14
|
// Running it twice adds nothing: each row is looked up by a key that does not
|
|
10
15
|
// change between runs and is skipped where it is already there.
|
|
11
16
|
import { createHmac } from 'node:crypto'
|
|
17
|
+
import { organizations as mode } from '@asas/mithaq/auth/organizations'
|
|
18
|
+
import { databaseOf } from './local-database.ts'
|
|
19
|
+
// add-on: courses
|
|
12
20
|
import type { RichTextNode, RichTextValue } from 'mithaq'
|
|
21
|
+
// end add-on: courses
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
function refuse(reason: string): never {
|
|
24
|
+
console.error(`Refusing to seed: ${reason}`)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
// The language falls back to the local development database when no URL is set,
|
|
18
29
|
// which is right for the dev server and wrong here: a seed writes, so it runs
|
|
19
30
|
// only against a database someone named. Checked before the build is imported,
|
|
20
31
|
// since the import is what reads it.
|
|
21
|
-
if (!process.env.DATABASE_URL)
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
if (!process.env.DATABASE_URL) refuse('set DATABASE_URL to the database to fill. Nothing is seeded by default.')
|
|
33
|
+
if (process.env.NODE_ENV === 'production') refuse('NODE_ENV is production, and the seed fills a development database only.')
|
|
34
|
+
const { SEED_EMAIL, SEED_ORGANIZATION } = process.env
|
|
35
|
+
// Without a name the seed picks whoever signed up first, which on a shared
|
|
36
|
+
// database is someone's real organization. So it fills only this machine's
|
|
37
|
+
// database by itself, and another only when told whose and told twice.
|
|
38
|
+
const at = databaseOf(process.env.DATABASE_URL)
|
|
39
|
+
if (!at) refuse('DATABASE_URL is not a connection string pg reads.')
|
|
40
|
+
const { database, local } = at
|
|
41
|
+
if (!local) {
|
|
42
|
+
if (!SEED_EMAIL) refuse(`DATABASE_URL names ${database}, which is not this machine. Name the owner whose organization to fill there with SEED_EMAIL, and set SEED_ALLOW_REMOTE=1.`)
|
|
43
|
+
if (process.env.SEED_ALLOW_REMOTE !== '1') {
|
|
44
|
+
refuse(`DATABASE_URL names ${database}, which is not this machine. Set SEED_ALLOW_REMOTE=1 to fill ${SEED_ORGANIZATION ? `the organization "${SEED_ORGANIZATION}"` : 'the first organization'} that ${SEED_EMAIL} owns there.`)
|
|
45
|
+
}
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
const { asas } = await import('./asas.ts')
|
|
@@ -38,6 +60,7 @@ const pick = <T>(list: readonly T[]) => list[Math.floor(random() * list.length)]
|
|
|
38
60
|
const DAY = 86_400_000
|
|
39
61
|
const now = Date.now()
|
|
40
62
|
|
|
63
|
+
// add-on: courses
|
|
41
64
|
/** One entry per course: the title it is found by on a later run, and a summary in the other locale. */
|
|
42
65
|
const catalogue = [
|
|
43
66
|
['Introduction to Arabic calligraphy', 'مدخل إلى الخط العربي بأدواته وأساسياته', ['script']],
|
|
@@ -76,6 +99,7 @@ const courseDetails = (title: string, summary: string, topics: readonly string[]
|
|
|
76
99
|
const topicNames: Record<string, string> = { script: 'الخط', grammar: 'النحو', recitation: 'التلاوة' }
|
|
77
100
|
const sessionDetails = (format: 'workshop' | 'lecture') =>
|
|
78
101
|
richText(block('paragraph', [words(format === 'workshop' ? 'A hands-on workshop.' : 'A lecture with questions at the end.')], { textFormat: 0, textStyle: '' }), block('paragraph', [words(format === 'workshop' ? 'ورشة عمل تطبيقية.' : 'محاضرة تتبعها أسئلة.')], { textFormat: 0, textStyle: '' }))
|
|
102
|
+
// end add-on: courses
|
|
79
103
|
|
|
80
104
|
/** A tag's name is copy a screen shows, so it is one string per locale, and a later run finds it by its English. */
|
|
81
105
|
const tagNames = [
|
|
@@ -87,6 +111,7 @@ const tagNames = [
|
|
|
87
111
|
{ en: 'Children', ar: 'أطفال' },
|
|
88
112
|
]
|
|
89
113
|
|
|
114
|
+
// add-on: roles
|
|
90
115
|
/** Three custom roles, each labelled in every locale and holding keys the build's own catalog lists. */
|
|
91
116
|
const customRoles = {
|
|
92
117
|
teacher: {
|
|
@@ -103,6 +128,7 @@ const customRoles = {
|
|
|
103
128
|
holds: (key: string) => /^enrollments\./.test(key) || /^(courses|sessions)\.(find|findByID|count|fields\.)/.test(key) || key === 'screen.dashboard',
|
|
104
129
|
},
|
|
105
130
|
}
|
|
131
|
+
// end add-on: roles
|
|
106
132
|
|
|
107
133
|
// The owner and the organization, read through the provider's own adapter:
|
|
108
134
|
// the generated auth kayan answer no operation to anyone, so this is how a
|
|
@@ -112,7 +138,10 @@ type Row = Record<string, unknown> & { id: string }
|
|
|
112
138
|
type AuthContext = {
|
|
113
139
|
secret: string
|
|
114
140
|
authCookies: { sessionToken: { name: string } }
|
|
115
|
-
adapter: {
|
|
141
|
+
adapter: {
|
|
142
|
+
findMany(query: { model: string; where?: { field: string; value: string }[]; limit?: number; sortBy?: { field: string; direction: 'asc' } }): Promise<Row[]>
|
|
143
|
+
create(query: { model: string; data: Record<string, unknown> }): Promise<Row>
|
|
144
|
+
}
|
|
116
145
|
internalAdapter: {
|
|
117
146
|
findUserByEmail(email: string): Promise<{ user: Row } | null>
|
|
118
147
|
createSession(userId: string, dontRememberMe?: boolean, override?: Record<string, unknown>): Promise<{ token: string }>
|
|
@@ -121,21 +150,27 @@ type AuthContext = {
|
|
|
121
150
|
}
|
|
122
151
|
const auth = await server.$context
|
|
123
152
|
|
|
153
|
+
const oldestFirst = { field: 'createdAt', direction: 'asc' } as const
|
|
154
|
+
const owner = SEED_EMAIL
|
|
155
|
+
? (await auth.internalAdapter.findUserByEmail(SEED_EMAIL))?.user
|
|
156
|
+
: (await auth.adapter.findMany({ model: 'user', limit: 1, sortBy: oldestFirst }))[0]
|
|
157
|
+
// Where the first owner comes from: the first sign-up with many organizations, first-run otherwise.
|
|
158
|
+
const makeOne = mode === 'many' ? 'Sign up in the tenant app and create an organization first.' : 'Set the app up on the tenant app\'s /auth/first-run first.'
|
|
159
|
+
if (!owner) refuse(SEED_EMAIL ? `no user signs in as ${SEED_EMAIL}.` : `the database holds no user yet. ${makeOne}`)
|
|
160
|
+
const owned = (await auth.adapter.findMany({ model: 'member', where: [{ field: 'userId', value: owner.id }], sortBy: oldestFirst }))
|
|
161
|
+
.filter((member) => String(member.role).split(',').includes('owner'))
|
|
162
|
+
.map((member) => String(member.organizationId))
|
|
124
163
|
// A name is not unique, so the organization is the one of that name the owner
|
|
125
164
|
// owns, and there has to be exactly one.
|
|
126
|
-
const
|
|
127
|
-
const owned = owner
|
|
128
|
-
? (await auth.adapter.findMany({ model: 'member', where: [{ field: 'userId', value: owner.id }] }))
|
|
129
|
-
.filter((member) => String(member.role).split(',').includes('owner'))
|
|
130
|
-
.map((member) => String(member.organizationId))
|
|
131
|
-
: []
|
|
132
|
-
const organizations = (await auth.adapter.findMany({ model: 'organization', where: [{ field: 'name', value: ORGANIZATION }] }))
|
|
165
|
+
const organizations = (await auth.adapter.findMany({ model: 'organization', where: [SEED_ORGANIZATION ? { field: 'name', value: SEED_ORGANIZATION } : { field: 'id', value: owned[0] ?? '' }] }))
|
|
133
166
|
.filter((one) => owned.includes(one.id))
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
167
|
+
if (organizations.length !== 1) {
|
|
168
|
+
refuse(SEED_ORGANIZATION
|
|
169
|
+
? `${owner.email} owns ${organizations.length ? 'more than one' : 'no'} organization named "${SEED_ORGANIZATION}".`
|
|
170
|
+
: `${owner.email} owns no organization. ${makeOne}`)
|
|
137
171
|
}
|
|
138
172
|
const organization = organizations[0]!
|
|
173
|
+
console.log(`Seeding the organization "${organization.name}" that ${owner.email} owns, in ${database}.`)
|
|
139
174
|
const members = await auth.adapter.findMany({ model: 'member', where: [{ field: 'organizationId', value: organization.id }] })
|
|
140
175
|
|
|
141
176
|
// A session of the owner's own, in that organization, which the provider
|
|
@@ -146,9 +181,32 @@ const { token } = await auth.internalAdapter.createSession(owner.id, true, { act
|
|
|
146
181
|
const signature = createHmac('sha256', auth.secret).update(token).digest('base64')
|
|
147
182
|
const headers = new Headers({ cookie: `${auth.authCookies.sessionToken.name}=${encodeURIComponent(`${token}.${signature}`)}` })
|
|
148
183
|
|
|
149
|
-
const created = {
|
|
184
|
+
const created = {
|
|
185
|
+
tags: 0,
|
|
186
|
+
// add-on: courses
|
|
187
|
+
courses: 0,
|
|
188
|
+
sessions: 0,
|
|
189
|
+
enrollments: 0,
|
|
190
|
+
// end add-on: courses
|
|
191
|
+
// add-on: roles
|
|
192
|
+
roles: 0,
|
|
193
|
+
memberRoles: 0,
|
|
194
|
+
// end add-on: roles
|
|
195
|
+
// add-on: settings
|
|
196
|
+
settings: 0,
|
|
197
|
+
// end add-on: settings
|
|
198
|
+
}
|
|
150
199
|
/** The rows this run brought up to date rather than made. */
|
|
151
|
-
const repaired = {
|
|
200
|
+
const repaired = {
|
|
201
|
+
tags: 0,
|
|
202
|
+
tagColumns: 0,
|
|
203
|
+
// add-on: courses
|
|
204
|
+
details: 0,
|
|
205
|
+
// end add-on: courses
|
|
206
|
+
// add-on: roles
|
|
207
|
+
roles: 0,
|
|
208
|
+
// end add-on: roles
|
|
209
|
+
}
|
|
152
210
|
const client = (asas.targets.drizzle as unknown as { db: { $client: { query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> } } }).db.$client
|
|
153
211
|
|
|
154
212
|
try {
|
|
@@ -175,9 +233,15 @@ try {
|
|
|
175
233
|
repaired.tagColumns = 1
|
|
176
234
|
}
|
|
177
235
|
const actor = await asas.auth.actor(headers)
|
|
178
|
-
if (!actor) throw new Error(`The session made for ${
|
|
236
|
+
if (!actor) throw new Error(`The session made for ${owner.email} did not authenticate.`)
|
|
179
237
|
const options = { actor }
|
|
180
|
-
const {
|
|
238
|
+
const { tags } = asas.kayan
|
|
239
|
+
// add-on: courses
|
|
240
|
+
const { courses, sessions, enrollments } = asas.kayan
|
|
241
|
+
// end add-on: courses
|
|
242
|
+
// add-on: roles
|
|
243
|
+
const { roles, memberRoles } = asas.kayan
|
|
244
|
+
// end add-on: roles
|
|
181
245
|
|
|
182
246
|
const tagIds: string[] = []
|
|
183
247
|
for (const name of tagNames) {
|
|
@@ -196,10 +260,11 @@ try {
|
|
|
196
260
|
tagIds.push(found.id)
|
|
197
261
|
}
|
|
198
262
|
|
|
263
|
+
// add-on: courses
|
|
199
264
|
// What this run made and when each is said to have been made, for the one
|
|
200
265
|
// write below that goes around the operations.
|
|
201
266
|
const backdate: { table: 'courses' | 'enrollments'; id: string; at: Date }[] = []
|
|
202
|
-
const planned: { course: string; age: number; sessions: string[] }[] = []
|
|
267
|
+
const planned: { course: string; age: number; sessions: string[]; archived: boolean }[] = []
|
|
203
268
|
for (const [index, [title, summary, topics]] of catalogue.entries()) {
|
|
204
269
|
// Drawn whether or not the course is there, so every run draws the same plan.
|
|
205
270
|
const age = 89 - index
|
|
@@ -218,15 +283,16 @@ try {
|
|
|
218
283
|
notifications: random() < 0.7,
|
|
219
284
|
opensOn: new Date(now - (age - 7) * DAY).toISOString().slice(0, 10),
|
|
220
285
|
tags: tagIds.filter(() => random() < 0.35),
|
|
221
|
-
// Two of the twenty are archived, with the reason the archive form asks for.
|
|
222
|
-
...(index % 9 === 8 && { archived: true, archiveReason: 'Replaced by a newer course.' }),
|
|
223
286
|
}
|
|
224
287
|
const sessionCount = 2 + Math.floor(random() * 5)
|
|
288
|
+
// Two of the twenty are archived, with the reason the archive form asks for.
|
|
289
|
+
const archived = index % 9 === 8
|
|
225
290
|
const found = (await courses.operations.find({ where: { title: { equals: title } }, limit: 1 }, options)).docs[0]
|
|
226
291
|
const course = found ?? (await courses.operations.create(input, options))
|
|
227
292
|
if (!found) {
|
|
228
293
|
created.courses++
|
|
229
294
|
backdate.push({ table: 'courses', id: course.id, at: new Date(now - age * DAY) })
|
|
295
|
+
if (archived) await courses.operations.archive({ id: course.id, reason: 'Replaced by a newer course.' }, options)
|
|
230
296
|
} else if (!found.details) {
|
|
231
297
|
// A course seeded without details is given them, once.
|
|
232
298
|
await courses.operations.update({ id: found.id, details: input.details }, options)
|
|
@@ -247,20 +313,40 @@ try {
|
|
|
247
313
|
held.push((await sessions.operations.create({ ...session, details: sessionDetails(session.format) }, options)).id)
|
|
248
314
|
created.sessions++
|
|
249
315
|
}
|
|
250
|
-
planned.push({ course: course.id, age, sessions: held })
|
|
316
|
+
planned.push({ course: course.id, age, sessions: held, archived })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Six students, made once by address, since a seat is held by someone holding
|
|
320
|
+
// the student role and an organization the seed starts from may hold none.
|
|
321
|
+
// They have no password, so nobody signs in as one, and are written past the
|
|
322
|
+
// sign-up hooks, since nobody signed up.
|
|
323
|
+
const students: string[] = []
|
|
324
|
+
for (let at = 1; at <= 6; at++) {
|
|
325
|
+
const email = `seed-student-${at}@example.invalid`
|
|
326
|
+
const user = (await auth.internalAdapter.findUserByEmail(email))?.user ?? await auth.adapter.create({ model: 'user', data: { email, name: `Student ${at}`, emailVerified: true, createdAt: new Date(), updatedAt: new Date() } })
|
|
327
|
+
const id = String(user.id)
|
|
328
|
+
if (!(await auth.adapter.findMany({ model: 'member', where: [{ field: 'organizationId', value: organization.id }, { field: 'userId', value: id }] })).length) {
|
|
329
|
+
await auth.adapter.create({ model: 'member', data: { organizationId: organization.id, userId: id, role: 'member', createdAt: new Date() } })
|
|
330
|
+
}
|
|
331
|
+
if (!(await memberRoles.operations.find({ where: { user: { equals: id }, role: { equals: 'student' } }, limit: 1 }, options)).docs.length) {
|
|
332
|
+
await memberRoles.operations.create({ user: id, role: 'student' }, options)
|
|
333
|
+
created.memberRoles++
|
|
334
|
+
}
|
|
335
|
+
students.push(id)
|
|
251
336
|
}
|
|
252
337
|
|
|
253
338
|
// Sixty enrollments, each on a session of its own drawn from every course's
|
|
254
|
-
// list and taken by the
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
339
|
+
// list and taken by the students in turn, so one session and one student are
|
|
340
|
+
// the pair a later run finds it by. Each is said to have been made after its
|
|
341
|
+
// course was, more of them lately than long ago.
|
|
342
|
+
// An archived course's sessions take no new seat, so seats are drawn from the open courses alone.
|
|
343
|
+
const pairs = planned.filter((one) => !one.archived).flatMap((one) => one.sessions.map((session) => ({ session, age: one.age })))
|
|
258
344
|
for (let at = pairs.length - 1; at > 0; at--) {
|
|
259
345
|
const other = Math.floor(random() * (at + 1))
|
|
260
346
|
;[pairs[at], pairs[other]] = [pairs[other]!, pairs[at]!]
|
|
261
347
|
}
|
|
262
348
|
for (const [index, { session, age }] of pairs.slice(0, 60).entries()) {
|
|
263
|
-
const student =
|
|
349
|
+
const student = students[index % students.length]!
|
|
264
350
|
const at = new Date(now - Math.floor(random() ** 2 * age) * DAY - random() * DAY)
|
|
265
351
|
const found = (await enrollments.operations.find({ where: { session: { equals: session }, student: { equals: student } }, limit: 1 }, options)).docs[0]
|
|
266
352
|
if (found) continue
|
|
@@ -269,6 +355,17 @@ try {
|
|
|
269
355
|
backdate.push({ table: 'enrollments', id: made.id, at })
|
|
270
356
|
}
|
|
271
357
|
|
|
358
|
+
// `createdAt` is the application's, filled in by the create and refused from
|
|
359
|
+
// a caller, so no operation can say a row was made in the past. The
|
|
360
|
+
// dashboard's chart needs a past, so the rows this run just made, and only
|
|
361
|
+
// those, get one here with a plain update through the drizzle target's own
|
|
362
|
+
// connection, keyed on each row's id and this tenant.
|
|
363
|
+
for (const { table, id, at } of backdate) {
|
|
364
|
+
await client.query(`update "${table}" set "created_at" = $1 where "id" = $2 and "organization_id" = $3`, [at, id, organization.id])
|
|
365
|
+
}
|
|
366
|
+
// end add-on: courses
|
|
367
|
+
|
|
368
|
+
// add-on: roles
|
|
272
369
|
// A role already there is brought to the keys it would be made with, so a
|
|
273
370
|
// database seeded earlier is repaired by running the seed again. A row that
|
|
274
371
|
// already holds exactly these keys is left alone, so a second run still
|
|
@@ -287,38 +384,43 @@ try {
|
|
|
287
384
|
}
|
|
288
385
|
// Each custom role given to a member in turn, the owner included: the
|
|
289
386
|
// owner's own `administrator` already holds every key, so this widens nothing.
|
|
387
|
+
// A student takes none, so the students a run makes leave a later run's turns
|
|
388
|
+
// where the first run's were.
|
|
389
|
+
const studying = new Set((await memberRoles.operations.find({ where: { role: { equals: 'student' } }, limit: 0 }, options)).docs.map((one) => String(one.user)))
|
|
390
|
+
const staff = members.filter((one) => !studying.has(String(one.userId)))
|
|
391
|
+
const turns = staff.length ? staff : members
|
|
290
392
|
for (const [index, role] of Object.keys(customRoles).entries()) {
|
|
291
|
-
const user = String(
|
|
393
|
+
const user = String(turns[index % turns.length]!.userId)
|
|
292
394
|
if ((await memberRoles.operations.find({ where: { user: { equals: user }, role: { equals: role } }, limit: 1 }, options)).docs.length) continue
|
|
293
395
|
await memberRoles.operations.create({ user, role }, options)
|
|
294
396
|
created.memberRoles++
|
|
295
397
|
}
|
|
398
|
+
// end add-on: roles
|
|
296
399
|
|
|
400
|
+
// add-on: settings
|
|
297
401
|
const { settings } = asas.global
|
|
298
402
|
if (!(await settings.operations.findOne({}, options))) {
|
|
299
|
-
await settings.operations.update({
|
|
403
|
+
await settings.operations.update({ contactEmail: String(owner.email), maintenance: false }, options)
|
|
300
404
|
created.settings++
|
|
301
405
|
}
|
|
302
|
-
|
|
303
|
-
// `createdAt` is the application's, filled in by the create and refused from
|
|
304
|
-
// a caller, so no operation can say a row was made in the past. The
|
|
305
|
-
// dashboard's chart needs a past, so the rows this run just made, and only
|
|
306
|
-
// those, get one here with a plain update through the drizzle target's own
|
|
307
|
-
// connection, keyed on each row's id and this tenant.
|
|
308
|
-
for (const { table, id, at } of backdate) {
|
|
309
|
-
await client.query(`update "${table}" set "created_at" = $1 where "id" = $2 and "organization_id" = $3`, [at, id, organization.id])
|
|
310
|
-
}
|
|
406
|
+
// end add-on: settings
|
|
311
407
|
|
|
312
408
|
const totals = {
|
|
313
409
|
tags: (await tags.operations.count({}, options)).totalDocs,
|
|
410
|
+
// add-on: courses
|
|
314
411
|
courses: (await courses.operations.count({}, options)).totalDocs,
|
|
315
412
|
sessions: (await sessions.operations.count({}, options)).totalDocs,
|
|
316
413
|
enrollments: (await enrollments.operations.count({}, options)).totalDocs,
|
|
414
|
+
// end add-on: courses
|
|
415
|
+
// add-on: roles
|
|
317
416
|
roles: (await roles.operations.count({}, options)).totalDocs,
|
|
318
417
|
memberRoles: (await memberRoles.operations.count({}, options)).totalDocs,
|
|
418
|
+
// end add-on: roles
|
|
419
|
+
// add-on: settings
|
|
319
420
|
settings: (await settings.operations.findOne({}, options)) ? 1 : 0,
|
|
421
|
+
// end add-on: settings
|
|
320
422
|
}
|
|
321
|
-
console.log(JSON.stringify({ organization:
|
|
423
|
+
console.log(JSON.stringify({ organization: organization.name, created, repaired, totals }))
|
|
322
424
|
} finally {
|
|
323
425
|
await auth.internalAdapter.deleteSession(token)
|
|
324
426
|
await asas.destroy()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What `/
|
|
1
|
+
// What `/backoffice` itself draws: where staff start, inside the backoffice's
|
|
2
2
|
// shell, with the figure card the dashboard and the portal homes draw for each
|
|
3
3
|
// list the nav offers the viewer, with how many rows of it they may read across
|
|
4
4
|
// every organization and a link to it. A list whose count the viewer may not
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
import { ArrowRight } from 'lucide-react'
|
|
7
7
|
import type { LoaderTools, PageProps } from 'mithaq/admin'
|
|
8
8
|
import { useLocale, useScreen } from 'mithaq/admin/react'
|
|
9
|
-
import {
|
|
10
|
-
import { KpiCard } from '
|
|
11
|
-
import { navLabel } from '
|
|
9
|
+
import { BackofficeShell, iconOf } from '@asas/mithaq/ui/app-shell.tsx'
|
|
10
|
+
import { KpiCard } from '@asas/mithaq/ui/kpi-card.tsx'
|
|
11
|
+
import { navLabel } from '@asas/mithaq/ui/screen-nav.tsx'
|
|
12
12
|
|
|
13
13
|
/** How many rows of each kayan the viewer may count, read as the viewer, keyed by its kayan. */
|
|
14
14
|
export const loader = async ({ client, access }: LoaderTools): Promise<Record<string, number | undefined>> =>
|
|
@@ -24,14 +24,14 @@ export function Home({ loaderData: counts }: PageProps<Record<string, number | u
|
|
|
24
24
|
const { locale, t } = useLocale()
|
|
25
25
|
const screen = useScreen()
|
|
26
26
|
return (
|
|
27
|
-
<
|
|
27
|
+
<BackofficeShell
|
|
28
28
|
regions={{
|
|
29
|
-
header: <h1>{t('
|
|
29
|
+
header: <h1>{t('backoffice.title', 'Backoffice')}</h1>,
|
|
30
30
|
main: (
|
|
31
|
-
<section data-dashboard="
|
|
32
|
-
<p className="text-muted-foreground">{t('
|
|
31
|
+
<section data-dashboard="backoffice" className="@container grid gap-4">
|
|
32
|
+
<p className="text-muted-foreground">{t('backoffice.about', 'Every organization and every person on the backoffice, for its staff.')}</p>
|
|
33
33
|
{/* Cards as tall as their content, the figure and the link: the card's own padding and gap
|
|
34
|
-
are the roomy ones of the
|
|
34
|
+
are the roomy ones of the backoffice's style, which a count and one link do not need. */}
|
|
35
35
|
<div className="grid grid-cols-2 items-start gap-4 @2xl:grid-cols-3 @5xl:grid-cols-4 [&_[data-slot=card]]:[--card-spacing:--spacing(4)]">
|
|
36
36
|
{screen.nav.flatMap((entry) => {
|
|
37
37
|
if (!entry.kayan) return []
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The list's cell for the `slug` column. Rename is drawn only where this actor holds
|
|
2
|
+
// `organization.rename`, with many organizations alone (with one, the name lives in code), and
|
|
3
|
+
// members only where this actor reads them; the route refuses a rename without the key. A rename that lands asks the list behind it to be read again through
|
|
4
|
+
// `mithaq:refresh`, so the list keeps its scroll.
|
|
5
|
+
import { useState, type FormEvent } from 'react'
|
|
6
|
+
import { holds, type CellProps } from 'mithaq/admin'
|
|
7
|
+
import { useAccess, useLocale, useScreen } from 'mithaq/admin/react'
|
|
8
|
+
import { writeQuery } from 'mithaq-admin/list-query.ts'
|
|
9
|
+
import { endonym } from 'mithaq-admin/locale-menu.ts'
|
|
10
|
+
import { defaultLocale, nameIn, organizationName, organizations } from '@asas/mithaq/auth/organizations'
|
|
11
|
+
import { toast } from 'mithaq-admin/toast.tsx'
|
|
12
|
+
import { Button } from '@/components/ui/button'
|
|
13
|
+
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
14
|
+
import { Input } from '@/components/ui/input'
|
|
15
|
+
import { Label } from '@/components/ui/label'
|
|
16
|
+
|
|
17
|
+
const slugShape = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
18
|
+
|
|
19
|
+
export function OrganizationActions({ value, row }: CellProps) {
|
|
20
|
+
const { t } = useLocale()
|
|
21
|
+
const access = useAccess()
|
|
22
|
+
const screen = useScreen()
|
|
23
|
+
const renames = organizations === 'many' && holds(access, 'organization.rename')
|
|
24
|
+
// The default locale's name is the required `name` box, so only the others get optional boxes of their own.
|
|
25
|
+
const others = Object.keys(organizationName).filter((one) => one !== defaultLocale)
|
|
26
|
+
const members = holds(access, 'member.find') && screen.kayan.member?.pages.list !== undefined
|
|
27
|
+
const [open, setOpen] = useState(false)
|
|
28
|
+
const [refusal, setRefusal] = useState<string>()
|
|
29
|
+
/** Said here rather than by the browser's own bubble, which is in the browser's language. */
|
|
30
|
+
const [wrong, setWrong] = useState<{ name?: string; slug?: string }>({})
|
|
31
|
+
const id = String(row.id ?? '')
|
|
32
|
+
const save = async (event: FormEvent<HTMLFormElement>) => {
|
|
33
|
+
event.preventDefault()
|
|
34
|
+
const box = (key: string) => (event.currentTarget.elements.namedItem(key) as HTMLInputElement | null)?.value ?? ''
|
|
35
|
+
const name = box('name')
|
|
36
|
+
const slug = box('slug')
|
|
37
|
+
// An empty one falls back to `name`, so it is left out.
|
|
38
|
+
const names = { [defaultLocale]: name.trim(), ...Object.fromEntries(others.map((one) => [one, box(`name-${one}`).trim()]).filter(([, one]) => one)) }
|
|
39
|
+
const found = {
|
|
40
|
+
name: name.trim() ? undefined : t('organization.name.required', 'This field is required.'),
|
|
41
|
+
slug: !slug ? t('organization.slug.required', 'This field is required.') : slugShape.test(slug) ? undefined : t('backoffice.slugPattern', 'A slug is lowercase letters and digits joined by single dashes, such as admin-org.'),
|
|
42
|
+
}
|
|
43
|
+
setWrong(found)
|
|
44
|
+
if (found.name || found.slug) return
|
|
45
|
+
const response = await fetch('/api/backoffice/organizations/update', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'content-type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({ id, names, slug }),
|
|
49
|
+
})
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const key = ((await response.json().catch(() => null)) as { data?: { key?: string } } | null)?.data?.key
|
|
52
|
+
return setRefusal((key && t(key)) ?? t('backoffice.refused', 'The backoffice refused that action.'))
|
|
53
|
+
}
|
|
54
|
+
window.dispatchEvent(new Event('mithaq:refresh'))
|
|
55
|
+
// A function, so a name holding `$&` is put in as written.
|
|
56
|
+
toast.success((t('backoffice.renamed', 'The organization is now named {name}.') ?? '').replace('{name}', () => name))
|
|
57
|
+
setOpen(false)
|
|
58
|
+
}
|
|
59
|
+
return (
|
|
60
|
+
<div data-organization={id} className="flex items-center gap-2 whitespace-nowrap">
|
|
61
|
+
{/* An identifier, read left to right in either locale. */}
|
|
62
|
+
<span dir="ltr">{String(value ?? '')}</span>
|
|
63
|
+
{renames ? (
|
|
64
|
+
<Button size="sm" variant="outline" className="max-sm:h-11" data-action="rename" onClick={() => setOpen(true)}>
|
|
65
|
+
{t('backoffice.rename', 'Rename')}
|
|
66
|
+
</Button>
|
|
67
|
+
) : null}
|
|
68
|
+
{members ? (
|
|
69
|
+
<Button
|
|
70
|
+
size="sm"
|
|
71
|
+
variant="outline"
|
|
72
|
+
className="max-sm:h-11"
|
|
73
|
+
data-action="members"
|
|
74
|
+
nativeButton={false}
|
|
75
|
+
render={<a href={`${screen.path}${screen.kayan.member!.pages.list!.path}?${new URLSearchParams(writeQuery({ where: { or: [{ and: [{ organizationId: { equals: id } }] }] } }))}`} />}
|
|
76
|
+
>
|
|
77
|
+
{t('backoffice.members', 'Members')}
|
|
78
|
+
</Button>
|
|
79
|
+
) : null}
|
|
80
|
+
{open ? (
|
|
81
|
+
<Dialog open onOpenChange={setOpen}>
|
|
82
|
+
{/* Cancel is the way out, so the primitive's own close, named in
|
|
83
|
+
English alone, is not drawn beside it. */}
|
|
84
|
+
<DialogContent showCloseButton={false}>
|
|
85
|
+
<DialogHeader>
|
|
86
|
+
<DialogTitle>{t('backoffice.rename', 'Rename')}</DialogTitle>
|
|
87
|
+
<DialogDescription>{t('backoffice.renameAbout', 'The new name and slug take the place of the old ones for everyone in this organization.')}</DialogDescription>
|
|
88
|
+
</DialogHeader>
|
|
89
|
+
<form noValidate onSubmit={(event) => void save(event)} className="grid gap-4">
|
|
90
|
+
<div className="grid gap-2">
|
|
91
|
+
<Label htmlFor={`organization-name-${id}`}>{t('organization.name', 'Name')}</Label>
|
|
92
|
+
<Input id={`organization-name-${id}`} name="name" required aria-required dir="auto" defaultValue={String(row.name ?? '')} aria-invalid={wrong.name ? true : undefined} aria-describedby={wrong.name ? `organization-name-${id}-error` : undefined} />
|
|
93
|
+
{wrong.name ? <p id={`organization-name-${id}-error`} role="alert" className="text-sm text-destructive">{wrong.name}</p> : null}
|
|
94
|
+
</div>
|
|
95
|
+
{others.map((one) => (
|
|
96
|
+
<div key={one} className="grid gap-2">
|
|
97
|
+
<Label htmlFor={`organization-name-${one}-${id}`}>{(t('backoffice.nameIn', 'Name in {language}, if it has one') ?? '').replace('{language}', () => endonym(one))}</Label>
|
|
98
|
+
<Input id={`organization-name-${one}-${id}`} name={`name-${one}`} lang={one} dir="auto" defaultValue={nameIn({ name: '', names: row.names }, one)} />
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
<div className="grid gap-2">
|
|
102
|
+
<Label htmlFor={`organization-slug-${id}`}>{t('organization.slug', 'Slug')}</Label>
|
|
103
|
+
<Input id={`organization-slug-${id}`} name="slug" required aria-required dir="ltr" pattern={slugShape.source.slice(1, -1)} defaultValue={String(value ?? '')} aria-invalid={wrong.slug ? true : undefined} aria-describedby={wrong.slug ? `organization-slug-${id}-error` : undefined} />
|
|
104
|
+
{wrong.slug ? <p id={`organization-slug-${id}-error`} role="alert" className="text-sm text-destructive">{wrong.slug}</p> : null}
|
|
105
|
+
</div>
|
|
106
|
+
{refusal ? (
|
|
107
|
+
<p role="alert" className="text-sm text-destructive">
|
|
108
|
+
{refusal}
|
|
109
|
+
</p>
|
|
110
|
+
) : null}
|
|
111
|
+
<DialogFooter>
|
|
112
|
+
<DialogClose render={<Button type="button" variant="outline" />}>{t('admin.cancel', 'Cancel')}</DialogClose>
|
|
113
|
+
<Button type="submit">{t('admin.save', 'Save')}</Button>
|
|
114
|
+
</DialogFooter>
|
|
115
|
+
</form>
|
|
116
|
+
</DialogContent>
|
|
117
|
+
</Dialog>
|
|
118
|
+
) : null}
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The backoffice's record page: the shared one under a link back to the list the
|
|
2
2
|
// record belongs to, so a person's page, an organization's and a role's are never
|
|
3
3
|
// a dead end. The link is named as the nav names the list, which is already the
|
|
4
4
|
// viewer's own word for it.
|
|
@@ -6,12 +6,12 @@ import type { ReactNode } from 'react'
|
|
|
6
6
|
import { ArrowLeft } from 'lucide-react'
|
|
7
7
|
import type { LayoutProps } from 'mithaq/admin'
|
|
8
8
|
import { useKayan, useLocale, useScreen } from 'mithaq/admin/react'
|
|
9
|
-
import { RecordPage } from '
|
|
10
|
-
import { navLabel } from '
|
|
9
|
+
import { RecordPage } from '@asas/mithaq/ui/record-page.tsx'
|
|
10
|
+
import { navLabel } from '@asas/mithaq/ui/screen-nav.tsx'
|
|
11
11
|
|
|
12
12
|
export const regions = ['main', 'side', 'danger'] as const
|
|
13
13
|
|
|
14
|
-
export function
|
|
14
|
+
export function BackofficeRecordPage(props: LayoutProps<readonly ['main', 'side', 'danger'], ReactNode>) {
|
|
15
15
|
const { locale, t } = useLocale()
|
|
16
16
|
const screen = useScreen()
|
|
17
17
|
const kayan = useKayan()
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
// The two role columns the
|
|
1
|
+
// The two role columns the backoffice lists, each named in the viewer's locale
|
|
2
2
|
// where the column holds a slug, so an Arabic page reads no English word and
|
|
3
3
|
// no slug for them.
|
|
4
|
+
import type { CellProps } from 'mithaq/admin'
|
|
5
|
+
import { useLocale } from 'mithaq/admin/react'
|
|
6
|
+
import { listed } from 'mithaq-admin/format.tsx'
|
|
7
|
+
// add-on: roles
|
|
4
8
|
import { use } from 'react'
|
|
5
9
|
import { useQuery } from '@tanstack/react-query'
|
|
6
|
-
import type {
|
|
7
|
-
import { ListContext, useClient,
|
|
10
|
+
import type { Row } from 'mithaq/admin'
|
|
11
|
+
import { ListContext, useClient, useScreen } from 'mithaq/admin/react'
|
|
8
12
|
import type { asas } from '../asas.ts'
|
|
9
|
-
import {
|
|
10
|
-
import { systemRoles } from '
|
|
13
|
+
import { refusalKeyOf } from 'mithaq-admin/refusal.ts'
|
|
14
|
+
import { systemRoles } from '@asas/mithaq/roles/system-roles.ts'
|
|
15
|
+
// end add-on: roles
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* A membership's role, which is the provider's own governance role, `owner`,
|
|
@@ -16,9 +21,10 @@ import { systemRoles } from '../roles.ts'
|
|
|
16
21
|
export function MembershipRole({ value }: CellProps) {
|
|
17
22
|
const { locale, t } = useLocale()
|
|
18
23
|
if (typeof value !== 'string' || !value) return null
|
|
19
|
-
return listed(value.split(',').map((one) => t(`
|
|
24
|
+
return listed(value.split(',').map((one) => t(`backoffice.membership.${one.trim()}`) ?? one.trim()), locale)
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
// add-on: roles
|
|
22
28
|
/**
|
|
23
29
|
* A business role an assignment names by its slug, by its label: a fixed
|
|
24
30
|
* role's from its code, and a custom role's from its row of the assignment's own tenant, one read for
|
|
@@ -50,3 +56,4 @@ export function RoleName({ value, row }: CellProps) {
|
|
|
50
56
|
: (custom?.find((one) => one.name === value && one.organization === row.organization)?.label as Record<string, string> | undefined)
|
|
51
57
|
return label?.[locale] ?? value
|
|
52
58
|
}
|
|
59
|
+
// end add-on: roles
|