diff --git a/backend/src/controllers/EventController.ts b/backend/src/controllers/EventController.ts index 49191c49..6634795d 100644 --- a/backend/src/controllers/EventController.ts +++ b/backend/src/controllers/EventController.ts @@ -97,18 +97,50 @@ export default class EventController { } } + // TODO: Once delete deadline is implemented separately in the frontend, remove this nested logic /** * Deletes a specified event. * @param req.params.id ID of the event. */ public static async deleteEvent(req: Request, res: Response) { try { - const event = await prisma.user_events.delete({ + const event = await prisma.user_events.deleteMany({ where: { id: req.params.id }, // Ensure id is uuid }); - res.json(event); - } catch { - res.status(404).json({ error: 'Not found' }); + if (event.count === 0) { + const deadline = await prisma.user_deadlines.deleteMany({ + where: { id: req.params.id }, + }); + if (deadline.count === 0) { + res.status(404).json({ error: 'Did not find event or deadline with such id' }); + } else { + res.json({ message: 'Deleted deadline', count: deadline.count }); + } + } else { + res.json({ message: 'Deleted event', count: event.count }); + } + } catch (error:any) { + console.error(error); + res.status(404).json({ error: error }); + } + } + + /** + * Deletes all generated events for a specific user. + * @param req.params.user_id ID of the user. + */ + public static async deleteGeneratedEvent(req: Request, res: Response) { + try { + const user_id = req.params.user_id; + const generatedEvents = await prisma.user_events.deleteMany({ + where: { + user_id: user_id, + is_generated: true, + }, + }); + res.json(generatedEvents); + } catch (error:any) { + res.status(404).json({ error: error }); } } diff --git a/backend/src/controllers/ScheduleController.ts b/backend/src/controllers/ScheduleController.ts index 37c719ff..2122792e 100644 --- a/backend/src/controllers/ScheduleController.ts +++ b/backend/src/controllers/ScheduleController.ts @@ -84,7 +84,7 @@ export default class ScheduleController { data: eventDataWithoutId, }); } - + res.json(scheduledEvents); } catch (error : any) { res .status(500) diff --git a/backend/src/routes/calendar.ts b/backend/src/routes/calendar.ts index 674e1c7f..c2b30ce3 100644 --- a/backend/src/routes/calendar.ts +++ b/backend/src/routes/calendar.ts @@ -20,6 +20,9 @@ router.put('/events/id/:id', EventController.putEvent); // Delete an event router.delete('/events/id/:id', EventController.deleteEvent); +// Delete all generated event +router.delete('/generated-events/:user_id', EventController.deleteGeneratedEvent); + // Get all deadlines for a particular user router.get('/deadlines/by-user/:user_id', DeadlineController.getUserDeadlines); diff --git a/backend/src/utils/Scheduler.ts b/backend/src/utils/Scheduler.ts index e3e008bd..33884ffe 100644 --- a/backend/src/utils/Scheduler.ts +++ b/backend/src/utils/Scheduler.ts @@ -121,7 +121,8 @@ export default class Scheduler { // Move search forward // TODO: end_time might be null - currentSearchedTime = new Date(newEvent.end_time!.getTime() + 60000); // +1 min buffer + // TODO: not hard coded 10 min buff but use user's break preference and use is_break in event model + currentSearchedTime = new Date(newEvent.end_time!.getTime() + 10 * 60000); // +10 min buffer } // Update unscheduledMinutes in remainder remainder.unscheduledMinutes = minutesLeft; diff --git a/frontend/src/app/dashboard/calendar/page.tsx b/frontend/src/app/dashboard/calendar/page.tsx index e18b065c..c6c25b0b 100644 --- a/frontend/src/app/dashboard/calendar/page.tsx +++ b/frontend/src/app/dashboard/calendar/page.tsx @@ -1,17 +1,6 @@ /** * followed a YouTube tutorial on how to make calendar view with TypeScript * link: https://youtu.be/VrC5XhjW6W0?si=_ibhdo7doCMXNtB3 - * - * - */ -// Monthly default view -/** - * TO DO: fix draggable event color in dark mode (done) - * make a help button to link to help page - * make events editable in drag event area - * add documentation and comments to each function - * sync event to backend to test out syntax - * send rest of attributes to backend everytime event in changed */ 'use client'; import FullCalendar from '@fullcalendar/react'; @@ -21,17 +10,24 @@ import interactionPlugin, { DropArg, } from '@fullcalendar/interaction'; import timeGridPlugin from '@fullcalendar/timegrid'; -import { Fragment, useEffect, useState } from 'react'; +import { Fragment, useEffect, useMemo, useState } from 'react'; import { Dialog, Transition } from '@headlessui/react'; -import { CheckIcon, ExclamationTriangleIcon } from '@heroicons/react/20/solid'; +import { CheckIcon } from '@heroicons/react/20/solid'; import { EventSourceInput } from '@fullcalendar/core/index.js'; import { useAuth } from '@/components/context/auth/AuthContext'; import axios from 'axios'; import { backendBaseUrl } from '@/lib/utils'; -import { UserEvent } from '@/lib/types'; +import { UserDeadline, UserEvent } from '@/lib/types'; export default function Home() { - //imported from the backend user preferences + const colorTypes = useMemo( + () => ({ + deadline: '#e11d48', + eventGenerated: '#a1cc76', + eventByUser: '#6e9adb', + }), + [] // empty dependency array means this object will remain stable + ); const { user } = useAuth(); const [events] = useState([ { title: 'event 1', id: '1' }, @@ -43,7 +39,8 @@ export default function Home() { const [allEvents, setAllEvents] = useState([]); const [showModal, setShowModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [idToDelete, setIdToDelete] = useState(null); + const [idToDelete, setIdToDelete] = useState(null); + const example: UserEvent = { title: '', end_time: null, @@ -60,42 +57,77 @@ export default function Home() { recurrence_pattern: null, recurrence_start_date: null, }; + const [selectedEvent, setSelectedEvent] = useState(example); + const [newEvent, setNewEvent] = useState({ ...example, }); + const [isDeadline, setIsDeadline] = useState(false); + // Fetching data from backend useEffect(() => { - async function fetchEvents() { + async function fetchSchedule() { try { if (!user) { console.log('No user found'); return; } - const response = await axios.get( + // fetch deadlines + const responseDeadlines = await axios.get( + `${backendBaseUrl}/api/calendar/deadlines/by-user/${user.uid}` + ); + console.log('Fetched raw deadlines:', responseDeadlines.data); + + // fetch events + const responseEvents = await axios.get( `${backendBaseUrl}/api/calendar/events/by-user/${user.uid}` ); - console.log('Fetched raw events:', response.data); + console.log('Fetched raw events:', responseEvents.data); + + // map deadlines + const extractedDeadlines = responseDeadlines.data.map( + (deadline: UserDeadline) => ({ + id: deadline.id, + title: deadline.title, + start: deadline.due_time ? new Date(deadline.due_time) : undefined, + description: deadline.description, + user_id: deadline.user_id, + // Optional: You could add more fields here if FullCalendar needs + color: colorTypes.deadline, + }) + ); - const extractedEvents = response.data.map((event: UserEvent) => ({ + // map events + const extractedEvents = responseEvents.data.map((event: UserEvent) => ({ id: event.id, title: event.title, start: event.start_time ? new Date(event.start_time) : undefined, end: event.end_time ? new Date(event.end_time) : undefined, + description: event.description, + user_id: event.user_id, // Optional: You could add more fields here if FullCalendar needs + + // color for generated events are different than user input events + color: event.is_generated + ? colorTypes.eventGenerated + : colorTypes.eventByUser, })); + console.log('Mapped deadlines for calendar:', extractedDeadlines); console.log('Mapped events for calendar:', extractedEvents); - setAllEvents(extractedEvents); + + // setting frontend + setAllEvents([...extractedEvents, ...extractedDeadlines]); } catch (error) { console.error('Error fetching events:', error); } } - fetchEvents().then(() => { - console.log('Fetched events for user: ', user?.displayName); + fetchSchedule().then(() => { + console.log('Fetched deadlines and events for user: ', user?.displayName); }); - }, [user]); + }, [user, colorTypes]); useEffect(() => { const draggableEl = document.getElementById('draggable-el'); @@ -153,7 +185,7 @@ export default function Home() { return; } - const event: UserEvent & { start: Date; end?: Date } = { + const event: UserEvent & { start: Date; end?: Date; color?: string } = { ...newEvent, user_id: user.uid, start_time: data.date.toISOString(), @@ -170,6 +202,7 @@ export default function Home() { recurrence_end_date: newEvent.recurrence_end_date, recurrence_pattern: newEvent.recurrence_pattern, recurrence_start_date: newEvent.recurrence_start_date, + color: colorTypes.eventByUser, }; setAllEvents([...allEvents, event]); @@ -184,19 +217,51 @@ export default function Home() { console.log(error); }); } - //TO DO: - function handleDeleteModal(data: { event: { id: string } }) { + + function handleOpenModal(data: { event: { id: string } }) { setShowDeleteModal(true); - setIdToDelete(Number(data.event.id)); + setIdToDelete(data.event.id); + console.log(data.event.id); + console.log(allEvents); + const selected = allEvents.find((event) => event.id === data.event.id); + if (selected) { + setSelectedEvent(selected); + } } - function handleDelete() { - setAllEvents( - allEvents.filter((event) => Number(event.id) !== Number(idToDelete)) - ); - setShowDeleteModal(false); - setIdToDelete(null); + async function handleDelete() { + console.log('handleDelete called for event: ', idToDelete); + + if (!user) { + console.log('no user found'); + return; + } + + if (!idToDelete) { + console.log('no idToDelete found'); + return; + } + + //added to test getting event name to the backend + axios + .delete(backendBaseUrl + `/api/calendar/events/id/${idToDelete}`) + .then((response) => { + console.log('Successfully delete event: ', idToDelete); + console.log(response); + // UI local state + setAllEvents(allEvents.filter((event) => event.id !== idToDelete)); + }) + .catch((error) => { + console.log(error); + window.alert('Failed to delete event, try again another time :('); + }) + .finally(() => { + setShowDeleteModal(false); + setIdToDelete(null); + setSelectedEvent(example); + }); } + function handleCloseModal() { setShowModal(false); setNewEvent({ @@ -204,30 +269,32 @@ export default function Home() { }); setShowDeleteModal(false); setIdToDelete(null); + setSelectedEvent(example); } - const handleChange = (e: React.ChangeEvent): void => { - console.log('handleChange called'); - setNewEvent({ - ...newEvent, - title: e.target.value, - }); - }; - function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!user) { console.log('no user found'); return; } - const eventWithUser: UserEvent & { start: Date; end?: Date } = { + + if (isDeadline) { + addDeadlineFromEvent(); + return; + } + + const eventWithUser: UserEvent & { + start: Date; + end?: Date; + color?: string; + } = { ...newEvent, user_id: user.uid, start: new Date(newEvent.start_time), // <-- ensure start is there end: newEvent.end_time ? new Date(newEvent.end_time) : undefined, title: newEvent.title, - - id: newEvent.id, // UUIDs are strings + id: newEvent.id, // this is FullCalendar id break_time: newEvent.break_time, start_time: newEvent.start_time, created_at: new Date().toISOString(), @@ -238,13 +305,18 @@ export default function Home() { recurrence_end_date: newEvent.recurrence_end_date, recurrence_pattern: newEvent.recurrence_pattern, recurrence_start_date: newEvent.recurrence_start_date, + color: colorTypes.eventByUser, }; - setAllEvents([...allEvents, eventWithUser]); axios .post(backendBaseUrl + `/api/calendar/events`, eventWithUser) .then((response) => { console.log('Successfully saved event:', response.data); + // We want to be consistent and use our backend id + const correctId = response.data.id; + const { id, ...restEvent } = eventWithUser; + console.log(`Replace ${id} with ${correctId}`); + setAllEvents([...allEvents, { id: correctId, ...restEvent }]); }) .catch((error) => { console.error('Error saving event:', error); @@ -256,13 +328,102 @@ export default function Home() { }); } + // TODO: handle dragging data + function addDeadlineFromEvent() { + console.log('addDeadlineFromEvent called'); + + if (!user) { + console.log('no user found'); + return; + } + + // TODO: very ugly but works - show deadline after creating one + const deadline: UserDeadline & UserEvent & { start: Date; color?: string } = + { + ...example, + id: newEvent.id, + user_id: user.uid, + title: newEvent.title, + due_time: newEvent.end_time ? newEvent.end_time : newEvent.start_time, + description: newEvent.description, + priority: null, + projected_duration: parseInt(newEvent.location_place || '60'), + created_at: newEvent.created_at, + start: new Date(newEvent.start_time), + color: colorTypes.deadline, + }; + + //added to test getting deadline name to the backend + axios + .post(backendBaseUrl + `/api/calendar/deadlines`, deadline) + .then((response) => { + console.log( + 'Successfully saved deadline into backend for user id: ', + user!.uid + ); + console.log(response); + // We want to be consistent and use our backend id + const correctId = response.data.id; + const { id, ...restEvent } = deadline; + console.log(`Replace ${id} with ${correctId}`); + setAllEvents([...allEvents, { id: correctId, ...restEvent }]); + }) + .catch((error) => { + console.log(error); + }); + + setShowModal(false); + setNewEvent({ + ...example, + }); + setIsDeadline(false); + } + + function deletePerfectSchedule() { + console.log('deletePerfectSchedule called'); + if (!user) { + console.log('no user found'); + return; + } + axios + .delete(backendBaseUrl + `/api/calendar/generated-events/${user.uid}`) + .then((response) => { + console.log('Successfully deleted schedule for user: ', user!.uid); + console.log(response); + if (response.data.count !== 0) { + window.location.reload(); + } else { + window.alert('You do not have any generated events to remove!'); + } + }) + .catch((error) => { + console.log(error); + }); + } + function generatePerfectSchedule() { console.log('Generate perfect schedule clicked'); - // Add your logic to generate the perfect schedule here - // For example, you can call an API endpoint or perform some calculations - // and then update the state accordingly. - } + if (!user) { + console.log('no user found'); + return; + } + axios + .post(backendBaseUrl + `/api/user/generate-schedule/${user.uid}`) + .then((response) => { + console.log('Successfully generated schedule for user: ', user!.uid); + console.log(response); + if (response.data.length !== 0) { + window.location.reload(); + } else { + window.alert('Add some deadlines first!'); + } + }) + .catch((error) => { + console.log(error); + }) + .finally(() => console.log('End posting generate-schedule')); + } return ( <> @@ -289,14 +450,16 @@ export default function Home() { selectMirror={true} dateClick={handleDateClick} drop={(data) => addEvent(data)} - eventClick={(data) => handleDeleteModal(data)} + eventClick={(data) => handleOpenModal(data)} />
-

Frequent Events

+

+ Frequent Events +

{events.map((event) => (
+
+
+ + {selectedEvent?.title + ? selectedEvent?.title + : 'EVENT/DEADLINE DETAILS'} + + + {selectedEvent?.description && ( +

+ Description:{' '} + {selectedEvent.description} +

+ )} + {selectedEvent?.location_place && ( +

+ Location:{' '} + {selectedEvent.location_place} +

+ )} + +

+ Would you like to delete this event? The action is + permanent. +

+
+
+ +
+ + +
+
+ + {/*
@@ -354,6 +574,11 @@ export default function Home() { aria-hidden="true" />
+ {selectedEvent?.description && ( +

+ Description: {selectedEvent.description} +

+ )}
-
+ */}
@@ -431,10 +656,30 @@ export default function Home() { as="h3" className="text-base font-semibold leading-6 text-gray-900" > - Add Event + Add Event or Deadline
+ + handleChange(e)} + onChange={(e) => + setNewEvent({ + ...newEvent, + title: e.target.value, + }) + } placeholder=" Title" /> @@ -481,7 +731,11 @@ export default function Home() { location_place: e.target.value, }) } - placeholder=" Location" + placeholder={ + isDeadline + ? ' Projected Duration (minutes)' + : ' Location' + } /> - - setNewEvent({ - ...newEvent, - end_time: new Date( - e.target.value - ).toISOString(), - }) - } - placeholder=" End Time" // Does not work - /> - - setNewEvent({ - ...newEvent, - is_recurring: e.target.checked, - }) - } - placeholder=" Recurring" - /> - + {!isDeadline && ( + <> + + setNewEvent({ + ...newEvent, + end_time: new Date( + e.target.value + ).toISOString(), + }) + } + placeholder=" End Time" // Does not work + /> + + setNewEvent({ + ...newEvent, + is_recurring: e.target.checked, + }) + } + placeholder=" Recurring" + /> + + + )} {newEvent.is_recurring && ( )} - {newEvent.is_recurring && ( + {newEvent.is_recurring && !isDeadline && ( )} - {newEvent.is_recurring && ( + {newEvent.is_recurring && !isDeadline && ( <>