Sistem web pentru rezervarea de birouri și săli de ședință cu flux de aprobare administrativă. Stack: FastAPI + Vue.js 3 + SQLite + TypeScript Features implementate: - Autentificare JWT + Self-registration cu email verification - CRUD Spații, Utilizatori, Settings (Admin) - Calendar interactiv (FullCalendar) cu drag-and-drop - Creare rezervări cu validare (durată, program, overlap, max/zi) - Rezervări recurente (săptămânal) - Admin: aprobare/respingere/anulare cereri - Admin: creare directă rezervări (bypass approval) - Admin: editare orice rezervare - User: editare/anulare rezervări proprii - Notificări in-app (bell icon + dropdown) - Notificări email (async SMTP cu BackgroundTasks) - Jurnal acțiuni administrative (audit log) - Rapoarte avansate (utilizare, top users, approval rate) - Șabloane rezervări (booking templates) - Atașamente fișiere (upload/download) - Conflict warnings (verificare disponibilitate real-time) - Integrare Google Calendar (OAuth2) - Suport timezone (UTC storage + user preference) - 225+ teste backend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
7.5 KiB
Python
230 lines
7.5 KiB
Python
"""Booking template endpoints."""
|
|
from datetime import datetime, timedelta
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session, joinedload
|
|
|
|
from app.core.deps import get_current_user, get_db
|
|
from app.models.booking import Booking
|
|
from app.models.booking_template import BookingTemplate
|
|
from app.models.user import User
|
|
from app.schemas.booking import BookingResponse
|
|
from app.schemas.booking_template import BookingTemplateCreate, BookingTemplateRead
|
|
from app.services.booking_service import validate_booking_rules
|
|
from app.services.email_service import send_booking_notification
|
|
from app.services.notification_service import create_notification
|
|
|
|
router = APIRouter(prefix="/booking-templates", tags=["booking-templates"])
|
|
|
|
|
|
@router.post("", response_model=BookingTemplateRead, status_code=status.HTTP_201_CREATED)
|
|
def create_template(
|
|
data: BookingTemplateCreate,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
) -> BookingTemplateRead:
|
|
"""
|
|
Create a new booking template.
|
|
|
|
- **name**: Template name (e.g., "Weekly Team Sync")
|
|
- **space_id**: Optional default space ID
|
|
- **duration_minutes**: Default duration in minutes
|
|
- **title**: Default booking title
|
|
- **description**: Optional default description
|
|
|
|
Returns the created template.
|
|
"""
|
|
template = BookingTemplate(
|
|
user_id=current_user.id, # type: ignore[arg-type]
|
|
name=data.name,
|
|
space_id=data.space_id,
|
|
duration_minutes=data.duration_minutes,
|
|
title=data.title,
|
|
description=data.description,
|
|
)
|
|
db.add(template)
|
|
db.commit()
|
|
db.refresh(template)
|
|
|
|
return BookingTemplateRead(
|
|
id=template.id, # type: ignore[arg-type]
|
|
user_id=template.user_id, # type: ignore[arg-type]
|
|
name=template.name, # type: ignore[arg-type]
|
|
space_id=template.space_id, # type: ignore[arg-type]
|
|
space_name=template.space.name if template.space else None, # type: ignore[union-attr]
|
|
duration_minutes=template.duration_minutes, # type: ignore[arg-type]
|
|
title=template.title, # type: ignore[arg-type]
|
|
description=template.description, # type: ignore[arg-type]
|
|
usage_count=template.usage_count, # type: ignore[arg-type]
|
|
)
|
|
|
|
|
|
@router.get("", response_model=list[BookingTemplateRead])
|
|
def list_templates(
|
|
db: Annotated[Session, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
) -> list[BookingTemplateRead]:
|
|
"""
|
|
List all booking templates for the current user.
|
|
|
|
Returns templates sorted by name.
|
|
"""
|
|
templates = (
|
|
db.query(BookingTemplate)
|
|
.options(joinedload(BookingTemplate.space))
|
|
.filter(BookingTemplate.user_id == current_user.id)
|
|
.order_by(BookingTemplate.name)
|
|
.all()
|
|
)
|
|
|
|
return [
|
|
BookingTemplateRead(
|
|
id=t.id, # type: ignore[arg-type]
|
|
user_id=t.user_id, # type: ignore[arg-type]
|
|
name=t.name, # type: ignore[arg-type]
|
|
space_id=t.space_id, # type: ignore[arg-type]
|
|
space_name=t.space.name if t.space else None, # type: ignore[union-attr]
|
|
duration_minutes=t.duration_minutes, # type: ignore[arg-type]
|
|
title=t.title, # type: ignore[arg-type]
|
|
description=t.description, # type: ignore[arg-type]
|
|
usage_count=t.usage_count, # type: ignore[arg-type]
|
|
)
|
|
for t in templates
|
|
]
|
|
|
|
|
|
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_template(
|
|
id: int,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
) -> None:
|
|
"""
|
|
Delete a booking template.
|
|
|
|
Users can only delete their own templates.
|
|
"""
|
|
template = (
|
|
db.query(BookingTemplate)
|
|
.filter(
|
|
BookingTemplate.id == id,
|
|
BookingTemplate.user_id == current_user.id,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if not template:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Template not found",
|
|
)
|
|
|
|
db.delete(template)
|
|
db.commit()
|
|
return None
|
|
|
|
|
|
@router.post("/from-template/{template_id}", response_model=BookingResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_booking_from_template(
|
|
template_id: int,
|
|
start_datetime: datetime,
|
|
background_tasks: BackgroundTasks,
|
|
db: Annotated[Session, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
) -> Booking:
|
|
"""
|
|
Create a booking from a template.
|
|
|
|
- **template_id**: ID of the template to use
|
|
- **start_datetime**: When the booking should start (ISO format)
|
|
|
|
The booking will use the template's space, title, description, and duration.
|
|
The end time is calculated automatically based on the template's duration.
|
|
|
|
Returns the created booking with status "pending" (requires admin approval).
|
|
"""
|
|
# Find template
|
|
template = (
|
|
db.query(BookingTemplate)
|
|
.filter(
|
|
BookingTemplate.id == template_id,
|
|
BookingTemplate.user_id == current_user.id,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if not template:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Template not found",
|
|
)
|
|
|
|
if not template.space_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Template does not have a default space",
|
|
)
|
|
|
|
# Calculate end time
|
|
end_datetime = start_datetime + timedelta(minutes=template.duration_minutes) # type: ignore[arg-type]
|
|
|
|
# Validate booking rules
|
|
user_id = int(current_user.id) # type: ignore[arg-type]
|
|
errors = validate_booking_rules(
|
|
db=db,
|
|
space_id=int(template.space_id), # type: ignore[arg-type]
|
|
start_datetime=start_datetime,
|
|
end_datetime=end_datetime,
|
|
user_id=user_id,
|
|
)
|
|
|
|
if errors:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=errors[0], # Return first error
|
|
)
|
|
|
|
# Create booking
|
|
booking = Booking(
|
|
user_id=user_id,
|
|
space_id=template.space_id, # type: ignore[arg-type]
|
|
title=template.title, # type: ignore[arg-type]
|
|
description=template.description, # type: ignore[arg-type]
|
|
start_datetime=start_datetime,
|
|
end_datetime=end_datetime,
|
|
status="pending",
|
|
created_at=datetime.utcnow(),
|
|
)
|
|
|
|
db.add(booking)
|
|
|
|
# Increment usage count
|
|
template.usage_count = int(template.usage_count) + 1 # type: ignore[arg-type, assignment]
|
|
|
|
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 {template.space.name} pentru {booking.start_datetime.strftime('%d.%m.%Y %H:%M')}", # type: ignore[union-attr, union-attr]
|
|
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 booking
|