timekeeper/app/routes/clock_edit.py
2026-01-15 15:46:35 -05:00

98 lines
3.3 KiB
Python

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)