- 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>
327 lines
16 KiB
HTML
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) ×
|
|
<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 %}
|