feat: time tracking / work logs (create, list, summary, project summary, CLI commands)
This commit is contained in:
97
app/main.py
97
app/main.py
@@ -269,6 +269,7 @@ def startup():
|
||||
from app.models import activity
|
||||
from app.models import milestone
|
||||
from app.models import notification
|
||||
from app.models import worklog
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
@@ -1014,3 +1015,99 @@ def assign_issue(issue_id: int, assignee_id: int, db: Session = Depends(get_db))
|
||||
"issue", issue.id)
|
||||
|
||||
return {"issue_id": issue.id, "assignee_id": assignee_id, "title": issue.title}
|
||||
|
||||
|
||||
# ============ Work Logs / Time Tracking ============
|
||||
|
||||
from app.models.worklog import WorkLog
|
||||
|
||||
|
||||
class WorkLogCreate(PydanticBaseModel):
|
||||
issue_id: int
|
||||
user_id: int
|
||||
hours: float
|
||||
description: str | None = None
|
||||
logged_date: datetime
|
||||
|
||||
|
||||
class WorkLogResponse(PydanticBaseModel):
|
||||
id: int
|
||||
issue_id: int
|
||||
user_id: int
|
||||
hours: float
|
||||
description: str | None = None
|
||||
logged_date: datetime
|
||||
created_at: datetime
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@app.post("/worklogs", response_model=WorkLogResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_worklog(wl: WorkLogCreate, db: Session = Depends(get_db)):
|
||||
"""Log time spent on an issue."""
|
||||
issue = db.query(models.Issue).filter(models.Issue.id == wl.issue_id).first()
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="Issue not found")
|
||||
user = db.query(models.User).filter(models.User.id == wl.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if wl.hours <= 0:
|
||||
raise HTTPException(status_code=400, detail="Hours must be positive")
|
||||
db_wl = WorkLog(**wl.model_dump())
|
||||
db.add(db_wl)
|
||||
db.commit()
|
||||
db.refresh(db_wl)
|
||||
return db_wl
|
||||
|
||||
|
||||
@app.get("/issues/{issue_id}/worklogs", response_model=List[WorkLogResponse])
|
||||
def list_issue_worklogs(issue_id: int, db: Session = Depends(get_db)):
|
||||
"""List all work logs for an issue."""
|
||||
return db.query(WorkLog).filter(WorkLog.issue_id == issue_id).order_by(WorkLog.logged_date.desc()).all()
|
||||
|
||||
|
||||
@app.get("/issues/{issue_id}/worklogs/summary")
|
||||
def issue_worklog_summary(issue_id: int, db: Session = Depends(get_db)):
|
||||
"""Get total hours logged on an issue."""
|
||||
issue = db.query(models.Issue).filter(models.Issue.id == issue_id).first()
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="Issue not found")
|
||||
from sqlalchemy import func as sqlfunc
|
||||
total = db.query(sqlfunc.sum(WorkLog.hours)).filter(WorkLog.issue_id == issue_id).scalar() or 0
|
||||
count = db.query(WorkLog).filter(WorkLog.issue_id == issue_id).count()
|
||||
return {"issue_id": issue_id, "total_hours": round(total, 2), "log_count": count}
|
||||
|
||||
|
||||
@app.get("/users/{user_id}/worklogs", response_model=List[WorkLogResponse])
|
||||
def list_user_worklogs(user_id: int, limit: int = 50, db: Session = Depends(get_db)):
|
||||
"""List work logs by user."""
|
||||
return db.query(WorkLog).filter(WorkLog.user_id == user_id).order_by(WorkLog.logged_date.desc()).limit(limit).all()
|
||||
|
||||
|
||||
@app.delete("/worklogs/{worklog_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_worklog(worklog_id: int, db: Session = Depends(get_db)):
|
||||
wl = db.query(WorkLog).filter(WorkLog.id == worklog_id).first()
|
||||
if not wl:
|
||||
raise HTTPException(status_code=404, detail="Work log not found")
|
||||
db.delete(wl)
|
||||
db.commit()
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/projects/{project_id}/worklogs/summary")
|
||||
def project_worklog_summary(project_id: int, db: Session = Depends(get_db)):
|
||||
"""Get time tracking summary for a project."""
|
||||
from sqlalchemy import func as sqlfunc
|
||||
results = db.query(
|
||||
models.User.id,
|
||||
models.User.username,
|
||||
sqlfunc.sum(WorkLog.hours).label("total_hours"),
|
||||
sqlfunc.count(WorkLog.id).label("log_count")
|
||||
).join(WorkLog, WorkLog.user_id == models.User.id)\
|
||||
.join(models.Issue, WorkLog.issue_id == models.Issue.id)\
|
||||
.filter(models.Issue.project_id == project_id)\
|
||||
.group_by(models.User.id, models.User.username).all()
|
||||
|
||||
total = sum(r.total_hours for r in results)
|
||||
by_user = [{"user_id": r.id, "username": r.username, "hours": round(r.total_hours, 2), "logs": r.log_count} for r in results]
|
||||
return {"project_id": project_id, "total_hours": round(total, 2), "by_user": by_user}
|
||||
|
||||
Reference in New Issue
Block a user