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">
|
||||
<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