Add day-grouped timesheet with day accept and commit history
This commit is contained in:
+62
-15
@@ -3,9 +3,10 @@ import os
|
||||
import secrets
|
||||
import shutil
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from urllib.parse import urlencode
|
||||
from uuid import uuid4
|
||||
|
||||
import pdfplumber
|
||||
@@ -21,7 +22,7 @@ from starlette.middleware.sessions import SessionMiddleware
|
||||
from .database import Base, engine, get_db
|
||||
from .models import CallImport, RawEvent, TimeEntry
|
||||
from .schemas import TimeTrackSync, ZedSync
|
||||
from .services import TZ, derive_zed_suggestions, overlaps_accepted, parse_timestamp
|
||||
from .services import TZ, derive_zed_suggestions, group_commits_by_day, group_entries_by_day, overlaps_accepted, parse_timestamp
|
||||
|
||||
APP_TIMEZONE = os.environ.get("APP_TIMEZONE", "Europe/Vienna")
|
||||
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "")
|
||||
@@ -157,24 +158,52 @@ def sync_zed_commits(payload: ZedSync, db: Session = Depends(get_db)) -> dict:
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index(request: Request, start: str | None = None, end: str | None = None, db: Session = Depends(get_db)):
|
||||
require_web(request)
|
||||
query = select(TimeEntry).order_by(TimeEntry.start_at.desc())
|
||||
entries = db.scalars(query).all()
|
||||
if start:
|
||||
start_date = datetime.fromisoformat(start).replace(tzinfo=TZ)
|
||||
entries = [e for e in entries if e.start_at >= start_date]
|
||||
if end:
|
||||
end_date = datetime.fromisoformat(end).replace(tzinfo=TZ).replace(hour=23, minute=59, second=59)
|
||||
entries = [e for e in entries if e.start_at <= end_date]
|
||||
accepted = sum(e.duration_seconds for e in entries if e.status == "accepted")
|
||||
suggested = sum(e.duration_seconds for e in entries if e.status == "suggested")
|
||||
start_date = datetime.fromisoformat(start).replace(tzinfo=TZ) if start else None
|
||||
end_date = datetime.fromisoformat(end).replace(tzinfo=TZ).replace(hour=23, minute=59, second=59) if end else None
|
||||
|
||||
entries = list(db.scalars(select(TimeEntry)))
|
||||
entries = [
|
||||
entry for entry in entries
|
||||
if (start_date is None or entry.start_at >= start_date)
|
||||
and (end_date is None or entry.start_at <= end_date)
|
||||
]
|
||||
commits = list(db.scalars(select(RawEvent).where(RawEvent.source == "zed_commit").order_by(RawEvent.occurred_at)))
|
||||
commits = [
|
||||
commit for commit in commits
|
||||
if commit.occurred_at is not None
|
||||
and (start_date is None or commit.occurred_at >= start_date)
|
||||
and (end_date is None or commit.occurred_at <= end_date)
|
||||
]
|
||||
|
||||
entry_days = group_entries_by_day(entries)
|
||||
commit_days = group_commits_by_day(commits)
|
||||
days = []
|
||||
for day_date in sorted(set(entry_days) | set(commit_days), reverse=True):
|
||||
day_entries = entry_days.get(day_date, [])
|
||||
days.append({
|
||||
"date": day_date,
|
||||
"entries": day_entries,
|
||||
"accepted": sum(e.duration_seconds for e in day_entries if e.status == "accepted"),
|
||||
"suggested": sum(e.duration_seconds for e in day_entries if e.status == "suggested"),
|
||||
"commits": commit_days.get(day_date, []),
|
||||
})
|
||||
|
||||
return templates.TemplateResponse(request, "index.html", {
|
||||
"entries": entries, "accepted": accepted, "suggested": suggested,
|
||||
"days": days,
|
||||
"accepted": sum(day["accepted"] for day in days),
|
||||
"suggested": sum(day["suggested"] for day in days),
|
||||
"start": start or "", "end": end or "", "timezone": APP_TIMEZONE, "display_tz": TZ,
|
||||
"format_seconds": lambda seconds: f"{seconds // 3600}h {(seconds % 3600) // 60:02d}m",
|
||||
"overlaps_accepted": lambda entry: overlaps_accepted(db, entry),
|
||||
})
|
||||
|
||||
|
||||
def index_redirect(start: str | None, end: str | None) -> RedirectResponse:
|
||||
params = {name: value for name, value in (("start", start), ("end", end)) if value}
|
||||
suffix = "?" + urlencode(params) if params else ""
|
||||
return RedirectResponse("/" + suffix, status_code=303)
|
||||
|
||||
|
||||
@app.post("/entries/manual")
|
||||
def create_manual(request: Request, start_at: Annotated[str, Form()], end_at: Annotated[str, Form()], task: Annotated[str, Form()] = "", project_slug: Annotated[str, Form()] = "", db: Session = Depends(get_db)):
|
||||
require_web(request)
|
||||
@@ -209,8 +238,26 @@ def edit_entry(entry_id: int, request: Request, start_at: Annotated[str, Form()]
|
||||
return RedirectResponse("/", status_code=303)
|
||||
|
||||
|
||||
@app.post("/days/{day}/accept")
|
||||
def accept_day(day: str, request: Request, start: Annotated[str, Form()] = "", end: Annotated[str, Form()] = "", db: Session = Depends(get_db)):
|
||||
require_web(request)
|
||||
try:
|
||||
target = date.fromisoformat(day)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(422, "Invalid day") from exc
|
||||
changed = 0
|
||||
for entry in db.scalars(select(TimeEntry)).all():
|
||||
if entry.status == "suggested" and entry.start_at.astimezone(TZ).date() == target:
|
||||
entry.status = "accepted"
|
||||
if entry.kind == "zed_inferred":
|
||||
entry.kind = "manual"
|
||||
changed += 1
|
||||
db.commit()
|
||||
return index_redirect(start or None, end or None)
|
||||
|
||||
|
||||
@app.post("/entries/{entry_id}/status")
|
||||
def set_entry_status(entry_id: int, request: Request, status: Annotated[str, Form()], db: Session = Depends(get_db)):
|
||||
def set_entry_status(entry_id: int, request: Request, status: Annotated[str, Form()], start: Annotated[str, Form()] = "", end: Annotated[str, Form()] = "", db: Session = Depends(get_db)):
|
||||
require_web(request)
|
||||
if status not in {"accepted", "rejected", "suggested"}:
|
||||
raise HTTPException(422, "Invalid status")
|
||||
@@ -222,7 +269,7 @@ def set_entry_status(entry_id: int, request: Request, status: Annotated[str, For
|
||||
if status == "accepted" and entry.kind == "zed_inferred":
|
||||
entry.kind = "manual"
|
||||
db.commit()
|
||||
return RedirectResponse("/", status_code=303)
|
||||
return index_redirect(start or None, end or None)
|
||||
|
||||
|
||||
@app.post("/call-imports", response_class=HTMLResponse)
|
||||
|
||||
Reference in New Issue
Block a user