from datetime import datetime, timedelta from typing import Optional from fastapi import APIRouter, Depends, Form, Request from fastapi.responses import RedirectResponse from sqlalchemy.orm import Session from ..db import get_db from ..models import TimeEntry router = APIRouter() def _parse_dt_local(v: Optional[str]) -> Optional[datetime]: """ Parse HTML datetime-local values safely: - 'YYYY-MM-DDTHH:MM' - 'YYYY-MM-DDTHH:MM:SS' Returns None for blank/None. """ if not v: return None v = v.strip() if not v: return None try: # Python 3.11+ supports fromisoformat for both shapes return datetime.fromisoformat(v) except Exception: for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"): try: return datetime.strptime(v, fmt) except ValueError: continue return None def _safe_float(v, default=None): if v is None or v == "": return default try: return float(v) except Exception: return default @router.post("/timesheet/update-clocks") async def update_clocks( request: Request, entry_id: int = Form(...), clock_in: Optional[str] = Form(None), clock_out: Optional[str] = Form(None), recalc_total: Optional[str] = Form(None), redirect_to: Optional[str] = Form(None), db: Session = Depends(get_db), ): """ Update only the clock_in/clock_out fields for a time entry. Optionally recalculate total_hours from clocks when recalc_total=1. Re-applies hours_paid = max(0, total - break) + PTO + Holiday + Bereavement. """ # Optional: enforce admin-only is_admin = bool(getattr(request, "session", {}).get("is_admin")) if hasattr(request, "session") else True if not is_admin: # Fall back to normal redirect with no change return RedirectResponse(url=redirect_to or "/", status_code=303) entry: TimeEntry = db.query(TimeEntry).filter(TimeEntry.id == entry_id).first() if not entry: return RedirectResponse(url=redirect_to or "/", status_code=303) new_ci = _parse_dt_local(clock_in) new_co = _parse_dt_local(clock_out) # Apply if provided; if one is provided and the other left blank, keep the existing other value if new_ci is not None: entry.clock_in = new_ci if new_co is not None: entry.clock_out = new_co # Handle overnight: if both set and out <= in, assume next day if entry.clock_in and entry.clock_out and entry.clock_out <= entry.clock_in: entry.clock_out = entry.clock_out + timedelta(days=1) # Optionally recompute total from clocks if recalc_total == "1" and entry.clock_in and entry.clock_out: entry.total_hours = round((entry.clock_out - entry.clock_in).total_seconds() / 3600.0, 2) # Recompute hours_paid using the canonical rule total = _safe_float(entry.total_hours, 0.0) or 0.0 brk = _safe_float(entry.break_hours, 0.0) or 0.0 pto = _safe_float(entry.pto_hours, 0.0) or 0.0 hol = _safe_float(entry.holiday_hours, 0.0) or 0.0 oth = _safe_float(entry.bereavement_hours, 0.0) or 0.0 worked = max(0.0, total - brk) entry.hours_paid = round(worked + pto + hol + oth, 2) db.add(entry) db.commit() return RedirectResponse(url=redirect_to or "/", status_code=303)