Replace the green accent with a warm orange/amber palette and switch to a dark-first design. Add a fixed sidebar for desktop navigation and a bottom tab bar for mobile, replacing the top navbar. Cards now use glass-morphism with left accent bars, buttons use orange gradients, and decorative glow effects add depth. All 8 page templates updated, both light and dark modes tested across desktop and mobile viewports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
288 lines
14 KiB
HTML
288 lines
14 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Log Work | FoxFitt{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container py-4">
|
|
<!-- === Page Header === -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="page-title"><i class="fas fa-clipboard-list me-2" style="color: var(--accent);"></i>Log Daily Attendance</h1>
|
|
</div>
|
|
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-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">
|
|
<div class="card-body p-4 p-md-5">
|
|
|
|
{# --- Conflict Warning --- #}
|
|
{% if conflicts %}
|
|
<div class="alert alert-warning 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 %}
|
|
{% 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="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 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 --- #}
|
|
<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 style="color: var(--text-tertiary); font-weight: 400;">(optional)</span>
|
|
</label>
|
|
{{ form.end_date }}
|
|
<small style="color: var(--text-tertiary);">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 style="color: var(--text-tertiary); font-weight: 400;">(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: var(--bg-inset); border-color: var(--border-default) !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 --- #}
|
|
<div class="d-grid mt-5">
|
|
<button type="submit" class="btn btn-lg btn-accent">
|
|
<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 sticky-top" style="top: 80px;">
|
|
<div class="card-body p-4">
|
|
<h6 class="fw-bold mb-3">
|
|
<i class="fas fa-calculator me-2" style="color: var(--accent);"></i>Estimated Cost
|
|
</h6>
|
|
<div class="text-center py-3">
|
|
<div id="estimatedCost" style="font-size: 2rem; font-weight: 700; font-family: 'Poppins', sans-serif; color: var(--accent);">
|
|
R 0.00
|
|
</div>
|
|
<small style="color: var(--text-secondary);">
|
|
<span id="selectedWorkerCount">0</span> worker(s) ×
|
|
<span id="selectedDayCount">1</span> day(s)
|
|
</small>
|
|
</div>
|
|
<hr style="border-color: var(--border-default);">
|
|
<small style="color: var(--text-tertiary);">
|
|
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: Team auto-select + Cost estimator === -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
// === TEAM AUTO-SELECT ===
|
|
var teamWorkersMap = JSON.parse('{{ team_workers_json|escapejs }}');
|
|
var teamSelect = document.querySelector('[name="team"]');
|
|
if (teamSelect) {
|
|
teamSelect.addEventListener('change', function() {
|
|
var teamId = this.value;
|
|
var allBoxes = document.querySelectorAll('input[name="workers"]');
|
|
allBoxes.forEach(function(cb) { cb.checked = false; });
|
|
|
|
if (teamId && teamWorkersMap[teamId]) {
|
|
teamWorkersMap[teamId].forEach(function(id) {
|
|
var checkbox = document.querySelector('input[name="workers"][value="' + id + '"]');
|
|
if (checkbox) checkbox.checked = true;
|
|
});
|
|
}
|
|
|
|
if (typeof updateEstimatedCost === 'function') updateEstimatedCost();
|
|
});
|
|
}
|
|
|
|
{% if is_admin %}
|
|
// === ESTIMATED COST CALCULATOR (Admin Only) ===
|
|
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() {
|
|
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();
|
|
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() {
|
|
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;
|
|
if (costDisplay) costDisplay.textContent = 'R ' + totalCost.toLocaleString('en-ZA', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
|
if (workerCountDisplay) workerCountDisplay.textContent = selectedCount;
|
|
if (dayCountDisplay) dayCountDisplay.textContent = days;
|
|
}
|
|
|
|
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);
|
|
updateEstimatedCost();
|
|
{% endif %}
|
|
});
|
|
</script>
|
|
{% endblock %}
|