diff --git a/(auth)/login.tsx b/(auth)/login.tsx new file mode 100644 index 0000000..e69de29 diff --git a/(users)/search-users.tsx b/(users)/search-users.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/(courses)/assessment-detail.tsx b/app/(courses)/assessment-detail.tsx new file mode 100644 index 0000000..cca70b8 --- /dev/null +++ b/app/(courses)/assessment-detail.tsx @@ -0,0 +1 @@ +export { default } from '../../screens/AssessmentDetailScreen'; diff --git a/app/(courses)/assessments.tsx b/app/(courses)/assessments.tsx new file mode 100644 index 0000000..8b90df4 --- /dev/null +++ b/app/(courses)/assessments.tsx @@ -0,0 +1 @@ +export { default } from '../../screens/AssessmentScreen'; diff --git a/app/(courses)/course-stats.tsx b/app/(courses)/course-stats.tsx new file mode 100644 index 0000000..8b3190a --- /dev/null +++ b/app/(courses)/course-stats.tsx @@ -0,0 +1 @@ +export { default } from '../../screens/CourseStatsScreen'; diff --git a/app/(courses)/exercises-correction.tsx b/app/(courses)/exercises-correction.tsx new file mode 100644 index 0000000..8f18225 --- /dev/null +++ b/app/(courses)/exercises-correction.tsx @@ -0,0 +1 @@ +export { default } from '../../screens/CorrectionExerciseScreen'; diff --git a/app/(courses)/exercises.tsx b/app/(courses)/exercises.tsx new file mode 100644 index 0000000..6a413d1 --- /dev/null +++ b/app/(courses)/exercises.tsx @@ -0,0 +1 @@ +export { default } from '../../screens/ExercisesScreen'; diff --git a/components/ui/forms/AssessmentForm.tsx b/components/ui/forms/AssessmentForm.tsx new file mode 100644 index 0000000..c31ecfb --- /dev/null +++ b/components/ui/forms/AssessmentForm.tsx @@ -0,0 +1,480 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + StyleSheet, + Text, + TouchableOpacity, + ScrollView, + Modal, + Alert, +} from 'react-native'; +import TextField from '../fields/TextField'; +import Button from '../buttons/Button'; +import Dialog from '../alerts/Dialog'; +import { spacing } from '../../../constants/spacing'; +import { fonts } from '../../../constants/fonts'; +import { useTheme } from '../../../context/ThemeContext'; +import { useAuth } from '../../../context/AuthContext'; +import DateTimePicker from '@react-native-community/datetimepicker'; +import ExerciseForm from './ExerciseForm'; +import { + AssessmentType, + AssessmentExercise, + createAssessment, + updateAssessment, + Assessment, +} from '../../../services/assessmentsApi'; + +type AssessmentFormProps = { + courseId: number; + onClose: () => void; + initialData?: Assessment; +}; + +export default function AssessmentForm({ courseId, onClose, initialData }: AssessmentFormProps) { + const theme = useTheme(); + const { user, authToken } = useAuth(); + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [type, setType] = useState('Exam'); + const [startDate, setStartDate] = useState(new Date()); + const [deadline, setDeadline] = useState(new Date()); + const [tolerance, setTolerance] = useState('2'); + const [errorVisible, setErrorVisible] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const [exercises, setExercises] = useState([]); + const [exerciseModalVisible, setExerciseModalVisible] = useState(false); + const [editingIndex, setEditingIndex] = useState(null); + + const [showPickerModal, setShowPickerModal] = useState(false); + const [pickerMode, setPickerMode] = useState<'date' | 'time'>('date'); + const [pickerField, setPickerField] = useState<'start' | 'deadline' | null>(null); + const [dateBuffer, setDateBuffer] = useState(new Date()); + + useEffect(() => { + if (initialData) { + const copy = JSON.parse(JSON.stringify(initialData)); + setTitle(copy.title); + setDescription(copy.description); + setType(copy.type); + setTolerance(String(copy.toleranceTime)); + setStartDate(new Date(copy.startTime)); + setDeadline(new Date(copy.deadline)); + setExercises(copy.exercises || []); + } + }, []); + + useEffect(() => { + if (type === 'Task') { + setTolerance('0'); + } + }, [type]); + + const handleSubmit = async () => { + if (!title.trim()) { + setErrorMessage('❌ The assessment must have a title.'); + setErrorVisible(true); + return; + } + + if (!user?.uuid) { + setErrorMessage('❌ User is not authenticated. Please log in again.'); + setErrorVisible(true); + return; + } + + if (!authToken) { + setErrorMessage('❌ Authentication token is missing. Please reload the app.'); + setErrorVisible(true); + return; + } + + if (startDate >= deadline) { + setErrorMessage('❌ Start time must be before the deadline.'); + setErrorVisible(true); + return; + } + + const parsedTolerance = parseInt(tolerance) || 0; + + if (type === 'Exam' && parsedTolerance === 0) { + setErrorMessage('❌ Exam duration cannot be 0. Please specify a valid tolerance time.'); + setErrorVisible(true); + return; + } + + if (exercises.length === 0) { + setErrorMessage('❌ At least one exercise is required.'); + setErrorVisible(true); + return; + } + + try { + const basePayload = { + title: title.trim(), + description: description.trim(), + toleranceTime: type === 'Task' ? 0 : parsedTolerance, + startTime: startDate.toISOString(), + deadline: deadline.toISOString(), + exercises, + }; + + if (initialData) { + console.log('Updating assessment with data:', basePayload); + await updateAssessment(initialData.id, basePayload, user.uuid, authToken); + Alert.alert('✅ Assessment updated'); + } else { + const fullPayload = { + ...basePayload, + type, + }; + console.log('Creating assessment with data:', fullPayload); + await createAssessment(fullPayload, courseId, user.uuid, authToken); + Alert.alert('✅ Assessment created'); + } + + onClose(); + } catch (e: unknown) { + if (e instanceof Error) { + //console.error(e.message); + setErrorMessage(`❌ ${e.message}`); + } else { + //console.error(e); + setErrorMessage('❌ Something went wrong. Please try again.'); + } + setErrorVisible(true); + } + }; + + const openPicker = (field: 'start' | 'deadline', initial: Date) => { + setPickerField(field); + setDateBuffer(initial); + setPickerMode('date'); + setShowPickerModal(true); + }; + + const renderDateField = (label: string, date: Date, field: 'start' | 'deadline') => ( + openPicker(field, date)} + > + + {label}: {date.toLocaleDateString()} {date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + ); + + const handleAddExercise = (exercise: AssessmentExercise) => { + if (editingIndex !== null) { + const updated = [...exercises]; + updated[editingIndex] = exercise; + setExercises(updated); + setEditingIndex(null); + } else { + setExercises((prev) => [...prev, exercise]); + } + setExerciseModalVisible(false); + }; + + const handleEditExercise = (index: number) => { + setEditingIndex(index); + setExerciseModalVisible(true); + }; + + const confirmRemoveExercise = (index: number) => { + Alert.alert( + 'Delete Exercise', + 'Are you sure you want to delete this exercise?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Delete', + style: 'destructive', + onPress: () => { + const copy = [...exercises]; + copy.splice(index, 1); + setExercises(copy); + }, + }, + ] + ); + }; + + return ( + <> + + + {initialData ? 'Edit Assessment' : 'Create Assessment'} + + + + {(['Exam', 'Task'] as AssessmentType[]).map((option) => ( + setType(option)} + style={[styles.typeButton, { + backgroundColor: type === option ? theme.primary : theme.card, + borderColor: theme.primary, + }]} + > + + {option} + + + ))} + + + + + + + + {renderDateField('Start Time', startDate, 'start')} + {renderDateField('Deadline', deadline, 'deadline')} + + + Exercises + { + setEditingIndex(null); + setExerciseModalVisible(true); + }}> + + + + + + {exercises.map((ex, i) => ( + + • {ex.enunciate} ({ex.type}) + handleEditExercise(i)}> + ✏️ + + confirmRemoveExercise(i)}> + 🗑️ + + + ))} + + + +