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

@@ -13,7 +13,7 @@ from sqlalchemy.orm import Session
from sqlalchemy import or_
from database.models import (
DailyParkingAssignment, User, UserPresence, Office,
DailyParkingAssignment, User, UserPresence, Office, OfficeSpot,
ParkingGuarantee, ParkingExclusion, OfficeClosingDay, OfficeWeeklyClosingDay,
UserRole, PresenceStatus
)
@@ -23,45 +23,20 @@ from app import config
def get_spot_prefix(office: Office, db: Session) -> str:
"""Get the spot prefix for an office (from office.spot_prefix or auto-assign)"""
# Logic moved to Office creation/update mostly, but keeping helper if needed
if office.spot_prefix:
return office.spot_prefix
# Auto-assign based on alphabetical order of offices without prefix
offices = db.query(Office).filter(
Office.spot_prefix == None
).order_by(Office.name).all()
# Find existing prefixes
existing_prefixes = set(
o.spot_prefix for o in db.query(Office).filter(
Office.spot_prefix != None
).all()
)
# Find first available letter
office_index = next((i for i, o in enumerate(offices) if o.id == office.id), 0)
letter = 'A'
count = 0
while letter in existing_prefixes or count < office_index:
if letter not in existing_prefixes:
count += 1
letter = chr(ord(letter) + 1)
if ord(letter) > ord('Z'):
letter = 'A'
break
return letter
return "A" # Fallback
def get_spot_display_name(spot_id: str, office_id: str, db: Session) -> str:
"""Get display name for a spot (e.g., 'A3' instead of 'spot-3')"""
office = db.query(Office).filter(Office.id == office_id).first()
if not office:
return spot_id
prefix = get_spot_prefix(office, db)
spot_number = spot_id.replace("spot-", "")
return f"{prefix}{spot_number}"
"""Get display name for a spot"""
# Now easy: fetch from OfficeSpot
# But wait: spot_id in assignment IS the OfficeSpot.id
spot = db.query(OfficeSpot).filter(OfficeSpot.id == spot_id).first()
if spot:
return spot.name
return "Unknown"
def is_closing_day(office_id: str, check_date: date, db: Session) -> bool:
@@ -95,35 +70,36 @@ def is_closing_day(office_id: str, check_date: date, db: Session) -> bool:
def initialize_parking_pool(office_id: str, quota: int, pool_date: date, db: Session) -> int:
"""Initialize empty parking spots for an office's pool on a given date.
Returns 0 if it's a closing day (no parking available).
"""
# Don't create pool on closing days
Get total capacity for the date.
(Legacy name kept for compatibility, but now it just returns count).
"""
if is_closing_day(office_id, pool_date, db):
return 0
existing = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.office_id == office_id,
DailyParkingAssignment.date == pool_date
return db.query(OfficeSpot).filter(
OfficeSpot.office_id == office_id,
OfficeSpot.is_unavailable == False
).count()
if existing > 0:
return existing
for i in range(1, quota + 1):
spot = DailyParkingAssignment(
id=generate_uuid(),
date=pool_date,
spot_id=f"spot-{i}",
user_id=None,
office_id=office_id,
created_at=datetime.now(timezone.utc)
)
db.add(spot)
db.commit()
config.logger.debug(f"Initialized {quota} parking spots for office {office_id} on {pool_date}")
return quota
def get_available_spots(office_id: str, pool_date: date, db: Session) -> list[OfficeSpot]:
"""Get list of unassigned OfficeSpots for a date"""
# 1. Get all active spots
all_spots = db.query(OfficeSpot).filter(
OfficeSpot.office_id == office_id,
OfficeSpot.is_unavailable == False
).all()
# 2. Get assigned spot IDs
assigned_ids = db.query(DailyParkingAssignment.spot_id).filter(
DailyParkingAssignment.office_id == office_id,
DailyParkingAssignment.date == pool_date
).all()
assigned_set = {a[0] for a in assigned_ids}
# 3. Filter
return [s for s in all_spots if s.id not in assigned_set]
def get_user_parking_ratio(user_id: str, office_id: str, db: Session) -> float:
@@ -151,21 +127,31 @@ def get_user_parking_ratio(user_id: str, office_id: str, db: Session) -> float:
def is_user_excluded(user_id: str, office_id: str, check_date: date, db: Session) -> bool:
"""Check if user is excluded from parking for this date"""
exclusion = db.query(ParkingExclusion).filter(
exclusions = db.query(ParkingExclusion).filter(
ParkingExclusion.office_id == office_id,
ParkingExclusion.user_id == user_id
).first()
).all()
if not exclusion:
if not exclusions:
return False
# Check date range
if exclusion.start_date and check_date < exclusion.start_date:
return False
if exclusion.end_date and check_date > exclusion.end_date:
return False
# Check against all exclusions
for exclusion in exclusions:
# If any exclusion covers this date, user is excluded
# Check date range
start_ok = True
if exclusion.start_date and check_date < exclusion.start_date:
start_ok = False
end_ok = True
if exclusion.end_date and check_date > exclusion.end_date:
end_ok = False
if start_ok and end_ok:
return True
return True
return False
def has_guarantee(user_id: str, office_id: str, check_date: date, db: Session) -> bool:
@@ -227,47 +213,45 @@ def get_users_wanting_parking(office_id: str, pool_date: date, db: Session) -> l
return candidates
def assign_parking_fairly(office_id: str, pool_date: date, db: Session) -> dict:
"""
Assign parking spots fairly based on parking ratio.
Called after presence is set for a date.
Returns {assigned: [...], waitlist: [...]}
Creates new DailyParkingAssignment rows only for assigned users.
"""
office = db.query(Office).filter(Office.id == office_id).first()
if not office or not office.parking_quota:
return {"assigned": [], "waitlist": []}
# No parking on closing days
if is_closing_day(office_id, pool_date, db):
return {"assigned": [], "waitlist": [], "closed": True}
# Initialize pool
initialize_parking_pool(office_id, office.parking_quota, pool_date, db)
return {"assigned": [], "waitlist": [], "closed": True}
# Get candidates sorted by fairness
candidates = get_users_wanting_parking(office_id, pool_date, db)
# Get available spots
free_spots = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.office_id == office_id,
DailyParkingAssignment.date == pool_date,
DailyParkingAssignment.user_id == None
).all()
# Get available spots (OfficeSpots not yet in assignments table)
free_spots = get_available_spots(office_id, pool_date, db)
assigned = []
waitlist = []
for candidate in candidates:
if free_spots:
# Sort spots by number to fill A1, A2... order
free_spots.sort(key=lambda s: s.spot_number)
spot = free_spots.pop(0)
spot.user_id = candidate["user_id"]
# Create assignment
assignment = DailyParkingAssignment(
id=generate_uuid(),
date=pool_date,
spot_id=spot.id,
user_id=candidate["user_id"],
office_id=office_id,
created_at=datetime.now(timezone.utc)
)
db.add(assignment)
assigned.append(candidate["user_id"])
else:
waitlist.append(candidate["user_id"])
db.commit()
return {"assigned": assigned, "waitlist": waitlist}
@@ -281,15 +265,29 @@ def release_user_spot(office_id: str, user_id: str, pool_date: date, db: Session
if not assignment:
return False
# Capture spot ID before deletion
spot_id = assignment.spot_id
# Release the spot
assignment.user_id = None
# Release the spot (Delete the row)
db.delete(assignment)
db.commit()
# Try to assign to next user in fairness queue
candidates = get_users_wanting_parking(office_id, pool_date, db)
if candidates:
assignment.user_id = candidates[0]["user_id"]
top_candidate = candidates[0]
# Create new assignment for top candidate
new_assignment = DailyParkingAssignment(
id=generate_uuid(),
date=pool_date,
spot_id=spot_id,
user_id=top_candidate["user_id"],
office_id=office_id,
created_at=datetime.now(timezone.utc)
)
db.add(new_assignment)
db.commit()
return True
@@ -309,8 +307,7 @@ def handle_presence_change(user_id: str, change_date: date, old_status: Presence
if not office or not office.parking_quota:
return
# Initialize pool if needed
initialize_parking_pool(office.id, office.parking_quota, change_date, db)
# No initialization needed for sparse model
if old_status == PresenceStatus.PRESENT and new_status in [PresenceStatus.REMOTE, PresenceStatus.ABSENT]:
# User no longer coming - release their spot (will auto-reassign)
@@ -344,13 +341,12 @@ def clear_assignments_for_office_date(office_id: str, pool_date: date, db: Sessi
"""
assignments = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.office_id == office_id,
DailyParkingAssignment.date == pool_date,
DailyParkingAssignment.user_id != None
DailyParkingAssignment.date == pool_date
).all()
count = len(assignments)
for a in assignments:
a.user_id = None
db.delete(a)
db.commit()
return count