From b008b6005256c4d4a2238f78d4ada99b190c8c5e Mon Sep 17 00:00:00 2001 From: ludavidca Date: Tue, 1 Apr 2025 21:51:52 -0400 Subject: [PATCH 1/6] Finished PR, need Big Dawg's approval --- src/components/FileUpload.tsx | 4 - src/components/InputForm.tsx | 7 +- src/components/LessonPlanView.tsx | 147 ++++++++++++++++++++++++++---- 3 files changed, 136 insertions(+), 22 deletions(-) diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index e13b6b33..655e452b 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -65,10 +65,6 @@ export const FileUpload: React.FC = ({ return ( - - {label} - - = ({ {errors.reasonOfAbsence} - + + + Lesson Plan + + + diff --git a/src/components/LessonPlanView.tsx b/src/components/LessonPlanView.tsx index c399324f..08a6dc48 100644 --- a/src/components/LessonPlanView.tsx +++ b/src/components/LessonPlanView.tsx @@ -6,15 +6,22 @@ import { Link, Text, useTheme, + VStack, + Button, + HStack, + useToast, } from '@chakra-ui/react'; import { FiFileText } from 'react-icons/fi'; import { VscArrowSwap } from 'react-icons/vsc'; +import { FileUpload } from './FileUpload'; +import React, { useState, useEffect } from 'react'; const LessonPlanDisplay = ({ href, fileName, fileSize, isUserAbsentTeacher, + onSwapClick, }) => { const theme = useTheme(); @@ -55,6 +62,7 @@ const LessonPlanDisplay = ({ } size="sm" variant="ghost" + onClick={onSwapClick} /> )} @@ -98,39 +106,144 @@ const NoLessonPlanViewingDisplay = ({ ); }; -const NoLessonPlanDeclaredDisplay = () => { +const NoLessonPlanDeclaredDisplay = ({ onLessonPlanUploaded }) => { const theme = useTheme(); + const toast = useToast(); + const [lessonPlan, setLessonPlan] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const uploadFile = async (file) => { + const formData = new FormData(); + formData.append('file', file); + formData.append('fileName', file.name); + + const res = await fetch('/api/uploadFile/', { + method: 'POST', + body: formData, + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.message || 'Failed to upload file'); + } + + const data = await res.json(); + return `https://drive.google.com/file/d/${data.fileId}/view`; + }; + + const handleUploadFile = async (e) => { + e.preventDefault(); + setIsSubmitting(true); + + try { + if (lessonPlan) { + const lessonPlanUrl = await uploadFile(lessonPlan); + console.log('File uploaded successfully:', lessonPlanUrl); + setLessonPlan(null); + + if (onLessonPlanUploaded) { + onLessonPlanUploaded(lessonPlanUrl); + } + //TODO: Add the patch to the DB, and also make sure that this component knows which absence to edit / interact with + toast({ + title: 'Success', + description: `You have successfully uploaded your lesson plan.`, + status: 'success', + duration: 5000, + isClosable: true, + }); + } + } catch (error) { + console.error('Error uploading file:', error); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = (e) => { + e.preventDefault(); + setLessonPlan(null); + }; return ( - - - Upload PDF component - - + + + + {lessonPlan && ( + + + + + )} + ); }; const LessonPlanView = ({ - lessonPlan, + lessonPlan: initialLessonPlan, absentTeacherFirstName, isUserAbsentTeacher, isUserSubstituteTeacher, }) => { - const getFileName = (url) => (url ? 'File name' : ''); - const getFileSize = (url) => (url ? 'File size' : ''); + const [currentLessonPlan, setCurrentLessonPlan] = useState(initialLessonPlan); + const [showUploadForm, setShowUploadForm] = useState(false); + + useEffect(() => { + setCurrentLessonPlan(initialLessonPlan); + }, [initialLessonPlan]); + + const handleLessonPlanUploaded = (newLessonPlanUrl) => { + setCurrentLessonPlan(newLessonPlanUrl); + setShowUploadForm(false); + }; - return lessonPlan ? ( + const handleSwapClick = () => { + setShowUploadForm(true); + }; + + const getFileName = (url) => + url ? url.split('/').pop() || 'Lesson Plan' : ''; + const getFileSize = (url) => (url ? 'PDF Document' : ''); + + if (showUploadForm && isUserAbsentTeacher) { + return ( + + ); + } + + return currentLessonPlan ? ( ) : isUserAbsentTeacher ? ( - + ) : ( Date: Fri, 11 Apr 2025 17:29:18 -0400 Subject: [PATCH 2/6] Squash --- .../{InputForm.tsx => DeclareAbsenceForm.tsx} | 260 ++++---------- src/components/EditAbsenceForm.tsx | 322 ++++++++++++++++++ src/components/FileUpload.tsx | 23 +- src/components/LessonPlanView.tsx | 243 ++++++------- 4 files changed, 509 insertions(+), 339 deletions(-) rename src/components/{InputForm.tsx => DeclareAbsenceForm.tsx} (50%) create mode 100644 src/components/EditAbsenceForm.tsx diff --git a/src/components/InputForm.tsx b/src/components/DeclareAbsenceForm.tsx similarity index 50% rename from src/components/InputForm.tsx rename to src/components/DeclareAbsenceForm.tsx index b19f2ec3..1dc1badc 100644 --- a/src/components/InputForm.tsx +++ b/src/components/DeclareAbsenceForm.tsx @@ -5,44 +5,39 @@ import { FormErrorMessage, FormLabel, Input, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, Text, Textarea, VStack, useDisclosure, useToast, } from '@chakra-ui/react'; +import { submitAbsence } from '@utils/submitAbsence'; +import { validateAbsenceForm } from '@utils/validateAbsenceForm'; import { Absence, Prisma } from '@prisma/client'; import { useState } from 'react'; +import { AdminTeacherFields } from './AdminTeacherFields'; +import { ConfirmAbsenceModal } from './ConfirmAbsenceModal'; import { DateOfAbsence } from './DateOfAbsence'; import { FileUpload } from './FileUpload'; import { InputDropdown } from './InputDropdown'; -import { SearchDropdown } from './SearchDropdown'; -interface InputFormProps { +interface DeclareAbsenceFormProps { onClose?: () => void; - onDeclareAbsence: ( - absence: Prisma.AbsenceCreateManyInput - ) => Promise; userId: number; onTabChange: (tab: 'explore' | 'declared') => void; initialDate: Date; isAdminMode: boolean; + fetchAbsences: () => Promise; } -const InputForm: React.FC = ({ +const DeclareAbsenceForm: React.FC = ({ onClose, - onDeclareAbsence, userId, onTabChange, initialDate, isAdminMode, + fetchAbsences, }) => { const toast = useToast(); const { isOpen, onOpen, onClose: closeModal } = useDisclosure(); @@ -61,24 +56,7 @@ const InputForm: React.FC = ({ const [errors, setErrors] = useState>({}); const validateForm = () => { - const newErrors: Record = {}; - - if (!formData.absentTeacherId) { - newErrors.absentTeacherId = 'Absent teacher ID is required'; - } - if (!formData.locationId) { - newErrors.locationId = 'Location ID is required'; - } - if (!formData.subjectId) { - newErrors.subjectId = 'Subject ID is required'; - } - if (!formData.reasonOfAbsence) { - newErrors.reasonOfAbsence = 'Reason of absence is required'; - } - if (!formData.lessonDate) { - newErrors.lessonDate = 'Date is required'; - } - + const newErrors = validateAbsenceForm(formData); setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -120,80 +98,34 @@ const InputForm: React.FC = ({ setIsSubmitting(true); try { - const lessonDate = new Date(formData.lessonDate + 'T00:00:00'); - - let lessonPlanData: { url: string; name: string; size: number } | null = - null; - if (lessonPlan) { - const lessonPlanUrl = await uploadFile(lessonPlan); - - if (lessonPlanUrl === null) { - toast({ - title: 'Error', - description: 'Failed to upload the lesson plan file', - status: 'error', - duration: 5000, - isClosable: true, - }); - return; - } - - lessonPlanData = { - url: lessonPlanUrl, - name: lessonPlan.name, - size: lessonPlan.size, - }; - } - - const absenceData = { - lessonDate: lessonDate, - reasonOfAbsence: formData.reasonOfAbsence, - absentTeacherId: parseInt(formData.absentTeacherId, 10), - substituteTeacherId: formData.substituteTeacherId - ? parseInt(formData.substituteTeacherId, 10) - : null, - locationId: parseInt(formData.locationId, 10), - subjectId: parseInt(formData.subjectId, 10), - notes: formData.notes, - roomNumber: formData.roomNumber || null, - lessonPlanFile: lessonPlanData, - }; - - const response = await onDeclareAbsence(absenceData); - - if (response) { - const options: Intl.DateTimeFormatOptions = { - weekday: 'long', - month: 'long', - day: 'numeric', - }; - const formattedLessonDate = lessonDate.toLocaleDateString( - 'en-US', - options - ); + const result = await submitAbsence({ + formData, + lessonPlan, + onDeclareAbsence: handleDeclareAbsence, + }); + if (result.success) { toast({ title: 'Success', - description: `You have successfully declared an absence on ${formattedLessonDate}.`, + description: `You have successfully declared an absence on ${result.message}.`, status: 'success', duration: 5000, isClosable: true, }); - if ( + const userIsInvolved = parseInt(formData.substituteTeacherId, 10) === userId || - parseInt(formData.absentTeacherId, 10) === userId - ) { + parseInt(formData.absentTeacherId, 10) === userId; + + if (userIsInvolved) { onTabChange('declared'); } - if (onClose) { - onClose(); - } + onClose?.(); } else { toast({ title: 'Error', - description: 'Failed to declare absence', + description: result.message, status: 'error', duration: 5000, isClosable: true, @@ -213,23 +145,27 @@ const InputForm: React.FC = ({ } }; - const uploadFile = async (file: File): Promise => { - const formData = new FormData(); - formData.append('file', file); - formData.append('fileName', file.name); + const handleDeclareAbsence = async ( + absence: Prisma.AbsenceCreateManyInput + ): Promise => { + try { + const res = await fetch('/api/declareAbsence', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(absence), + }); - const res = await fetch('/api/uploadFile/', { - method: 'POST', - body: formData, - }); + if (!res.ok) { + throw new Error(`Failed to add absence: ${res.statusText}`); + } - if (!res.ok) { - const data = await res.json(); - throw new Error(data.message || 'Failed to upload file'); + const addedAbsence = await res.json(); + await fetchAbsences(); + return addedAbsence; + } catch (error) { + console.error('Error adding absence:', error); + return null; } - - const data = await res.json(); - return `https://drive.google.com/file/d/${data.fileId}/view`; }; const handleDateSelect = (date: Date) => { @@ -249,58 +185,12 @@ const InputForm: React.FC = ({ > {isAdminMode && ( - <> - - - Teacher Absent - - { - // Handle selected subject - setFormData((prev) => ({ - ...prev, - absentTeacherId: value ? String(value.id) : '', - })); - // Clear error when user selects a value - if (errors.absentTeacherId) { - setErrors((prev) => ({ - ...prev, - absentTeacherId: '', - })); - } - }} - /> - {errors.absentTeacherId} - - - - - Substitute Teacher - - { - // Handle selected subject - setFormData((prev) => ({ - ...prev, - substituteTeacherId: value ? String(value.id) : '', - })); - // Clear error when user selects a value - if (errors.substituteTeacherId) { - setErrors((prev) => ({ - ...prev, - substituteTeacherId: '', - })); - } - }} - /> - - + )} @@ -308,7 +198,7 @@ const InputForm: React.FC = ({ Subject { setFormData((prev) => ({ @@ -331,7 +221,7 @@ const InputForm: React.FC = ({ Location { setFormData((prev) => ({ @@ -349,10 +239,11 @@ const InputForm: React.FC = ({ {errors.locationId} - + Room Number = ({ /> - + Reason of Absence