diff --git a/src/api/productivity/urls.py b/src/api/productivity/urls.py index e69de29..0d3adb5 100644 --- a/src/api/productivity/urls.py +++ b/src/api/productivity/urls.py @@ -0,0 +1,46 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.productivity_dashboard, name="productivity_dashboard"), + # Notes + path("notes/", views.note_list, name="note_list"), + path("notes/create/", views.note_create, name="note_create"), + path("notes//edit/", views.note_edit, name="note_edit"), + path("notes//delete/", views.note_delete, name="note_delete"), + # Time entries + path("time-entries/", views.timeentry_list, name="timeentry_list"), + path("time-entries/create/", views.timeentry_create, name="timeentry_create"), + path("time-entries//edit/", views.timeentry_edit, name="timeentry_edit"), + path( + "time-entries//delete/", + views.timeentry_delete, + name="timeentry_delete", + ), +] + +from django.urls import path + +from . import views + +urlpatterns = [ + # Dashboard + path("", views.productivity_dashboard, name="productivity_dashboard"), + # Notes + path("notes/", views.note_list, name="note_list"), + path("notes/create/", views.note_create, name="note_create"), + path("notes//edit/", views.note_edit, name="note_edit"), + path("notes//delete/", views.note_delete, name="note_delete"), + # Time entries + path("time-entries/", views.timeentry_list, name="timeentry_list"), + path("time-entries/create/", views.timeentry_create, name="timeentry_create"), + path("time-entries//edit/", views.timeentry_edit, name="timeentry_edit"), + path( + "time-entries//delete/", + views.timeentry_delete, + name="timeentry_delete", + ), +] + + diff --git a/src/api/productivity/views.py b/src/api/productivity/views.py index e69de29..4187c3a 100644 --- a/src/api/productivity/views.py +++ b/src/api/productivity/views.py @@ -0,0 +1,166 @@ +from datetime import datetime + +from django.shortcuts import get_object_or_404, redirect, render +from django.views.decorators.http import require_http_methods + +from src.models.productivity import Note, TimeEntry +from src.models.projects import Project, Task + + +def productivity_dashboard(request): + recent_notes = Note.objects.all()[:5] + recent_time_entries = ( + TimeEntry.objects.select_related("project", "task").all()[:5] + ) + total_tracked_hours = ( + sum( + (entry.duration.total_seconds() for entry in recent_time_entries if entry.duration), + 0, + ) + / 3600 + if recent_time_entries + else 0 + ) + return render( + request, + "productivity/dashboard.html", + { + "recent_notes": recent_notes, + "recent_time_entries": recent_time_entries, + "total_tracked_hours": total_tracked_hours, + }, + ) + + +def note_list(request): + notes = Note.objects.all() + return render(request, "productivity/note_list.html", {"notes": notes}) + + +def note_create(request): + if request.method == "POST": + title = request.POST.get("title", "").strip() + content = request.POST.get("content", "").strip() + + if title and content: + Note.objects.create(title=title, content=content) + return redirect("note_list") + + return render(request, "productivity/note_form.html") + + +def note_edit(request, pk): + note = get_object_or_404(Note, pk=pk) + + if request.method == "POST": + title = request.POST.get("title", "").strip() + content = request.POST.get("content", "").strip() + + if title and content: + note.title = title + note.content = content + note.save() + return redirect("note_list") + + return render(request, "productivity/note_form.html", {"note": note}) + + +@require_http_methods(["DELETE", "POST"]) +def note_delete(request, pk): + note = get_object_or_404(Note, pk=pk) + note.delete() + return redirect("note_list") + + +def _parse_datetime(value: str): + if not value: + return None + # Expect HTML datetime-local format: "YYYY-MM-DDTHH:MM" + try: + return datetime.fromisoformat(value) + except ValueError: + return None + + +def timeentry_list(request): + time_entries = TimeEntry.objects.select_related("project", "task").all() + return render( + request, + "productivity/timeentry_list.html", + {"time_entries": time_entries}, + ) + + +def timeentry_create(request): + projects = Project.objects.all() + tasks = Task.objects.all() + + if request.method == "POST": + project_id = request.POST.get("project") + task_id = request.POST.get("task") + description = request.POST.get("description", "").strip() + start_time_raw = request.POST.get("start_time", "").strip() + end_time_raw = request.POST.get("end_time", "").strip() + + start_time = _parse_datetime(start_time_raw) + end_time = _parse_datetime(end_time_raw) + + if description and start_time: + project = Project.objects.filter(id=project_id).first() if project_id else None + task = Task.objects.filter(id=task_id).first() if task_id else None + + TimeEntry.objects.create( + project=project, + task=task, + description=description, + start_time=start_time, + end_time=end_time, + ) + return redirect("timeentry_list") + + return render( + request, + "productivity/timeentry_form.html", + {"projects": projects, "tasks": tasks}, + ) + + +def timeentry_edit(request, pk): + time_entry = get_object_or_404(TimeEntry, pk=pk) + projects = Project.objects.all() + tasks = Task.objects.all() + + if request.method == "POST": + project_id = request.POST.get("project") + task_id = request.POST.get("task") + description = request.POST.get("description", "").strip() + start_time_raw = request.POST.get("start_time", "").strip() + end_time_raw = request.POST.get("end_time", "").strip() + + start_time = _parse_datetime(start_time_raw) + end_time = _parse_datetime(end_time_raw) + + if description and start_time: + time_entry.project = Project.objects.filter(id=project_id).first() if project_id else None + time_entry.task = Task.objects.filter(id=task_id).first() if task_id else None + time_entry.description = description + time_entry.start_time = start_time + time_entry.end_time = end_time + time_entry.save() + return redirect("timeentry_list") + + context = { + "time_entry": time_entry, + "projects": projects, + "tasks": tasks, + } + return render(request, "productivity/timeentry_form.html", context) + + +@require_http_methods(["DELETE", "POST"]) +def timeentry_delete(request, pk): + time_entry = get_object_or_404(TimeEntry, pk=pk) + time_entry.delete() + return redirect("timeentry_list") + + diff --git a/src/urls.py b/src/urls.py index 2051b0d..a2d229f 100644 --- a/src/urls.py +++ b/src/urls.py @@ -11,4 +11,5 @@ urlpatterns = [ path("clients/", include("src.api.clients.urls")), path("projects/", include("src.api.projects.urls")), path("finance/", include("src.api.finance.urls")), + path("productivity/", include("src.api.productivity.urls")), ]