38686-vm/core/templates/core/attendance_log.html
Konrad du Plessis 19c662ec7d Fix 3 critical bugs in dashboard + attendance logging
- Fix outstanding payments: check per-worker (not per-log) to handle partially-paid WorkLogs
- Fix adjustment math: deductions now subtract from outstanding instead of adding
- Fix conflict resolution: use explicit worker ID list (QueryDict.getlist) instead of broken form.data.workers iteration
- Add missing migration 0003 for Project start_date/end_date fields
- Add CLAUDE.md project documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:28:11 +02:00

327 lines
16 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}Log Work | Fox Fitt{% endblock %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0" style="font-family: 'Poppins', sans-serif;">Log Daily Attendance</h1>
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm shadow-sm">
<i class="fas fa-arrow-left fa-sm me-1"></i> Back
</a>
</div>
<div class="row">
<!-- Main Form Column -->
<div class="{% if is_admin %}col-lg-8{% else %}col-lg-8 mx-auto{% endif %}">
<div class="card shadow-sm border-0" style="border-radius: 12px;">
<div class="card-body p-4 p-md-5">
{# --- Conflict Warning --- #}
{# If we found workers already logged on selected dates, show this warning #}
{% if conflicts %}
<div class="alert alert-warning border-0 shadow-sm mb-4" role="alert">
<h6 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>Conflicts Found
</h6>
<p class="mb-2">The following workers already have work logs on the selected dates:</p>
<ul class="mb-3">
{% for c in conflicts %}
<li><strong>{{ c.worker_name }}</strong> on {{ c.date }} ({{ c.project_name }})</li>
{% endfor %}
</ul>
<div class="d-flex gap-2">
<form method="POST" class="d-inline">
{% csrf_token %}
{# Re-submit all form data with a conflict_action flag #}
{# Non-multi-value fields from form.data #}
{% for key, value in form.data.items %}
{% if key != 'csrfmiddlewaretoken' and key != 'conflict_action' and key != 'workers' %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endif %}
{% endfor %}
{# Workers is a multi-value field — use the explicit list #}
{# passed from the view (QueryDict.getlist) to avoid losing values #}
{% for wid in selected_worker_ids %}
<input type="hidden" name="workers" value="{{ wid }}">
{% endfor %}
<input type="hidden" name="conflict_action" value="skip">
<button type="submit" class="btn btn-outline-warning btn-sm">
<i class="fas fa-forward me-1"></i> Skip Conflicts
</button>
</form>
<form method="POST" class="d-inline">
{% csrf_token %}
{% for key, value in form.data.items %}
{% if key != 'csrfmiddlewaretoken' and key != 'conflict_action' and key != 'workers' %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endif %}
{% endfor %}
{% for wid in selected_worker_ids %}
<input type="hidden" name="workers" value="{{ wid }}">
{% endfor %}
<input type="hidden" name="conflict_action" value="overwrite">
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="fas fa-sync me-1"></i> Overwrite Existing
</button>
</form>
</div>
</div>
{% endif %}
{# --- Form Errors --- #}
{% if form.errors %}
<div class="alert alert-danger border-0 shadow-sm mb-4">
<strong><i class="fas fa-exclamation-circle me-1"></i> Please fix the following:</strong>
<ul class="mb-0 mt-2">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
<form method="POST" id="attendanceForm">
{% csrf_token %}
{# --- Date Range Section --- #}
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-semibold">Start Date</label>
{{ form.date }}
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">
End Date <span class="text-muted fw-normal">(optional)</span>
</label>
{{ form.end_date }}
<small class="text-muted">Leave blank to log a single day</small>
</div>
</div>
{# --- Weekend Checkboxes --- #}
<div class="d-flex gap-4 mb-4">
<div class="form-check">
{{ form.include_saturday }}
<label class="form-check-label ms-1" for="{{ form.include_saturday.id_for_label }}">
Include Saturdays
</label>
</div>
<div class="form-check">
{{ form.include_sunday }}
<label class="form-check-label ms-1" for="{{ form.include_sunday.id_for_label }}">
Include Sundays
</label>
</div>
</div>
{# --- Project and Team --- #}
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-semibold">Project</label>
{{ form.project }}
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">
Team <span class="text-muted fw-normal">(optional — selects all team workers)</span>
</label>
{{ form.team }}
</div>
</div>
{# --- Worker Checkboxes --- #}
<div class="mb-4">
<label class="form-label d-block mb-3 fw-semibold">Workers Present</label>
<div class="worker-selection border rounded p-3" style="max-height: 300px; overflow-y: auto; background-color: #f8fafc; border-color: #e2e8f0 !important;">
<div class="row">
{% for worker in form.workers %}
<div class="col-md-6 mb-2">
<div class="form-check">
{{ worker.tag }}
<label class="form-check-label ms-1" for="{{ worker.id_for_label }}">
{{ worker.choice_label }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{# --- Overtime --- #}
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-semibold">Overtime</label>
{{ form.overtime_amount }}
</div>
</div>
{# --- Notes --- #}
<div class="mb-4">
<label class="form-label fw-semibold">Notes</label>
{{ form.notes }}
</div>
{# --- Submit Button --- #}
<div class="d-grid mt-5">
<button type="submit" class="btn btn-lg btn-accent shadow-sm" style="border-radius: 8px;">
<i class="fas fa-save me-2"></i>Log Work
</button>
</div>
</form>
</div>
</div>
</div>
{# --- Estimated Cost Card (Admin Only) --- #}
{% if is_admin %}
<div class="col-lg-4 mt-4 mt-lg-0">
<div class="card shadow-sm border-0 sticky-top" style="border-radius: 12px; top: 80px;">
<div class="card-body p-4">
<h6 class="fw-bold mb-3">
<i class="fas fa-calculator me-2 text-success"></i>Estimated Cost
</h6>
<div class="text-center py-3">
<div class="display-6 fw-bold" id="estimatedCost" style="color: var(--accent-color, #10b981);">
R 0.00
</div>
<small class="text-muted">
<span id="selectedWorkerCount">0</span> worker(s) &times;
<span id="selectedDayCount">1</span> day(s)
</small>
</div>
<hr>
<small class="text-muted">
This estimate is based on each worker's daily rate multiplied by the
number of working days selected. Overtime is not included.
</small>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{# --- JavaScript for dynamic features --- #}
<script>
document.addEventListener('DOMContentLoaded', function() {
// === TEAM AUTO-SELECT ===
// When a team is chosen from the dropdown, automatically check all workers
// that belong to that team. Uses team_workers_json passed from the view.
var teamWorkersMap = JSON.parse('{{ team_workers_json|escapejs }}');
var teamSelect = document.querySelector('[name="team"]');
if (teamSelect) {
teamSelect.addEventListener('change', function() {
var teamId = this.value;
// First, uncheck ALL worker checkboxes
var allBoxes = document.querySelectorAll('input[name="workers"]');
allBoxes.forEach(function(cb) {
cb.checked = false;
});
// Then check workers that belong to the selected team
if (teamId && teamWorkersMap[teamId]) {
var workerIds = teamWorkersMap[teamId];
workerIds.forEach(function(id) {
var checkbox = document.querySelector('input[name="workers"][value="' + id + '"]');
if (checkbox) {
checkbox.checked = true;
}
});
}
// Recalculate estimated cost if the admin cost calculator exists
if (typeof updateEstimatedCost === 'function') {
updateEstimatedCost();
}
});
}
{% if is_admin %}
// === ESTIMATED COST CALCULATOR (Admin Only) ===
// Updates the cost card in real-time as workers and dates are selected.
// Worker daily rates passed from the view
const workerRates = {{ worker_rates_json|safe }};
const startDateInput = document.querySelector('[name="date"]');
const endDateInput = document.querySelector('[name="end_date"]');
const satCheckbox = document.querySelector('[name="include_saturday"]');
const sunCheckbox = document.querySelector('[name="include_sunday"]');
const workerCheckboxes = document.querySelectorAll('[name="workers"]');
const costDisplay = document.getElementById('estimatedCost');
const workerCountDisplay = document.getElementById('selectedWorkerCount');
const dayCountDisplay = document.getElementById('selectedDayCount');
function countWorkingDays() {
// Count how many working days are in the selected date range
const startDate = startDateInput ? new Date(startDateInput.value) : null;
const endDateVal = endDateInput ? endDateInput.value : '';
const endDate = endDateVal ? new Date(endDateVal) : startDate;
if (!startDate || isNaN(startDate)) return 1;
if (!endDate || isNaN(endDate)) return 1;
let count = 0;
let current = new Date(startDate);
while (current <= endDate) {
const day = current.getDay(); // 0=Sun, 6=Sat
if (day === 6 && !(satCheckbox && satCheckbox.checked)) {
current.setDate(current.getDate() + 1);
continue;
}
if (day === 0 && !(sunCheckbox && sunCheckbox.checked)) {
current.setDate(current.getDate() + 1);
continue;
}
count++;
current.setDate(current.getDate() + 1);
}
return Math.max(count, 1);
}
function updateEstimatedCost() {
// Add up daily rates of all checked workers, multiply by number of days
let totalDailyRate = 0;
let selectedCount = 0;
workerCheckboxes.forEach(function(cb) {
if (cb.checked) {
const workerId = cb.value;
if (workerRates[workerId]) {
totalDailyRate += parseFloat(workerRates[workerId]);
}
selectedCount++;
}
});
const days = countWorkingDays();
const totalCost = totalDailyRate * days;
// Update the display
if (costDisplay) costDisplay.textContent = 'R ' + totalCost.toLocaleString('en-ZA', {minimumFractionDigits: 2, maximumFractionDigits: 2});
if (workerCountDisplay) workerCountDisplay.textContent = selectedCount;
if (dayCountDisplay) dayCountDisplay.textContent = days;
}
// Listen for changes on all relevant inputs
workerCheckboxes.forEach(function(cb) {
cb.addEventListener('change', updateEstimatedCost);
});
if (startDateInput) startDateInput.addEventListener('change', updateEstimatedCost);
if (endDateInput) endDateInput.addEventListener('change', updateEstimatedCost);
if (satCheckbox) satCheckbox.addEventListener('change', updateEstimatedCost);
if (sunCheckbox) sunCheckbox.addEventListener('change', updateEstimatedCost);
// Run once on page load in case of pre-selected values
updateEstimatedCost();
{% endif %}
});
</script>
{% endblock %}