diff --git a/prisma/migrations/20240419025521_changed_request_shape/migration.sql b/prisma/migrations/20240419025521_changed_request_shape/migration.sql new file mode 100644 index 0000000..dbbb9ed --- /dev/null +++ b/prisma/migrations/20240419025521_changed_request_shape/migration.sql @@ -0,0 +1,36 @@ +/* + Warnings: + + - You are about to drop the `_equipmenttorequest` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_facilitytorequest` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `equipmentId` to the `Request` table without a default value. This is not possible if the table is not empty. + - Added the required column `facilityId` to the `Request` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE `_equipmenttorequest` DROP FOREIGN KEY `_EquipmentToRequest_A_fkey`; + +-- DropForeignKey +ALTER TABLE `_equipmenttorequest` DROP FOREIGN KEY `_EquipmentToRequest_B_fkey`; + +-- DropForeignKey +ALTER TABLE `_facilitytorequest` DROP FOREIGN KEY `_FacilityToRequest_A_fkey`; + +-- DropForeignKey +ALTER TABLE `_facilitytorequest` DROP FOREIGN KEY `_FacilityToRequest_B_fkey`; + +-- AlterTable +ALTER TABLE `request` ADD COLUMN `equipmentId` INTEGER NOT NULL, + ADD COLUMN `facilityId` INTEGER NOT NULL; + +-- DropTable +DROP TABLE `_equipmenttorequest`; + +-- DropTable +DROP TABLE `_facilitytorequest`; + +-- AddForeignKey +ALTER TABLE `Request` ADD CONSTRAINT `Request_facilityId_fkey` FOREIGN KEY (`facilityId`) REFERENCES `Facility`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Request` ADD CONSTRAINT `Request_equipmentId_fkey` FOREIGN KEY (`equipmentId`) REFERENCES `Equipment`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20240419034705_fixed_description_request/migration.sql b/prisma/migrations/20240419034705_fixed_description_request/migration.sql new file mode 100644 index 0000000..ea62a8a --- /dev/null +++ b/prisma/migrations/20240419034705_fixed_description_request/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `request` MODIFY `description` TINYTEXT NOT NULL; diff --git a/prisma/migrations/20240419055629_changed_request_dates_json/migration.sql b/prisma/migrations/20240419055629_changed_request_dates_json/migration.sql new file mode 100644 index 0000000..2f65709 --- /dev/null +++ b/prisma/migrations/20240419055629_changed_request_dates_json/migration.sql @@ -0,0 +1,15 @@ +-- DropForeignKey +ALTER TABLE `request` DROP FOREIGN KEY `Request_equipmentId_fkey`; + +-- DropForeignKey +ALTER TABLE `request` DROP FOREIGN KEY `Request_facilityId_fkey`; + +-- AlterTable +ALTER TABLE `request` MODIFY `equipmentId` INTEGER NULL, + MODIFY `facilityId` INTEGER NULL; + +-- AddForeignKey +ALTER TABLE `Request` ADD CONSTRAINT `Request_facilityId_fkey` FOREIGN KEY (`facilityId`) REFERENCES `Facility`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Request` ADD CONSTRAINT `Request_equipmentId_fkey` FOREIGN KEY (`equipmentId`) REFERENCES `Equipment`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20240419061409_request_status_json/migration.sql b/prisma/migrations/20240419061409_request_status_json/migration.sql new file mode 100644 index 0000000..0939497 --- /dev/null +++ b/prisma/migrations/20240419061409_request_status_json/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `isAllowed` on the `request` table. All the data in the column will be lost. + - Added the required column `requestStatus` to the `Request` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `request` DROP COLUMN `isAllowed`, + ADD COLUMN `requestStatus` JSON NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f250554..8feee71 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -107,16 +107,21 @@ model Equipment { } model Request { - id Int @id @default(autoincrement()) - description String - facilities Facility[] @relation(name: "FacilityToRequest") - equipments Equipment[] @relation(name: "EquipmentToRequest") - students Student[] @relation(name: "StudentToRequest") - isAllowed Boolean @default(false) - admins Admin[] @relation(name: "AdminToRequest") + id Int @id @default(autoincrement()) + description String @db.TinyText + facility Facility? @relation(name: "FacilityToRequest", fields: [facilityId], references: [id]) + equipment Equipment? @relation(name: "EquipmentToRequest", fields: [equipmentId], references: [id]) + students Student[] @relation(name: "StudentToRequest") + admins Admin[] @relation(name: "AdminToRequest") + + facilityId Int? + equipmentId Int? /// [DatesType] requestDates Json + + /// [RequestStatusType] + requestStatus Json } enum UserRole { diff --git a/src/app.d.ts b/src/app.d.ts index 939a785..cb22855 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -22,11 +22,13 @@ declare global { namespace PrismaJson { type DepartmentsType = { departments: Array?} - type DatesType = { dates: Array?} + type DatesType = { start: Date, end: Date }[] + type RequestStatusType = { adminId: number, requestStatus: "APPROVED" | "WAITING" | "REJECTED" }[] } } type Departments = PrismaJson.DepartmentsType; type Dates = PrismaJson.DatesType; +type RequestStatus = PrismaJson.RequestStatusType; -export { Departments, Dates }; +export { Departments, Dates, RequestStatus }; diff --git a/src/lib/components/search.ts b/src/lib/components/search.ts new file mode 100644 index 0000000..05d9b77 --- /dev/null +++ b/src/lib/components/search.ts @@ -0,0 +1,12 @@ +import type { Page } from "@sveltejs/kit"; + +export const searchQuery = (searchTerm: string | undefined | null, category: string, page: Page, string | null>) => { + if (searchTerm === null || typeof searchTerm === "undefined") return {query: "", term: ""}; + if (searchTerm.trim() === "") return {query: "", term: ""}; + + let query = new URLSearchParams(page.url.searchParams.toString()); + query.set("term", searchTerm.trim()); + query.set("cat", category); + + return {query: query.toString() || "", term: searchTerm.trim()}; + }; \ No newline at end of file diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..4970d5b --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,36 @@ + + +
+
+

{$page.status}

+

{statusMessage}

+
+ + +
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..fc332c0 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,29 @@ +import type { LayoutServerLoad } from './$types' +import { redirect, type Actions } from '@sveltejs/kit'; +import { lucia } from '$lib/server/auth'; +import db from '$lib/prisma'; + +export const load: LayoutServerLoad = async (event) => { + console.log(event.locals.user); + + if (!event.locals.user) { + return { isLoggedIn: false } + } + + const user = await db.user.findUnique({ + select: { role: true, student: true, admin: true }, + where: { id: event.locals.user.id } + }) + + if (user?.role == "STUDENT" && user?.student) { + return { isStudent: true } + } + + if (!user?.admin && !user?.student) { + return { needsRegisterFollowup: true } + } + + if (user?.role == "ADMIN" && user?.admin) { + return { isAdmin: true } + } +}; \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1256413..fd1a512 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,5 +1,20 @@ - +{#if ($page.url.pathname === "/") || (data.isLoggedIn === false || data.needsRegisterFollowup === true)} +
+{:else if data.isStudent === true} +Dashboard +Search +{:else if data.isAdmin === true} +Dashboard +Approvals +{/if} + +
+ \ No newline at end of file diff --git a/src/routes/admindashboard/+page.server.ts b/src/routes/admindashboard/+page.server.ts index 6442433..803b5c6 100644 --- a/src/routes/admindashboard/+page.server.ts +++ b/src/routes/admindashboard/+page.server.ts @@ -1,4 +1,4 @@ -import { redirect } from "@sveltejs/kit"; +import { error, redirect } from "@sveltejs/kit"; import type { Equipment, Facility, Student, User } from "@prisma/client"; import db from "$lib/prisma"; import type { PageServerLoad } from "./$types"; @@ -8,69 +8,63 @@ export const load: PageServerLoad = async (event) => { redirect(302, "/login"); } - let returnInformation: { - userInfo: Omit, - facilities: ({ admins: { user: Pick }[]; } & Omit & { image: string })[] | null, - equipments: ({ admins: { user: Pick }[]; } & Omit & { image: string })[] | null - } = { - userInfo: { ...event.locals.user }, - facilities: null, - equipments: null - } - const user = await db.user.findUnique({ select: { role: true, student: true, admin: true }, where: { id: event.locals.user.id } }) if (user?.role == "STUDENT" && user?.student) { - redirect(302, "/dashboard"); + throw error(401, "Unauthorized") } if (!user?.admin && !user?.student) { redirect(302, "/register/next"); } - if (user?.role == "ADMIN" && user?.admin) { - const response = await db.admin.findUnique({ - select: { - facilities: { - include: { - admins: { - select: { - user: { select: { firstName: true, lastName: true, id: true } } - } + if (!(user?.role == "ADMIN" || user?.admin)) { + throw error(401, "Unauthorized") + } + + const response = await db.admin.findUnique({ + select: { + facilities: { + include: { + admins: { + select: { + user: { select: { firstName: true, lastName: true, id: true } } } } - }, - equipments: { - include: { - admins: { - select: { - user: { select: { firstName: true, lastName: true, id: true } } - } + } + }, + equipments: { + include: { + admins: { + select: { + user: { select: { firstName: true, lastName: true, id: true } } } } } - }, - where: { userId: event.locals.user.id } - }) + } + }, + where: { userId: event.locals.user.id } + }) - returnInformation.facilities = (response?.facilities.map(facility => { - const { image, ...otherProps } = facility; - return { - ...otherProps, - image, - }; - })) ?? null - returnInformation.equipments = (response?.equipments.map(equipment => { - const { image, ...otherProps } = equipment; - return { - ...otherProps, - image, - }; - })) ?? null - } + const requests = await db.request.findMany({ + where: { admins: { some: { userId: event.locals.user.id } } }, + select: { + id: true, + requestStatus: true, + facility: { select: { name: true, id: true, blockedDates: true } }, + equipment: { select: { name: true, id: true, blockedDates: true } }, + students: { include: { user: { select: { firstName: true, lastName: true, id: true } } } }, + admins: { include: { user: { select: { firstName: true, lastName: true, id: true } } } } + } + }) - return returnInformation; + return { + facilities: response?.facilities, + equipments: response?.equipments, + requests, + userInfo: { ...event.locals.user } + }; }; \ No newline at end of file diff --git a/src/routes/admindashboard/+page.svelte b/src/routes/admindashboard/+page.svelte index 5b33faf..cb9f152 100644 --- a/src/routes/admindashboard/+page.svelte +++ b/src/routes/admindashboard/+page.svelte @@ -1,6 +1,5 @@ @@ -25,13 +24,7 @@ .join(", ")} {facility.department} - {facility.name} + {facility.name} {/each} {/if} @@ -61,11 +54,7 @@ > {equipment.department} {equipment.name}{equipment.name} {/each} diff --git a/src/routes/approvals/+page.server.ts b/src/routes/approvals/+page.server.ts new file mode 100644 index 0000000..397982e --- /dev/null +++ b/src/routes/approvals/+page.server.ts @@ -0,0 +1,188 @@ +import db from "$lib/prisma"; +import { error, redirect } from "@sveltejs/kit"; +import type { Actions, PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async (event) => { + if (!event.locals.user) { + redirect(302, "/login"); + } + + const user = await db.user.findUnique({ + select: { role: true, student: true, admin: true }, + where: { id: event.locals.user.id } + }) + + if (!user) { + throw error(401, "Unauthorized") + } + + if (user.role == "STUDENT" && user.student) { + throw error(401, "Unauthorized") + } + + if (!user.admin && !user.student) { + redirect(302, "/register/next"); + } + + if (!(user.role == "ADMIN" || user.admin)) { + throw error(401, "Unauthorized") + } + + const requests = await db.request.findMany({ + where: { admins: { some: { userId: event.locals.user.id } } }, + select: { + id: true, + requestStatus: true, + requestDates: true, + description: true, + facility: { select: { name: true, id: true, blockedDates: true, image: true } }, + equipment: { select: { name: true, id: true, blockedDates: true, image: true } }, + students: { + include: { + user: { select: { firstName: true, lastName: true, id: true } }, + section: { select: { grade: true, name: true, id: true } } + } + }, + admins: { include: { user: { select: { firstName: true, lastName: true, id: true } } } } + } + }) + + return { requests, user } +} + +export const actions = { + approve: async ({ request }) => { + const formData = Object.fromEntries(await request.formData()) + //{ itemId: '8', type: 'facility', adminId: '24' } + const { type, adminId, requestId } = formData as { + type: "facility" | "equipment", + adminId: string, + requestId: string + } + + if (!type || !adminId || !requestId) { + throw error(400, "Invalid request, missing fields") + } + + const parsedAdminId = Number.parseInt(adminId) + const parsedRequestId = Number.parseInt(requestId) + + if (Number.isNaN(parsedAdminId) || Number.isNaN(parsedRequestId)) { + throw error(400, "Invalid request, wrong fields") + } + + const placeRequest = await db.request.findUnique({ + where: { id: parsedRequestId }, + select: { + requestStatus: true, + facility: true, + equipment: true, + requestDates: true, + id: true + } + }) + + if (!placeRequest || !placeRequest.requestStatus) { + throw error(400, "Invalid request, no request found") + } + + if (!placeRequest.equipment && !placeRequest.facility) { + throw error(400, "Invalid request, wrong request fields") + } + + let adminStatus = placeRequest?.requestStatus.find(status => status.adminId === parsedAdminId) + if (!adminStatus) { + throw error(400, "Invalid request, no admin found") + } + adminStatus.requestStatus = "APPROVED" + + await db.request.update({ + where: { id: parsedRequestId }, + data: { + requestStatus: placeRequest?.requestStatus + } + }) + + if (!(placeRequest?.requestStatus.map(status => status.requestStatus).every(status => status === "APPROVED"))) return + + if (type === "facility") { + let blockedDates = Array.isArray(placeRequest.facility?.blockedDates) ? placeRequest.facility?.blockedDates : []; + blockedDates = blockedDates.concat(placeRequest.requestDates) + + const newFacility = await db.facility.update({ + where: { id: placeRequest.facility?.id }, + data: { + blockedDates + } + }) + + console.log(`New facility: ${newFacility}`) + } else if (type === "equipment") { + let blockedDates = Array.isArray(placeRequest.facility?.blockedDates) ? placeRequest.facility?.blockedDates : []; + blockedDates = blockedDates.concat(placeRequest.requestDates) + + const newEquipment = await db.equipment.update({ + where: { id: placeRequest.equipment?.id }, + data: { + blockedDates + } + }) + + console.log(`New equipment: ${newEquipment}`) + } else { + throw error(400, "Invalid request, wrong type") + } + }, + deny: async ({ request }) => { + const formData = Object.fromEntries(await request.formData()) + //{ itemId: '8', type: 'facility', adminId: '24' } + const { type, adminId, requestId } = formData as { + type: "facility" | "equipment", + adminId: string, + requestId: string + } + + if (!type || !adminId || !requestId) { + throw error(400, "Invalid request") + } + + const parsedAdminId = Number.parseInt(adminId) + const parsedRequestId = Number.parseInt(requestId) + + if (Number.isNaN(parsedAdminId) || Number.isNaN(parsedRequestId)) { + throw error(400, "Invalid request") + } + + const placeRequest = await db.request.findUnique({ + where: { id: parsedRequestId }, + select: { + requestStatus: true, + facility: true, + equipment: true, + requestDates: true, + id: true + } + }) + + if (!placeRequest || !placeRequest.requestStatus) { + throw error(400, "Invalid request") + } + + if (!placeRequest.equipment && !placeRequest.facility) { + throw error(400, "Invalid request") + } + + let adminStatus = placeRequest?.requestStatus.find(status => status.adminId === parsedAdminId) + if (!adminStatus) { + throw error(400, "Invalid request") + } + adminStatus.requestStatus = "REJECTED" + + await db.request.update({ + where: { id: parsedRequestId }, + data: { + requestStatus: placeRequest?.requestStatus + } + }) + } +} satisfies Actions; \ No newline at end of file diff --git a/src/routes/approvals/+page.svelte b/src/routes/approvals/+page.svelte new file mode 100644 index 0000000..fbffb28 --- /dev/null +++ b/src/routes/approvals/+page.svelte @@ -0,0 +1,113 @@ + + +For your approvals: + + + + + {#each items as item (item.requestId)} + + + + + + + + + + {/each} + +
{item.place.name}Requested for: +
    + {#each item.requestDates as requestDate} +
  • + {new Date(requestDate.start).toLocaleDateString()} - {new Date( + requestDate.end + ).toLocaleDateString()} +
  • + {:else} + No dates given. + {/each} +
+
+ {#each item.students as student (student.id)} +

{student.name} ({student.section})

+ {/each} +
+ {#each item.adminsStatus as admin (admin.id)} +

+ {admin.name} + {admin.id === data.user.admin?.id ? "(YOU)" : ""} - {admin.status} +

+ {/each} +
+ {#if item.description === "" || item.description.length === 0} + No description given. + {:else} +

{item.description}

+ {/if} +
+ {#if item.adminsStatus.find((admin) => admin.id === data.user.admin?.id)?.status === "REJECTED"} +

DENIED

+ {:else if item.adminsStatus.find((admin) => admin.id === data.user.admin?.id)?.status === "APPROVED"} +

APPROVED

+ {:else} +
+ + + +
+ +
+
+ +
+
+ {/if} +
diff --git a/src/routes/dashboard/+page.svelte b/src/routes/dashboard/+page.svelte index 2167d47..11f4953 100644 --- a/src/routes/dashboard/+page.svelte +++ b/src/routes/dashboard/+page.svelte @@ -1,10 +1,31 @@
+

Looking for something to roam about?

+
{ + const { query, term } = searchQuery(searchTerm, searchCategory, $page); + searchTerm = term; + if (query != "") goto(`/search?${query}`); + }} + > + + + +
+

Welcome, {data.userInfo.firstName}!


Section: {data?.sectionInfo?.section ?? "null"}

@@ -30,7 +51,9 @@ Recent bookings:
    +
    +