From c95619244fd7a19659e55cbeb8ada3954f68c5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 22 Sep 2024 20:10:20 +0200 Subject: [PATCH 1/4] refactor: rename DatePickerInput to DateRangeInput --- .../form/{DatePickerInput.tsx => DateRangeInput.tsx} | 4 ++-- pages/admin/new-period.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename components/form/{DatePickerInput.tsx => DateRangeInput.tsx} (95%) diff --git a/components/form/DatePickerInput.tsx b/components/form/DateRangeInput.tsx similarity index 95% rename from components/form/DatePickerInput.tsx rename to components/form/DateRangeInput.tsx index 6ef4474c..e3182dbf 100644 --- a/components/form/DatePickerInput.tsx +++ b/components/form/DateRangeInput.tsx @@ -4,7 +4,7 @@ interface Props { updateDates: (dates: { start: string; end: string }) => void; } -const DatePickerInput = (props: Props) => { +const DateRangeInput = (props: Props) => { const [fromDate, setFromDate] = useState(""); const [toDate, setToDate] = useState(""); @@ -42,4 +42,4 @@ const DatePickerInput = (props: Props) => { ); }; -export default DatePickerInput; +export default DateRangeInput; diff --git a/pages/admin/new-period.tsx b/pages/admin/new-period.tsx index ee9035d1..90a4f935 100644 --- a/pages/admin/new-period.tsx +++ b/pages/admin/new-period.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/router"; import Button from "../../components/Button"; import ApplicationForm from "../../components/form/ApplicationForm"; import CheckboxInput from "../../components/form/CheckboxInput"; -import DatePickerInput from "../../components/form/DatePickerInput"; +import DateRangeInput from "../../components/form/DateRangeInput"; import TextAreaInput from "../../components/form/TextAreaInput"; import TextInput from "../../components/form/TextInput"; import { DeepPartial, periodType } from "../../lib/types/types"; @@ -154,11 +154,11 @@ const NewPeriod = () => { /> - - @@ -214,7 +214,7 @@ const NewPeriod = () => {
{}} + setApplicationData={() => { }} availableCommittees={ (periodData.committees?.filter(Boolean) as string[]) || [] } From fcb7d4cbacac92759ed5113e31c3632d4665a720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 22 Sep 2024 20:21:04 +0200 Subject: [PATCH 2/4] feat: la til frontend for innsetting av rom for admin --- components/RoomOverview.tsx | 137 +++++++++++++++++++++++++++++ components/Table.tsx | 7 +- components/form/DateInput.tsx | 29 ++++++ components/form/TextInput.tsx | 3 +- components/form/TimeRangeInput.tsx | 47 ++++++++++ pages/admin/[period-id]/index.tsx | 46 ++++++---- 6 files changed, 246 insertions(+), 23 deletions(-) create mode 100644 components/RoomOverview.tsx create mode 100644 components/form/DateInput.tsx create mode 100644 components/form/TimeRangeInput.tsx diff --git a/components/RoomOverview.tsx b/components/RoomOverview.tsx new file mode 100644 index 00000000..f43f4cc9 --- /dev/null +++ b/components/RoomOverview.tsx @@ -0,0 +1,137 @@ +import { useState } from "react"; +import { periodType, committeeInterviewType } from "../lib/types/types"; +import Button from "./Button"; +import Table, { RowType } from "./Table" +import { ColumnType } from "./Table"; +import TextInput from "./form/TextInput"; +import DateInput from "./form/DateInput"; +import TimeRangeInput from "./form/TimeRangeInput"; + +import toast from "react-hot-toast"; + +interface Interview { + id: string; + title: string; + start: string; + end: string; +} + +interface RoomBooking { + room: String, + startDate: String + endDate: String +} + +interface Props { + period: periodType | null; +} + +const RoomInterview = ({ + period +}: Props) => { + + // TODO: Fix correct tabbing + + const [roomBookings, setRoomBookings] = useState([]) + + const [date, setDate] = useState(""); + const [startTime, setStartTime] = useState(""); + const [endTime, setEndTime] = useState(""); + const [room, setRoom] = useState(""); + + const isValidBooking = () => { + if (!room) { + toast.error("Vennligst fyll inn rom") + return false; + } + if (!date) { + toast.error("Vennligst velg dato") + return false; + } + if (!startTime || !endTime) { + toast.error("Vennligst velg tidspunkt") + return false; + } + if (Date.parse("2003-07-26T" + startTime) - Date.parse("2003-07-26T" + endTime) > 0) { + toast.error("Starttid må være før sluttid") + return false; + } + + return true; + } + + const handleAddBooking = () => { + if (!isValidBooking()) return; + addBooking() + setRoom("") + } + + const addBooking = () => { + const booking: RoomBooking = { + room: room, + startDate: date.split("T")[0] + "T" + startTime, + endDate: date.split("T")[0] + "T" + endTime + } + + roomBookings.push(booking) + setRoomBookings(roomBookings) + } + + const columns: ColumnType[] = [ + { label: "Rom", field: "room" }, + { label: "Dato", field: "date" }, + { label: "Fra", field: "from" }, + { label: "Til", field: "to" }, + { label: "Slett", field: "delete" }, + ] + + return
+

Legg inn romvalg

+
+
+ { + setRoom(input) + }} + label="Romnavn" + className="mx-0" + /> + { + setDate(date) + }} + label="Test" + /> + { + setStartTime(times.start) + setEndTime(times.end) + }} + className="mx-0" + /> +
+ +
+

Alle tilgjengelige romvalg

+ {roomBookings.length ? { + return { + id: roomBooking.room + "]" + roomBooking.startDate + "]" + roomBooking.endDate, + room: roomBooking.room, + date: roomBooking.startDate.split("T")[0], + from: roomBooking.startDate.split("T")[1], + to: roomBooking.endDate.split("T")[1] + } + })} onDelete={(id: string, name: string) => { + const [room, startDate, endDate] = id.split("]") + setRoomBookings(roomBookings.filter((booking, index, array) => { + return !(booking.room == room + && booking.startDate == startDate + && booking.endDate == endDate) + })) + }} /> + :

Legg inn et rom, så dukker det opp her.

} + +}; + +export default RoomInterview; diff --git a/components/Table.tsx b/components/Table.tsx index 78039b12..67281dc9 100644 --- a/components/Table.tsx +++ b/components/Table.tsx @@ -3,7 +3,7 @@ import { FaTrash } from "react-icons/fa"; import React from "react"; import { useRouter } from "next/router"; -type ColumnType = { +export type ColumnType = { label: string; field: string; }; @@ -52,9 +52,8 @@ const Table = ({ rows, columns, onDelete }: TableProps) => { {columns.map((column) => (
{column.field === "delete" && onDelete ? (
diff --git a/components/form/DateInput.tsx b/components/form/DateInput.tsx new file mode 100644 index 00000000..eaff3c9b --- /dev/null +++ b/components/form/DateInput.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +interface Props { + label?: string; + updateDate: (date: string) => void; +} + +const DateRangeInput = (props: Props) => { + const [date, setDate] = useState(""); + + useEffect(() => { + const dateString = date ? `${date}T00:00` : ""; + props.updateDate(dateString); + }, [date]); + + return ( +
+ setDate(e.target.value)} + className="border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 border-gray-300 text-gray-900 dark:border-gray-600 dark:bg-online-darkBlue dark:text-gray-200" + /> +
+ ); +}; + +export default DateRangeInput; diff --git a/components/form/TextInput.tsx b/components/form/TextInput.tsx index d54d130b..aa4e2d48 100644 --- a/components/form/TextInput.tsx +++ b/components/form/TextInput.tsx @@ -4,6 +4,7 @@ interface Props { disabled?: boolean; placeholder?: string; defaultValue?: string; + className?: string; } const TextInput = (props: Props) => { @@ -12,7 +13,7 @@ const TextInput = (props: Props) => { }; return ( -
+
void; + className?: string; +} + +const TimeRangeInput = (props: Props) => { + const [fromTime, setFromTime] = useState(""); + const [toTime, setToTime] = useState(""); + new Date() + + useEffect(() => { + const startTime = fromTime ? `${fromTime}` : ""; + const endTime = toTime ? `${toTime}` : ""; + props.updateTimes({ start: startTime, end: endTime }); + }, [fromTime, toTime]); + + return ( +
+ +
+ setFromTime(e.target.value)} + className="border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 border-gray-300 text-gray-900 dark:border-gray-600 dark:bg-online-darkBlue dark:text-gray-200" + /> + til + setToTime(e.target.value)} + className="border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 border-gray-300 text-gray-900 dark:border-gray-600 dark:bg-online-darkBlue dark:text-gray-200" + /> +
+
+ ); +}; + +export default TimeRangeInput; diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index d025954d..fadc959c 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -4,8 +4,9 @@ import router from "next/router"; import { periodType } from "../../../lib/types/types"; import NotFound from "../../404"; import ApplicantsOverview from "../../../components/applicantoverview/ApplicantsOverview"; +import RoomOverview from "../../../components/RoomOverview"; import { Tabs } from "../../../components/Tabs"; -import { CalendarIcon, InboxIcon } from "@heroicons/react/24/solid"; +import { CalendarIcon, InboxIcon, BuildingOffice2Icon } from "@heroicons/react/24/solid"; import Button from "../../../components/Button"; import { useQuery } from "@tanstack/react-query"; import { fetchPeriodById } from "../../../lib/api/periodApi"; @@ -87,26 +88,35 @@ const Admin = () => { /> ), }, + { + title: "Romoppsett", + icon: , + content: ( + + ) + }, //Super admin :) ...(session?.user?.email && - ["fhansteen@gmail.com", "jotto0214@gmail.com"].includes( - session.user.email - ) + ["fhansteen@gmail.com", "jotto0214@gmail.com"].includes( + session.user.email + ) ? [ - { - title: "Send ut", - icon: , - content: ( -
-
- ), - }, - ] + { + title: "Send ut", + icon: , + content: ( +
+
+ ), + }, + ] : []), ]} /> From a04dd9a136683437adfcfdcffcf9dd83a0b3a6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 19 Jan 2025 16:37:36 +0100 Subject: [PATCH 3/4] fix: made new database table for roombookings --- components/RoomOverview.tsx | 137 --------------------- components/admin/RoomOverview.tsx | 171 +++++++++++++++++++++++++++ lib/api/roomApi.ts | 41 +++++++ lib/mongo/rooms.ts | 133 +++++++++++++++++++++ lib/types/types.ts | 8 ++ lib/utils/validators.ts | 11 ++ package-lock.json | 5 + package.json | 1 + pages/admin/[period-id]/index.tsx | 2 +- pages/api/periods/[id].ts | 6 +- pages/api/rooms/[period-id]/[id].ts | 61 ++++++++++ pages/api/rooms/[period-id]/index.ts | 33 ++++++ pages/api/rooms/index.ts | 46 +++++++ 13 files changed, 515 insertions(+), 140 deletions(-) delete mode 100644 components/RoomOverview.tsx create mode 100644 components/admin/RoomOverview.tsx create mode 100644 lib/api/roomApi.ts create mode 100644 lib/mongo/rooms.ts create mode 100644 pages/api/rooms/[period-id]/[id].ts create mode 100644 pages/api/rooms/[period-id]/index.ts create mode 100644 pages/api/rooms/index.ts diff --git a/components/RoomOverview.tsx b/components/RoomOverview.tsx deleted file mode 100644 index f43f4cc9..00000000 --- a/components/RoomOverview.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { useState } from "react"; -import { periodType, committeeInterviewType } from "../lib/types/types"; -import Button from "./Button"; -import Table, { RowType } from "./Table" -import { ColumnType } from "./Table"; -import TextInput from "./form/TextInput"; -import DateInput from "./form/DateInput"; -import TimeRangeInput from "./form/TimeRangeInput"; - -import toast from "react-hot-toast"; - -interface Interview { - id: string; - title: string; - start: string; - end: string; -} - -interface RoomBooking { - room: String, - startDate: String - endDate: String -} - -interface Props { - period: periodType | null; -} - -const RoomInterview = ({ - period -}: Props) => { - - // TODO: Fix correct tabbing - - const [roomBookings, setRoomBookings] = useState([]) - - const [date, setDate] = useState(""); - const [startTime, setStartTime] = useState(""); - const [endTime, setEndTime] = useState(""); - const [room, setRoom] = useState(""); - - const isValidBooking = () => { - if (!room) { - toast.error("Vennligst fyll inn rom") - return false; - } - if (!date) { - toast.error("Vennligst velg dato") - return false; - } - if (!startTime || !endTime) { - toast.error("Vennligst velg tidspunkt") - return false; - } - if (Date.parse("2003-07-26T" + startTime) - Date.parse("2003-07-26T" + endTime) > 0) { - toast.error("Starttid må være før sluttid") - return false; - } - - return true; - } - - const handleAddBooking = () => { - if (!isValidBooking()) return; - addBooking() - setRoom("") - } - - const addBooking = () => { - const booking: RoomBooking = { - room: room, - startDate: date.split("T")[0] + "T" + startTime, - endDate: date.split("T")[0] + "T" + endTime - } - - roomBookings.push(booking) - setRoomBookings(roomBookings) - } - - const columns: ColumnType[] = [ - { label: "Rom", field: "room" }, - { label: "Dato", field: "date" }, - { label: "Fra", field: "from" }, - { label: "Til", field: "to" }, - { label: "Slett", field: "delete" }, - ] - - return
-

Legg inn romvalg

-
-
- { - setRoom(input) - }} - label="Romnavn" - className="mx-0" - /> - { - setDate(date) - }} - label="Test" - /> - { - setStartTime(times.start) - setEndTime(times.end) - }} - className="mx-0" - /> -
- -
-

Alle tilgjengelige romvalg

- {roomBookings.length ? { - return { - id: roomBooking.room + "]" + roomBooking.startDate + "]" + roomBooking.endDate, - room: roomBooking.room, - date: roomBooking.startDate.split("T")[0], - from: roomBooking.startDate.split("T")[1], - to: roomBooking.endDate.split("T")[1] - } - })} onDelete={(id: string, name: string) => { - const [room, startDate, endDate] = id.split("]") - setRoomBookings(roomBookings.filter((booking, index, array) => { - return !(booking.room == room - && booking.startDate == startDate - && booking.endDate == endDate) - })) - }} /> - :

Legg inn et rom, så dukker det opp her.

} - -}; - -export default RoomInterview; diff --git a/components/admin/RoomOverview.tsx b/components/admin/RoomOverview.tsx new file mode 100644 index 00000000..72cf4f41 --- /dev/null +++ b/components/admin/RoomOverview.tsx @@ -0,0 +1,171 @@ +import { useEffect, useState } from "react"; +import { periodType, RoomBooking } from "../../lib/types/types"; +import Button from "../Button"; +import Table, { ColumnType } from "../Table"; +import DateInput from "../form/DateInput"; +import TextInput from "../form/TextInput"; +import TimeRangeInput from "../form/TimeRangeInput"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { WithId } from "mongodb"; +import toast from "react-hot-toast"; +import { createRoom, deleteRoom, fetchRoomsByPeriodId } from "../../lib/api/roomApi"; + +interface Props { + period?: periodType; +} + +const RoomInterview = ({ + period +}: Props) => { + + const queryClient = useQueryClient(); + + // TODO: Fix correct tabbing + const [roomBookings, setRoomBookings] = useState[]>([]) + + const [date, setDate] = useState(""); + const [startTime, setStartTime] = useState(""); + const [endTime, setEndTime] = useState(""); + const [room, setRoom] = useState(""); + + const isValidBooking = () => { + if (!room) { + toast.error("Vennligst fyll inn rom") + return false; + } + if (!date) { + toast.error("Vennligst velg dato") + return false; + } + if (!startTime || !endTime) { + toast.error("Vennligst velg tidspunkt") + return false; + } + if (Date.parse("2003-07-26T" + startTime) - Date.parse("2003-07-26T" + endTime) > 0) { + toast.error("Starttid må være før sluttid") + return false; + } + + // TODO: Make sure time is within interviewPeriod + + return true; + } + + const { + data: roomData, + } = useQuery({ + queryKey: ["rooms", period?._id], + queryFn: fetchRoomsByPeriodId + }) + + const createRoomBookingMutation = useMutation({ + mutationFn: createRoom, + onSuccess: () => + queryClient.invalidateQueries({ + // TODO: try to update cache instead + queryKey: ["rooms", period?._id], + }), + }); + + const deleteRoomBookingMutation = useMutation({ + mutationFn: (roomId: string) => deleteRoom(roomId, String(period!._id)), + onSuccess: () => + queryClient.invalidateQueries({ + // TODO: try to update cache instead + queryKey: ["rooms", period?._id], + }), + }); + + useEffect(() => { + if (!roomData) return + const { rooms } = roomData + console.log(rooms) + setRoomBookings(rooms) + }, [roomData]) + + const handleAddBooking = () => { + if (!isValidBooking()) return; + addBooking() + setRoom("") + } + + const addBooking = () => { + const booking: RoomBooking = { + periodId: String(period?._id), + room: room, + startDate: new Date(date.split("T")[0] + "T" + startTime), + endDate: new Date(date.split("T")[0] + "T" + endTime) + } + + createRoomBookingMutation.mutate(booking) + } + + const handleDeleteBooking = async (booking: WithId) => { + if (!period) return; + const isConfirmed = window.confirm( + `Er det sikker på at du ønsker å fjerne bookingen av ${booking.room} fra ${booking.startDate} til ${booking.endDate}?` + ); + if (!isConfirmed) return; + + setRoomBookings(roomBookings.filter((bookingA) => bookingA._id != booking._id)) + + deleteRoomBookingMutation.mutate(String(booking._id)) + }; + + const columns: ColumnType[] = [ + { label: "Rom", field: "room" }, + { label: "Dato", field: "date" }, + { label: "Fra", field: "from" }, + { label: "Til", field: "to" }, + { label: "Slett", field: "delete" }, + ] + + return
+

Legg inn romvalg

+
+
+ { + setRoom(input) + }} + label="Romnavn" + className="mx-0" + /> + { + setDate(date) + }} + label="Test" + /> + { + setStartTime(times.start) + setEndTime(times.end) + }} + className="mx-0" + /> +
+ +
+

Alle tilgjengelige romvalg

+ {roomBookings?.length ?
{ + return { + id: String(roomBooking._id), + room: roomBooking.room, + date: new Date(roomBooking.startDate).toLocaleDateString(), + from: new Date(roomBooking.startDate).toLocaleTimeString(), + to: new Date(roomBooking.endDate).toLocaleTimeString() + } + })} onDelete={(id: string, name: string) => { + const deletedBooking = roomBookings.find((booking, index, array) => { + return String(booking._id) == id + }) + handleDeleteBooking(deletedBooking!) + }} /> + :

Legg inn et rom, så dukker det opp her.

} + +}; + +export default RoomInterview; diff --git a/lib/api/roomApi.ts b/lib/api/roomApi.ts new file mode 100644 index 00000000..8fd9bf3c --- /dev/null +++ b/lib/api/roomApi.ts @@ -0,0 +1,41 @@ +import { QueryFunctionContext } from "@tanstack/react-query"; +import { RoomBooking } from "../types/types"; + +export const fetchRoomByPeriodAndId = async (context: QueryFunctionContext) => { + const periodId = context.queryKey[1]; + const roomId = context.queryKey[2]; + return fetch(`/api/rooms/${periodId}/${roomId}`).then((res) => res.json()); +}; + +export const fetchRoomsByPeriodId = async (context: QueryFunctionContext) => { + const periodId = context.queryKey[1]; + return fetch(`/api/rooms/${periodId}`).then((res) => res.json()); +}; + +export const createRoom = async (room: RoomBooking): Promise => { + const response = await fetch(`/api/rooms/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(room), + }); + + const { message, data, error } = await response.json(); + if (!response.ok) { + throw new Error(error || "Unknown error occurred"); + } + return data; +}; + +export const deleteRoom = async (roomId: string, periodId: string) => { + const response = await fetch(`/api/rooms/${periodId}/${roomId}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error("Failed to delete the room"); + } + + return response; +}; diff --git a/lib/mongo/rooms.ts b/lib/mongo/rooms.ts new file mode 100644 index 00000000..2a42d923 --- /dev/null +++ b/lib/mongo/rooms.ts @@ -0,0 +1,133 @@ +import { Collection, Db, MongoClient, ObjectId } from "mongodb"; +import { RoomBooking } from "../types/types"; +import clientPromise from "./mongodb"; + +let client: MongoClient; +let db: Db; +let rooms: Collection; + +async function init() { + if (db) return; + try { + client = await clientPromise; + db = client.db(); + rooms = db.collection("rooms"); + } catch (error) { + console.error(error); + throw new Error("Failed to establish connection to database"); + } +} + +(async () => { + await init(); +})(); + +export const createRoom = async (roomData: RoomBooking) => { + try { + if (!rooms) await init(); + + const existingRoom = await rooms.findOne({ + room: roomData.room, + periodId: roomData.periodId, + $or: [ + /* Tillater ikke overlappende tidspunkt på samme rom. */ + { + $and: [ + { + startDate: { $gte: roomData.startDate }, + }, + { startDate: { $lte: roomData.endDate } }, + ], + }, + { + $and: [ + { + endDate: { $gte: roomData.startDate }, + }, + { endDate: { $lte: roomData.endDate } }, + ], + }, + ], + }); + + if (existingRoom) { + return { error: "409 Room booking already exists for this period" }; + } + + const result = await rooms.insertOne(roomData); + if (result.insertedId) { + const insertedRoom = await rooms.findOne({ + _id: result.insertedId, + }); + if (insertedRoom) { + return { room: insertedRoom }; + } else { + return { error: "Failed to retrieve the created room" }; + } + } else { + return { error: "Failed to create room" }; + } + } catch (error) { + console.error(error); + return { error: "Failed to create room" }; + } +}; + +export const getRooms = async () => { + try { + if (!rooms) await init(); + const result = await rooms.find({}).toArray(); + return { rooms: result }; + } catch (error) { + return { error: "Failed to fetch rooms" }; + } +}; + +export const getRoom = async (roomId: string) => { + try { + if (!rooms) await init(); + + const result = await rooms.findOne({ + _id: new ObjectId(roomId), + }); + + return { room: result, exists: !!result }; + } catch (error) { + console.error(error); + return { error: "Failed to fetch room", exists: false }; + } +}; + +export const getRoomsByPeriod = async (periodId: string) => { + try { + if (!rooms) await init(); + + const result = await rooms + .find({ periodId: periodId }) // No ObjectId conversion needed + .toArray(); + + return { rooms: result, exists: result.length > 0 }; + } catch (error) { + console.error(error); + return { error: "Failed to fetch rooms" }; + } +}; + +export const deleteRoom = async (id: string) => { + try { + if (!rooms) await init(); + + const result = await rooms.deleteOne({ + _id: new ObjectId(id), + }); + + if (result.deletedCount === 1) { + return { message: "Room deleted successfully" }; + } else { + return { error: "Room not found or already deleted" }; + } + } catch (error) { + console.error(error); + return { error: "Failed to delete room" }; + } +}; diff --git a/lib/types/types.ts b/lib/types/types.ts index 708214ec..1e2adaa3 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -141,3 +141,11 @@ export type emailApplicantInterviewType = { }; }[]; }; + +export interface RoomBooking { + periodId: string; + room: String; + startDate: Date; + endDate: Date; + committeeId?: String; +} diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index e4433c4f..30000d55 100644 --- a/lib/utils/validators.ts +++ b/lib/utils/validators.ts @@ -3,6 +3,7 @@ import { committeeInterviewType, periodType, preferencesType, + RoomBooking, } from "../types/types"; export const isApplicantType = ( @@ -191,3 +192,13 @@ export const isPeriodType = (data: any): data is periodType => { return hasBasicFields; }; + +export const isRoomBookings = (data: any): data is RoomBooking[] => { + // TODO: Implement + return true; +}; + +export const isRoomBooking = (data: any): data is RoomBooking[] => { + // TODO: Implement + return true; +}; diff --git a/package-lock.json b/package-lock.json index 6bf93c5d..d5a5825b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "mongodb": "^6.1.0", "next": "^12.3.4", "next-auth": "^4.24.5", + "online-opptak": "file:", "react": "18.2.0", "react-dom": "18.2.0", "react-hot-toast": "^2.4.1", @@ -5182,6 +5183,10 @@ "wrappy": "1" } }, + "node_modules/online-opptak": { + "resolved": "", + "link": true + }, "node_modules/openid-client": { "version": "5.6.5", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", diff --git a/package.json b/package.json index 9fedecce..4b5e32b9 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "mongodb": "^6.1.0", "next": "^12.3.4", "next-auth": "^4.24.5", + "online-opptak": "file:", "react": "18.2.0", "react-dom": "18.2.0", "react-hot-toast": "^2.4.1", diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index fadc959c..081bc142 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -4,7 +4,7 @@ import router from "next/router"; import { periodType } from "../../../lib/types/types"; import NotFound from "../../404"; import ApplicantsOverview from "../../../components/applicantoverview/ApplicantsOverview"; -import RoomOverview from "../../../components/RoomOverview"; +import RoomOverview from "../../../components/admin/RoomOverview"; import { Tabs } from "../../../components/Tabs"; import { CalendarIcon, InboxIcon, BuildingOffice2Icon } from "@heroicons/react/24/solid"; import Button from "../../../components/Button"; diff --git a/pages/api/periods/[id].ts b/pages/api/periods/[id].ts index 3f51d27e..07d55820 100644 --- a/pages/api/periods/[id].ts +++ b/pages/api/periods/[id].ts @@ -1,8 +1,8 @@ import { NextApiRequest, NextApiResponse } from "next"; import { getServerSession } from "next-auth"; -import { authOptions } from "../auth/[...nextauth]"; import { deletePeriodById, getPeriodById } from "../../../lib/mongo/periods"; import { hasSession, isAdmin } from "../../../lib/utils/apiChecks"; +import { authOptions } from "../auth/[...nextauth]"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -29,7 +29,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(200).json({ exists, period }); } else if (req.method === "DELETE") { - if (!isAdmin) return res.status(403).json({ error: "Unauthorized" }); + // TODO: The next line is probably supposed to be !isAdmin(res, session)? + if (!isAdmin(res, session)) + return res.status(403).json({ error: "Unauthorized" }); const { error } = await deletePeriodById(id); if (error) throw new Error(error); diff --git a/pages/api/rooms/[period-id]/[id].ts b/pages/api/rooms/[period-id]/[id].ts new file mode 100644 index 00000000..ce2f5f92 --- /dev/null +++ b/pages/api/rooms/[period-id]/[id].ts @@ -0,0 +1,61 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../auth/[...nextauth]"; +import { hasSession, isAdmin } from "../../../../lib/utils/apiChecks"; +import { getPeriodById } from "../../../../lib/mongo/periods"; +import { deleteRoom, getRoom } from "../../../../lib/mongo/rooms"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getServerSession(req, res, authOptions); + + if (!hasSession(res, session)) return; + + const id = req.query.id; + const periodId = req.query["period-id"]; + + if (typeof id !== "string" || typeof periodId !== "string") { + return res.status(400).json({ error: "Invalid ID format" }); + } + + const { period } = await getPeriodById(periodId); + + if (!period) { + return res.status(404).json({ error: "Period not found" }); + } + + if (!isAdmin(res, session)) + return res.status(403).json({ error: "Unauthorized" }); + + try { + if (req.method === "GET") { + const { room, exists, error } = await getRoom(id); + + if (error) { + return res.status(500).json({ error }); + } + + if (!exists) { + return res.status(404).json({ message: "Room not found" }); + } + + return res.status(200).json({ exists, room }); + } else if (req.method === "DELETE") { + const { error } = await deleteRoom(id); + + if (error) throw new Error(error); + + return res.status(204).end(); + } + } catch (error) { + if (error instanceof Error) { + return res.status(500).json({ error: error.message }); + } + + return res.status(500).json("Unexpected error occurred"); + } + + res.setHeader("Allow", ["GET", "DELETE"]); + res.status(405).end(`Method ${req.method} Not Allowed`); +}; + +export default handler; diff --git a/pages/api/rooms/[period-id]/index.ts b/pages/api/rooms/[period-id]/index.ts new file mode 100644 index 00000000..9ff0c5cc --- /dev/null +++ b/pages/api/rooms/[period-id]/index.ts @@ -0,0 +1,33 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth"; +import { getRoomsByPeriod } from "../../../../lib/mongo/rooms"; +import { hasSession } from "../../../../lib/utils/apiChecks"; +import { authOptions } from "../../auth/[...nextauth]"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getServerSession(req, res, authOptions); + + if (!hasSession(res, session)) return; + + const periodId = req.query["period-id"]; + + try { + if (req.method === "GET") { + if (typeof periodId != "string") { + throw new Error("Not a valid period id string"); + } + const { rooms, error } = await getRoomsByPeriod(periodId); + + if (error) throw new Error(error); + + return res.status(200).json({ rooms }); + } + } catch { + res.status(500).json("An error occurred"); + } + + res.setHeader("Allow", ["GET", "POST"]); + res.status(405).end(`Method ${req.method} is not allowed.`); +}; + +export default handler; diff --git a/pages/api/rooms/index.ts b/pages/api/rooms/index.ts new file mode 100644 index 00000000..eb6ba759 --- /dev/null +++ b/pages/api/rooms/index.ts @@ -0,0 +1,46 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth"; +import { createRoom, getRooms } from "../../../lib/mongo/rooms"; +import { RoomBooking } from "../../../lib/types/types"; +import { hasSession, isAdmin } from "../../../lib/utils/apiChecks"; +import { isRoomBooking } from "../../../lib/utils/validators"; +import { authOptions } from "../auth/[...nextauth]"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getServerSession(req, res, authOptions); + + if (!hasSession(res, session)) return; + + try { + if (req.method === "GET") { + const { rooms, error } = await getRooms(); + + if (error) throw new Error(error); + + return res.status(200).json({ rooms }); + } + + if (req.method === "POST") { + if (!isAdmin(res, session)) return; + const room = req.body as RoomBooking; + + if (!isRoomBooking(req.body)) { + return res.status(400).json({ error: "Invalid data format" }); + } + + const { room: createdRoom, error } = await createRoom(room); + if (error) throw new Error(error); + return res.status(201).json({ + message: "Period created successfully", + data: createdRoom, + }); + } + } catch { + res.status(500).json("An error occurred"); + } + + res.setHeader("Allow", ["GET", "POST"]); + res.status(405).end(`Method ${req.method} is not allowed.`); +}; + +export default handler; From e8356c7af9dd89e3a981aea64e10c4bdf1f55508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 19 Jan 2025 17:25:37 +0100 Subject: [PATCH 4/4] fix: implement validators --- lib/utils/validators.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index 30000d55..52aed83d 100644 --- a/lib/utils/validators.ts +++ b/lib/utils/validators.ts @@ -152,11 +152,11 @@ export const validateCommittee = (data: any, period: periodType): boolean => { ); }; -export const isPeriodType = (data: any): data is periodType => { - const isDateString = (str: any): boolean => { - return typeof str === "string" && !isNaN(Date.parse(str)); - }; +const isDateString = (str: any): boolean => { + return typeof str === "string" && !isNaN(Date.parse(str)); +}; +export const isPeriodType = (data: any): data is periodType => { const isValidPeriod = (period: any): boolean => { return ( typeof period === "object" && @@ -194,11 +194,19 @@ export const isPeriodType = (data: any): data is periodType => { }; export const isRoomBookings = (data: any): data is RoomBooking[] => { - // TODO: Implement - return true; + if (!Array.isArray(data)) return false; + return data.every(isRoomBooking); }; -export const isRoomBooking = (data: any): data is RoomBooking[] => { - // TODO: Implement - return true; +export const isRoomBooking = (data: any): data is RoomBooking => { + return ( + typeof data === "object" && + data !== null && + typeof data.periodId === "string" && + typeof data.room === "string" && + isDateString(data.startDate) && + isDateString(data.endDate) && + (typeof data.committeeId === "string" || + typeof data.committeeId === "undefined") + ); };