create-mithaq 0.0.0-stage → 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/LICENSE +22 -0
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/LICENSES/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/index.js +521 -0
- package/package.json +19 -4
- package/template/apps/asas-backoffice/components.json +25 -0
- package/template/apps/asas-backoffice/gitignore +3 -0
- package/template/apps/asas-backoffice/package.json +57 -0
- package/template/apps/asas-backoffice/public/favicon.svg +1 -0
- package/template/apps/asas-backoffice/src/api.ts +147 -0
- package/template/apps/asas-backoffice/src/auth-frame.tsx +150 -0
- package/template/apps/asas-backoffice/src/components/ui/accordion.tsx +74 -0
- package/template/apps/asas-backoffice/src/components/ui/alert-dialog.tsx +185 -0
- package/template/apps/asas-backoffice/src/components/ui/avatar.tsx +106 -0
- package/template/apps/asas-backoffice/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-backoffice/src/components/ui/breadcrumb.tsx +124 -0
- package/template/apps/asas-backoffice/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-backoffice/src/components/ui/button.tsx +55 -0
- package/template/apps/asas-backoffice/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-backoffice/src/components/ui/card.tsx +102 -0
- package/template/apps/asas-backoffice/src/components/ui/chart.tsx +371 -0
- package/template/apps/asas-backoffice/src/components/ui/checkbox.tsx +28 -0
- package/template/apps/asas-backoffice/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-backoffice/src/components/ui/combobox.tsx +308 -0
- package/template/apps/asas-backoffice/src/components/ui/dialog.tsx +158 -0
- package/template/apps/asas-backoffice/src/components/ui/direction.tsx +6 -0
- package/template/apps/asas-backoffice/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-backoffice/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-backoffice/src/components/ui/field.tsx +236 -0
- package/template/apps/asas-backoffice/src/components/ui/input-group.tsx +155 -0
- package/template/apps/asas-backoffice/src/components/ui/input-otp.tsx +84 -0
- package/template/apps/asas-backoffice/src/components/ui/input.tsx +19 -0
- package/template/apps/asas-backoffice/src/components/ui/label.tsx +19 -0
- package/template/apps/asas-backoffice/src/components/ui/popover.tsx +90 -0
- package/template/apps/asas-backoffice/src/components/ui/radio-group.tsx +37 -0
- package/template/apps/asas-backoffice/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-backoffice/src/components/ui/scroll-area.tsx +54 -0
- package/template/apps/asas-backoffice/src/components/ui/select.tsx +204 -0
- package/template/apps/asas-backoffice/src/components/ui/separator.tsx +24 -0
- package/template/apps/asas-backoffice/src/components/ui/sheet.tsx +139 -0
- package/template/apps/asas-backoffice/src/components/ui/sidebar.tsx +714 -0
- package/template/apps/asas-backoffice/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-backoffice/src/components/ui/slider.tsx +55 -0
- package/template/apps/asas-backoffice/src/components/ui/spinner.tsx +10 -0
- package/template/apps/asas-backoffice/src/components/ui/switch.tsx +29 -0
- package/template/apps/asas-backoffice/src/components/ui/table.tsx +115 -0
- package/template/apps/asas-backoffice/src/components/ui/tabs.tsx +79 -0
- package/template/apps/asas-backoffice/src/components/ui/textarea.tsx +17 -0
- package/template/apps/asas-backoffice/src/components/ui/toggle-group.tsx +89 -0
- package/template/apps/asas-backoffice/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-backoffice/src/components/ui/tooltip.tsx +63 -0
- package/template/apps/asas-backoffice/src/components.ts +35 -0
- package/template/apps/asas-backoffice/src/copy.ts +1 -0
- package/template/apps/asas-backoffice/src/first-user.ts +14 -0
- package/template/apps/asas-backoffice/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-backoffice/src/lib/utils.ts +1 -0
- package/template/apps/asas-backoffice/src/links.ts +38 -0
- package/template/apps/asas-backoffice/src/locale-switch.tsx +28 -0
- 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-backoffice/src/request-headers.ts +11 -0
- 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-backoffice/src/routes/(screens)/backoffice/screen.json +3971 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/types.ts +163 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.$.tsx +17 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.index.tsx +10 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.tsx +13 -0
- package/template/apps/asas-backoffice/src/routes/__root.tsx +95 -0
- 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-backoffice/src/routes/auth.sign-in.tsx +77 -0
- package/template/apps/asas-backoffice/src/routes/unauthorized.tsx +134 -0
- package/template/apps/asas-backoffice/src/screen-form.tsx +36 -0
- package/template/apps/asas-backoffice/src/screen-list.tsx +140 -0
- package/template/apps/asas-backoffice/src/screen-names.ts +10 -0
- package/template/apps/asas-backoffice/src/screens.tsx +888 -0
- package/template/apps/asas-backoffice/src/server.ts +30 -0
- package/template/apps/asas-backoffice/src/slide.ts +22 -0
- package/template/apps/asas-backoffice/src/start.ts +42 -0
- package/template/apps/asas-backoffice/src/styles.css +202 -0
- package/template/apps/asas-backoffice/src/theme.ts +2 -0
- package/template/apps/asas-backoffice/tsconfig.json +19 -0
- package/template/apps/asas-backoffice/vite.config.ts +32 -0
- package/template/apps/asas-tanstack/components.json +25 -0
- package/template/apps/asas-tanstack/gitignore +3 -0
- package/template/apps/asas-tanstack/package.json +58 -0
- package/template/apps/asas-tanstack/public/favicon.svg +1 -0
- package/template/apps/asas-tanstack/src/api.ts +147 -0
- package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
- package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
- package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
- package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
- package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
- package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
- package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
- package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
- package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
- package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
- package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
- package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
- package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
- package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
- package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
- package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
- package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
- package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
- package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
- package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
- package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
- package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
- package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
- package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
- package/template/apps/asas-tanstack/src/components.ts +35 -0
- package/template/apps/asas-tanstack/src/copy.ts +1207 -0
- package/template/apps/asas-tanstack/src/first-run.ts +16 -0
- package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
- package/template/apps/asas-tanstack/src/links.ts +38 -0
- package/template/apps/asas-tanstack/src/locale-switch.tsx +28 -0
- package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
- package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
- package/template/apps/asas-tanstack/src/redirect.ts +39 -0
- package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +424 -0
- package/template/apps/asas-tanstack/src/router.tsx +18 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +11 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +4242 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +160 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +9 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1564 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +94 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
- package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +108 -0
- package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +98 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +143 -0
- package/template/apps/asas-tanstack/src/routes/index.tsx +146 -0
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +134 -0
- package/template/apps/asas-tanstack/src/screen-form.tsx +36 -0
- package/template/apps/asas-tanstack/src/screen-list.tsx +140 -0
- package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
- package/template/apps/asas-tanstack/src/screens.tsx +888 -0
- package/template/apps/asas-tanstack/src/server.ts +26 -0
- package/template/apps/asas-tanstack/src/slide.ts +22 -0
- package/template/apps/asas-tanstack/src/start.ts +57 -0
- package/template/apps/asas-tanstack/src/styles.css +163 -0
- package/template/apps/asas-tanstack/src/theme.ts +5 -0
- package/template/apps/asas-tanstack/tsconfig.json +19 -0
- package/template/apps/asas-tanstack/vite.config.ts +34 -0
- package/template/package.json +16 -0
- 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 +33 -0
- 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/mithaq.hints.json +25 -0
- package/template/templates/asas/package.json +53 -0
- package/template/templates/asas/src/asas.ts +118 -0
- package/template/templates/asas/src/backoffice-http.ts +117 -0
- 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 +132 -0
- package/template/templates/asas/src/messages/auditLog.json +304 -0
- package/template/templates/asas/src/messages/backofficeLog.json +132 -0
- package/template/templates/asas/src/messages/courses.json +312 -0
- package/template/templates/asas/src/messages/enrollments.json +60 -0
- package/template/templates/asas/src/messages/invitation.json +82 -0
- package/template/templates/asas/src/messages/localUsers.json +48 -0
- package/template/templates/asas/src/messages/media.json +76 -0
- package/template/templates/asas/src/messages/member.json +62 -0
- package/template/templates/asas/src/messages/memberRoles.json +48 -0
- package/template/templates/asas/src/messages/mithaq.json +214 -0
- package/template/templates/asas/src/messages/organization.json +72 -0
- package/template/templates/asas/src/messages/organizationRole.json +62 -0
- package/template/templates/asas/src/messages/roles.json +72 -0
- package/template/templates/asas/src/messages/session.json +102 -0
- package/template/templates/asas/src/messages/sessions.json +136 -0
- package/template/templates/asas/src/messages/settings.json +64 -0
- package/template/templates/asas/src/messages/tags.json +48 -0
- package/template/templates/asas/src/messages/user.json +112 -0
- package/template/templates/asas/src/messages/verification.json +62 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
- 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/drizzle.config.ts +11 -0
- package/template/templates/asas/src/migrations/schema.ts +30 -0
- package/template/templates/asas/src/mithaq.ts +4 -0
- package/template/templates/asas/src/organizations.ts +3 -0
- package/template/templates/asas/src/seed.ts +427 -0
- package/template/templates/asas/src/ui-backoffice/home.tsx +63 -0
- package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
- package/template/templates/asas/src/ui-backoffice/record-page.tsx +30 -0
- package/template/templates/asas/src/ui-backoffice/role-cells.tsx +59 -0
- package/template/templates/asas/src/ui-backoffice/user-actions.tsx +249 -0
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +300 -0
- 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 +829 -0
- package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
- package/template/templates/asas/src/ui-dashboard/versions.tsx +446 -0
- package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
- package/template/templates/asas/src/ui-portal/home.tsx +73 -0
- package/template/templates/asas/src/ui-portal/page.tsx +11 -0
- 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/features/auth/local-users/local-users.ts +143 -0
- package/template/templates/features/auth/package.json +13 -0
- package/template/templates/features/auth/user/account.access.ts +8 -0
- package/template/templates/features/auth/user/account.ts +31 -0
- package/template/templates/features/auth/user/index.ts +18 -0
- package/template/templates/features/auth/user/invitation.access.ts +4 -0
- package/template/templates/features/auth/user/invitation.ts +27 -0
- package/template/templates/features/auth/user/member.access.ts +10 -0
- package/template/templates/features/auth/user/member.screens.ts +37 -0
- package/template/templates/features/auth/user/member.ts +26 -0
- package/template/templates/features/auth/user/organization.access.ts +20 -0
- package/template/templates/features/auth/user/organization.screens.ts +37 -0
- package/template/templates/features/auth/user/organization.ts +25 -0
- package/template/templates/features/auth/user/organizationRole.access.ts +8 -0
- package/template/templates/features/auth/user/organizationRole.ts +25 -0
- package/template/templates/features/auth/user/session.access.ts +8 -0
- package/template/templates/features/auth/user/session.ts +28 -0
- package/template/templates/features/auth/user/user.access.ts +30 -0
- package/template/templates/features/auth/user/user.screens.ts +45 -0
- package/template/templates/features/auth/user/user.ts +31 -0
- package/template/templates/features/auth/user/verification.access.ts +8 -0
- package/template/templates/features/auth/user/verification.ts +23 -0
- package/template/templates/features/courses/courses/courses.ts +601 -0
- 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/features/courses/courses/ui/fee-cell.tsx +19 -0
- package/template/templates/features/courses/courses/ui/no-courses.tsx +13 -0
- package/template/templates/features/courses/courses/ui/status-cell.tsx +33 -0
- 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/features/courses/enrollments/ui/session-cell.tsx +75 -0
- package/template/templates/features/courses/enrollments/ui/session-labels.tsx +36 -0
- 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/features/courses/sessions/sessions.ts +186 -0
- package/template/templates/features/media/index.ts +10 -0
- package/template/templates/features/media/media/media.ts +131 -0
- package/template/templates/features/media/media/ui/preview.tsx +143 -0
- package/template/templates/features/media/package.json +19 -0
- package/template/templates/features/roles/index.ts +4 -0
- package/template/templates/features/roles/member-roles/member-roles.ts +207 -0
- package/template/templates/features/roles/package.json +21 -0
- package/template/templates/features/roles/roles/roles.ts +184 -0
- package/template/templates/features/roles/roles/ui/role-form.tsx +267 -0
- package/template/templates/features/roles/roles/ui/roles.tsx +190 -0
- package/template/templates/features/settings/index.ts +3 -0
- package/template/templates/features/settings/package.json +13 -0
- package/template/templates/features/settings/settings/settings.ts +64 -0
- package/template/templates/features/tags/index.ts +3 -0
- package/template/templates/features/tags/package.json +14 -0
- package/template/templates/features/tags/tags/tags.ts +80 -0
- package/template/templates/mithaq/components.json +22 -0
- 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/mithaq/src/mithaq/audit-sink.ts +111 -0
- package/template/templates/mithaq/src/mithaq/auth/backoffice-roles.ts +108 -0
- 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/mithaq/src/mithaq/pages.ts +83 -0
- 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/mithaq/src/roles/grants.ts +47 -0
- package/template/templates/mithaq/src/roles/member-access.ts +11 -0
- package/template/templates/mithaq/src/roles/statements.ts +13 -0
- package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
- package/template/templates/mithaq/src/roles/tenant-access.ts +260 -0
- package/template/templates/mithaq/src/roles/tenant-roles.ts +321 -0
- package/template/templates/mithaq/src/steps/expected-updated-at.ts +41 -0
- package/template/templates/mithaq/src/storage/defaults.ts +156 -0
- package/template/templates/mithaq/src/storage/index.ts +97 -0
- package/template/templates/mithaq/src/storage/local.ts +143 -0
- package/template/templates/mithaq/src/storage/r2.ts +5 -0
- package/template/templates/mithaq/src/storage/s3.ts +91 -0
- package/template/templates/mithaq/src/storage/uploadthing.ts +59 -0
- package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
- package/template/templates/mithaq/src/ui/activity.tsx +527 -0
- package/template/templates/mithaq/src/ui/app-shell.tsx +268 -0
- package/template/templates/mithaq/src/ui/kpi-card.tsx +25 -0
- package/template/templates/mithaq/src/ui/record-page.tsx +50 -0
- package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
- package/template/templates/mithaq/src/ui/screen-nav.tsx +28 -0
- package/template/templates/mithaq/src/ui/table-page.tsx +9 -0
- package/template/templates/mithaq/src/ui/top-shell.tsx +60 -0
- package/README.md +0 -3
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Written by `mithaq migrate`. Commit it.
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { asas } from '../asas.ts'
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
dialect: 'postgresql',
|
|
7
|
+
schema: fileURLToPath(new URL('schema.ts', import.meta.url)),
|
|
8
|
+
out: fileURLToPath(new URL('.', import.meta.url)),
|
|
9
|
+
dbCredentials: { url: asas.targets.drizzle.connectionString },
|
|
10
|
+
migrations: { schema: 'drizzle', table: '__mithaq_migrations' },
|
|
11
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Written by `mithaq migrate`. Commit it.
|
|
2
|
+
// Every table the language built, named one per line. drizzle-kit reads named
|
|
3
|
+
// exports alone: an object export contributes no table and reports no changes
|
|
4
|
+
// forever, and a partial export still emits foreign keys to the tables it left
|
|
5
|
+
// out. `mithaq migrate` rewrites this file when a kayan is added or removed.
|
|
6
|
+
import { asas } from '../asas.ts'
|
|
7
|
+
|
|
8
|
+
export const {
|
|
9
|
+
user,
|
|
10
|
+
session,
|
|
11
|
+
account,
|
|
12
|
+
verification,
|
|
13
|
+
organization,
|
|
14
|
+
organizationRole,
|
|
15
|
+
member,
|
|
16
|
+
invitation,
|
|
17
|
+
localUsers,
|
|
18
|
+
roles,
|
|
19
|
+
memberRoles,
|
|
20
|
+
tags,
|
|
21
|
+
media,
|
|
22
|
+
courses,
|
|
23
|
+
sessions,
|
|
24
|
+
enrollments,
|
|
25
|
+
auditLog,
|
|
26
|
+
backofficeLog,
|
|
27
|
+
settings,
|
|
28
|
+
courses_tags,
|
|
29
|
+
_courses_versions,
|
|
30
|
+
} = asas.targets.drizzle.tables
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The `mithaq` command runs from this package and reads the language from
|
|
2
|
+
// `src/mithaq.ts` beside `src/asas.ts`: the lock, the generators and `migrate`
|
|
3
|
+
// read these four, wherever the language's own package sits.
|
|
4
|
+
export { defineKayan, generate, locales, targets } from '@asas/mithaq'
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// The organization mode and who may sign up, for the apps' pages, which import the asas alone. It
|
|
2
|
+
// imports nothing of the provider, so a page bundles a few constants and no server.
|
|
3
|
+
export { defaultLocale, nameIn, organizations, showOrganization, signUp, verifyEmail } from '@asas/mithaq/auth/organizations'
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// Fills a database with a working organization's worth of rows, so the dashboard
|
|
2
|
+
// can be tried with something in it.
|
|
3
|
+
//
|
|
4
|
+
// DATABASE_URL=postgres://... npm run seed
|
|
5
|
+
//
|
|
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.
|
|
14
|
+
// Running it twice adds nothing: each row is looked up by a key that does not
|
|
15
|
+
// change between runs and is skipped where it is already there.
|
|
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
|
|
20
|
+
import type { RichTextNode, RichTextValue } from 'mithaq'
|
|
21
|
+
// end add-on: courses
|
|
22
|
+
|
|
23
|
+
function refuse(reason: string): never {
|
|
24
|
+
console.error(`Refusing to seed: ${reason}`)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The language falls back to the local development database when no URL is set,
|
|
29
|
+
// which is right for the dev server and wrong here: a seed writes, so it runs
|
|
30
|
+
// only against a database someone named. Checked before the build is imported,
|
|
31
|
+
// since the import is what reads it.
|
|
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
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { asas } = await import('./asas.ts')
|
|
49
|
+
|
|
50
|
+
/** A generator of the same numbers on every run, so the plan a second run makes is the plan the first one wrote. */
|
|
51
|
+
const seeded = (state: number) => () => {
|
|
52
|
+
state = (state + 0x6d2b79f5) | 0
|
|
53
|
+
let t = Math.imul(state ^ (state >>> 15), 1 | state)
|
|
54
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
55
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
56
|
+
}
|
|
57
|
+
const random = seeded(20260927)
|
|
58
|
+
const pick = <T>(list: readonly T[]) => list[Math.floor(random() * list.length)]!
|
|
59
|
+
|
|
60
|
+
const DAY = 86_400_000
|
|
61
|
+
const now = Date.now()
|
|
62
|
+
|
|
63
|
+
// add-on: courses
|
|
64
|
+
/** One entry per course: the title it is found by on a later run, and a summary in the other locale. */
|
|
65
|
+
const catalogue = [
|
|
66
|
+
['Introduction to Arabic calligraphy', 'مدخل إلى الخط العربي بأدواته وأساسياته', ['script']],
|
|
67
|
+
['Naskh script from first strokes', 'خط النسخ من الحروف المفردة إلى الكلمات', ['script']],
|
|
68
|
+
['Thuluth for advanced students', 'خط الثلث للمتقدمين وتركيب اللوحات', ['script']],
|
|
69
|
+
['Diwani and decorative lettering', 'الخط الديواني والكتابة الزخرفية', ['script']],
|
|
70
|
+
['Arabic grammar essentials', 'أساسيات النحو العربي للمبتدئين', ['grammar']],
|
|
71
|
+
['Morphology and verb patterns', 'علم الصرف وأوزان الأفعال', ['grammar']],
|
|
72
|
+
['Rhetoric in classical texts', 'البلاغة في النصوص الكلاسيكية', ['grammar']],
|
|
73
|
+
['Reading classical poetry', 'قراءة الشعر العربي القديم وتذوقه', ['grammar', 'recitation']],
|
|
74
|
+
['Tajweed rules for beginners', 'أحكام التجويد للمبتدئين', ['recitation']],
|
|
75
|
+
['Quran recitation practice circle', 'حلقة تدريب على التلاوة', ['recitation']],
|
|
76
|
+
['Makharij al-huruf in depth', 'مخارج الحروف وصفاتها بالتفصيل', ['recitation']],
|
|
77
|
+
['Memorisation techniques', 'أساليب الحفظ والمراجعة', ['recitation']],
|
|
78
|
+
['Modern Standard Arabic conversation', 'المحادثة بالعربية الفصحى المعاصرة', ['grammar']],
|
|
79
|
+
['Arabic for business correspondence', 'العربية في المراسلات التجارية', ['grammar', 'script']],
|
|
80
|
+
['Children\'s handwriting workshop', 'ورشة تحسين الخط للأطفال', ['script']],
|
|
81
|
+
['Illumination and manuscript art', 'فن التذهيب وزخرفة المخطوطات', ['script']],
|
|
82
|
+
['Prosody and poetic metres', 'علم العروض وبحور الشعر', ['grammar', 'recitation']],
|
|
83
|
+
['Dictation and spelling', 'الإملاء وقواعد الكتابة', ['grammar', 'script']],
|
|
84
|
+
['Public speaking in Arabic', 'فن الخطابة والإلقاء بالعربية', ['recitation']],
|
|
85
|
+
['Teaching Arabic to non-native speakers', 'تعليم العربية للناطقين بغيرها', ['grammar']],
|
|
86
|
+
] as const
|
|
87
|
+
|
|
88
|
+
/** Lexical's editor state holding these blocks, the shape a `richText` field stores. */
|
|
89
|
+
const richText = (...children: RichTextNode[]): RichTextValue => ({ root: { type: 'root', version: 1, direction: null, format: '', indent: 0, children } })
|
|
90
|
+
const words = (text: string, format = 0): RichTextNode => ({ type: 'text', version: 1, text, format, detail: 0, mode: 'normal', style: '' })
|
|
91
|
+
const block = (type: string, children: RichTextNode[], more: Record<string, unknown> = {}): RichTextNode => ({ type, version: 1, direction: null, format: '', indent: 0, children, ...more })
|
|
92
|
+
const courseDetails = (title: string, summary: string, topics: readonly string[]) =>
|
|
93
|
+
richText(
|
|
94
|
+
block('heading', [words(title)], { tag: 'h2' }),
|
|
95
|
+
block('paragraph', [words(summary)], { textFormat: 0, textStyle: '' }),
|
|
96
|
+
block('paragraph', [words('Covers ', 0), words(topics.join(', '), 1), words(' · يغطي: ', 0), words(topics.map((topic) => topicNames[topic] ?? topic).join('، '), 1)], { textFormat: 0, textStyle: '' }),
|
|
97
|
+
block('list', [block('listitem', [words('Bring a notebook · أحضر دفترا')], { value: 1 }), block('listitem', [words('Arrive ten minutes early · احضر قبل الموعد بعشر دقائق')], { value: 2 })], { listType: 'bullet', start: 1, tag: 'ul' }),
|
|
98
|
+
)
|
|
99
|
+
const topicNames: Record<string, string> = { script: 'الخط', grammar: 'النحو', recitation: 'التلاوة' }
|
|
100
|
+
const sessionDetails = (format: 'workshop' | 'lecture') =>
|
|
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
|
|
103
|
+
|
|
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. */
|
|
105
|
+
const tagNames = [
|
|
106
|
+
{ en: 'Beginner friendly', ar: 'مناسب للمبتدئين' },
|
|
107
|
+
{ en: 'Weekend', ar: 'عطلة نهاية الأسبوع' },
|
|
108
|
+
{ en: 'Online', ar: 'عبر الإنترنت' },
|
|
109
|
+
{ en: 'Certificate', ar: 'شهادة' },
|
|
110
|
+
{ en: 'Intensive', ar: 'مكثف' },
|
|
111
|
+
{ en: 'Children', ar: 'أطفال' },
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
// add-on: roles
|
|
115
|
+
/** Three custom roles, each labelled in every locale and holding keys the build's own catalog lists. */
|
|
116
|
+
const customRoles = {
|
|
117
|
+
teacher: {
|
|
118
|
+
label: { en: 'Teacher', ar: 'معلم' },
|
|
119
|
+
holds: (key: string) => /^(sessions|enrollments)\./.test(key) || /^courses\.(find|findByID|count|fields\.)/.test(key) || key === 'screen.dashboard',
|
|
120
|
+
},
|
|
121
|
+
accountant: {
|
|
122
|
+
label: { en: 'Accountant', ar: 'محاسب' },
|
|
123
|
+
// Every seat of the organization, not only its own.
|
|
124
|
+
holds: (key: string) => /^(courses|enrollments|sessions)\.(find|findByID|count|fields\.)/.test(key) || key === 'enrollments.others' || key === 'screen.dashboard',
|
|
125
|
+
},
|
|
126
|
+
receptionist: {
|
|
127
|
+
label: { en: 'Receptionist', ar: 'موظف استقبال' },
|
|
128
|
+
holds: (key: string) => /^enrollments\./.test(key) || /^(courses|sessions)\.(find|findByID|count|fields\.)/.test(key) || key === 'screen.dashboard',
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
// end add-on: roles
|
|
132
|
+
|
|
133
|
+
// The owner and the organization, read through the provider's own adapter:
|
|
134
|
+
// the generated auth kayan answer no operation to anyone, so this is how a
|
|
135
|
+
// script asks who someone is.
|
|
136
|
+
const server = (asas.auth.strategies.find((strategy) => strategy.name === 'user') as unknown as { server: { $context: Promise<AuthContext> } }).server
|
|
137
|
+
type Row = Record<string, unknown> & { id: string }
|
|
138
|
+
type AuthContext = {
|
|
139
|
+
secret: string
|
|
140
|
+
authCookies: { sessionToken: { name: string } }
|
|
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
|
+
}
|
|
145
|
+
internalAdapter: {
|
|
146
|
+
findUserByEmail(email: string): Promise<{ user: Row } | null>
|
|
147
|
+
createSession(userId: string, dontRememberMe?: boolean, override?: Record<string, unknown>): Promise<{ token: string }>
|
|
148
|
+
deleteSession(token: string): Promise<void>
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const auth = await server.$context
|
|
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))
|
|
163
|
+
// A name is not unique, so the organization is the one of that name the owner
|
|
164
|
+
// owns, and there has to be exactly one.
|
|
165
|
+
const organizations = (await auth.adapter.findMany({ model: 'organization', where: [SEED_ORGANIZATION ? { field: 'name', value: SEED_ORGANIZATION } : { field: 'id', value: owned[0] ?? '' }] }))
|
|
166
|
+
.filter((one) => owned.includes(one.id))
|
|
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}`)
|
|
171
|
+
}
|
|
172
|
+
const organization = organizations[0]!
|
|
173
|
+
console.log(`Seeding the organization "${organization.name}" that ${owner.email} owns, in ${database}.`)
|
|
174
|
+
const members = await auth.adapter.findMany({ model: 'member', where: [{ field: 'organizationId', value: organization.id }] })
|
|
175
|
+
|
|
176
|
+
// A session of the owner's own, in that organization, which the provider
|
|
177
|
+
// signs and reads back like any browser's. It is deleted when the seed ends,
|
|
178
|
+
// so the run leaves no way in behind it. The cookie is signed the way the
|
|
179
|
+
// provider's router signs it.
|
|
180
|
+
const { token } = await auth.internalAdapter.createSession(owner.id, true, { activeOrganizationId: organization.id })
|
|
181
|
+
const signature = createHmac('sha256', auth.secret).update(token).digest('base64')
|
|
182
|
+
const headers = new Headers({ cookie: `${auth.authCookies.sessionToken.name}=${encodeURIComponent(`${token}.${signature}`)}` })
|
|
183
|
+
|
|
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
|
+
}
|
|
199
|
+
/** The rows this run brought up to date rather than made. */
|
|
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
|
+
}
|
|
210
|
+
const client = (asas.targets.drizzle as unknown as { db: { $client: { query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> } } }).db.$client
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
// A dev database seeded before a tag's name was localized holds one `name`
|
|
214
|
+
// column where the kayan reads `name_en` and `name_ar`, so every read below
|
|
215
|
+
// would fail. A migrated database gets the change from the committed
|
|
216
|
+
// migration; a dev database is pushed by `connect()` instead, and the push
|
|
217
|
+
// cannot tell a rename from a drop and an add, so it stops on this table.
|
|
218
|
+
// This block makes the change the way the migration does, and a push after it
|
|
219
|
+
// runs clean. `name_ar` starts as a copy of the English because a row the
|
|
220
|
+
// read cannot parse is refused whole; a tag a person made reads in English
|
|
221
|
+
// under Arabic until someone writes its Arabic, which the run reports as
|
|
222
|
+
// `repaired.tagColumns`. The five statements are one query, so postgres runs
|
|
223
|
+
// them as one transaction and a failure leaves the table as it was.
|
|
224
|
+
const columns = new Set((await client.query(`select column_name from information_schema.columns where table_schema = 'public' and table_name = 'tags'`)).rows.map((row) => row.column_name))
|
|
225
|
+
if (columns.has('name') && !columns.has('name_en')) {
|
|
226
|
+
await client.query([
|
|
227
|
+
'alter table "tags" rename column "name" to "name_en"',
|
|
228
|
+
'alter table "tags" add column "name_ar" varchar(60)',
|
|
229
|
+
'update "tags" set "name_ar" = "name_en"',
|
|
230
|
+
'alter table "tags" alter column "name_ar" set not null',
|
|
231
|
+
'create index "tags_name_ar_index" on "tags" ("name_ar")',
|
|
232
|
+
].join('; '))
|
|
233
|
+
repaired.tagColumns = 1
|
|
234
|
+
}
|
|
235
|
+
const actor = await asas.auth.actor(headers)
|
|
236
|
+
if (!actor) throw new Error(`The session made for ${owner.email} did not authenticate.`)
|
|
237
|
+
const options = { actor }
|
|
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
|
|
245
|
+
|
|
246
|
+
const tagIds: string[] = []
|
|
247
|
+
for (const name of tagNames) {
|
|
248
|
+
const found = (await tags.operations.find({ where: { 'name.en': { equals: name.en } }, limit: 1 }, options)).docs[0]
|
|
249
|
+
if (!found) {
|
|
250
|
+
created.tags++
|
|
251
|
+
tagIds.push((await tags.operations.create({ name }, options)).id)
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
// A tag made before its name was localized carries no Arabic, and is
|
|
255
|
+
// brought to both here; one that already holds both is left alone.
|
|
256
|
+
if (found.name?.ar !== name.ar) {
|
|
257
|
+
await tags.operations.update({ id: found.id, name }, options)
|
|
258
|
+
repaired.tags++
|
|
259
|
+
}
|
|
260
|
+
tagIds.push(found.id)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// add-on: courses
|
|
264
|
+
// What this run made and when each is said to have been made, for the one
|
|
265
|
+
// write below that goes around the operations.
|
|
266
|
+
const backdate: { table: 'courses' | 'enrollments'; id: string; at: Date }[] = []
|
|
267
|
+
const planned: { course: string; age: number; sessions: string[]; archived: boolean }[] = []
|
|
268
|
+
for (const [index, [title, summary, topics]] of catalogue.entries()) {
|
|
269
|
+
// Drawn whether or not the course is there, so every run draws the same plan.
|
|
270
|
+
const age = 89 - index
|
|
271
|
+
const delivery = pick(['live', 'recorded'] as const)
|
|
272
|
+
const input = {
|
|
273
|
+
title,
|
|
274
|
+
summary,
|
|
275
|
+
details: courseDetails(title, summary, topics),
|
|
276
|
+
topics: [...topics],
|
|
277
|
+
fee: pick([0, 150, 250, 300, 450, 600, 900]),
|
|
278
|
+
currency: pick(['SAR', 'USD'] as const),
|
|
279
|
+
level: 1 + Math.floor(random() * 5),
|
|
280
|
+
audience: pick(['beginner', 'advanced'] as const),
|
|
281
|
+
delivery,
|
|
282
|
+
...(delivery === 'live' && { accessCode: String(100000 + Math.floor(random() * 900000)) }),
|
|
283
|
+
notifications: random() < 0.7,
|
|
284
|
+
opensOn: new Date(now - (age - 7) * DAY).toISOString().slice(0, 10),
|
|
285
|
+
tags: tagIds.filter(() => random() < 0.35),
|
|
286
|
+
}
|
|
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
|
|
290
|
+
const found = (await courses.operations.find({ where: { title: { equals: title } }, limit: 1 }, options)).docs[0]
|
|
291
|
+
const course = found ?? (await courses.operations.create(input, options))
|
|
292
|
+
if (!found) {
|
|
293
|
+
created.courses++
|
|
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)
|
|
296
|
+
} else if (!found.details) {
|
|
297
|
+
// A course seeded without details is given them, once.
|
|
298
|
+
await courses.operations.update({ id: found.id, details: input.details }, options)
|
|
299
|
+
repaired.details++
|
|
300
|
+
}
|
|
301
|
+
// A course's sessions are keyed by their place in its list, which a later
|
|
302
|
+
// run reads back in the order of their start.
|
|
303
|
+
const held = (await sessions.operations.find({ where: { course: { equals: course.id } }, sort: 'startsAt', limit: 0 }, options)).docs.map((one) => one.id)
|
|
304
|
+
for (let at = 0; at < sessionCount; at++) {
|
|
305
|
+
const session = {
|
|
306
|
+
course: course.id,
|
|
307
|
+
startsAt: new Date(now - (60 - index * 3 - at * 7) * DAY + (random() < 0.5 ? 9 : 18) * 3_600_000 - (now % DAY)),
|
|
308
|
+
capacity: 10 + Math.floor(random() * 21),
|
|
309
|
+
venue: pick(['online', 'campus'] as const),
|
|
310
|
+
format: pick(['workshop', 'lecture'] as const),
|
|
311
|
+
}
|
|
312
|
+
if (held[at]) continue
|
|
313
|
+
held.push((await sessions.operations.create({ ...session, details: sessionDetails(session.format) }, options)).id)
|
|
314
|
+
created.sessions++
|
|
315
|
+
}
|
|
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)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Sixty enrollments, each on a session of its own drawn from every course's
|
|
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 })))
|
|
344
|
+
for (let at = pairs.length - 1; at > 0; at--) {
|
|
345
|
+
const other = Math.floor(random() * (at + 1))
|
|
346
|
+
;[pairs[at], pairs[other]] = [pairs[other]!, pairs[at]!]
|
|
347
|
+
}
|
|
348
|
+
for (const [index, { session, age }] of pairs.slice(0, 60).entries()) {
|
|
349
|
+
const student = students[index % students.length]!
|
|
350
|
+
const at = new Date(now - Math.floor(random() ** 2 * age) * DAY - random() * DAY)
|
|
351
|
+
const found = (await enrollments.operations.find({ where: { session: { equals: session }, student: { equals: student } }, limit: 1 }, options)).docs[0]
|
|
352
|
+
if (found) continue
|
|
353
|
+
const made = await enrollments.operations.create({ session, student }, options)
|
|
354
|
+
created.enrollments++
|
|
355
|
+
backdate.push({ table: 'enrollments', id: made.id, at })
|
|
356
|
+
}
|
|
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
|
|
369
|
+
// A role already there is brought to the keys it would be made with, so a
|
|
370
|
+
// database seeded earlier is repaired by running the seed again. A row that
|
|
371
|
+
// already holds exactly these keys is left alone, so a second run still
|
|
372
|
+
// writes nothing. A row whose label is its slug is brought to its real label
|
|
373
|
+
// the same way.
|
|
374
|
+
for (const [name, { label, holds }] of Object.entries(customRoles)) {
|
|
375
|
+
const permissions = asas.catalog.filter(holds)
|
|
376
|
+
const [found] = (await roles.operations.find({ where: { name: { equals: name } }, limit: 1 }, options)).docs
|
|
377
|
+
if (!found) {
|
|
378
|
+
await roles.operations.create({ name, label, permissions }, options)
|
|
379
|
+
created.roles++
|
|
380
|
+
} else if (JSON.stringify(found.permissions) !== JSON.stringify(permissions) || found.label?.en !== label.en || found.label?.ar !== label.ar) {
|
|
381
|
+
await roles.operations.update({ id: found.id, label, permissions }, options)
|
|
382
|
+
repaired.roles++
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Each custom role given to a member in turn, the owner included: the
|
|
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
|
|
392
|
+
for (const [index, role] of Object.keys(customRoles).entries()) {
|
|
393
|
+
const user = String(turns[index % turns.length]!.userId)
|
|
394
|
+
if ((await memberRoles.operations.find({ where: { user: { equals: user }, role: { equals: role } }, limit: 1 }, options)).docs.length) continue
|
|
395
|
+
await memberRoles.operations.create({ user, role }, options)
|
|
396
|
+
created.memberRoles++
|
|
397
|
+
}
|
|
398
|
+
// end add-on: roles
|
|
399
|
+
|
|
400
|
+
// add-on: settings
|
|
401
|
+
const { settings } = asas.global
|
|
402
|
+
if (!(await settings.operations.findOne({}, options))) {
|
|
403
|
+
await settings.operations.update({ contactEmail: String(owner.email), maintenance: false }, options)
|
|
404
|
+
created.settings++
|
|
405
|
+
}
|
|
406
|
+
// end add-on: settings
|
|
407
|
+
|
|
408
|
+
const totals = {
|
|
409
|
+
tags: (await tags.operations.count({}, options)).totalDocs,
|
|
410
|
+
// add-on: courses
|
|
411
|
+
courses: (await courses.operations.count({}, options)).totalDocs,
|
|
412
|
+
sessions: (await sessions.operations.count({}, options)).totalDocs,
|
|
413
|
+
enrollments: (await enrollments.operations.count({}, options)).totalDocs,
|
|
414
|
+
// end add-on: courses
|
|
415
|
+
// add-on: roles
|
|
416
|
+
roles: (await roles.operations.count({}, options)).totalDocs,
|
|
417
|
+
memberRoles: (await memberRoles.operations.count({}, options)).totalDocs,
|
|
418
|
+
// end add-on: roles
|
|
419
|
+
// add-on: settings
|
|
420
|
+
settings: (await settings.operations.findOne({}, options)) ? 1 : 0,
|
|
421
|
+
// end add-on: settings
|
|
422
|
+
}
|
|
423
|
+
console.log(JSON.stringify({ organization: organization.name, created, repaired, totals }))
|
|
424
|
+
} finally {
|
|
425
|
+
await auth.internalAdapter.deleteSession(token)
|
|
426
|
+
await asas.destroy()
|
|
427
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What `/backoffice` itself draws: where staff start, inside the backoffice's
|
|
2
|
+
// shell, with the figure card the dashboard and the portal homes draw for each
|
|
3
|
+
// list the nav offers the viewer, with how many rows of it they may read across
|
|
4
|
+
// every organization and a link to it. A list whose count the viewer may not
|
|
5
|
+
// read, or whose count failed, shows a dash.
|
|
6
|
+
import { ArrowRight } from 'lucide-react'
|
|
7
|
+
import type { LoaderTools, PageProps } from 'mithaq/admin'
|
|
8
|
+
import { useLocale, useScreen } from 'mithaq/admin/react'
|
|
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
|
+
|
|
13
|
+
/** How many rows of each kayan the viewer may count, read as the viewer, keyed by its kayan. */
|
|
14
|
+
export const loader = async ({ client, access }: LoaderTools): Promise<Record<string, number | undefined>> =>
|
|
15
|
+
Object.fromEntries(
|
|
16
|
+
await Promise.all(
|
|
17
|
+
Object.entries(access.collections ?? {})
|
|
18
|
+
.filter(([, one]) => one?.operations?.count === true)
|
|
19
|
+
.map(async ([kayan]) => [kayan, await (client({ kayan, operation: 'count', input: {} }) as Promise<{ totalDocs: number }>).then((found) => found.totalDocs).catch(() => undefined)] as const),
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export function Home({ loaderData: counts }: PageProps<Record<string, number | undefined>>) {
|
|
24
|
+
const { locale, t } = useLocale()
|
|
25
|
+
const screen = useScreen()
|
|
26
|
+
return (
|
|
27
|
+
<BackofficeShell
|
|
28
|
+
regions={{
|
|
29
|
+
header: <h1>{t('backoffice.title', 'Backoffice')}</h1>,
|
|
30
|
+
main: (
|
|
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
|
+
{/* 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 backoffice's style, which a count and one link do not need. */}
|
|
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
|
+
{screen.nav.flatMap((entry) => {
|
|
37
|
+
if (!entry.kayan) return []
|
|
38
|
+
const Icon = iconOf(entry.path)
|
|
39
|
+
const counted = counts?.[entry.kayan]
|
|
40
|
+
return [
|
|
41
|
+
<KpiCard
|
|
42
|
+
key={entry.kayan}
|
|
43
|
+
kpi={entry.kayan}
|
|
44
|
+
icon={<Icon className="size-4" />}
|
|
45
|
+
title={navLabel({ entry, screen, locale, t })}
|
|
46
|
+
value={counted === undefined ? '—' : new Intl.NumberFormat(locale).format(counted)}
|
|
47
|
+
// Underlined, with an arrow that follows the reading direction, so it reads as a link.
|
|
48
|
+
footer={
|
|
49
|
+
<a href={`${screen.path}${entry.path}`} className="text-foreground inline-flex items-center gap-1 underline underline-offset-4 max-sm:min-h-11">
|
|
50
|
+
{t('portal.open', 'Open the list')}
|
|
51
|
+
<ArrowRight className="size-3.5 rtl:-scale-x-100" aria-hidden="true" />
|
|
52
|
+
</a>
|
|
53
|
+
}
|
|
54
|
+
/>,
|
|
55
|
+
]
|
|
56
|
+
})}
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
),
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|