292 lines
13 KiB
HTML
292 lines
13 KiB
HTML
{% extends 'base.html' %}
|
||
{% load static %}
|
||
|
||
{% block title %}Log Daily Attendance | LabourFlow{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="dashboard-header">
|
||
<div class="container">
|
||
<h1 class="display-5 mb-2">Log Daily Attendance</h1>
|
||
<p class="lead opacity-75">Record work for projects and labourers.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container mb-5 mt-n4">
|
||
<div class="row justify-content-center">
|
||
<div class="col-lg-10">
|
||
<div class="card p-4 shadow-sm">
|
||
<form method="post" id="workLogForm">
|
||
{% csrf_token %}
|
||
|
||
<div class="row mb-4">
|
||
<div class="col-md-3">
|
||
<label class="form-label fw-bold">Start Date</label>
|
||
{{ form.date }}
|
||
{% if form.date.errors %}
|
||
<div class="text-danger mt-1 small">{{ form.date.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label fw-bold">End Date (Optional)</label>
|
||
{{ form.end_date }}
|
||
<div class="mt-2">
|
||
<div class="form-check form-check-inline">
|
||
{{ form.include_saturday }}
|
||
<label class="form-check-label small" for="{{ form.include_saturday.id_for_label }}">Sat</label>
|
||
</div>
|
||
<div class="form-check form-check-inline">
|
||
{{ form.include_sunday }}
|
||
<label class="form-check-label small" for="{{ form.include_sunday.id_for_label }}">Sun</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label fw-bold">Project</label>
|
||
{{ form.project }}
|
||
{% if form.project.errors %}
|
||
<div class="text-danger mt-1 small">{{ form.project.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label fw-bold">Team (Optional)</label>
|
||
{{ form.team }}
|
||
{% if form.team.errors %}
|
||
<div class="text-danger mt-1 small">{{ form.team.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-4">
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<label class="form-label fw-bold mb-0">Select Labourers</label>
|
||
<a href="{% url 'manage_resources' %}" class="small text-decoration-none text-primary">Manage Resources</a>
|
||
</div>
|
||
<div class="row">
|
||
{% for checkbox in form.workers %}
|
||
<div class="col-md-6 col-lg-4 mb-2">
|
||
<div class="form-check p-3 border rounded-3 hover-shadow transition-all">
|
||
{{ checkbox.tag }}
|
||
<label class="form-check-label ms-2" for="{{ checkbox.id_for_label }}">
|
||
{{ checkbox.choice_label }}
|
||
</label>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% if form.workers.errors %}
|
||
<div class="text-danger mt-1 small">{{ form.workers.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="mb-4">
|
||
<label class="form-label fw-bold">Notes / Comments</label>
|
||
{{ form.notes }}
|
||
{% if form.notes.errors %}
|
||
<div class="text-danger mt-1 small">{{ form.notes.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if is_admin_user %}
|
||
<!-- Total Cost Estimation (Admin only) -->
|
||
<div class="card p-3 mb-4 bg-light border-0 shadow-sm">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h5 class="mb-0 text-muted">Estimated Cost</h5>
|
||
<small class="text-muted" id="calculationDetails">0 workers × 0 days</small>
|
||
</div>
|
||
<h3 class="mb-0 fw-bold text-dark font-monospace" id="estimatedTotal">R 0.00</h3>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||
<a href="{% url 'home' %}" class="btn btn-light px-4">Cancel</a>
|
||
<button type="submit" class="btn btn-primary px-5">Save Work Log</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if is_conflict %}
|
||
<!-- Conflict Resolution Modal -->
|
||
<div class="modal fade" id="conflictModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="conflictModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-warning text-dark">
|
||
<h5 class="modal-title fw-bold" id="conflictModalLabel">
|
||
Duplicate Logs Detected
|
||
</h5>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p class="fw-bold">The following duplicate entries were found:</p>
|
||
<div class="card bg-light mb-3" style="max-height: 200px; overflow-y: auto;">
|
||
<ul class="list-group list-group-flush">
|
||
{% for conflict in conflicting_workers %}
|
||
<li class="list-group-item bg-transparent">{{ conflict.name }}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
<p>How would you like to proceed?</p>
|
||
<div class="alert alert-info small mb-0">
|
||
<strong>Skip:</strong> Log only the new entries. Existing logs remain unchanged.<br>
|
||
<strong>Overwrite:</strong> Update existing logs for these dates with the new project/team selection.
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="submitConflict('skip')">Skip Duplicates</button>
|
||
<button type="button" class="btn btn-primary" onclick="submitConflict('overwrite')">Overwrite Existing</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<style>
|
||
.hover-shadow:hover {
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||
border-color: #0d6efd !important;
|
||
}
|
||
.transition-all {
|
||
transition: all 0.2s ease-in-out;
|
||
}
|
||
.mt-n4 {
|
||
margin-top: -3rem !important;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const teamSelect = document.getElementById('{{ form.team.id_for_label }}');
|
||
const teamWorkersMap = {{ team_workers_json|safe }};
|
||
|
||
if (teamSelect) {
|
||
teamSelect.addEventListener('change', function() {
|
||
const teamId = this.value;
|
||
|
||
// First, deselect ALL workers to clear previous selection
|
||
const allCheckboxes = document.querySelectorAll('input[name="workers"]');
|
||
allCheckboxes.forEach(cb => {
|
||
cb.checked = false;
|
||
});
|
||
|
||
if (teamId && teamWorkersMap[teamId]) {
|
||
const workerIds = teamWorkersMap[teamId];
|
||
// Select workers belonging to the team
|
||
workerIds.forEach(function(id) {
|
||
const checkbox = document.querySelector(`input[name="workers"][value="${id}"]`);
|
||
if (checkbox) {
|
||
checkbox.checked = true;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Show conflict modal if it exists
|
||
const conflictModalEl = document.getElementById('conflictModal');
|
||
if (conflictModalEl) {
|
||
var myModal = new bootstrap.Modal(conflictModalEl);
|
||
myModal.show();
|
||
}
|
||
|
||
// --- Cost Calculation Logic (Admin only) ---
|
||
{% if is_admin_user %}
|
||
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();
|
||
{% endif %}
|
||
});
|
||
|
||
function submitConflict(action) {
|
||
const form = document.getElementById('workLogForm');
|
||
// Check if input already exists
|
||
let input = form.querySelector('input[name="conflict_action"]');
|
||
if (!input) {
|
||
input = document.createElement('input');
|
||
input.type = 'hidden';
|
||
input.name = 'conflict_action';
|
||
form.appendChild(input);
|
||
}
|
||
input.value = action;
|
||
form.submit();
|
||
}
|
||
</script>
|
||
{% endblock %}
|