feat(v3): restructure project + add complete frontend

- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
commit 11a7265460
392 changed files with 62287 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
export * from "./keys";
export * from "./useCourseware";
export * from "./useCommunication";
export * from "./useNotifications";
export * from "./useFaq";
export * from "./useLms";
export * from "./useUsers";
export * from "./useExams";
export * from "./useAssignments";
export * from "./useEntities";
export * from "./useAdaptive";
export * from "./useAcademic";
export * from "./useAdmissions";
export * from "./useInstitutionalExams";
export * from "./useStudentLeave";
export * from "./useFees";
export * from "./useLessons";
export * from "./useGradebook";
export * from "./useStudentProgress";
export * from "./useLibrary";
export * from "./useActivities";
export * from "./useFacilities";
export * from "./useExamTemplates";
export * from "./useEntityOnboarding";
export * from "./useAiCourse";
export * from "./useAdaptiveEngine";
export * from "./useScoreRelease";
export { usePermissions } from "../usePermissions";

304
src/hooks/queries/keys.ts Normal file
View File

@@ -0,0 +1,304 @@
export const queryKeys = {
auth: {
user: ["auth", "user"] as const,
},
users: {
all: ["users"] as const,
list: (params: Record<string, unknown>) => ["users", "list", params] as const,
detail: (id: number) => ["users", id] as const,
},
entities: {
all: ["entities"] as const,
list: (params?: Record<string, unknown>) => ["entities", "list", params] as const,
detail: (id: number) => ["entities", id] as const,
roles: (entityId: number) => ["entities", entityId, "roles"] as const,
permissions: (entityId: number) => ["entities", entityId, "permissions"] as const,
},
exams: {
all: ["exams"] as const,
list: (module: string, params?: Record<string, unknown>) => ["exams", module, params] as const,
detail: (module: string, id: number) => ["exams", module, id] as const,
rubrics: (params?: Record<string, unknown>) => ["exams", "rubrics", params] as const,
rubricGroups: (params?: Record<string, unknown>) => ["exams", "rubric-groups", params] as const,
structures: (params?: Record<string, unknown>) => ["exams", "structures", params] as const,
avatars: ["exams", "avatars"] as const,
},
assignments: {
all: ["assignments"] as const,
list: (params?: Record<string, unknown>) => ["assignments", "list", params] as const,
detail: (id: number) => ["assignments", id] as const,
},
classrooms: {
all: ["classrooms"] as const,
list: (params?: Record<string, unknown>) => ["classrooms", "list", params] as const,
detail: (id: number) => ["classrooms", id] as const,
},
stats: {
sessions: (params?: Record<string, unknown>) => ["stats", "sessions", params] as const,
stats: (params?: Record<string, unknown>) => ["stats", "stats", params] as const,
statistical: (params?: Record<string, unknown>) => ["stats", "statistical", params] as const,
performance: (params?: Record<string, unknown>) => ["stats", "performance", params] as const,
},
training: {
all: ["training"] as const,
tips: (params?: Record<string, unknown>) => ["training", "tips", params] as const,
walkthroughs: (module?: string) => ["training", "walkthroughs", module] as const,
},
subscriptions: {
packages: ["subscriptions", "packages"] as const,
payments: (params?: Record<string, unknown>) => ["subscriptions", "payments", params] as const,
discounts: (params?: Record<string, unknown>) => ["subscriptions", "discounts", params] as const,
},
tickets: {
all: ["tickets"] as const,
list: (params?: Record<string, unknown>) => ["tickets", "list", params] as const,
assigned: ["tickets", "assigned"] as const,
},
approvals: {
all: ["approvals"] as const,
list: (params?: Record<string, unknown>) => ["approvals", "list", params] as const,
},
taxonomy: {
subjects: ["taxonomy", "subjects"] as const,
subject: (id: number) => ["taxonomy", "subjects", id] as const,
tree: (subjectId: number) => ["taxonomy", "tree", subjectId] as const,
domains: (params?: Record<string, unknown>) => ["taxonomy", "domains", params] as const,
topics: (params?: Record<string, unknown>) => ["taxonomy", "topics", params] as const,
},
adaptive: {
proficiency: (params?: Record<string, unknown>) => ["adaptive", "proficiency", params] as const,
summary: ["adaptive", "proficiency", "summary"] as const,
plan: (params?: Record<string, unknown>) => ["adaptive", "plan", params] as const,
topicContent: (topicId: number) => ["adaptive", "topic", topicId, "content"] as const,
diagnostic: (sessionId: number) => ["adaptive", "diagnostic", sessionId] as const,
},
resources: {
all: ["resources"] as const,
list: (params?: Record<string, unknown>) => ["resources", "list", params] as const,
},
lms: {
courses: (params?: Record<string, unknown>) => ["lms", "courses", params] as const,
course: (id: number) => ["lms", "courses", id] as const,
students: (params?: Record<string, unknown>) => ["lms", "students", params] as const,
teachers: (params?: Record<string, unknown>) => ["lms", "teachers", params] as const,
batches: (params?: Record<string, unknown>) => ["lms", "batches", params] as const,
batch: (id: number) => ["lms", "batches", id] as const,
timetable: (params?: Record<string, unknown>) => ["lms", "timetable", params] as const,
attendance: (params?: Record<string, unknown>) => ["lms", "attendance", params] as const,
grades: (params?: Record<string, unknown>) => ["lms", "grades", params] as const,
},
analytics: {
student: (params?: Record<string, unknown>) => ["analytics", "student", params] as const,
class: (params?: Record<string, unknown>) => ["analytics", "class", params] as const,
subject: (params?: Record<string, unknown>) => ["analytics", "subject", params] as const,
contentGaps: (params?: Record<string, unknown>) => ["analytics", "content-gaps", params] as const,
alerts: ["analytics", "alerts"] as const,
},
academicYears: {
all: ["academic-years"] as const,
list: (params?: Record<string, unknown>) => ["academic-years", "list", params] as const,
detail: (id: number) => ["academic-years", id] as const,
},
academicTerms: {
all: ["academic-terms"] as const,
list: (params?: Record<string, unknown>) => ["academic-terms", "list", params] as const,
detail: (id: number) => ["academic-terms", id] as const,
},
departments: {
all: ["departments"] as const,
list: (params?: Record<string, unknown>) => ["departments", "list", params] as const,
detail: (id: number) => ["departments", id] as const,
},
admissionRegisters: {
all: ["admission-registers"] as const,
list: (params?: Record<string, unknown>) => ["admission-registers", "list", params] as const,
detail: (id: number) => ["admission-registers", id] as const,
},
admissions: {
all: ["admissions"] as const,
list: (params?: Record<string, unknown>) => ["admissions", "list", params] as const,
detail: (id: number) => ["admissions", id] as const,
},
instExamSessions: {
all: ["inst-exam-sessions"] as const,
list: (params?: Record<string, unknown>) => ["inst-exam-sessions", "list", params] as const,
detail: (id: number) => ["inst-exam-sessions", id] as const,
},
examTypes: {
all: ["exam-types"] as const,
list: (params?: Record<string, unknown>) => ["exam-types", "list", params] as const,
},
gradeConfigs: {
all: ["grade-configs"] as const,
list: (params?: Record<string, unknown>) => ["grade-configs", "list", params] as const,
},
resultTemplates: {
all: ["result-templates"] as const,
list: (params?: Record<string, unknown>) => ["result-templates", "list", params] as const,
},
marksheets: {
all: ["marksheets"] as const,
list: (params?: Record<string, unknown>) => ["marksheets", "list", params] as const,
detail: (id: number) => ["marksheets", id] as const,
},
courseAssignments: {
all: ["course-assignments"] as const,
list: (params?: Record<string, unknown>) => ["course-assignments", "list", params] as const,
detail: (id: number) => ["course-assignments", id] as const,
},
assignmentTypes: {
all: ["assignment-types"] as const,
list: (params?: Record<string, unknown>) => ["assignment-types", "list", params] as const,
},
subjectRegistrations: {
all: ["subject-registrations"] as const,
list: (params?: Record<string, unknown>) => ["subject-registrations", "list", params] as const,
available: (params?: Record<string, unknown>) => ["subject-registrations", "available", params] as const,
},
chapters: (courseId: number) => ["chapters", "list", courseId] as const,
chapter: (id: number) => ["chapters", "detail", id] as const,
chapterMaterials: (chapterId: number) => ["chapter-materials", chapterId] as const,
chapterProgress: (chapterId: number) => ["chapter-progress", chapterId] as const,
discussionBoards: (params?: Record<string, unknown>) => ["discussion-boards", params] as const,
posts: (boardId: number, params?: Record<string, unknown>) => ["posts", boardId, params] as const,
announcements: (params?: Record<string, unknown>) => ["announcements", params] as const,
messages: (params?: Record<string, unknown>) => ["messages", params] as const,
sentMessages: (params?: Record<string, unknown>) => ["messages", "sent", params] as const,
unreadMessages: ["messages", "unread-count"] as const,
notifications: (params?: Record<string, unknown>) => ["notifications", params] as const,
unreadNotifications: ["notifications", "unread-count"] as const,
notificationRules: ["notification-rules"] as const,
notificationPreferences: ["notification-preferences"] as const,
faqCategories: (audience?: string) => ["faq", "categories", audience] as const,
faqItems: (params?: Record<string, unknown>) => ["faq", "items", params] as const,
studentLeaves: {
all: ["student-leaves"] as const,
list: (params?: Record<string, unknown>) => ["student-leaves", "list", params] as const,
},
studentLeaveTypes: {
all: ["student-leave-types"] as const,
list: (params?: Record<string, unknown>) => ["student-leave-types", "list", params] as const,
},
feesPlans: {
all: ["fees-plans"] as const,
list: (params?: Record<string, unknown>) => ["fees-plans", "list", params] as const,
detail: (id: number) => ["fees-plans", id] as const,
},
studentFees: {
all: ["student-fees"] as const,
list: (params?: Record<string, unknown>) => ["student-fees", "list", params] as const,
},
feesTerms: {
all: ["fees-terms"] as const,
list: (params?: Record<string, unknown>) => ["fees-terms", "list", params] as const,
},
lessons: {
all: ["lessons"] as const,
list: (params?: Record<string, unknown>) => ["lessons", "list", params] as const,
},
gradebooks: {
all: ["gradebooks"] as const,
list: (params?: Record<string, unknown>) => ["gradebooks", "list", params] as const,
},
gradebookLines: {
all: ["gradebook-lines"] as const,
list: (params?: Record<string, unknown>) => ["gradebook-lines", "list", params] as const,
},
gradingAssignments: {
all: ["grading-assignments"] as const,
list: (params?: Record<string, unknown>) => ["grading-assignments", "list", params] as const,
},
studentProgress: {
all: ["student-progress"] as const,
list: (params?: Record<string, unknown>) => ["student-progress", "list", params] as const,
},
libraryMedia: {
all: ["library-media"] as const,
list: (params?: Record<string, unknown>) => ["library-media", "list", params] as const,
},
libraryMovements: {
all: ["library-movements"] as const,
list: (params?: Record<string, unknown>) => ["library-movements", "list", params] as const,
},
libraryCards: {
all: ["library-cards"] as const,
list: (params?: Record<string, unknown>) => ["library-cards", "list", params] as const,
},
activities: {
all: ["activities"] as const,
list: (params?: Record<string, unknown>) => ["activities", "list", params] as const,
},
activityTypes: {
all: ["activity-types"] as const,
list: (params?: Record<string, unknown>) => ["activity-types", "list", params] as const,
},
facilities: {
all: ["facilities"] as const,
list: (params?: Record<string, unknown>) => ["facilities", "list", params] as const,
},
assets: {
all: ["assets"] as const,
list: (params?: Record<string, unknown>) => ["assets", "list", params] as const,
},
signup: {
goals: ["signup", "goals"] as const,
},
placement: {
results: (sessionId: string) => ["placement", "results", sessionId] as const,
speakingStatus: (sessionId: string) => ["placement", "speaking-status", sessionId] as const,
learningPath: (sessionId: string) => ["placement", "learning-path", sessionId] as const,
},
examTemplates: {
international: ["exam-templates", "international"] as const,
custom: ["exam-templates", "custom"] as const,
detail: (id: number) => ["exam-templates", id] as const,
},
ieltsExam: {
skills: (examId: number) => ["ielts-exam", examId, "skills"] as const,
contentPool: (examId: number, filters?: Record<string, unknown>) => ["ielts-exam", examId, "content-pool", filters] as const,
validation: (examId: number) => ["ielts-exam", examId, "validation"] as const,
},
examSession: {
session: (examId: number) => ["exam-session", examId] as const,
status: (examId: number) => ["exam-session", examId, "status"] as const,
},
grading: {
queue: (params?: Record<string, unknown>) => ["grading", "queue", params] as const,
response: (attemptId: number, skill: string) => ["grading", attemptId, "response", skill] as const,
rubric: (attemptId: number, skill: string) => ["grading", attemptId, "rubric", skill] as const,
},
courseGeneration: {
gapAnalysis: (source: string, sourceId: number) => ["course-gen", "gap", source, sourceId] as const,
course: (id: number) => ["course-gen", "course", id] as const,
},
aiCourse: {
course: (id: number) => ["ai-course", id] as const,
tracks: (id: number) => ["ai-course", id, "tracks"] as const,
quality: (id: number) => ["ai-course", id, "quality"] as const,
ieltsValidation: (id: number) => ["ai-course", id, "ielts-validation"] as const,
englishTaxonomy: ["ai-course", "english", "taxonomy"] as const,
},
entityOnboarding: {
credentials: (params?: Record<string, unknown>) => ["entity-onboarding", "credentials", params] as const,
},
adaptiveEngine: {
dashboard: ["adaptive-engine", "dashboard"] as const,
students: (params?: Record<string, unknown>) => ["adaptive-engine", "students", params] as const,
signals: (studentId: number) => ["adaptive-engine", "signals", studentId] as const,
ability: (studentId: number) => ["adaptive-engine", "ability", studentId] as const,
settings: ["adaptive-engine", "settings"] as const,
},
levelMapping: {
get: (entityId: number) => ["level-mapping", entityId] as const,
},
branding: {
get: (entityId: number) => ["branding", entityId] as const,
current: ["branding", "current"] as const,
},
scoreRelease: {
pending: (params?: Record<string, unknown>) => ["score-release", "pending", params] as const,
},
verification: {
verify: (hash: string) => ["verification", hash] as const,
},
};

View File

@@ -0,0 +1,104 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { academicService } from "@/services/academic.service";
import type { PaginationParams } from "@/types";
import type {
AcademicYearCreateRequest,
AcademicTermCreateRequest,
DepartmentCreateRequest,
} from "@/types/academic";
export function useAcademicYears(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.academicYears.list(params),
queryFn: () => academicService.listAcademicYears(params),
});
}
export function useAcademicYear(id: number) {
return useQuery({
queryKey: queryKeys.academicYears.detail(id),
queryFn: () => academicService.getAcademicYear(id),
enabled: !!id,
});
}
export function useAcademicTerms(yearId?: number) {
return useQuery({
queryKey: queryKeys.academicTerms.list({ year_id: yearId }),
queryFn: () => academicService.listTerms({ year_id: yearId }),
});
}
export function useDepartments(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.departments.list(params),
queryFn: () => academicService.listDepartments(params),
});
}
export function useCreateAcademicYear() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: AcademicYearCreateRequest) => academicService.createAcademicYear(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicYears.all }),
});
}
export function useUpdateAcademicYear() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<AcademicYearCreateRequest> }) =>
academicService.updateAcademicYear(id, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicYears.all }),
});
}
export function useDeleteAcademicYear() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => academicService.deleteAcademicYear(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicYears.all }),
});
}
export function useGenerateTerms() {
const qc = useQueryClient();
return useMutation({
mutationFn: (yearId: number) => academicService.generateTerms(yearId),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicTerms.all }),
});
}
export function useCreateAcademicTerm() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: AcademicTermCreateRequest) => academicService.createTerm(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicTerms.all }),
});
}
export function useCreateDepartment() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: DepartmentCreateRequest) => academicService.createDepartment(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.departments.all }),
});
}
export function useUpdateDepartment() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<DepartmentCreateRequest> }) =>
academicService.updateDepartment(id, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.departments.all }),
});
}
export function useDeleteDepartment() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => academicService.deleteDepartment(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.departments.all }),
});
}

View File

@@ -0,0 +1,60 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { activityService } from "@/services/activity.service";
import type { PaginationParams } from "@/types";
import type { ActivityCreateRequest, ActivityType } from "@/types/activity";
export function useActivities(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.activities.list(params),
queryFn: () => activityService.listActivities(params),
});
}
export function useActivityTypes(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.activityTypes.list(params),
queryFn: () => activityService.listActivityTypes(params),
});
}
export function useCreateActivity() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: ActivityCreateRequest) => activityService.createActivity(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.activities.all }),
});
}
export function useUpdateActivity() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<ActivityCreateRequest> }) =>
activityService.updateActivity(id, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.activities.all }),
});
}
export function useDeleteActivity() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => activityService.deleteActivity(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.activities.all }),
});
}
export function useCreateActivityType() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: Pick<ActivityType, "name">) => activityService.createActivityType(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.activityTypes.all }),
});
}
export function useDeleteActivityType() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => activityService.deleteActivityType(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.activityTypes.all }),
});
}

View File

@@ -0,0 +1,81 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { adaptiveService } from "@/services";
import { taxonomyService } from "@/services";
export function useSubjects() {
return useQuery({
queryKey: queryKeys.taxonomy.subjects,
queryFn: () => taxonomyService.listSubjects(),
});
}
export function useTaxonomyTree(subjectId: number) {
return useQuery({
queryKey: queryKeys.taxonomy.tree(subjectId),
queryFn: () => taxonomyService.getTaxonomyTree(subjectId),
enabled: !!subjectId,
});
}
export function useProficiency(params?: { subject_id?: number }) {
return useQuery({
queryKey: queryKeys.adaptive.proficiency(params),
queryFn: () => adaptiveService.getProficiency(params ?? {}),
});
}
export function useProficiencySummary() {
return useQuery({
queryKey: queryKeys.adaptive.summary,
queryFn: () => adaptiveService.getProficiencySummary(),
});
}
export function useLearningPlan(params?: { subject_id?: number }) {
return useQuery({
queryKey: queryKeys.adaptive.plan(params),
queryFn: () => adaptiveService.getLearningPlan(params ?? {}),
enabled: !!params?.subject_id,
});
}
export function useTopicContent(topicId: number) {
return useQuery({
queryKey: queryKeys.adaptive.topicContent(topicId),
queryFn: () => adaptiveService.getTopicContent(topicId),
enabled: !!topicId,
});
}
export function useStartDiagnostic() {
return useMutation({
mutationFn: (subjectId: number) => adaptiveService.startDiagnostic(subjectId),
});
}
export function useAnswerDiagnostic() {
return useMutation({
mutationFn: adaptiveService.answerDiagnostic,
});
}
export function useGenerateLearningPlan() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: { subject_id: number; target_completion?: string }) =>
adaptiveService.generateLearningPlan(data),
onSuccess: () => qc.invalidateQueries({ queryKey: ["adaptive", "plan"] }),
});
}
export function useSubmitMasteryQuiz() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ topicId, answers }: { topicId: number; answers: unknown[] }) =>
adaptiveService.submitMasteryQuiz(topicId, answers),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["adaptive"] });
},
});
}

View File

@@ -0,0 +1,51 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { adaptiveEngineService } from "@/services/adaptive-engine.service";
import type { AdaptiveThresholdSettings } from "@/types";
export function useAdaptiveDashboard() {
return useQuery({
queryKey: queryKeys.adaptiveEngine.dashboard,
queryFn: () => adaptiveEngineService.getDashboard(),
});
}
export function useAdaptiveStudents(params?: { page?: number; limit?: number }) {
return useQuery({
queryKey: queryKeys.adaptiveEngine.students(params as Record<string, unknown> | undefined),
queryFn: () => adaptiveEngineService.getStudents(params),
});
}
export function useStudentSignals(studentId: number | undefined) {
return useQuery({
queryKey: queryKeys.adaptiveEngine.signals(studentId ?? 0),
queryFn: () => adaptiveEngineService.getStudentSignals(studentId!),
enabled: !!studentId && studentId > 0,
});
}
export function useStudentAbility(studentId: number | undefined) {
return useQuery({
queryKey: queryKeys.adaptiveEngine.ability(studentId ?? 0),
queryFn: () => adaptiveEngineService.getStudentAbility(studentId!),
enabled: !!studentId && studentId > 0,
});
}
export function useAdaptiveSettings() {
return useQuery({
queryKey: queryKeys.adaptiveEngine.settings,
queryFn: () => adaptiveEngineService.getSettings(),
});
}
export function useUpdateAdaptiveSettings() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: AdaptiveThresholdSettings) => adaptiveEngineService.updateSettings(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: queryKeys.adaptiveEngine.settings });
},
});
}

View File

@@ -0,0 +1,94 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { admissionService } from "@/services/admission.service";
import type { PaginationParams } from "@/types";
import type {
AdmissionRegisterCreateRequest,
AdmissionCreateRequest,
} from "@/types/admission";
export function useAdmissionRegisters(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.admissionRegisters.list(params),
queryFn: () => admissionService.listRegisters(params),
});
}
export function useAdmissions(params?: PaginationParams & { state?: string; register_id?: number; course_id?: number }) {
return useQuery({
queryKey: queryKeys.admissions.list(params),
queryFn: () => admissionService.listAdmissions(params),
});
}
export function useAdmission(id: number) {
return useQuery({
queryKey: queryKeys.admissions.detail(id),
queryFn: () => admissionService.getAdmission(id),
enabled: !!id,
});
}
export function useCreateAdmissionRegister() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: AdmissionRegisterCreateRequest) => admissionService.createRegister(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissionRegisters.all }),
});
}
export function useConfirmRegister() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => admissionService.confirmRegister(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissionRegisters.all }),
});
}
export function useCloseRegister() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => admissionService.closeRegister(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissionRegisters.all }),
});
}
export function useCreateAdmission() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: AdmissionCreateRequest) => admissionService.createAdmission(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }),
});
}
export function useSubmitAdmission() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => admissionService.submitAdmission(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }),
});
}
export function useConfirmAdmission() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => admissionService.confirmAdmission(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }),
});
}
export function useAdmitAdmission() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => admissionService.admitAdmission(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }),
});
}
export function useRejectAdmission() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => admissionService.rejectAdmission(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }),
});
}

View File

@@ -0,0 +1,97 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { aiCourseService } from "@/services/ai-course.service";
import type { ExaminerReview } from "@/types";
export function useAiCourse(courseId: number | undefined) {
return useQuery({
queryKey: queryKeys.aiCourse.course(courseId ?? 0),
queryFn: () => aiCourseService.getCourse(courseId!),
enabled: !!courseId && courseId > 0,
});
}
export function useAiCourseTracks(courseId: number | undefined) {
return useQuery({
queryKey: queryKeys.aiCourse.tracks(courseId ?? 0),
queryFn: () => aiCourseService.getTracks(courseId!),
enabled: !!courseId && courseId > 0,
});
}
export function useCreateEnglishCourse() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: { current_level: string; target_level: string; learning_style: string[] }) =>
aiCourseService.createEnglish(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["ai-course"] });
},
});
}
export function useCreateIeltsCourse() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: { exam_type: string; target_band: number; skills: string[] }) =>
aiCourseService.createIelts(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["ai-course"] });
},
});
}
export function useQualityGate(courseId: number | undefined) {
return useQuery({
queryKey: queryKeys.aiCourse.quality(courseId ?? 0),
queryFn: () => aiCourseService.getQualityGate(courseId!),
enabled: !!courseId && courseId > 0,
});
}
export function useApproveQuality() {
const qc = useQueryClient();
return useMutation({
mutationFn: (courseId: number) => aiCourseService.approveQuality(courseId),
onSuccess: (_d, courseId) => {
qc.invalidateQueries({ queryKey: queryKeys.aiCourse.quality(courseId) });
qc.invalidateQueries({ queryKey: queryKeys.aiCourse.course(courseId) });
},
});
}
export function useRejectQuality() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ courseId, notes }: { courseId: number; notes: string }) =>
aiCourseService.rejectQuality(courseId, notes),
onSuccess: (_d, { courseId }) => {
qc.invalidateQueries({ queryKey: queryKeys.aiCourse.quality(courseId) });
},
});
}
export function useEnglishTaxonomy() {
return useQuery({
queryKey: queryKeys.aiCourse.englishTaxonomy,
queryFn: () => aiCourseService.getEnglishTaxonomy(),
});
}
export function useIeltsValidation(courseId: number | undefined) {
return useQuery({
queryKey: queryKeys.aiCourse.ieltsValidation(courseId ?? 0),
queryFn: () => aiCourseService.getIeltsValidation(courseId!),
enabled: !!courseId && courseId > 0,
});
}
export function useSubmitExaminerReview() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: ExaminerReview) => aiCourseService.submitExaminerReview(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["ai-course"] });
},
});
}

View File

@@ -0,0 +1,27 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { assignmentsService, type AssignmentListParams } from "@/services/assignments.service";
import type { AssignmentCreateRequest } from "@/types";
export function useAssignments(params?: AssignmentListParams) {
return useQuery({
queryKey: queryKeys.assignments.list(params),
queryFn: () => assignmentsService.list(params),
});
}
export function useAssignment(id: number) {
return useQuery({
queryKey: queryKeys.assignments.detail(id),
queryFn: () => assignmentsService.getById(id),
enabled: !!id,
});
}
export function useCreateAssignment() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: AssignmentCreateRequest) => assignmentsService.create(data),
onSuccess: () => qc.invalidateQueries({ queryKey: ["assignments"] }),
});
}

View File

@@ -0,0 +1,140 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { communicationService } from "@/services/communication.service";
import type {
DiscussionBoard,
PostCreateRequest,
AnnouncementCreateRequest,
MessageCreateRequest,
} from "@/types/communication";
import type { PaginationParams } from "@/types";
export function useDiscussionBoards(params?: { course_id?: number; batch_id?: number }) {
return useQuery({
queryKey: queryKeys.discussionBoards(params),
queryFn: () => communicationService.listDiscussionBoards(params),
});
}
export function usePosts(boardId: number, params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.posts(boardId, params),
queryFn: () => communicationService.listPosts(boardId, params),
enabled: !!boardId,
});
}
export function useAnnouncements(params?: { course_id?: number; priority?: string }) {
return useQuery({
queryKey: queryKeys.announcements(params),
queryFn: () => communicationService.listAnnouncements(params),
});
}
export function useMessages(params?: { is_read?: boolean } & PaginationParams) {
return useQuery({
queryKey: queryKeys.messages(params),
queryFn: () => communicationService.listMessages(params),
});
}
export function useSentMessages(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.sentMessages(params),
queryFn: () => communicationService.listSentMessages(params),
});
}
export function useUnreadMessageCount() {
return useQuery({
queryKey: queryKeys.unreadMessages,
queryFn: () => communicationService.getUnreadMessageCount(),
});
}
export function useCreateBoard() {
const qc = useQueryClient();
return useMutation({
mutationFn: (
data: Partial<DiscussionBoard> & Pick<DiscussionBoard, "name" | "course_id">,
) => communicationService.createDiscussionBoard(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["discussion-boards"] });
},
});
}
export function useCreatePost() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ boardId, data }: { boardId: number; data: PostCreateRequest }) =>
communicationService.createPost(boardId, data),
onSuccess: (_, { boardId }) => {
qc.invalidateQueries({ queryKey: ["posts", boardId] });
qc.invalidateQueries({ queryKey: ["discussion-boards"] });
},
});
}
export function usePinPost() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, body }: { id: number; body?: { pinned?: boolean } }) =>
communicationService.pinPost(id, body),
onSuccess: (post) => {
qc.invalidateQueries({ queryKey: ["posts", post.board_id] });
},
});
}
export function useResolvePost() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => communicationService.resolvePost(id),
onSuccess: (post) => {
qc.invalidateQueries({ queryKey: ["posts", post.board_id] });
},
});
}
export function useCreateAnnouncement() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: AnnouncementCreateRequest) => communicationService.createAnnouncement(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["announcements"] });
},
});
}
export function usePublishAnnouncement() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => communicationService.publishAnnouncement(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["announcements"] });
},
});
}
export function useSendMessage() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: MessageCreateRequest) => communicationService.sendMessage(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["messages"] });
qc.invalidateQueries({ queryKey: queryKeys.unreadMessages });
},
});
}
export function useDeleteMessage() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => communicationService.deleteMessage(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["messages"] });
qc.invalidateQueries({ queryKey: queryKeys.unreadMessages });
},
});
}

View File

@@ -0,0 +1,48 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { courseGenerationService } from "@/services/course-generation.service";
import type { CourseConfig, CourseProgress } from "@/types";
export function useGapAnalysis(source: "exam" | "placement", sourceId: number) {
return useQuery({
queryKey: queryKeys.courseGeneration.gapAnalysis(source, sourceId),
queryFn: () => courseGenerationService.getGapAnalysis(source, sourceId),
enabled: !!sourceId,
});
}
export function useAutoGenerateCourse() {
return useMutation({
mutationFn: (sourceId: number) => courseGenerationService.autoGenerate(sourceId),
});
}
export function useCreateCourse() {
return useMutation({
mutationFn: (data: Partial<CourseConfig>) => courseGenerationService.create(data),
});
}
export function useCourseDetail(courseId: number) {
return useQuery({
queryKey: queryKeys.courseGeneration.course(courseId),
queryFn: () => courseGenerationService.getCourse(courseId),
enabled: !!courseId,
});
}
export function useUpdateCourse() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: { courseId: number; payload: Partial<CourseConfig> }) =>
courseGenerationService.updateCourse(data.courseId, data.payload),
onSuccess: (_, vars) => qc.invalidateQueries({ queryKey: queryKeys.courseGeneration.course(vars.courseId) }),
});
}
export function useTrackProgress() {
return useMutation({
mutationFn: (data: { courseId: number; progress: CourseProgress }) =>
courseGenerationService.trackProgress(data.courseId, data.progress),
});
}

View File

@@ -0,0 +1,204 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { coursewareService } from "@/services/courseware.service";
import type {
ChapterCreateRequest,
WorkbenchGenerateRequest,
} from "@/types/courseware";
export function useChapters(courseId: number) {
return useQuery({
queryKey: queryKeys.chapters(courseId),
queryFn: () => coursewareService.listChapters(courseId),
enabled: !!courseId,
});
}
export function useChapter(id: number) {
return useQuery({
queryKey: queryKeys.chapter(id),
queryFn: () => coursewareService.getChapter(id),
enabled: !!id,
});
}
export function useChapterMaterials(chapterId: number) {
return useQuery({
queryKey: queryKeys.chapterMaterials(chapterId),
queryFn: () => coursewareService.listMaterials(chapterId),
enabled: !!chapterId,
});
}
export function useChapterProgress(chapterId: number) {
return useQuery({
queryKey: queryKeys.chapterProgress(chapterId),
queryFn: () => coursewareService.getChapterProgress(chapterId),
enabled: !!chapterId,
});
}
export function useCreateChapter() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ courseId, data }: { courseId: number; data: ChapterCreateRequest }) =>
coursewareService.createChapter(courseId, data),
onSuccess: (_, { courseId }) => {
qc.invalidateQueries({ queryKey: queryKeys.chapters(courseId) });
},
});
}
export function useUpdateChapter() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<ChapterCreateRequest> }) =>
coursewareService.updateChapter(id, data),
onSuccess: (chapter) => {
qc.invalidateQueries({ queryKey: queryKeys.chapter(chapter.id) });
qc.invalidateQueries({ queryKey: queryKeys.chapters(chapter.course_id) });
},
});
}
export function useDeleteChapter() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, courseId }: { id: number; courseId: number }) =>
coursewareService.deleteChapter(id),
onSuccess: (_, { courseId }) => {
qc.invalidateQueries({ queryKey: queryKeys.chapters(courseId) });
},
});
}
export function useUnlockChapter() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, courseId }: { id: number; courseId: number }) =>
coursewareService.unlockChapter(id),
onSuccess: (_, { id, courseId }) => {
qc.invalidateQueries({ queryKey: queryKeys.chapter(id) });
qc.invalidateQueries({ queryKey: queryKeys.chapters(courseId) });
},
});
}
export function useLockChapter() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, courseId }: { id: number; courseId: number }) =>
coursewareService.lockChapter(id),
onSuccess: (_, { id, courseId }) => {
qc.invalidateQueries({ queryKey: queryKeys.chapter(id) });
qc.invalidateQueries({ queryKey: queryKeys.chapters(courseId) });
},
});
}
export function useReorderChapters() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ courseId, body }: { courseId: number; body: { ids: number[] } }) =>
coursewareService.reorderChapters(courseId, body),
onSuccess: (_, { courseId }) => {
qc.invalidateQueries({ queryKey: queryKeys.chapters(courseId) });
},
});
}
export function useUploadMaterial() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ chapterId, formData }: { chapterId: number; formData: FormData }) =>
coursewareService.uploadMaterial(chapterId, formData),
onSuccess: (_, { chapterId }) => {
qc.invalidateQueries({ queryKey: queryKeys.chapterMaterials(chapterId) });
qc.invalidateQueries({ queryKey: queryKeys.chapterProgress(chapterId) });
qc.invalidateQueries({ queryKey: queryKeys.chapter(chapterId) });
},
});
}
export function useDeleteMaterial() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, chapterId }: { id: number; chapterId: number }) =>
coursewareService.deleteMaterial(id),
onSuccess: (_, { chapterId }) => {
qc.invalidateQueries({ queryKey: queryKeys.chapterMaterials(chapterId) });
qc.invalidateQueries({ queryKey: queryKeys.chapterProgress(chapterId) });
qc.invalidateQueries({ queryKey: queryKeys.chapter(chapterId) });
},
});
}
export function useMarkMaterialViewed() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, chapterId }: { id: number; chapterId: number }) =>
coursewareService.markMaterialViewed(id),
onSuccess: (_, { chapterId }) => {
qc.invalidateQueries({ queryKey: queryKeys.chapterMaterials(chapterId) });
qc.invalidateQueries({ queryKey: queryKeys.chapterProgress(chapterId) });
},
});
}
export function useCompleteChapter() {
const qc = useQueryClient();
return useMutation({
mutationFn: (chapterId: number) => coursewareService.markChapterComplete(chapterId),
onSuccess: (_, chapterId) => {
qc.invalidateQueries({ queryKey: queryKeys.chapterProgress(chapterId) });
qc.invalidateQueries({ queryKey: queryKeys.chapter(chapterId) });
},
});
}
export function useGenerateOutline() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: WorkbenchGenerateRequest) => coursewareService.generateOutline(data),
onSuccess: (_, data) => {
qc.invalidateQueries({ queryKey: queryKeys.chapters(data.course_id) });
qc.invalidateQueries({ queryKey: queryKeys.lms.course(data.course_id) });
},
});
}
export function useGenerateChapterContent() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: WorkbenchGenerateRequest) => coursewareService.generateChapterContent(data),
onSuccess: (_, data) => {
qc.invalidateQueries({ queryKey: queryKeys.chapters(data.course_id) });
qc.invalidateQueries({ queryKey: queryKeys.lms.course(data.course_id) });
},
});
}
export function useGenerateRubric() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: WorkbenchGenerateRequest) => coursewareService.generateRubric(data),
onSuccess: (_, data) => {
qc.invalidateQueries({ queryKey: queryKeys.chapters(data.course_id) });
qc.invalidateQueries({ queryKey: queryKeys.lms.course(data.course_id) });
},
});
}
export function usePublishWorkbench() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: Record<string, unknown>) => coursewareService.publishWorkbench(data),
onSuccess: (_, data) => {
const courseId = data.course_id;
if (typeof courseId === "number") {
qc.invalidateQueries({ queryKey: queryKeys.chapters(courseId) });
qc.invalidateQueries({ queryKey: queryKeys.lms.course(courseId) });
}
},
});
}

View File

@@ -0,0 +1,27 @@
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { entitiesService } from "@/services";
import type { PaginationParams } from "@/types";
export function useEntities(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.entities.list(params),
queryFn: () => entitiesService.list(params),
});
}
export function useEntity(id: number) {
return useQuery({
queryKey: queryKeys.entities.detail(id),
queryFn: () => entitiesService.getById(id),
enabled: !!id,
});
}
export function useEntityRoles(entityId: number) {
return useQuery({
queryKey: queryKeys.entities.roles(entityId),
queryFn: () => entitiesService.getRoles(entityId),
enabled: !!entityId,
});
}

View File

@@ -0,0 +1,57 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { entityOnboardingService } from "@/services/entity-onboarding.service";
import type { CredentialFilters } from "@/types";
export function useValidateCSV() {
return useMutation({
mutationFn: (file: File) => entityOnboardingService.validateCsv(file),
});
}
export function useBulkCreate() {
const qc = useQueryClient();
return useMutation({
mutationFn: (payload: { validate_session_id?: string }) => entityOnboardingService.bulkCreate(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["entity-onboarding"] });
},
});
}
export function useSendCredentials() {
const qc = useQueryClient();
return useMutation({
mutationFn: (studentIds: number[]) => entityOnboardingService.sendCredentials(studentIds),
onSuccess: () => {
qc.invalidateQueries({ queryKey: queryKeys.entityOnboarding.credentials() });
},
});
}
export function useCredentialStatuses(filters?: CredentialFilters & { page?: number; limit?: number }) {
return useQuery({
queryKey: queryKeys.entityOnboarding.credentials(filters as Record<string, unknown> | undefined),
queryFn: () => entityOnboardingService.getCredentialStatuses(filters),
});
}
export function useResendCredential() {
const qc = useQueryClient();
return useMutation({
mutationFn: (studentId: number) => entityOnboardingService.resendCredential(studentId),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["entity-onboarding", "credentials"] });
},
});
}
export function useResendAllPendingCredentials() {
const qc = useQueryClient();
return useMutation({
mutationFn: () => entityOnboardingService.resendAllPending(),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["entity-onboarding", "credentials"] });
},
});
}

View File

@@ -0,0 +1,34 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { examSessionService } from "@/services/exam-session.service";
import type { ExamAutoSave } from "@/types";
export function useExamSession(examId: number) {
return useQuery({
queryKey: queryKeys.examSession.session(examId),
queryFn: () => examSessionService.getSession(examId),
enabled: !!examId,
});
}
export function useExamStatus(examId: number, enabled: boolean) {
return useQuery({
queryKey: queryKeys.examSession.status(examId),
queryFn: () => examSessionService.getStatus(examId),
enabled: !!examId && enabled,
refetchInterval: 10000,
});
}
export function useExamAutoSave() {
return useMutation({
mutationFn: (data: { examId: number; payload: ExamAutoSave }) =>
examSessionService.autoSave(data.examId, data.payload),
});
}
export function useExamSubmit() {
return useMutation({
mutationFn: (examId: number) => examSessionService.submit(examId),
});
}

View File

@@ -0,0 +1,85 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { examTemplateService } from "@/services/exam-template.service";
import { ieltsExamService } from "@/services/ielts-exam.service";
import { customExamService } from "@/services/custom-exam.service";
import type { IELTSExamConfig, ContentPoolFilters, ExamAssignRequest, CustomExamCreateRequest, SaveAsTemplateRequest } from "@/types";
export function useInternationalTemplates() {
return useQuery({
queryKey: queryKeys.examTemplates.international,
queryFn: examTemplateService.listInternational,
});
}
export function useCustomTemplates() {
return useQuery({
queryKey: queryKeys.examTemplates.custom,
queryFn: examTemplateService.listCustom,
});
}
export function useCreateIeltsExam() {
return useMutation({
mutationFn: (data: IELTSExamConfig) => ieltsExamService.create(data),
});
}
export function useIeltsSkills(examId: number) {
return useQuery({
queryKey: queryKeys.ieltsExam.skills(examId),
queryFn: () => ieltsExamService.getSkills(examId),
enabled: !!examId,
});
}
export function useIeltsContentPool(examId: number, filters: ContentPoolFilters) {
return useQuery({
queryKey: queryKeys.ieltsExam.contentPool(examId, filters),
queryFn: () => ieltsExamService.getContentPool(examId, filters),
enabled: !!examId,
});
}
export function useIeltsAutoAssemble() {
return useMutation({
mutationFn: (examId: number) => ieltsExamService.autoAssemble(examId),
});
}
export function useIeltsValidation(examId: number) {
return useQuery({
queryKey: queryKeys.ieltsExam.validation(examId),
queryFn: () => ieltsExamService.validate(examId),
enabled: !!examId,
});
}
export function usePublishIeltsExam() {
const qc = useQueryClient();
return useMutation({
mutationFn: (examId: number) => ieltsExamService.update(examId, { status: "published" }),
onSuccess: () => qc.invalidateQueries({ queryKey: ["ielts-exam"] }),
});
}
export function useAssignIeltsExam() {
return useMutation({
mutationFn: (data: { examId: number; assignment: ExamAssignRequest }) =>
ieltsExamService.assign(data.examId, data.assignment),
});
}
export function useCreateCustomExam() {
return useMutation({
mutationFn: (data: CustomExamCreateRequest) => customExamService.create(data),
});
}
export function useSaveAsTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: SaveAsTemplateRequest) => examTemplateService.saveCustomTemplate(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.examTemplates.custom }),
});
}

View File

@@ -0,0 +1,48 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { examsService } from "@/services";
import type { ExamModule, PaginationParams } from "@/types";
export function useExams(module: ExamModule, params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.exams.list(module, params),
queryFn: () => examsService.list(module, params),
});
}
export function useExam(module: ExamModule, id: number) {
return useQuery({
queryKey: queryKeys.exams.detail(module, id),
queryFn: () => examsService.getById(module, id),
enabled: !!id,
});
}
export function useRubrics(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.exams.rubrics(params),
queryFn: () => examsService.listRubrics(params),
});
}
export function useRubricGroups(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.exams.rubricGroups(params),
queryFn: () => examsService.listRubricGroups(params),
});
}
export function useExamStructures(params?: PaginationParams & { entity_id?: number }) {
return useQuery({
queryKey: queryKeys.exams.structures(params),
queryFn: () => examsService.listStructures(params),
});
}
export function useDeleteExam() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => examsService.delete(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ["exams"] }),
});
}

View File

@@ -0,0 +1,60 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { facilityService } from "@/services/facility.service";
import type { PaginationParams } from "@/types";
import type { FacilityCreateRequest, AssetCreateRequest } from "@/types/facility";
export function useFacilities(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.facilities.list(params),
queryFn: () => facilityService.listFacilities(params),
});
}
export function useCreateFacility() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: FacilityCreateRequest) => facilityService.createFacility(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.facilities.all }),
});
}
export function useUpdateFacility() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<FacilityCreateRequest> }) =>
facilityService.updateFacility(id, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.facilities.all }),
});
}
export function useDeleteFacility() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => facilityService.deleteFacility(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.facilities.all }),
});
}
export function useAssets(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.assets.list(params),
queryFn: () => facilityService.listAssets(params),
});
}
export function useCreateAsset() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: AssetCreateRequest) => facilityService.createAsset(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.assets.all }),
});
}
export function useDeleteAsset() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => facilityService.deleteAsset(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.assets.all }),
});
}

View File

@@ -0,0 +1,85 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { faqService } from "@/services/faq.service";
import type { FaqCategoryCreateRequest, FaqItemCreateRequest } from "@/types/faq";
export function useFaqCategories(audience?: string) {
return useQuery({
queryKey: queryKeys.faqCategories(audience),
queryFn: () => faqService.listCategories(audience ? { audience } : undefined),
});
}
export function useFaqItems(params?: { category_id?: number; audience?: string; search?: string }) {
return useQuery({
queryKey: queryKeys.faqItems(params),
queryFn: () => faqService.listItems(params),
});
}
export function useCreateFaqCategory() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: FaqCategoryCreateRequest) => faqService.createCategory(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["faq", "categories"] });
},
});
}
export function useUpdateFaqCategory() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<FaqCategoryCreateRequest> }) =>
faqService.updateCategory(id, data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["faq", "categories"] });
qc.invalidateQueries({ queryKey: ["faq", "items"] });
},
});
}
export function useDeleteFaqCategory() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => faqService.deleteCategory(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["faq", "categories"] });
qc.invalidateQueries({ queryKey: ["faq", "items"] });
},
});
}
export function useCreateFaqItem() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: FaqItemCreateRequest) => faqService.createItem(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["faq", "items"] });
qc.invalidateQueries({ queryKey: ["faq", "categories"] });
},
});
}
export function useUpdateFaqItem() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<FaqItemCreateRequest> }) =>
faqService.updateItem(id, data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["faq", "items"] });
qc.invalidateQueries({ queryKey: ["faq", "categories"] });
},
});
}
export function useDeleteFaqItem() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => faqService.deleteItem(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["faq", "items"] });
qc.invalidateQueries({ queryKey: ["faq", "categories"] });
},
});
}

View File

@@ -0,0 +1,33 @@
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { feesService } from "@/services/fees.service";
import type { PaginationParams } from "@/types";
export function useFeesPlans(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.feesPlans.list(params),
queryFn: () => feesService.listFeesPlans(params),
});
}
export function useFeesPlan(id: number) {
return useQuery({
queryKey: queryKeys.feesPlans.detail(id),
queryFn: () => feesService.getFeesPlan(id),
enabled: !!id,
});
}
export function useStudentFees(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.studentFees.list(params),
queryFn: () => feesService.listStudentFees(params),
});
}
export function useFeesTerms(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.feesTerms.list(params),
queryFn: () => feesService.listFeesTerms(params),
});
}

View File

@@ -0,0 +1,52 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { gradebookService } from "@/services/gradebook.service";
import type { PaginationParams } from "@/types";
import type { GradingAssignmentCreateRequest } from "@/types/gradebook";
export function useGradebooks(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.gradebooks.list(params),
queryFn: () => gradebookService.listGradebooks(params),
});
}
export function useGradebookLines(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.gradebookLines.list(params),
queryFn: () => gradebookService.listGradebookLines(params),
});
}
export function useGradingAssignments(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.gradingAssignments.list(params),
queryFn: () => gradebookService.listGradingAssignments(params),
});
}
export function useCreateGradingAssignment() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: GradingAssignmentCreateRequest) =>
gradebookService.createGradingAssignment(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.gradingAssignments.all }),
});
}
export function useUpdateGradingAssignment() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<GradingAssignmentCreateRequest> }) =>
gradebookService.updateGradingAssignment(id, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.gradingAssignments.all }),
});
}
export function useDeleteGradingAssignment() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => gradebookService.deleteGradingAssignment(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.gradingAssignments.all }),
});
}

View File

@@ -0,0 +1,42 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { gradingService } from "@/services/grading.service";
import type { GradeSubmission } from "@/types";
export function useGradingQueue(params?: { skill?: string; status?: string; exam_id?: number }) {
return useQuery({
queryKey: queryKeys.grading.queue(params),
queryFn: () => gradingService.getQueue(params),
});
}
export function useStudentResponse(attemptId: number, skill: string) {
return useQuery({
queryKey: queryKeys.grading.response(attemptId, skill),
queryFn: () => gradingService.getStudentResponse(attemptId, skill),
enabled: !!attemptId,
});
}
export function useGradingRubric(attemptId: number, skill: string) {
return useQuery({
queryKey: queryKeys.grading.rubric(attemptId, skill),
queryFn: () => gradingService.getRubric(attemptId, skill),
enabled: !!attemptId,
});
}
export function useAIGradeSuggestion() {
return useMutation({
mutationFn: (data: { attemptId: number; skill: string }) =>
gradingService.getAISuggestion(data.attemptId, data.skill),
});
}
export function useSubmitGrade() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: GradeSubmission) => gradingService.submitGrade(data),
onSuccess: () => qc.invalidateQueries({ queryKey: ["grading"] }),
});
}

View File

@@ -0,0 +1,199 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { institutionalExamService } from "@/services/institutional-exam.service";
import type { PaginationParams } from "@/types";
import type {
InstExamSessionCreateRequest,
InstExamCreateRequest,
ResultTemplateCreateRequest,
CourseAssignmentCreateRequest,
SubjectRegistrationCreateRequest,
AssignmentSubmission,
} from "@/types/institutional-exam";
export function useInstitutionalExamSessions(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.instExamSessions.list(params),
queryFn: () => institutionalExamService.listSessions(params),
});
}
export function useInstitutionalExamSession(id: number) {
return useQuery({
queryKey: queryKeys.instExamSessions.detail(id),
queryFn: () => institutionalExamService.getSession(id),
enabled: !!id,
});
}
export function useExamTypes(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.examTypes.list(params),
queryFn: () => institutionalExamService.listExamTypes(params),
});
}
export function useGradeConfigs(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.gradeConfigs.list(params),
queryFn: () => institutionalExamService.listGradeConfigs(params),
});
}
export function useResultTemplates(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.resultTemplates.list(params),
queryFn: () => institutionalExamService.listResultTemplates(params),
});
}
export function useMarksheets(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.marksheets.list(params),
queryFn: () => institutionalExamService.listMarksheets(params),
});
}
export function useMarksheet(id: number) {
return useQuery({
queryKey: queryKeys.marksheets.detail(id),
queryFn: () => institutionalExamService.getMarksheet(id),
enabled: !!id,
});
}
export function useCourseAssignments(params?: PaginationParams & { course_id?: number; batch_id?: number; state?: string }) {
return useQuery({
queryKey: queryKeys.courseAssignments.list(params),
queryFn: () => institutionalExamService.listCourseAssignments(params),
});
}
export function useCourseAssignment(id: number) {
return useQuery({
queryKey: queryKeys.courseAssignments.detail(id),
queryFn: () => institutionalExamService.getCourseAssignment(id),
enabled: !!id,
});
}
export function useSubjectRegistrations(params?: PaginationParams & { student_id?: number; course_id?: number; state?: string }) {
return useQuery({
queryKey: queryKeys.subjectRegistrations.list(params),
queryFn: () => institutionalExamService.listSubjectRegistrations(params),
});
}
export function useAvailableSubjects(params?: { student_id?: number; course_id?: number }) {
return useQuery({
queryKey: queryKeys.subjectRegistrations.available(params),
queryFn: () => institutionalExamService.getAvailableSubjects(params),
});
}
export function useCreateExamSession() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: InstExamSessionCreateRequest) => institutionalExamService.createSession(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.instExamSessions.all }),
});
}
export function useScheduleExamSession() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => institutionalExamService.scheduleSession(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.instExamSessions.all }),
});
}
export function useCreateInstitutionalExam() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: InstExamCreateRequest) => institutionalExamService.createExam(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.instExamSessions.all }),
});
}
export function useEnterMarks() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ examId, data }: { examId: number; data: { marks: { student_id: number; score: number }[] } }) =>
institutionalExamService.enterMarks(examId, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.instExamSessions.all }),
});
}
export function useCreateResultTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: ResultTemplateCreateRequest) => institutionalExamService.createResultTemplate(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.resultTemplates.all }),
});
}
export function useGenerateMarksheets() {
const qc = useQueryClient();
return useMutation({
mutationFn: (templateId: number) => institutionalExamService.generateMarksheets(templateId),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.marksheets.all }),
});
}
export function useValidateMarksheet() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => institutionalExamService.validateMarksheet(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.marksheets.all }),
});
}
export function useCreateCourseAssignment() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: CourseAssignmentCreateRequest) => institutionalExamService.createCourseAssignment(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.courseAssignments.all }),
});
}
export function usePublishCourseAssignment() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => institutionalExamService.publishCourseAssignment(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.courseAssignments.all }),
});
}
export function useSubmitAssignment() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ assignmentId, file }: { assignmentId: number; file: File }) =>
institutionalExamService.submitAssignment(assignmentId, file),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.courseAssignments.all }),
});
}
export function useGradeSubmission() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ submissionId, data }: { submissionId: number; data: Partial<AssignmentSubmission> }) =>
institutionalExamService.gradeSubmission(submissionId, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.courseAssignments.all }),
});
}
export function useCreateSubjectRegistration() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: SubjectRegistrationCreateRequest) => institutionalExamService.createSubjectRegistration(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.subjectRegistrations.all }),
});
}
export function useConfirmSubjectRegistration() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => institutionalExamService.confirmSubjectRegistration(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.subjectRegistrations.all }),
});
}

View File

@@ -0,0 +1,37 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { lessonService } from "@/services/lesson.service";
import type { PaginationParams } from "@/types";
import type { LessonCreateRequest } from "@/types/lesson";
export function useLessons(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.lessons.list(params),
queryFn: () => lessonService.listLessons(params),
});
}
export function useCreateLesson() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: LessonCreateRequest) => lessonService.createLesson(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.lessons.all }),
});
}
export function useUpdateLesson() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<LessonCreateRequest> }) =>
lessonService.updateLesson(id, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.lessons.all }),
});
}
export function useDeleteLesson() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => lessonService.deleteLesson(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.lessons.all }),
});
}

View File

@@ -0,0 +1,79 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { libraryService } from "@/services/library.service";
import type { PaginationParams } from "@/types";
import type { MediaCreateRequest, MediaMovement } from "@/types/library";
type MovementWrite = Partial<
Omit<MediaMovement, "id" | "media_name" | "student_name" | "faculty_name">
>;
export function useLibraryMedia(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.libraryMedia.list(params),
queryFn: () => libraryService.listMedia(params),
});
}
export function useCreateMedia() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: MediaCreateRequest) => libraryService.createMedia(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.libraryMedia.all }),
});
}
export function useLibraryMovements(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.libraryMovements.list(params),
queryFn: () => libraryService.listMovements(params),
});
}
export function useCreateMovement() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: MovementWrite) => libraryService.createMovement(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.libraryMovements.all }),
});
}
export function useUpdateMovement() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: MovementWrite }) =>
libraryService.updateMovement(id, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.libraryMovements.all }),
});
}
export function useDeleteMedia() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => libraryService.deleteMedia(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.libraryMedia.all }),
});
}
export function useReturnMovement() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => libraryService.returnMovement(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.libraryMovements.all }),
});
}
export function useLibraryCards(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.libraryCards.list(params),
queryFn: () => libraryService.listCards(params),
});
}
export function useCreateCard() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: { student_id: number }) => libraryService.createCard(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.libraryCards.all }),
});
}

View File

@@ -0,0 +1,97 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { lmsService } from "@/services";
import type { PaginationParams, CourseCreateRequest, LmsStudentCreateRequest, LmsTeacherCreateRequest } from "@/types";
export function useCourses(params?: PaginationParams & { status?: string }) {
return useQuery({
queryKey: queryKeys.lms.courses(params),
queryFn: () => lmsService.listCourses(params),
});
}
export function useCourse(id: number) {
return useQuery({
queryKey: queryKeys.lms.course(id),
queryFn: () => lmsService.getCourse(id),
enabled: !!id,
});
}
export function useCreateCourse() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: CourseCreateRequest) => lmsService.createCourse(data),
onSuccess: () => qc.invalidateQueries({ queryKey: ["lms", "courses"] }),
});
}
export function useStudents(params?: PaginationParams & { search?: string; batch_id?: number }) {
return useQuery({
queryKey: queryKeys.lms.students(params),
queryFn: () => lmsService.listStudents(params),
});
}
export function useCreateStudent() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: LmsStudentCreateRequest) => lmsService.createStudent(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["lms", "students"] });
},
});
}
export function useTeachers(params?: PaginationParams & { search?: string }) {
return useQuery({
queryKey: queryKeys.lms.teachers(params),
queryFn: () => lmsService.listTeachers(params),
});
}
export function useCreateTeacher() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: LmsTeacherCreateRequest) => lmsService.createTeacher(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["lms", "teachers"] });
},
});
}
export function useBatches(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.lms.batches(params),
queryFn: () => lmsService.listBatches(params),
});
}
export function useBatch(id: number) {
return useQuery({
queryKey: queryKeys.lms.batch(id),
queryFn: () => lmsService.getBatch(id),
enabled: !!id,
});
}
export function useTimetable(params?: Record<string, unknown>) {
return useQuery({
queryKey: queryKeys.lms.timetable(params),
queryFn: () => lmsService.getTimetable(params as Record<string, string | number | boolean | undefined>),
});
}
export function useAttendance(params?: Record<string, unknown>) {
return useQuery({
queryKey: queryKeys.lms.attendance(params),
queryFn: () => lmsService.getAttendance(params as Record<string, string | number | boolean | undefined>),
});
}
export function useGrades(params?: Record<string, unknown>) {
return useQuery({
queryKey: queryKeys.lms.grades(params),
queryFn: () => lmsService.getGrades(params as Record<string, string | number | boolean | undefined>),
});
}

View File

@@ -0,0 +1,103 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { notificationService } from "@/services/notification.service";
import type { NotificationPreferences, NotificationRuleCreateRequest } from "@/types/notification";
import type { PaginationParams } from "@/types";
export function useNotifications(params?: PaginationParams & { type?: string; is_read?: boolean }) {
return useQuery({
queryKey: queryKeys.notifications(params),
queryFn: () => notificationService.listNotifications(params),
});
}
export function useUnreadNotificationCount() {
return useQuery({
queryKey: queryKeys.unreadNotifications,
queryFn: () => notificationService.getUnreadNotificationCount(),
});
}
export function useNotificationRules() {
return useQuery({
queryKey: queryKeys.notificationRules,
queryFn: () => notificationService.listNotificationRules(),
});
}
export function useNotificationPreferences() {
return useQuery({
queryKey: queryKeys.notificationPreferences,
queryFn: () => notificationService.getNotificationPreferences(),
});
}
export function useMarkNotificationRead() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => notificationService.markNotificationRead(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["notifications"] });
qc.invalidateQueries({ queryKey: queryKeys.unreadNotifications });
},
});
}
export function useMarkAllNotificationsRead() {
const qc = useQueryClient();
return useMutation({
mutationFn: () => notificationService.markAllNotificationsRead(),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["notifications"] });
qc.invalidateQueries({ queryKey: queryKeys.unreadNotifications });
},
});
}
export function useCreateNotificationRule() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: NotificationRuleCreateRequest) =>
notificationService.createNotificationRule(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: queryKeys.notificationRules });
},
});
}
export function useUpdateNotificationRule() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({
id,
data,
}: {
id: number;
data: Partial<NotificationRuleCreateRequest>;
}) => notificationService.updateNotificationRule(id, data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: queryKeys.notificationRules });
},
});
}
export function useDeleteNotificationRule() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => notificationService.deleteNotificationRule(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: queryKeys.notificationRules });
},
});
}
export function useUpdateNotificationPreferences() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: Partial<NotificationPreferences>) =>
notificationService.updateNotificationPreferences(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: queryKeys.notificationPreferences });
},
});
}

View File

@@ -0,0 +1,54 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { placementService } from "@/services/placement.service";
import type { PlacementSubject, CATAnswer, CATAutoSave } from "@/types";
export function usePlacementStart() {
return useMutation({
mutationFn: (subject: PlacementSubject) => placementService.start(subject),
});
}
export function usePlacementAnswer() {
return useMutation({
mutationFn: (data: CATAnswer) => placementService.submitAnswer(data),
});
}
export function usePlacementAutoSave() {
return useMutation({
mutationFn: (data: CATAutoSave) => placementService.autoSave(data),
});
}
export function usePlacementResults(sessionId: string) {
return useQuery({
queryKey: queryKeys.placement.results(sessionId),
queryFn: () => placementService.getResults(sessionId),
enabled: !!sessionId,
});
}
export function useSpeakingStatus(sessionId: string, enabled: boolean) {
return useQuery({
queryKey: queryKeys.placement.speakingStatus(sessionId),
queryFn: () => placementService.getSpeakingStatus(sessionId),
enabled: !!sessionId && enabled,
refetchInterval: 30000,
});
}
export function useLearningPath(sessionId: string) {
return useQuery({
queryKey: queryKeys.placement.learningPath(sessionId),
queryFn: () => placementService.getLearningPath(sessionId),
enabled: !!sessionId,
});
}
export function useSpeakingUpload() {
return useMutation({
mutationFn: (data: { sessionId: string; promptId: number; audioFile: File }) =>
placementService.uploadSpeaking(data.sessionId, data.promptId, data.audioFile),
});
}

View File

@@ -0,0 +1,41 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { scoreReleaseService } from "@/services/score-release.service";
export function usePendingScores(params?: { page?: number; limit?: number }) {
return useQuery({
queryKey: queryKeys.scoreRelease.pending(params as Record<string, unknown> | undefined),
queryFn: () => scoreReleaseService.getPending(params),
});
}
export function useReleaseScore() {
const qc = useQueryClient();
return useMutation({
mutationFn: (attemptId: number) => scoreReleaseService.release(attemptId),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["score-release", "pending"] });
},
});
}
export function useRejectScore() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ attemptId, reason }: { attemptId: number; reason: string }) =>
scoreReleaseService.reject(attemptId, reason),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["score-release", "pending"] });
},
});
}
export function useBulkRelease() {
const qc = useQueryClient();
return useMutation({
mutationFn: (attemptIds: number[]) => scoreReleaseService.bulkRelease(attemptIds),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["score-release", "pending"] });
},
});
}

View File

@@ -0,0 +1,41 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { signupService } from "@/services/signup.service";
import type { RegisterRequest, VerifyEmailRequest, OnboardingData } from "@/types";
export function useGoals() {
return useQuery({
queryKey: queryKeys.signup.goals,
queryFn: signupService.getGoals,
});
}
export function useRegister() {
return useMutation({
mutationFn: (data: RegisterRequest) => signupService.register(data),
});
}
export function useVerifyEmail() {
return useMutation({
mutationFn: (data: VerifyEmailRequest) => signupService.verifyEmail(data),
});
}
export function useResendOtp() {
return useMutation({
mutationFn: (email: string) => signupService.resendOtp({ email }),
});
}
export function useCheckEmail() {
return useMutation({
mutationFn: (email: string) => signupService.checkEmail(email),
});
}
export function useCompleteOnboarding() {
return useMutation({
mutationFn: (data: OnboardingData) => signupService.completeOnboarding(data),
});
}

View File

@@ -0,0 +1,60 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { studentLeaveService } from "@/services/student-leave.service";
import type { PaginationParams } from "@/types";
import type { StudentLeaveCreateRequest, StudentLeaveType } from "@/types/student-leave";
export function useStudentLeaves(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.studentLeaves.list(params),
queryFn: () => studentLeaveService.listLeaves(params),
});
}
export function useStudentLeaveTypes(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.studentLeaveTypes.list(params),
queryFn: () => studentLeaveService.listLeaveTypes(params),
});
}
export function useCreateStudentLeave() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: StudentLeaveCreateRequest) => studentLeaveService.createLeave(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.studentLeaves.all }),
});
}
export function useUpdateStudentLeave() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<StudentLeaveCreateRequest> }) =>
studentLeaveService.updateLeave(id, data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.studentLeaves.all }),
});
}
export function useApproveStudentLeave() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => studentLeaveService.approveLeave(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.studentLeaves.all }),
});
}
export function useRejectStudentLeave() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => studentLeaveService.rejectLeave(id),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.studentLeaves.all }),
});
}
export function useCreateStudentLeaveType() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: Pick<StudentLeaveType, "name">) => studentLeaveService.createLeaveType(data),
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.studentLeaveTypes.all }),
});
}

View File

@@ -0,0 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { studentProgressService } from "@/services/student-progress.service";
import type { PaginationParams } from "@/types";
export function useStudentProgressList(params?: PaginationParams) {
return useQuery({
queryKey: queryKeys.studentProgress.list(params),
queryFn: () => studentProgressService.listProgress(params),
});
}

View File

@@ -0,0 +1,18 @@
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { usersService, type UserListParams } from "@/services/users.service";
export function useUsers(params: UserListParams) {
return useQuery({
queryKey: queryKeys.users.list(params),
queryFn: () => usersService.list(params),
});
}
export function useUser(id: number) {
return useQuery({
queryKey: queryKeys.users.detail(id),
queryFn: () => usersService.getById(id),
enabled: !!id,
});
}

19
src/hooks/use-mobile.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

186
src/hooks/use-toast.ts Normal file
View File

@@ -0,0 +1,186 @@
import * as React from "react";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };

View File

@@ -0,0 +1,54 @@
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/contexts/AuthContext";
import { entitiesService } from "@/services/entities.service";
interface UsePermissionsReturn {
permissions: string[];
isLoading: boolean;
hasPermission: (permission: string) => boolean;
hasAnyPermission: (permissions: string[]) => boolean;
hasAllPermissions: (permissions: string[]) => boolean;
isAdmin: boolean;
isDeveloper: boolean;
}
export function usePermissions(entityId?: number): UsePermissionsReturn {
const { user } = useAuth();
const activeEntityId = entityId ?? user?.entities?.[0]?.id;
const { data: permissions = [], isLoading } = useQuery({
queryKey: ["permissions", activeEntityId],
queryFn: () => entitiesService.getPermissions(activeEntityId!),
enabled: !!activeEntityId && !!user,
staleTime: 10 * 60 * 1000,
});
const isAdmin = user?.user_type === "admin" || user?.user_type === "developer";
const isDeveloper = user?.user_type === "developer";
const hasPermission = (permission: string): boolean => {
if (isAdmin) return true;
return permissions.includes(permission);
};
const hasAnyPermission = (perms: string[]): boolean => {
if (isAdmin) return true;
return perms.some((p) => permissions.includes(p));
};
const hasAllPermissions = (perms: string[]): boolean => {
if (isAdmin) return true;
return perms.every((p) => permissions.includes(p));
};
return {
permissions,
isLoading,
hasPermission,
hasAnyPermission,
hasAllPermissions,
isAdmin,
isDeveloper,
};
}