Added available timeslot overview and filter selector
parent
f6c5e0f0c4
commit
8d3e683402
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div class="centerSelector">
|
||||||
|
<div class="header">
|
||||||
|
<div>Center</div>
|
||||||
|
</div>
|
||||||
|
<div class="centers">
|
||||||
|
<div class="center" v-for="center in centers" :key="center.id" v-on:click="clicked(center.id)"
|
||||||
|
v-bind:class="{ 'selected': selectedCenters.includes(center.id) }">
|
||||||
|
{{ center.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Center } from '@/interfaces/center';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "CenterSelector",
|
||||||
|
emits: ["selectionChanged"],
|
||||||
|
async mounted() {
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/center`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
// @ts-ignore
|
||||||
|
this.centers = await res.json();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
centers: [] as Center[],
|
||||||
|
selectedCenters: [] as number[]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clicked(id: number) {
|
||||||
|
if (this.selectedCenters.includes(id)) {
|
||||||
|
this.selectedCenters = this.selectedCenters.filter(c => c !== id);
|
||||||
|
} else {
|
||||||
|
this.selectedCenters.push(id);
|
||||||
|
}
|
||||||
|
this.$emit("selectionChanged", this.selectedCenters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.centerSelector {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
min-width: fit-content;
|
||||||
|
width: 300px;
|
||||||
|
/* padding: 5px; */
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid #cbd5e1;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header div {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centers {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center.selected {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}</style>
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="trainerSelector">
|
||||||
|
<div class="header">
|
||||||
|
<div>Trainer</div>
|
||||||
|
</div>
|
||||||
|
<div class="trainers">
|
||||||
|
<div class="trainer" v-for="trainer in trainers" :key="trainer.id" v-on:click="clicked(trainer.id)"
|
||||||
|
v-bind:class="{ 'selected': selectedTrainers.includes(trainer.id) }">
|
||||||
|
{{ trainer.first_name }} {{ trainer.last_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Trainer } from '@/interfaces/trainer';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TrainerSelector",
|
||||||
|
emits: ["selectionChanged"],
|
||||||
|
props: {
|
||||||
|
centers: Array<number>
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
const filters = this.centers?.map(c => `center=${c}`).join("&");
|
||||||
|
console.log(filters)
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/trainer?${filters}`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.trainers = await res.json();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
centers: {
|
||||||
|
async handler(centers: number[]) {
|
||||||
|
const filters = centers?.map(c => `center=${c}`).join("&");
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/trainer?${filters}`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.trainers = await res.json();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
trainers: [] as Trainer[],
|
||||||
|
selectedTrainers: [] as number[]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clicked(id: number) {
|
||||||
|
if (this.selectedTrainers.includes(id)) {
|
||||||
|
this.selectedTrainers = this.selectedTrainers.filter(c => c !== id);
|
||||||
|
} else {
|
||||||
|
this.selectedTrainers.push(id);
|
||||||
|
}
|
||||||
|
this.$emit("selectionChanged", this.selectedTrainers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trainerSelector {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
min-width: fit-content;
|
||||||
|
width: 300px;
|
||||||
|
/* padding: 5px; */
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid #cbd5e1;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header div {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainers {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer.selected {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
export interface Center {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
city: string
|
||||||
|
zip_code: number
|
||||||
|
address: string
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export interface Timeslot {
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
export interface Trainer {
|
||||||
|
id: number
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
center_id: number
|
||||||
|
center_name: string
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import type { Timeslot } from "./timeslot";
|
||||||
|
import type { Trainer } from "./trainer";
|
||||||
|
|
||||||
|
export interface TrainerWithTimeslots extends Trainer {
|
||||||
|
timeslots: Timeslot[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrainerWithDateGroupedTimeslots extends Trainer {
|
||||||
|
dates: DateGroupedTimeslotsList
|
||||||
|
timeslots: Timeslot[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DateGroupedTimeslotsList {
|
||||||
|
[key: string]: DateGroupedTimeslots
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DateGroupedTimeslots {
|
||||||
|
timeslots: Timeslot[]
|
||||||
|
date: string
|
||||||
|
}
|
||||||
@ -1,3 +1,213 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="Home">
|
||||||
</template>
|
<VDatePicker class="dateSelector" v-model.range="range" mode="date" :rules="datePickerRules" timezone="utc"
|
||||||
|
:input-debounce="50" />
|
||||||
|
<CenterSelector class="centerSelector" @selectionChanged="centerSelectionChanged"></CenterSelector>
|
||||||
|
<TrainerSelector class="trainerSelector" v-bind:centers="centers" @selectionChanged="trainerSelectionChanged">
|
||||||
|
</TrainerSelector>
|
||||||
|
<div class="trainers">
|
||||||
|
<div class="trainer" v-for="trainer in trainersWithDateGroupedTimeslots" :key="trainer.id">
|
||||||
|
<div class="top">
|
||||||
|
{{ trainer.first_name }} {{ trainer.last_name }}
|
||||||
|
</div>
|
||||||
|
<div class="dates">
|
||||||
|
<div class="date" v-for="date in trainer.dates">
|
||||||
|
{{ date.date }}
|
||||||
|
<div class="timeslots">
|
||||||
|
<div class="timeslot" v-for="timeslot in date.timeslots"
|
||||||
|
@click="createOrder(trainer, timeslot)">
|
||||||
|
{{ formatTime(timeslot.startDate) }} - {{ formatTime(timeslot.endDate) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- {{ trainersWithTimeslots }} -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.Home {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 3fr 1fr;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
grid-column-gap: 50px;
|
||||||
|
grid-row-gap: 30px;
|
||||||
|
padding: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateSelector {
|
||||||
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centerSelector {
|
||||||
|
grid-area: 1 / 3 / 2 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainerSelector {
|
||||||
|
grid-area: 2 / 3 / 3 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainers {
|
||||||
|
grid-area: 1 / 2 / 4 / 3;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer .top {
|
||||||
|
border-bottom: 1px solid #cbd5e1;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import CenterSelector from '@/components/CenterSelector.vue';
|
||||||
|
import TrainerSelector from '@/components/TrainerSelector.vue';
|
||||||
|
import type { Timeslot } from '@/interfaces/timeslot';
|
||||||
|
import type { DateGroupedTimeslots, DateGroupedTimeslotsList, TrainerWithDateGroupedTimeslots, TrainerWithTimeslots } from '@/interfaces/trainerWithTimeslots';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import LocalizedFormat from "dayjs/plugin/localizedFormat"
|
||||||
|
import { } from "dayjs/locale/da";
|
||||||
|
import type { Trainer } from '@/interfaces/trainer';
|
||||||
|
|
||||||
|
dayjs.extend(LocalizedFormat);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Home",
|
||||||
|
components: {
|
||||||
|
CenterSelector,
|
||||||
|
TrainerSelector
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
range: {
|
||||||
|
start: new Date(),
|
||||||
|
end: new Date()
|
||||||
|
},
|
||||||
|
datePickerRules: {
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0
|
||||||
|
},
|
||||||
|
centers: [] as number[],
|
||||||
|
trainers: [] as number[],
|
||||||
|
trainersWithTimeslots: [] as TrainerWithTimeslots[]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
trainersWithDateGroupedTimeslots() {
|
||||||
|
return this.trainersWithTimeslots.map(trainer => {
|
||||||
|
const dates: DateGroupedTimeslotsList = {};
|
||||||
|
trainer.timeslots.forEach(timeslot => {
|
||||||
|
console.log(new Date(timeslot.startDate).toISOString().split("T")[0]);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
const trainerWithDateGroupedTimeslots: TrainerWithDateGroupedTimeslots = { ...trainer, dates };
|
||||||
|
return trainerWithDateGroupedTimeslots;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
centers: {
|
||||||
|
handler() {
|
||||||
|
this.fetchTimeslots();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
trainers: {
|
||||||
|
handler() {
|
||||||
|
this.fetchTimeslots();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
range: {
|
||||||
|
handler() {
|
||||||
|
this.fetchTimeslots();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
createOrder(trainer: Trainer, timeslot: Timeslot) {
|
||||||
|
console.log(trainer);
|
||||||
|
console.log(timeslot);
|
||||||
|
this.$router.push({
|
||||||
|
path: "/createOrder", query: {
|
||||||
|
trainer: trainer.id,
|
||||||
|
startDate: new Date(timeslot.startDate).toISOString(),
|
||||||
|
endDate: new Date(timeslot.endDate).toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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).locale("da").format("HH:mm");
|
||||||
|
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
centerSelectionChanged(selection: number[]) {
|
||||||
|
this.centers = selection;
|
||||||
|
},
|
||||||
|
trainerSelectionChanged(selection: number[]) {
|
||||||
|
this.trainers = selection;
|
||||||
|
},
|
||||||
|
async fetchTimeslots() {
|
||||||
|
const centerFilters = this.centers.map(c => `center=${c}`);
|
||||||
|
const trainerFilters = this.trainers.map(c => `trainer=${c}`);
|
||||||
|
const startDateFilter = `startDate=${this.range.start.toISOString()}`;
|
||||||
|
const endDateFilter = `endDate=${this.range.end.toISOString()}`;
|
||||||
|
const filters = [...centerFilters, ...trainerFilters, startDateFilter, endDateFilter].join("&");
|
||||||
|
console.log(filters);
|
||||||
|
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/timeslot?${filters}`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.trainersWithTimeslots = await res.json();
|
||||||
|
console.log(this.trainersWithTimeslots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue