From 5333df38347af1bbdb364c37749251bed81c6c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=A0=D1=94=D0=B7?= =?UTF-8?q?=D1=94=D0=BD=D0=BA=D0=BE=D0=B2?= <108422398+RezenkovD@users.noreply.github.com> Date: Tue, 15 Aug 2023 20:30:01 +0300 Subject: [PATCH] Feat/booking calendar * feat: add API for av slots for month * fix: btn z-index * feat: add getAvailableSlotsMonth * feat: add color * feat: block hover if av_slots <= 0 * ref: add async * feat: max 2 month advance * feat: hidden btn --- vr_club_site/templates/site/calendar.html | 31 +++++++- vr_club_site/templates/site/js/calendar.js | 35 +++++++-- .../templates/site/style/calendar.css | 23 +++++- vr_club_site/templates/site/style/index.css | 2 + vr_club_site/urls.py | 11 ++- vr_club_site/utils.py | 76 +++++++++++++++++-- vr_club_site/views.py | 10 +++ 7 files changed, 174 insertions(+), 14 deletions(-) diff --git a/vr_club_site/templates/site/calendar.html b/vr_club_site/templates/site/calendar.html index 8493bd3..540dc5a 100644 --- a/vr_club_site/templates/site/calendar.html +++ b/vr_club_site/templates/site/calendar.html @@ -18,7 +18,7 @@
-
+
@@ -42,7 +42,7 @@ \ No newline at end of file diff --git a/vr_club_site/templates/site/js/calendar.js b/vr_club_site/templates/site/js/calendar.js index 667326f..15de5db 100644 --- a/vr_club_site/templates/site/js/calendar.js +++ b/vr_club_site/templates/site/js/calendar.js @@ -6,7 +6,7 @@ document.addEventListener("DOMContentLoaded", function () { const nextMonthBtn = document.getElementById("nextMonthBtn"); let currentYear, currentMonth; - function generateCalendar(year, month) { + async function generateCalendar(year, month) { const now = new Date(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const firstDayIndex = new Date(year, month, 0).getDay(); @@ -38,23 +38,26 @@ document.addEventListener("DOMContentLoaded", function () { let today = now.getDate(); let number; + let day_; for (let i = 1; i <= daysInMonth; i++) { + day_ = i number = i; if (number < 10) { number = "0" + number; } if (i === now.getDate() && month === now.getMonth() && year === now.getFullYear()) { today = i; - days += `
${number}
12 місць
`; + days += `
${number}
`; } else { if (i < today && month <= now.getMonth() && year <= now.getFullYear() || month < now.getMonth() && year <= now.getFullYear() || month > now.getMonth() && year < now.getFullYear() || i >= today && month === now.getMonth() && year < now.getFullYear()) { days += `
${number}
`; } else { - days += `
${number}
`; + days += `
${number}
`; } } } + await getAvailableSlotsMonth(year, month) for (let i = 1; i < 7 - lastDayIndex; i++) { days += `
`; @@ -85,6 +88,19 @@ document.addEventListener("DOMContentLoaded", function () { prevMonth_.style.cursor = "pointer" } + const nextMonth_ = document.getElementById("nextMonth"); + const currentDate = new Date(currentYear, currentMonth); + const maxAllowedDate = new Date(); + maxAllowedDate.setMonth(maxAllowedDate.getMonth() + maxMonthsAhead - 1); + + if (currentDate > maxAllowedDate) { + nextMonth_.style.opacity = 0; + nextMonth_.style.cursor = "default" + } else { + nextMonth_.style.opacity = 1; + nextMonth_.style.cursor = "pointer" + } + } function updateCalendar() { @@ -102,7 +118,17 @@ document.addEventListener("DOMContentLoaded", function () { } } + const maxMonthsAhead = 2; + function nextMonth() { + const currentDate = new Date(currentYear, currentMonth); + + const maxAllowedDate = new Date(); + maxAllowedDate.setMonth(maxAllowedDate.getMonth() + maxMonthsAhead - 1); + + if (currentDate > maxAllowedDate) { + return; + } currentMonth++; if (currentMonth > 11) { currentMonth = 0; @@ -142,8 +168,7 @@ async function openPopup(day, month, year) { dateDiv.textContent = `${parseInt(day, 10)} ${monthsNames[parseInt(month) - 1]} ${year}`; - getAvailableSlots(`${year}-${parseInt(month, 10)}-${parseInt(day, 10)}`) - await new Promise(resolve => setTimeout(resolve, 150)); + await getAvailableSlots(`${year}-${parseInt(month, 10)}-${parseInt(day, 10)}`) const dateInput = document.getElementById('id_date'); dateInput.value = `${day}-${month}-${year}`; diff --git a/vr_club_site/templates/site/style/calendar.css b/vr_club_site/templates/site/style/calendar.css index ece347e..a36fb58 100644 --- a/vr_club_site/templates/site/style/calendar.css +++ b/vr_club_site/templates/site/style/calendar.css @@ -214,7 +214,7 @@ } .count_place { - color: #001729; + color: #4FB4FE; text-align: left; font: 500 18px/150% "Montserrat", sans-serif; position: relative; @@ -230,6 +230,23 @@ margin-top: 44px; } +.wrap:hover .count_place { + color: #010103; +} + +.inactive .base-day { + color: #768A9A; +} + +.inactive .count_place { + color: #768A9A; +} + +.wrap.inactive { + pointer-events: none; +} + + @media screen and (max-width: 768px) { .booking { font: 700 28px "Montserrat", sans-serif; @@ -362,6 +379,10 @@ height: 80px; } + .wrap:hover .count_place { + color: #010103; + } + .count_place { font: 500 12px/110% "Montserrat", sans-serif; height: 28px; diff --git a/vr_club_site/templates/site/style/index.css b/vr_club_site/templates/site/style/index.css index be1faf2..182fba8 100644 --- a/vr_club_site/templates/site/style/index.css +++ b/vr_club_site/templates/site/style/index.css @@ -337,6 +337,7 @@ cursor: pointer; margin-left: auto; margin-right: auto; + z-index: 9999; } .section-rules-club { @@ -687,6 +688,7 @@ cursor: pointer; margin-left: auto; margin-right: auto; + z-index: 9999; } } diff --git a/vr_club_site/urls.py b/vr_club_site/urls.py index 4897a1f..e01e69c 100644 --- a/vr_club_site/urls.py +++ b/vr_club_site/urls.py @@ -1,6 +1,10 @@ from django.urls import path -from .views import index_page, get_available_slots_view +from .views import ( + index_page, + get_available_slots_view, + get_available_slots_for_month_view, +) from games.views import game_page app_name = "site" @@ -13,4 +17,9 @@ get_available_slots_view, name="get_available_slots", ), + path( + "api/get_available_slots_for_month/", + get_available_slots_for_month_view, + name="get_available_slots_for_month", + ), ] diff --git a/vr_club_site/utils.py b/vr_club_site/utils.py index ab111ca..99bae03 100644 --- a/vr_club_site/utils.py +++ b/vr_club_site/utils.py @@ -1,7 +1,10 @@ +import calendar import decimal +from datetime import datetime, date from django.db import IntegrityError, transaction -from django.db.models import Sum +from django.db.models import F, Sum, Case, When, Value, IntegerField +from django.db.models.functions import Greatest, Least from django.contrib import messages from .models import Booking, BookingTime, ACTUAL, Settings @@ -11,9 +14,7 @@ MAX_SESSION = 2 -def get_available_slots(selected_date=None): - _available_slots = [] - +def get_session_seats(): try: setting = Settings.objects.get(name="seats") setting_type = setting.variable_type @@ -28,12 +29,77 @@ def get_available_slots(selected_date=None): except Settings.DoesNotExist: session_seats = 1 + return session_seats + + +def get_available_slots_for_month(currentYear=None, currentMonth=None): + _available_slots = [] + session_seats = get_session_seats() + days_in_month = calendar.monthrange(int(currentYear), int(currentMonth))[1] + + time_choice_to_index = { + choice[0]: index for index, choice in enumerate(BookingTime.TIME_CHOICES) + } + + all_time_choices = [choice[0] for choice in BookingTime.TIME_CHOICES] + + daily_available_slots = { + day: [session_seats] * len(all_time_choices) + for day in range(1, days_in_month + 1) + } + + bookings = ( + Booking.objects.filter( + time__status=ACTUAL, + time__date__year=currentYear, + time__date__month=currentMonth, + ) + .annotate( + time_index=Case( + *[ + When(time__time=choice, then=Value(index)) + for choice, index in time_choice_to_index.items() + ], + output_field=IntegerField(), + ) + ) + .values("time__date", "time_index") + .annotate(total_people=Sum("people_count")) + ) + + for booking in bookings: + day = booking["time__date"].day + time_index = booking["time_index"] + total_people = booking["total_people"] + daily_available_slots[day][time_index] -= total_people + + for day, available_slots in daily_available_slots.items(): + selected_date = date(int(currentYear), int(currentMonth), day) + total_available_slots = sum(max(slot, 0) for slot in available_slots) + _available_slots.append( + { + "date": selected_date.strftime("%Y-%m-%d"), + "available_slots": total_available_slots, + } + ) + + return _available_slots + + +def get_available_slots(selected_date=None): + _available_slots = [] + session_seats = get_session_seats() + for x in BookingTime.TIME_CHOICES: try: slot = ( Booking.objects.filter( time__status=ACTUAL, time__time=x[0], time__date=selected_date - ).aggregate(total_people=Sum("people_count"))["total_people"] + ).aggregate( + total_people=Least(Greatest(Sum("people_count"), 0), session_seats) + )[ + "total_people" + ] or 0 ) except BookingTime.DoesNotExist: diff --git a/vr_club_site/views.py b/vr_club_site/views.py index 2377f6d..7536f98 100644 --- a/vr_club_site/views.py +++ b/vr_club_site/views.py @@ -7,6 +7,8 @@ from .models import BookingTime from .utils import ( get_available_slots, + get_available_slots_for_month, + get_session_seats, is_all_neighbors, has_invalid_slots, book_session, @@ -21,6 +23,14 @@ def get_available_slots_view(request): return JsonResponse({"available_slots": available_slots}) +def get_available_slots_for_month_view(request): + if request.method == "GET": + year = request.GET.get("year") + month = request.GET.get("month") + available_slots = get_available_slots_for_month(year, month) + return JsonResponse({"available_slots": available_slots}) + + def index_page(request): if request.method == "POST": form = BookingForm(request.POST)