Autosave: 20260213-093348
This commit is contained in:
parent
f3ada22ab3
commit
23f7726fb9
Binary file not shown.
@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Charts Section -->
|
<!-- Charts Section -->
|
||||||
<div class="row mb-5">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<div class="card-header bg-white fw-bold small text-uppercase text-muted">Monthly Payroll Totals</div>
|
<div class="card-header bg-white fw-bold small text-uppercase text-muted">Monthly Payroll Totals</div>
|
||||||
@ -83,6 +83,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Overtime Chart Row -->
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white fw-bold small text-uppercase text-muted">Overtime History</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="otHistoryChart" height="150"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filter Tabs -->
|
<!-- Filter Tabs -->
|
||||||
<ul class="nav nav-pills mb-4">
|
<ul class="nav nav-pills mb-4">
|
||||||
@ -463,6 +475,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const totals = {{ chart_totals_json|safe }};
|
const totals = {{ chart_totals_json|safe }};
|
||||||
const projectData = {{ project_chart_json|safe }};
|
const projectData = {{ project_chart_json|safe }};
|
||||||
const allOtData = {{ overtime_data_json|default:"[]"|safe }};
|
const allOtData = {{ overtime_data_json|default:"[]"|safe }};
|
||||||
|
const otHistoryTotals = {{ ot_history_json|default:"[]"|safe }};
|
||||||
|
|
||||||
// Overtime Modal Logic
|
// Overtime Modal Logic
|
||||||
const otModalEl = document.getElementById('priceOTModal');
|
const otModalEl = document.getElementById('priceOTModal');
|
||||||
@ -612,6 +625,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Chart 3: Overtime History (New)
|
||||||
|
new Chart(document.getElementById('otHistoryChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Overtime Paid (R)',
|
||||||
|
data: otHistoryTotals,
|
||||||
|
backgroundColor: '#f59e0b',
|
||||||
|
borderRadius: 4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: { callback: v => 'R ' + v.toLocaleString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,10 @@
|
|||||||
|
|
||||||
{% block title %}Work Log History | LabourFlow{% endblock %}
|
{% block title %}Work Log History | LabourFlow{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="dashboard-header pb-5">
|
<div class="dashboard-header pb-5">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -32,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Card -->
|
<!-- Filter Card -->
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<form method="get" class="row g-3">
|
<form method="get" class="row g-3">
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||||
@ -104,6 +108,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Overtime Chart (Admin Only) -->
|
||||||
|
{% if is_admin_user %}
|
||||||
|
<div class="card border-0 shadow-sm mb-4" id="otChartContainer" style="display: none;">
|
||||||
|
<div class="card-header bg-white fw-bold small text-uppercase text-muted">Overtime History (Monthly)</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="height: 200px;">
|
||||||
|
<canvas id="otHistoryChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mt-2 mb-0 fst-italic text-center">
|
||||||
|
Showing overtime costs for the current filters.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -343,9 +362,51 @@
|
|||||||
.cal-day--selected { background-color: rgba(13, 110, 253, 0.08) !important; box-shadow: inset 0 0 0 2px var(--bs-primary); }
|
.cal-day--selected { background-color: rgba(13, 110, 253, 0.08) !important; box-shadow: inset 0 0 0 2px var(--bs-primary); }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{% if view_mode == 'calendar' %}
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
{% if is_admin_user %}
|
||||||
|
const otLabels = {{ ot_chart_labels|safe|default:"[]" }};
|
||||||
|
const otData = {{ ot_chart_data|safe|default:"[]" }};
|
||||||
|
const chartContainer = document.getElementById('otChartContainer');
|
||||||
|
|
||||||
|
// Only show chart if we have data and more than 0 entries
|
||||||
|
if (otData.length > 0 && otData.some(val => val > 0)) {
|
||||||
|
chartContainer.style.display = 'block';
|
||||||
|
|
||||||
|
const ctx = document.getElementById('otHistoryChart');
|
||||||
|
if (ctx) {
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: otLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Overtime Paid (R)',
|
||||||
|
data: otData,
|
||||||
|
backgroundColor: '#f59e0b',
|
||||||
|
borderRadius: 4,
|
||||||
|
barThickness: 40
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: { callback: v => 'R ' + v.toLocaleString() }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if view_mode == 'calendar' %}
|
||||||
const detailData = {{ calendar_detail_json|safe }};
|
const detailData = {{ calendar_detail_json|safe }};
|
||||||
const panel = document.getElementById('dayDetailPanel');
|
const panel = document.getElementById('dayDetailPanel');
|
||||||
const panelTitle = document.getElementById('dayDetailTitle');
|
const panelTitle = document.getElementById('dayDetailTitle');
|
||||||
@ -458,7 +519,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
el.classList.remove('cal-day--selected');
|
el.classList.remove('cal-day--selected');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -359,6 +359,21 @@ def work_log_list(request):
|
|||||||
total_amount = 0
|
total_amount = 0
|
||||||
combined_records = []
|
combined_records = []
|
||||||
|
|
||||||
|
# Prepare Chart Data (Overtime) - Admin only
|
||||||
|
ot_chart_labels = []
|
||||||
|
ot_chart_data = []
|
||||||
|
|
||||||
|
if user_is_admin:
|
||||||
|
from django.db.models.functions import TruncMonth
|
||||||
|
ot_stats = adjustments.filter(type='OVERTIME') \
|
||||||
|
.annotate(month=TruncMonth('date')) \
|
||||||
|
.values('month') \
|
||||||
|
.annotate(total=Sum('amount')) \
|
||||||
|
.order_by('month')
|
||||||
|
|
||||||
|
ot_chart_labels = [s['month'].strftime('%b %Y') for s in ot_stats]
|
||||||
|
ot_chart_data = [float(s['total']) for s in ot_stats]
|
||||||
|
|
||||||
# Process Logs
|
# Process Logs
|
||||||
for log in logs:
|
for log in logs:
|
||||||
record = {
|
record = {
|
||||||
@ -431,6 +446,8 @@ def work_log_list(request):
|
|||||||
'selected_payment_status': payment_status,
|
'selected_payment_status': payment_status,
|
||||||
'target_worker': target_worker,
|
'target_worker': target_worker,
|
||||||
'view_mode': view_mode,
|
'view_mode': view_mode,
|
||||||
|
'ot_chart_labels': json.dumps(ot_chart_labels),
|
||||||
|
'ot_chart_data': json.dumps(ot_chart_data),
|
||||||
}
|
}
|
||||||
|
|
||||||
if view_mode == 'calendar':
|
if view_mode == 'calendar':
|
||||||
@ -777,6 +794,8 @@ def payroll_dashboard(request):
|
|||||||
all_project_names = list(Project.objects.values_list('name', flat=True).order_by('name'))
|
all_project_names = list(Project.objects.values_list('name', flat=True).order_by('name'))
|
||||||
project_monthly = {name: [] for name in all_project_names}
|
project_monthly = {name: [] for name in all_project_names}
|
||||||
|
|
||||||
|
ot_history_totals = [] # Overtime history
|
||||||
|
|
||||||
for year, month in chart_months:
|
for year, month in chart_months:
|
||||||
chart_labels.append(f"{calendar.month_abbr[month]} {year}")
|
chart_labels.append(f"{calendar.month_abbr[month]} {year}")
|
||||||
|
|
||||||
@ -789,6 +808,14 @@ def payroll_dashboard(request):
|
|||||||
date__gte=month_start, date__lte=month_end
|
date__gte=month_start, date__lte=month_end
|
||||||
).aggregate(total=Sum('amount'))['total'] or 0
|
).aggregate(total=Sum('amount'))['total'] or 0
|
||||||
chart_totals.append(float(month_paid))
|
chart_totals.append(float(month_paid))
|
||||||
|
|
||||||
|
# Overtime paid this month
|
||||||
|
ot_month_total = PayrollAdjustment.objects.filter(
|
||||||
|
type='OVERTIME',
|
||||||
|
date__gte=month_start,
|
||||||
|
date__lte=month_end
|
||||||
|
).aggregate(total=Sum('amount'))['total'] or 0
|
||||||
|
ot_history_totals.append(float(ot_month_total))
|
||||||
|
|
||||||
# Per-project labour cost this month (from work logs × day rates)
|
# Per-project labour cost this month (from work logs × day rates)
|
||||||
month_logs = WorkLog.objects.filter(
|
month_logs = WorkLog.objects.filter(
|
||||||
@ -826,6 +853,7 @@ def payroll_dashboard(request):
|
|||||||
'chart_totals_json': json.dumps(chart_totals),
|
'chart_totals_json': json.dumps(chart_totals),
|
||||||
'project_chart_json': json.dumps(project_chart_data),
|
'project_chart_json': json.dumps(project_chart_data),
|
||||||
'overtime_data_json': json.dumps(all_ot_data),
|
'overtime_data_json': json.dumps(all_ot_data),
|
||||||
|
'ot_history_json': json.dumps(ot_history_totals),
|
||||||
}
|
}
|
||||||
return render(request, 'core/payroll_dashboard.html', context)
|
return render(request, 'core/payroll_dashboard.html', context)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user