diff --git a/config/settings.py b/config/settings.py
index 9d8bcde..d906642 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -190,4 +190,13 @@ if EMAIL_USE_SSL:
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'home'
-LOGOUT_REDIRECT_URL = 'login'
\ No newline at end of file
+LOGOUT_REDIRECT_URL = 'login'
+
+# === MESSAGE TAGS ===
+# Django's messages.error() tags messages as "error", but Bootstrap uses "danger"
+# for red alerts. Without this mapping, error messages would render as "alert-error"
+# which doesn't exist in Bootstrap — making them invisible to the user!
+from django.contrib.messages import constants as message_constants
+MESSAGE_TAGS = {
+ message_constants.ERROR: 'danger',
+}
\ No newline at end of file
diff --git a/core/templates/core/attendance_log.html b/core/templates/core/attendance_log.html
index 6903ade..10223c0 100644
--- a/core/templates/core/attendance_log.html
+++ b/core/templates/core/attendance_log.html
@@ -172,7 +172,7 @@
{# --- Submit Button --- #}
diff --git a/core/templates/core/work_history.html b/core/templates/core/work_history.html
index 8e4b194..765b19c 100644
--- a/core/templates/core/work_history.html
+++ b/core/templates/core/work_history.html
@@ -179,20 +179,48 @@
{# === Day Detail Panel === #}
- {# Hidden by default. When you click a day cell with logs, this panel
- appears showing full details for all entries on that day. #}
+ {# Hidden by default. Click day cells to select them (multi-select supported).
+ The panel shows combined details for all selected days with totals. #}
-
-
- Details
-
-
+
+
+
+ Details
+
+
+
+
+
+
+ {# Hint text for multi-select #}
+
+ Click more days to add them to the selection
+
{# Content built by JavaScript #}
+ {# === Totals Footer (admin only, shown when days are selected) === #}
+ {% if is_admin %}
+
+ {% endif %}
{# Pass calendar detail data to JavaScript safely using json_script #}
@@ -202,133 +230,254 @@
(function() {
'use strict';
- // Parse calendar detail data (keyed by date string)
+ // === CALENDAR MULTI-DAY SELECTION ===
+ // Click a day to add it to the selection. Click again to deselect.
+ // The detail panel shows combined data from ALL selected days.
+ // Admin users see a total amount across all selected days.
+
+ // Parse calendar detail data (keyed by date string, e.g. "2026-02-22")
var calDetail = JSON.parse(document.getElementById('calDetailJson').textContent);
var detailPanel = document.getElementById('dayDetailPanel');
var detailTitle = document.getElementById('dayDetailTitle');
var detailBody = document.getElementById('dayDetailBody');
- var closeBtn = document.getElementById('closeDayDetail');
+ var clearBtn = document.getElementById('clearDaySelection');
+ var selCountBadge = document.getElementById('daySelectionCount');
+ var multiSelectHint = document.getElementById('multiSelectHint');
var isAdmin = {{ is_admin|yesno:"true,false" }};
+ var detailFooter = document.getElementById('dayDetailFooter');
+
+ // Track which dates are currently selected (array of date strings)
+ var selectedDates = [];
+
+ // Short month names for formatting dates
+ var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
+
+ // === Format a date string (YYYY-MM-DD) for display (e.g. "22 Feb") ===
+ function formatDateShort(dateStr) {
+ var parts = dateStr.split('-');
+ var day = parseInt(parts[2], 10);
+ var monthIdx = parseInt(parts[1], 10) - 1;
+ return day + ' ' + months[monthIdx];
+ }
+
+ // === Format a date string for longer display (e.g. "22 Feb 2026") ===
+ function formatDateLong(dateStr) {
+ var parts = dateStr.split('-');
+ var day = parseInt(parts[2], 10);
+ var monthIdx = parseInt(parts[1], 10) - 1;
+ return day + ' ' + months[monthIdx] + ' ' + parts[0];
+ }
+
+ // === Update the detail panel with data from all selected dates ===
+ function updateDetailPanel() {
+ if (selectedDates.length === 0) {
+ // Nothing selected — hide the panel
+ detailPanel.classList.add('d-none');
+ return;
+ }
+
+ // Sort selected dates chronologically
+ selectedDates.sort();
+
+ // Collect all entries from all selected dates
+ var allEntries = [];
+ var totalAmount = 0;
+ var uniqueWorkers = {};
+
+ selectedDates.forEach(function(dateStr) {
+ var entries = calDetail[dateStr] || [];
+ entries.forEach(function(entry) {
+ // Tag each entry with its date for display
+ allEntries.push({ date: dateStr, entry: entry });
+ // Track unique workers
+ entry.workers.forEach(function(w) {
+ uniqueWorkers[w] = true;
+ });
+ // Sum amounts (admin only)
+ if (isAdmin && entry.amount !== undefined) {
+ totalAmount += entry.amount;
+ }
+ });
+ });
+
+ // === Update panel title ===
+ detailTitle.textContent = '';
+ var icon = document.createElement('i');
+ icon.className = 'fas fa-calendar-day me-2';
+ detailTitle.appendChild(icon);
+
+ if (selectedDates.length === 1) {
+ // Single day: show full date
+ detailTitle.appendChild(document.createTextNode(
+ formatDateLong(selectedDates[0]) + ' — ' + allEntries.length + ' log(s)'
+ ));
+ } else {
+ // Multiple days: show date range or count
+ detailTitle.appendChild(document.createTextNode(
+ selectedDates.length + ' days selected — ' + allEntries.length + ' log(s)'
+ ));
+ }
+
+ // Update selection count badge
+ if (selectedDates.length > 1) {
+ selCountBadge.textContent = selectedDates.length + ' days';
+ selCountBadge.classList.remove('d-none');
+ multiSelectHint.classList.add('d-none');
+ } else {
+ selCountBadge.classList.add('d-none');
+ multiSelectHint.classList.remove('d-none');
+ }
+
+ // === Clear previous content ===
+ while (detailBody.firstChild) detailBody.removeChild(detailBody.firstChild);
+
+ // === Build detail table ===
+ var table = document.createElement('table');
+ table.className = 'table table-sm table-hover mb-0';
+
+ var thead = document.createElement('thead');
+ thead.className = 'table-light';
+ var headRow = document.createElement('tr');
+ // Show Date column when multiple days are selected
+ var headers = selectedDates.length > 1
+ ? ['Date', 'Project', 'Workers', 'Supervisor', 'OT', 'Status']
+ : ['Project', 'Workers', 'Supervisor', 'OT', 'Status'];
+ if (isAdmin) headers.push('Amount');
+ headers.forEach(function(h) {
+ var th = document.createElement('th');
+ th.className = (h === 'Project' || h === 'Date') ? 'ps-3' : '';
+ th.textContent = h;
+ headRow.appendChild(th);
+ });
+ thead.appendChild(headRow);
+ table.appendChild(thead);
+
+ var tbody = document.createElement('tbody');
+ allEntries.forEach(function(item) {
+ var entry = item.entry;
+ var tr = document.createElement('tr');
+
+ // Date column (only for multi-day selection)
+ if (selectedDates.length > 1) {
+ var tdDate = document.createElement('td');
+ tdDate.className = 'ps-3';
+ tdDate.textContent = formatDateShort(item.date);
+ tr.appendChild(tdDate);
+ }
+
+ // Project
+ var tdProj = document.createElement('td');
+ tdProj.className = selectedDates.length === 1 ? 'ps-3' : '';
+ var strong = document.createElement('strong');
+ strong.textContent = entry.project;
+ tdProj.appendChild(strong);
+ tr.appendChild(tdProj);
+
+ // Workers
+ var tdWork = document.createElement('td');
+ tdWork.textContent = entry.workers.join(', ');
+ tr.appendChild(tdWork);
+
+ // Supervisor
+ var tdSup = document.createElement('td');
+ tdSup.textContent = entry.supervisor;
+ tr.appendChild(tdSup);
+
+ // Overtime
+ var tdOt = document.createElement('td');
+ if (entry.overtime) {
+ var otBadge = document.createElement('span');
+ otBadge.className = 'badge bg-warning text-dark';
+ otBadge.textContent = entry.overtime;
+ tdOt.appendChild(otBadge);
+ } else {
+ tdOt.textContent = '-';
+ tdOt.className = 'text-muted';
+ }
+ tr.appendChild(tdOt);
+
+ // Status
+ var tdStatus = document.createElement('td');
+ var statusBadge = document.createElement('span');
+ if (entry.is_paid) {
+ statusBadge.className = 'badge bg-success';
+ statusBadge.textContent = 'Paid';
+ } else {
+ statusBadge.className = 'badge bg-danger bg-opacity-75';
+ statusBadge.textContent = 'Unpaid';
+ }
+ tdStatus.appendChild(statusBadge);
+ tr.appendChild(tdStatus);
+
+ // Amount (admin only)
+ if (isAdmin) {
+ var tdAmt = document.createElement('td');
+ tdAmt.textContent = entry.amount !== undefined
+ ? 'R ' + entry.amount.toFixed(2)
+ : '-';
+ tr.appendChild(tdAmt);
+ }
+
+ tbody.appendChild(tr);
+ });
+ table.appendChild(tbody);
+ detailBody.appendChild(table);
+
+ // === Update totals footer (admin only) ===
+ if (isAdmin && detailFooter) {
+ var totalDaysEl = document.getElementById('totalDays');
+ var totalLogsEl = document.getElementById('totalLogs');
+ var totalWorkersEl = document.getElementById('totalWorkers');
+ var totalAmountEl = document.getElementById('totalAmount');
+
+ var uniqueCount = Object.keys(uniqueWorkers).length;
+
+ totalDaysEl.textContent = selectedDates.length + ' day' + (selectedDates.length !== 1 ? 's' : '');
+ totalLogsEl.textContent = allEntries.length + ' log' + (allEntries.length !== 1 ? 's' : '');
+ totalWorkersEl.textContent = uniqueCount + ' unique worker' + (uniqueCount !== 1 ? 's' : '');
+ totalAmountEl.textContent = 'R ' + totalAmount.toFixed(2);
+
+ detailFooter.classList.remove('d-none');
+ }
+
+ // Show the panel and scroll to it
+ detailPanel.classList.remove('d-none');
+ detailPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
// === Click handler for day cells with logs ===
+ // Toggle selection: click to add, click again to remove
document.querySelectorAll('.cal-day--has-logs').forEach(function(cell) {
cell.addEventListener('click', function() {
var dateStr = this.dataset.date;
var entries = calDetail[dateStr] || [];
if (entries.length === 0) return;
- // Remove "selected" class from all cells, add to clicked one
- document.querySelectorAll('.cal-day--selected').forEach(function(c) {
- c.classList.remove('cal-day--selected');
- });
- this.classList.add('cal-day--selected');
+ // Toggle this date in the selection
+ var idx = selectedDates.indexOf(dateStr);
+ if (idx !== -1) {
+ // Already selected — remove it
+ selectedDates.splice(idx, 1);
+ this.classList.remove('cal-day--selected');
+ } else {
+ // Not selected — add it
+ selectedDates.push(dateStr);
+ this.classList.add('cal-day--selected');
+ }
- // Format date for display (e.g. "22 Feb 2026")
- var parts = dateStr.split('-');
- var dateObj = new Date(parts[0], parts[1] - 1, parts[2]);
- var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
- var displayDate = dateObj.getDate() + ' ' + months[dateObj.getMonth()] + ' ' + dateObj.getFullYear();
-
- // Update panel title
- detailTitle.textContent = '';
- var icon = document.createElement('i');
- icon.className = 'fas fa-calendar-day me-2';
- detailTitle.appendChild(icon);
- detailTitle.appendChild(document.createTextNode(displayDate + ' — ' + entries.length + ' log(s)'));
-
- // Clear previous content
- while (detailBody.firstChild) detailBody.removeChild(detailBody.firstChild);
-
- // Build detail table
- var table = document.createElement('table');
- table.className = 'table table-sm table-hover mb-0';
-
- var thead = document.createElement('thead');
- thead.className = 'table-light';
- var headRow = document.createElement('tr');
- var headers = ['Project', 'Workers', 'Supervisor', 'OT', 'Status'];
- if (isAdmin) headers.push('Amount');
- headers.forEach(function(h) {
- var th = document.createElement('th');
- th.className = h === 'Project' ? 'ps-3' : '';
- th.textContent = h;
- headRow.appendChild(th);
- });
- thead.appendChild(headRow);
- table.appendChild(thead);
-
- var tbody = document.createElement('tbody');
- entries.forEach(function(entry) {
- var tr = document.createElement('tr');
-
- // Project
- var tdProj = document.createElement('td');
- tdProj.className = 'ps-3';
- var strong = document.createElement('strong');
- strong.textContent = entry.project;
- tdProj.appendChild(strong);
- tr.appendChild(tdProj);
-
- // Workers
- var tdWork = document.createElement('td');
- tdWork.textContent = entry.workers.join(', ');
- tr.appendChild(tdWork);
-
- // Supervisor
- var tdSup = document.createElement('td');
- tdSup.textContent = entry.supervisor;
- tr.appendChild(tdSup);
-
- // Overtime
- var tdOt = document.createElement('td');
- if (entry.overtime) {
- var otBadge = document.createElement('span');
- otBadge.className = 'badge bg-warning text-dark';
- otBadge.textContent = entry.overtime;
- tdOt.appendChild(otBadge);
- } else {
- tdOt.textContent = '-';
- tdOt.className = 'text-muted';
- }
- tr.appendChild(tdOt);
-
- // Status
- var tdStatus = document.createElement('td');
- var statusBadge = document.createElement('span');
- if (entry.is_paid) {
- statusBadge.className = 'badge bg-success';
- statusBadge.textContent = 'Paid';
- } else {
- statusBadge.className = 'badge bg-danger bg-opacity-75';
- statusBadge.textContent = 'Unpaid';
- }
- tdStatus.appendChild(statusBadge);
- tr.appendChild(tdStatus);
-
- // Amount (admin only)
- if (isAdmin) {
- var tdAmt = document.createElement('td');
- tdAmt.textContent = entry.amount !== undefined ? 'R ' + entry.amount.toFixed(2) : '-';
- tr.appendChild(tdAmt);
- }
-
- tbody.appendChild(tr);
- });
- table.appendChild(tbody);
- detailBody.appendChild(table);
-
- // Show the panel and scroll to it
- detailPanel.classList.remove('d-none');
- detailPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ // Refresh the detail panel with the updated selection
+ updateDetailPanel();
});
});
- // Close detail panel
- closeBtn.addEventListener('click', function() {
- detailPanel.classList.add('d-none');
+ // === Clear all selections ===
+ clearBtn.addEventListener('click', function() {
+ selectedDates = [];
document.querySelectorAll('.cal-day--selected').forEach(function(c) {
c.classList.remove('cal-day--selected');
});
+ detailPanel.classList.add('d-none');
+ if (detailFooter) detailFooter.classList.add('d-none');
});
})();
diff --git a/core/views.py b/core/views.py
index a6aa595..f96a455 100644
--- a/core/views.py
+++ b/core/views.py
@@ -324,10 +324,9 @@ def attendance_log(request):
return redirect('home')
else:
- form = AttendanceLogForm(
- user=user,
- initial={'date': timezone.now().date()}
- )
+ # Don't pre-fill the start date — force the user to pick one
+ # so they don't accidentally log work on the wrong day
+ form = AttendanceLogForm(user=user)
# Build a list of worker data for the estimated cost JavaScript
# (admins only — supervisors don't see the cost card)