Autosave: 20260213-093348
This commit is contained in:
parent
f3ada22ab3
commit
23f7726fb9
Binary file not shown.
@ -65,7 +65,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Charts Section -->
|
||||
<div class="row mb-5">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6 mb-3">
|
||||
<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>
|
||||
@ -83,6 +83,18 @@
|
||||
</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 -->
|
||||
<ul class="nav nav-pills mb-4">
|
||||
@ -463,6 +475,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const totals = {{ chart_totals_json|safe }};
|
||||
const projectData = {{ project_chart_json|safe }};
|
||||
const allOtData = {{ overtime_data_json|default:"[]"|safe }};
|
||||
const otHistoryTotals = {{ ot_history_json|default:"[]"|safe }};
|
||||
|
||||
// Overtime Modal Logic
|
||||
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>
|
||||
|
||||
|
||||
@ -3,6 +3,10 @@
|
||||
|
||||
{% 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 %}
|
||||
<div class="dashboard-header pb-5">
|
||||
<div class="container">
|
||||
@ -32,7 +36,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Filter Card -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<form method="get" class="row g-3">
|
||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||
@ -104,6 +108,21 @@
|
||||
</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>
|
||||
|
||||
@ -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); }
|
||||
</style>
|
||||
|
||||
{% if view_mode == 'calendar' %}
|
||||
<script>
|
||||
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 panel = document.getElementById('dayDetailPanel');
|
||||
const panelTitle = document.getElementById('dayDetailTitle');
|
||||
@ -458,7 +519,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
el.classList.remove('cal-day--selected');
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -359,6 +359,21 @@ def work_log_list(request):
|
||||
total_amount = 0
|
||||
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
|
||||
for log in logs:
|
||||
record = {
|
||||
@ -431,6 +446,8 @@ def work_log_list(request):
|
||||
'selected_payment_status': payment_status,
|
||||
'target_worker': target_worker,
|
||||
'view_mode': view_mode,
|
||||
'ot_chart_labels': json.dumps(ot_chart_labels),
|
||||
'ot_chart_data': json.dumps(ot_chart_data),
|
||||
}
|
||||
|
||||
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'))
|
||||
project_monthly = {name: [] for name in all_project_names}
|
||||
|
||||
ot_history_totals = [] # Overtime history
|
||||
|
||||
for year, month in chart_months:
|
||||
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
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
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)
|
||||
month_logs = WorkLog.objects.filter(
|
||||
@ -826,6 +853,7 @@ def payroll_dashboard(request):
|
||||
'chart_totals_json': json.dumps(chart_totals),
|
||||
'project_chart_json': json.dumps(project_chart_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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user