feat(timetable): add page components, services, stores and types
- Add timetable page components: - ClassesListPage.tsx (browse and search classes) - MyClassesPage.tsx (student enrolled classes) - EnrollmentRequestsPage.tsx (teacher approval interface) - TimetablePage.tsx (weekly schedule view) - LessonViewPage.tsx (TLDraw-integrated lesson view) - Add timetableService.ts for API communication - Add timetableStore.ts for state management - Add timetable.types.ts for TypeScript definitions - Add common components (LoadingSpinner, ErrorMessage, EmptyState) - Add .env.development with local development configuration
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
import axios from 'axios';
|
||||
import { supabase } from '../supabaseClient';
|
||||
import {
|
||||
Class,
|
||||
ClassWithRelations,
|
||||
ClassFilters,
|
||||
Timetable,
|
||||
TimetableWithRelations,
|
||||
TimetableLesson,
|
||||
Lesson,
|
||||
LessonWithRelations,
|
||||
EnrollmentRequest,
|
||||
EnrollmentRequestWithProfile,
|
||||
} from '../types/timetable.types';
|
||||
|
||||
// API base URL
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
|
||||
// ============================================================================
|
||||
// Helper: Get auth headers
|
||||
// ============================================================================
|
||||
|
||||
async function getAuthHeaders(): Promise<Record<string, string>> {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (!session?.access_token) {
|
||||
throw new Error('No authentication token available');
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Class Service
|
||||
// ============================================================================
|
||||
|
||||
export const classService = {
|
||||
async listClasses(filters?: ClassFilters): Promise<{ classes: Class[]; total: number }> {
|
||||
const headers = await getAuthHeaders();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters?.subject) params.append('subject', filters.subject);
|
||||
if (filters?.school_year) params.append('school_year', filters.school_year);
|
||||
if (filters?.academic_term) params.append('academic_term', filters.academic_term);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.skip !== undefined) params.append('skip', filters.skip.toString());
|
||||
if (filters?.limit !== undefined) params.append('limit', filters.limit.toString());
|
||||
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/classes?${params}`, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getClass(classId: string): Promise<ClassWithRelations> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/classes/${classId}`, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createClass(data: {
|
||||
name: string;
|
||||
subject: string;
|
||||
school_year: string;
|
||||
academic_term: string;
|
||||
description?: string;
|
||||
}): Promise<Class> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.post(`${API_BASE}/database/timetable/classes`, data, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateClass(classId: string, data: Partial<Class>): Promise<Class> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.patch(`${API_BASE}/database/timetable/classes/${classId}`, data, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteClass(classId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.delete(`${API_BASE}/database/timetable/classes/${classId}`, { headers });
|
||||
},
|
||||
|
||||
async getMyClasses(): Promise<Class[]> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/classes/me/student`, { headers });
|
||||
return response.data.classes;
|
||||
},
|
||||
|
||||
async getMyTeachingClasses(): Promise<Class[]> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/classes/me/teacher`, { headers });
|
||||
return response.data.classes;
|
||||
},
|
||||
|
||||
// Teacher management
|
||||
async addTeacher(classId: string, teacherId: string, isPrimary: boolean = false): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(
|
||||
`${API_BASE}/database/timetable/classes/${classId}/teachers`,
|
||||
{ teacher_id: teacherId, is_primary: isPrimary },
|
||||
{ headers }
|
||||
);
|
||||
},
|
||||
|
||||
async removeTeacher(classId: string, teacherId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.delete(`${API_BASE}/database/timetable/classes/${classId}/teachers/${teacherId}`, { headers });
|
||||
},
|
||||
|
||||
// Student management
|
||||
async addStudent(classId: string, studentId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(
|
||||
`${API_BASE}/database/timetable/classes/${classId}/students`,
|
||||
{ student_id: studentId },
|
||||
{ headers }
|
||||
);
|
||||
},
|
||||
|
||||
async removeStudent(classId: string, studentId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.delete(`${API_BASE}/database/timetable/classes/${classId}/students/${studentId}`, { headers });
|
||||
},
|
||||
|
||||
async leaveClass(classId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(`${API_BASE}/database/timetable/classes/${classId}/leave`, {}, { headers });
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Timetable Service
|
||||
// ============================================================================
|
||||
|
||||
export const timetableService = {
|
||||
async listTimetables(filters?: {
|
||||
class_id?: string;
|
||||
type?: 'ad-hoc' | 'recurring';
|
||||
active?: boolean;
|
||||
}): Promise<Timetable[]> {
|
||||
const headers = await getAuthHeaders();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters?.class_id) params.append('class_id', filters.class_id);
|
||||
if (filters?.type) params.append('type', filters.type);
|
||||
if (filters?.active !== undefined) params.append('active', filters.active.toString());
|
||||
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/timetables?${params}`, { headers });
|
||||
return response.data.timetables;
|
||||
},
|
||||
|
||||
async getTimetable(timetableId: string): Promise<TimetableWithRelations> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/timetables/${timetableId}`, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createTimetable(data: {
|
||||
class_id: string;
|
||||
name: string;
|
||||
type: 'ad-hoc' | 'recurring';
|
||||
start_date: string;
|
||||
end_date?: string;
|
||||
recurrence_pattern?: Record<string, unknown>;
|
||||
recurrence_rule?: string;
|
||||
whiteboard_type?: 'single' | 'multiple';
|
||||
}): Promise<Timetable> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.post(`${API_BASE}/database/timetable/timetables`, data, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateTimetable(timetableId: string, data: Partial<Timetable>): Promise<Timetable> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.patch(`${API_BASE}/database/timetable/timetables/${timetableId}`, data, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteTimetable(timetableId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.delete(`${API_BASE}/database/timetable/timetables/${timetableId}`, { headers });
|
||||
},
|
||||
|
||||
async generateLessons(timetableId: string, startDate: string, endDate: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(
|
||||
`${API_BASE}/database/timetable/timetables/${timetableId}/generate-lessons`,
|
||||
{ start_date: startDate, end_date: endDate },
|
||||
{ headers }
|
||||
);
|
||||
},
|
||||
|
||||
// Timetable Teachers
|
||||
async addTimetableTeacher(timetableId: string, teacherId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(
|
||||
`${API_BASE}/database/timetable/timetables/${timetableId}/teachers`,
|
||||
{ teacher_id: teacherId },
|
||||
{ headers }
|
||||
);
|
||||
},
|
||||
|
||||
async removeTimetableTeacher(timetableId: string, teacherId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.delete(`${API_BASE}/database/timetable/timetables/${timetableId}/teachers/${teacherId}`, { headers });
|
||||
},
|
||||
|
||||
// Lessons from timetable
|
||||
async getTimetableLessons(timetableId: string): Promise<TimetableLesson[]> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/timetables/${timetableId}/lessons`, { headers });
|
||||
return response.data.lessons;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Lesson Service
|
||||
// ============================================================================
|
||||
|
||||
export const lessonService = {
|
||||
async listLessons(filters?: {
|
||||
timetable_id?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
status?: 'scheduled' | 'in-progress' | 'completed' | 'cancelled';
|
||||
}): Promise<Lesson[]> {
|
||||
const headers = await getAuthHeaders();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters?.timetable_id) params.append('timetable_id', filters.timetable_id);
|
||||
if (filters?.start_date) params.append('start_date', filters.start_date);
|
||||
if (filters?.end_date) params.append('end_date', filters.end_date);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/lessons?${params}`, { headers });
|
||||
return response.data.lessons;
|
||||
},
|
||||
|
||||
async getLesson(lessonId: string): Promise<LessonWithRelations> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/lessons/${lessonId}`, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createLesson(data: {
|
||||
timetable_id: string;
|
||||
scheduled_start: string;
|
||||
scheduled_end: string;
|
||||
teacher_id?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}): Promise<Lesson> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.post(`${API_BASE}/database/timetable/lessons`, data, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateLesson(lessonId: string, data: Partial<Lesson>): Promise<Lesson> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.patch(`${API_BASE}/database/timetable/lessons/${lessonId}`, data, { headers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteLesson(lessonId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.delete(`${API_BASE}/database/timetable/lessons/${lessonId}`, { headers });
|
||||
},
|
||||
|
||||
async startLesson(lessonId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(`${API_BASE}/database/timetable/lessons/${lessonId}/start`, {}, { headers });
|
||||
},
|
||||
|
||||
async endLesson(lessonId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(`${API_BASE}/database/timetable/lessons/${lessonId}/end`, {}, { headers });
|
||||
},
|
||||
|
||||
async cancelLesson(lessonId: string, reason?: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(
|
||||
`${API_BASE}/database/timetable/lessons/${lessonId}/cancel`,
|
||||
{ reason },
|
||||
{ headers }
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Enrollment Request Service
|
||||
// ============================================================================
|
||||
|
||||
export const enrollmentService = {
|
||||
async requestEnrollment(classId: string, message?: string): Promise<EnrollmentRequest> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.post(
|
||||
`${API_BASE}/database/timetable/classes/${classId}/enroll`,
|
||||
{ message },
|
||||
{ headers }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async listEnrollmentRequests(classId: string): Promise<EnrollmentRequestWithProfile[]> {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await axios.get(`${API_BASE}/database/timetable/classes/${classId}/enrollment-requests`, { headers });
|
||||
return response.data.requests;
|
||||
},
|
||||
|
||||
async respondToEnrollment(requestId: string, status: 'approved' | 'rejected'): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(
|
||||
`${API_BASE}/database/timetable/enrollment-requests/${requestId}/respond`,
|
||||
{ status },
|
||||
{ headers }
|
||||
);
|
||||
},
|
||||
|
||||
async cancelEnrollmentRequest(requestId: string): Promise<void> {
|
||||
const headers = await getAuthHeaders();
|
||||
await axios.post(`${API_BASE}/database/timetable/enrollment-requests/${requestId}/cancel`, {}, { headers });
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Combined Export
|
||||
// ============================================================================
|
||||
|
||||
export const timetableService = {
|
||||
...classService,
|
||||
...timetableService,
|
||||
...lessonService,
|
||||
...enrollmentService,
|
||||
};
|
||||
|
||||
export default timetableService;
|
||||
Reference in New Issue
Block a user