diff --git a/pms/forms.py b/pms/forms.py index 3607a95b0..8b7e4dae1 100644 --- a/pms/forms.py +++ b/pms/forms.py @@ -51,3 +51,16 @@ class Meta: 'state':forms.HiddenInput(), } +class BookingDateForm(ModelForm): + class Meta: + model = Booking + fields = ['checkin', 'checkout'] + labels = { + 'checkin': 'Entrada', + 'checkout': 'Salida' + } + widgets = { + 'checkin': forms.DateInput(attrs={'type': 'date'}), + 'checkout': forms.DateInput(attrs={'type': 'date'}), + } + diff --git a/pms/services/booking_service.py b/pms/services/booking_service.py new file mode 100644 index 000000000..5d2cfb3e7 --- /dev/null +++ b/pms/services/booking_service.py @@ -0,0 +1,19 @@ +from ..models import Booking + + +def is_room_available(room, checkin, checkout, exclude_booking_id=None): + """ + Valida si una habitación está disponible en las fechas especificadas. + bool: True si disponible, False si hay conflicto + """ + conflicts = Booking.objects.filter( + state=Booking.NEW, + room=room, + checkin__lte=checkout, + checkout__gte=checkin, + ) + + if exclude_booking_id: + conflicts = conflicts.exclude(id=exclude_booking_id) + + return not conflicts.exists() diff --git a/pms/services/dashboard_service.py b/pms/services/dashboard_service.py new file mode 100644 index 000000000..739567f1c --- /dev/null +++ b/pms/services/dashboard_service.py @@ -0,0 +1,54 @@ +from datetime import date, datetime, time +from django.db.models import Sum +from django.utils.timezone import make_aware +from ..models import Booking, Room + + +def get_dashboard_data(): + today = date.today() + + today_min = make_aware(datetime.combine(today, time.min)) + today_max = make_aware(datetime.combine(today, time.max)) + today_range = (today_min, today_max) + + # bookings created today + new_bookings = Booking.objects.filter( + created__range=today_range + ).count() + + # checkin guests + incoming = Booking.objects.filter( + checkin=today + ).exclude(state="DEL").count() + + # checkout guests + outcoming = Booking.objects.filter( + checkout=today + ).exclude(state="DEL").count() + + # invoiced today + invoiced = Booking.objects.filter( + created__range=today_range + ).exclude(state="DEL").aggregate(total=Sum('total'))["total"] or 0 + + # total rooms + total_rooms = Room.objects.count() + + confirmed_bookings = Booking.objects.filter( + state=Booking.NEW, # excluye canceladas aunque las fechas coincidan + checkin__lte=today, + checkout__gt=today, + ).count() + + occupancy_rate = ( + (confirmed_bookings / total_rooms) * 100 + if total_rooms > 0 else 0 + ) + + return { + 'new_bookings': new_bookings, + 'incoming_guests': incoming, + 'outcoming_guests': outcoming, + 'invoiced': invoiced, + 'occupancy_rate': occupancy_rate + } \ No newline at end of file diff --git a/pms/statics/js/live_search.js b/pms/statics/js/live_search.js new file mode 100644 index 000000000..14fb6a052 --- /dev/null +++ b/pms/statics/js/live_search.js @@ -0,0 +1,17 @@ +// pms/statics/js/live_search.js +document.addEventListener('DOMContentLoaded', function () { + const input = document.getElementById('live-search-input'); + if (!input) return; // el componente no está en esta página, salir + + const targetId = input.dataset.target; + const container = document.getElementById(targetId); + if (!container) return; + + input.addEventListener('input', function () { + const query = input.value.toLowerCase().trim(); + Array.from(container.children).forEach(function (card) { + const text = (card.dataset.search || '').toLowerCase(); + card.style.display = text.includes(query) ? '' : 'none'; + }); + }); +}); diff --git a/pms/templates/components/live_search.html b/pms/templates/components/live_search.html new file mode 100644 index 000000000..f323e7067 --- /dev/null +++ b/pms/templates/components/live_search.html @@ -0,0 +1,14 @@ +{# components/live_search.html #} +{# Uso: {% include "components/live_search.html" with target_id="rooms-list" %} #} +