diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2ec5074..499a72a 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/templates/base.html b/core/templates/base.html index f1305e7..1f614ac 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -33,17 +33,44 @@ + +
+
+
+
Estimated Cost
+ 0 workers × 0 days +
+

R 0.00

+
+
+
Cancel @@ -153,13 +164,7 @@ const teamId = this.value; if (teamId && teamWorkersMap[teamId]) { const workerIds = teamWorkersMap[teamId]; - // Uncheck all first? No, maybe append. Let's append as per common expectations unless explicit clear needed. - // Actually, if I change team, I probably expect to select THAT team's workers. - // Let's clear and select. - // But maybe I want to mix teams. - // User didn't specify. Previous logic was: select workers belonging to team. - // Let's stick to "select", don't uncheck others. - + // Select workers belonging to the team workerIds.forEach(function(id) { const checkbox = document.querySelector(`input[name="workers"][value="${id}"]`); if (checkbox) { @@ -176,6 +181,86 @@ var myModal = new bootstrap.Modal(conflictModalEl); myModal.show(); } + + // --- Cost Calculation Logic --- + const workerRates = {{ worker_rates_json|safe }}; + const startDateInput = document.getElementById('{{ form.date.id_for_label }}'); + const endDateInput = document.getElementById('{{ form.end_date.id_for_label }}'); + const satCheckbox = document.getElementById('{{ form.include_saturday.id_for_label }}'); + const sunCheckbox = document.getElementById('{{ form.include_sunday.id_for_label }}'); + const workerCheckboxes = document.querySelectorAll('input[name="workers"]'); + const totalDisplay = document.getElementById('estimatedTotal'); + const detailsDisplay = document.getElementById('calculationDetails'); + + function calculateTotal() { + // 1. Calculate Days + let days = 0; + const start = startDateInput.value ? new Date(startDateInput.value) : null; + const end = endDateInput.value ? new Date(endDateInput.value) : null; + + if (start) { + if (!end || end < start) { + days = 1; + } else { + // Iterate dates + let curr = new Date(start); + // Reset time components to avoid TZ issues + curr.setHours(0,0,0,0); + const last = new Date(end); + last.setHours(0,0,0,0); + + while (curr <= last) { + const dayOfWeek = curr.getDay(); // 0 = Sun, 6 = Sat + let isWorkingDay = true; + + if (dayOfWeek === 6 && !satCheckbox.checked) isWorkingDay = false; + if (dayOfWeek === 0 && !sunCheckbox.checked) isWorkingDay = false; + + if (isWorkingDay) days++; + + curr.setDate(curr.getDate() + 1); + } + } + } + + // 2. Sum Worker Rates + let dailyRateSum = 0; + let workerCount = 0; + + workerCheckboxes.forEach(cb => { + if (cb.checked) { + const rate = workerRates[cb.value] || 0; + dailyRateSum += rate; + workerCount++; + } + }); + + // 3. Update UI + const total = dailyRateSum * days; + totalDisplay.textContent = 'R ' + total.toLocaleString('en-ZA', {minimumFractionDigits: 2, maximumFractionDigits: 2}); + detailsDisplay.textContent = `${workerCount} worker${workerCount !== 1 ? 's' : ''} × ${days} day${days !== 1 ? 's' : ''}`; + } + + // Attach Listeners + if (startDateInput) startDateInput.addEventListener('change', calculateTotal); + if (endDateInput) endDateInput.addEventListener('change', calculateTotal); + if (satCheckbox) satCheckbox.addEventListener('change', calculateTotal); + if (sunCheckbox) sunCheckbox.addEventListener('change', calculateTotal); + + workerCheckboxes.forEach(cb => { + cb.addEventListener('change', calculateTotal); + }); + + // Also update when team changes (since it selects workers programmatically) + if (teamSelect) { + teamSelect.addEventListener('change', function() { + // Give it a moment for the check logic to finish + setTimeout(calculateTotal, 100); + }); + } + + // Initial Run + calculateTotal(); }); function submitConflict(action) { diff --git a/core/templates/core/work_log_list.html b/core/templates/core/work_log_list.html index 50974f9..fdd58ca 100644 --- a/core/templates/core/work_log_list.html +++ b/core/templates/core/work_log_list.html @@ -80,13 +80,24 @@
- {% if selected_worker or selected_team or selected_project or selected_payment_status and selected_payment_status != 'all' %} -
- - Clear all filters - + +
+ +
+
+ {% if selected_worker or selected_team or selected_project or selected_payment_status and selected_payment_status != 'all' %} + + Clear all filters + + {% else %} + Showing all records + {% endif %} +
+
+ Total Value + R {{ total_amount|floatformat:2 }} +
- {% endif %}
diff --git a/core/views.py b/core/views.py index 9c0cc92..655c5d2 100644 --- a/core/views.py +++ b/core/views.py @@ -24,6 +24,8 @@ def is_staff_or_supervisor(user): """Check if user is staff or manages at least one team/project.""" if user.is_staff or user.is_superuser: return True + if user.has_perm('core.view_project'): + return True return user.managed_teams.exists() or user.assigned_projects.exists() @login_required @@ -141,11 +143,16 @@ def log_attendance(request): conflicts.append(conflict_entry) if conflicts: + # Prepare worker rates for JS calculation + worker_qs = form.fields['workers'].queryset + worker_rates = {w.id: float(w.day_rate) for w in worker_qs} + context = { 'form': form, 'team_workers_json': json.dumps(team_workers_map), 'conflicting_workers': conflicts, 'is_conflict': True, + 'worker_rates_json': json.dumps(worker_rates), } return render(request, 'core/log_attendance.html', context) @@ -203,9 +210,14 @@ def log_attendance(request): else: form = WorkLogForm(user=request.user if request.user.is_authenticated else None) + # Pass worker rates for frontend total calculation + worker_qs = form.fields['workers'].queryset + worker_rates = {w.id: float(w.day_rate) for w in worker_qs} + context = { 'form': form, - 'team_workers_json': json.dumps(team_workers_map) + 'team_workers_json': json.dumps(team_workers_map), + 'worker_rates_json': json.dumps(worker_rates) } return render(request, 'core/log_attendance.html', context) @@ -831,4 +843,4 @@ def create_receipt(request): return render(request, 'core/create_receipt.html', { 'form': form, 'items': items - }) + }) \ No newline at end of file