Added trainer create/delete weeklytimeslot view and endpoint
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
5eacfe7167
commit
0f8d73b4cc
@ -1,4 +1,11 @@
|
|||||||
export interface Timeslot {
|
export interface Timeslot {
|
||||||
startDate: Date
|
startDate: Date
|
||||||
endDate: Date
|
endDate: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeeklyTimeslot {
|
||||||
|
id: number
|
||||||
|
day_of_week: number
|
||||||
|
start_time: string
|
||||||
|
end_time: string
|
||||||
}
|
}
|
||||||
@ -0,0 +1,201 @@
|
|||||||
|
<template>
|
||||||
|
<div class="Schedule">
|
||||||
|
<div class="days">
|
||||||
|
<div class="day" v-for="day in days">
|
||||||
|
{{ day.name }}
|
||||||
|
<div class="timeslots">
|
||||||
|
<div class="timeslot" v-for="timeslot in day.timeslots" @click="deleteTimeslot(timeslot.id)">
|
||||||
|
{{ timeslot.start_time.split(":").slice(0, 2).join(":") }} -
|
||||||
|
{{ timeslot.end_time.split(":").slice(0, 2).join(":") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="newTimeslot">
|
||||||
|
<label class="startLabel" for="startHour">Start</label>
|
||||||
|
<input class="startHour" type="number" placeholder="HH" name="startHour" min="0" max="23"
|
||||||
|
v-model="newDayInput[day.day_of_week - 1].startHour" />
|
||||||
|
<input class="startMinute" type="number" placeholder="MM" name="startMinute" min="0" max="59"
|
||||||
|
v-model="newDayInput[day.day_of_week - 1].startMinute" />
|
||||||
|
<label class="endLabel" for="endHour">Slut</label>
|
||||||
|
<input class="endHour" type="number" placeholder="HH" name="endHour" min="0" max="23"
|
||||||
|
v-model="newDayInput[day.day_of_week - 1].endHour" />
|
||||||
|
<input class="endMinute" type="number" placeholder="MM" name="endMinute" min="0" max="59"
|
||||||
|
v-model="newDayInput[day.day_of_week - 1].endMinute" />
|
||||||
|
<input class="submit" type="submit" name="submit" value="Submit" @click="addTimeslot(day.day_of_week)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.Schedule {
|
||||||
|
margin: 30px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newTimeslot {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(5, 1fr);
|
||||||
|
grid-column-gap: 0px;
|
||||||
|
grid-row-gap: 0px;
|
||||||
|
width: fit-content;
|
||||||
|
margin: auto;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startLabel {
|
||||||
|
grid-area: 1 / 1 / 2 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startHour {
|
||||||
|
grid-area: 2 / 1 / 3 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startMinute {
|
||||||
|
grid-area: 2 / 2 / 3 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endLabel {
|
||||||
|
grid-area: 3 / 1 / 4 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endHour {
|
||||||
|
grid-area: 4 / 1 / 5 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endMinute {
|
||||||
|
grid-area: 4 / 2 / 5 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
grid-area: 5 / 1 / 6 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslots {
|
||||||
|
border-top: 1px solid #cbd5e1;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslot {
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslot:hover {
|
||||||
|
border-color: lightcoral;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import LocalizedFormat from "dayjs/plugin/localizedFormat"
|
||||||
|
import utc from "dayjs/plugin/utc"
|
||||||
|
import { } from "dayjs/locale/da";
|
||||||
|
import type { WeeklyTimeslot } from '@/interfaces/timeslot';
|
||||||
|
|
||||||
|
dayjs.extend(LocalizedFormat);
|
||||||
|
dayjs.extend(utc);
|
||||||
|
|
||||||
|
interface Day {
|
||||||
|
name: string
|
||||||
|
timeslots: WeeklyTimeslot[]
|
||||||
|
day_of_week: number
|
||||||
|
}
|
||||||
|
interface NewDayInput {
|
||||||
|
startHour: number | undefined
|
||||||
|
startMinute: number | undefined
|
||||||
|
endHour: number | undefined
|
||||||
|
endMinute: number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TrainerSchedule",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newDayInput: Array.from({ length: 7 }, () => {
|
||||||
|
return {
|
||||||
|
startHour: undefined,
|
||||||
|
startMinute: undefined,
|
||||||
|
endHour: undefined,
|
||||||
|
endMinute: undefined
|
||||||
|
}
|
||||||
|
}) as NewDayInput[],
|
||||||
|
weeklyTimeslots: [] as WeeklyTimeslot[]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.fetchSchedule();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
days(): Day[] {
|
||||||
|
const days: Day[] = [
|
||||||
|
{ day_of_week: 1, name: "Mandag", timeslots: [] },
|
||||||
|
{ day_of_week: 2, name: "Tirsdag", timeslots: [] },
|
||||||
|
{ day_of_week: 3, name: "Onsdag", timeslots: [] },
|
||||||
|
{ day_of_week: 4, name: "Torsdag", timeslots: [] },
|
||||||
|
{ day_of_week: 5, name: "Fredag", timeslots: [] },
|
||||||
|
{ day_of_week: 6, name: "Lørdag", timeslots: [] },
|
||||||
|
{ day_of_week: 7, name: "Søndag", timeslots: [] }
|
||||||
|
];
|
||||||
|
|
||||||
|
this.weeklyTimeslots.forEach((timeslot: WeeklyTimeslot) => {
|
||||||
|
days[timeslot.day_of_week - 1].timeslots.push(timeslot);
|
||||||
|
});
|
||||||
|
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async addTimeslot(day_of_week: number) {
|
||||||
|
console.log(this.newDayInput[day_of_week - 1]);
|
||||||
|
const input: NewDayInput = this.newDayInput[day_of_week - 1];
|
||||||
|
const start_time = `${input.startHour}:${input.startMinute ? input.startMinute : 0}`;
|
||||||
|
const end_time = `${input.endHour}:${input.endMinute ? input.endMinute : 0}`;
|
||||||
|
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/trainer/timeslot/`, {
|
||||||
|
credentials: import.meta.env.DEV ? "include" : undefined,
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
day_of_week,
|
||||||
|
start_time,
|
||||||
|
end_time
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "Application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.status === 401 || res.status === 403) {
|
||||||
|
this.$router.push({ path: "/login", query: { ref: this.$route.path } });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchSchedule();
|
||||||
|
},
|
||||||
|
async deleteTimeslot(id: number) {
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/trainer/timeslot/${id}`, {
|
||||||
|
credentials: import.meta.env.DEV ? "include" : undefined,
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
if (res.status === 401 || res.status === 403) {
|
||||||
|
this.$router.push({ path: "/login", query: { ref: this.$route.path } });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchSchedule();
|
||||||
|
},
|
||||||
|
async fetchSchedule() {
|
||||||
|
// TODO: remove these credentials
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/trainer/timeslot`, import.meta.env.DEV ? { credentials: "include" } : {});
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.weeklyTimeslots = await res.json();
|
||||||
|
} else if (res.status === 401 || res.status === 403) {
|
||||||
|
this.$router.push({ path: "/login", query: { ref: this.$route.path } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import express, { Express, Router } from "express";
|
||||||
|
|
||||||
|
const router: Router = express.Router();
|
||||||
|
import trainer from "./trainer";
|
||||||
|
import weeklyTimeslot from "./weeklyTimeslot";
|
||||||
|
|
||||||
|
router.use(weeklyTimeslot);
|
||||||
|
router.use(trainer);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import express, { Router, Request, Response } from "express";
|
import express, { Router, Request, Response } from "express";
|
||||||
import Joi from "joi"
|
import Joi from "joi"
|
||||||
|
|
||||||
import { client } from "../db";
|
import { client } from "../../db";
|
||||||
import { DatabaseError } from "pg";
|
import { DatabaseError } from "pg";
|
||||||
import Trainer from "../interfaces/trainer";
|
import Trainer from "../../interfaces/trainer";
|
||||||
import { AdminAuth } from "../middlewares/auth";
|
import { AdminAuth } from "../../middlewares/auth";
|
||||||
import { trainerExists } from "../middlewares/trainer";
|
import { trainerExists } from "../../middlewares/trainer";
|
||||||
import { idSchema } from "../schemas";
|
import { idSchema } from "../../schemas";
|
||||||
|
|
||||||
const router: Router = express.Router();
|
const router: Router = express.Router();
|
||||||
|
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
import express, { Router, Response } from "express";
|
||||||
|
import Joi from "joi"
|
||||||
|
|
||||||
|
import { client } from "../../db";
|
||||||
|
import { DatabaseError } from "pg";
|
||||||
|
import { TrainerAuth } from "../../middlewares/auth";
|
||||||
|
import { timeSchema } from "../../schemas";
|
||||||
|
import { AuthedRequest } from "../../interfaces/auth";
|
||||||
|
import { WeeklyTimeslot } from "../../interfaces/timeslot";
|
||||||
|
|
||||||
|
const router: Router = express.Router();
|
||||||
|
|
||||||
|
async function getTrainerIdFromUserId(userId: number | undefined): Promise<number | undefined> {
|
||||||
|
const trainerLookupResult = await client.query(`
|
||||||
|
SELECT id FROM trainers
|
||||||
|
WHERE user_id = $1;
|
||||||
|
`, [
|
||||||
|
userId
|
||||||
|
]);
|
||||||
|
return trainerLookupResult.rows[0]?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/trainer/timeslot", TrainerAuth, async (req: AuthedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const trainerId: number | undefined = await getTrainerIdFromUserId(req.user?.userId);
|
||||||
|
if (trainerId === undefined)
|
||||||
|
return res.sendStatus(403);
|
||||||
|
|
||||||
|
const databaseResult = await client.query(`
|
||||||
|
SELECT id, day_of_week, start_time, end_time FROM weekly_timeslots
|
||||||
|
WHERE trainer_id = $1;
|
||||||
|
`, [
|
||||||
|
trainerId
|
||||||
|
]);
|
||||||
|
|
||||||
|
const timeslots: WeeklyTimeslot[] = databaseResult.rows;
|
||||||
|
|
||||||
|
return res.status(200).send(timeslots);
|
||||||
|
} catch (error: DatabaseError | Error | any) {
|
||||||
|
console.error(error);
|
||||||
|
return res.sendStatus(500);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const weeklyTimeslotSchema = Joi.object({
|
||||||
|
day_of_week: Joi.number().integer().min(1).max(7).required(),
|
||||||
|
start_time: timeSchema.required(),
|
||||||
|
end_time: timeSchema.required()
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/trainer/timeslot", TrainerAuth, async (req: AuthedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const trainerId: number | undefined = await getTrainerIdFromUserId(req.user?.userId);
|
||||||
|
if (trainerId === undefined)
|
||||||
|
return res.sendStatus(403);
|
||||||
|
|
||||||
|
const validation = weeklyTimeslotSchema.validate(req.body, { abortEarly: false });
|
||||||
|
if (validation.error !== undefined) {
|
||||||
|
return res.status(400).send(validation.error.details);
|
||||||
|
}
|
||||||
|
|
||||||
|
const weeklyTimeslot: WeeklyTimeslot = validation.value;
|
||||||
|
|
||||||
|
const databaseResult = await client.query(`
|
||||||
|
INSERT INTO weekly_timeslots (trainer_id, day_of_week, start_time, end_time)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING *;
|
||||||
|
`, [
|
||||||
|
trainerId,
|
||||||
|
weeklyTimeslot.day_of_week,
|
||||||
|
weeklyTimeslot.start_time,
|
||||||
|
weeklyTimeslot.end_time
|
||||||
|
]);
|
||||||
|
|
||||||
|
const timeslots: WeeklyTimeslot = databaseResult.rows[0];
|
||||||
|
|
||||||
|
return res.status(200).send(timeslots);
|
||||||
|
} catch (error: DatabaseError | Error | any) {
|
||||||
|
console.error(error);
|
||||||
|
return res.sendStatus(500);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.delete("/trainer/timeslot/:id", TrainerAuth, async (req: AuthedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const trainerId: number | undefined = await getTrainerIdFromUserId(req.user?.userId);
|
||||||
|
if (trainerId === undefined)
|
||||||
|
return res.sendStatus(403);
|
||||||
|
|
||||||
|
const databaseResult = await client.query(`
|
||||||
|
DELETE FROM weekly_timeslots
|
||||||
|
WHERE trainer_id = $1
|
||||||
|
AND id = $2;
|
||||||
|
`, [
|
||||||
|
trainerId,
|
||||||
|
req.params.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (databaseResult.rowCount < 1)
|
||||||
|
return res.sendStatus(404);
|
||||||
|
|
||||||
|
return res.sendStatus(204);
|
||||||
|
} catch (error: DatabaseError | Error | any) {
|
||||||
|
console.error(error);
|
||||||
|
return res.sendStatus(500);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router;
|
||||||
Loading…
Reference in New Issue