geocrop-platform./apps/nextgen/client/src/store/assignments.ts

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;