299 lines
8.8 KiB
TypeScript
299 lines
8.8 KiB
TypeScript
/**
|
|
* 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<number, AssignmentDraft>; // keyed by assignment_id
|
|
currentAssignment: Assignment | null;
|
|
isLoading: boolean;
|
|
isSubmitting: boolean;
|
|
error: string | null;
|
|
|
|
// Actions - Assignments
|
|
fetchAssignments: () => Promise<void>;
|
|
fetchAssignment: (id: number) => Promise<void>;
|
|
|
|
// Actions - Submissions
|
|
submitAssignment: (assignmentId: number, data: Partial<AssignmentSubmission>) => Promise<void>;
|
|
fetchSubmission: (assignmentId: number) => Promise<AssignmentSubmission | null>;
|
|
|
|
// 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<number, AssignmentDraft>;
|
|
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<DraftStore>()(
|
|
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<AssignmentStore>((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; |