diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e89796c..f66deb5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,8 +35,6 @@ model User { organizationId String? @db.ObjectId organization Organization? @relation(fields: [organizationId], references: [id]) code Code? - eventIds String[] @db.ObjectId - events Event[] @relation(fields: [eventIds], references: [id]) timeSlots TimeSlot[] volunteerSessions VolunteerSession[] } @@ -82,16 +80,6 @@ model Code { userId String @unique @db.ObjectId } -model Event { - id String @id @default(auto()) @map("_id") @db.ObjectId - userIds String[] @db.ObjectId - users User[] @relation(fields: [userIds], references: [id]) - eventName String @default("") - dateTime DateTime @default(now()) - description String @default("") - maxPeople Int @default(0) -} - model Organization { id String @id @default(auto()) @map("_id") @db.ObjectId name String @unique diff --git a/src/app/api/event/route.client.ts b/src/app/api/event/route.client.ts deleted file mode 100644 index 323f119..0000000 --- a/src/app/api/event/route.client.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Event } from "@prisma/client"; - -type CreateEventInput = Pick< - Event, - "eventName" | "description" | "maxPeople" | "dateTime" ->; - -export const fetchApi = async ( - endpoint: string, - method: "POST" | "GET" | "DELETE" | "PATCH", - body?: Record -) => { - const response = await fetch(endpoint, { - method, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - const responseData = await response.json(); - - if (!response.ok) { - throw new Error( - JSON.stringify({ - code: responseData.code, - message: responseData.message, - }) - ); - } - - return responseData; -}; - -export const addEvent = async (event: CreateEventInput) => { - fetchApi("/api/event", "POST", { event }); -}; - -export const getEvent = async (eventID: string) => { - const url = `/api/event?id=${eventID}`; - return fetchApi(url, "GET"); -}; - -export const getAllEvents = async () => { - return fetchApi("/api/event", "GET"); -}; - -export const updateEvent = async (event: Event) => { - return fetchApi("/api/event", "PATCH", { event }); -}; - -export const deleteEvent = async (eventID: string) => { - const url = `/api/user?id=${eventID}`; - return fetchApi(url, "DELETE"); -}; diff --git a/src/app/api/event/route.ts b/src/app/api/event/route.ts deleted file mode 100644 index e91f9cc..0000000 --- a/src/app/api/event/route.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { PrismaClient } from "@prisma/client"; -import { NextRequest, NextResponse } from "next/server"; - -const prisma = new PrismaClient(); - -export const POST = async (request: NextRequest) => { - try { - /* @TODO: Add auth */ - - const { event } = await request.json(); - - const savedEvent = await prisma.event.create({ - data: event, - }); - - return NextResponse.json( - { - code: "SUCCESS", - message: savedEvent.eventName, - }, - { status: 201 } - ); - } catch (error) { - console.error("Error:", error); - return NextResponse.json( - { - code: "ERROR", - message: error, - }, - { status: 500 } - ); - } -}; - -export const PATCH = async (request: NextRequest) => { - try { - const { event } = await request.json(); - - const updatedEvent = await prisma.event.update({ - where: { - id: event.id, - }, - data: { - ...event, - id: undefined, - }, - }); - return NextResponse.json( - { - code: "SUCCESS", - message: updatedEvent.eventName, - }, - { status: 200 } - ); - } catch (error) { - console.error("Error:", error); - return NextResponse.json( - { - code: "ERROR", - message: error, - }, - { status: 500 } - ); - } -}; - -export const DELETE = async (request: NextRequest) => { - const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); - - // Check if id is null - if (!id) { - return NextResponse.json( - { - code: "BAD_REQUEST", - message: "Event ID is required.", - }, - { status: 400 } - ); - } - - try { - const deletedEvent = await prisma.event.delete({ - where: { id }, - }); - - return NextResponse.json( - { - code: "SUCCESS", - message: "Event deleted successfully", - data: deletedEvent, - }, - { status: 200 } - ); - } catch (error) { - console.error("Error:", error); - return NextResponse.json( - { - code: "ERROR", - message: error, - }, - { status: 500 } - ); - } -}; - -export const GET = async (request: NextRequest) => { - const { searchParams } = new URL(request.url); - const eventId = searchParams.get("id"); - - try { - if (eventId) { - const fetchedEvent = await prisma.event.findUnique({ - where: { id: eventId }, - }); - - if (!fetchedEvent) { - return NextResponse.json( - { code: "NOT_FOUND", message: "No event found" }, - { status: 404 } - ); - } - - return NextResponse.json( - { code: "SUCCESS", data: fetchedEvent }, - { status: 200 } - ); - } else { - const fetchedEvents = await prisma.event.findMany(); - return NextResponse.json( - { code: "SUCCESS", data: fetchedEvents }, - { status: 200 } - ); - } - } catch (error) { - console.error("Error fetching events:", error); - return NextResponse.json( - { code: "ERROR", message: "An error occurred while fetching events" }, - { status: 500 } - ); - } -}; diff --git a/src/app/api/organization/route.client.ts b/src/app/api/organization/route.client.ts index 93f1b66..80aa038 100644 --- a/src/app/api/organization/route.client.ts +++ b/src/app/api/organization/route.client.ts @@ -39,3 +39,8 @@ export const getOrganizations = async () => { const url = `/api/organization`; return fetchApi(url, "GET"); }; + +export const deleteOrganization = async (organizationId: string) => { + const url = `/api/organization?id=${organizationId}`; + return fetchApi(url, "DELETE"); +}; diff --git a/src/app/api/organization/route.ts b/src/app/api/organization/route.ts index c612d6d..0dacfbf 100644 --- a/src/app/api/organization/route.ts +++ b/src/app/api/organization/route.ts @@ -107,3 +107,53 @@ export const GET = async (request: NextRequest) => { ); } }; + +export const DELETE = async (request: NextRequest) => { + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + + if (!id) { + return NextResponse.json( + { code: "BAD_REQUEST", message: "Organization ID is required." }, + { status: 400 } + ); + } + + try { + await prisma.$transaction(async (tx) => { + // Nullify organizationId for Users + await tx.user.updateMany({ + where: { organizationId: id }, + data: { organizationId: null }, + }); + + // Nullify organizationId for TimeSlots + await tx.timeSlot.updateMany({ + where: { organizationId: id }, + data: { organizationId: null }, + }); + + // Nullify organizationId for VolunteerSessions + await tx.volunteerSession.updateMany({ + where: { organizationId: id }, + data: { organizationId: null }, + }); + + // Delete the Organization + await tx.organization.delete({ + where: { id }, + }); + }); + + return NextResponse.json( + { code: "SUCCESS", message: "Organization deleted successfully." }, + { status: 200 } + ); + } catch (error) { + console.error("Error deleting organization:", error); + return NextResponse.json( + { code: "ERROR", message: "Failed to delete organization." }, + { status: 500 } + ); + } +}; diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 6cb692a..f0c5a05 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -95,24 +95,79 @@ export const DELETE = async (request: NextRequest) => { ); } + // @TODO: If there is time figure out the logic for removing user while preserving previous time slots/sessions try { - await prisma.code.delete({ - where: { userId: id }, - }); + await prisma.$transaction(async (tx) => { + // Delete TimeSlots where organizationId is null + // await tx.timeSlot.deleteMany({ + // where: { + // userId: id, + // organizationId: null, + // }, + // }); + + // Delete VolunteerSessions where organizationId is null + // await tx.volunteerSession.deleteMany({ + // where: { + // userId: id, + // organizationId: null, + // }, + // }); + + // For TimeSlots with organizationId, nullify userId + // await tx.timeSlot.updateMany({ + // where: { + // userId: id, + // NOT: { + // organizationId: null, + // }, + // }, + // data: { + // userId: undefined, // nullify userId + // }, + // }); + + // For VolunteerSessions with organizationId, nullify userId + // await tx.volunteerSession.updateMany({ + // where: { + // userId: id, + // NOT: { + // organizationId: null, + // }, + // }, + // data: { + // userId: undefined, // nullify userId + // }, + // }); + + await tx.timeSlot.deleteMany({ + where: { userId: id }, + }); - await prisma.volunteerDetails.delete({ - where: { userId: id }, - }); + await tx.volunteerSession.deleteMany({ + where: { userId: id }, + }); - const deletedUser = await prisma.user.delete({ - where: { id }, + // Delete related Code + await tx.code.deleteMany({ + where: { userId: id }, + }); + + // Delete related VolunteerDetails + await tx.volunteerDetails.deleteMany({ + where: { userId: id }, + }); + + // Delete the User + await tx.user.delete({ + where: { id }, + }); }); return NextResponse.json( { code: "SUCCESS", message: "User deleted successfully", - data: deletedUser, }, { status: 200 } ); diff --git a/src/app/private/events/page.tsx b/src/app/private/events/page.tsx index 3a2102e..d4ba801 100644 --- a/src/app/private/events/page.tsx +++ b/src/app/private/events/page.tsx @@ -18,14 +18,16 @@ import { getUsersByDate } from "@api/user/route.client"; import { useSearchParams } from "next/navigation"; import { getStandardDate } from "../../utils"; import { getCustomDay } from "@api/customDay/route.client"; +import useApiThrottle from "../../../hooks/useApiThrottle"; export default function EventsPage() { const { data: session } = useSession(); const searchParams = useSearchParams(); const date = searchParams.get("date"); + const [pageLoading, setPageLoading] = React.useState(true); const [selectedDate, setSelectedDate] = React.useState( - getStandardDate(date ?? "") + undefined ); const [timeSlots, setTimeSlots] = React.useState([ { start: "", end: "", submitted: false }, @@ -109,6 +111,15 @@ export default function EventsPage() { const hasSubmittedSlot = timeSlots.some((slot) => slot.submitted); + const isPastOrToday = (date?: Date) => { + if (!date) return false; + const today = new Date(); + today.setHours(0, 0, 0, 0); + const compareDate = new Date(date); + compareDate.setHours(0, 0, 0, 0); + return compareDate <= today; + }; + const isSameSlot = ( a: { start: string; end: string }, b: { start: string; end: string } @@ -177,98 +188,122 @@ export default function EventsPage() { } }; - React.useEffect(() => { - const fetchTimeSlots = async () => { - if (!session?.user.id || !selectedDate) return; + const { fetching: confirmLoading, fn: throttledHandleConfirm } = + useApiThrottle({ + fn: handleConfirm, + }); - try { - if (session.user.role === Role.ADMIN) { - const result = await getUsersByDate(selectedDate); - const users = result.data; - - const individuals = users - .map((user: User & { timeSlots: TimeSlot[] }) => ({ - ...user, - timeSlots: user.timeSlots.filter((slot) => !slot.organizationId), - })) - .filter( - (user: User & { timeSlots: TimeSlot[] }) => - user.timeSlots.length > 0 - ); - - const groups = users - .map((user: User & { timeSlots: TimeSlot[] }) => ({ - ...user, - timeSlots: user.timeSlots.filter((slot) => slot.organizationId), - })) - .filter( - (user: User & { timeSlots: TimeSlot[] }) => - user.timeSlots.length > 0 - ); - - setIndividuals(individuals); - setGroups(groups); - } else { - const result = await getTimeSlotsByDate( - session.user.id, - selectedDate - ); - const slots = result.data; - - const formatted: { - start: string; - end: string; - submitted: boolean; - }[] = slots.map((slot: TimeSlot) => ({ - start: new Date(slot.startTime).toTimeString().slice(0, 5), - end: new Date(slot.endTime).toTimeString().slice(0, 5), - submitted: true, - })); - - formatted.sort((a, b) => a.start.localeCompare(b.start)); - - setTimeSlots([ - ...formatted, - { start: "", end: "", submitted: false }, - ]); - } - } catch (error) { - console.error("Failed to lead slots", error); - } - }; + React.useEffect(() => { + if (date !== null) { + setSelectedDate(getStandardDate(date)); + } else { + const today = new Date(); + today.setHours(0, 0, 0, 0); + setSelectedDate(today); + } + }, [date]); - const fetchCustomDay = async () => { - if (!selectedDate) return; + React.useEffect(() => { + const fetchAllData = async () => { + if (!session?.user.id || !selectedDate) return; try { - const result = await getCustomDay(selectedDate); - const customDay = result.data; - - if (customDay) { - const start = new Date(customDay.startTime) - .toTimeString() - .slice(0, 5); - const end = new Date(customDay.endTime).toTimeString().slice(0, 5); - setCustomDayHours({ start, end }); - setCustomDayTitle(customDay.title ?? ""); - setCustomDayDescription(customDay.description ?? ""); - } else { - setCustomDayHours({ start: "10:00", end: "18:00" }); - setCustomDayTitle(""); - setCustomDayDescription(""); - } + await Promise.all([ + (async () => { + if (session.user.role === Role.ADMIN) { + const result = await getUsersByDate(selectedDate); + const users = result.data; + + const individuals = users + .map((user: User & { timeSlots: TimeSlot[] }) => ({ + ...user, + timeSlots: user.timeSlots.filter( + (slot) => !slot.organizationId + ), + })) + .filter( + (user: User & { timeSlots: TimeSlot[] }) => + user.timeSlots.length > 0 + ); + + const groups = users + .map((user: User & { timeSlots: TimeSlot[] }) => ({ + ...user, + timeSlots: user.timeSlots.filter( + (slot) => slot.organizationId + ), + })) + .filter( + (user: User & { timeSlots: TimeSlot[] }) => + user.timeSlots.length > 0 + ); + + setIndividuals(individuals); + setGroups(groups); + } else { + const result = await getTimeSlotsByDate( + session.user.id, + selectedDate + ); + const slots = result.data; + + const formatted: { + start: string; + end: string; + submitted: boolean; + }[] = slots.map((slot: TimeSlot) => ({ + start: new Date(slot.startTime).toTimeString().slice(0, 5), + end: new Date(slot.endTime).toTimeString().slice(0, 5), + submitted: true, + })); + + formatted.sort((a, b) => a.start.localeCompare(b.start)); + + setTimeSlots([ + ...formatted, + { start: "", end: "", submitted: false }, + ]); + } + })(), + (async () => { + const result = await getCustomDay(selectedDate); + const customDay = result.data; + + if (customDay) { + const start = new Date(customDay.startTime) + .toTimeString() + .slice(0, 5); + const end = new Date(customDay.endTime) + .toTimeString() + .slice(0, 5); + setCustomDayHours({ start, end }); + setCustomDayTitle(customDay.title ?? ""); + setCustomDayDescription(customDay.description ?? ""); + } else { + setCustomDayHours({ start: "10:00", end: "18:00" }); + setCustomDayTitle(""); + setCustomDayDescription(""); + } + })(), + ]); } catch (error) { - console.error("Failed to fetch custom day:", error); - setCustomDayHours({ start: "10:00", end: "18:00" }); - setCustomDayTitle(""); - setCustomDayDescription(""); + console.error("Error fetching event page data:", error); + } finally { + setPageLoading(false); } }; - fetchTimeSlots(); - fetchCustomDay(); + fetchAllData(); }, [session?.user.id, selectedDate, session?.user.role]); + if (pageLoading || !session) { + return ( +
+ Loading... +
+ ); + } + return (
@@ -280,7 +315,8 @@ export default function EventsPage() { setPage(0)} />
@@ -299,9 +335,11 @@ export default function EventsPage() {
{customDayTitle === "" - ? `Sign up for your volunteering time! We are open from${" "} + ? !isPastOrToday(selectedDate) + ? `Sign up for your volunteering time! We are open from${" "} ${formatTime(customDayHours.start)} -${" "} ${formatTime(customDayHours.end)}.` + : "Your Volunteer Hours" : customDayTitle}
{customDayDescription !== "" && ( @@ -319,155 +357,153 @@ export default function EventsPage() { />
{formattedDate - ? `Choose Your Time (${formattedDate})` + ? !isPastOrToday(selectedDate) + ? `Choose Your Time (${formattedDate})` + : formattedDate : "Choose Your Time"}
-
+
- {timeSlots.map((slot, index) => ( -
- {slot.submitted ? ( -
-
- -
- {formatTime(slot.start)} -{" "} - {formatTime(slot.end)} + {isPastOrToday(selectedDate) && + timeSlots.length === 1 ? ( +
No time slots!
+ ) : ( + timeSlots.map((slot, index) => ( +
+ {slot.submitted ? ( +
+
+ +
+ {formatTime(slot.start)} -{" "} + {formatTime(slot.end)} +
+ {!isPastOrToday(selectedDate) ? ( + { + const newSlots = [...timeSlots]; + newSlots.splice(index, 1); + setTimeSlots(newSlots); + }} + /> + ) : null}
- { - const newSlots = [...timeSlots]; - newSlots.splice(index, 1); - setTimeSlots(newSlots); - }} - /> -
- ) : ( -
-
- { - const newSlots = [...timeSlots]; - newSlots[index].start = e.target.value; - setTimeSlots(newSlots); - }} - error={Boolean( - (slot.start && - slot.end && - slot.start > slot.end) || + ) : !isPastOrToday(selectedDate) ? ( +
+
+ { + const newSlots = [...timeSlots]; + newSlots[index].start = + e.target.value; + setTimeSlots(newSlots); + }} + error={Boolean( (slot.start && slot.end && - doesOverlap( - slot.start, - slot.end, - index - )) || - isOutOfBounds(slot.start) - )} - slotProps={{ - inputLabel: { shrink: true }, - htmlInput: { - max: slot.end || "23:59", - }, - }} - /> - { - const newSlots = [...timeSlots]; - newSlots[index].end = e.target.value; - setTimeSlots(newSlots); - }} - error={Boolean( - (slot.start && - slot.end && - slot.end < slot.start) || + slot.start > slot.end) || + (slot.start && + slot.end && + doesOverlap( + slot.start, + slot.end, + index + )) || + isOutOfBounds(slot.start) + )} + slotProps={{ + inputLabel: { shrink: true }, + htmlInput: { + max: slot.end || "23:59", + }, + }} + /> + { + const newSlots = [...timeSlots]; + newSlots[index].end = e.target.value; + setTimeSlots(newSlots); + }} + error={Boolean( (slot.start && slot.end && - doesOverlap( - slot.start, - slot.end, - index - )) || - isOutOfBounds(slot.end) - )} - slotProps={{ - inputLabel: { shrink: true }, - htmlInput: { - min: slot.start || "00:00", - }, + slot.end < slot.start) || + (slot.start && + slot.end && + doesOverlap( + slot.start, + slot.end, + index + )) || + isOutOfBounds(slot.end) + )} + slotProps={{ + inputLabel: { shrink: true }, + htmlInput: { + min: slot.start || "00:00", + }, + }} + /> +
+
- -
- )} -
- ))} + ) : null} +
+ )) + )}
-
-
- -
Any comments?
-
-