/** * Student Assignments Module - Zustand Store */ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import api from './auth'; export interface Assignment { id: number; title: string; description: string; subject: string; class_id: number; class_name: string; teacher_id: number; teacher_name: string; due_date: string; total_marks: number; status: 'pending' | 'in_progress' | 'completed' | 'graded'; attachments?: string[]; instructions?: string; } export interface AssignmentDraft { assignment_id: number; content: string; file_name?: string; file_data?: string; saved_at: string; } interface AssignmentSubmission { id?: number; assignment_id: number; content: string; file_name?: string; file_data?: string; submitted_at?: string; marks?: number; max_marks?: number; feedback?: string; graded_at?: string; } interface AssignmentStore { // State assignments: Assignment[]; submissions: AssignmentSubmission[]; drafts: Record; // keyed by assignment_id currentAssignment: Assignment | null; isLoading: boolean; isSubmitting: boolean; error: string | null; // Actions - Assignments fetchAssignments: () => Promise; fetchAssignment: (id: number) => Promise; // Actions - Submissions submitAssignment: (assignmentId: number, data: Partial) => Promise; fetchSubmission: (assignmentId: number) => Promise; // Actions - Drafts (local storage) saveDraft: (assignmentId: number, content: string, file?: { name: string; data: string }) => void; loadDraft: (assignmentId: number) => AssignmentDraft | null; deleteDraft: (assignmentId: number) => void; getDraftsCount: () => number; // Helpers getPendingAssignments: () => Assignment[]; getInProgressAssignments: () => Assignment[]; getCompletedAssignments: () => Assignment[]; getOverdueAssignments: () => Assignment[]; isOverdue: (dueDate: string) => boolean; getTimeRemaining: (dueDate: string) => string; } // Draft storage with persist middleware interface DraftStore { drafts: Record; saveDraft: (assignmentId: number, content: string, file?: { name: string; data: string }) => void; loadDraft: (assignmentId: number) => AssignmentDraft | null; deleteDraft: (assignmentId: number) => void; getDraftsCount: () => number; } const useDraftStore = create()( persist( (set, get) => ({ drafts: {}, saveDraft: (assignmentId, content, file) => { const draft: AssignmentDraft = { assignment_id: assignmentId, content, file_name: file?.name, file_data: file?.data, saved_at: new Date().toISOString(), }; set(state => ({ drafts: { ...state.drafts, [assignmentId]: draft }, })); }, loadDraft: (assignmentId) => { return get().drafts[assignmentId] || null; }, deleteDraft: (assignmentId) => { set(state => { const { [assignmentId]: _, ...rest } = state.drafts; return { drafts: rest }; }); }, getDraftsCount: () => { return Object.keys(get().drafts).length; }, }), { name: 'assignment-drafts' } ) ); export const useAssignmentStore = create((set, get) => ({ assignments: [], submissions: [], drafts: {}, currentAssignment: null, isLoading: false, isSubmitting: false, error: null, // ============ Assignments ============ fetchAssignments: async () => { try { set({ isLoading: true, error: null }); const response = await api.get('/assignments'); set({ assignments: response.data, isLoading: false }); } catch (error: any) { set({ error: error.message, isLoading: false }); console.error('Failed to fetch assignments:', error); } }, fetchAssignment: async (id) => { try { set({ isLoading: true, error: null }); const response = await api.get(`/assignments/${id}`); set({ currentAssignment: response.data, isLoading: false }); } catch (error: any) { set({ error: error.message, isLoading: false }); console.error('Failed to fetch assignment:', error); } }, // ============ Submissions ============ submitAssignment: async (assignmentId, data) => { try { set({ isSubmitting: true, error: null }); const formData = new FormData(); formData.append('content', data.content || ''); formData.append('assignment_id', String(assignmentId)); if (data.file_name) { formData.append('file_name', data.file_name); } if (data.file_data) { // Convert base64 to blob if needed const byteCharacters = atob(data.file_data); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray]); formData.append('file', blob, data.file_name || 'submission'); } const response = await api.post('/assignments/submit', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); // Delete local draft after successful submission useDraftStore.getState().deleteDraft(assignmentId); // Update assignment status in local state set(state => ({ submissions: [...state.submissions, response.data], assignments: state.assignments.map(a => a.id === assignmentId ? { ...a, status: 'completed' as const } : a ), isSubmitting: false, })); } catch (error: any) { set({ error: error.response?.data?.error || error.message, isSubmitting: false }); console.error('Failed to submit assignment:', error); throw error; } }, fetchSubmission: async (assignmentId) => { try { const response = await api.get(`/assignments/${assignmentId}/submission`); if (response.data) { set(state => ({ submissions: [ ...state.submissions.filter(s => s.assignment_id !== assignmentId), response.data, ], })); } return response.data || null; } catch (error: any) { if (error.response?.status !== 404) { console.error('Failed to fetch submission:', error); } return null; } }, // ============ Drafts (delegated to draft store) ============ saveDraft: (assignmentId, content, file) => { useDraftStore.getState().saveDraft(assignmentId, content, file); set({ drafts: useDraftStore.getState().drafts }); }, loadDraft: (assignmentId) => { return useDraftStore.getState().loadDraft(assignmentId); }, deleteDraft: (assignmentId) => { useDraftStore.getState().deleteDraft(assignmentId); set({ drafts: useDraftStore.getState().drafts }); }, getDraftsCount: () => { return useDraftStore.getState().getDraftsCount(); }, // ============ Helpers ============ getPendingAssignments: () => { return get().assignments.filter(a => a.status === 'pending'); }, getInProgressAssignments: () => { return get().assignments.filter(a => a.status === 'in_progress'); }, getCompletedAssignments: () => { return get().assignments.filter(a => a.status === 'completed' || a.status === 'graded'); }, getOverdueAssignments: () => { const now = new Date(); return get().assignments.filter(a => { if (a.status === 'completed' || a.status === 'graded') return false; const dueDate = new Date(a.due_date); return dueDate < now; }); }, isOverdue: (dueDate) => { return new Date(dueDate) < new Date(); }, getTimeRemaining: (dueDate) => { const now = new Date(); const due = new Date(dueDate); const diff = due.getTime() - now.getTime(); if (diff < 0) { const overdueDays = Math.ceil(Math.abs(diff) / (1000 * 60 * 60 * 24)); return `${overdueDays} day${overdueDays > 1 ? 's' : ''} overdue`; } const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); if (days > 0) { return `${days} day${days > 1 ? 's' : ''} left`; } if (hours > 0) { return `${hours} hour${hours > 1 ? 's' : ''} left`; } const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); return `${minutes} minute${minutes > 1 ? 's' : ''} left`; }, })); // Export both stores export { useDraftStore }; export default useAssignmentStore;