98 lines
3.3 KiB
Python
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) |