diff --git a/src/routes/(app)/booking/+page.svelte b/src/routes/(app)/booking/+page.svelte
index 0c188c977..480145e3a 100644
--- a/src/routes/(app)/booking/+page.svelte
+++ b/src/routes/(app)/booking/+page.svelte
@@ -4,7 +4,7 @@
import { isAuthorized } from "$lib/utils/authorization";
import { page } from "$app/stores";
import apiNames from "$lib/utils/apiNames";
- import StatusComponent from "./admin/StatusComponent.svelte";
+ import StatusComponent from "./StatusComponent.svelte";
import dayjs from "dayjs";
import ConfirmDialog from "$lib/components/ConfirmDialog.svelte";
import BookingCalendar from "./BookingCalendar.svelte";
@@ -60,6 +60,7 @@
diff --git a/src/routes/(app)/booking/BookingEditor.svelte b/src/routes/(app)/booking/BookingEditor.svelte
index bf8bd1db2..b554b9879 100644
--- a/src/routes/(app)/booking/BookingEditor.svelte
+++ b/src/routes/(app)/booking/BookingEditor.svelte
@@ -3,15 +3,21 @@
import type { BookingSchema } from "./schema";
import { superForm } from "$lib/utils/client/superForms";
import * as m from "$paraglide/messages";
- import type { Bookable } from "@prisma/client";
+ import type { Bookable, BookingRequest } from "@prisma/client";
+ import StatusComponent from "./StatusComponent.svelte";
+ type BookingRequestWithBookables = BookingRequest & { bookables: Bookable[] };
export let data: {
form: SuperValidated>;
bookables: Bookable[];
+ booking?: BookingRequestWithBookables;
+ allBookingRequests?: BookingRequestWithBookables[];
};
+ $: bookingRequest = data.booking;
+
const { form, errors, enhance, constraints } = superForm(data.form);
- export let mode: "create" | "edit" = "create";
+ export let mode: "create" | "edit" | "review" = "create";
let start = $form.start;
let end = $form.end;
@@ -60,6 +66,23 @@
diff --git a/src/routes/(app)/booking/admin/StatusComponent.svelte b/src/routes/(app)/booking/StatusComponent.svelte
similarity index 92%
rename from src/routes/(app)/booking/admin/StatusComponent.svelte
rename to src/routes/(app)/booking/StatusComponent.svelte
index 844b8e175..92a8c7b35 100644
--- a/src/routes/(app)/booking/admin/StatusComponent.svelte
+++ b/src/routes/(app)/booking/StatusComponent.svelte
@@ -2,10 +2,14 @@
import type { Bookable, BookingRequest } from "@prisma/client";
import dayjs from "dayjs";
import * as m from "$paraglide/messages";
+ import { twMerge } from "tailwind-merge";
type T = BookingRequest & { bookables: Bookable[] };
export let bookingRequest: T;
export let bookingRequests: T[];
+ let clazz: string | undefined = undefined;
+ export { clazz as class };
+
$: otherBookingRequests = bookingRequests.filter(
(br) => br.id !== bookingRequest.id && br.status !== "DENIED",
);
@@ -29,7 +33,7 @@
$: conflictWarning = conflict && !conflictError;
-
+
{#if bookingRequest.status === "ACCEPTED"}
{m.booking_accepted()}
diff --git a/src/routes/(app)/booking/[id]/edit/+page.server.ts b/src/routes/(app)/booking/[id]/edit/+page.server.ts
index 2927a5114..3bf3c8c34 100644
--- a/src/routes/(app)/booking/[id]/edit/+page.server.ts
+++ b/src/routes/(app)/booking/[id]/edit/+page.server.ts
@@ -5,33 +5,14 @@ import { redirect } from "$lib/utils/redirect";
import * as m from "$paraglide/messages";
import { isAuthorized } from "$lib/utils/authorization";
import apiNames from "$lib/utils/apiNames";
-import { error } from "@sveltejs/kit";
-import dayjs from "dayjs";
+import { getBookingRequestOrThrow, getSuperValidatedForm } from "../../utils";
export const load = async ({ locals, params }) => {
const { prisma } = locals;
const bookables = await prisma.bookable.findMany();
- const bookingRequest = await prisma.bookingRequest.findUnique({
- where: { id: params.id },
- include: { bookables: true },
- });
-
- if (!bookingRequest) {
- throw error(404, m.booking_errors_notFound());
- }
-
- const initialData = {
- name: bookingRequest.event ?? undefined,
- start: bookingRequest.start
- ? dayjs(bookingRequest.start).format("YYYY-MM-DDTHH:MM")
- : undefined,
- end: bookingRequest.end
- ? dayjs(bookingRequest.end).format("YYYY-MM-DDTHH:MM")
- : undefined,
- bookables: bookingRequest.bookables?.map((bookable) => bookable.id),
- };
- const form = await superValidate(initialData, zod(bookingSchema));
+ const bookingRequest = await getBookingRequestOrThrow(prisma, params.id);
+ const form = await getSuperValidatedForm(bookingRequest);
return { bookables, form, booking: bookingRequest };
};
diff --git a/src/routes/(app)/booking/admin/+page.server.ts b/src/routes/(app)/booking/admin/+page.server.ts
index 1885ea050..5e76f66e4 100644
--- a/src/routes/(app)/booking/admin/+page.server.ts
+++ b/src/routes/(app)/booking/admin/+page.server.ts
@@ -1,94 +1,15 @@
import apiNames from "$lib/utils/apiNames";
import { authorize } from "$lib/utils/authorization";
-import sendNotification from "$lib/utils/notifications";
-import { NotificationType } from "$lib/utils/notifications/types";
-import dayjs from "dayjs";
-import type { Actions, PageServerLoad } from "./$types";
+import type { PageServerLoad } from "./$types";
+import { actions, getUpcomingBookingRequests } from "../utils";
export const load: PageServerLoad = async ({ locals }) => {
const { prisma, user } = locals;
authorize(apiNames.BOOKINGS.UPDATE, user);
- const bookingRequests = await prisma.bookingRequest.findMany({
- where: {
- start: {
- gte: dayjs().subtract(1, "week").toDate(),
- },
- },
- orderBy: [{ start: "asc" }, { end: "asc" }, { status: "asc" }],
- include: {
- bookables: true,
- booker: true,
- },
- });
+ const bookingRequests = await getUpcomingBookingRequests(prisma);
+
return { bookingRequests };
};
-export const actions: Actions = {
- accept: async (event) => {
- const { request, locals } = event;
- const { prisma, user } = locals;
- const formData = await request.formData();
- const id = formData.get("id");
- if (id && typeof id === "string") {
- await prisma.bookingRequest.update({
- where: { id },
- data: {
- status: "ACCEPTED",
- },
- });
- const request = await prisma.bookingRequest.findFirst({
- where: {
- id,
- },
- select: {
- bookerId: true,
- event: true,
- },
- });
- if (request && request.bookerId != null && user && user.memberId) {
- sendNotification({
- title: "Booking request accepted",
- message: `Your booking request for ${request.event} has been accepted`,
- type: NotificationType.BOOKING_REQUEST,
- link: "/booking",
- memberIds: [request.bookerId],
- fromMemberId: user.memberId,
- });
- }
- }
- },
- reject: async (event) => {
- const { request, locals } = event;
- const { prisma, user } = locals;
- const formData = await request.formData();
- const id = formData.get("id");
- if (id && typeof id === "string") {
- await prisma.bookingRequest.update({
- where: { id },
- data: {
- status: "DENIED",
- },
- });
- const request = await prisma.bookingRequest.findFirst({
- where: {
- id,
- },
- select: {
- bookerId: true,
- event: true,
- },
- });
- if (request && request.bookerId != null && user && user.memberId) {
- sendNotification({
- title: "Booking request denied",
- message: `Your booking request for ${request.event} has been denied`,
- type: NotificationType.BOOKING_REQUEST,
- link: "/booking",
- memberIds: [request.bookerId],
- fromMemberId: user.memberId,
- });
- }
- }
- },
-};
+export { actions };
diff --git a/src/routes/(app)/booking/admin/+page.svelte b/src/routes/(app)/booking/admin/+page.svelte
index 950028d34..f0f9c3f1f 100644
--- a/src/routes/(app)/booking/admin/+page.svelte
+++ b/src/routes/(app)/booking/admin/+page.svelte
@@ -1,6 +1,6 @@
+
+
+
+
+
+
diff --git a/src/routes/(app)/booking/create/+page.server.ts b/src/routes/(app)/booking/create/+page.server.ts
index 5696b30fc..c7b8814e2 100644
--- a/src/routes/(app)/booking/create/+page.server.ts
+++ b/src/routes/(app)/booking/create/+page.server.ts
@@ -5,6 +5,13 @@ import { redirect } from "$lib/utils/redirect";
import * as m from "$paraglide/messages";
import { bookingSchema } from "../schema";
import dayjs from "dayjs";
+import {
+ type Bookable,
+ type BookingRequest,
+ type PrismaClient,
+} from "@prisma/client";
+import sendNotification from "$lib/utils/notifications";
+import { NotificationType } from "$lib/utils/notifications/types";
export const load = async ({ locals }) => {
const { prisma } = locals;
@@ -26,6 +33,41 @@ export const load = async ({ locals }) => {
return { bookables, bookingRequests, form };
};
+const sendNotificationToKM = async (
+ bookingRequest: BookingRequest & { bookables: Bookable[] },
+ prisma: PrismaClient,
+) => {
+ const kallarMastare = await prisma.member.findFirstOrThrow({
+ where: {
+ mandates: {
+ some: {
+ positionId: "dsek.km.mastare",
+ startDate: { lte: new Date() },
+ endDate: { gte: new Date() },
+ },
+ },
+ },
+ });
+
+ const booker = (await prisma.member.findUnique({
+ where: {
+ id: bookingRequest.bookerId ?? undefined,
+ },
+ })) ?? { firstName: "Unknown", lastName: "" };
+
+ const bookablesString = bookingRequest.bookables
+ .map((bookable) => bookable.nameEn)
+ .join(", ");
+
+ await sendNotification({
+ title: `New booking request: ${bookingRequest.event}`,
+ message: `${booker.firstName} ${booker.lastName} wants to book '${bookablesString}' from ${dayjs(bookingRequest.start).format("DD/MM HH:mm")} until ${dayjs(bookingRequest.end).format("DD/MM HH:mm")}.`,
+ type: NotificationType.BOOKING_REQUEST,
+ link: `/booking/admin/${bookingRequest.id}`,
+ memberIds: [kallarMastare.id],
+ });
+};
+
export const actions = {
default: async (event) => {
const { request, locals } = event;
@@ -35,7 +77,7 @@ export const actions = {
if (!form.valid) return fail(400, { form });
const { start, end, name, bookables } = form.data;
- await prisma.bookingRequest.create({
+ const createdRequest = await prisma.bookingRequest.create({
data: {
bookerId: user?.memberId,
start: new Date(start),
@@ -48,6 +90,11 @@ export const actions = {
},
status: "PENDING",
},
+ include: { bookables: true },
+ });
+
+ await sendNotificationToKM(createdRequest, prisma).catch((e) => {
+ console.log("Failed sending notifications to KM: ", e);
});
throw redirect(
diff --git a/src/routes/(app)/booking/utils.ts b/src/routes/(app)/booking/utils.ts
new file mode 100644
index 000000000..2f9a2dfa3
--- /dev/null
+++ b/src/routes/(app)/booking/utils.ts
@@ -0,0 +1,104 @@
+import sendNotification from "$lib/utils/notifications";
+import { NotificationType } from "$lib/utils/notifications/types";
+import { error, type RequestEvent } from "@sveltejs/kit";
+import type { Actions } from "./$types";
+import dayjs from "dayjs";
+import type { Bookable, BookingRequest, PrismaClient } from "@prisma/client";
+import { superValidate } from "sveltekit-superforms/server";
+import { zod } from "sveltekit-superforms/adapters";
+import { bookingSchema } from "./schema";
+import * as m from "$paraglide/messages";
+
+export const actions: Actions = {
+ accept: async (event: RequestEvent) => {
+ await performAction(event, true);
+ },
+ reject: async (event: RequestEvent) => {
+ await performAction(event, false);
+ },
+};
+
+export async function getUpcomingBookingRequests(prisma: PrismaClient) {
+ return prisma.bookingRequest.findMany({
+ where: {
+ start: {
+ gte: dayjs().subtract(1, "week").toDate(),
+ },
+ },
+ orderBy: [{ start: "asc" }, { end: "asc" }, { status: "asc" }],
+ include: {
+ bookables: true,
+ booker: true,
+ },
+ });
+}
+
+export async function getBookingRequestOrThrow(
+ prisma: PrismaClient,
+ id: string,
+) {
+ return prisma.bookingRequest
+ .findUniqueOrThrow({
+ where: { id },
+ include: { bookables: true },
+ })
+ .catch(() => {
+ throw error(404, m.booking_errors_notFound());
+ });
+}
+
+export async function getSuperValidatedForm(
+ bookingRequest: BookingRequest & { bookables: Bookable[] },
+) {
+ const initialData = {
+ name: bookingRequest.event ?? undefined,
+ start: bookingRequest.start
+ ? dayjs(bookingRequest.start).format("YYYY-MM-DDTHH:mm")
+ : undefined,
+ end: bookingRequest.end
+ ? dayjs(bookingRequest.end).format("YYYY-MM-DDTHH:mm")
+ : undefined,
+ bookables: bookingRequest.bookables?.map((bookable) => bookable.id),
+ };
+ return await superValidate(initialData, zod(bookingSchema));
+}
+
+async function performAction(event: RequestEvent, accepted: boolean) {
+ const { request, locals } = event;
+ const { prisma, user } = locals;
+ const formData = await request.formData();
+ const id = formData.get("id");
+ const status = accepted ? "ACCEPTED" : "DENIED";
+
+ if (id && typeof id === "string") {
+ await prisma.bookingRequest.update({
+ where: {
+ id,
+ },
+ data: {
+ status,
+ },
+ });
+
+ const request = await prisma.bookingRequest.findFirst({
+ where: {
+ id,
+ },
+ select: {
+ bookerId: true,
+ event: true,
+ },
+ });
+
+ if (request && request.bookerId != null && user && user.memberId) {
+ sendNotification({
+ title: `Booking request ${status.toLowerCase()}`,
+ message: `Your booking request for ${request.event} has been ${status.toLowerCase()}`,
+ type: NotificationType.BOOKING_REQUEST,
+ link: `/booking`,
+ memberIds: [request.bookerId],
+ fromMemberId: user.memberId,
+ });
+ }
+ }
+}
diff --git a/src/translations/en.json b/src/translations/en.json
index 8965f25cd..1e2dfaf78 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -763,6 +763,7 @@
"nollning_wikia_literature_body": "Facebook groupo where a lot of second-hand course literature is being sold by older students, it's a private group but everyone is allowed to join, just ask a phadder to approve you.",
"members_errors_tooLargePicture": "Profile picture too big, max size is {size}.",
"nollning_events_ticketCTA": "Ticket/Register",
+ "booking_reviewBooking": "Review booking",
"home_volunteer": "Volunteer",
"committees_cafe_openinghours": "Opening hours",
"committees_cafe_thecafe": "The Cafe",
diff --git a/src/translations/sv.json b/src/translations/sv.json
index 031325a96..046f47bf7 100644
--- a/src/translations/sv.json
+++ b/src/translations/sv.json
@@ -30,8 +30,8 @@
"access": "Åtkomst",
"doors": "Dörrar",
"emailAliases": "Mejlaliaser",
- "alerts": "Globala meddelanden",
"linkShortener": "Länkförkortare",
+ "alerts": "Globala meddelanden",
"adminSettings": "Admininställningar",
"files": "Filer",
"phadderGroups": "Phaddergrupper",
@@ -759,6 +759,7 @@
"nollning_wikia_literature_body": "Facebookgrupp där mycket kurslitteratur säljs av äldre studenter, det är en privat grupp men alla får gå med, be en phadder godkänna dig.",
"members_errors_tooLargePicture": "Profilbild är för stor, den får max vara {size}.",
"nollning_events_ticketCTA": "Biljett/Anmälan",
+ "booking_reviewBooking": "Granska bokning",
"home_volunteer": "Funktionär",
"committees_cafe_openinghours": "Öppettider",
"committees_cafe_thecafe": "Caféet",
|