2025-01-28 21:14:15 +01:00
|
|
|
import datetime
|
2025-01-27 18:50:24 +01:00
|
|
|
from datetime import timedelta
|
|
|
|
|
2025-02-04 00:19:05 +01:00
|
|
|
from django.db.models import Sum, Q, QuerySet, Subquery, OuterRef, DurationField, Value, DecimalField
|
|
|
|
from django.db.models.functions import Coalesce
|
2025-01-26 02:01:38 +01:00
|
|
|
from django.http import JsonResponse
|
2025-01-26 00:53:17 +01:00
|
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
|
|
from django.views import generic
|
2025-02-02 13:33:56 +01:00
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
2025-01-26 02:01:38 +01:00
|
|
|
from django.views.decorators.http import require_POST
|
2025-02-04 00:19:05 +01:00
|
|
|
from rest_framework.status import HTTP_400_BAD_REQUEST
|
2025-01-26 00:53:17 +01:00
|
|
|
|
2025-02-12 13:53:17 +01:00
|
|
|
from dispatcher.core import import_tasks, convert_timedelta, str_to_timedelta
|
2025-02-02 15:33:11 +01:00
|
|
|
from dispatcher.forms import ProjectForm, ImportForm, ParentForm
|
2025-02-04 00:19:05 +01:00
|
|
|
from dispatcher.models import Project, Parent, Clocking, Task, RealSageXHours
|
|
|
|
from dispatcher.serializers import ClockingSerializer, RealSageXHoursSerializer
|
2025-01-25 00:47:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
def dashboard_view(request):
|
2025-01-26 00:53:17 +01:00
|
|
|
context = {
|
|
|
|
"projects": Project.objects.all(),
|
|
|
|
"parents": Parent.objects.all()
|
|
|
|
}
|
|
|
|
return render(request, "dashboard.html", context)
|
|
|
|
|
|
|
|
def projects_view(request):
|
|
|
|
context = {
|
2025-02-02 21:48:12 +01:00
|
|
|
"class_name": "parent",
|
2025-01-26 00:53:17 +01:00
|
|
|
"projects": Project.objects.all(),
|
|
|
|
"parents": Parent.objects.all()
|
|
|
|
}
|
|
|
|
return render(request, "projects.html", context)
|
|
|
|
|
2025-02-02 15:33:11 +01:00
|
|
|
|
|
|
|
def parent_view(request, id):
|
|
|
|
parent = get_object_or_404(Parent, id=id)
|
2025-02-02 22:43:52 +01:00
|
|
|
|
|
|
|
if request.method == "DELETE":
|
|
|
|
parent.delete()
|
|
|
|
return JsonResponse({"status": "success"})
|
|
|
|
|
2025-02-02 15:33:11 +01:00
|
|
|
context = {
|
|
|
|
"class": "parent",
|
|
|
|
"id": id
|
|
|
|
}
|
|
|
|
form = ParentForm(request.POST or None, request.FILES or None, instance=parent)
|
|
|
|
if form.is_valid():
|
|
|
|
form.save()
|
2025-02-02 21:48:12 +01:00
|
|
|
return redirect("parents")
|
2025-02-02 15:33:11 +01:00
|
|
|
context["form"] = ParentForm(instance=parent)
|
|
|
|
return render(request, "edit.html", context)
|
|
|
|
|
2025-02-02 22:43:52 +01:00
|
|
|
def parent_on_delete_view(request, id):
|
|
|
|
parent = get_object_or_404(Parent, id=id)
|
|
|
|
projects = parent.project_set.count()
|
|
|
|
tasks = Task.objects.filter(project__parent=parent).count()
|
|
|
|
|
|
|
|
return JsonResponse({
|
|
|
|
"status": "success",
|
|
|
|
"projects": projects,
|
|
|
|
"tasks": tasks
|
|
|
|
})
|
|
|
|
|
2025-02-02 23:01:17 +01:00
|
|
|
def project_on_delete_view(request, id):
|
|
|
|
project = get_object_or_404(Project, id=id)
|
|
|
|
tasks = project.task_set.count()
|
|
|
|
|
|
|
|
return JsonResponse({
|
|
|
|
"status": "success",
|
|
|
|
"tasks": tasks
|
|
|
|
})
|
|
|
|
|
2025-02-02 21:48:12 +01:00
|
|
|
def new_parent_view(request):
|
|
|
|
context = {
|
|
|
|
"class": "parent"
|
|
|
|
}
|
|
|
|
form = ParentForm(request.POST or None, request.FILES or None)
|
|
|
|
if form.is_valid():
|
|
|
|
form.save()
|
|
|
|
return redirect("parents")
|
|
|
|
context["form"] = form
|
|
|
|
return render(request, "add.html", context)
|
|
|
|
|
2025-01-26 00:53:17 +01:00
|
|
|
def project_view(request, id):
|
|
|
|
project = get_object_or_404(Project, id=id)
|
2025-02-02 23:01:17 +01:00
|
|
|
|
|
|
|
if request.method == "DELETE":
|
|
|
|
project.delete()
|
|
|
|
return JsonResponse({"status": "success"})
|
|
|
|
|
2025-02-02 15:33:11 +01:00
|
|
|
context = {
|
|
|
|
"class": "project",
|
|
|
|
"id": id
|
|
|
|
}
|
2025-01-26 00:53:17 +01:00
|
|
|
form = ProjectForm(request.POST or None, request.FILES or None, instance=project)
|
|
|
|
if form.is_valid():
|
|
|
|
form.save()
|
|
|
|
context["form"] = ProjectForm(instance=project)
|
2025-02-02 15:33:11 +01:00
|
|
|
return render(request, "edit.html", context)
|
2025-01-26 00:53:17 +01:00
|
|
|
|
2025-02-02 23:19:18 +01:00
|
|
|
def new_project_view(request):
|
|
|
|
context = {
|
|
|
|
"class": "project"
|
|
|
|
}
|
|
|
|
form = ProjectForm(request.POST or None, request.FILES or None)
|
|
|
|
if form.is_valid():
|
|
|
|
form.save()
|
|
|
|
return redirect("projects")
|
|
|
|
context["form"] = form
|
|
|
|
return render(request, "add.html", context)
|
|
|
|
|
2025-01-28 21:14:15 +01:00
|
|
|
def table_view(request):
|
|
|
|
return render(request, "table.html")
|
|
|
|
|
2025-01-26 02:01:38 +01:00
|
|
|
@require_POST
|
|
|
|
def set_parent(request, id):
|
|
|
|
project = get_object_or_404(Project, id=id)
|
2025-02-02 23:01:17 +01:00
|
|
|
parent_id = request.POST.get("parent_id") or None
|
2025-01-26 02:01:38 +01:00
|
|
|
try:
|
|
|
|
parent = Parent.objects.get(id=parent_id)
|
|
|
|
except Parent.DoesNotExist:
|
|
|
|
parent = None
|
|
|
|
|
|
|
|
project.parent = parent
|
|
|
|
project.save()
|
|
|
|
return JsonResponse({"status": "success"})
|
|
|
|
|
|
|
|
|
2025-01-26 00:53:17 +01:00
|
|
|
def import_view(request):
|
|
|
|
if request.method == "POST":
|
|
|
|
form = ImportForm(request.POST, request.FILES)
|
|
|
|
if form.is_valid():
|
|
|
|
print(request.FILES)
|
|
|
|
import_tasks(request.FILES["file"].read().decode("utf-8"))
|
|
|
|
return redirect("dashboard")
|
|
|
|
|
|
|
|
return render(request, "import.html")
|
|
|
|
|
2025-01-26 02:01:38 +01:00
|
|
|
def get_stats_by_month(request, year: int, month: int):
|
|
|
|
if month < 1 or month > 12:
|
|
|
|
return JsonResponse({"status": "error", "error": f"Invalid month {month}"})
|
|
|
|
|
2025-02-04 00:19:05 +01:00
|
|
|
sagex_subquery = RealSageXHours.objects.filter(
|
|
|
|
parent=OuterRef("pk"),
|
|
|
|
date__year=year,
|
|
|
|
date__month=month
|
|
|
|
).values("hours")[:1]
|
|
|
|
|
2025-01-26 02:01:38 +01:00
|
|
|
parents = Parent.objects.annotate(
|
|
|
|
total_duration=Sum(
|
|
|
|
"project__task__duration",
|
|
|
|
filter=Q(
|
|
|
|
project__task__date__year=year,
|
|
|
|
project__task__date__month=month
|
|
|
|
)
|
|
|
|
)
|
2025-02-04 00:19:05 +01:00
|
|
|
).annotate(
|
|
|
|
real_sagex_hours=Coalesce(
|
|
|
|
Subquery(sagex_subquery, output_field=DurationField()),
|
|
|
|
timedelta(0)
|
|
|
|
)
|
2025-01-26 02:01:38 +01:00
|
|
|
)
|
2025-01-27 18:50:24 +01:00
|
|
|
projects = Project.objects.annotate(
|
|
|
|
total_duration=Sum(
|
|
|
|
"task__duration",
|
|
|
|
filter=Q(
|
|
|
|
task__date__year=year,
|
|
|
|
task__date__month=month
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2025-02-03 12:06:59 +01:00
|
|
|
clockings = Clocking.objects.filter(
|
|
|
|
date__year=year,
|
|
|
|
date__month=month
|
|
|
|
)
|
|
|
|
return JsonResponse({"status": "success", "data": format_stats(parents, projects, clockings)})
|
2025-01-27 18:50:24 +01:00
|
|
|
|
2025-02-03 12:06:59 +01:00
|
|
|
def format_stats(parents: QuerySet[Parent], projects: QuerySet[Project], clockings: QuerySet[Clocking]):
|
2025-01-27 18:50:24 +01:00
|
|
|
data = {
|
|
|
|
"parents": [
|
|
|
|
{
|
|
|
|
"id": parent.id,
|
|
|
|
"name": parent.name,
|
|
|
|
"project_num": parent.project_num,
|
2025-02-03 12:06:59 +01:00
|
|
|
"is_productive": parent.is_productive,
|
2025-02-04 00:19:05 +01:00
|
|
|
"duration": parent.total_duration,
|
|
|
|
"real_sagex_hours": convert_timedelta(parent.real_sagex_hours)
|
2025-01-27 18:50:24 +01:00
|
|
|
}
|
|
|
|
for parent in parents
|
|
|
|
],
|
|
|
|
"projects": [
|
|
|
|
{
|
|
|
|
"id": project.id,
|
|
|
|
"name": project.name,
|
|
|
|
"duration": project.total_duration
|
|
|
|
}
|
|
|
|
for project in projects
|
2025-02-03 12:06:59 +01:00
|
|
|
],
|
|
|
|
"clockings": ClockingSerializer(clockings, many=True).data
|
2025-01-27 18:50:24 +01:00
|
|
|
}
|
2025-02-03 12:06:59 +01:00
|
|
|
Clocking.objects.filter()
|
2025-01-27 18:50:24 +01:00
|
|
|
return data
|
2025-01-26 02:01:38 +01:00
|
|
|
|
2025-01-26 00:53:17 +01:00
|
|
|
class ParentsView(generic.ListView):
|
|
|
|
model = Parent
|
|
|
|
template_name = "parents.html"
|
|
|
|
context_object_name = "elements"
|
2025-01-28 21:14:15 +01:00
|
|
|
|
2025-02-02 23:01:17 +01:00
|
|
|
class ProjectsView(generic.ListView):
|
|
|
|
model = Project
|
|
|
|
template_name = "projects.html"
|
|
|
|
context_object_name = "elements"
|
2025-02-03 12:06:59 +01:00
|
|
|
ordering = ["parent"]
|
2025-02-02 23:01:17 +01:00
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context["parents"] = Parent.objects.all()
|
|
|
|
return context
|
|
|
|
|
2025-01-28 21:14:15 +01:00
|
|
|
|
|
|
|
def get_table_data(request, start_date: datetime.date, end_date: datetime.date):
|
|
|
|
end_date = end_date + timedelta(days=1)
|
2025-02-02 13:33:56 +01:00
|
|
|
clockings = Clocking.objects.filter(
|
|
|
|
date__gte=start_date,
|
|
|
|
date__lte=end_date
|
|
|
|
)
|
|
|
|
|
2025-01-28 21:14:15 +01:00
|
|
|
parents = Parent.objects.all().order_by("id")
|
|
|
|
data = {
|
|
|
|
"parents": [
|
|
|
|
{
|
|
|
|
"id": parent.id,
|
|
|
|
"name": parent.name,
|
|
|
|
"project_num": parent.project_num,
|
2025-02-02 18:47:35 +01:00
|
|
|
"is_productive": parent.is_productive,
|
2025-01-28 21:14:15 +01:00
|
|
|
"projects": [
|
|
|
|
{
|
|
|
|
"id": project.id,
|
|
|
|
"name": project.name,
|
|
|
|
"tasks": [
|
|
|
|
{
|
|
|
|
"date": task["date"],
|
|
|
|
"duration": task["duration"]
|
|
|
|
}
|
|
|
|
for task in project.task_set.filter(
|
|
|
|
date__gte=start_date,
|
|
|
|
date__lt=end_date
|
|
|
|
).values("date").order_by("date").annotate(duration=Sum("duration"))
|
|
|
|
]
|
|
|
|
}
|
|
|
|
for project in parent.project_set.order_by("id")
|
|
|
|
]
|
|
|
|
}
|
|
|
|
for parent in parents
|
2025-02-02 13:33:56 +01:00
|
|
|
],
|
|
|
|
"clockings": ClockingSerializer(clockings, many=True).data
|
2025-01-28 21:14:15 +01:00
|
|
|
}
|
2025-02-02 13:33:56 +01:00
|
|
|
return JsonResponse({"status": "success", "data": data})
|
|
|
|
|
|
|
|
@require_POST
|
|
|
|
@csrf_exempt
|
|
|
|
def set_clocking(request, date: datetime.date):
|
|
|
|
clocking, created = Clocking.objects.get_or_create(date=date)
|
|
|
|
clocking.in_am = request.POST.get("in_am", clocking.in_am) or None
|
|
|
|
clocking.out_am = request.POST.get("out_am", clocking.out_am) or None
|
|
|
|
clocking.in_pm = request.POST.get("in_pm", clocking.in_pm) or None
|
|
|
|
clocking.out_pm = request.POST.get("out_pm", clocking.out_pm) or None
|
2025-02-12 13:53:17 +01:00
|
|
|
remote = request.POST.get("remote", clocking.remote) or None
|
|
|
|
if remote is not None:
|
|
|
|
remote = str_to_timedelta(remote)
|
|
|
|
else:
|
|
|
|
remote = timedelta()
|
|
|
|
clocking.remote = remote
|
2025-02-02 13:33:56 +01:00
|
|
|
clocking.save()
|
|
|
|
clocking.refresh_from_db()
|
|
|
|
|
|
|
|
return JsonResponse({
|
|
|
|
"status": "success",
|
|
|
|
"clocking": ClockingSerializer(clocking).data
|
2025-02-04 00:19:05 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
@require_POST
|
|
|
|
def set_real_sagex(request, id, month: datetime.date):
|
|
|
|
parent = get_object_or_404(Parent, id=id)
|
|
|
|
minutes = request.POST.get("minutes")
|
|
|
|
if minutes is None:
|
|
|
|
return JsonResponse({
|
|
|
|
"status": "error",
|
|
|
|
"error": "Missing minutes field"
|
|
|
|
}, status=HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
|
|
minutes = int(minutes)
|
|
|
|
except ValueError:
|
|
|
|
return JsonResponse({
|
|
|
|
"status": "error",
|
|
|
|
"error": "Invalid value for minutes, must be an int"
|
|
|
|
}, status=HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
|
|
hours, minutes = divmod(minutes, 60)
|
|
|
|
|
|
|
|
entry, created = RealSageXHours.objects.get_or_create(parent=parent, date=month)
|
|
|
|
entry.hours = timedelta(hours=int(hours), minutes=int(minutes))
|
|
|
|
entry.save()
|
|
|
|
entry.refresh_from_db()
|
|
|
|
|
|
|
|
return JsonResponse({
|
|
|
|
"status": "success",
|
|
|
|
"sagex": RealSageXHoursSerializer(entry).data
|
2025-02-02 13:33:56 +01:00
|
|
|
})
|