feat(course): auto-enrol button for paid course join requests (#769)

* feat(course): add auto-enrol flow for paid course join requests

- Extend teacher buy-request email with manual access steps and an
  Auto Enrol this Student button
- Link teachers to the course People page with grantAccess query param
- Add grant-access modal that sends the same org audience invite used
  by the Invite new students flow

Co-authored-by: Rotimi Best <[email protected]>

* fix(i18n): translate grant access modal strings

Replace English placeholders copied into non-en locale files with
proper translations for each supported language.

Co-authored-by: Rotimi Best <[email protected]>

* fix(email): shorten auto-enrol button label per review

Co-authored-by: Rotimi Best <[email protected]>

* fix(email): use American spelling Enroll in auto-enroll CTA

Co-authored-by: Rotimi Best <[email protected]>

* fix(i18n): localize Danish grant-access send invite label

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-19 12:02:11 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 634ffeab0f
commit 6c3a982daf
14 changed files with 180 additions and 0 deletions
@@ -0,0 +1,91 @@
<script lang="ts">
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import * as Dialog from '@cio/ui/base/dialog';
import { Button } from '@cio/ui/base/button';
import { t } from '$lib/utils/functions/translations';
import { courseApi } from '$features/course/api';
import { orgApi } from '$features/org/api/org.svelte';
import { profile } from '$lib/utils/store/user';
import { isStudentLimitReached } from '$lib/utils/store/org';
import { UpgradeBanner } from '$features/ui';
const GRANT_ACCESS_PARAM = 'grantAccess';
let courseId = $derived(courseApi.course?.id ?? '');
const studentEmail = $derived(new URLSearchParams(page.url.search).get(GRANT_ACCESS_PARAM)?.trim() ?? '');
const isOpen = $derived(Boolean(studentEmail));
let isSubmitting = $state(false);
function closeModal() {
const nextParams = new URLSearchParams(page.url.search);
nextParams.delete(GRANT_ACCESS_PARAM);
const query = nextParams.toString();
goto(resolve(`${page.url.pathname}${query ? `?${query}` : ''}`, {}));
}
async function handleSendInvite() {
if (!courseId || !studentEmail || $isStudentLimitReached) {
return;
}
isSubmitting = true;
try {
const response = await orgApi.importAudienceMembers({
recipientCsv: studentEmail,
courseIds: [courseId],
sendEmail: true
});
if (!response) {
return;
}
await courseApi.refreshCourse(courseId, $profile.id);
closeModal();
} finally {
isSubmitting = false;
}
}
</script>
<Dialog.Root
open={isOpen}
onOpenChange={(open) => {
if (!open) closeModal();
}}
>
<Dialog.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>{$t('course.navItem.people.grant_access_modal.title')}</Dialog.Title>
<Dialog.Description>
{$t('course.navItem.people.grant_access_modal.description', { email: studentEmail })}
</Dialog.Description>
</Dialog.Header>
{#if $isStudentLimitReached}
<UpgradeBanner removeParams={[GRANT_ACCESS_PARAM]}>
{$t('course.navItem.people.invite_modal.student_limit_reached')}
</UpgradeBanner>
{/if}
<div class="flex justify-end gap-2">
<Button type="button" variant="outline" onclick={closeModal} disabled={isSubmitting}>
{$t('course.navItem.people.grant_access_modal.cancel')}
</Button>
<Button
type="button"
variant="secondary"
onclick={handleSendInvite}
loading={isSubmitting}
disabled={$isStudentLimitReached || !studentEmail}
>
{$t('course.navItem.people.grant_access_modal.send_invite')}
</Button>
</div>
</Dialog.Content>
</Dialog.Root>
@@ -11,6 +11,7 @@
import * as Avatar from '@cio/ui/base/avatar';
import { ComingSoon, RoleBasedSecurity, UpgradeBanner } from '$features/ui';
import InvitationModal from '$features/course/components/people/invitation-modal.svelte';
import GrantAccessModal from '$features/course/components/people/grant-access-modal.svelte';
import DeleteConfirmation from '$features/course/components/people/delete-confirmation.svelte';
import { isStudentLimitReached } from '$lib/utils/store/org';
@@ -101,6 +102,7 @@
</script>
<InvitationModal />
<GrantAccessModal />
{#if $isStudentLimitReached}
<UpgradeBanner className="mb-2">{$t('course.navItem.people.invite_modal.student_limit_reached')}</UpgradeBanner>