Added trainer order handling views and endpoints
parent
720fe60821
commit
0713b98baf
@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<div class="popup">
|
||||||
|
<div class="TrainerNewTime" @click.stop>
|
||||||
|
<VDatePicker class="dateSelector" v-model.range="range" mode="datetime" :rules="datePickerRules" is24hr
|
||||||
|
timezone="utc" :input-debounce="50" />
|
||||||
|
<div class="submitButton" @click="selectTimeslot()">
|
||||||
|
Vælg
|
||||||
|
</div>
|
||||||
|
<div class="closeButton" @click="$emit('closePopup')">x</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Timeslot } from '@/interfaces/timeslot';
|
||||||
|
import type { Order } from '@/interfaces/order';
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import LocalizedFormat from "dayjs/plugin/localizedFormat"
|
||||||
|
import utc from "dayjs/plugin/utc"
|
||||||
|
import { } from "dayjs/locale/da";
|
||||||
|
|
||||||
|
dayjs.extend(LocalizedFormat);
|
||||||
|
dayjs.extend(utc);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Home",
|
||||||
|
emits: ["closePopup", "refreshList"],
|
||||||
|
props: {
|
||||||
|
order: {
|
||||||
|
type: Object as PropType<Order>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
range: {
|
||||||
|
start: dayjs().set("minute", 0).toDate(),
|
||||||
|
end: dayjs().set("minute", 0).toDate()
|
||||||
|
},
|
||||||
|
datePickerRules: {
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0
|
||||||
|
},
|
||||||
|
timeslots: [] as Timeslot[]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async selectTimeslot() {
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/trainer/order/${this.order.id}`, {
|
||||||
|
credentials: import.meta.env.DEV ? "include" : undefined,
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({ startDate: this.range.start, endDate: this.range.end }),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "Application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.status === 401) {
|
||||||
|
this.$router.push({ path: "/login", query: { ref: this.$route.path } });
|
||||||
|
}
|
||||||
|
else if (res.status === 400) {
|
||||||
|
const data = await res.json();
|
||||||
|
alert(data[0].message)
|
||||||
|
}
|
||||||
|
else if (res.status === 204) {
|
||||||
|
this.$emit("refreshList");
|
||||||
|
this.$emit("closePopup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #000a;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TrainerNewTime {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 50px;
|
||||||
|
margin: 50px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 20px;
|
||||||
|
font-size: 2em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div class="popup" @click="$emit('closePopup')">
|
||||||
|
<div class="Order" @click.stop v-show="!showNewTimes">
|
||||||
|
<div class="trainer">
|
||||||
|
{{ order.trainer.first_name }} {{ order.trainer.last_name }}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
{{ order.trainer.center_name }}
|
||||||
|
</div>
|
||||||
|
<div class="date">
|
||||||
|
{{ formatDate(order.startDate) }}
|
||||||
|
</div>
|
||||||
|
<div class="time">
|
||||||
|
{{ formatTime(order.startDate) }} -
|
||||||
|
{{ formatTime(order.endDate) }}
|
||||||
|
</div>
|
||||||
|
<div class="price">
|
||||||
|
{{ formatPrice(order.price) }}
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="moveButton" @click="moveOrder()" v-if="showButtons">
|
||||||
|
Flyt tid
|
||||||
|
</div>
|
||||||
|
<div class="cancelButton" @click="cancelOrder(order.id)" v-if="showButtons">
|
||||||
|
Aflys
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="closeButton" @click="$emit('closePopup')">x</div>
|
||||||
|
</div>
|
||||||
|
<TrainerNewTime :order="order" @closePopup="$emit('closePopup')" @refreshList="$emit('refreshList')" v-if="showNewTimes"></TrainerNewTime>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import TrainerNewTime from '@/components/TrainerNewTime.vue'
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import LocalizedFormat from "dayjs/plugin/localizedFormat"
|
||||||
|
import utc from "dayjs/plugin/utc"
|
||||||
|
import { } from "dayjs/locale/da";
|
||||||
|
import type { Order } from '@/interfaces/order';
|
||||||
|
|
||||||
|
dayjs.extend(LocalizedFormat);
|
||||||
|
dayjs.extend(utc);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "UserOrderPopup",
|
||||||
|
emits: ["closePopup", "refreshList"],
|
||||||
|
components: {
|
||||||
|
TrainerNewTime
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
order: {
|
||||||
|
type: Object as PropType<Order>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showNewTimes: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showButtons(): boolean {
|
||||||
|
console.log(this.order.status !== 'CancelledByTrainer' , this.order.status !== 'CancelledByUser' , (new Date(this.order.startDate.split("Z")[0])).getTime() > (new Date()).getTime());
|
||||||
|
console.log(this.order.startDate.split("Z")[0]);
|
||||||
|
console.log(1, new Date(this.order.startDate), new Date(this.order.startDate.split("Z")[0]));
|
||||||
|
|
||||||
|
console.log(new Date(this.order.startDate.split("Z")[0]), new Date());
|
||||||
|
return this.order.status !== 'CancelledByTrainer' && this.order.status !== 'CancelledByUser' && (new Date(this.order.startDate.split("Z")[0])).getTime() > (new Date()).getTime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
moveOrder() {
|
||||||
|
this.showNewTimes = true;
|
||||||
|
},
|
||||||
|
async cancelOrder(id: number) {
|
||||||
|
if (id === undefined) return;
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/trainer/order/${id}/cancel`, {
|
||||||
|
credentials: import.meta.env.DEV ? "include" : undefined,
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
if (res.status === 401) {
|
||||||
|
this.$router.push({ path: "/login", query: { ref: this.$route.path } });
|
||||||
|
}
|
||||||
|
else if (res.status === 204) {
|
||||||
|
this.$emit('refreshList');
|
||||||
|
this.$emit('closePopup');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate(date: string): string {
|
||||||
|
let output: string = dayjs(date).locale("da").format("dddd D. MMM YYYY");
|
||||||
|
let outputStringArray = output.split("");
|
||||||
|
outputStringArray[0] = outputStringArray[0].toLocaleUpperCase();
|
||||||
|
output = outputStringArray.join("");
|
||||||
|
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
formatTime(date: string): string {
|
||||||
|
let output: string = dayjs(date).utcOffset(0).locale("da").format("HH:mm");
|
||||||
|
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
formatPrice(price: number): string {
|
||||||
|
const DanishKrone = new Intl.NumberFormat('dk', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'DKK',
|
||||||
|
});
|
||||||
|
|
||||||
|
return DanishKrone.format(price / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #000a;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Order {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 50%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
align-self: flex-end;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelButton {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moveButton {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moveButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 20px;
|
||||||
|
font-size: 2em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div class="Orders">
|
||||||
|
<div class="order" v-for="order of orders" :key="order.id" @click="openPopup(order)"
|
||||||
|
:class="{ cancelled: order.status === 'CancelledByTrainer' || order.status === 'CancelledByUser' }">
|
||||||
|
<div class="time">
|
||||||
|
{{ formatDate(order.startDate) }} {{ formatTime(order.startDate) }} - {{ formatTime(order.endDate) }}
|
||||||
|
</div>
|
||||||
|
<div class="status">
|
||||||
|
{{ formatStatus(order.status) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TrainerOrderPopup @closePopup="closePopup" @refreshList="fetchOrders" :order="selectedOrder" v-if="showPopup">
|
||||||
|
</TrainerOrderPopup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import TrainerOrderPopup from '@/components/TrainerOrderPopup.vue'
|
||||||
|
import type { Order } from '@/interfaces/order';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import LocalizedFormat from "dayjs/plugin/localizedFormat"
|
||||||
|
import utc from "dayjs/plugin/utc"
|
||||||
|
import { } from "dayjs/locale/da";
|
||||||
|
|
||||||
|
dayjs.extend(LocalizedFormat);
|
||||||
|
dayjs.extend(utc);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TrainerOrdersView",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
orders: [] as Order[],
|
||||||
|
showPopup: false,
|
||||||
|
selectedOrder: {} as Order
|
||||||
|
};
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
TrainerOrderPopup
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closePopup() {
|
||||||
|
this.showPopup = false;
|
||||||
|
},
|
||||||
|
openPopup(order: Order) {
|
||||||
|
this.selectedOrder = order;
|
||||||
|
this.showPopup = true;
|
||||||
|
},
|
||||||
|
async fetchOrders() {
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/trainer/order`, {
|
||||||
|
credentials: import.meta.env.DEV ? "include" : undefined
|
||||||
|
});
|
||||||
|
if (res.status === 401 || res.status === 403) {
|
||||||
|
this.$router.push({ path: "/login", query: { ref: this.$route.path } });
|
||||||
|
}
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.orders = await res.json();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate(date: string): string {
|
||||||
|
let output: string = dayjs(date).locale("da").format("dddd D. MMM YYYY");
|
||||||
|
let outputStringArray = output.split("");
|
||||||
|
outputStringArray[0] = outputStringArray[0].toLocaleUpperCase();
|
||||||
|
output = outputStringArray.join("");
|
||||||
|
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
formatTime(date: string): string {
|
||||||
|
let output: string = dayjs(date).utcOffset(0).locale("da").format("HH:mm");
|
||||||
|
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
formatStatus(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case "Successful":
|
||||||
|
return "Bestilt"
|
||||||
|
|
||||||
|
case "Processing":
|
||||||
|
return "Afventer betaling"
|
||||||
|
|
||||||
|
case "CancelledByTrainer":
|
||||||
|
return "Aflyst af dig"
|
||||||
|
|
||||||
|
case "CancelledByUser":
|
||||||
|
return "Aflyst af brugeren"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchOrders();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.Orders {
|
||||||
|
display: flex;
|
||||||
|
/* flex-direction: column; */
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order:hover {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.order.cancelled .status {
|
||||||
|
color: lightcoral;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,258 @@
|
|||||||
|
import express, { Router, Request, Response } from "express";
|
||||||
|
import Joi from "joi";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import { client } from "../../db";
|
||||||
|
import { DatabaseError } from "pg";
|
||||||
|
import { AuthedRequest } from "../../interfaces/auth";
|
||||||
|
import Trainer from "../../interfaces/trainer";
|
||||||
|
import { OrderObject, OrderObjectStatus, OrderStatus, PaymentIntentStatus } from "../../interfaces/order";
|
||||||
|
import { TrainerAuth } from "../../middlewares/auth";
|
||||||
|
|
||||||
|
const router: Router = express.Router();
|
||||||
|
|
||||||
|
interface CancelOrderLookup {
|
||||||
|
order_status: OrderStatus
|
||||||
|
payment_intent_status: PaymentIntentStatus
|
||||||
|
start_time: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/trainer/order", TrainerAuth, async (req: AuthedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const databaseResult = await client.query(`
|
||||||
|
SELECT
|
||||||
|
orders.id,
|
||||||
|
order_status,
|
||||||
|
price,
|
||||||
|
created_at,
|
||||||
|
payment_intents.status as payment_intent_status,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
json_build_object(
|
||||||
|
'id',trainers.id,
|
||||||
|
'first_name',users.first_name,
|
||||||
|
'last_name',users.last_name,
|
||||||
|
'center_id',trainers.center_id,
|
||||||
|
'center_name',centers.name
|
||||||
|
) as trainer
|
||||||
|
FROM orders
|
||||||
|
LEFT JOIN reserved_timeslots ON reserved_timeslots.id = orders.timeslot_id
|
||||||
|
LEFT JOIN payment_intents ON payment_intents.id = orders.payment_intent
|
||||||
|
LEFT JOIN trainers ON trainers.id = reserved_timeslots.trainer_id
|
||||||
|
LEFT JOIN centers on trainers.center_id = centers.id
|
||||||
|
LEFT JOIN users on users.id = trainers.user_id
|
||||||
|
WHERE
|
||||||
|
trainers.user_id = $1
|
||||||
|
AND (
|
||||||
|
orders.order_status = 'Confirmed'
|
||||||
|
OR orders.order_status = 'CancelledByTrainer'
|
||||||
|
OR orders.order_status = 'CancelledByUser'
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
reserved_timeslots.start_time DESC;
|
||||||
|
`, [
|
||||||
|
req.user?.userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
const orders: OrderObject[] = databaseResult.rows.map((order): OrderObject => {
|
||||||
|
const paymentStatus = order.payment_intent_status === "Successful" ? "Successful" : "Processing"
|
||||||
|
|
||||||
|
let status: OrderObjectStatus = paymentStatus;
|
||||||
|
if (order.order_status === "CancelledByTrainer")
|
||||||
|
status = "CancelledByTrainer"
|
||||||
|
else if (order.order_status === "CancelledByUser")
|
||||||
|
status = "CancelledByUser"
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: order.id,
|
||||||
|
trainer: order.trainer,
|
||||||
|
startDate: dayjs(order.start_time).format('YYYY-MM-DDTHH:mm:ss[Z]'),
|
||||||
|
endDate: dayjs(order.end_time).format('YYYY-MM-DDTHH:mm:ss[Z]'),
|
||||||
|
status: status,
|
||||||
|
price: order.price,
|
||||||
|
created_at: order.created_at,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(orders);
|
||||||
|
} catch (error: DatabaseError | Error | any) {
|
||||||
|
console.error(error);
|
||||||
|
return res.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ChangeOrderLookup {
|
||||||
|
order_status: OrderStatus
|
||||||
|
start_time: Date
|
||||||
|
end_time: Date
|
||||||
|
trainer_id: number
|
||||||
|
timeslot_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderBody {
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderSchema = Joi.object({
|
||||||
|
startDate: Joi.date().min("now").required(),
|
||||||
|
endDate: Joi.date().min(Joi.ref("startDate")).required()
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put("/trainer/order/:id", TrainerAuth, async (req: AuthedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const lookupResult = await client.query(`
|
||||||
|
SELECT
|
||||||
|
order_status,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
trainer_id,
|
||||||
|
timeslot_id
|
||||||
|
FROM orders
|
||||||
|
LEFT JOIN payment_intents ON payment_intents.id = orders.payment_intent
|
||||||
|
LEFT JOIN reserved_timeslots ON reserved_timeslots.id = orders.timeslot_id
|
||||||
|
LEFT JOIN trainers ON trainers.id = reserved_timeslots.trainer_id
|
||||||
|
WHERE orders.id = $1
|
||||||
|
AND trainers.user_id = $2
|
||||||
|
AND order_status = 'Confirmed';
|
||||||
|
`, [
|
||||||
|
req.params.id,
|
||||||
|
req.user?.userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (lookupResult.rows.length !== 1) {
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const order: ChangeOrderLookup = lookupResult.rows[0];
|
||||||
|
|
||||||
|
const originalStartDate: dayjs.Dayjs = dayjs(order.start_time).utcOffset(0);
|
||||||
|
const originalEndDate: dayjs.Dayjs = dayjs(order.end_time).utcOffset(0);
|
||||||
|
|
||||||
|
/** The duration of the original timeslot in miliseconds */
|
||||||
|
const originalDuration: number = originalEndDate.diff(originalStartDate);
|
||||||
|
|
||||||
|
const validation = orderSchema.validate(req.body, { abortEarly: false });
|
||||||
|
if (validation.error !== undefined) {
|
||||||
|
return res.status(400).send(validation.error.details);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderBody: OrderBody = validation.value;
|
||||||
|
|
||||||
|
const startDate: dayjs.Dayjs = dayjs(orderBody.startDate).utcOffset(0);
|
||||||
|
const endDate: dayjs.Dayjs = dayjs(orderBody.endDate).utcOffset(0);
|
||||||
|
|
||||||
|
const startTime: string = `${startDate.hour()}:${startDate.minute()}`;
|
||||||
|
const endTime: string = `${endDate.hour()}:${endDate.minute()}`;
|
||||||
|
|
||||||
|
const weekday = startDate.isoWeekday();
|
||||||
|
|
||||||
|
const timeslotValidQuery = await client.query(`
|
||||||
|
select
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM public.reserved_timeslots
|
||||||
|
JOIN orders ON reserved_timeslots.id = orders.timeslot_id
|
||||||
|
WHERE
|
||||||
|
((start_time >= $2 AND start_time < $3)
|
||||||
|
OR (end_time > $2 AND end_time <= $3))
|
||||||
|
AND trainer_id = $1
|
||||||
|
AND (
|
||||||
|
orders.order_status = 'Confirmed'
|
||||||
|
OR orders.order_status = 'Created'
|
||||||
|
)
|
||||||
|
AND reserved_timeslots.id != $4
|
||||||
|
) as time_already_reserved;
|
||||||
|
`, [
|
||||||
|
order.trainer_id,
|
||||||
|
startDate.toISOString(),
|
||||||
|
endDate.toISOString(),
|
||||||
|
order.timeslot_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
const time_already_reserved: boolean = timeslotValidQuery.rows[0].time_already_reserved;
|
||||||
|
|
||||||
|
if (time_already_reserved)
|
||||||
|
return res.status(400).send([{
|
||||||
|
message: "timeslot is not available",
|
||||||
|
path: [
|
||||||
|
"startDate"
|
||||||
|
],
|
||||||
|
type: "startDate.invalid"
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const duration = endDate.diff(startDate);
|
||||||
|
|
||||||
|
if (duration !== originalDuration)
|
||||||
|
return res.status(400).send([{
|
||||||
|
message: "timeslot is not same length as original",
|
||||||
|
path: [
|
||||||
|
"startDate"
|
||||||
|
],
|
||||||
|
type: "startDate.invalid"
|
||||||
|
}]);
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
UPDATE reserved_timeslots SET
|
||||||
|
start_time = $1,
|
||||||
|
end_time = $2
|
||||||
|
WHERE id = $3;
|
||||||
|
`, [
|
||||||
|
startDate.toISOString(),
|
||||||
|
endDate.toISOString(),
|
||||||
|
order.timeslot_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return res.sendStatus(204);
|
||||||
|
} catch (error: DatabaseError | Error | any) {
|
||||||
|
console.error(error);
|
||||||
|
return res.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/trainer/order/:id/cancel", TrainerAuth, async (req: AuthedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const lookupResult = await client.query(`
|
||||||
|
SELECT
|
||||||
|
order_status,
|
||||||
|
payment_intents.status as payment_intent_status,
|
||||||
|
start_time
|
||||||
|
FROM orders
|
||||||
|
LEFT JOIN payment_intents ON payment_intents.id = orders.payment_intent
|
||||||
|
LEFT JOIN reserved_timeslots ON reserved_timeslots.id = orders.timeslot_id
|
||||||
|
LEFT JOIN trainers ON trainers.id = reserved_timeslots.trainer_id
|
||||||
|
WHERE orders.id = $1
|
||||||
|
AND trainers.user_id = $2
|
||||||
|
AND order_status = 'Confirmed';
|
||||||
|
`, [
|
||||||
|
req.params.id,
|
||||||
|
req.user?.userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (lookupResult.rows.length !== 1) {
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const order: CancelOrderLookup = lookupResult.rows[0];
|
||||||
|
|
||||||
|
const dateValidation = Joi.date().min(Date.now()).validate(order.start_time)
|
||||||
|
if (dateValidation.error !== undefined) {
|
||||||
|
return res.status(400).send({ message: "Timeslot has already occurred" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
UPDATE orders SET
|
||||||
|
order_status = 'CancelledByTrainer'
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
|
`, [
|
||||||
|
req.params.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return res.sendStatus(204);
|
||||||
|
} catch (error: DatabaseError | Error | any) {
|
||||||
|
console.error(error);
|
||||||
|
return res.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
Loading…
Reference in New Issue