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/3] 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/3] 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 6afc3e391bbcc704b7320a13b044afd48d04a31e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Fri, 27 Sep 2024 12:30:58 +0200 Subject: [PATCH 3/3] hot commit for fremvisning --- components/{ => admin}/RoomOverview.tsx | 27 +++++++----------------- lib/api/periodApi.ts | 15 ++++++++++++- lib/mongo/periods.ts | 28 ++++++++++++++++++++++++- lib/types/types.ts | 7 +++++++ lib/utils/validators.ts | 6 ++++++ package-lock.json | 5 +++++ package.json | 1 + pages/admin/[period-id]/index.tsx | 2 +- pages/api/periods/[id].ts | 18 ++++++++++++++++ 9 files changed, 86 insertions(+), 23 deletions(-) rename components/{ => admin}/RoomOverview.tsx (87%) diff --git a/components/RoomOverview.tsx b/components/admin/RoomOverview.tsx similarity index 87% rename from components/RoomOverview.tsx rename to components/admin/RoomOverview.tsx index f43f4cc9..406a6fc7 100644 --- a/components/RoomOverview.tsx +++ b/components/admin/RoomOverview.tsx @@ -1,27 +1,14 @@ 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 { periodType, committeeInterviewType, RoomBooking } 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; } diff --git a/lib/api/periodApi.ts b/lib/api/periodApi.ts index a5662f53..36b4dfa4 100644 --- a/lib/api/periodApi.ts +++ b/lib/api/periodApi.ts @@ -1,5 +1,5 @@ import { QueryFunctionContext } from "@tanstack/react-query"; -import { periodType } from "../types/types"; +import { periodType, RoomBooking } from "../types/types"; export const fetchPeriodById = async (context: QueryFunctionContext) => { const id = context.queryKey[1]; @@ -25,3 +25,16 @@ export const createPeriod = async (period: periodType) => { }, }); }; + +export const updateRoomsForPeriod = async ( + id: string, + rooms: RoomBooking[] +) => { + return fetch(`/api/periods${id}`, { + method: "PATCH", + body: JSON.stringify(rooms), + headers: { + "Content-Type": "application/json", + }, + }); +}; diff --git a/lib/mongo/periods.ts b/lib/mongo/periods.ts index dccc87d8..28c88734 100644 --- a/lib/mongo/periods.ts +++ b/lib/mongo/periods.ts @@ -1,6 +1,6 @@ import { Collection, Db, MongoClient, ObjectId } from "mongodb"; import clientPromise from "./mongodb"; -import { periodType } from "../types/types"; +import { periodType, RoomBooking } from "../types/types"; let client: MongoClient; let db: Db; @@ -85,6 +85,32 @@ export const getCurrentPeriods = async () => { } }; +export const updateRoomsForPeriod = async ( + id: string | ObjectId, + rooms: RoomBooking[] +) => { + try { + if (!periods) await init(); + + // Checks if period exists + const period = await getPeriodById(id); + if (!period.exists) { + return { error: "Period not found" }; + } + + const response = await periods.updateOne( + { _id: new ObjectId(id) }, + { $set: { rooms: rooms } } + ); + + return response.modifiedCount >= 1 + ? { message: "Period updated with rooms" } + : { message: "Period not updated" }; + } catch (error) { + return { error: "Failed to update rooms for period" }; + } +}; + export const getPeriodById = async (id: string | ObjectId) => { try { if (!periods) await init(); diff --git a/lib/types/types.ts b/lib/types/types.ts index 708214ec..b0c03a3b 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -66,6 +66,7 @@ export type periodType = { committees: string[]; optionalCommittees: string[]; hasSentInterviewTimes: boolean; + rooms?: RoomBooking[]; }; export type AvailableTime = { @@ -141,3 +142,9 @@ export type emailApplicantInterviewType = { }; }[]; }; + +export interface RoomBooking { + room: String; + startDate: String; + endDate: String; +} diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index e4433c4f..21352459 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,8 @@ export const isPeriodType = (data: any): data is periodType => { return hasBasicFields; }; + +export const isRoomBookings = (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..ecc2d8cf 100644 --- a/pages/api/periods/[id].ts +++ b/pages/api/periods/[id].ts @@ -3,6 +3,9 @@ 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 { updateRoomsForPeriod } from "../../../lib/api/periodApi"; +import { isRoomBookings } from "../../../lib/utils/validators"; +import { RoomBooking } from "../../../lib/types/types"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -28,7 +31,22 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } return res.status(200).json({ exists, period }); + } else if (req.method === "PATCH") { + if (!isAdmin(res, session)) { + return res.status(403).json({ error: "Unauthorized" }); + } + + if (!isRoomBookings(req.body)) { + return res.status(400).json({ error: "Invalid data format" }); + } + + const bookings: RoomBooking[] = req.body as RoomBooking[]; + + const { error } = await updateRoomsForPeriod(id, bookings); + if (error) throw new Error(error); + return res.status(200).json({ message: updated }); } else if (req.method === "DELETE") { + // TODO: The next line is probably supposed to be !isAdmin(res, session)? if (!isAdmin) return res.status(403).json({ error: "Unauthorized" }); const { error } = await deletePeriodById(id);