aggiunti trasferte, export excel, miglioramenti generali

This commit is contained in:
2026-02-04 12:55:04 +01:00
parent 17453f5d13
commit 5f4ef6faee
30 changed files with 1558 additions and 325 deletions

View File

@@ -68,6 +68,7 @@ AUTHELIA_ADMIN_GROUP = os.getenv("AUTHELIA_ADMIN_GROUP", "parking_admins")
# External URLs for Authelia mode
# When AUTHELIA_ENABLED, login redirects to Authelia and register to external portal
AUTHELIA_LOGIN_URL = os.getenv("AUTHELIA_LOGIN_URL", "") # e.g., https://auth.rocketscale.it
AUTHELIA_LOGOUT_URL = os.getenv("AUTHELIA_LOGOUT_URL", "") # e.g., https://auth.rocketscale.it/logout
REGISTRATION_URL = os.getenv("REGISTRATION_URL", "") # e.g., https://register.rocketscale.it
# Email configuration (following org-stack pattern)

View File

@@ -145,6 +145,7 @@ def get_auth_config():
return {
"authelia_enabled": config.AUTHELIA_ENABLED,
"login_url": config.AUTHELIA_LOGIN_URL if config.AUTHELIA_ENABLED else None,
"logout_url": config.AUTHELIA_LOGOUT_URL if config.AUTHELIA_ENABLED else None,
"registration_url": config.REGISTRATION_URL if config.AUTHELIA_ENABLED else None
}

View File

@@ -125,6 +125,11 @@ def create_office(data: ValidOfficeCreate, db: Session = Depends(get_db), user=D
)
db.add(office)
db.commit()
# Sync spots
from services.offices import sync_office_spots
sync_office_spots(office.id, office.parking_quota, office.spot_prefix, db)
return office
@router.get("/{office_id}")
@@ -186,6 +191,10 @@ def update_office_settings(office_id: str, data: OfficeSettingsUpdate, db: Sessi
office.updated_at = datetime.utcnow()
db.commit()
# Sync spots
from services.offices import sync_office_spots
sync_office_spots(office.id, office.parking_quota, office.spot_prefix, db)
return {
"id": office.id,
"name": office.name,
@@ -459,16 +468,20 @@ def add_office_exclusion(office_id: str, data: ExclusionCreate, db: Session = De
if not db.query(User).filter(User.id == data.user_id).first():
raise HTTPException(status_code=404, detail="User not found")
existing = db.query(ParkingExclusion).filter(
ParkingExclusion.office_id == office_id,
ParkingExclusion.user_id == data.user_id
).first()
if existing:
raise HTTPException(status_code=400, detail="User already has a parking exclusion")
# Relaxed unique check - user can have multiple exclusions (different periods)
# existing = db.query(ParkingExclusion).filter(
# ParkingExclusion.office_id == office_id,
# ParkingExclusion.user_id == data.user_id
# ).first()
# if existing:
# raise HTTPException(status_code=400, detail="User already has a parking exclusion")
if data.start_date and data.end_date and data.end_date < data.start_date:
raise HTTPException(status_code=400, detail="End date must be after start date")
if data.end_date and not data.start_date:
raise HTTPException(status_code=400, detail="Start date is required if an end date is specified")
exclusion = ParkingExclusion(
id=generate_uuid(),
office_id=office_id,

View File

@@ -23,7 +23,7 @@ from pydantic import BaseModel
from sqlalchemy.orm import Session
from database.connection import get_db
from database.models import DailyParkingAssignment, User, UserRole, Office
from database.models import DailyParkingAssignment, User, UserRole, Office, OfficeSpot
from utils.auth_middleware import get_current_user, require_manager_or_admin
from services.parking import (
initialize_parking_pool, get_spot_display_name, release_user_spot,
@@ -91,35 +91,70 @@ def init_office_pool(request: InitPoolRequest, db: Session = Depends(get_db), cu
@router.get("/assignments/{date_val}", response_model=List[AssignmentResponse])
def get_assignments(date_val: date, office_id: str = None, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Get parking assignments for a date, optionally filtered by office"""
def get_assignments(date_val: date, office_id: str | None = None, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Get parking assignments for a date, merging active assignments with empty spots"""
query_date = date_val
# Defaults to user's office if not specified
target_office_id = office_id or current_user.office_id
if not target_office_id:
# Admin looking at all? Or error?
# If no office_id, we might fetch all spots from all offices?
# Let's support specific office filtering primarily as per UI use case
# If office_id is None, we proceed with caution (maybe all offices)
pass
query = db.query(DailyParkingAssignment).filter(DailyParkingAssignment.date == query_date)
if office_id:
query = query.filter(DailyParkingAssignment.office_id == office_id)
# 1. Get ALL spots for the target office(s)
# Note: Sorting by spot_number for consistent display order
spot_query = db.query(OfficeSpot).filter(OfficeSpot.is_unavailable == False)
if target_office_id:
spot_query = spot_query.filter(OfficeSpot.office_id == target_office_id)
spots = spot_query.order_by(OfficeSpot.spot_number).all()
assignments = query.all()
# 2. Get EXISTING assignments
assign_query = db.query(DailyParkingAssignment).filter(DailyParkingAssignment.date == query_date)
if target_office_id:
assign_query = assign_query.filter(DailyParkingAssignment.office_id == target_office_id)
active_assignments = assign_query.all()
# Map assignment by spot_id for O(1) lookup
assignment_map = {a.spot_id: a for a in active_assignments}
results = []
for assignment in assignments:
# Get display name using office's spot prefix
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
result = AssignmentResponse(
id=assignment.id,
date=assignment.date,
spot_id=assignment.spot_id,
spot_display_name=spot_display_name,
user_id=assignment.user_id,
office_id=assignment.office_id
)
if assignment.user_id:
user = db.query(User).filter(User.id == assignment.user_id).first()
if user:
result.user_name = user.name
result.user_email = user.email
# 3. Merge
for spot in spots:
assignment = assignment_map.get(spot.id)
if assignment:
# Active assignment
result = AssignmentResponse(
id=assignment.id,
date=assignment.date,
spot_id=spot.id, # The FK
spot_display_name=spot.name,
user_id=assignment.user_id,
office_id=spot.office_id
)
if assignment.user_id:
user = db.query(User).filter(User.id == assignment.user_id).first()
if user:
result.user_name = user.name
result.user_email = user.email
else:
# Empty spot (Virtual assignment response)
# We use "virtual" ID or just None? Schema says ID is str.
# Frontend might need an ID for keys. Let's use "virtual-{spot.id}"
result = AssignmentResponse(
id=f"virtual-{spot.id}",
date=query_date,
spot_id=spot.id,
spot_display_name=spot.name,
user_id=None,
office_id=spot.office_id
)
results.append(result)
@@ -158,9 +193,6 @@ def get_my_assignments(start_date: date = None, end_date: date = None, db: Sessi
return results
return results
@router.post("/run-allocation")
def run_fair_allocation(data: RunAllocationRequest, db: Session = Depends(get_db), current_user=Depends(require_manager_or_admin)):
"""Manually trigger fair allocation for a date (Test Tool)"""
@@ -203,32 +235,43 @@ def manual_assign(data: ManualAssignRequest, db: Session = Depends(get_db), curr
if current_user.role != UserRole.ADMIN and not is_manager:
raise HTTPException(status_code=403, detail="Not authorized to assign spots for this office")
# Check if spot exists and is free
spot = db.query(DailyParkingAssignment).filter(
# Check if spot exists (OfficeSpot)
spot_def = db.query(OfficeSpot).filter(OfficeSpot.id == data.spot_id).first()
if not spot_def:
raise HTTPException(status_code=404, detail="Spot definition not found")
# Check if spot is already assigned
existing_assignment = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.office_id == data.office_id,
DailyParkingAssignment.date == assign_date,
DailyParkingAssignment.spot_id == data.spot_id
).first()
if not spot:
raise HTTPException(status_code=404, detail="Spot not found")
if spot.user_id:
if existing_assignment:
raise HTTPException(status_code=400, detail="Spot already assigned")
# Check if user already has a spot for this date (from any manager)
existing = db.query(DailyParkingAssignment).filter(
user_has_spot = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.date == assign_date,
DailyParkingAssignment.user_id == data.user_id
).first()
if existing:
if user_has_spot:
raise HTTPException(status_code=400, detail="User already has a spot for this date")
spot.user_id = data.user_id
# Create Assignment
new_assignment = DailyParkingAssignment(
id=generate_uuid(),
date=assign_date,
spot_id=data.spot_id,
user_id=data.user_id,
office_id=data.office_id,
created_at=datetime.utcnow()
)
db.add(new_assignment)
db.commit()
spot_display_name = get_spot_display_name(data.spot_id, data.office_id, db)
return {"message": "Spot assigned", "spot_id": data.spot_id, "spot_display_name": spot_display_name}
return {"message": "Spot assigned", "spot_id": data.spot_id, "spot_display_name": spot_def.name}
@router.post("/release-my-spot/{assignment_id}")
@@ -245,15 +288,16 @@ def release_my_spot(assignment_id: str, db: Session = Depends(get_db), current_u
raise HTTPException(status_code=403, detail="You can only release your own parking spot")
# Get spot display name for notification
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
spot_name = assignment.spot.name if assignment.spot else "Unknown"
assignment.user_id = None
# Delete assignment (Release)
db.delete(assignment)
db.commit()
# Send notification (self-release, so just confirmation)
notify_parking_released(current_user, assignment.date, spot_display_name)
notify_parking_released(current_user, assignment.date, spot_name)
config.logger.info(f"User {current_user.email} released parking spot {spot_display_name} on {assignment.date}")
config.logger.info(f"User {current_user.email} released parking spot {spot_name} on {assignment.date}")
return {"message": "Parking spot released"}
@@ -282,7 +326,7 @@ def reassign_spot(data: ReassignSpotRequest, db: Session = Depends(get_db), curr
old_user = db.query(User).filter(User.id == old_user_id).first() if old_user_id else None
# Get spot display name for notifications
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
spot_name = assignment.spot.name if assignment.spot else "Unknown"
if data.new_user_id == "auto":
# "Auto assign" means releasing the spot so the system picks the next person
@@ -308,46 +352,49 @@ def reassign_spot(data: ReassignSpotRequest, db: Session = Depends(get_db), curr
if existing:
raise HTTPException(status_code=400, detail="User already has a spot for this date")
# Update assignment to new user
assignment.user_id = data.new_user_id
# Send notifications
# Notify old user that spot was reassigned
if old_user and old_user.id != new_user.id:
notify_parking_reassigned(old_user, assignment.date, spot_display_name, new_user.name)
notify_parking_reassigned(old_user, assignment.date, spot_name, new_user.name)
# Notify new user that spot was assigned
notify_parking_assigned(new_user, assignment.date, spot_display_name)
notify_parking_assigned(new_user, assignment.date, spot_name)
config.logger.info(f"Parking spot {spot_display_name} on {assignment.date} reassigned from {old_user.email if old_user else 'unassigned'} to {new_user.email}")
config.logger.info(f"Parking spot {spot_name} on {assignment.date} reassigned from {old_user.email if old_user else 'unassigned'} to {new_user.email}")
db.commit()
db.refresh(assignment)
result = AssignmentResponse(
id=assignment.id,
date=assignment.date,
spot_id=assignment.spot_id,
spot_display_name=spot_name,
user_id=assignment.user_id,
office_id=assignment.office_id
)
if assignment.user_id:
result.user_name = new_user.name
result.user_email = new_user.email
return result
else:
assignment.user_id = None
# Release (Delete assignment)
db.delete(assignment)
# Notify old user that spot was released
if old_user:
notify_parking_released(old_user, assignment.date, spot_display_name)
notify_parking_released(old_user, assignment.date, spot_name)
config.logger.info(f"Parking spot {spot_display_name} on {assignment.date} released by {old_user.email if old_user else 'unknown'}")
db.commit()
db.refresh(assignment)
# Build response
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
result = AssignmentResponse(
id=assignment.id,
date=assignment.date,
spot_id=assignment.spot_id,
spot_display_name=spot_display_name,
user_id=assignment.user_id,
office_id=assignment.office_id
)
if assignment.user_id:
user = db.query(User).filter(User.id == assignment.user_id).first()
if user:
result.user_name = user.name
result.user_email = user.email
return result
config.logger.info(f"Parking spot {spot_name} on {assignment.date} released by {old_user.email if old_user else 'unknown'}")
db.commit()
return {"message": "Spot released"}
@router.get("/eligible-users/{assignment_id}")
@@ -394,3 +441,91 @@ def get_eligible_users(assignment_id: str, db: Session = Depends(get_db), curren
})
return result
class TestEmailRequest(BaseModel):
date: date = None
office_id: str
@router.post("/test-email")
def send_test_email_tool(data: TestEmailRequest, db: Session = Depends(get_db), current_user=Depends(require_manager_or_admin)):
"""Send a test email to the current user (Test Tool)"""
from services.notifications import send_email
from database.models import OfficeClosingDay, OfficeWeeklyClosingDay
from datetime import timedelta
# Verify office access
if current_user.role == UserRole.MANAGER and current_user.office_id != data.office_id:
raise HTTPException(status_code=403, detail="Not authorized for this office")
target_date = data.date
if not target_date:
# Find next open day
# Start from tomorrow (or today? Prompt says "dopo il giorno corrente" -> after today)
check_date = date.today() + timedelta(days=1)
# Load closing rules
weekly_closed = db.query(OfficeWeeklyClosingDay.weekday).filter(
OfficeWeeklyClosingDay.office_id == data.office_id
).all()
weekly_closed_set = {w[0] for w in weekly_closed}
specific_closed = db.query(OfficeClosingDay).filter(
OfficeClosingDay.office_id == data.office_id,
OfficeClosingDay.date >= check_date
).all()
# Max lookahead 30 days to avoid infinite loop
found = False
for _ in range(30):
# Check weekly
if check_date.weekday() in weekly_closed_set:
check_date += timedelta(days=1)
continue
# Check specific
is_specific = False
for d in specific_closed:
s = d.date
e = d.end_date or d.date
if s <= check_date <= e:
is_specific = True
break
if is_specific:
check_date += timedelta(days=1)
continue
found = True
break
if found:
target_date = check_date
else:
# Fallback
target_date = date.today() + timedelta(days=1)
# Send Email
subject = f"Test Email - Parking System ({target_date})"
body_html = f"""
<html>
<body>
<h2>Parking System Test Email</h2>
<p>Hi {current_user.name},</p>
<p>This is a test email triggered from the Parking Manager Test Tools.</p>
<p><strong>Selected Date:</strong> {target_date}</p>
<p><strong>SMTP Status:</strong> {'Enabled' if config.SMTP_ENABLED else 'Disabled (File Logging)'}</p>
<p>If you received this, the notification system is working.</p>
</body>
</html>
"""
success = send_email(current_user.email, subject, body_html)
return {
"success": success,
"mode": "SMTP" if config.SMTP_ENABLED else "FILE",
"date": target_date,
"message": "Email sent successfully" if success else "Failed to send email"
}

View File

@@ -329,3 +329,49 @@ def get_user_presences(user_id: str, start_date: date = None, end_date: date = N
})
return result
class ClearOfficePresenceRequest(BaseModel):
start_date: date
end_date: date
office_id: str
@router.post("/admin/clear-office-presence")
def clear_office_presence(data: ClearOfficePresenceRequest, db: Session = Depends(get_db), current_user=Depends(require_manager_or_admin)):
"""Clear all presence and parking for an office in a date range (Test Tool)"""
# Verify office access
if current_user.role == UserRole.MANAGER and current_user.office_id != data.office_id:
raise HTTPException(status_code=403, detail="Not authorized for this office")
# Get all users in the office
users = db.query(User).filter(User.office_id == data.office_id).all()
user_ids = [u.id for u in users]
if not user_ids:
return {"message": "No users in office", "count_presence": 0, "count_parking": 0}
# 1. Delete Parking Assignments
parking_delete = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.user_id.in_(user_ids),
DailyParkingAssignment.date >= data.start_date,
DailyParkingAssignment.date <= data.end_date
)
parking_count = parking_delete.delete(synchronize_session=False)
# 2. Delete Presence
presence_delete = db.query(UserPresence).filter(
UserPresence.user_id.in_(user_ids),
UserPresence.date >= data.start_date,
UserPresence.date <= data.end_date
)
presence_count = presence_delete.delete(synchronize_session=False)
db.commit()
return {
"message": "Cleared office presence and parking",
"count_presence": presence_count,
"count_parking": parking_count
}

195
app/routes/reports.py Normal file
View File

@@ -0,0 +1,195 @@
from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from sqlalchemy.orm import Session
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from io import BytesIO
from database.connection import get_db
from database.models import User, UserPresence, DailyParkingAssignment, UserRole, Office
from utils.auth_middleware import require_manager_or_admin
router = APIRouter(prefix="/api/reports", tags=["reports"])
@router.get("/team-export")
def export_team_data(
start_date: date,
end_date: date,
office_id: Optional[str] = None,
db: Session = Depends(get_db),
current_user: User = Depends(require_manager_or_admin)
):
"""
Export team presence and parking data to Excel.
"""
# 1. Determine Scope (Admin vs Manager)
target_office_id = office_id
if current_user.role == UserRole.MANAGER:
# Manager is restricted to their own office
if office_id and office_id != current_user.office_id:
raise HTTPException(status_code=403, detail="Cannot export data for other offices")
target_office_id = current_user.office_id
# 2. Fetch Users
query = db.query(User)
if target_office_id:
query = query.filter(User.office_id == target_office_id)
users = query.all()
user_ids = [u.id for u in users]
# Map users for quick lookup
user_map = {u.id: u for u in users}
# 3. Fetch Presences
presences = db.query(UserPresence).filter(
UserPresence.user_id.in_(user_ids),
UserPresence.date >= start_date,
UserPresence.date <= end_date
).all()
# 4. Fetch Parking Assignments
assignments = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.user_id.in_(user_ids),
DailyParkingAssignment.date >= start_date,
DailyParkingAssignment.date <= end_date
).all()
# Organize data by Date -> User -> Info
# Structure: data_map[date_str][user_id] = { presence: ..., parking: ... }
data_map = {}
for p in presences:
d_str = p.date.isoformat()
if d_str not in data_map: data_map[d_str] = {}
if p.user_id not in data_map[d_str]: data_map[d_str][p.user_id] = {}
data_map[d_str][p.user_id]['presence'] = p.status.value # 'present', 'remote', etc.
for a in assignments:
d_str = a.date.isoformat()
if d_str not in data_map: data_map[d_str] = {}
if a.user_id not in data_map[d_str]: data_map[d_str][a.user_id] = {}
if a.spot:
data_map[d_str][a.user_id]['parking'] = a.spot.name
else:
data_map[d_str][a.user_id]['parking'] = "Unknown"
# 5. Generate Excel
wb = Workbook()
ws = wb.active
ws.title = "Report Presenze Matrix"
# --- Header Row (Dates) ---
# Column A: "Utente"
ws.cell(row=1, column=1, value="Utente")
# Generate date range
from datetime import timedelta
date_cols = {} # date_str -> col_index
col_idx = 2
curr = start_date
while curr <= end_date:
d_str = curr.isoformat()
# Header: DD/MM
header_val = f"{curr.day}/{curr.month}"
cell = ws.cell(row=1, column=col_idx, value=header_val)
date_cols[d_str] = col_idx
# Style Header
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal="center")
col_idx += 1
curr += timedelta(days=1)
# Style First Header (Utente)
first_header = ws.cell(row=1, column=1)
first_header.font = Font(bold=True)
first_header.alignment = Alignment(horizontal="left")
# Define Fills
fill_present = PatternFill(start_color="dcfce7", end_color="dcfce7", fill_type="solid") # Light Green
fill_remote = PatternFill(start_color="dbeafe", end_color="dbeafe", fill_type="solid") # Light Blue
fill_absent = PatternFill(start_color="fee2e2", end_color="fee2e2", fill_type="solid") # Light Red
fill_trip = PatternFill(start_color="fef3c7", end_color="fef3c7", fill_type="solid") # Light Orange (matching frontend warning-bg)
# --- User Rows ---
row_idx = 2
for user in users:
# User Name in Col 1
name_cell = ws.cell(row=row_idx, column=1, value=user.name)
name_cell.font = Font(bold=True)
# Determine Office label (optional append?)
# name_cell.value = f"{user.name} ({user.office.name})" if user.office else user.name
# Fill Dates
curr = start_date
while curr <= end_date:
d_str = curr.isoformat()
if d_str in date_cols:
c_idx = date_cols[d_str]
# Get Data
u_data = data_map.get(d_str, {}).get(user.id, {})
presence = u_data.get('presence', '')
parking = u_data.get('parking', '')
cell_val = ""
fill = None
if presence == 'present':
cell_val = "In Sede"
fill = fill_present
elif presence == 'remote':
cell_val = "Remoto"
fill = fill_remote
elif presence == 'absent':
cell_val = "Ferie"
fill = fill_absent
elif presence == 'business_trip':
cell_val = "Trasferta"
fill = fill_trip
# Append Parking info if present
if parking:
if cell_val:
cell_val += f" ({parking})"
else:
cell_val = f"({parking})" # Parking without presence? Unusual but possible
cell = ws.cell(row=row_idx, column=c_idx, value=cell_val)
if fill:
cell.fill = fill
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
curr += timedelta(days=1)
row_idx += 1
# Adjust column widths
ws.column_dimensions['A'].width = 25 # User name column
# Auto-width for date columns (approx)
for i in range(2, col_idx):
col_letter = ws.cell(row=1, column=i).column_letter
ws.column_dimensions[col_letter].width = 12
# Save to buffer
output = BytesIO()
wb.save(output)
output.seek(0)
filename = f"report_parking_matrix_{start_date}_{end_date}.xlsx"
headers = {
'Content-Disposition': f'attachment; filename="{filename}"'
}
return Response(
content=output.getvalue(),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers=headers
)

View File

@@ -2,13 +2,13 @@
User Management Routes
Admin user CRUD and user self-service (profile, settings, password)
"""
from datetime import datetime
from datetime import datetime, date
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr
from sqlalchemy.orm import Session
from database.connection import get_db
from database.models import User, UserRole, Office
from database.models import User, UserRole, Office, ParkingExclusion
from utils.auth_middleware import get_current_user, require_admin
from utils.helpers import (
generate_uuid, is_ldap_user, is_ldap_admin,
@@ -54,6 +54,20 @@ class ChangePasswordRequest(BaseModel):
new_password: str
class UserExclusionCreate(BaseModel):
start_date: date | None = None
end_date: date | None = None
notes: str | None = None
class UserExclusionResponse(BaseModel):
id: str
start_date: date | None
end_date: date | None
notes: str | None
is_excluded: bool = True
class UserResponse(BaseModel):
id: str
email: str
@@ -332,3 +346,105 @@ def change_password(data: ChangePasswordRequest, db: Session = Depends(get_db),
db.commit()
config.logger.info(f"User {current_user.email} changed password")
return {"message": "Password changed"}
# Exclusion Management (Self-Service)
# Exclusion Management (Self-Service)
@router.get("/me/exclusion", response_model=list[UserExclusionResponse])
def get_my_exclusions(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Get current user's parking exclusions"""
if not current_user.office_id:
return []
exclusions = db.query(ParkingExclusion).filter(
ParkingExclusion.user_id == current_user.id,
ParkingExclusion.office_id == current_user.office_id
).all()
return [
{
"id": e.id,
"start_date": e.start_date,
"end_date": e.end_date,
"notes": e.notes,
"is_excluded": True
}
for e in exclusions
]
@router.post("/me/exclusion")
def create_my_exclusion(data: UserExclusionCreate, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Create new parking exclusion for current user"""
if not current_user.office_id:
raise HTTPException(status_code=400, detail="User is not assigned to an office")
if data.start_date and data.end_date and data.end_date < data.start_date:
raise HTTPException(status_code=400, detail="End date must be after start date")
if data.end_date and not data.start_date:
raise HTTPException(status_code=400, detail="Start date is required if an end date is specified")
exclusion = ParkingExclusion(
id=generate_uuid(),
office_id=current_user.office_id,
user_id=current_user.id,
start_date=data.start_date,
end_date=data.end_date,
notes=data.notes or "Auto-esclusione utente",
created_at=datetime.utcnow()
)
db.add(exclusion)
db.commit()
return {"message": "Exclusion created", "id": exclusion.id}
@router.put("/me/exclusion/{exclusion_id}")
def update_my_exclusion(exclusion_id: str, data: UserExclusionCreate, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Update specific parking exclusion for current user"""
if not current_user.office_id:
raise HTTPException(status_code=400, detail="User is not assigned to an office")
exclusion = db.query(ParkingExclusion).filter(
ParkingExclusion.id == exclusion_id,
ParkingExclusion.user_id == current_user.id,
ParkingExclusion.office_id == current_user.office_id
).first()
if not exclusion:
raise HTTPException(status_code=404, detail="Exclusion not found")
if data.start_date and data.end_date and data.end_date < data.start_date:
raise HTTPException(status_code=400, detail="End date must be after start date")
if data.end_date and not data.start_date:
raise HTTPException(status_code=400, detail="Start date is required if an end date is specified")
exclusion.start_date = data.start_date
exclusion.end_date = data.end_date
if data.notes is not None:
exclusion.notes = data.notes
exclusion.updated_at = datetime.utcnow()
db.commit()
return {"message": "Exclusion updated", "id": exclusion.id}
@router.delete("/me/exclusion/{exclusion_id}")
def delete_my_exclusion(exclusion_id: str, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Remove specific parking exclusion for current user"""
if not current_user.office_id:
raise HTTPException(status_code=400, detail="User is not assigned to an office")
exclusion = db.query(ParkingExclusion).filter(
ParkingExclusion.id == exclusion_id,
ParkingExclusion.user_id == current_user.id,
ParkingExclusion.office_id == current_user.office_id
).first()
if not exclusion:
raise HTTPException(status_code=404, detail="Exclusion not found")
db.delete(exclusion)
db.commit()
return {"message": "Exclusion removed"}