From 28685d82546d265e2a940a7651e4d51e5569bf10 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Thu, 12 Feb 2026 10:21:32 +0000 Subject: [PATCH] feat(dashboard): redesign with active bookings, calendar, and compact stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major Dashboard improvements focusing on active reservations and calendar view: Frontend changes: - Add ActiveBookings component showing in-progress bookings with progress bars - Add DashboardCalendar component with read-only calendar view of all user bookings - Refactor Dashboard layout: active bookings → stats grid → calendar → activity - Remove redundant Quick Actions and Available Spaces sections - Make Quick Stats compact (36px icons, 20px font) and clickable (router-link) - Add datetime utility functions (isBookingActive, getBookingProgress, formatRemainingTime) - Fix MyBookings to read status query parameter from URL - Auto-refresh active bookings every 60s with proper cleanup Backend changes: - Add GET /api/bookings/my/calendar endpoint with date range filtering - Fix Google Calendar sync in reschedule_booking and admin_update_booking - Add Google OAuth environment variables to .env.example Design: - Dark mode compatible with CSS variables throughout - Mobile responsive (768px breakpoint, 2-column stats grid) - CollapsibleSection pattern for all dashboard sections - Progress bars with accent colors for active bookings Performance: - Optimized API calls (calendar uses date range filtering) - Remove duplicate calendar data loading on mount - Computed property caching for stats and filtered bookings - Memory leak prevention (setInterval cleanup on unmount) Co-Authored-By: Claude Sonnet 4.5 --- backend/.env.example | 5 + backend/app/api/bookings.py | 55 ++- frontend/src/components/ActiveBookings.vue | 189 ++++++++++ frontend/src/components/DashboardCalendar.vue | 176 +++++++++ frontend/src/services/api.ts | 7 + frontend/src/utils/datetime.ts | 36 ++ frontend/src/views/Dashboard.vue | 338 +++++------------- frontend/src/views/MyBookings.vue | 5 + 8 files changed, 560 insertions(+), 251 deletions(-) create mode 100644 frontend/src/components/ActiveBookings.vue create mode 100644 frontend/src/components/DashboardCalendar.vue diff --git a/backend/.env.example b/backend/.env.example index b78d07a..8e2617d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -17,3 +17,8 @@ SMTP_USER= SMTP_PASSWORD= SMTP_FROM_ADDRESS=noreply@space-booking.local SMTP_ENABLED=false + +# Google Calendar Integration +GOOGLE_CLIENT_ID=your_google_client_id_here +GOOGLE_CLIENT_SECRET=your_google_client_secret_here +GOOGLE_REDIRECT_URI=https://your-domain.com/api/integrations/google/callback diff --git a/backend/app/api/bookings.py b/backend/app/api/bookings.py index cd7761c..91e1a24 100644 --- a/backend/app/api/bookings.py +++ b/backend/app/api/bookings.py @@ -12,7 +12,11 @@ from app.models.space import Space from app.models.user import User from app.services.audit_service import log_action from app.services.email_service import send_booking_notification -from app.services.google_calendar_service import create_calendar_event, delete_calendar_event +from app.services.google_calendar_service import ( + create_calendar_event, + delete_calendar_event, + update_calendar_event, +) from app.services.notification_service import create_notification from app.schemas.booking import ( AdminCancelRequest, @@ -202,6 +206,37 @@ def get_my_bookings( return [BookingWithSpace.model_validate(b) for b in bookings] +@bookings_router.get("/my/calendar", response_model=list[BookingWithSpace]) +def get_my_bookings_calendar( + start: Annotated[datetime, Query(description="Start datetime (ISO format)")], + end: Annotated[datetime, Query(description="End datetime (ISO format)")], + db: Annotated[Session, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +) -> list[BookingWithSpace]: + """ + Get user's bookings for calendar view within date range. + + Query parameters: + - **start**: Start datetime in ISO format (e.g., 2024-01-01T00:00:00) + - **end**: End datetime in ISO format (e.g., 2024-01-31T23:59:59) + + Returns bookings with status approved or pending, sorted by start time. + """ + bookings = ( + db.query(Booking) + .join(Space, Booking.space_id == Space.id) + .filter( + Booking.user_id == current_user.id, + Booking.start_datetime < end, + Booking.end_datetime > start, + Booking.status.in_(["approved", "pending"]), + ) + .order_by(Booking.start_datetime) + .all() + ) + return [BookingWithSpace.model_validate(b) for b in bookings] + + @bookings_router.post("", response_model=BookingResponse, status_code=status.HTTP_201_CREATED) def create_booking( booking_data: BookingCreate, @@ -873,6 +908,15 @@ def admin_update_booking( detail=errors[0], ) + # Sync with Google Calendar if event exists + if booking.google_calendar_event_id: + update_calendar_event( + db=db, + booking=booking, + user_id=int(booking.user_id), # type: ignore[arg-type] + event_id=booking.google_calendar_event_id, + ) + # Log audit log_action( db=db, @@ -1025,6 +1069,15 @@ def reschedule_booking( booking.start_datetime = data.start_datetime # type: ignore[assignment] booking.end_datetime = data.end_datetime # type: ignore[assignment] + # Sync with Google Calendar if event exists + if booking.google_calendar_event_id: + update_calendar_event( + db=db, + booking=booking, + user_id=int(booking.user_id), # type: ignore[arg-type] + event_id=booking.google_calendar_event_id, + ) + # Log audit log_action( db=db, diff --git a/frontend/src/components/ActiveBookings.vue b/frontend/src/components/ActiveBookings.vue new file mode 100644 index 0000000..b196232 --- /dev/null +++ b/frontend/src/components/ActiveBookings.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/frontend/src/components/DashboardCalendar.vue b/frontend/src/components/DashboardCalendar.vue new file mode 100644 index 0000000..c068a0d --- /dev/null +++ b/frontend/src/components/DashboardCalendar.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 69872af..61c85d1 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -190,6 +190,13 @@ export const bookingsApi = { createRecurring: async (data: RecurringBookingCreate): Promise => { const response = await api.post('/bookings/recurring', data) return response.data + }, + + getMyCalendar: async (start: string, end: string): Promise => { + const response = await api.get('/bookings/my/calendar', { + params: { start, end } + }) + return response.data } } diff --git a/frontend/src/utils/datetime.ts b/frontend/src/utils/datetime.ts index ad6195b..0cb3d27 100644 --- a/frontend/src/utils/datetime.ts +++ b/frontend/src/utils/datetime.ts @@ -131,3 +131,39 @@ export const isoToLocalDateTime = (isoDateTime: string, timezone: string = 'UTC' // Format as YYYY-MM-DDTHH:mm for datetime-local input return `${year}-${month}-${day}T${hour.padStart(2, '0')}:${minute.padStart(2, '0')}` } + +/** + * Check if a booking is currently active (in progress). + */ +export const isBookingActive = (startDatetime: string, endDatetime: string): boolean => { + const now = new Date() + const start = new Date(ensureUTC(startDatetime)) + const end = new Date(ensureUTC(endDatetime)) + return start <= now && end >= now +} + +/** + * Calculate progress percentage for an active booking. + */ +export const getBookingProgress = (startDatetime: string, endDatetime: string): number => { + const now = new Date() + const start = new Date(ensureUTC(startDatetime)) + const end = new Date(ensureUTC(endDatetime)) + const total = end.getTime() - start.getTime() + const elapsed = now.getTime() - start.getTime() + return Math.min(100, Math.max(0, (elapsed / total) * 100)) +} + +/** + * Format remaining time for an active booking. + */ +export const formatRemainingTime = (endDatetime: string): string => { + const now = new Date() + const end = new Date(ensureUTC(endDatetime)) + const remaining = end.getTime() - now.getTime() + if (remaining <= 0) return 'Ended' + const hours = Math.floor(remaining / 3600000) + const minutes = Math.floor((remaining % 3600000) / 60000) + if (hours > 0) return `${hours}h ${minutes}m remaining` + return `${minutes}m remaining` +} diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 408ee58..39911df 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -10,77 +10,8 @@
- - -
-
-
- -
-
-

{{ stats.total }}

-

Total Bookings

-
-
- -
-
- -
-
-

{{ stats.pending }}

-

Pending

-
-
- -
-
- -
-
-

{{ stats.approved }}

-

Approved

-
-
- - -
-
- -
-
-

{{ adminStats.pendingRequests }}

-

Pending Requests

-
-
-
-
- - -
-

Quick Actions

-
- - - Book a Space - - - - - My Bookings - - - - - Manage Bookings - - - - - Manage Spaces - -
-
+ +
@@ -117,39 +48,58 @@
- - - - -
- -

No available spaces

-
- -
- -
- + + +
+ +
+
-
-

{{ space.name }}

-

- {{ formatType(space.type) }} · Capacity: {{ space.capacity }} -

+
+

{{ stats.total }}

+

Total Bookings

+
+ + + +
+ +
+
+

{{ stats.pending }}

+

Pending

+
+
+ + +
+ +
+
+

{{ stats.approved }}

+

Approved

+
+
+ + + +
+ +
+
+

{{ adminStats.pendingRequests }}

+

Pending Requests

-
+ + + + +