Added user view for orders with cancel and new time.
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Fixed some issues with wrong timeslots on the backend.main
parent
38a34b3c56
commit
2ef1023a90
@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="popup">
|
||||
<div class="UserNewTime" @click.stop>
|
||||
<VDatePicker class="dateSelector" v-model.range="range" mode="date" :rules="datePickerRules" timezone="utc"
|
||||
:input-debounce="50" />
|
||||
<div class="dates">
|
||||
<div class="date" v-for="date in dateGroupedTimeslots">
|
||||
{{ date.date }}
|
||||
<div class="timeslots">
|
||||
<div class="timeslot" v-for="timeslot in date.timeslots" @click="selectTimeslot(timeslot)">
|
||||
{{ formatTime(timeslot.startDate) }} - {{ formatTime(timeslot.endDate) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="closeButton" @click="$emit('closePopup')">x</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Timeslot } from '@/interfaces/timeslot';
|
||||
import type { DateGroupedTimeslotsList } from '@/interfaces/trainerWithTimeslots';
|
||||
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: new Date(),
|
||||
end: dayjs().add(7, "days").toDate()
|
||||
},
|
||||
datePickerRules: {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
milliseconds: 0
|
||||
},
|
||||
timeslots: [] as Timeslot[]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dateGroupedTimeslots() {
|
||||
const dates: DateGroupedTimeslotsList = {};
|
||||
this.timeslots.forEach(timeslot => {
|
||||
const day: string = new Date(timeslot.startDate).toISOString().split("T")[0];
|
||||
if (!dates[day]) {
|
||||
dates[day] = {
|
||||
date: this.formatDate(new Date(day)),
|
||||
timeslots: [] as Timeslot[]
|
||||
};
|
||||
}
|
||||
dates[day].timeslots.push(timeslot);
|
||||
});
|
||||
return dates;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
range: {
|
||||
handler() {
|
||||
this.fetchTimeslots();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async selectTimeslot(timeslot: Timeslot) {
|
||||
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/order/${this.order.id}`, {
|
||||
credentials: import.meta.env.DEV ? "include" : undefined,
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ startDate: timeslot.startDate, endDate: timeslot.endDate }),
|
||||
headers: {
|
||||
"Content-Type": "Application/json"
|
||||
}
|
||||
});
|
||||
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: Date): 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: Date): string {
|
||||
let output: string = dayjs(date).utcOffset(0).locale("da").format("HH:mm");
|
||||
|
||||
return output;
|
||||
},
|
||||
getSearchQueryFilters(): string {
|
||||
const startDateFilter = `startDate=${this.range.start.toISOString()}`;
|
||||
const endDateFilter = `endDate=${this.range.end.toISOString()}`;
|
||||
const filters = [startDateFilter, endDateFilter].join("&");
|
||||
return filters;
|
||||
},
|
||||
async fetchTimeslots() {
|
||||
const filters = this.getSearchQueryFilters();
|
||||
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/order/${this.order.id}/newtimeslots?${filters}`, {
|
||||
credentials: import.meta.env.DEV ? "include" : undefined
|
||||
});
|
||||
if (res.status === 200) {
|
||||
this.timeslots = await res.json();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.UserNewTime {
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 30px;
|
||||
min-width: 50%;
|
||||
margin: 50px;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-template-rows: auto auto;
|
||||
grid-column-gap: 50px;
|
||||
grid-row-gap: 30px;
|
||||
}
|
||||
|
||||
.dates {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.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: black;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
font-size: 2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</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">
|
||||
Afbestil
|
||||
</div>
|
||||
</div>
|
||||
<div class="closeButton" @click="$emit('closePopup')">x</div>
|
||||
</div>
|
||||
<UserNewTime :order="order" @closePopup="$emit('closePopup')" @refreshList="$emit('refreshList')" v-if="showNewTimes"></UserNewTime>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import UserNewTime from '@/components/UserNewTime.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: {
|
||||
UserNewTime
|
||||
},
|
||||
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}/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,11 @@
|
||||
import type { Trainer } from "./trainer"
|
||||
|
||||
export interface Order {
|
||||
id: number
|
||||
startDate: string
|
||||
endDate: string
|
||||
createdAt: string
|
||||
status: string
|
||||
price: number
|
||||
trainer: Trainer
|
||||
}
|
||||
@ -1,23 +1,140 @@
|
||||
<template>
|
||||
<div class="Orders">
|
||||
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="trainer">
|
||||
{{ order.trainer.first_name }} {{ order.trainer.last_name }}
|
||||
</div>
|
||||
<div class="center">
|
||||
{{ order.trainer.center_name }}
|
||||
</div>
|
||||
<div class="time">
|
||||
{{ formatDate(order.startDate) }} {{ formatTime(order.startDate) }} - {{ formatTime(order.endDate) }}
|
||||
</div>
|
||||
<div class="status">
|
||||
{{ formatStatus(order.status) }}
|
||||
</div>
|
||||
</div>
|
||||
<UserOrderPopup @closePopup="closePopup" @refreshList="fetchOrders" :order="selectedOrder" v-if="showPopup">
|
||||
</UserOrderPopup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import UserOrderPopup from '@/components/UserOrderPopup.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: "UserOrdersView",
|
||||
data() {
|
||||
return {
|
||||
orders: [] as Order[],
|
||||
showPopup: false,
|
||||
selectedOrder: {} as Order
|
||||
};
|
||||
},
|
||||
components: {
|
||||
UserOrderPopup
|
||||
},
|
||||
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}/orders`, {
|
||||
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/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 træneren"
|
||||
|
||||
case "CancelledByUser":
|
||||
return "Aflyst af dig"
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchOrders();
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<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>
|
||||
Loading…
Reference in New Issue