feat: Space Booking System - MVP complet
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>
This commit is contained in:
229
backend/app/api/booking_templates.py
Normal file
229
backend/app/api/booking_templates.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user