feat: aggiunti: loggica random, tema scuro, correzioni mail, miglioramenti generali, cache;

This commit is contained in:
StefanoSalemi
2026-04-17 18:27:37 +02:00
parent a7ef46640d
commit 104ad53a9a
26 changed files with 861 additions and 216 deletions

View File

@@ -138,6 +138,33 @@ def notify_parking_released(user: "User", assignment_date: date, spot_name: str)
send_email(user.email, subject, body_html)
def notify_parking_released_to_user(user: "User", assignment_date: date, spot_name: str, previous_user_name: str = None):
"""Send notification when a parking spot is granted due to someone else releasing it"""
if not user.notify_parking_changes:
return
day_name = assignment_date.strftime("%d/%m/%Y")
subject = f"Assegnazione Riparatoria - {day_name}"
if previous_user_name:
message = f"Ti è stato ceduto il posto da {previous_user_name} per il giorno {day_name}:"
else:
message = f"Hai ottenuto un posto in ritardo per una rinuncia per il giorno {day_name}:"
body_html = f"""
<html>
<body>
<h2>Posto Auto Assegnato (Rinuncia)</h2>
<p>Ciao {user.name},</p>
<p>{message}</p>
<p style="font-size: 18px; font-weight: bold;">Posto {spot_name}</p>
<p>Cordiali saluti,<br>Team Parking Manager</p>
</body>
</html>
"""
send_email(user.email, subject, body_html)
def notify_parking_reassigned(user: "User", assignment_date: date, spot_name: str, new_user_name: str):
"""Send notification when parking spot is reassigned to someone else"""
if not user.notify_parking_changes:
@@ -225,7 +252,7 @@ def send_presence_reminder(user: "User", next_week_dates: List[date], db: "Sessi
def send_daily_parking_reminder(user: "User", date_obj: datetime, db: "Session") -> bool:
"""Send daily parking reminder for a specific date"""
from database.models import DailyParkingAssignment, NotificationLog
from database.models import DailyParkingAssignment
from services.parking import get_spot_display_name
config.logger.info(f"[SCHEDULER] Checking daily parking reminder for user {user.email}")
@@ -234,19 +261,8 @@ def send_daily_parking_reminder(user: "User", date_obj: datetime, db: "Session")
config.logger.debug(f"[SCHEDULER] User {user.email} has disabled daily parking notifications")
return False
date_str = date_obj.strftime("%Y-%m-%d")
assignment_date = date_obj.date()
# Check if already sent for this date
existing = db.query(NotificationLog).filter(
NotificationLog.user_id == user.id,
NotificationLog.notification_type == NotificationType.DAILY_PARKING,
NotificationLog.reference_date == date_str
).first()
if existing:
return False
# Get parking assignment for this date
assignment = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.user_id == user.id,
@@ -273,15 +289,6 @@ def send_daily_parking_reminder(user: "User", date_obj: datetime, db: "Session")
"""
if send_email(user.email, subject, body_html):
log = NotificationLog(
id=generate_uuid(),
user_id=user.id,
notification_type=NotificationType.DAILY_PARKING,
reference_date=date_str,
sent_at=datetime.utcnow()
)
db.add(log)
db.commit()
return True
return False
@@ -321,7 +328,7 @@ def run_scheduled_notifications(db: "Session"):
user_minute = user.notify_daily_parking_minute or 0
# Check if it's the right time for this user
if current_hour == user_hour and abs(current_minute - user_minute) < 5:
if current_hour == user_hour and current_minute == user_minute:
config.logger.info(f"[SCHEDULER] Triggering Daily Parking Reminder check for user {user.email} (Scheduled: {user_hour}:{user_minute})")
# Check if Office is OPEN today
is_office_open = True

View File

@@ -216,15 +216,29 @@ def get_users_wanting_parking(office_id: str, pool_date: date, db: Session) -> l
def assign_parking_fairly(office_id: str, pool_date: date, db: Session) -> dict:
"""
Assign parking spots fairly based on parking ratio.
Assign parking spots based on the office's assignment_mode (fairness or random).
Creates new DailyParkingAssignment rows only for assigned users.
"""
if is_closing_day(office_id, pool_date, db):
return {"assigned": [], "waitlist": [], "closed": True}
# Get candidates sorted by fairness
# Retrieve office to check assignment mode
office = db.query(Office).filter(Office.id == office_id).first()
mode = office.assignment_mode if office and getattr(office, 'assignment_mode', None) else "fairness"
# Get candidates sorted by fairness (guaranteed first, then by ratio)
candidates = get_users_wanting_parking(office_id, pool_date, db)
if mode == "random":
import random
guaranteed = [c for c in candidates if c["has_guarantee"]]
non_guaranteed = [c for c in candidates if not c["has_guarantee"]]
# Shuffle non-guaranteed users to pick randomly
random.shuffle(non_guaranteed)
candidates = guaranteed + non_guaranteed
# Get available spots (OfficeSpots not yet in assignments table)
free_spots = get_available_spots(office_id, pool_date, db)
@@ -281,6 +295,10 @@ def release_user_spot(office_id: str, user_id: str, pool_date: date, db: Session
if not assignment:
return False
# Get old user name for notification
old_user = db.query(User).filter(User.id == user_id).first()
old_user_name = old_user.name if old_user else "un collega"
# Capture spot ID before deletion
spot_id = assignment.spot_id
@@ -305,6 +323,13 @@ def release_user_spot(office_id: str, user_id: str, pool_date: date, db: Session
db.add(new_assignment)
db.commit()
# Notify the lucky user
from services.notifications import notify_parking_released_to_user
top_user = db.query(User).filter(User.id == top_candidate["user_id"]).first()
if top_user:
spot_name = get_spot_display_name(spot_id, office_id, db)
notify_parking_released_to_user(top_user, pool_date, spot_name, old_user_name)
return True
@@ -328,31 +353,6 @@ def handle_presence_change(user_id: str, change_date: date, old_status: Presence
# User no longer coming - release their spot (will auto-reassign)
release_user_spot(office.id, user_id, change_date, db)
elif new_status == PresenceStatus.PRESENT:
# Check booking window
should_assign = True
if office.booking_window_enabled:
from zoneinfo import ZoneInfo
tz = ZoneInfo(config.TIMEZONE)
now = datetime.now(tz)
# Allocation time is Day-1 at cutoff hour
cutoff_dt = datetime.combine(change_date - timedelta(days=1), datetime.min.time())
cutoff_dt = cutoff_dt.replace(
hour=office.booking_window_end_hour,
minute=office.booking_window_end_minute,
tzinfo=tz
)
# If now is before cutoff, do not assign yet (wait for batch job)
if now < cutoff_dt:
should_assign = False
config.logger.debug(f"Queuing parking request for user {user_id} on {change_date} (Window open until {cutoff_dt})")
if should_assign:
# User coming in - run fair assignment for this date
assign_parking_fairly(office.id, change_date, db)
def clear_assignments_for_office_date(office_id: str, pool_date: date, db: Session) -> int:
"""
@@ -405,7 +405,24 @@ def process_daily_allocations(db: Session):
# Cutoff is defined as "Previous Day" (today) at Booking End Hour
# If NOW matches the cutoff time, we run allocation for TOMORROW
if now.hour == office.booking_window_end_hour and now.minute == office.booking_window_end_minute:
# Non eseguiamo l'assegnazione se oggi è un giorno di chiusura
# (è già stata fatta l'assegnazione per i giorni futuri nell'ultimo giorno lavorativo)
if is_closing_day(office.id, now.date(), db):
config.logger.info(f"[SCHEDULER] Skipping batch allocation for {office.name} because today ({now.date()}) is a closing day.")
continue
# Troviamo il prossimo giorno lavorativo a partire da "domani"
target_date = now.date() + timedelta(days=1)
days_ahead = 1
while is_closing_day(office.id, target_date, db) and days_ahead <= 30:
target_date += timedelta(days=1)
days_ahead += 1
if days_ahead > 30:
config.logger.warning(f"[SCHEDULER] Could not find a working day within 30 days for {office.name}.")
continue
config.logger.info(f"[SCHEDULER] CUTOFF REACHED for {office.name} (Cutoff: {office.booking_window_end_hour}:{office.booking_window_end_minute}). Starting Assegnazione Giornaliera parcheggi for {target_date}")
try: