Enhance audience management features with new components and API integrations

- Added `AssignCoursesModal`, `AudienceMemberRow`, `AudienceTable`, and related components for improved audience management UI.
- Implemented audience invite actions including resend and revoke functionality.
- Updated audience API to support importing members and assigning courses.
- Refactored audience data handling to include status and invite management.
- Improved overall user experience with pagination and selection features in the audience table.
This commit is contained in:
Rotimi Best
2026-03-22 18:42:23 +01:00
parent e9c4082f89
commit 3919ff6c2f
157 changed files with 13183 additions and 872 deletions
@@ -13,19 +13,11 @@
import { peopleApi } from '$features/course/api';
import { orgApi } from '$features/org/api/org.svelte';
import type { OrgTeamMember } from '$lib/utils/types/org';
import {
DEFAULT_INVITE_SETTINGS_STATE,
DEFAULT_STUDENT_INVITE_LINK_STATE,
inviteSettingsStore,
studentInviteLinkStore
} from './store';
import type { Tutor } from './types';
import type { OrgStudent, Tutor } from './types';
import TutorSelectSection from './tutor-select-section.svelte';
import InviteSettingsSection from './invite-settings-section.svelte';
import ExistingStudentsSection from './existing-students-section.svelte';
import BulkEmailSection from './bulk-email-section.svelte';
import InviteListSection from './invite-list-section.svelte';
import type { InviteListItem } from './types';
let tutors = $state<Tutor[]>([]);
let selectedIds = $state<string[]>([]);
@@ -36,21 +28,9 @@
const selectedTutors = $derived(tutors.filter((t) => selectedIds.includes(t.id.toString())));
const INVITE_MODAL = 'course.navItem.people.invite_modal';
let existingInvites = $state<InviteListItem[]>([]);
let isLoadingInvites = $state(false);
let activeTab = $state<'tutors' | 'students'>('tutors');
let activeStudentTab = $state<'email' | 'history'>('email');
async function loadInvites() {
if (!courseId) return;
isLoadingInvites = true;
try {
const invites = await peopleApi.listStudentInvites(courseId);
existingInvites = (invites || []) as InviteListItem[];
} finally {
isLoadingInvites = false;
}
}
let isLoadingStudents = $state(false);
let activeTab = $state<'tutors' | 'students'>('students');
const availableStudents = $derived.by(() => getAvailableStudents(orgApi.audience, courseApi.group.people));
function getTutors(team: OrgTeamMember[]) {
const existingTutors = courseApi?.group?.tutors || [];
@@ -65,6 +45,22 @@
}));
}
function getAvailableStudents(students: OrgStudent[], courseMembers: typeof courseApi.group.people) {
const enrolledStudentIds = new Set(
courseMembers
.filter((member) => Number(member.roleId) === ROLE.STUDENT)
.map((member) => member.profileId)
.filter((profileId): profileId is string => Boolean(profileId))
);
return students.filter((student) => {
if (!student.profileId) {
return false;
}
return !enrolledStudentIds.has(student.profileId);
});
}
function setTutors(orgId: string | undefined) {
if (!orgId) return;
untrack(async () => {
@@ -77,6 +73,18 @@
});
}
function loadStudents(orgId: string | undefined) {
if (!orgId) return;
untrack(async () => {
isLoadingStudents = true;
try {
await orgApi.getOrgAudience(orgId);
} finally {
isLoadingStudents = false;
}
});
}
async function onSubmit() {
if (!selectedTutors.length) {
goto(resolve(page.url.pathname, {}));
@@ -105,11 +113,8 @@
$effect(() => {
if (!isOpen || !courseId) return;
untrack(() => {
activeTab = 'tutors';
activeStudentTab = 'email';
inviteSettingsStore.set({ ...DEFAULT_INVITE_SETTINGS_STATE });
studentInviteLinkStore.set({ ...DEFAULT_STUDENT_INVITE_LINK_STATE });
void loadInvites();
activeTab = 'students';
void loadStudents($currentOrg.id);
});
});
</script>
@@ -149,35 +154,8 @@
<UnderlineTabs.Content value="students">
<div class="mt-6 space-y-6">
<InviteSettingsSection />
<UnderlineTabs.Root bind:value={activeStudentTab}>
<UnderlineTabs.List class="flex flex-wrap">
<UnderlineTabs.Trigger value="email">
{$t(`${INVITE_MODAL}.direct_email_bulk`)}
</UnderlineTabs.Trigger>
<UnderlineTabs.Trigger value="history">
{$t(`${INVITE_MODAL}.existing_invites`)}
</UnderlineTabs.Trigger>
</UnderlineTabs.List>
<UnderlineTabs.Content value="email">
<div class="mt-6">
<BulkEmailSection {courseId} onInviteCreated={loadInvites} />
</div>
</UnderlineTabs.Content>
<UnderlineTabs.Content value="history">
<div class="mt-6">
<InviteListSection
{courseId}
invites={existingInvites}
isLoading={isLoadingInvites}
onRefresh={loadInvites}
/>
</div>
</UnderlineTabs.Content>
</UnderlineTabs.Root>
<ExistingStudentsSection {courseId} students={availableStudents} isLoading={isLoadingStudents} />
<BulkEmailSection {courseId} />
</div>
</UnderlineTabs.Content>
</UnderlineTabs.Root>