Replace decouple config calls for Google OAuth credentials with settings; add new board view and associated context processors to render project board and dashboard charts; update serializer import path; centralize configuration and improve maintainability.
215 lines
6.3 KiB
Python
215 lines
6.3 KiB
Python
import json
|
|
from datetime import date, timedelta
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.db.models import Count, Q
|
|
from django.http import JsonResponse
|
|
from django.shortcuts import render
|
|
from django.utils import timezone
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from src.projects.models import Project
|
|
from src.tickets.models import Ticket, TicketHistory
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
def dashboard(request):
|
|
return render(
|
|
request,
|
|
"dashboard/index.html",
|
|
{
|
|
"project_count": Project.objects.count(),
|
|
"ticket_count": Ticket.objects.count(),
|
|
"user_count": User.objects.count(),
|
|
"my_assigned": Ticket.objects.filter(assignees=request.user).count(),
|
|
},
|
|
)
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
def stats_cards(request):
|
|
user = request.user
|
|
now = timezone.now()
|
|
today = now.date()
|
|
week_start = today - timedelta(days=today.weekday())
|
|
|
|
if user.is_superuser:
|
|
total_projects = Project.objects.count()
|
|
total_tickets = Ticket.objects.count()
|
|
team_members = User.objects.count()
|
|
else:
|
|
total_projects = Project.objects.filter(members=user).count()
|
|
total_tickets = (
|
|
Ticket.objects.filter(Q(project__members=user) | Q(assignees=user))
|
|
.distinct()
|
|
.count()
|
|
)
|
|
team_members = (
|
|
User.objects.filter(project_memberships__project__members=user)
|
|
.distinct()
|
|
.count()
|
|
)
|
|
|
|
my_assigned = Ticket.objects.filter(assignees=user).count()
|
|
my_created = Ticket.objects.filter(created_by=user).count()
|
|
my_overdue = Ticket.objects.filter(
|
|
assignees=user, due_date__lt=today, status__is_completed=False
|
|
).count()
|
|
completed_this_week = Ticket.objects.filter(
|
|
Q(assignees=user) | Q(created_by=user),
|
|
status__is_completed=True,
|
|
updated_at__gte=week_start,
|
|
).count()
|
|
new_this_week = Ticket.objects.filter(
|
|
Q(assignees=user) | Q(created_by=user),
|
|
created_at__gte=week_start,
|
|
).count()
|
|
|
|
return render(
|
|
request,
|
|
"dashboard/partials/stats_cards.html",
|
|
{
|
|
"total_projects": total_projects,
|
|
"total_tickets": total_tickets,
|
|
"team_members": team_members,
|
|
"my_assigned": my_assigned,
|
|
"my_created": my_created,
|
|
"my_overdue": my_overdue,
|
|
"completed_this_week": completed_this_week,
|
|
"new_this_week": new_this_week,
|
|
"is_superuser": user.is_superuser,
|
|
},
|
|
)
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
def tickets_per_project_chart(request):
|
|
data = list(
|
|
Project.objects.annotate(count=Count("tickets")).values(
|
|
"name", "count", "color"
|
|
)
|
|
)
|
|
max_count = max((d["count"] for d in data), default=1)
|
|
for d in data:
|
|
d["pct"] = round(d["count"] / max_count * 100, 1) if max_count else 0
|
|
return render(
|
|
request,
|
|
"dashboard/partials/tickets_per_project_chart.html",
|
|
{
|
|
"chart_data": data,
|
|
},
|
|
)
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
def user_statistics_data(request):
|
|
users = User.objects.annotate(
|
|
project_count=Count("projects", distinct=True),
|
|
assigned_count=Count("assigned_tickets", distinct=True),
|
|
).values("username", "project_count", "assigned_count")[:10]
|
|
|
|
return JsonResponse(
|
|
{
|
|
"labels": [u["username"] for u in users],
|
|
"projects": [u["project_count"] for u in users],
|
|
"assigned": [u["assigned_count"] for u in users],
|
|
}
|
|
)
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
def monthly_trend_chart(request):
|
|
now = timezone.now()
|
|
months = []
|
|
for i in range(5, -1, -1):
|
|
m = now.month - i
|
|
y = now.year
|
|
while m < 1:
|
|
m += 12
|
|
y -= 1
|
|
months.append({"year": y, "month": m, "label": date(y, m, 1).strftime("%b %Y")})
|
|
|
|
data = []
|
|
for m in months:
|
|
count = Ticket.objects.filter(
|
|
created_at__year=m["year"],
|
|
created_at__month=m["month"],
|
|
).count()
|
|
data.append({"label": m["label"], "count": count})
|
|
|
|
return render(
|
|
request,
|
|
"dashboard/partials/monthly_trend_chart.html",
|
|
{
|
|
"chart_data": json.dumps(data),
|
|
},
|
|
)
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
def recent_activity(request):
|
|
histories = TicketHistory.objects.select_related(
|
|
"ticket", "user", "status", "ticket__project"
|
|
).order_by("-created_at")[:20]
|
|
|
|
return render(
|
|
request,
|
|
"dashboard/partials/recent_activity.html",
|
|
{
|
|
"histories": histories,
|
|
},
|
|
)
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
def project_timeline_widget(request):
|
|
filter_type = request.GET.get("filter", "pinned")
|
|
qs = Project.objects.filter(start_date__isnull=False, end_date__isnull=False)
|
|
|
|
pinned_count = qs.filter(pinned_date__isnull=False).count()
|
|
all_count = qs.count()
|
|
|
|
if filter_type == "pinned":
|
|
qs = qs.filter(pinned_date__isnull=False).order_by("-pinned_date")
|
|
else:
|
|
qs = qs.order_by("start_date")
|
|
|
|
today = date.today()
|
|
for p in qs:
|
|
total = (p.end_date - p.start_date).days or 1
|
|
elapsed = (today - p.start_date).days
|
|
p.progress = max(0, min(100, int(elapsed / total * 100)))
|
|
remaining = (p.end_date - today).days
|
|
if remaining < 0:
|
|
p.status_label = "Overdue"
|
|
p.status_color = "badge-error"
|
|
p.bar_color = "bg-red-500"
|
|
elif remaining <= 7:
|
|
p.status_label = "Approaching Deadline"
|
|
p.status_color = "badge-warning"
|
|
p.bar_color = "bg-yellow-500"
|
|
elif p.progress >= 100:
|
|
p.status_label = "Completed"
|
|
p.status_color = "badge-success"
|
|
p.bar_color = "bg-emerald-500"
|
|
elif p.progress > 0:
|
|
p.status_label = "In Progress"
|
|
p.status_color = "badge-info"
|
|
p.bar_color = "bg-blue-500"
|
|
else:
|
|
p.status_label = "Not Started"
|
|
p.status_color = "badge-ghost"
|
|
p.bar_color = "bg-gray-400"
|
|
|
|
return render(
|
|
request,
|
|
"dashboard/partials/project_timeline.html",
|
|
{
|
|
"projects": qs,
|
|
"current_filter": filter_type,
|
|
"pinned_count": pinned_count,
|
|
"all_count": all_count,
|
|
},
|
|
)
|