Dashboard view per Client
This commit is contained in:
parent
cedb637763
commit
e7935eb585
BIN
assets/pasted-20260122-091742-8812311a.png
Normal file
BIN
assets/pasted-20260122-091742-8812311a.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
Binary file not shown.
@ -130,7 +130,7 @@
|
|||||||
<div class="row g-4 mb-5">
|
<div class="row g-4 mb-5">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<h6 class="mb-3">Incomplete Folder Breakdown</h6>
|
<h6 class="mb-3">Incomplete Folder Breakdown (Overall)</h6>
|
||||||
{% if incomplete_folder_breakdown %}
|
{% if incomplete_folder_breakdown %}
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for item in incomplete_folder_breakdown %}
|
{% for item in incomplete_folder_breakdown %}
|
||||||
@ -149,45 +149,57 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card border-0 shadow-sm mb-5">
|
{# Client-specific breakdown #}
|
||||||
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
|
{% if client_stats %}
|
||||||
<h5 class="mb-0">Jobs by Client</h5>
|
{% for client_stat in client_stats %}
|
||||||
</div>
|
<div class="card border-0 shadow-sm mb-5">
|
||||||
<div class="card-body">
|
<div class="card-header bg-white py-3 border-0">
|
||||||
{% if jobs_by_client %}
|
<h5 class="mb-0">{{ client_stat.client_name }}</h5>
|
||||||
{% for client_name, client_jobs in jobs_by_client.items %}
|
</div>
|
||||||
<h6 class="mt-3 mb-2">{{ client_name }} ({{ client_jobs|length }} Jobs)</h6>
|
<div class="card-body">
|
||||||
<div class="table-responsive mb-4">
|
<div class="row g-4 mb-4">
|
||||||
<table class="table align-middle mb-0 table-sm">
|
<div class="col-md-4">
|
||||||
<thead class="bg-light">
|
<div class="stat-card">
|
||||||
<tr>
|
<div class="stat-value text-primary">{{ client_stat.total_jobs }}</div>
|
||||||
<th class="border-0 px-4">Job Ref</th>
|
<div class="stat-label">Total Jobs</div>
|
||||||
<th class="border-0">Address</th>
|
</div>
|
||||||
<th class="border-0">Status</th>
|
|
||||||
<th class="border-0 text-end px-4">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for job in client_jobs %}
|
|
||||||
<tr>
|
|
||||||
<td class="px-4 fw-bold text-primary">{{ job.job_ref }}</td>
|
|
||||||
<td>{{ job.address_line_1 }}, {{ job.postcode }}</td>
|
|
||||||
<td><span class="badge bg-light text-dark border">{{ job.status.name }}</span></td>
|
|
||||||
<td class="text-end px-4">
|
|
||||||
<a href="{% url 'job_detail' job.pk %}" class="btn btn-sm btn-outline-secondary">View</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
<div class="col-md-4">
|
||||||
{% else %}
|
<div class="stat-card">
|
||||||
<p class="text-muted">No jobs found or clients assigned.</p>
|
<div class="stat-value text-warning">{{ client_stat.incomplete_jobs }}</div>
|
||||||
{% endif %}
|
<div class="stat-label">Jobs with Incomplete Folders</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h6 class="mb-3">Incomplete Folders</h6>
|
||||||
|
{% if client_stat.incomplete_folder_breakdown %}
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for folder_item in client_stat.incomplete_folder_breakdown %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0 py-1">
|
||||||
|
<a href="{% url 'job_list' %}?missing_folder_id={{ folder_item.folder_id }}" class="text-decoration-none text-dark">
|
||||||
|
{{ folder_item.folder_name }}
|
||||||
|
</a>
|
||||||
|
<span class="badge bg-danger rounded-pill">{{ folder_item.missing_count }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">All folders complete.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="card border-0 shadow-sm mb-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted">No client-specific job data available.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
|
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Q
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -114,7 +114,7 @@ def dashboard(request):
|
|||||||
return redirect('company_setup')
|
return redirect('company_setup')
|
||||||
|
|
||||||
company = profile.company
|
company = profile.company
|
||||||
jobs = Job.objects.filter(company=company).select_related('client')
|
jobs = Job.objects.filter(company=company).select_related('client', 'status')
|
||||||
|
|
||||||
total_jobs = jobs.count()
|
total_jobs = jobs.count()
|
||||||
|
|
||||||
@ -129,8 +129,7 @@ def dashboard(request):
|
|||||||
|
|
||||||
for folder in required_folders:
|
for folder in required_folders:
|
||||||
# Count jobs where this specific folder is NOT completed
|
# Count jobs where this specific folder is NOT completed
|
||||||
missing_count = Job.objects.filter(
|
missing_count = jobs.filter(
|
||||||
company=company,
|
|
||||||
folder_completions__folder=folder,
|
folder_completions__folder=folder,
|
||||||
folder_completions__is_completed=False
|
folder_completions__is_completed=False
|
||||||
).distinct().count()
|
).distinct().count()
|
||||||
@ -141,13 +140,62 @@ def dashboard(request):
|
|||||||
'missing_count': missing_count
|
'missing_count': missing_count
|
||||||
})
|
})
|
||||||
|
|
||||||
# NEW LOGIC: Group jobs by client
|
# NEW LOGIC: Group jobs by client with detailed statistics
|
||||||
jobs_by_client = {}
|
client_stats = []
|
||||||
for job in jobs:
|
clients = company.clients.all()
|
||||||
client_name = job.client.name if job.client else "No Client Assigned"
|
|
||||||
if client_name not in jobs_by_client:
|
# Add a "No Client Assigned" entry first if there are jobs without a client
|
||||||
jobs_by_client[client_name] = []
|
jobs_no_client = jobs.filter(client__isnull=True)
|
||||||
jobs_by_client[client_name].append(job)
|
if jobs_no_client.exists():
|
||||||
|
no_client_total_jobs = jobs_no_client.count()
|
||||||
|
no_client_incomplete_jobs = jobs_no_client.filter(folder_completions__is_completed=False).distinct().count()
|
||||||
|
|
||||||
|
no_client_incomplete_folder_breakdown = []
|
||||||
|
for folder in required_folders:
|
||||||
|
missing_count = jobs_no_client.filter(
|
||||||
|
folder_completions__folder=folder,
|
||||||
|
folder_completions__is_completed=False
|
||||||
|
).distinct().count()
|
||||||
|
if missing_count > 0:
|
||||||
|
no_client_incomplete_folder_breakdown.append({
|
||||||
|
'folder_name': folder.name,
|
||||||
|
'missing_count': missing_count,
|
||||||
|
'folder_id': folder.id
|
||||||
|
})
|
||||||
|
client_stats.append({
|
||||||
|
'client_name': "No Client Assigned",
|
||||||
|
'total_jobs': no_client_total_jobs,
|
||||||
|
'incomplete_jobs': no_client_incomplete_jobs,
|
||||||
|
'incomplete_folder_breakdown': no_client_incomplete_folder_breakdown
|
||||||
|
})
|
||||||
|
|
||||||
|
for client in clients:
|
||||||
|
client_jobs = jobs.filter(client=client)
|
||||||
|
total_client_jobs = client_jobs.count()
|
||||||
|
|
||||||
|
if total_client_jobs > 0: # Only include clients with jobs
|
||||||
|
client_incomplete_jobs = client_jobs.filter(folder_completions__is_completed=False).distinct().count()
|
||||||
|
|
||||||
|
client_incomplete_folder_breakdown = []
|
||||||
|
for folder in required_folders:
|
||||||
|
missing_count = client_jobs.filter(
|
||||||
|
folder_completions__folder=folder,
|
||||||
|
folder_completions__is_completed=False
|
||||||
|
).distinct().count()
|
||||||
|
if missing_count > 0:
|
||||||
|
client_incomplete_folder_breakdown.append({
|
||||||
|
'folder_name': folder.name,
|
||||||
|
'missing_count': missing_count,
|
||||||
|
'folder_id': folder.id
|
||||||
|
})
|
||||||
|
|
||||||
|
client_stats.append({
|
||||||
|
'client_name': client.name,
|
||||||
|
'total_jobs': total_client_jobs,
|
||||||
|
'incomplete_jobs': client_incomplete_jobs,
|
||||||
|
'incomplete_folder_breakdown': client_incomplete_folder_breakdown
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'company': company,
|
'company': company,
|
||||||
@ -156,7 +204,7 @@ def dashboard(request):
|
|||||||
'jobs_with_incomplete_folders': jobs_with_incomplete_folders,
|
'jobs_with_incomplete_folders': jobs_with_incomplete_folders,
|
||||||
'jobs': jobs.order_by('-created_at')[:5],
|
'jobs': jobs.order_by('-created_at')[:5],
|
||||||
'incomplete_folder_breakdown': incomplete_folder_breakdown,
|
'incomplete_folder_breakdown': incomplete_folder_breakdown,
|
||||||
'jobs_by_client': jobs_by_client,
|
'client_stats': client_stats, # Replaced jobs_by_client
|
||||||
}
|
}
|
||||||
return render(request, 'core/dashboard.html', context)
|
return render(request, 'core/dashboard.html', context)
|
||||||
|
|
||||||
@ -277,6 +325,8 @@ def job_delete(request, pk):
|
|||||||
job = get_object_or_404(Job, pk=pk, company=company)
|
job = get_object_or_404(Job, pk=pk, company=company)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
# You might want to add a check here if there are any jobs associated with this client
|
||||||
|
# If jobs are associated, you might want to reassign them or prevent deletion.
|
||||||
job_ref = job.job_ref
|
job_ref = job.job_ref
|
||||||
job.delete()
|
job.delete()
|
||||||
messages.success(request, f"Job {job_ref} deleted.")
|
messages.success(request, f"Job {job_ref} deleted.")
|
||||||
@ -781,4 +831,4 @@ def user_delete(request, pk):
|
|||||||
user_to_delete.delete()
|
user_to_delete.delete()
|
||||||
messages.success(request, f"User {username} deleted.")
|
messages.success(request, f"User {username} deleted.")
|
||||||
|
|
||||||
return redirect('user_list')
|
return redirect('user_list')
|
||||||
|
|||||||
BIN
staticfiles/pasted-20260122-091742-8812311a.png
Normal file
BIN
staticfiles/pasted-20260122-091742-8812311a.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
Loading…
x
Reference in New Issue
Block a user