import express, { Router, Request, Response, NextFunction } from "express"; import Joi from "joi" import dayjs from "dayjs" import isoWeek from "dayjs/plugin/isoWeek" import utc from "dayjs/plugin/utc" import { client } from "../db"; import { DatabaseError } from "pg"; import Trainer from "../interfaces/trainer"; import { ReservedTimeslots, Timeslot, WeeklyTimeslot } from "../interfaces/timeslot"; import { idSchema, timeSchema } from "../schemas"; dayjs.extend(isoWeek); dayjs.extend(utc); const router: Router = express.Router(); const timeslotFiltersSchema = Joi.object({ trainer: Joi.array().single().items( idSchema.required() ), center: idSchema, startDate: Joi.date().default(Date.now()), endDate: Joi.date().default(Date.now()), startTime: timeSchema, endTime: timeSchema }); interface TimeslotFilters { trainer?: number[] center?: number startDate: string endDate: string startTime?: string endTime?: string } interface DatabaseResult { trainer: Trainer timeslots: WeeklyTimeslot[] reserved_timeslots: ReservedTimeslots[] } interface TrainerWithAvailableTimeslots extends Trainer { timeslots: Timeslot[] } router.get("/timeslot", async (req: Request, res: Response) => { try { const validation = timeslotFiltersSchema.validate(req.query, { abortEarly: false, stripUnknown: true }); if (validation.error !== undefined) { return res.status(400).send(validation.error.details); } const timeslotFilters: TimeslotFilters = validation.value; let filterStartDate: dayjs.Dayjs = dayjs(timeslotFilters.startDate).utcOffset(0).startOf("date"); const filterEndDate: dayjs.Dayjs = dayjs(timeslotFilters.endDate).utcOffset(0).startOf("date"); const nowDate: dayjs.Dayjs = dayjs().utcOffset(0).startOf("date"); if (filterStartDate.isBefore(nowDate)) { filterStartDate = nowDate; } let weekdays = []; for (let day: dayjs.Dayjs = filterStartDate; !day.isAfter(filterEndDate); day = day.add(1, "day")) { weekdays.push(day.isoWeekday()); } const queryResult = await client.query(` SELECT weekly_timeslots.trainer_id, json_build_object( 'id',weekly_timeslots.trainer_id, 'first_name',users.first_name, 'last_name',users.last_name, 'center_id',trainers.center_id, 'center_name',centers.name ) as trainer, json_agg(json_build_object( 'day_of_week',day_of_week, 'start_time',start_time, 'end_time',end_time) ) as timeslots, ( SELECT json_agg(json_build_object( 'id', id, 'start_time', start_time, 'end_time', end_time )) FROM public.reserved_timeslots WHERE (start_time between $1 AND $2 OR end_time between $1 AND $2) AND weekly_timeslots.trainer_id = reserved_timeslots.trainer_id ) as reserved_timeslots FROM weekly_timeslots JOIN trainers ON weekly_timeslots.trainer_id = trainers.id JOIN users ON trainers.user_id = users.id JOIN centers ON trainers.center_id = centers.id WHERE ((trainer_id = ANY($3)) OR $4) AND (day_of_week = ANY($5)) AND (trainers.center_id = $6 OR $7) GROUP BY weekly_timeslots.trainer_id, users.first_name, users.last_name, trainers.center_id, centers.name, trainer_id; `, [ new Date(timeslotFilters.startDate).toISOString(), new Date(timeslotFilters.endDate).toISOString(), timeslotFilters.trainer !== undefined ? timeslotFilters.trainer : [], timeslotFilters.trainer === undefined, weekdays, timeslotFilters.center, timeslotFilters.center === undefined ]); const databaseResult: DatabaseResult[] = queryResult.rows; const trainers: TrainerWithAvailableTimeslots[] = []; for (const trainer of databaseResult) { const trainerWithAvailableTimeslots: TrainerWithAvailableTimeslots = { ...trainer.trainer, timeslots: [] } for (let day: dayjs.Dayjs = filterStartDate; !day.isAfter(filterEndDate); day = day.add(1, "day")) { const weekDay = day.isoWeekday(); const reservedTimeslots = trainer.reserved_timeslots !== null ? trainer.reserved_timeslots : []; timeslots: for (const timeslot of trainer.timeslots) { if (timeslot.day_of_week !== weekDay) continue timeslots; if (timeslotFilters.startTime) { if (parseInt(timeslotFilters.startTime.split(":")[0]) > parseInt(timeslot.start_time.split(":")[0])) continue timeslots; if (timeslotFilters.startTime.split(":")[0] === timeslot.start_time.split(":")[0] && parseInt(timeslotFilters.startTime.split(":")[1]) > parseInt(timeslot.start_time.split(":")[1])) continue timeslots; } if (timeslotFilters.endTime) { if (parseInt(timeslotFilters.endTime.split(":")[0]) < parseInt(timeslot.end_time.split(":")[0])) continue timeslots; if (timeslotFilters.endTime.split(":")[0] === timeslot.end_time.split(":")[0] && parseInt(timeslotFilters.endTime.split(":")[1]) < parseInt(timeslot.end_time.split(":")[1])) continue timeslots; } const startTime = day.clone() .hour(parseInt(timeslot.start_time.split(":")[0])) .minute(parseInt(timeslot.start_time.split(":")[1])) .second(parseInt(timeslot.start_time.split(":")[2])); const endTime = day.clone() .hour(parseInt(timeslot.end_time.split(":")[0])) .minute(parseInt(timeslot.end_time.split(":")[1])) .second(parseInt(timeslot.end_time.split(":")[2])); for (const reservedTimeslot of reservedTimeslots) { const reservedTimeStart = dayjs(reservedTimeslot.start_time); const reservedTimeEnd = dayjs(reservedTimeslot.end_time); if ((!reservedTimeStart.isBefore(startTime) && !reservedTimeStart.isAfter(endTime)) || (!reservedTimeEnd.isBefore(startTime) && !reservedTimeStart.isAfter(endTime))) { continue timeslots; } } trainerWithAvailableTimeslots.timeslots.push({ startDate: startTime.toDate(), endDate: endTime.toDate() }); } } trainers.push(trainerWithAvailableTimeslots); } return res.status(200).send(trainers); } catch (error: DatabaseError | Error | any) { console.error(error); return res.sendStatus(500); } }) export default router;