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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user