feat: certificates, landing pages, ai tutor, website update, compliance

This commit is contained in:
Rotimi Best
2026-05-18 03:47:22 +01:00
parent c31ad63955
commit ac3a2c14a7
644 changed files with 56050 additions and 5439 deletions
@@ -0,0 +1,129 @@
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
import type { TCreateProgramGoal, TUpdateProgramGoal } from '@cio/utils/validation/program';
import type { GoalsOverviewRow, MyGoalAssignment, ProgramGoal } from '../utils/types';
import { snackbar } from '$features/ui/snackbar/store';
import { t } from '$lib/utils/functions/translations';
import { currentOrg } from '$lib/utils/store/org';
import { get } from 'svelte/store';
class ProgramGoalApi extends BaseApiWithErrors {
goalsByProgramId = $state<Record<string, ProgramGoal[]>>({});
myGoals = $state<MyGoalAssignment[]>([]);
overviewRows = $state<GoalsOverviewRow[]>([]);
loadedProgramId = $state<string | null>(null);
goalsFor(programId: string): ProgramGoal[] {
return this.goalsByProgramId[programId] ?? [];
}
async listGoals(programId: string, force = false) {
if (!force && this.goalsByProgramId[programId] && this.loadedProgramId === programId) return;
await this.execute<(typeof classroomio.program)[':programId']['goals']['$get']>({
requestFn: () => classroomio.program[':programId'].goals.$get({ param: { programId } }),
onSuccess: (res) => {
this.goalsByProgramId = { ...this.goalsByProgramId, [programId]: res.data };
this.loadedProgramId = programId;
},
logContext: 'listProgramGoals'
});
}
async createGoal(programId: string, data: TCreateProgramGoal): Promise<boolean> {
let ok = false;
await this.execute<(typeof classroomio.program)[':programId']['goals']['$post']>({
requestFn: () => classroomio.program[':programId'].goals.$post({ param: { programId }, json: data }),
onSuccess: async () => {
await this.listGoals(programId, true);
snackbar.success(t.get('programs.goals.created') || 'Goal created');
ok = true;
},
logContext: 'createProgramGoal'
});
return ok;
}
async updateGoal(programId: string, goalId: string, data: TUpdateProgramGoal): Promise<boolean> {
let ok = false;
await this.execute<(typeof classroomio.program)[':programId']['goals'][':goalId']['$put']>({
requestFn: () =>
classroomio.program[':programId'].goals[':goalId'].$put({
param: { programId, goalId },
json: data
}),
onSuccess: async () => {
await this.listGoals(programId, true);
snackbar.success(t.get('programs.goals.updated') || 'Goal updated');
ok = true;
},
logContext: 'updateProgramGoal'
});
return ok;
}
async deleteGoal(programId: string, goalId: string): Promise<boolean> {
let ok = false;
await this.execute<(typeof classroomio.program)[':programId']['goals'][':goalId']['$delete']>({
requestFn: () =>
classroomio.program[':programId'].goals[':goalId'].$delete({
param: { programId, goalId }
}),
onSuccess: async () => {
await this.listGoals(programId, true);
snackbar.success(t.get('programs.goals.deleted') || 'Goal deleted');
ok = true;
},
logContext: 'deleteProgramGoal'
});
return ok;
}
async evaluateGoal(programId: string, goalId: string): Promise<boolean> {
let ok = false;
await this.execute<(typeof classroomio.program)[':programId']['goals'][':goalId']['evaluate']['$post']>({
requestFn: () =>
classroomio.program[':programId'].goals[':goalId'].evaluate.$post({
param: { programId, goalId }
}),
onSuccess: async () => {
await this.listGoals(programId, true);
ok = true;
},
logContext: 'evaluateProgramGoal'
});
return ok;
}
async listMyGoals() {
await this.execute<typeof classroomio.program.my.goals.$get>({
requestFn: () => classroomio.program.my.goals.$get({}),
onSuccess: (res) => {
this.myGoals = res.data;
},
logContext: 'listMyGoals'
});
}
async listOverview() {
const org = get(currentOrg);
if (!org.id) return;
await this.execute<typeof classroomio.program.goals.overview.$get>({
requestFn: () => classroomio.program.goals.overview.$get({ query: { organizationId: org.id } }),
onSuccess: (res) => {
this.overviewRows = res.data.goals;
},
logContext: 'listGoalsOverview'
});
}
}
export const programGoalApi = new ProgramGoalApi();
@@ -1,2 +1,3 @@
export { programApi } from './program.svelte';
export { programGoalApi } from './goal.svelte';
export { programNewsfeedApi, type ProgramNewsfeedCommentsByFeedId } from './newsfeed.svelte';
@@ -5,7 +5,9 @@ import type {
TUpdateProgram,
TAddProgramMembers,
TUpdateProgramMember,
TAddCourseToProgram
TAddCourseToProgram,
TInviteStudentsToProgram,
TAssignExistingStudentsToProgram
} from '@cio/utils/validation/program';
import { currentOrg } from '$lib/utils/store/org';
import { get } from 'svelte/store';
@@ -295,6 +297,50 @@ class ProgramApi extends BaseApiWithErrors {
});
}
// Invitations (program-scoped)
/**
* Invite new students to a program by CSV. Goes through the program-scoped
* endpoint so a Program ADMIN/TUTOR can invite without org-admin rights.
* Returns whether the call succeeded so callers can refresh their lists.
*/
async inviteStudentsToProgram(programId: string, data: TInviteStudentsToProgram): Promise<boolean> {
let ok = false;
await this.execute<(typeof classroomio.program)[':programId']['invite']['$post']>({
requestFn: () => classroomio.program[':programId'].invite.$post({ param: { programId }, json: data }),
onSuccess: async () => {
await this.listMembers(programId, true);
snackbar.success(t.get('programs.invite_sent') || 'Invites sent');
ok = true;
},
logContext: 'inviteStudentsToProgram'
});
return ok;
}
/**
* Assign existing org audience student profiles to this program.
* Same scoping as `inviteStudentsToProgram` — caller only needs program-team
* membership for this program.
*/
async assignExistingStudentsToProgram(programId: string, data: TAssignExistingStudentsToProgram): Promise<boolean> {
let ok = false;
await this.execute<(typeof classroomio.program)[':programId']['invite']['assign']['$post']>({
requestFn: () => classroomio.program[':programId'].invite.assign.$post({ param: { programId }, json: data }),
onSuccess: async () => {
await this.listMembers(programId, true);
snackbar.success(t.get('programs.members_added') || 'Members added');
ok = true;
},
logContext: 'assignExistingStudentsToProgram'
});
return ok;
}
// Newsfeed
async listNewsfeed(programId: string, force = false, cursor?: string) {