import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import { Class, ClassWithRelations, Timetable, TimetableWithRelations, TimetableLesson, Lesson, LessonWithRelations, EnrollmentRequest, EnrollmentRequestWithProfile, } from '../types/timetable.types'; import { timetableService } from '../services/timetableService'; // ============================================================================ // State Types // ============================================================================ interface TimetableState { // Classes classes: Class[]; currentClass: ClassWithRelations | null; myClasses: Class[]; myTeachingClasses: Class[]; classesLoading: boolean; classesError: string | null; // Timetables timetables: Timetable[]; currentTimetable: TimetableWithRelations | null; timetablesLoading: boolean; timetablesError: string | null; // Lessons lessons: Lesson[]; currentLesson: LessonWithRelations | null; lessonsLoading: boolean; lessonsError: string | null; // Enrollment Requests enrollmentRequests: EnrollmentRequestWithProfile[]; enrollmentRequestsLoading: boolean; enrollmentRequestsError: string | null; // Pagination totalCount: number; currentPage: number; pageSize: number; // Filters filterSubject: string | null; filterSchoolYear: string | null; filterAcademicTerm: string | null; searchQuery: string; } interface TimetableActions { // Class Actions fetchClasses: (params?: { subject?: string; schoolYear?: string; academicTerm?: string; search?: string; skip?: number; limit?: number; }) => Promise; fetchClass: (classId: string) => Promise; createClass: (data: { name: string; subject: string; schoolYear: string; academicTerm: string; description?: string; }) => Promise; updateClass: (classId: string, data: Partial) => Promise; deleteClass: (classId: string) => Promise; fetchMyClasses: () => Promise; fetchMyTeachingClasses: () => Promise; clearCurrentClass: () => void; // Class Teacher Actions addTeacherToClass: (classId: string, teacherId: string, isPrimary?: boolean) => Promise; removeTeacherFromClass: (classId: string, teacherId: string) => Promise; // Class Student Actions addStudentToClass: (classId: string, studentId: string) => Promise; removeStudentFromClass: (classId: string, studentId: string) => Promise; // Enrollment Request Actions fetchEnrollmentRequests: (classId: string) => Promise; requestEnrollment: (classId: string, message?: string) => Promise; respondToEnrollmentRequest: (requestId: string, status: 'approved' | 'rejected', responseMessage?: string) => Promise; // Timetable Actions fetchTimetables: (params?: { classId?: string; type?: 'adhoc' | 'recurring'; isActive?: boolean; skip?: number; limit?: number; }) => Promise; fetchTimetable: (timetableId: string) => Promise; createTimetable: (data: { classId: string; name: string; type: 'adhoc' | 'recurring'; startDate: string; endDate: string; recurrenceRule?: string; }) => Promise; updateTimetable: (timetableId: string, data: Partial) => Promise; deleteTimetable: (timetableId: string) => Promise; fetchMyTimetables: () => Promise; fetchMyTeachingTimetables: () => Promise; clearCurrentTimetable: () => void; // Timetable Teacher Actions addTeacherToTimetable: (timetableId: string, teacherId: string, isPrimary?: boolean) => Promise; removeTeacherFromTimetable: (timetableId: string, teacherId: string) => Promise; // Timetable Lesson Actions addTimetableLesson: (timetableId: string, data: { dayOfWeek: number; startTime: string; endTime: string; room?: string; maxStudents?: number; }) => Promise; updateTimetableLesson: (timetableId: string, lessonId: string, data: Partial) => Promise; removeTimetableLesson: (timetableId: string, lessonId: string) => Promise; // Lesson Actions fetchLessons: (params?: { timetableId?: string; startDate?: string; endDate?: string; status?: 'scheduled' | 'completed' | 'cancelled'; skip?: number; limit?: number; }) => Promise; fetchLesson: (lessonId: string) => Promise; generateLessons: (timetableId: string, startDate: string, endDate: string) => Promise; cancelLesson: (lessonId: string, reason?: string) => Promise; clearCurrentLesson: () => void; // Filter Actions setFilterSubject: (subject: string | null) => void; setFilterSchoolYear: (schoolYear: string | null) => void; setFilterAcademicTerm: (academicTerm: string | null) => void; setSearchQuery: (query: string) => void; setPage: (page: number) => void; setPageSize: (size: number) => void; clearFilters: () => void; } // ============================================================================ // Initial State // ============================================================================ const initialState: TimetableState = { classes: [], currentClass: null, myClasses: [], myTeachingClasses: [], classesLoading: false, classesError: null, timetables: [], currentTimetable: null, timetablesLoading: false, timetablesError: null, lessons: [], currentLesson: null, lessonsLoading: false, lessonsError: null, enrollmentRequests: [], enrollmentRequestsLoading: false, enrollmentRequestsError: null, totalCount: 0, currentPage: 0, pageSize: 20, filterSubject: null, filterSchoolYear: null, filterAcademicTerm: null, searchQuery: '', }; // ============================================================================ // Store // ============================================================================ const useTimetableStore = create()( devtools( persist( (set, get) => ({ ...initialState, // ============================================================================ // Class Actions // ============================================================================ fetchClasses: async (params = {}) => { set({ classesLoading: true, classesError: null }); try { const response = await timetableService.getClasses(params); set({ classes: response.items, totalCount: response.total, classesLoading: false, }); } catch (error) { console.error('Failed to fetch classes:', error); set({ classesError: error instanceof Error ? error.message : 'Failed to fetch classes', classesLoading: false, }); } }, fetchClass: async (classId: string) => { set({ classesLoading: true, classesError: null }); try { const classData = await timetableService.getClass(classId); set({ currentClass: classData, classesLoading: false }); } catch (error) { console.error('Failed to fetch class:', error); set({ classesError: error instanceof Error ? error.message : 'Failed to fetch class', classesLoading: false, }); } }, createClass: async (data) => { const newClass = await timetableService.createClass(data); set((state) => ({ classes: [newClass, ...state.classes], })); return newClass; }, updateClass: async (classId, data) => { const updatedClass = await timetableService.updateClass(classId, data); set((state) => ({ classes: state.classes.map((c) => (c.id === classId ? updatedClass : c)), currentClass: state.currentClass?.id === classId ? { ...state.currentClass, ...updatedClass } : state.currentClass, })); }, deleteClass: async (classId) => { await timetableService.deleteClass(classId); set((state) => ({ classes: state.classes.filter((c) => c.id !== classId), currentClass: state.currentClass?.id === classId ? null : state.currentClass, })); }, fetchMyClasses: async () => { set({ classesLoading: true, classesError: null }); try { const classes = await timetableService.getMyClasses(); set({ myClasses: classes, classesLoading: false }); } catch (error) { console.error('Failed to fetch my classes:', error); set({ classesError: error instanceof Error ? error.message : 'Failed to fetch my classes', classesLoading: false, }); } }, fetchMyTeachingClasses: async () => { set({ classesLoading: true, classesError: null }); try { const classes = await timetableService.getMyTeachingClasses(); set({ myTeachingClasses: classes, classesLoading: false }); } catch (error) { console.error('Failed to fetch my teaching classes:', error); set({ classesError: error instanceof Error ? error.message : 'Failed to fetch my teaching classes', classesLoading: false, }); } }, clearCurrentClass: () => set({ currentClass: null }), addTeacherToClass: async (classId, teacherId, isPrimary = false) => { await timetableService.addTeacherToClass(classId, teacherId, isPrimary); await get().fetchClass(classId); }, removeTeacherFromClass: async (classId, teacherId) => { await timetableService.removeTeacherFromClass(classId, teacherId); await get().fetchClass(classId); }, addStudentToClass: async (classId, studentId) => { await timetableService.addStudentToClass(classId, studentId); await get().fetchClass(classId); }, removeStudentFromClass: async (classId, studentId) => { await timetableService.removeStudentFromClass(classId, studentId); await get().fetchClass(classId); }, // ============================================================================ // Enrollment Request Actions // ============================================================================ fetchEnrollmentRequests: async (classId) => { set({ enrollmentRequestsLoading: true, enrollmentRequestsError: null }); try { const requests = await timetableService.getEnrollmentRequests(classId); set({ enrollmentRequests: requests, enrollmentRequestsLoading: false }); } catch (error) { console.error('Failed to fetch enrollment requests:', error); set({ enrollmentRequestsError: error instanceof Error ? error.message : 'Failed to fetch enrollment requests', enrollmentRequestsLoading: false, }); } }, requestEnrollment: async (classId, message) => { await timetableService.requestEnrollment(classId, message); }, respondToEnrollmentRequest: async (requestId, status, responseMessage) => { await timetableService.respondToEnrollmentRequest(requestId, status, responseMessage); await get().fetchEnrollmentRequests(get().currentClass?.id || ''); }, // ============================================================================ // Timetable Actions // ============================================================================ fetchTimetables: async (params = {}) => { set({ timetablesLoading: true, timetablesError: null }); try { const response = await timetableService.getTimetables(params); set({ timetables: response.items, totalCount: response.total, timetablesLoading: false, }); } catch (error) { console.error('Failed to fetch timetables:', error); set({ timetablesError: error instanceof Error ? error.message : 'Failed to fetch timetables', timetablesLoading: false, }); } }, fetchTimetable: async (timetableId) => { set({ timetablesLoading: true, timetablesError: null }); try { const timetable = await timetableService.getTimetable(timetableId); set({ currentTimetable: timetable, timetablesLoading: false }); } catch (error) { console.error('Failed to fetch timetable:', error); set({ timetablesError: error instanceof Error ? error.message : 'Failed to fetch timetable', timetablesLoading: false, }); } }, createTimetable: async (data) => { const newTimetable = await timetableService.createTimetable(data); set((state) => ({ timetables: [newTimetable, ...state.timetables], })); return newTimetable; }, updateTimetable: async (timetableId, data) => { const updatedTimetable = await timetableService.updateTimetable(timetableId, data); set((state) => ({ timetables: state.timetables.map((t) => (t.id === timetableId ? updatedTimetable : t)), currentTimetable: state.currentTimetable?.id === timetableId ? { ...state.currentTimetable, ...updatedTimetable } : state.currentTimetable, })); }, deleteTimetable: async (timetableId) => { await timetableService.deleteTimetable(timetableId); set((state) => ({ timetables: state.timetables.filter((t) => t.id !== timetableId), currentTimetable: state.currentTimetable?.id === timetableId ? null : state.currentTimetable, })); }, fetchMyTimetables: async () => { set({ timetablesLoading: true, timetablesError: null }); try { const timetables = await timetableService.getMyTimetables(); set({ timetables, timetablesLoading: false }); } catch (error) { console.error('Failed to fetch my timetables:', error); set({ timetablesError: error instanceof Error ? error.message : 'Failed to fetch my timetables', timetablesLoading: false, }); } }, fetchMyTeachingTimetables: async () => { set({ timetablesLoading: true, timetablesError: null }); try { const timetables = await timetableService.getMyTeachingTimetables(); set({ timetables, timetablesLoading: false }); } catch (error) { console.error('Failed to fetch my teaching timetables:', error); set({ timetablesError: error instanceof Error ? error.message : 'Failed to fetch my teaching timetables', timetablesLoading: false, }); } }, clearCurrentTimetable: () => set({ currentTimetable: null }), addTeacherToTimetable: async (timetableId, teacherId, isPrimary = false) => { await timetableService.addTeacherToTimetable(timetableId, teacherId, isPrimary); await get().fetchTimetable(timetableId); }, removeTeacherFromTimetable: async (timetableId, teacherId) => { await timetableService.removeTeacherFromTimetable(timetableId, teacherId); await get().fetchTimetable(timetableId); }, addTimetableLesson: async (timetableId, data) => { await timetableService.addTimetableLesson(timetableId, data); await get().fetchTimetable(timetableId); }, updateTimetableLesson: async (timetableId, lessonId, data) => { await timetableService.updateTimetableLesson(timetableId, lessonId, data); await get().fetchTimetable(timetableId); }, removeTimetableLesson: async (timetableId, lessonId) => { await timetableService.removeTimetableLesson(timetableId, lessonId); await get().fetchTimetable(timetableId); }, // ============================================================================ // Lesson Actions // ============================================================================ fetchLessons: async (params = {}) => { set({ lessonsLoading: true, lessonsError: null }); try { const response = await timetableService.getLessons(params); set({ lessons: response.items, totalCount: response.total, lessonsLoading: false, }); } catch (error) { console.error('Failed to fetch lessons:', error); set({ lessonsError: error instanceof Error ? error.message : 'Failed to fetch lessons', lessonsLoading: false, }); } }, fetchLesson: async (lessonId) => { set({ lessonsLoading: true, lessonsError: null }); try { const lesson = await timetableService.getLesson(lessonId); set({ currentLesson: lesson, lessonsLoading: false }); } catch (error) { console.error('Failed to fetch lesson:', error); set({ lessonsError: error instanceof Error ? error.message : 'Failed to fetch lesson', lessonsLoading: false, }); } }, generateLessons: async (timetableId, startDate, endDate) => { set({ lessonsLoading: true, lessonsError: null }); try { await timetableService.generateLessons(timetableId, startDate, endDate); await get().fetchLessons({ timetableId }); set({ lessonsLoading: false }); } catch (error) { console.error('Failed to generate lessons:', error); set({ lessonsError: error instanceof Error ? error.message : 'Failed to generate lessons', lessonsLoading: false, }); } }, cancelLesson: async (lessonId, reason) => { await timetableService.cancelLesson(lessonId, reason); await get().fetchLesson(lessonId); }, clearCurrentLesson: () => set({ currentLesson: null }), // ============================================================================ // Filter Actions // ============================================================================ setFilterSubject: (subject) => set({ filterSubject: subject, currentPage: 0 }), setFilterSchoolYear: (schoolYear) => set({ filterSchoolYear: schoolYear, currentPage: 0 }), setFilterAcademicTerm: (academicTerm) => set({ filterAcademicTerm: academicTerm, currentPage: 0 }), setSearchQuery: (query) => set({ searchQuery: query, currentPage: 0 }), setPage: (page) => set({ currentPage: page }), setPageSize: (size) => set({ pageSize: size, currentPage: 0 }), clearFilters: () => set({ filterSubject: null, filterSchoolYear: null, filterAcademicTerm: null, searchQuery: '', currentPage: 0, }), }), { name: 'timetable-store', partialize: (state) => ({ filterSubject: state.filterSubject, filterSchoolYear: state.filterSchoolYear, filterAcademicTerm: state.filterAcademicTerm, pageSize: state.pageSize, }), } ), { name: 'TimetableStore' } ) ); export default useTimetableStore;