feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { programApi } from './program.svelte';
|
||||
export { programNewsfeedApi, type ProgramNewsfeedCommentsByFeedId } from './newsfeed.svelte';
|
||||
@@ -0,0 +1,344 @@
|
||||
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
|
||||
import type {
|
||||
CreateProgramNewsfeedCommentRequest,
|
||||
CreateProgramNewsfeedRequest,
|
||||
DeleteProgramNewsfeedCommentRequest,
|
||||
DeleteProgramNewsfeedRequest,
|
||||
GetProgramNewsfeedCommentsRequest,
|
||||
ListProgramNewsfeed,
|
||||
ListProgramNewsfeedRequest,
|
||||
ProgramNewsfeedComment,
|
||||
ReactToProgramNewsfeedRequest,
|
||||
UpdateProgramNewsfeedRequest
|
||||
} from '../utils/types';
|
||||
import type {
|
||||
TCreateProgramNewsfeed,
|
||||
TCreateProgramNewsfeedComment,
|
||||
TUpdateProgramNewsfeed,
|
||||
TUpdateProgramReaction
|
||||
} from '@cio/utils/validation/program';
|
||||
import {
|
||||
ZCreateProgramNewsfeed,
|
||||
ZCreateProgramNewsfeedComment,
|
||||
ZUpdateProgramNewsfeed,
|
||||
ZUpdateProgramReaction
|
||||
} from '@cio/utils/validation/program';
|
||||
import { get } from 'svelte/store';
|
||||
import { getTextFromHTML } from '$lib/utils/functions/toHtml';
|
||||
import { mapZodErrorsToTranslations } from '$lib/utils/validation';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
|
||||
export type ProgramNewsfeedCommentsByFeedId = {
|
||||
items: ProgramNewsfeedComment[];
|
||||
totalCount: number;
|
||||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
cursor: string | null;
|
||||
};
|
||||
|
||||
export class ProgramNewsfeedApi extends BaseApiWithErrors {
|
||||
feeds = $state<ListProgramNewsfeed>([]);
|
||||
|
||||
commentsByFeedId = $state<Record<string, ProgramNewsfeedCommentsByFeedId>>({});
|
||||
|
||||
isNewFeedModalOpen = $state(false);
|
||||
|
||||
openNewFeedModal() {
|
||||
this.isNewFeedModalOpen = true;
|
||||
}
|
||||
|
||||
closeNewFeedModal() {
|
||||
this.isNewFeedModalOpen = false;
|
||||
}
|
||||
|
||||
async list(programId: string) {
|
||||
await this.execute<ListProgramNewsfeedRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed.$get({
|
||||
param: { programId },
|
||||
query: {}
|
||||
}),
|
||||
logContext: 'listing program newsfeed',
|
||||
onSuccess: (response) => {
|
||||
this.feeds = response.data.items || [];
|
||||
this.commentsByFeedId = {};
|
||||
|
||||
for (const feed of this.feeds) {
|
||||
this.initComments(feed.id, feed.commentCount);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
snackbar.error('snackbar.program.error.news_feed_fail');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async create(programId: string, fields: TCreateProgramNewsfeed) {
|
||||
this.success = false;
|
||||
|
||||
const validationResult = ZCreateProgramNewsfeed.safeParse({
|
||||
...fields,
|
||||
content: getTextFromHTML(fields.content)
|
||||
});
|
||||
|
||||
if (!validationResult.success) {
|
||||
this.errors = mapZodErrorsToTranslations(validationResult.error, 'newsfeed');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.execute<CreateProgramNewsfeedRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed.$post({
|
||||
param: { programId },
|
||||
json: fields
|
||||
}),
|
||||
logContext: 'creating program newsfeed',
|
||||
onSuccess: (response) => {
|
||||
const author = get(profile);
|
||||
const newFeed = {
|
||||
...response.data,
|
||||
authorProfileId: author.id,
|
||||
authorFullname: author.fullname,
|
||||
authorUsername: author.username,
|
||||
authorAvatarUrl: author.avatarUrl,
|
||||
commentCount: 0
|
||||
};
|
||||
|
||||
this.feeds = [newFeed, ...this.feeds];
|
||||
this.initComments(newFeed.id, 0);
|
||||
snackbar.success('snackbar.program.success.post_created');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async update(programId: string, feedId: string, fields: TUpdateProgramNewsfeed) {
|
||||
this.success = false;
|
||||
|
||||
const validationResult = ZUpdateProgramNewsfeed.safeParse({
|
||||
...fields,
|
||||
...(fields.content ? { content: getTextFromHTML(fields.content) } : {})
|
||||
});
|
||||
|
||||
if (!validationResult.success) {
|
||||
this.errors = mapZodErrorsToTranslations(validationResult.error, 'newsfeed');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.execute<UpdateProgramNewsfeedRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed[':feedId'].$put({
|
||||
param: { programId, feedId },
|
||||
json: fields
|
||||
}),
|
||||
logContext: 'updating program newsfeed',
|
||||
onSuccess: (response) => {
|
||||
const index = this.feeds.findIndex((feed) => feed.id === feedId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingFeed = this.feeds[index];
|
||||
this.feeds[index] = {
|
||||
...existingFeed,
|
||||
...response.data,
|
||||
authorProfileId: existingFeed.authorProfileId,
|
||||
authorFullname: existingFeed.authorFullname,
|
||||
authorUsername: existingFeed.authorUsername,
|
||||
authorAvatarUrl: existingFeed.authorAvatarUrl,
|
||||
commentCount: existingFeed.commentCount
|
||||
};
|
||||
|
||||
snackbar.success('snackbar.program.success.post_updated');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async react(programId: string, feedId: string, reaction: TUpdateProgramReaction['reaction']) {
|
||||
const validationResult = ZUpdateProgramReaction.safeParse({ reaction });
|
||||
|
||||
if (!validationResult.success) {
|
||||
this.errors = mapZodErrorsToTranslations(validationResult.error, 'newsfeed');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.execute<ReactToProgramNewsfeedRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed[':feedId'].react.$put({
|
||||
param: { programId, feedId },
|
||||
json: validationResult.data
|
||||
}),
|
||||
logContext: 'reacting to program newsfeed',
|
||||
onSuccess: (response) => {
|
||||
const index = this.feeds.findIndex((feed) => feed.id === feedId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingFeed = this.feeds[index];
|
||||
this.feeds[index] = {
|
||||
...existingFeed,
|
||||
...response.data,
|
||||
authorProfileId: existingFeed.authorProfileId,
|
||||
authorFullname: existingFeed.authorFullname,
|
||||
authorUsername: existingFeed.authorUsername,
|
||||
authorAvatarUrl: existingFeed.authorAvatarUrl,
|
||||
commentCount: existingFeed.commentCount
|
||||
};
|
||||
},
|
||||
onError: () => {
|
||||
snackbar.error('snackbar.program.error.reaction_error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async delete(programId: string, feedId: string) {
|
||||
await this.execute<DeleteProgramNewsfeedRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed[':feedId'].$delete({
|
||||
param: { programId, feedId }
|
||||
}),
|
||||
logContext: 'deleting program newsfeed',
|
||||
onSuccess: () => {
|
||||
this.feeds = this.feeds.filter((feed) => feed.id !== feedId);
|
||||
|
||||
const { [feedId]: _removedComments, ...remainingComments } = this.commentsByFeedId;
|
||||
this.commentsByFeedId = remainingComments;
|
||||
|
||||
snackbar.success('snackbar.program.success.feed_delete_success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initComments(feedId: string, totalCount: number) {
|
||||
if (!this.commentsByFeedId[feedId]) {
|
||||
this.commentsByFeedId[feedId] = {
|
||||
items: [],
|
||||
totalCount,
|
||||
hasMore: false,
|
||||
isLoading: false,
|
||||
cursor: null
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this.commentsByFeedId[feedId].totalCount = totalCount;
|
||||
}
|
||||
|
||||
async getComments(programId: string, feedId: string) {
|
||||
if (!this.commentsByFeedId[feedId]) {
|
||||
this.initComments(feedId, 0);
|
||||
}
|
||||
|
||||
this.commentsByFeedId[feedId].isLoading = true;
|
||||
|
||||
await this.execute<GetProgramNewsfeedCommentsRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed[':feedId'].comments.$get({
|
||||
param: { programId, feedId }
|
||||
}),
|
||||
logContext: 'fetching program newsfeed comments',
|
||||
onSuccess: (response) => {
|
||||
this.commentsByFeedId[feedId] = {
|
||||
items: response.data,
|
||||
totalCount: response.data.length,
|
||||
hasMore: false,
|
||||
isLoading: false,
|
||||
cursor: null
|
||||
};
|
||||
},
|
||||
onError: () => {
|
||||
this.commentsByFeedId[feedId].isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createComment(
|
||||
programId: string,
|
||||
feedId: string,
|
||||
content: string,
|
||||
author: { id: string; username: string; fullname: string; avatarUrl: string }
|
||||
) {
|
||||
const validationResult = ZCreateProgramNewsfeedComment.safeParse({
|
||||
content: getTextFromHTML(content)
|
||||
} satisfies TCreateProgramNewsfeedComment);
|
||||
|
||||
if (!validationResult.success) {
|
||||
this.errors = mapZodErrorsToTranslations(validationResult.error, 'newsfeed');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.execute<CreateProgramNewsfeedCommentRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed[':feedId'].comment.$post({
|
||||
param: { programId, feedId },
|
||||
json: { content }
|
||||
}),
|
||||
logContext: 'creating program newsfeed comment',
|
||||
onSuccess: (response) => {
|
||||
const existingCommentState = this.commentsByFeedId[feedId];
|
||||
if (!existingCommentState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newComment = {
|
||||
...response.data,
|
||||
authorProfileId: author.id,
|
||||
authorFullname: author.fullname,
|
||||
authorUsername: author.username,
|
||||
authorAvatarUrl: author.avatarUrl
|
||||
};
|
||||
|
||||
this.commentsByFeedId[feedId] = {
|
||||
...existingCommentState,
|
||||
items: [newComment, ...existingCommentState.items],
|
||||
totalCount: existingCommentState.totalCount + 1
|
||||
};
|
||||
|
||||
const feedIndex = this.feeds.findIndex((feed) => feed.id === feedId);
|
||||
if (feedIndex !== -1) {
|
||||
this.feeds[feedIndex] = {
|
||||
...this.feeds[feedIndex],
|
||||
commentCount: this.feeds[feedIndex].commentCount + 1
|
||||
};
|
||||
}
|
||||
|
||||
snackbar.success('snackbar.program.success.comment_added');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async deleteComment(programId: string, feedId: string, commentId: string) {
|
||||
await this.execute<DeleteProgramNewsfeedCommentRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed[':feedId'].comment[':commentId'].$delete({
|
||||
param: { programId, feedId, commentId }
|
||||
}),
|
||||
logContext: 'deleting program newsfeed comment',
|
||||
onSuccess: () => {
|
||||
const existingCommentState = this.commentsByFeedId[feedId];
|
||||
if (!existingCommentState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.commentsByFeedId[feedId] = {
|
||||
...existingCommentState,
|
||||
items: existingCommentState.items.filter((item) => String(item.id) !== commentId),
|
||||
totalCount: Math.max(0, existingCommentState.totalCount - 1)
|
||||
};
|
||||
|
||||
const feedIndex = this.feeds.findIndex((feed) => feed.id === feedId);
|
||||
if (feedIndex !== -1) {
|
||||
this.feeds[feedIndex] = {
|
||||
...this.feeds[feedIndex],
|
||||
commentCount: Math.max(0, this.feeds[feedIndex].commentCount - 1)
|
||||
};
|
||||
}
|
||||
|
||||
snackbar.success('snackbar.program.success.comment_deleted');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const programNewsfeedApi = new ProgramNewsfeedApi();
|
||||
@@ -0,0 +1,330 @@
|
||||
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
|
||||
import type { Program, ProgramDetail, ProgramMember, ProgramCourse, ProgramNewsfeed } from '../utils/types';
|
||||
import type {
|
||||
TCreateProgram,
|
||||
TUpdateProgram,
|
||||
TAddProgramMembers,
|
||||
TUpdateProgramMember,
|
||||
TAddCourseToProgram
|
||||
} from '@cio/utils/validation/program';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { get } from 'svelte/store';
|
||||
import { isStudentExperience } from '$lib/utils/store/app';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
class ProgramApi extends BaseApiWithErrors {
|
||||
programs = $state<Program[]>([]);
|
||||
program = $state<ProgramDetail | null>(null);
|
||||
members = $state<ProgramMember[]>([]);
|
||||
courses = $state<ProgramCourse[]>([]);
|
||||
newsfeed = $state<ProgramNewsfeed | null>(null);
|
||||
isProgramShellLoading = $state(false);
|
||||
currentProgramId = $state<string | null>(null);
|
||||
|
||||
loadedProgramId = $state<string | null>(null);
|
||||
loadedMembersProgramId = $state<string | null>(null);
|
||||
loadedCoursesProgramId = $state<string | null>(null);
|
||||
loadedCoursesStudentExperience = $state<boolean | null>(null);
|
||||
loadedNewsfeedProgramId = $state<string | null>(null);
|
||||
|
||||
syncProgramCourseCount(programId: string, courseCount: number) {
|
||||
this.programs = this.programs.map((program) => (program.id === programId ? { ...program, courseCount } : program));
|
||||
}
|
||||
|
||||
async listPrograms() {
|
||||
const org = get(currentOrg);
|
||||
if (!org.id) return;
|
||||
|
||||
await this.execute<typeof classroomio.program.$get>({
|
||||
requestFn: () => classroomio.program.$get({ query: { organizationId: org.id } }),
|
||||
onSuccess: (data) => {
|
||||
this.programs = data.data;
|
||||
},
|
||||
logContext: 'listPrograms'
|
||||
});
|
||||
}
|
||||
|
||||
async getProgram(programId: string, force = false) {
|
||||
if (!force && this.loadedProgramId === programId) return;
|
||||
|
||||
await this.execute<(typeof classroomio.program)[':programId']['$get']>({
|
||||
requestFn: () => classroomio.program[':programId'].$get({ param: { programId } }),
|
||||
onSuccess: (data) => {
|
||||
this.program = data.data;
|
||||
this.loadedProgramId = programId;
|
||||
},
|
||||
logContext: 'getProgram'
|
||||
});
|
||||
}
|
||||
|
||||
resetProgramShell(programId?: string) {
|
||||
this.currentProgramId = programId ?? null;
|
||||
this.program = null;
|
||||
this.members = [];
|
||||
this.courses = [];
|
||||
this.newsfeed = null;
|
||||
this.loadedProgramId = null;
|
||||
this.loadedMembersProgramId = null;
|
||||
this.loadedCoursesProgramId = null;
|
||||
this.loadedCoursesStudentExperience = null;
|
||||
this.loadedNewsfeedProgramId = null;
|
||||
}
|
||||
|
||||
async ensureProgramShell(programId: string, force = false) {
|
||||
const studentExperience = get(isStudentExperience);
|
||||
const isShellReady =
|
||||
this.loadedProgramId === programId &&
|
||||
this.loadedMembersProgramId === programId &&
|
||||
this.loadedCoursesProgramId === programId &&
|
||||
this.loadedCoursesStudentExperience === studentExperience;
|
||||
|
||||
if (!force && isShellReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isProgramChanged = this.currentProgramId !== programId;
|
||||
if (force || isProgramChanged) {
|
||||
this.resetProgramShell(programId);
|
||||
}
|
||||
|
||||
this.currentProgramId = programId;
|
||||
this.isProgramShellLoading = true;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.getProgram(programId, force),
|
||||
this.listMembers(programId, force),
|
||||
this.listCourses(programId, force)
|
||||
]);
|
||||
} finally {
|
||||
this.isProgramShellLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async createProgram(data: TCreateProgram) {
|
||||
const org = get(currentOrg);
|
||||
if (!org.id) return;
|
||||
|
||||
await this.execute<typeof classroomio.program.$post>({
|
||||
requestFn: () =>
|
||||
classroomio.program.$post({
|
||||
json: { ...data, organizationId: org.id }
|
||||
}),
|
||||
onSuccess: (res) => {
|
||||
this.programs = [res.data, ...this.programs];
|
||||
snackbar.success(t.get('programs.create_success') || 'Program created');
|
||||
},
|
||||
logContext: 'createProgram'
|
||||
});
|
||||
}
|
||||
|
||||
async updateProgram(programId: string, data: TUpdateProgram) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['$put']>({
|
||||
requestFn: () => classroomio.program[':programId'].$put({ param: { programId }, json: data }),
|
||||
onSuccess: (res) => {
|
||||
this.program = res.data;
|
||||
this.programs = this.programs.map((p) => (p.id === programId ? { ...p, ...res.data } : p));
|
||||
snackbar.success(t.get('programs.update_success') || 'Program updated');
|
||||
},
|
||||
onError: (result) => {
|
||||
const message = typeof result === 'string' ? result : result.error;
|
||||
snackbar.error(message || 'Failed to update program');
|
||||
},
|
||||
logContext: 'updateProgram'
|
||||
});
|
||||
}
|
||||
|
||||
async deleteProgram(programId: string) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['$delete']>({
|
||||
requestFn: () => classroomio.program[':programId'].$delete({ param: { programId } }),
|
||||
onSuccess: () => {
|
||||
this.programs = this.programs.filter((p) => p.id !== programId);
|
||||
snackbar.success(t.get('programs.delete_success') || 'Program deleted');
|
||||
},
|
||||
logContext: 'deleteProgram'
|
||||
});
|
||||
}
|
||||
|
||||
// Members
|
||||
|
||||
async listMembers(programId: string, force = false) {
|
||||
if (!force && this.loadedMembersProgramId === programId) return;
|
||||
|
||||
await this.execute<(typeof classroomio.program)[':programId']['members']['$get']>({
|
||||
requestFn: () => classroomio.program[':programId'].members.$get({ param: { programId } }),
|
||||
onSuccess: (data) => {
|
||||
this.members = data.data;
|
||||
this.loadedMembersProgramId = programId;
|
||||
},
|
||||
logContext: 'listProgramMembers'
|
||||
});
|
||||
}
|
||||
|
||||
async addMembers(programId: string, data: TAddProgramMembers) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['members']['$post']>({
|
||||
requestFn: () => classroomio.program[':programId'].members.$post({ param: { programId }, json: data }),
|
||||
onSuccess: async () => {
|
||||
await this.listMembers(programId, true);
|
||||
snackbar.success(t.get('programs.members_added') || 'Members added');
|
||||
},
|
||||
logContext: 'addProgramMembers'
|
||||
});
|
||||
}
|
||||
|
||||
async updateMember(programId: string, memberId: string, data: TUpdateProgramMember) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['members'][':memberId']['$put']>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].members[':memberId'].$put({
|
||||
param: { programId, memberId },
|
||||
json: data
|
||||
}),
|
||||
onSuccess: (res) => {
|
||||
this.members = this.members.map((m) => (m.id === memberId ? { ...m, ...res.data } : m));
|
||||
},
|
||||
logContext: 'updateProgramMember'
|
||||
});
|
||||
}
|
||||
|
||||
async removeMember(programId: string, memberId: string) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['members'][':memberId']['$delete']>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].members[':memberId'].$delete({
|
||||
param: { programId, memberId }
|
||||
}),
|
||||
onSuccess: () => {
|
||||
this.members = this.members.filter((m) => m.id !== memberId);
|
||||
snackbar.success(t.get('programs.member_removed') || 'Member removed');
|
||||
},
|
||||
logContext: 'removeProgramMember'
|
||||
});
|
||||
}
|
||||
|
||||
// Courses
|
||||
|
||||
async listCourses(programId: string, force = false) {
|
||||
const studentExperience = get(isStudentExperience);
|
||||
|
||||
if (
|
||||
!force &&
|
||||
this.loadedCoursesProgramId === programId &&
|
||||
this.loadedCoursesStudentExperience === studentExperience
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.execute<(typeof classroomio.program)[':programId']['courses']['$get']>({
|
||||
requestFn: () => classroomio.program[':programId'].courses.$get({ param: { programId } }),
|
||||
onSuccess: (data) => {
|
||||
this.courses = data.data;
|
||||
this.syncProgramCourseCount(programId, data.data.length);
|
||||
this.loadedCoursesProgramId = programId;
|
||||
this.loadedCoursesStudentExperience = studentExperience;
|
||||
},
|
||||
logContext: 'listProgramCourses'
|
||||
});
|
||||
}
|
||||
|
||||
async addCourse(programId: string, data: TAddCourseToProgram) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['courses']['$post']>({
|
||||
requestFn: () => classroomio.program[':programId'].courses.$post({ param: { programId }, json: data }),
|
||||
onSuccess: async () => {
|
||||
await this.listCourses(programId, true);
|
||||
this.syncProgramCourseCount(programId, this.courses.length);
|
||||
snackbar.success(t.get('programs.course_added') || 'Course added');
|
||||
},
|
||||
logContext: 'addCourseToProgram'
|
||||
});
|
||||
}
|
||||
|
||||
async addCourses(programId: string, courseIds: string[]) {
|
||||
if (courseIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
this.success = false;
|
||||
|
||||
try {
|
||||
for (const courseId of courseIds) {
|
||||
const response = await classroomio.program[':programId'].courses.$post({
|
||||
param: { programId },
|
||||
json: { courseId }
|
||||
});
|
||||
const result = (await response.json()) as { success?: boolean; error?: string };
|
||||
|
||||
if (!result.success) {
|
||||
this.error = result.error ?? 'Failed to add course to program';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await this.listCourses(programId, true);
|
||||
this.syncProgramCourseCount(programId, this.courses.length);
|
||||
|
||||
const successMessage =
|
||||
courseIds.length === 1
|
||||
? t.get('programs.course_added') || 'Course added'
|
||||
: t.get('programs.multiple_added') || 'Courses added';
|
||||
|
||||
snackbar.success(successMessage);
|
||||
this.success = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error in addCoursesToProgram:', error);
|
||||
this.error = error instanceof Error ? error.message : 'Failed to add courses to program';
|
||||
return false;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async removeCourse(programId: string, courseId: string) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['courses'][':courseId']['$delete']>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].courses[':courseId'].$delete({
|
||||
param: { programId, courseId }
|
||||
}),
|
||||
onSuccess: () => {
|
||||
this.courses = this.courses.filter((c) => c.courseId !== courseId);
|
||||
this.syncProgramCourseCount(programId, this.courses.length);
|
||||
snackbar.success(t.get('programs.course_removed') || 'Course removed');
|
||||
},
|
||||
logContext: 'removeCourseFromProgram'
|
||||
});
|
||||
}
|
||||
|
||||
// Newsfeed
|
||||
|
||||
async listNewsfeed(programId: string, force = false, cursor?: string) {
|
||||
if (!force && this.loadedNewsfeedProgramId === programId) return;
|
||||
|
||||
await this.execute<(typeof classroomio.program)[':programId']['newsfeed']['$get']>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed.$get({
|
||||
param: { programId },
|
||||
query: { limit: '10', ...(cursor ? { cursor } : {}) }
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
this.newsfeed = data.data;
|
||||
this.loadedNewsfeedProgramId = programId;
|
||||
},
|
||||
logContext: 'listProgramNewsfeed'
|
||||
});
|
||||
}
|
||||
|
||||
// LMS — enrolled programs
|
||||
|
||||
async listEnrolledPrograms() {
|
||||
await this.execute<typeof classroomio.program.enrolled.$get>({
|
||||
requestFn: () => classroomio.program.enrolled.$get({}),
|
||||
onSuccess: (data) => {
|
||||
this.programs = data.data;
|
||||
},
|
||||
logContext: 'listEnrolledPrograms'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const programApi = new ProgramApi();
|
||||
Reference in New Issue
Block a user