app/src/services/timetableService.ts
Agent Zero 11c139b410 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
2026-02-26 03:27:46 +00:00

336 lines
12 KiB
TypeScript

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;