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

@@ -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"
}