diff --git a/src/components/InputComponents/DatepickerRange/CalendarComponent.vue b/src/components/InputComponents/DatepickerRange/CalendarComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..ebcf3a1f2342f548f2490316e31fa0a32d5e41a8 --- /dev/null +++ b/src/components/InputComponents/DatepickerRange/CalendarComponent.vue @@ -0,0 +1,313 @@ +<template> + <div class="calendar"> + <div + class=" + grid grid-cols-7 + py-2 + mt-0.5 + border-b border-black border-opacity-10 + dark:border-litepie-secondary-700 dark:border-opacity-100 + " + > + <div class="months" v-for="day in days" :key="day">{{ day }}</div> + </div> + <div class="grid grid-cols-7 gap-y-0.5 my-1"> + <div + class="days blocked" + v-for="index in daysBeforeStartOfMonth" + :key="index" + > + <button> + {{ daysInMonthBeforeMonth - (daysBeforeStartOfMonth - index) }} + </button> + </div> + <div + class="days relative" + v-for="index in daysInMonth" + :key="index" + :class="{ + disabled: this.isBlocked(index), + selected: this.isDayStartDate(index) && !this.endDate, + start: this.isDayStartDate(index), + end: this.isDayEndDate(index), + error: !!( + this.isBlocked(index) && this.isDayBetweenStartAndEndDate(index) + ), + between: this.isDayBetweenStartAndEndDate(index), + }" + > + <span class="overlay absolute inset-0"></span> + <button v-on:Click="selectDate(index)">{{ index }}</button> + </div> + <div + class="days blocked" + v-for="index in daysRemainingInLastWeek" + :key="index" + > + <button>{{ index }}</button> + </div> + </div> + <p v-if="doesDaysSelectedContainBlockedDays()" class="text-red-500"> + {{ errorMessage }} + </p> + </div> +</template> + +<script> +export default { + props: { + month: { + type: Date, + required: true, + }, + blockedDaysRange: { + type: Array, + default: () => [], + }, + start: { + type: Date, + default: () => null, + }, + end: { + type: Date, + default: () => null, + }, + errorMessage: { + type: String, + default: () => "You cannot select a blocked day", + }, + }, + data() { + return { + days: ["Man", "Tir", "Ons", "Tor", "Fre", "Lør", "Søn"], + }; + }, + computed: { + monthDate() { + return new Date(this.month); + }, + startDate() { + return this.start ? new Date(this.start) : null; + }, + endDate() { + return this.end ? new Date(this.end) : null; + }, + blockedDays() { + const blockedDays = []; + this.blockedDaysRange.forEach((blockedDay) => { + if (blockedDay.length === 2) { + const start = new Date(blockedDay[0]); + const end = new Date(blockedDay[1]); + + // Check if the start or end dates range ends in the current month or after the current month + if(start.getMonth() <= this.monthDate.getMonth() && end.getMonth() >= this.monthDate.getMonth()) { + if(start.getMonth() < this.monthDate.getMonth() && end.getMonth() > this.monthDate.getMonth()) { + // Add all days of month to the list + for(let i = 1; i <= this.daysInMonth; i++) { + blockedDays.push(i); + } + } else if(start.getMonth() === this.monthDate.getMonth()) { + // Add all days of start month to the list + for(let i = start.getDate(); i <= this.daysInMonth; i++) { + blockedDays.push(i); + } + } else if(end.getMonth() === this.monthDate.getMonth()) { + // Add all days of end month to the list + for(let i = 1; i <= end.getDate(); i++) { + blockedDays.push(i); + } + } + } + } else { + // check that day is in this month + const day = new Date(blockedDay[0]); + if (day.getMonth() !== this.monthDate.getMonth()) return; + blockedDays.push(day.getDate()); + } + }); + return blockedDays; + }, + daysInMonth() { + return new Date( + this.monthDate.getFullYear(), + this.monthDate.getMonth() + 1, + 0 + ).getDate(); + }, + daysRemainingInLastWeek() { + return ( + 7 - + new Date( + new Date(this.month).getFullYear(), + new Date(this.month).getMonth() + 1, + 0 + ).getDay() + ); + }, + daysInMonthBeforeMonth() { + let daysInMonth = new Date( + new Date(this.month).getFullYear(), + new Date(this.month).getMonth(), + 0 + ).getDate(); + return daysInMonth; + }, + daysBeforeStartOfMonth() { + let firstDayOfMonth = new Date( + new Date(this.month).getFullYear(), + new Date(this.month).getMonth(), + 1 + ); + let dayOfWeek = firstDayOfMonth.getDay(); + let daysBefore = dayOfWeek === 0 ? 6 : dayOfWeek - 1; + return daysBefore; + }, + }, + methods: { + isDateInMonth(date) { + if (!date) return null; + return ( + date.getFullYear() === this.monthDate.getFullYear() && + date.getMonth() === this.monthDate.getMonth() + ); + }, + isDayStartDate(day) { + // Check that start day is in month + if (!this.isDateInMonth(this.startDate)) return false; + return this.startDate && this.startDate.getDate() === day; + }, + isDayEndDate(day) { + // Check that end day is in month + if (!this.isDateInMonth(this.endDate)) return false; + return this.endDate && this.endDate.getDate() === day; + }, + selectDate(day) { + const date = new Date( + this.monthDate.getFullYear(), + this.monthDate.getMonth(), + day + ); + this.$emit("selectDate", date); + }, + isBlocked(day) { + return this.blockedDays.includes(day); + }, + isDayBlocked(day) { + return this.blockedDays.includes(day); + }, + isDayBetweenBlocked(day) { + return this.blockedDaysRange.some(([start, end]) => { + return start <= day && day <= end; + }); + }, + // Get date from day and month and check if it is between start and end + isDayBetweenStartAndEndDate(day) { + if (!this.startDate || !this.endDate) return false; + const date = new Date( + this.monthDate.getFullYear(), + this.monthDate.getMonth(), + day + ); + return ( + this.startDate.getTime() < date.getTime() && + date.getTime() < this.endDate.getTime() + ); + }, + doesDaysSelectedContainBlockedDays() { + return false; + }, + }, +}; +</script> + +<style scoped> +.calendar { + max-width: 400px; +} + +.months { + font-size: small; + text-align: center; +} +.days { + font-size: small; + text-align: center; +} + +.blocked button { + cursor: not-allowed; + color: gray; +} + +.disabled { + cursor: not-allowed; +} + +.disabled button { + cursor: not-allowed; +} + +button { + background-color: transparent; + width: 3rem; + height: 3rem; +} + +.disabled span { + background-color: red; + border-radius: 5px; + opacity: 0.6; +} + +.disabled button { + color: black; +} + +.selected.start span { + background-color: blue; + opacity: 0.8; + border-radius: 5px; +} + +.start span { + background-color: blue; + opacity: 0.8; + border-radius: 10px 0 0 10px; +} + +.start button { + color: white; + font-weight: bold; +} + +.end button { + color: white; + font-weight: bold; +} + +.between span { + background-color: lightblue; + opacity: 0.8; +} + +.error span { + background-color: red; + opacity: 0.7; + border-radius: 0; +} + +.end button { + color: white; + font-weight: bold; +} + +.end span { + background-color: blue; + opacity: 0.8; + border-radius: 0 10px 10px 0; +} + +.overlay { + z-index: -1; +} +</style> \ No newline at end of file diff --git a/src/components/InputComponents/DatepickerRange/DatepickerRange.vue b/src/components/InputComponents/DatepickerRange/DatepickerRange.vue new file mode 100644 index 0000000000000000000000000000000000000000..018a201804c698ac1ab40b9054be4bc5a3bed0cb --- /dev/null +++ b/src/components/InputComponents/DatepickerRange/DatepickerRange.vue @@ -0,0 +1,276 @@ +<template> + <div> + <div class="input" v-on:click="openCalendar()"> + <label> + <input type="text" v-model="value" placeholder="HEI HEI"> + </label> + </div> + <div class="picker" :style="style"> + <div class="calenders"> + <div> + <month-selector + @back="back" + type="month" + @forward="forward('month')" + :month="month" + ></month-selector> + <calendar-component + :month="month" + :start="startDate" + :end="endDate" + @selectDate="selectDate" + :blockedDaysRange="blockedDaysRange" + ></calendar-component> + </div> + <div class="split"></div> + <div> + <month-selector + @back="back" + type="monghM" + @forward="forward('monghM')" + :month="monghM" + ></month-selector> + <calendar-component + :month="monghM" + :start="startDate" + :end="endDate" + @selectDate="selectDate" + :blockedDaysRange="blockedDaysRange" + ></calendar-component> + </div> + </div> + <div class="footer"> + <p v-if="error" class="error"> {{ errorMessage }} </p> + <div v-else></div> + <div class="buttons"> + <button class="btn btn-gray" v-on:click="reset()">RESET</button> + <button class="btn btn-blue" v-on:click="complete()">COMPLETE</button> + </div> + </div> + </div> + </div> +</template> + +<script> +import CalendarComponent from "./CalendarComponent.vue"; +import MonthSelector from "./MonthSelector.vue"; +export default { + components: { + CalendarComponent, + MonthSelector, + }, + props: { + range: { + type: Boolean, + default: true, + }, + blockedDaysRange: { + type: Array, + default: () => [], + } + }, + data() { + return { + month: new Date(), + monghM: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1), + startDate: new Date(Number.MAX_VALUE), + endDate: null, + error: false, + errorMessage: "Produktet kan ikke utlånes i denne perioden", + open: false, + value: null, + style: { + display: "none", + }, + }; + }, + methods: { + openCalendar() { + // Edit style + this.open = true; + this.style = { + display: "flex", + }; + }, + reset() { + this.startDate = new Date(Number.MAX_VALUE); + this.endDate = null; + this.value = null; + this.error = false; + }, + complete() { + if(this.error) return; + this.open = false; + this.style = { + display: "none", + }; + + // Set value to the start and end date + this.value = `${this.startDate.toLocaleDateString()} - ${this.endDate.toLocaleDateString()}`; + this.$emit('value', { + startDate: this.startDate, + endDate: this.endDate + }) + }, + selectDate(date) { + const day = date.getDate(); + const month = date.getMonth(); + const year = date.getFullYear(); + if (this.isBlocked(day, month, year)) return; + if (date > this.startDate) { + this.endDate = date; + } else { + this.startDate = date; + } + + if (this.range) { + this.value = `${this.startDate?.toLocaleDateString()} - ${this.endDate?.toLocaleDateString() || ""}`; + } else { + this.value = `${this.startDate.toLocaleDateString()}`; + } + // Check if start and end check if any days in range is blocked + for(let i = new Date(this.startDate.toUTCString()); i <= this.endDate; i.setDate(i.getDate() + 1)) { + if (this.isBlocked(i.getDate(), i.getMonth(), i.getFullYear())) { + this.error = true; + return; + } + } + + if(this.error) this.error = false; + }, + isBlocked(day, month, year) { + return this.blockedDaysRange.some(([start, end]) => { + // Check if start is the same day + if ( + start.getDate() === day && + start.getMonth() === month && + start.getFullYear() === year + ) { + return true; + } + return ( + (start <= new Date(year, month, day) && + new Date(year, month, day) <= end) + ); + }); + }, + back(type) { + switch (type) { + case "month": { + this.month.setMonth(this.month.getMonth() - 1); + this.month = new Date(this.month); + break; + } + case "monghM": + this.monghM.setMonth(this.monghM.getMonth() - 1); + this.monghM = new Date(this.monghM); + if (this.monghM.getMonth() === this.month.getMonth()) { + this.month.setMonth(this.month.getMonth() - 1); + this.month = new Date(this.month); + } + break; + } + }, + forward(type) { + switch (type) { + case "month": { + this.month.setMonth(this.month.getMonth() + 1); + this.month = new Date(this.month); + if (this.month.getMonth() == this.monghM.getMonth()) { + this.monghM.setMonth(this.monghM.getMonth() + 1); + this.monghM = new Date(this.monghM); + } + break; + } + case "monghM": + this.monghM.setMonth(this.monghM.getMonth() + 1); + this.monghM = new Date(this.monghM); + break; + } + }, + }, +}; +</script> + +<style scoped> +.input label { + display: block; + position: relative; +} + +.input input { + position: relative; + width: 100%; + overflow: hidden; + border: 1px solid #ccc; + border-radius: 10px; + padding: 10px; + padding-right: 50px; +} + +.picker { + position: absolute; + background-color: white; + margin-top: 5px; + z-index: 50; + border: 1px solid black; + border-radius: 4px; + padding: 5px; + flex-direction: column; + width: auto; + max-width: 700px; + overflow: auto; + -webkit-box-shadow: 0px 10px 13px -7px #000000, 5px 5px 14px 5px rgba(0,0,0,0); + box-shadow: 0px 10px 13px -7px #000000, 5px 5px 14px 5px rgba(0,0,0,0); + +} +.split { + border-left: 2px solid black; + margin-left: 10px; + margin-right: 10px; + height: auto; +} + +.calenders { + display: flex; +} + +.footer { + display: flex; + justify-content: space-between; +} + +.error { + align-self: center; + color: red; +} + +.buttons { + float: right; + display: flex; + justify-content: flex-end; +} + +@media only screen and (max-width: 800px) { + .calenders { + flex-direction: column; + } + + .picker { + max-width: 420px; + } +} +.btn { + @apply font-bold py-2 px-4 rounded; +} +.btn-blue { + @apply bg-blue-500 text-white; +} +.btn-blue:hover { + @apply bg-blue-700; +} +.btn-gray:hover { + @apply bg-gray-300; + +} +</style> \ No newline at end of file diff --git a/src/components/InputComponents/DatepickerRange/MonthSelector.vue b/src/components/InputComponents/DatepickerRange/MonthSelector.vue new file mode 100644 index 0000000000000000000000000000000000000000..bb6f60e3f6c6350a4e6ea30305c1c889a12c3b0a --- /dev/null +++ b/src/components/InputComponents/DatepickerRange/MonthSelector.vue @@ -0,0 +1,92 @@ +<template> + <div class="container-c main"> + <div class="button" v-on:click="back"> + <button> + <span> + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 19l-7-7 7-7"></path></svg> + </span> + </button> + </div> + <div class="container-c text"> + <div class="date"> + {{monthShort}} + </div> + <div class="date"> + {{year}} + </div> + </div> + <div class="button" v-on:click="forward"> + <button> + <span> + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5l7 7-7 7"></path></svg> + </span> + </button> + </div> + </div> +</template> + +<script> +export default { + props: { + month: { + type: Date, + required: true, + }, + type: { + type: String, + required: true + } + }, + computed: { + monthDate() { + return new Date(this.month); + }, + monthShort() { + return this.monthDate.toLocaleString('default', { month: 'short' }).toUpperCase(); + }, + year() { + return this.monthDate.getFullYear(); + }, + }, methods: { + back() { + this.$emit('back', this.type); + }, + forward() { + console.log(this.type) + this.$emit('forward', this.type); + }, + + } +} +</script> + +<style> + .container-c { + display: flex; + } + + .container-c.main { + justify-content: space-between; + border: 1px solid black; + padding: 5px; + border-radius: 5px; + max-width: 400px; + } + + .text { + justify-content: space-between; + width: 50%; + text-align: center; + } + + .date { + width: 50%; + text-align: center; + } + + .button { + display: flex; + justify-content: center; + align-items: center; + } +</style> \ No newline at end of file