feat: complete UI/UX overhaul - dashboard unification, calendar UX, mobile optimization

- Dashboard redesign as command center with filters, quick actions, inline approve/reject
- Reusable components: BookingRow, BookingFilters, ActionMenu, BookingPreviewModal, BookingEditModal
- Calendar: drag & drop reschedule, eventClick preview modal, grid/list toggle
- Mobile: segmented control bookings/calendar toggle, compact pills, responsive layout
- Collapsible filters with active count badge
- Smart menu positioning with Teleport
- Calendar/list bidirectional data sync
- Navigation: unified History page, removed AdminPending
- Google Calendar OAuth integration
- Dark mode contrast improvements, breadcrumb navigation
- useLocalStorage composable for state persistence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-12 15:34:47 +00:00
parent a4d3f862d2
commit d245c72757
36 changed files with 5275 additions and 1569 deletions

View File

@@ -68,9 +68,10 @@ def get_space_bookings(
detail="Space not found",
)
# Query bookings in the time range
# Query bookings in the time range (only active bookings)
query = db.query(Booking).filter(
Booking.space_id == space_id,
Booking.status.in_(["approved", "pending"]),
Booking.start_datetime < end,
Booking.end_datetime > start,
)
@@ -292,6 +293,9 @@ def create_booking(
detail=errors[0], # Return first error
)
# Auto-approve if admin, otherwise pending
is_admin = current_user.role == "admin"
# Create booking (with UTC times)
booking = Booking(
user_id=user_id,
@@ -300,7 +304,8 @@ def create_booking(
end_datetime=end_datetime_utc,
title=booking_data.title,
description=booking_data.description,
status="pending",
status="approved" if is_admin else "pending",
approved_by=current_user.id if is_admin else None, # type: ignore[assignment]
created_at=datetime.utcnow(),
)
@@ -308,26 +313,27 @@ def create_booking(
db.commit()
db.refresh(booking)
# Notify all admins about the new booking request
admins = db.query(User).filter(User.role == "admin").all()
for admin in admins:
create_notification(
db=db,
user_id=admin.id, # type: ignore[arg-type]
type="booking_created",
title="Noua Cerere de Rezervare",
message=f"Utilizatorul {current_user.full_name} a solicitat rezervarea spațiului {space.name} pentru {booking.start_datetime.strftime('%d.%m.%Y %H:%M')}",
booking_id=booking.id,
)
# Send email notification to admin
background_tasks.add_task(
send_booking_notification,
booking,
"created",
admin.email,
current_user.full_name,
None,
)
if not is_admin:
# Notify all admins about the new booking request
admins = db.query(User).filter(User.role == "admin").all()
for admin in admins:
create_notification(
db=db,
user_id=admin.id, # type: ignore[arg-type]
type="booking_created",
title="Noua Cerere de Rezervare",
message=f"Utilizatorul {current_user.full_name} a solicitat rezervarea spațiului {space.name} pentru {booking.start_datetime.strftime('%d.%m.%Y %H:%M')}",
booking_id=booking.id,
)
# Send email notification to admin
background_tasks.add_task(
send_booking_notification,
booking,
"created",
admin.email,
current_user.full_name,
None,
)
# Return with timezone conversion
return BookingResponse.from_booking_with_timezone(booking, user_timezone)
@@ -637,6 +643,56 @@ def cancel_booking(
admin_router = APIRouter(prefix="/admin/bookings", tags=["admin"])
@admin_router.get("/all", response_model=list[BookingPendingDetail])
def get_all_bookings(
status_filter: Annotated[str | None, Query(alias="status")] = None,
space_id: Annotated[int | None, Query()] = None,
user_id: Annotated[int | None, Query()] = None,
start: Annotated[datetime | None, Query(description="Start datetime (ISO format)")] = None,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
db: Annotated[Session, Depends(get_db)] = None, # type: ignore[assignment]
current_admin: Annotated[User, Depends(get_current_admin)] = None, # type: ignore[assignment]
) -> list[BookingPendingDetail]:
"""
Get all bookings across all users (admin only).
Returns bookings with user and space details.
Query parameters:
- **status** (optional): Filter by status (pending/approved/rejected/canceled)
- **space_id** (optional): Filter by space ID
- **user_id** (optional): Filter by user ID
- **start** (optional): Only bookings starting from this datetime
- **limit** (optional): Max results (1-100, default 20)
"""
query = (
db.query(Booking)
.join(Space, Booking.space_id == Space.id)
.join(User, Booking.user_id == User.id)
)
if status_filter is not None:
query = query.filter(Booking.status == status_filter)
if space_id is not None:
query = query.filter(Booking.space_id == space_id)
if user_id is not None:
query = query.filter(Booking.user_id == user_id)
if start is not None:
# Use end_datetime to include bookings still in progress (started but not ended)
query = query.filter(Booking.end_datetime > start)
bookings = (
query.order_by(Booking.start_datetime.asc())
.limit(limit)
.all()
)
return [BookingPendingDetail.model_validate(b) for b in bookings]
@admin_router.get("/pending", response_model=list[BookingPendingDetail])
def get_pending_bookings(
space_id: Annotated[int | None, Query()] = None,