From 965813068f954a6763c31b728baab41f79c40bc1 Mon Sep 17 00:00:00 2001 From: Colin Toft Date: Fri, 11 Apr 2025 19:58:53 -0400 Subject: [PATCH 01/11] Move absence details to the right of event --- src/components/AbsenceDetails.tsx | 363 ++++++++++++++---------------- src/pages/calendar.tsx | 147 +++++++++--- 2 files changed, 286 insertions(+), 224 deletions(-) diff --git a/src/components/AbsenceDetails.tsx b/src/components/AbsenceDetails.tsx index 4b5f1f29..4d8b89cd 100644 --- a/src/components/AbsenceDetails.tsx +++ b/src/components/AbsenceDetails.tsx @@ -1,6 +1,7 @@ import { Box, Button, + CloseButton, Flex, IconButton, Modal, @@ -20,7 +21,7 @@ import { EventDetails, Role } from '@utils/types'; import { Buildings, Calendar } from 'iconsax-react'; import { useState } from 'react'; import { FiEdit2, FiMapPin, FiTrash2, FiUser } from 'react-icons/fi'; -import { IoEyeOutline } from 'react-icons/io5'; +import { IoCloseOutline, IoEyeOutline } from 'react-icons/io5'; import AbsenceClaimThanks from './AbsenceClaimThanks'; import AbsenceStatusTag from './AbsenceStatusTag'; import EditableNotes from './EditableNotes'; @@ -28,7 +29,6 @@ import EditAbsenceForm from './EditAbsenceForm'; import LessonPlanView from './LessonPlanView'; interface AbsenceDetailsProps { - isOpen: boolean; onClose: () => void; event: EventDetails; onDelete?: (absenceId: number) => void; @@ -36,8 +36,8 @@ interface AbsenceDetailsProps { fetchAbsences: () => Promise; } +// This component is the content of the popover, not the entire popover/modal const AbsenceDetails: React.FC = ({ - isOpen, onClose, event, onDelete, @@ -209,205 +209,190 @@ const AbsenceDetails: React.FC = ({ }; return ( <> - - - - - + + + + {isAdminMode && ( + } + size="sm" + variant="ghost" + onClick={handleEditClick} /> - - {isAdminMode && ( - - } - size="sm" - variant="ghost" - onClick={handleEditClick} - /> - )} - - {(isAdminMode || - (isUserAbsentTeacher && !event.substituteTeacher)) && ( - - } - size="sm" - variant="ghost" - onClick={handleDeleteClick} - /> - )} - - + )} + + {(isAdminMode || + (isUserAbsentTeacher && !event.substituteTeacher)) && ( + } + size="sm" + variant="ghost" + onClick={handleDeleteClick} + /> + )} + + + + + + + {event.title} + + + + {event.location} + - - - - - {event.title} - - - - {event.location} - - - - - - {absenceDate} - - + + + + {absenceDate} + + + + + + {event.absentTeacherFullName} + + + {event.roomNumber && ( - + - {event.absentTeacherFullName} + Room {event.roomNumber} - {event.roomNumber && ( - - - - Room {event.roomNumber} - - - )} + )} + + + Lesson Plan + + + + {(isAdminMode || isUserAbsentTeacher) && ( - Lesson Plan + Reason of Absence - - - {(isAdminMode || isUserAbsentTeacher) && ( - - - Reason of Absence - - - {event.reasonOfAbsence} - + + {event.reasonOfAbsence} - )} - {isUserAbsentTeacher && !isAdminMode && ( - - )} - {event.notes && (!isUserAbsentTeacher || isAdminMode) && ( - - - Notes - - - - {event.notes} - - - )} + + )} + {isUserAbsentTeacher && !isAdminMode && ( + + )} + {event.notes && (!isUserAbsentTeacher || isAdminMode) && ( + + + Notes + - {/* Visibility Tag*/} - {event.substituteTeacher && - !isAdminMode && - (isUserAbsentTeacher || isUserSubstituteTeacher) && ( - - {isUserAbsentTeacher ? ( - <> - - - {' '} - Only visible to{' '} - - {event.absentTeacher.firstName} - {' '} - and{' '} - - {event.substituteTeacher.firstName} - - . + + {event.notes} + + + )} + + {/* Visibility Tag*/} + {event.substituteTeacher && + !isAdminMode && + (isUserAbsentTeacher || isUserSubstituteTeacher) && ( + + {isUserAbsentTeacher ? ( + <> + + + {' '} + Only visible to{' '} + + {event.absentTeacher.firstName} + {' '} + and{' '} + + {event.substituteTeacher.firstName} - - ) : isUserSubstituteTeacher ? ( - <> - - - {' '} - Only visible to{' '} - - {event.substituteTeacher.firstName} - {' '} - and{' '} - - {event.absentTeacher.firstName} - - . + . + + + ) : isUserSubstituteTeacher ? ( + <> + + + {' '} + Only visible to{' '} + + {event.substituteTeacher.firstName} + {' '} + and{' '} + + {event.absentTeacher.firstName} - - ) : null} - - )} - - {!event.substituteTeacher && - !isUserAbsentTeacher && - !isAdminMode && ( - - )} - - - - + . + + + ) : null} + + )} + + {!event.substituteTeacher && + !isUserAbsentTeacher && + !isAdminMode && ( + + )} + + + { const { events, fetchAbsences } = useAbsences(); const [claimedDays, setClaimedDays] = useState>(new Set()); + const [clickedEventId, setClickedEventId] = useState(null); const calendarRef = useRef(null); const [filteredEvents, setFilteredEvents] = useState([]); @@ -96,6 +103,12 @@ const Calendar: React.FC = () => { onClose: onInputFormClose, } = useDisclosure(); + const [isClosingDetails, setIsClosingDetails] = useState(false); + + const handleDeleteAbsence = async (absenceId: string | number) => { + await fetchAbsences(); + }; + const renderEventContent = useCallback( (eventInfo: EventContentArg) => { const { @@ -106,6 +119,7 @@ const Calendar: React.FC = () => { locationAbbreviation, subjectAbbreviation, lessonPlan, + absenceId, } = eventInfo.event.extendedProps; const eventDate = new Date(eventInfo.event.start!!); @@ -125,7 +139,27 @@ const Calendar: React.FC = () => { : `${absentTeacherDisplayName} -> Unfilled` : undefined; - return ( + const eventDetails = { + title: eventInfo.event.title, + start: eventInfo.event.start!!, + absentTeacher: eventInfo.event.extendedProps.absentTeacher, + absentTeacherFullName: + eventInfo.event.extendedProps.absentTeacherFullName, + substituteTeacher: + eventInfo.event.extendedProps.substituteTeacher || null, + substituteTeacherFullName: + eventInfo.event.extendedProps.substituteTeacherFullName || '', + location: eventInfo.event.extendedProps.location, + locationId: eventInfo.event.extendedProps.locationId, + subjectId: eventInfo.event.extendedProps.subjectId, + lessonPlan: eventInfo.event.extendedProps.lessonPlan || null, + roomNumber: eventInfo.event.extendedProps.roomNumber || '', + reasonOfAbsence: eventInfo.event.extendedProps.reasonOfAbsence, + notes: eventInfo.event.extendedProps.notes || '', + absenceId: eventInfo.event.extendedProps.absenceId, + }; + + const absenceBox = ( { opacity={opacity} /> ); + + const isCurrentEvent = absenceId === clickedEventId; + + // Use Popover component with Portal for proper positioning + return ( + + + { + setSelectedEvent(eventDetails); + setClickedEventId(absenceId); + onAbsenceDetailsOpen(); + }} + display="inline-block" + position="relative" + width="100%" + height="100%" + cursor="pointer" + > + {absenceBox} + + + {isCurrentEvent && ( + + + + + + + + )} + + ); }, - [userData?.id] + [ + userData?.id, + isAdminMode, + isAbsenceDetailsOpen, + onAbsenceDetailsOpen, + clickedEventId, + handleDeleteAbsence, + ] ); useEffect(() => { @@ -183,8 +281,10 @@ const Calendar: React.FC = () => { }, []); const handleDateClick = (arg: { date: Date }) => { - setSelectedDate(arg.date); - onInputFormOpen(); + if (!isClosingDetails) { + setSelectedDate(arg.date); + onInputFormOpen(); + } }; const handleTodayClick = useCallback(() => { @@ -226,26 +326,8 @@ const Calendar: React.FC = () => { }, [updateMonthYearTitle]); const handleAbsenceClick = (clickInfo: EventClickArg) => { - setSelectedEvent({ - title: clickInfo.event.title, - start: clickInfo.event.start!!, - absentTeacher: clickInfo.event.extendedProps.absentTeacher, - absentTeacherFullName: - clickInfo.event.extendedProps.absentTeacherFullName, - substituteTeacher: - clickInfo.event.extendedProps.substituteTeacher || null, - substituteTeacherFullName: - clickInfo.event.extendedProps.substituteTeacherFullName || '', - location: clickInfo.event.extendedProps.location, - locationId: clickInfo.event.extendedProps.locationId, - subjectId: clickInfo.event.extendedProps.subjectId, - lessonPlan: clickInfo.event.extendedProps.lessonPlan || null, - roomNumber: clickInfo.event.extendedProps.roomNumber || '', - reasonOfAbsence: clickInfo.event.extendedProps.reasonOfAbsence, - notes: clickInfo.event.extendedProps.notes || '', - absenceId: clickInfo.event.extendedProps.absenceId, - }); - onAbsenceDetailsOpen(); + // Prevent default behavior so our custom handlers work + clickInfo.jsEvent.preventDefault(); }; const handleDeclareAbsenceClick = () => { @@ -256,6 +338,7 @@ const Calendar: React.FC = () => { onInputFormOpen(); } }; + useEffect(() => { const { activeAbsenceStatusIds, @@ -303,8 +386,11 @@ const Calendar: React.FC = () => { setFilteredEvents(filtered); }, [searchQuery, events, activeTab, userData.id, isAdminMode]); - const handleDeleteAbsence = async () => { - await fetchAbsences(); + const handleCloseDetails = () => { + setClickedEventId(null); + onAbsenceDetailsClose(); + setIsClosingDetails(true); + setTimeout(() => setIsClosingDetails(false), 100); }; if (userData.isLoading) { @@ -452,15 +538,6 @@ const Calendar: React.FC = () => { - - Date: Sun, 13 Apr 2025 11:24:30 -0400 Subject: [PATCH 02/11] Move the filling logic to calendar.tsx. --- .../absences/details/AbsenceDetails.tsx | 126 +------------- src/pages/calendar.tsx | 154 ++++++++++++++++++ 2 files changed, 158 insertions(+), 122 deletions(-) diff --git a/src/components/absences/details/AbsenceDetails.tsx b/src/components/absences/details/AbsenceDetails.tsx index 2fb35630..00a2df45 100644 --- a/src/components/absences/details/AbsenceDetails.tsx +++ b/src/components/absences/details/AbsenceDetails.tsx @@ -22,7 +22,6 @@ import { Buildings, Calendar } from 'iconsax-react'; import { useState } from 'react'; import { FiEdit2, FiMapPin, FiTrash2, FiUser } from 'react-icons/fi'; import { IoEyeOutline } from 'react-icons/io5'; -import AbsenceFillThanks from './AbsenceFillThanks'; import AbsenceStatusTag from './AbsenceStatusTag'; import EditableNotes from './EditableNotes'; import EditAbsenceForm from '../edit/EditAbsenceForm'; @@ -34,6 +33,7 @@ interface AbsenceDetailsProps { onDelete?: (absenceId: number) => void; isAdminMode: boolean; fetchAbsences: () => Promise; + onFillClick: () => void; } // This component is the content of the popover, not the entire popover/modal @@ -43,15 +43,13 @@ const AbsenceDetails: React.FC = ({ onDelete, isAdminMode, fetchAbsences, + onFillClick, }) => { const theme = useTheme(); const userData = useUserData(); const [isDeleting, setIsDeleting] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [isFilling, setIsFilling] = useState(false); - const [isFillDialogOpen, setIsFillDialogOpen] = useState(false); - const [isFillThanksOpen, setIsFillThanksOpen] = useState(false); const toast = useToast(); @@ -87,69 +85,6 @@ const AbsenceDetails: React.FC = ({ const absenceDate = formatDate(event.start!!); - const handleFillThanksDone = () => { - setIsFillThanksOpen(false); - }; - - const handleFillAbsenceClick = () => { - setIsFillDialogOpen(true); - }; - - const handleFillCancel = () => { - setIsFillDialogOpen(false); - }; - - const handleFillConfirm = async () => { - setIsFilling(true); - - try { - const response = await fetch('/api/editAbsence', { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - id: event.absenceId, - lessonDate: event.start, - reasonOfAbsence: event.reasonOfAbsence, - notes: event.notes, - absentTeacherId: event.absentTeacher.id, - substituteTeacherId: userData.id, - locationId: event.locationId, - subjectId: event.subjectId, - roomNumber: event.roomNumber, - }), - }); - - if (!response.ok) { - throw new Error('Failed to fill absence'); - } - - toast({ - title: 'Absence filled', - description: 'You have successfully filled this absence.', - status: 'success', - duration: 5000, - isClosable: true, - }); - - await fetchAbsences(); - setIsFillDialogOpen(false); - setIsFillThanksOpen(true); - } catch (error) { - toast({ - title: 'Error', - description: error.message || 'Failed to fill absence', - status: 'error', - duration: 5000, - isClosable: true, - }); - } finally { - setIsFilling(false); - onClose(); - } - }; - const handleEditClick = () => { onClose(); setIsEditModalOpen(true); @@ -208,6 +143,7 @@ const AbsenceDetails: React.FC = ({ setIsDeleting(false); } }; + return ( <> @@ -386,7 +322,7 @@ const AbsenceDetails: React.FC = ({ height="44px" fontSize="16px" fontWeight="500" - onClick={handleFillAbsenceClick} + onClick={onFillClick} > Fill this Absence @@ -394,60 +330,6 @@ const AbsenceDetails: React.FC = ({ - - - - - Are you sure you want to fill this absence? - - - {"You won't be able to undo."} - - - - - - - - { const { refetchUserData, ...userData } = useUserData(); @@ -45,6 +49,29 @@ const Calendar: React.FC = () => { const searchParams = useSearchParams(); + const getOrdinalNum = (number: number) => { + let selector; + + if (number <= 0) { + selector = 4; + } else if ((number > 3 && number < 21) || number % 10 > 3) { + selector = 0; + } else { + selector = number % 10; + } + + return number + ['th', 'st', 'nd', 'rd', ''][selector]; + }; + + const formatDate = (date: Date) => { + const parsedDate = new Date(date); + const weekday = parsedDate.toLocaleDateString('en-CA', { weekday: 'long' }); + const month = parsedDate.toLocaleDateString('en-CA', { month: 'long' }); + const day = parsedDate.getDate(); + + return `${weekday}, ${month} ${getOrdinalNum(day)}`; + }; + useEffect(() => { if (!userData.isLoading && !userData.isAuthenticated) { router.push('/'); @@ -104,11 +131,80 @@ const Calendar: React.FC = () => { } = useDisclosure(); const [isClosingDetails, setIsClosingDetails] = useState(false); + const [isFilling, setIsFilling] = useState(false); + const [isFillDialogOpen, setIsFillDialogOpen] = useState(false); + const [isFillThanksOpen, setIsFillThanksOpen] = useState(false); + const toast = useToast(); const handleDeleteAbsence = async (absenceId: string | number) => { await fetchAbsences(); }; + const handleFillThanksDone = () => { + setIsFillThanksOpen(false); + }; + + const handleFillAbsenceClick = () => { + setIsFillDialogOpen(true); + }; + + const handleFillCancel = () => { + setIsFillDialogOpen(false); + }; + + const handleFillConfirm = async () => { + if (!selectedEvent) return; + + setIsFilling(true); + + try { + const response = await fetch('/api/editAbsence', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: selectedEvent.absenceId, + lessonDate: selectedEvent.start, + reasonOfAbsence: selectedEvent.reasonOfAbsence, + notes: selectedEvent.notes, + absentTeacherId: selectedEvent.absentTeacher.id, + substituteTeacherId: userData.id, + locationId: selectedEvent.locationId, + subjectId: selectedEvent.subjectId, + roomNumber: selectedEvent.roomNumber, + }), + }); + + if (!response.ok) { + throw new Error('Failed to fill absence'); + } + + toast({ + title: 'Absence filled', + description: 'You have successfully filled this absence.', + status: 'success', + duration: 5000, + isClosable: true, + }); + + await fetchAbsences(); + setIsFillDialogOpen(false); + setIsFillThanksOpen(true); + } catch (error) { + toast({ + title: 'Error', + description: error.message || 'Failed to fill absence', + status: 'error', + duration: 5000, + isClosable: true, + }); + } finally { + setIsFilling(false); + handleCloseDetails(); + } + }; + const renderEventContent = useCallback( (eventInfo: EventContentArg) => { const { @@ -226,6 +322,7 @@ const Calendar: React.FC = () => { onClose={handleCloseDetails} onDelete={handleDeleteAbsence} fetchAbsences={fetchAbsences} + onFillClick={handleFillAbsenceClick} /> @@ -241,6 +338,7 @@ const Calendar: React.FC = () => { onAbsenceDetailsOpen, clickedEventId, handleDeleteAbsence, + handleFillAbsenceClick, ] ); @@ -561,6 +659,62 @@ const Calendar: React.FC = () => { + + + + + Are you sure you want to fill this absence? + + + {"You won't be able to undo."} + + + + + + + + {selectedEvent && ( + + )} ); }; From f3dc4275ff956ff71d07da656a4177506a2fc50f Mon Sep 17 00:00:00 2001 From: Colin Toft Date: Sun, 13 Apr 2025 11:28:56 -0400 Subject: [PATCH 03/11] Move edit modal into calendar.tsx --- .../absences/details/AbsenceDetails.tsx | 41 +++---------------- src/pages/calendar.tsx | 35 ++++++++++++++++ 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/components/absences/details/AbsenceDetails.tsx b/src/components/absences/details/AbsenceDetails.tsx index 00a2df45..6ede2dae 100644 --- a/src/components/absences/details/AbsenceDetails.tsx +++ b/src/components/absences/details/AbsenceDetails.tsx @@ -24,7 +24,6 @@ import { FiEdit2, FiMapPin, FiTrash2, FiUser } from 'react-icons/fi'; import { IoEyeOutline } from 'react-icons/io5'; import AbsenceStatusTag from './AbsenceStatusTag'; import EditableNotes from './EditableNotes'; -import EditAbsenceForm from '../edit/EditAbsenceForm'; import LessonPlanView from './LessonPlanView'; interface AbsenceDetailsProps { @@ -34,6 +33,7 @@ interface AbsenceDetailsProps { isAdminMode: boolean; fetchAbsences: () => Promise; onFillClick: () => void; + onEditClick: () => void; } // This component is the content of the popover, not the entire popover/modal @@ -44,12 +44,12 @@ const AbsenceDetails: React.FC = ({ isAdminMode, fetchAbsences, onFillClick, + onEditClick, }) => { const theme = useTheme(); const userData = useUserData(); const [isDeleting, setIsDeleting] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); const toast = useToast(); @@ -85,11 +85,6 @@ const AbsenceDetails: React.FC = ({ const absenceDate = formatDate(event.start!!); - const handleEditClick = () => { - onClose(); - setIsEditModalOpen(true); - }; - const handleDeleteClick = () => { setIsDeleteDialogOpen(true); }; @@ -165,7 +160,10 @@ const AbsenceDetails: React.FC = ({ icon={} size="sm" variant="ghost" - onClick={handleEditClick} + onClick={() => { + onClose(); + onEditClick(); + }} /> )} @@ -352,33 +350,6 @@ const AbsenceDetails: React.FC = ({ - {isEditModalOpen && ( - setIsEditModalOpen(false)} - isCentered - > - - - - Edit Absence - - - - setIsEditModalOpen(false)} - /> - - - - )} ); }; diff --git a/src/pages/calendar.tsx b/src/pages/calendar.tsx index 196bb4c2..47509d10 100644 --- a/src/pages/calendar.tsx +++ b/src/pages/calendar.tsx @@ -42,6 +42,7 @@ import CalendarSidebar from '../components/calendar/sidebar/CalendarSidebar'; import { CalendarTabs } from '../components/calendar/CalendarTabs'; import DeclareAbsenceForm from '../components/absences/declare/DeclareAbsenceForm'; import AbsenceFillThanks from '../components/absences/details/AbsenceFillThanks'; +import EditAbsenceForm from '../components/absences/edit/EditAbsenceForm'; const Calendar: React.FC = () => { const { refetchUserData, ...userData } = useUserData(); @@ -135,6 +136,7 @@ const Calendar: React.FC = () => { const [isFillDialogOpen, setIsFillDialogOpen] = useState(false); const [isFillThanksOpen, setIsFillThanksOpen] = useState(false); const toast = useToast(); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); const handleDeleteAbsence = async (absenceId: string | number) => { await fetchAbsences(); @@ -205,6 +207,10 @@ const Calendar: React.FC = () => { } }; + const handleEditClick = () => { + setIsEditModalOpen(true); + }; + const renderEventContent = useCallback( (eventInfo: EventContentArg) => { const { @@ -323,6 +329,7 @@ const Calendar: React.FC = () => { onDelete={handleDeleteAbsence} fetchAbsences={fetchAbsences} onFillClick={handleFillAbsenceClick} + onEditClick={handleEditClick} /> @@ -339,6 +346,7 @@ const Calendar: React.FC = () => { clickedEventId, handleDeleteAbsence, handleFillAbsenceClick, + handleEditClick, ] ); @@ -715,6 +723,33 @@ const Calendar: React.FC = () => { absenceDate={formatDate(selectedEvent.start)} /> )} + {selectedEvent && ( + setIsEditModalOpen(false)} + isCentered + > + + + + Edit Absence + + + + setIsEditModalOpen(false)} + /> + + + + )} ); }; From 07ff46df7d7405b677bb32338fc4f2fef72a0d7e Mon Sep 17 00:00:00 2001 From: Colin Toft Date: Wed, 16 Apr 2025 22:08:32 -0400 Subject: [PATCH 04/11] Make delete great again --- .../absences/delete/DeleteAbsenceModal.tsx | 98 +++++++++++++++++++ .../absences/details/AbsenceDetails.tsx | 83 +--------------- src/pages/calendar.tsx | 30 +++++- 3 files changed, 127 insertions(+), 84 deletions(-) create mode 100644 src/components/absences/delete/DeleteAbsenceModal.tsx diff --git a/src/components/absences/delete/DeleteAbsenceModal.tsx b/src/components/absences/delete/DeleteAbsenceModal.tsx new file mode 100644 index 00000000..ce016dd1 --- /dev/null +++ b/src/components/absences/delete/DeleteAbsenceModal.tsx @@ -0,0 +1,98 @@ +import { + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, + useToast, +} from '@chakra-ui/react'; +import { useUserData } from '@hooks/useUserData'; +import { Role } from '@utils/types'; +import { useState } from 'react'; + +interface DeleteAbsenceModalProps { + isOpen: boolean; + onClose: () => void; + absenceId: number; + onDelete: (absenceId: number) => Promise; +} + +const DeleteAbsenceModal: React.FC = ({ + isOpen, + onClose, + absenceId, + onDelete, +}) => { + const [isDeleting, setIsDeleting] = useState(false); + const toast = useToast(); + const userData = useUserData(); + const isUserAdmin = userData.role === Role.ADMIN; + + const handleDeleteConfirm = async () => { + try { + setIsDeleting(true); + const response = await fetch(`/api/deleteAbsence`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + isUserAdmin, + absenceId, + }), + }); + + if (!response.ok) { + throw new Error('Failed to delete absence'); + } + + toast({ + title: 'Absence deleted', + description: 'The absence has been successfully deleted.', + status: 'success', + duration: 5000, + isClosable: true, + }); + + await onDelete(absenceId); + onClose(); + } catch (error) { + toast({ + title: 'Error', + description: error.message || 'Failed to delete absence', + status: 'error', + duration: 5000, + isClosable: true, + }); + } finally { + setIsDeleting(false); + } + }; + + return ( + + + + Delete Absence + + + Are you sure you want to delete this absence? + + + + + + + + ); +}; + +export default DeleteAbsenceModal; diff --git a/src/components/absences/details/AbsenceDetails.tsx b/src/components/absences/details/AbsenceDetails.tsx index 6ede2dae..d6834426 100644 --- a/src/components/absences/details/AbsenceDetails.tsx +++ b/src/components/absences/details/AbsenceDetails.tsx @@ -4,13 +4,6 @@ import { CloseButton, Flex, IconButton, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, Text, VStack, useTheme, @@ -48,8 +41,6 @@ const AbsenceDetails: React.FC = ({ }) => { const theme = useTheme(); const userData = useUserData(); - const [isDeleting, setIsDeleting] = useState(false); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const toast = useToast(); @@ -86,56 +77,8 @@ const AbsenceDetails: React.FC = ({ const absenceDate = formatDate(event.start!!); const handleDeleteClick = () => { - setIsDeleteDialogOpen(true); - }; - - const handleDeleteCancel = () => { - setIsDeleteDialogOpen(false); - }; - - const handleDeleteConfirm = async () => { - try { - setIsDeleting(true); - const response = await fetch(`/api/deleteAbsence`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - isUserAdmin, - absenceId: event.absenceId, - }), - }); - - if (!response.ok) { - throw new Error('Failed to delete absence'); - } - - toast({ - title: 'Absence deleted', - description: 'The absence has been successfully deleted.', - status: 'success', - duration: 5000, - isClosable: true, - }); - - await fetchAbsences(); - setIsDeleteDialogOpen(false); - onClose(); - - if (onDelete) { - onDelete(event.absenceId); - } - } catch (error) { - toast({ - title: 'Error', - description: error.message || 'Failed to delete absence', - status: 'error', - duration: 5000, - isClosable: true, - }); - } finally { - setIsDeleting(false); + if (onDelete) { + onDelete(event.absenceId); } }; @@ -328,28 +271,6 @@ const AbsenceDetails: React.FC = ({ - - - - Delete Absence - - - Are you sure you want to delete this absence? - - - - - - - ); }; diff --git a/src/pages/calendar.tsx b/src/pages/calendar.tsx index 47509d10..b93f5419 100644 --- a/src/pages/calendar.tsx +++ b/src/pages/calendar.tsx @@ -32,7 +32,7 @@ import { useUserData } from '@hooks/useUserData'; import { formatMonthYear } from '@utils/formatMonthYear'; import { getCalendarStyles } from '@utils/getCalendarStyles'; import { getDayCellClassNames } from '@utils/getDayCellClassNames'; -import { EventDetails } from '@utils/types'; +import { EventDetails, Role } from '@utils/types'; import { useRouter, useSearchParams } from 'next/navigation'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import AbsenceBox from '../components/absences/AbsenceBox'; @@ -43,6 +43,7 @@ import { CalendarTabs } from '../components/calendar/CalendarTabs'; import DeclareAbsenceForm from '../components/absences/declare/DeclareAbsenceForm'; import AbsenceFillThanks from '../components/absences/details/AbsenceFillThanks'; import EditAbsenceForm from '../components/absences/edit/EditAbsenceForm'; +import DeleteAbsenceModal from '../components/absences/delete/DeleteAbsenceModal'; const Calendar: React.FC = () => { const { refetchUserData, ...userData } = useUserData(); @@ -138,6 +139,20 @@ const Calendar: React.FC = () => { const toast = useToast(); const [isEditModalOpen, setIsEditModalOpen] = useState(false); + // State for delete modal + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [absenceToDelete, setAbsenceToDelete] = useState(null); + + const handleDeleteClick = (absenceId: number) => { + setAbsenceToDelete(absenceId); + setIsDeleteModalOpen(true); + }; + + const handleDeleteModalClose = () => { + setIsDeleteModalOpen(false); + setAbsenceToDelete(null); + }; + const handleDeleteAbsence = async (absenceId: string | number) => { await fetchAbsences(); }; @@ -326,7 +341,7 @@ const Calendar: React.FC = () => { event={eventDetails} isAdminMode={isAdminMode} onClose={handleCloseDetails} - onDelete={handleDeleteAbsence} + onDelete={handleDeleteClick} fetchAbsences={fetchAbsences} onFillClick={handleFillAbsenceClick} onEditClick={handleEditClick} @@ -344,7 +359,7 @@ const Calendar: React.FC = () => { isAbsenceDetailsOpen, onAbsenceDetailsOpen, clickedEventId, - handleDeleteAbsence, + handleDeleteClick, handleFillAbsenceClick, handleEditClick, ] @@ -750,6 +765,15 @@ const Calendar: React.FC = () => { )} + {/* Delete Absence Modal */} + {absenceToDelete && ( + + )} ); }; From 8870a5c34b63cd563db8431f911357dd932595c3 Mon Sep 17 00:00:00 2001 From: Colin Toft Date: Wed, 16 Apr 2025 22:24:41 -0400 Subject: [PATCH 05/11] Fix calendar scroll stuff --- src/pages/calendar.tsx | 7 ++++--- utils/getCalendarStyles.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pages/calendar.tsx b/src/pages/calendar.tsx index b93f5419..3a9bf031 100644 --- a/src/pages/calendar.tsx +++ b/src/pages/calendar.tsx @@ -608,7 +608,7 @@ const Calendar: React.FC = () => { return ( <> - + { height="100%" display="flex" flexDirection="column" + overflow="hidden" > { setIsAdminMode={setIsAdminMode} /> - + {!isAdminMode && ( )} @@ -642,7 +643,7 @@ const Calendar: React.FC = () => { headerToolbar={false} plugins={[dayGridPlugin, interactionPlugin]} initialView="dayGridMonth" - height="100%" + height="auto" events={filteredEvents} eventContent={renderEventContent} timeZone="local" diff --git a/utils/getCalendarStyles.ts b/utils/getCalendarStyles.ts index 36fcad7c..9f3d9de4 100644 --- a/utils/getCalendarStyles.ts +++ b/utils/getCalendarStyles.ts @@ -48,4 +48,15 @@ export const getCalendarStyles = (theme: CustomTheme) => css` .fc-daygrid-event-harness:first-of-type { margin-top: 0 !important; } + .fc-scroller { + overflow: auto !important; + max-height: 100% !important; + } + .fc-view-harness { + height: auto !important; + overflow: visible !important; + } + .fc .fc-scrollgrid { + border: none !important; + } `; From c4cf53acaaa91f90849181b6b9af3b63594786e8 Mon Sep 17 00:00:00 2001 From: Colin Toft Date: Wed, 16 Apr 2025 22:42:30 -0400 Subject: [PATCH 06/11] Add scroll to absence that is too long --- src/components/absences/details/AbsenceDetails.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/absences/details/AbsenceDetails.tsx b/src/components/absences/details/AbsenceDetails.tsx index d6834426..fde820ce 100644 --- a/src/components/absences/details/AbsenceDetails.tsx +++ b/src/components/absences/details/AbsenceDetails.tsx @@ -12,7 +12,6 @@ import { import { useUserData } from '@hooks/useUserData'; import { EventDetails, Role } from '@utils/types'; import { Buildings, Calendar } from 'iconsax-react'; -import { useState } from 'react'; import { FiEdit2, FiMapPin, FiTrash2, FiUser } from 'react-icons/fi'; import { IoEyeOutline } from 'react-icons/io5'; import AbsenceStatusTag from './AbsenceStatusTag'; @@ -128,7 +127,14 @@ const AbsenceDetails: React.FC = ({ - + {event.title} From 7dc1eec4fd6344990460b3f658a27cf79d395c4b Mon Sep 17 00:00:00 2001 From: Colin Toft Date: Thu, 17 Apr 2025 15:45:39 -0400 Subject: [PATCH 07/11] Fix error --- src/pages/calendar.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/calendar.tsx b/src/pages/calendar.tsx index 426034b4..cb0eec4b 100644 --- a/src/pages/calendar.tsx +++ b/src/pages/calendar.tsx @@ -223,6 +223,13 @@ const Calendar: React.FC = () => { setIsEditModalOpen(true); }; + const handleCloseDetails = () => { + setClickedEventId(null); + onAbsenceDetailsClose(); + setIsClosingDetails(true); + setTimeout(() => setIsClosingDetails(false), 100); + }; + const renderEventContent = useCallback( (eventInfo: EventContentArg) => { const { @@ -506,13 +513,6 @@ const Calendar: React.FC = () => { setFilteredEvents(filtered); }, [searchQuery, events, activeTab, userData.id, isAdminMode]); - const handleCloseDetails = () => { - setClickedEventId(null); - onAbsenceDetailsClose(); - setIsClosingDetails(true); - setTimeout(() => setIsClosingDetails(false), 100); - }; - if (userData.isLoading) { return null; } From 58b272b9a7b9c281d85bdcf3f15bd49c878893c2 Mon Sep 17 00:00:00 2001 From: Colin Toft Date: Thu, 17 Apr 2025 17:43:53 -0400 Subject: [PATCH 08/11] Use isLazy since it seems to help with jumpiness/resizing --- src/pages/calendar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/calendar.tsx b/src/pages/calendar.tsx index 67d6f5e3..e0ad05fe 100644 --- a/src/pages/calendar.tsx +++ b/src/pages/calendar.tsx @@ -330,6 +330,7 @@ const Calendar: React.FC = () => { closeOnBlur={true} closeOnEsc={true} gutter={16} + isLazy > Date: Thu, 17 Apr 2025 17:52:56 -0400 Subject: [PATCH 09/11] fix oops --- utils/getCalendarStyles.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/getCalendarStyles.ts b/utils/getCalendarStyles.ts index 6883c008..609f2610 100644 --- a/utils/getCalendarStyles.ts +++ b/utils/getCalendarStyles.ts @@ -65,5 +65,4 @@ export const getCalendarStyles = (theme: CustomTheme) => css` text-transform: uppercase; font-size: 14px; } - */ `; From 6805033a23a2ad2b73202f83b5ef9889f3b78715 Mon Sep 17 00:00:00 2001 From: Colin Toft Date: Thu, 17 Apr 2025 18:00:50 -0400 Subject: [PATCH 10/11] Better fix for the horizontal scroll thing --- src/components/absences/details/AbsenceDetails.tsx | 1 - src/components/absences/details/LessonPlanView.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/absences/details/AbsenceDetails.tsx b/src/components/absences/details/AbsenceDetails.tsx index 418b843a..b0fcf7ac 100644 --- a/src/components/absences/details/AbsenceDetails.tsx +++ b/src/components/absences/details/AbsenceDetails.tsx @@ -131,7 +131,6 @@ const AbsenceDetails: React.FC = ({ padding: '20px 0 0 0', maxHeight: '70vh', overflowY: 'auto', - overflowX: 'hidden', }} > diff --git a/src/components/absences/details/LessonPlanView.tsx b/src/components/absences/details/LessonPlanView.tsx index 962f0722..86fdd595 100644 --- a/src/components/absences/details/LessonPlanView.tsx +++ b/src/components/absences/details/LessonPlanView.tsx @@ -120,7 +120,7 @@ const NoLessonPlanViewingDisplay = ({ align="center" justify="center" gap="20px" - width="302px" + width="100%" sx={{ padding: '24px', borderRadius: '10px', From 41614aadb018eaf53c311ef4bc7468810119d03b Mon Sep 17 00:00:00 2001 From: Colin Toft Date: Thu, 17 Apr 2025 23:14:32 -0400 Subject: [PATCH 11/11] Clean --- src/components/CustomToast.tsx | 13 ++- .../absences/delete/DeleteAbsenceModal.tsx | 109 ------------------ .../modals/delete/ConfirmDeleteModal.tsx | 98 ---------------- src/pages/calendar.tsx | 4 +- 4 files changed, 9 insertions(+), 215 deletions(-) delete mode 100644 src/components/absences/delete/DeleteAbsenceModal.tsx delete mode 100644 src/components/absences/modals/delete/ConfirmDeleteModal.tsx diff --git a/src/components/CustomToast.tsx b/src/components/CustomToast.tsx index 7eec3b2e..4207f3e2 100644 --- a/src/components/CustomToast.tsx +++ b/src/components/CustomToast.tsx @@ -50,15 +50,16 @@ export const CustomToast: React.FC = ({ ); }; -export interface CustomToastOptions { - description?: string | ReactNode; - status?: ToastStatus; -} - export const useCustomToast = () => { const toast = useToast(); - return ({ description, status = 'success' }: CustomToastOptions) => { + return ({ + description, + status = 'success', + }: { + description?: string | ReactNode; + status?: string; + }) => { const validStatuses = ['success', 'error'] as const; const safeStatus = validStatuses.includes(status as ToastStatus) ? (status as ToastStatus) diff --git a/src/components/absences/delete/DeleteAbsenceModal.tsx b/src/components/absences/delete/DeleteAbsenceModal.tsx deleted file mode 100644 index e2919b27..00000000 --- a/src/components/absences/delete/DeleteAbsenceModal.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { - Button, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - Text, -} from '@chakra-ui/react'; -import { useUserData } from '@hooks/useUserData'; -import { formatFullDate } from '@utils/formatDate'; -import { EventDetails, Role } from '@utils/types'; -import { useState } from 'react'; -import { CustomToastOptions } from '../../CustomToast'; -interface DeleteAbsenceModalProps { - isOpen: boolean; - onClose: () => void; - absenceId: number; - onDelete: (absenceId: number) => Promise; - event: EventDetails; - showToast: (options: CustomToastOptions) => void; -} - -const DeleteAbsenceModal: React.FC = ({ - isOpen, - onClose, - absenceId, - onDelete, - event, - showToast, -}) => { - const [isDeleting, setIsDeleting] = useState(false); - - const userData = useUserData(); - const isUserAdmin = userData.role === Role.ADMIN; - - const handleDeleteConfirm = async () => { - try { - setIsDeleting(true); - const response = await fetch(`/api/deleteAbsence`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - isUserAdmin, - absenceId, - }), - }); - - if (!response.ok) { - throw new Error('Failed to delete absence'); - } - - const formattedDate = formatFullDate(event.start!!); - - showToast({ - status: 'success', - description: ( - - You have successfully deleted{' '} - - {event.absentTeacher.firstName}'s - {' '} - absence on{' '} - - {formattedDate}. - - - ), - }); - - onDelete(absenceId); - onClose(); - } catch (error) { - showToast({ - description: error.message || 'Failed to delete absence', - status: 'error', - }); - } finally { - setIsDeleting(false); - } - }; - - return ( - - - - Delete Absence - - - Are you sure you want to delete this absence? - - - - - - - - ); -}; - -export default DeleteAbsenceModal; diff --git a/src/components/absences/modals/delete/ConfirmDeleteModal.tsx b/src/components/absences/modals/delete/ConfirmDeleteModal.tsx deleted file mode 100644 index 87451daa..00000000 --- a/src/components/absences/modals/delete/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { - Button, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - Text, - useToast, -} from '@chakra-ui/react'; -import { useUserData } from '@hooks/useUserData'; -import { Role } from '@utils/types'; -import { useState } from 'react'; - -interface ConfirmDeleteModalProps { - isOpen: boolean; - onClose: () => void; - absenceId: number; - onDelete: (absenceId: number) => Promise; -} - -const ConfirmDeleteModal: React.FC = ({ - isOpen, - onClose, - absenceId, - onDelete, -}) => { - const [isDeleting, setIsDeleting] = useState(false); - const toast = useToast(); - const userData = useUserData(); - const isUserAdmin = userData.role === Role.ADMIN; - - const handleDeleteConfirm = async () => { - try { - setIsDeleting(true); - const response = await fetch(`/api/deleteAbsence`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - isUserAdmin, - absenceId, - }), - }); - - if (!response.ok) { - throw new Error('Failed to delete absence'); - } - - toast({ - title: 'Absence deleted', - description: 'The absence has been successfully deleted.', - status: 'success', - duration: 5000, - isClosable: true, - }); - - await onDelete(absenceId); - onClose(); - } catch (error) { - toast({ - title: 'Error', - description: error.message || 'Failed to delete absence', - status: 'error', - duration: 5000, - isClosable: true, - }); - } finally { - setIsDeleting(false); - } - }; - - return ( - - - - Delete Absence - - - Are you sure you want to delete this absence? - - - - - - - - ); -}; - -export default ConfirmDeleteModal; diff --git a/src/pages/calendar.tsx b/src/pages/calendar.tsx index 93d891a7..b78103dd 100644 --- a/src/pages/calendar.tsx +++ b/src/pages/calendar.tsx @@ -31,7 +31,7 @@ import DeclareAbsenceModal from '../components/absences/modals/declare/DeclareAb import { CalendarTabs } from '../components/calendar/CalendarTabs'; import AbsenceFillThanksModal from '../components/absences/details/AbsenceFillThanksModal'; import EditAbsenceModal from '../components/absences/modals/edit/EditAbsenceModal'; -import ConfirmDeleteModal from '../components/absences/modals/delete/ConfirmDeleteModal'; +import DeleteAbsenceModal from '../components/absences/details/DeleteAbsenceModal'; import FillAbsenceModal from '../components/absences/details/FillAbsenceModal'; import CalendarSidebar from '../components/calendar/sidebar/CalendarSidebar'; import CalendarHeader from '../components/header/calendar/CalendarHeader'; @@ -714,7 +714,7 @@ const Calendar: React.FC = () => { )} {/* Delete Absence Modal */} {absenceToDelete && ( -