Redesign UI with premium orange theme, sidebar nav, and bottom tab bar

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>
This commit is contained in:
Konrad du Plessis 2026-04-20 18:40:00 +02:00
parent a1ac8540ab
commit 82c1906607
9 changed files with 2150 additions and 1161 deletions

View File

@ -1,10 +1,21 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}FoxFitt{% endblock %}</title>
<!-- === THEME: Apply saved preference BEFORE first paint (prevents flash) === -->
<script>
(function() {
var saved = localStorage.getItem('foxfitt-theme');
if (saved === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script>
<!-- Bootstrap 5.3 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- Google Fonts -->
@ -13,112 +24,221 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@500;600;700&display=swap" rel="stylesheet">
<!-- Font Awesome 6 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Custom CSS -->
<!-- Custom CSS (cache-busted with deployment timestamp) -->
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ request.timestamp|default:'1.0' }}">
<style>
/* Layout helpers — keep body full-height so footer sticks to bottom */
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex-grow: 1; }
/* Branding — Fox in green, Fitt in white */
.navbar-brand-fox { color: #10b981; font-weight: 700; }
.navbar-brand-fitt { color: #ffffff; font-weight: 700; }
.nav-link { font-weight: 500; }
.dropdown-menu { border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark sticky-top shadow-sm">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="{% url 'home' %}">
<span class="navbar-brand-fox">Fox</span>
<span class="navbar-brand-fitt">Fitt</span>
</a>
{% if user.is_authenticated %}
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'home' %}active{% endif %}" href="{% url 'home' %}">
<i class="fas fa-home me-1"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'attendance_log' %}active{% endif %}" href="{% url 'attendance_log' %}">
<i class="fas fa-clipboard-list me-1"></i> Log Work
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'work_history' %}active{% endif %}" href="{% url 'work_history' %}">
<i class="fas fa-clock me-1"></i> Work History
</a>
</li>
{% if user.is_staff %}
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'payroll_dashboard' %}active{% endif %}" href="{% url 'payroll_dashboard' %}">
<i class="fas fa-wallet me-1"></i> Payroll
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'create_receipt' %}active{% endif %}" href="{% url 'create_receipt' %}">
<i class="fas fa-receipt me-1"></i> Receipts
</a>
</li>
{% if user.is_staff %}
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">
<i class="fas fa-cog me-1"></i> Admin
</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
<li class="nav-item d-flex align-items-center">
<span class="nav-link text-light pe-2">
<i class="fas fa-user-circle me-1"></i> {{ user.username }}
</span>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="fas fa-sign-out-alt me-1"></i> Logout
</button>
</form>
</li>
</ul>
</div>
{% endif %}
</div>
</nav>
<div class="container mt-4">
<!-- Messages Block -->
{% if user.is_authenticated %}
<!-- ===================================================================
APP LAYOUT — sidebar (desktop) + top bar (mobile) + content
=================================================================== -->
<div class="app-layout">
<!-- === SIDEBAR (desktop only, hidden on mobile via CSS) === -->
<aside class="app-sidebar d-print-none">
<!-- Brand / Logo -->
<div class="sidebar-brand">
<div class="sidebar-brand__icon">
<i class="fas fa-bolt"></i>
</div>
<a href="{% url 'home' %}" class="sidebar-brand__text">
<span>Fox</span>Fitt
</a>
</div>
<!-- Navigation Links -->
<nav class="sidebar-nav">
<a href="{% url 'home' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'home' %}active{% endif %}">
<i class="fas fa-th-large"></i>
<span>Dashboard</span>
</a>
<a href="{% url 'attendance_log' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'attendance_log' %}active{% endif %}">
<i class="fas fa-clipboard-list"></i>
<span>Log Work</span>
</a>
<a href="{% url 'work_history' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'work_history' %}active{% endif %}">
<i class="fas fa-clock"></i>
<span>History</span>
</a>
{% if user.is_staff %}
<a href="{% url 'payroll_dashboard' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'payroll_dashboard' %}active{% endif %}">
<i class="fas fa-wallet"></i>
<span>Payroll</span>
</a>
{% endif %}
<a href="{% url 'create_receipt' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'create_receipt' %}active{% endif %}">
<i class="fas fa-receipt"></i>
<span>Receipts</span>
</a>
{% if user.is_staff %}
<a href="{% url 'admin:index' %}" class="sidebar-nav__link">
<i class="fas fa-cog"></i>
<span>Admin</span>
</a>
{% endif %}
</nav>
<!-- Sidebar Footer: theme toggle + user -->
<div class="sidebar-footer">
<div class="d-flex align-items-center justify-content-between mb-3">
<button type="button" class="theme-toggle" id="themeToggle" title="Toggle dark/light mode">
<i class="fas fa-moon" id="themeIcon"></i>
</button>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit" class="theme-toggle" title="Sign out" style="color: var(--color-danger);">
<i class="fas fa-sign-out-alt"></i>
</button>
</form>
</div>
<div class="sidebar-user">
<div class="sidebar-user__avatar">
{{ user.username|make_list|first|upper }}
</div>
<div>
<div class="sidebar-user__name">{{ user.first_name|default:user.username }}</div>
<div class="sidebar-user__role">{% if user.is_staff %}Administrator{% else %}Supervisor{% endif %}</div>
</div>
</div>
</div>
</aside>
<!-- === MAIN CONTENT AREA === -->
<div class="app-main">
<!-- === TOP BAR (mobile only, hidden on desktop via CSS) === -->
<div class="app-topbar d-print-none">
<a href="{% url 'home' %}" style="text-decoration: none; font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 1.2rem;">
<span style="color: var(--accent);">Fox</span><span style="color: var(--text-on-nav);">Fitt</span>
</a>
<div class="d-flex align-items-center gap-2">
<button type="button" class="theme-toggle" id="themeToggleMobile" title="Toggle dark/light mode">
<i class="fas fa-moon" id="themeIconMobile"></i>
</button>
<form method="post" action="{% url 'logout' %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="theme-toggle" title="Sign out" style="color: var(--color-danger);">
<i class="fas fa-sign-out-alt"></i>
</button>
</form>
</div>
</div>
<!-- === Flash messages (Django messages framework) === -->
{% if messages %}
<div class="container-fluid px-3 px-lg-4 mt-3">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show shadow-sm" role="alert">
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{% if message.tags == 'success' %}<i class="fas fa-check-circle me-2"></i>
{% elif message.tags == 'error' or message.tags == 'danger' %}<i class="fas fa-exclamation-circle me-2"></i>
{% elif message.tags == 'warning' %}<i class="fas fa-exclamation-triangle me-2"></i>
{% elif message.tags == 'info' %}<i class="fas fa-info-circle me-2"></i>
{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
</div>
<!-- Main Content -->
<main>
{% block content %}
{% endblock %}
</main>
<!-- Footer -->
<footer class="py-4 mt-auto border-top border-secondary">
<div class="container text-center">
<p class="mb-0 small">&copy; {% now "Y" %} FoxFitt Construction. All rights reserved.</p>
</div>
</footer>
{% endif %}
<!-- Bootstrap 5.3 JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<!-- === Page Content === -->
<div class="app-content">
{% endif %}
{% block content %}
{% endblock %}
{% if user.is_authenticated %}
</div>
<!-- === Footer (inside main area) === -->
<footer class="app-footer d-print-none">
<div class="container">
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-center">
<p class="mb-1 mb-sm-0">
<span style="color: var(--accent); font-weight: 600;">Fox</span><span style="font-weight: 600;">Fitt</span>
<span class="ms-1">Construction</span>
</p>
<p class="mb-0">&copy; {% now "Y" %} All rights reserved.</p>
</div>
</div>
</footer>
<!-- === BOTTOM TAB BAR (mobile only, hidden on desktop via CSS) === -->
<nav class="app-bottom-nav d-print-none">
<div class="bottom-nav-inner">
<a href="{% url 'home' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'home' %}active{% endif %}">
<i class="fas fa-th-large"></i>
<span>Dashboard</span>
</a>
<a href="{% url 'attendance_log' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'attendance_log' %}active{% endif %}">
<i class="fas fa-clipboard-list"></i>
<span>Log Work</span>
</a>
{% if user.is_staff %}
<a href="{% url 'payroll_dashboard' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'payroll_dashboard' %}active{% endif %}">
<i class="fas fa-wallet"></i>
<span>Payroll</span>
</a>
{% endif %}
<a href="{% url 'work_history' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'work_history' %}active{% endif %}">
<i class="fas fa-clock"></i>
<span>History</span>
</a>
<a href="{% url 'create_receipt' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'create_receipt' %}active{% endif %}">
<i class="fas fa-receipt"></i>
<span>Receipts</span>
</a>
</div>
</nav>
</div>
</div>
{% endif %}
<!-- Bootstrap 5.3 JS Bundle (includes Popper) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<!-- === THEME TOGGLE — switches dark/light and persists to localStorage === -->
<script>
(function() {
// Both desktop sidebar and mobile top bar toggle buttons
var toggles = [
{ btn: document.getElementById('themeToggle'), icon: document.getElementById('themeIcon') },
{ btn: document.getElementById('themeToggleMobile'), icon: document.getElementById('themeIconMobile') }
];
function updateIcons() {
var isDark = document.documentElement.getAttribute('data-theme') !== 'light';
toggles.forEach(function(t) {
if (t.icon) {
t.icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
}
if (t.btn) {
t.btn.title = isDark ? 'Switch to light mode' : 'Switch to dark mode';
}
});
}
updateIcons();
toggles.forEach(function(t) {
if (t.btn) {
t.btn.addEventListener('click', function() {
var current = document.documentElement.getAttribute('data-theme');
var next = (current === 'light') ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('foxfitt-theme', next);
updateIcons();
});
}
});
})();
</script>
{% block extra_js %}{% endblock %}
</body>
</html>
</html>

View File

@ -1,27 +1,29 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Log Work | Fox Fitt{% endblock %}
{% 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">
<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">
<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 -->
<!-- === 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">
<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">
<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>
@ -34,15 +36,11 @@
<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 %}
@ -72,7 +70,7 @@
{# --- Form Errors --- #}
{% if form.errors %}
<div class="alert alert-danger border-0 shadow-sm mb-4">
<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 %}
@ -87,7 +85,7 @@
<form method="POST" id="attendanceForm">
{% csrf_token %}
{# --- Date Range Section --- #}
{# --- Date Range --- #}
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-semibold">Start Date</label>
@ -95,10 +93,10 @@
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">
End Date <span class="text-muted fw-normal">(optional)</span>
End Date <span style="color: var(--text-tertiary); font-weight: 400;">(optional)</span>
</label>
{{ form.end_date }}
<small class="text-muted">Leave blank to log a single day</small>
<small style="color: var(--text-tertiary);">Leave blank to log a single day</small>
</div>
</div>
@ -126,7 +124,7 @@
</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>
Team <span style="color: var(--text-tertiary); font-weight: 400;">(optional — selects all team workers)</span>
</label>
{{ form.team }}
</div>
@ -135,7 +133,7 @@
{# --- 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="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">
@ -165,9 +163,9 @@
{{ form.notes }}
</div>
{# --- Submit Button --- #}
{# --- Submit --- #}
<div class="d-grid mt-5">
<button type="submit" class="btn btn-lg btn-accent shadow-sm" style="border-radius: 8px;">
<button type="submit" class="btn btn-lg btn-accent">
<i class="fas fa-save me-2"></i>Log Work
</button>
</div>
@ -179,22 +177,22 @@
{# --- 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 sticky-top" style="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
<i class="fas fa-calculator me-2" style="color: var(--accent);"></i>Estimated Cost
</h6>
<div class="text-center py-3">
<div class="display-6 fw-bold" id="estimatedCost" style="color: var(--accent-color, #10b981);">
<div id="estimatedCost" style="font-size: 2rem; font-weight: 700; font-family: 'Poppins', sans-serif; color: var(--accent);">
R 0.00
</div>
<small class="text-muted">
<small style="color: var(--text-secondary);">
<span id="selectedWorkerCount">0</span> worker(s) &times;
<span id="selectedDayCount">1</span> day(s)
</small>
</div>
<hr>
<small class="text-muted">
<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>
@ -205,50 +203,33 @@
</div>
</div>
{# --- JavaScript for dynamic features --- #}
<!-- === JavaScript: Team auto-select + Cost estimator === -->
<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;
});
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) {
teamWorkersMap[teamId].forEach(function(id) {
var checkbox = document.querySelector('input[name="workers"][value="' + id + '"]');
if (checkbox) {
checkbox.checked = true;
}
if (checkbox) checkbox.checked = true;
});
}
// Recalculate estimated cost if the admin cost calculator exists
if (typeof updateEstimatedCost === 'function') {
updateEstimatedCost();
}
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"]');
@ -259,26 +240,18 @@ document.addEventListener('DOMContentLoaded', function() {
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;
}
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);
}
@ -286,39 +259,27 @@ document.addEventListener('DOMContentLoaded', function() {
}
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]);
}
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);
});
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 %}
});

View File

@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% block title %}Create Receipt | Fox Fitt{% endblock %}
{% block title %}Create Receipt | FoxFitt{% endblock %}
{% block content %}
<!-- === CREATE EXPENSE RECEIPT ===
@ -9,182 +9,164 @@
- Live VAT calculation (Included / Excluded / None)
- On submit: saves to database + emails HTML + PDF to Spark Receipt -->
<div class="container py-5">
<div class="card border-0 shadow-sm">
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<!-- Card header -->
<div class="card-header py-3" style="background-color: var(--primary-color);">
<h4 class="mb-0 text-white fw-bold">
<i class="fas fa-file-invoice-dollar me-2"></i> Create Expense Receipt
</h4>
</div>
<!-- Page header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="page-title"><i class="fas fa-receipt me-2" style="color: var(--accent);"></i>Create Expense Receipt</h1>
<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="card-body p-4">
<form method="post" id="receipt-form">
{% csrf_token %}
<div class="card">
<div class="card-body p-4">
<form method="post" id="receipt-form">
{% csrf_token %}
<!-- === RECEIPT HEADER FIELDS === -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-bold text-secondary">Date</label>
{{ form.date }}
</div>
<div class="col-md-6">
<label class="form-label fw-bold text-secondary">Vendor Name</label>
{{ form.vendor_name }}
<div class="form-text text-muted small">
<i class="fas fa-info-circle"></i> Will appear as the main title on the receipt.
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-bold text-secondary">Payment Method</label>
{{ form.payment_method }}
</div>
<div class="col-12">
<label class="form-label fw-bold text-secondary">Description</label>
{{ form.description }}
</div>
</div>
<hr class="my-4">
<!-- === LINE ITEMS SECTION ===
Each row is a product name + amount.
The "Add Line" button adds new rows via JavaScript.
The X button hides the row and checks a hidden DELETE checkbox. -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="fw-bold text-dark m-0">Items</h5>
<button type="button" id="add-item" class="btn btn-sm btn-outline-secondary rounded-pill">
<i class="fas fa-plus me-1"></i> Add Line
</button>
</div>
<!-- Django formset management form — tracks how many item forms exist -->
{{ items.management_form }}
<div id="items-container">
{% for item_form in items %}
<div class="item-row row g-2 align-items-center mb-2">
<!-- Hidden ID field (used by Django to track existing items) -->
{{ item_form.id }}
<!-- Product name (takes most of the row) -->
<div class="col-12 col-md-7">
{{ item_form.product_name }}
</div>
<!-- Amount with "R" prefix -->
<div class="col-10 col-md-4">
<div class="input-group">
<span class="input-group-text bg-light border-end-0">R</span>
{{ item_form.amount }}
<!-- === RECEIPT HEADER FIELDS === -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-semibold">Date</label>
{{ form.date }}
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Vendor Name</label>
{{ form.vendor_name }}
<small style="color: var(--text-tertiary);">
<i class="fas fa-info-circle"></i> Will appear as the main title on the receipt.
</small>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Payment Method</label>
{{ form.payment_method }}
</div>
<div class="col-12">
<label class="form-label fw-semibold">Description</label>
{{ form.description }}
</div>
</div>
<!-- Delete button — hides the row and checks the DELETE checkbox -->
<div class="col-2 col-md-1 text-center">
{% if items.can_delete %}
<div class="form-check d-none">
{{ item_form.DELETE }}
</div>
<button type="button" class="btn btn-outline-danger btn-sm delete-row rounded-circle" title="Remove">
<i class="fas fa-times"></i>
</button>
{% endif %}
<hr style="border-color: var(--border-default);">
<!-- === LINE ITEMS === -->
<div class="d-flex justify-content-between align-items-center mb-3 mt-4">
<h5 class="fw-bold m-0">Items</h5>
<button type="button" id="add-item" class="btn btn-sm btn-outline-secondary rounded-pill">
<i class="fas fa-plus me-1"></i> Add Line
</button>
</div>
</div>
{% endfor %}
</div>
<hr class="my-4">
{{ items.management_form }}
<!-- === VAT CONFIGURATION + LIVE TOTALS === -->
<div class="row">
<!-- Left: VAT type radio buttons -->
<div class="col-md-6 mb-3 mb-md-0">
<label class="form-label d-block fw-bold text-secondary mb-2">VAT Configuration (15%)</label>
<div class="card bg-light border-0 p-3">
{% for radio in form.vat_type %}
<div class="form-check mb-2">
{{ radio.tag }}
<label class="form-check-label" for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
</label>
<div id="items-container">
{% for item_form in items %}
<div class="item-row row g-2 align-items-center mb-2">
{{ item_form.id }}
<div class="col-12 col-md-7">
{{ item_form.product_name }}
</div>
<div class="col-10 col-md-4">
<div class="input-group">
<span class="input-group-text">R</span>
{{ item_form.amount }}
</div>
</div>
<div class="col-2 col-md-1 text-center">
{% if items.can_delete %}
<div class="form-check d-none">
{{ item_form.DELETE }}
</div>
<button type="button" class="btn btn-outline-danger btn-sm delete-row rounded-circle" title="Remove">
<i class="fas fa-times"></i>
</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Right: Live-updating totals panel -->
<div class="col-md-6">
<label class="form-label d-block fw-bold text-secondary mb-2">Receipt Totals</label>
<div class="p-3 rounded" style="background-color: #f8fafc; border: 1px solid #e2e8f0;">
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Subtotal (Excl. VAT):</span>
<span class="fw-bold">R <span id="display-subtotal">0.00</span></span>
<hr style="border-color: var(--border-default);">
<!-- === VAT + TOTALS === -->
<div class="row mt-4">
<!-- VAT radio buttons -->
<div class="col-md-6 mb-3 mb-md-0">
<label class="form-label d-block fw-semibold mb-2">VAT Configuration (15%)</label>
<div class="p-3 rounded" style="background: var(--bg-inset); border: 1px solid var(--border-default);">
{% for radio in form.vat_type %}
<div class="form-check mb-2">
{{ radio.tag }}
<label class="form-check-label" for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
</label>
</div>
{% endfor %}
</div>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">VAT (15%):</span>
<span class="fw-bold">R <span id="display-vat">0.00</span></span>
</div>
<div class="d-flex justify-content-between border-top pt-2 mt-2">
<span class="h5 mb-0 fw-bold">Total:</span>
<span class="h5 mb-0" style="color: var(--accent-color);">
R <span id="display-total">0.00</span>
</span>
<!-- Live totals -->
<div class="col-md-6">
<label class="form-label d-block fw-semibold mb-2">Receipt Totals</label>
<div class="p-3 rounded" style="background: var(--bg-inset); border: 1px solid var(--border-default);">
<div class="d-flex justify-content-between mb-2">
<span style="color: var(--text-secondary);">Subtotal (Excl. VAT):</span>
<span class="fw-bold">R <span id="display-subtotal">0.00</span></span>
</div>
<div class="d-flex justify-content-between mb-2">
<span style="color: var(--text-secondary);">VAT (15%):</span>
<span class="fw-bold">R <span id="display-vat">0.00</span></span>
</div>
<div class="d-flex justify-content-between pt-2 mt-2" style="border-top: 1px solid var(--border-default);">
<span class="h5 mb-0 fw-bold">Total:</span>
<span class="h5 mb-0" style="color: var(--accent); font-family: 'Poppins', sans-serif;">
R <span id="display-total">0.00</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- === SUBMIT BUTTON === -->
<div class="text-end mt-4">
<button type="submit" class="btn btn-accent btn-lg">
<i class="fas fa-paper-plane me-2"></i> Create & Send Receipt
</button>
<!-- Submit -->
<div class="text-end mt-4">
<button type="submit" class="btn btn-accent btn-lg">
<i class="fas fa-paper-plane me-2"></i> Create & Send Receipt
</button>
</div>
</form>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- ==========================================================================
JAVASCRIPT — Dynamic line items + live VAT calculation
========================================================================== -->
<!-- === JavaScript: Dynamic line items + live VAT calculation === -->
<script>
(function() {
'use strict';
// --- DOM REFERENCES ---
var itemsContainer = document.getElementById('items-container');
var addItemBtn = document.getElementById('add-item');
var totalForms = document.querySelector('#id_line_items-TOTAL_FORMS');
var displaySubtotal = document.getElementById('display-subtotal');
var displayVat = document.getElementById('display-vat');
var displayTotal = document.getElementById('display-total');
// All VAT radio buttons — we listen for changes on these
var vatRadios = document.querySelectorAll('input[name="vat_type"]');
// === ADD NEW LINE ITEM ROW ===
// When "Add Line" is clicked, build a new blank row using DOM methods.
// We increment TOTAL_FORMS so Django knows there's an extra form.
// === ADD NEW LINE ITEM ===
addItemBtn.addEventListener('click', function() {
var formIdx = parseInt(totalForms.value);
// Create the row container
var row = document.createElement('div');
row.className = 'item-row row g-2 align-items-center mb-2';
// Hidden ID input (required by Django formset)
var hiddenId = document.createElement('input');
hiddenId.type = 'hidden';
hiddenId.name = 'line_items-' + formIdx + '-id';
hiddenId.id = 'id_line_items-' + formIdx + '-id';
row.appendChild(hiddenId);
// Product name column
var prodCol = document.createElement('div');
prodCol.className = 'col-12 col-md-7';
var prodInput = document.createElement('input');
@ -196,13 +178,12 @@
prodCol.appendChild(prodInput);
row.appendChild(prodCol);
// Amount column with "R" prefix
var amtCol = document.createElement('div');
amtCol.className = 'col-10 col-md-4';
var inputGroup = document.createElement('div');
inputGroup.className = 'input-group';
var prefix = document.createElement('span');
prefix.className = 'input-group-text bg-light border-end-0';
prefix.className = 'input-group-text';
prefix.textContent = 'R';
var amtInput = document.createElement('input');
amtInput.type = 'number';
@ -216,7 +197,6 @@
amtCol.appendChild(inputGroup);
row.appendChild(amtCol);
// Delete button column
var delCol = document.createElement('div');
delCol.className = 'col-2 col-md-1 text-center';
var delBtn = document.createElement('button');
@ -229,98 +209,60 @@
delCol.appendChild(delBtn);
row.appendChild(delCol);
// Add to DOM and update form count
itemsContainer.appendChild(row);
totalForms.value = formIdx + 1;
// Recalculate totals
updateCalculations();
});
// === DELETE LINE ITEM ROW ===
// Uses event delegation — listens on the container for any delete button click.
// If the row has a DELETE checkbox (existing saved item), checks it and hides the row.
// If the row is brand new (no DELETE checkbox), just removes it from the DOM.
// === DELETE LINE ITEM ===
itemsContainer.addEventListener('click', function(e) {
var deleteBtn = e.target.closest('.delete-row');
if (!deleteBtn) return;
var row = deleteBtn.closest('.item-row');
var deleteCheckbox = row.querySelector('input[name$="-DELETE"]');
if (deleteCheckbox) {
// Existing item — check DELETE and hide (Django will delete on save)
deleteCheckbox.checked = true;
row.classList.add('d-none', 'deleted');
} else {
// New item — just remove from DOM
row.remove();
}
updateCalculations();
});
// === LIVE AMOUNT INPUT CHANGES ===
// Recalculate whenever an amount field changes
// === LIVE AMOUNT CHANGES ===
itemsContainer.addEventListener('input', function(e) {
if (e.target.classList.contains('item-amount')) {
updateCalculations();
}
if (e.target.classList.contains('item-amount')) updateCalculations();
});
// === VAT TYPE RADIO CHANGES ===
vatRadios.forEach(function(radio) {
radio.addEventListener('change', updateCalculations);
});
// === VAT CALCULATION LOGIC ===
// Mirrors the backend Python calculation exactly.
// Three modes: Included (reverse 15%), Excluded (add 15%), None (no VAT).
// === VAT CALCULATION ===
function updateCalculations() {
// Sum all visible (non-deleted) item amounts
var sum = 0;
var amounts = document.querySelectorAll('.item-row:not(.deleted) .item-amount');
amounts.forEach(function(input) {
var val = parseFloat(input.value) || 0;
sum += val;
document.querySelectorAll('.item-row:not(.deleted) .item-amount').forEach(function(input) {
sum += parseFloat(input.value) || 0;
});
// Find which VAT radio is selected
var vatType = 'None';
vatRadios.forEach(function(r) {
if (r.checked) vatType = r.value;
});
var subtotal = 0;
var vat = 0;
var total = 0;
vatRadios.forEach(function(r) { if (r.checked) vatType = r.value; });
var subtotal = 0, vat = 0, total = 0;
if (vatType === 'Included') {
// Entered amounts include VAT — reverse it out
total = sum;
subtotal = total / 1.15;
vat = total - subtotal;
total = sum; subtotal = total / 1.15; vat = total - subtotal;
} else if (vatType === 'Excluded') {
// Entered amounts are pre-VAT — add 15% on top
subtotal = sum;
vat = subtotal * 0.15;
total = subtotal + vat;
subtotal = sum; vat = subtotal * 0.15; total = subtotal + vat;
} else {
// No VAT
subtotal = sum;
vat = 0;
total = sum;
subtotal = sum; total = sum;
}
// Update the display using textContent (safe, no HTML injection)
displaySubtotal.textContent = subtotal.toFixed(2);
displayVat.textContent = vat.toFixed(2);
displayTotal.textContent = total.toFixed(2);
}
// Run once on page load (in case form has pre-filled values)
updateCalculations();
})();
</script>
{% endblock %}

View File

@ -4,15 +4,13 @@
{% block title %}Dashboard | FoxFitt{% endblock %}
{% block content %}
<style>
{# Hide resource rows — needs !important to override Bootstrap's d-flex !important #}
.resource-hidden { display: none !important; }
</style>
<!-- Gradient Header -->
<div class="dashboard-header mb-5 rounded shadow-sm p-4 d-flex justify-content-between align-items-center">
<!-- === DASHBOARD HEADER — gradient banner with welcome + CTA === -->
<div class="dashboard-header mb-5 rounded-0 p-4 d-flex justify-content-between align-items-center d-print-none">
<div>
<h1 class="h3 mb-0 text-white" style="font-family: 'Poppins', sans-serif;">Dashboard</h1>
<p class="text-white-50 mb-0">Welcome back, {{ user.first_name|default:user.username }}!</p>
<h1 class="h3 mb-1 text-white" style="font-family: 'Poppins', sans-serif;">Dashboard</h1>
<p class="mb-0" style="color: rgba(255,255,255,0.6); font-size: 0.9rem;">
Welcome back, {{ user.first_name|default:user.username }}
</p>
</div>
<a href="{% url 'attendance_log' %}" class="btn btn-accent shadow-sm">
<i class="fas fa-plus fa-sm me-1"></i> Log Daily Work
@ -20,377 +18,385 @@
</div>
<div class="container py-2" style="margin-top: -3rem;">
{% if is_admin %}
<!-- Admin View -->
<div class="row g-4 mb-4 position-relative">
<!-- Outstanding Payments Card -->
<!-- Shows the total owed to workers, with a breakdown of wages vs adjustments -->
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #ef4444;">
Outstanding Payments</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ outstanding_payments|floatformat:2 }}</div>
{# === BREAKDOWN — only shown when there are pending adjustments === #}
{% if pending_adjustments_add or pending_adjustments_sub %}
<div class="mt-2 pt-2 border-top" style="font-size: 0.75rem; color: #64748b;">
<div class="d-flex justify-content-between">
<span>Unpaid wages</span>
<span>R {{ unpaid_wages|floatformat:2 }}</span>
</div>
{% if pending_adjustments_add %}
<div class="d-flex justify-content-between">
<span>+ Additions</span>
<span class="text-success">R {{ pending_adjustments_add|floatformat:2 }}</span>
</div>
{% endif %}
{% if pending_adjustments_sub %}
<div class="d-flex justify-content-between">
<span>- Deductions</span>
<span class="text-danger">-R {{ pending_adjustments_sub|floatformat:2 }}</span>
</div>
{% endif %}
</div>
{% endif %}
<div class="mt-1" style="font-size: 0.65rem; color: #94a3b8;">
<i class="fas fa-info-circle"></i> Loan repayments deducted at payment time
</div>
</div>
<div class="col-auto align-self-start">
<i class="fas fa-exclamation-circle fa-2x text-danger opacity-50"></i>
</div>
</div>
{% if is_admin %}
<!-- ===================================================================
ADMIN VIEW — stats, quick actions, activity, resources
=================================================================== -->
<!-- === STAT CARDS (4 columns) === -->
<div class="row g-3 mb-4">
<!-- Outstanding Payments -->
<div class="col-xl-3 col-md-6">
<div class="stat-card stat-card--danger h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Outstanding Payments</div>
<div class="stat-value">R {{ outstanding_payments|floatformat:2 }}</div>
</div>
<div class="stat-icon stat-icon--danger">
<i class="fas fa-exclamation-circle"></i>
</div>
</div>
</div>
<!-- Paid This Month Card -->
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #10b981;">
Paid This Month</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ paid_this_month|floatformat:2 }}</div>
</div>
<div class="col-auto">
<i class="fas fa-check-circle fa-2x text-success opacity-50"></i>
</div>
</div>
{# Breakdown — wages + adjustments (shown when adjustments exist) #}
{% if pending_adjustments_add or pending_adjustments_sub %}
<div class="mt-2 pt-2" style="border-top: 1px solid var(--border-subtle); font-size: 0.75rem; color: var(--text-secondary);">
<div class="d-flex justify-content-between">
<span>Unpaid wages</span>
<span>R {{ unpaid_wages|floatformat:2 }}</span>
</div>
{% if pending_adjustments_add %}
<div class="d-flex justify-content-between">
<span>+ Additions</span>
<span style="color: var(--color-success);">R {{ pending_adjustments_add|floatformat:2 }}</span>
</div>
{% endif %}
{% if pending_adjustments_sub %}
<div class="d-flex justify-content-between">
<span>- Deductions</span>
<span style="color: var(--color-danger);">-R {{ pending_adjustments_sub|floatformat:2 }}</span>
</div>
{% endif %}
</div>
{% endif %}
<div class="mt-1" style="font-size: 0.65rem; color: var(--text-tertiary);">
<i class="fas fa-info-circle"></i> Loan repayments deducted at payment time
</div>
</div>
</div>
<!-- Active Loans Card -->
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #f59e0b;">
Active Loans ({{ active_loans_count }})</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ active_loans_balance|floatformat:2 }}</div>
</div>
<div class="col-auto">
<i class="fas fa-hand-holding-usd fa-2x text-warning opacity-50"></i>
</div>
</div>
<!-- Paid This Month -->
<div class="col-xl-3 col-md-6">
<div class="stat-card stat-card--success h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Paid This Month</div>
<div class="stat-value">R {{ paid_this_month|floatformat:2 }}</div>
</div>
</div>
</div>
<!-- Outstanding by Project -->
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #3b82f6;">
Outstanding by Project</div>
<div class="mb-0 text-gray-800" style="font-size: 0.85rem;">
{% if outstanding_by_project %}
<ul class="list-unstyled mb-0">
{% for proj, amount in outstanding_by_project.items %}
<li><strong>{{ proj }}:</strong> R {{ amount|floatformat:2 }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
<div class="col-auto">
<i class="fas fa-chart-pie fa-2x text-primary opacity-50"></i>
</div>
</div>
<div class="stat-icon stat-icon--success">
<i class="fas fa-check-circle"></i>
</div>
</div>
</div>
</div>
<!-- Quick Actions and This Week -->
<div class="row mb-4">
<!-- This Week -->
<div class="col-lg-4 mb-4 mb-lg-0">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">This Week Summary</h6>
<!-- Active Loans -->
<div class="col-xl-3 col-md-6">
<div class="stat-card stat-card--warning h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Active Loans ({{ active_loans_count }})</div>
<div class="stat-value">R {{ active_loans_balance|floatformat:2 }}</div>
</div>
<div class="card-body text-center d-flex flex-column justify-content-center">
<div class="h1 mb-0 font-weight-bold text-primary">{{ this_week_logs }}</div>
<div class="text-muted">Work Logs Created This Week</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="col-lg-8">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Quick Actions</h6>
</div>
<div class="card-body d-flex align-items-center justify-content-around flex-wrap">
<a href="{% url 'attendance_log' %}" class="btn btn-lg btn-outline-primary mb-2">
<i class="fas fa-clipboard-list mb-2 d-block fa-2x"></i> Log Work
</a>
<a href="{% url 'payroll_dashboard' %}" class="btn btn-lg btn-outline-success mb-2">
<i class="fas fa-money-check-alt mb-2 d-block fa-2x"></i> Run Payroll
</a>
<a href="{% url 'work_history' %}" class="btn btn-lg btn-outline-secondary mb-2">
<i class="fas fa-history mb-2 d-block fa-2x"></i> View History
</a>
<div class="stat-icon stat-icon--warning">
<i class="fas fa-hand-holding-usd"></i>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Recent Activity -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Recent Activity</h6>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for log in recent_activity %}
<div class="list-group-item px-4 py-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">{{ log.project.name }}</h6>
<small class="text-muted">{{ log.date }} &middot; {{ log.workers.count }} workers</small>
</div>
<span class="badge bg-light text-dark border">{{ log.supervisor.username }}</span>
</div>
</div>
{% empty %}
<div class="p-4 text-center text-muted">
No recent activity.
<!-- Outstanding by Project -->
<div class="col-xl-3 col-md-6">
<div class="stat-card stat-card--info h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div style="flex: 1;">
<div class="stat-label">Outstanding by Project</div>
{% if outstanding_by_project %}
<div style="font-size: 0.85rem; margin-top: 0.35rem;">
{% for proj, amount in outstanding_by_project.items %}
<div class="d-flex justify-content-between" style="color: var(--text-primary);">
<span class="text-truncate me-2">{{ proj }}</span>
<span class="fw-semibold" style="white-space: nowrap;">R {{ amount|floatformat:2 }}</span>
</div>
{% endfor %}
</div>
{% else %}
<span style="font-size: 0.85rem; color: var(--text-tertiary);">None</span>
{% endif %}
</div>
</div>
</div>
<!-- Manage Resources -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Manage Resources</h6>
<a href="{% url 'export_workers_csv' %}" class="btn btn-outline-success btn-sm">
<i class="fas fa-file-csv me-1"></i> Export Workers
</a>
</div>
<div class="card-body p-0">
<p class="text-muted small mb-0 px-3 pt-3">Toggle active status. Inactive items are hidden from forms.</p>
<ul class="nav nav-tabs px-3 pt-2" id="resourceTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="workers-tab" data-bs-toggle="tab" data-bs-target="#workers" type="button" role="tab" aria-selected="true">Workers</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="projects-tab" data-bs-toggle="tab" data-bs-target="#projects" type="button" role="tab" aria-selected="false">Projects</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="teams-tab" data-bs-toggle="tab" data-bs-target="#teams" type="button" role="tab" aria-selected="false">Teams</button>
</li>
</ul>
{# Filter bar — Active / Inactive / All (defaults to Active) #}
<div class="btn-group btn-group-sm w-100 px-3 mt-2" id="resourceFilter" role="group">
<button type="button" class="btn btn-outline-secondary active" data-filter="active">Active</button>
<button type="button" class="btn btn-outline-secondary" data-filter="inactive">Inactive</button>
<button type="button" class="btn btn-outline-secondary" data-filter="all">All</button>
</div>
<div class="tab-content px-0 mt-2" id="resourceTabsContent" style="max-height: 350px; overflow-y: auto;">
{# === WORKERS TAB === #}
<div class="tab-pane fade show active" id="workers" role="tabpanel">
{% for item in workers %}
<div class="resource-row d-flex justify-content-between align-items-center py-2 px-3 border-bottom {% if not item.active %}resource-hidden{% endif %}" data-active="{% if item.active %}true{% else %}false{% endif %}">
<strong class="small">{{ item.name }}</strong>
<div class="form-check form-switch">
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="worker" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
</div>
</div>
{% empty %}
<p class="text-muted small px-3 py-2">No workers found.</p>
{% endfor %}
<p class="text-muted small text-center mt-2 resource-empty" style="display:none;">No matching items.</p>
</div>
{# === PROJECTS TAB === #}
<div class="tab-pane fade" id="projects" role="tabpanel">
{% for item in projects %}
<div class="resource-row d-flex justify-content-between align-items-center py-2 px-3 border-bottom {% if not item.active %}resource-hidden{% endif %}" data-active="{% if item.active %}true{% else %}false{% endif %}">
<strong class="small">{{ item.name }}</strong>
<div class="form-check form-switch">
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="project" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
</div>
</div>
{% empty %}
<p class="text-muted small px-3 py-2">No projects found.</p>
{% endfor %}
<p class="text-muted small text-center mt-2 resource-empty" style="display:none;">No matching items.</p>
</div>
{# === TEAMS TAB === #}
<div class="tab-pane fade" id="teams" role="tabpanel">
{% for item in teams %}
<div class="resource-row d-flex justify-content-between align-items-center py-2 px-3 border-bottom {% if not item.active %}resource-hidden{% endif %}" data-active="{% if item.active %}true{% else %}false{% endif %}">
<strong class="small">{{ item.name }}</strong>
<div class="form-check form-switch">
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="team" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
</div>
</div>
{% empty %}
<p class="text-muted small px-3 py-2">No teams found.</p>
{% endfor %}
<p class="text-muted small text-center mt-2 resource-empty" style="display:none;">No matching items.</p>
</div>
</div>
<div class="stat-icon stat-icon--info ms-2">
<i class="fas fa-chart-pie"></i>
</div>
</div>
</div>
</div>
{% else %}
<!-- Supervisor View -->
<!-- Stat Cards — how many projects, teams, and workers this supervisor manages -->
<div class="row g-4 mb-4 position-relative">
<div class="col-md-4">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #8b5cf6;">
My Projects</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ my_projects_count }}</div>
</div>
<div class="col-auto">
<i class="fas fa-project-diagram fa-2x opacity-50" style="color: #8b5cf6;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #3b82f6;">
My Teams</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ my_teams_count }}</div>
</div>
<div class="col-auto">
<i class="fas fa-users fa-2x opacity-50" style="color: #3b82f6;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #10b981;">
My Workers</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ my_workers_count }}</div>
</div>
<div class="col-auto">
<i class="fas fa-hard-hat fa-2x opacity-50" style="color: #10b981;"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- This Week + Recent Activity -->
<div class="row mb-4">
<div class="col-lg-4 mb-4 mb-lg-0">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 fw-bold" style="color: #0f172a;">This Week Summary</h6>
</div>
<div class="card-body text-center d-flex flex-column justify-content-center">
<div class="h1 mb-0 fw-bold text-primary">{{ this_week_logs }}</div>
<div class="text-muted">Work Logs Created This Week</div>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 fw-bold" style="color: #0f172a;">Recent Activity</h6>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for log in recent_activity %}
<div class="list-group-item px-4 py-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">{{ log.project.name }}</h6>
<small class="text-muted">{{ log.date }} &middot; {{ log.workers.count }} workers</small>
</div>
</div>
</div>
{% empty %}
<div class="p-4 text-center text-muted">
<i class="fas fa-inbox fa-2x mb-2 d-block opacity-50"></i>
No recent activity.
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<!-- === ROW 2: This Week + Quick Actions === -->
<div class="row g-3 mb-4">
<!-- This Week Summary -->
<div class="col-lg-4 mb-3 mb-lg-0">
<div class="card h-100">
<div class="card-header py-3">
<h6 class="m-0 fw-bold"><i class="fas fa-calendar-week me-2" style="color: var(--accent);"></i>This Week</h6>
</div>
<div class="card-body text-center d-flex flex-column justify-content-center">
<div class="stat-value" style="font-size: 2.5rem; color: var(--accent);">{{ this_week_logs }}</div>
<div style="color: var(--text-secondary); font-size: 0.85rem;">Work Logs Created</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header py-3">
<h6 class="m-0 fw-bold"><i class="fas fa-bolt me-2" style="color: var(--accent);"></i>Quick Actions</h6>
</div>
<div class="card-body d-flex align-items-center justify-content-center gap-3 flex-wrap">
<a href="{% url 'attendance_log' %}" class="quick-action">
<i class="fas fa-clipboard-list"></i>
<span>Log Work</span>
</a>
<a href="{% url 'payroll_dashboard' %}" class="quick-action">
<i class="fas fa-money-check-alt"></i>
<span>Run Payroll</span>
</a>
<a href="{% url 'work_history' %}" class="quick-action">
<i class="fas fa-history"></i>
<span>View History</span>
</a>
<a href="{% url 'create_receipt' %}" class="quick-action">
<i class="fas fa-receipt"></i>
<span>New Receipt</span>
</a>
</div>
</div>
</div>
</div>
<!-- === ROW 3: Recent Activity + Manage Resources === -->
<div class="row g-3">
<!-- Recent Activity -->
<div class="col-lg-6 mb-4">
<div class="card h-100">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 fw-bold"><i class="fas fa-stream me-2" style="color: var(--accent);"></i>Recent Activity</h6>
<a href="{% url 'work_history' %}" class="btn btn-sm btn-outline-secondary">View All</a>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for log in recent_activity %}
<div class="list-group-item px-4 py-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1 fw-semibold" style="font-size: 0.9rem;">{{ log.project.name }}</h6>
<small style="color: var(--text-secondary);">
<i class="fas fa-calendar-day me-1"></i>{{ log.date }}
<span class="mx-1">&middot;</span>
<i class="fas fa-users me-1"></i>{{ log.workers.count }} workers
</small>
</div>
<span class="badge" style="background: var(--bg-inset); color: var(--text-secondary); font-size: 0.7rem;">
{{ log.supervisor.username }}
</span>
</div>
</div>
{% empty %}
<div class="p-4 text-center" style="color: var(--text-tertiary);">
<i class="fas fa-inbox fa-2x mb-2 d-block"></i>
No recent activity
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Manage Resources -->
<div class="col-lg-6 mb-4">
<div class="card h-100">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 fw-bold"><i class="fas fa-sliders-h me-2" style="color: var(--accent);"></i>Manage Resources</h6>
<a href="{% url 'export_workers_csv' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-file-csv me-1"></i> Export
</a>
</div>
<div class="card-body p-0 pt-2">
<p class="px-3 mb-2" style="font-size: 0.75rem; color: var(--text-tertiary);">
Toggle active status. Inactive items are hidden from forms.
</p>
<!-- Resource tabs -->
<ul class="nav nav-tabs px-3" id="resourceTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="workers-tab" data-bs-toggle="tab" data-bs-target="#workers" type="button" role="tab" aria-selected="true">Workers</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="projects-tab" data-bs-toggle="tab" data-bs-target="#projects" type="button" role="tab" aria-selected="false">Projects</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="teams-tab" data-bs-toggle="tab" data-bs-target="#teams" type="button" role="tab" aria-selected="false">Teams</button>
</li>
</ul>
<!-- Active / Inactive / All filter -->
<div class="btn-group btn-group-sm w-100 px-3 mt-2" id="resourceFilter" role="group">
<button type="button" class="btn btn-outline-secondary active" data-filter="active">Active</button>
<button type="button" class="btn btn-outline-secondary" data-filter="inactive">Inactive</button>
<button type="button" class="btn btn-outline-secondary" data-filter="all">All</button>
</div>
<!-- Tab content with scrollable list -->
<div class="tab-content mt-2" id="resourceTabsContent" style="max-height: 320px; overflow-y: auto;">
{# === WORKERS === #}
<div class="tab-pane fade show active" id="workers" role="tabpanel">
{% for item in workers %}
<div class="resource-row d-flex justify-content-between align-items-center py-2 px-3 border-bottom {% if not item.active %}resource-hidden{% endif %}" data-active="{% if item.active %}true{% else %}false{% endif %}">
<span class="fw-medium" style="font-size: 0.85rem;">{{ item.name }}</span>
<div class="form-check form-switch mb-0">
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="worker" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
</div>
</div>
{% empty %}
<p class="text-center py-3" style="color: var(--text-tertiary); font-size: 0.85rem;">No workers found.</p>
{% endfor %}
<p class="text-center py-2 resource-empty" style="display:none; color: var(--text-tertiary); font-size: 0.85rem;">No matching items.</p>
</div>
{# === PROJECTS === #}
<div class="tab-pane fade" id="projects" role="tabpanel">
{% for item in projects %}
<div class="resource-row d-flex justify-content-between align-items-center py-2 px-3 border-bottom {% if not item.active %}resource-hidden{% endif %}" data-active="{% if item.active %}true{% else %}false{% endif %}">
<span class="fw-medium" style="font-size: 0.85rem;">{{ item.name }}</span>
<div class="form-check form-switch mb-0">
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="project" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
</div>
</div>
{% empty %}
<p class="text-center py-3" style="color: var(--text-tertiary); font-size: 0.85rem;">No projects found.</p>
{% endfor %}
<p class="text-center py-2 resource-empty" style="display:none; color: var(--text-tertiary); font-size: 0.85rem;">No matching items.</p>
</div>
{# === TEAMS === #}
<div class="tab-pane fade" id="teams" role="tabpanel">
{% for item in teams %}
<div class="resource-row d-flex justify-content-between align-items-center py-2 px-3 border-bottom {% if not item.active %}resource-hidden{% endif %}" data-active="{% if item.active %}true{% else %}false{% endif %}">
<span class="fw-medium" style="font-size: 0.85rem;">{{ item.name }}</span>
<div class="form-check form-switch mb-0">
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="team" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
</div>
</div>
{% empty %}
<p class="text-center py-3" style="color: var(--text-tertiary); font-size: 0.85rem;">No teams found.</p>
{% endfor %}
<p class="text-center py-2 resource-empty" style="display:none; color: var(--text-tertiary); font-size: 0.85rem;">No matching items.</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}
<!-- ===================================================================
SUPERVISOR VIEW — projects, teams, workers + activity
=================================================================== -->
<!-- === STAT CARDS (3 columns) === -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="stat-card stat-card--purple h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">My Projects</div>
<div class="stat-value">{{ my_projects_count }}</div>
</div>
<div class="stat-icon stat-icon--purple">
<i class="fas fa-project-diagram"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-card stat-card--info h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">My Teams</div>
<div class="stat-value">{{ my_teams_count }}</div>
</div>
<div class="stat-icon stat-icon--info">
<i class="fas fa-users"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-card stat-card--success h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">My Workers</div>
<div class="stat-value">{{ my_workers_count }}</div>
</div>
<div class="stat-icon stat-icon--success">
<i class="fas fa-hard-hat"></i>
</div>
</div>
</div>
</div>
</div>
<!-- === This Week + Recent Activity === -->
<div class="row g-3 mb-4">
<div class="col-lg-4 mb-3 mb-lg-0">
<div class="card h-100">
<div class="card-header py-3">
<h6 class="m-0 fw-bold"><i class="fas fa-calendar-week me-2" style="color: var(--accent);"></i>This Week</h6>
</div>
<div class="card-body text-center d-flex flex-column justify-content-center">
<div class="stat-value" style="font-size: 2.5rem; color: var(--accent);">{{ this_week_logs }}</div>
<div style="color: var(--text-secondary); font-size: 0.85rem;">Work Logs Created</div>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header py-3">
<h6 class="m-0 fw-bold"><i class="fas fa-stream me-2" style="color: var(--accent);"></i>Recent Activity</h6>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for log in recent_activity %}
<div class="list-group-item px-4 py-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1 fw-semibold" style="font-size: 0.9rem;">{{ log.project.name }}</h6>
<small style="color: var(--text-secondary);">
<i class="fas fa-calendar-day me-1"></i>{{ log.date }}
<span class="mx-1">&middot;</span>
<i class="fas fa-users me-1"></i>{{ log.workers.count }} workers
</small>
</div>
</div>
</div>
{% empty %}
<div class="p-4 text-center" style="color: var(--text-tertiary);">
<i class="fas fa-inbox fa-2x mb-2 d-block"></i>
No recent activity
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<!-- === JAVASCRIPT: Resource filter + AJAX toggle === -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// === RESOURCE FILTER (Active / Inactive / All) ===
// Hides/shows resource rows based on their data-active attribute.
// Starts on "Active" so only current items are visible by default.
var currentFilter = 'active';
var filterBtns = document.querySelectorAll('#resourceFilter button');
function applyFilter() {
// Use the resource-hidden CLASS (not inline display:none) because
// Bootstrap's d-flex has !important which overrides inline styles.
// Our .resource-hidden also has !important, so it wins.
document.querySelectorAll('.resource-row').forEach(function(row) {
var isActive = row.dataset.active === 'true';
var show = false;
@ -403,13 +409,13 @@ document.addEventListener('DOMContentLoaded', function() {
row.classList.add('resource-hidden');
}
});
// Show "No matching items" if a tab has rows but none are visible
// Show "No matching items" if tab has rows but none visible
document.querySelectorAll('.tab-pane').forEach(function(pane) {
var rows = pane.querySelectorAll('.resource-row');
var visibleRows = Array.from(rows).filter(function(r) { return !r.classList.contains('resource-hidden'); });
var visible = Array.from(rows).filter(function(r) { return !r.classList.contains('resource-hidden'); });
var emptyMsg = pane.querySelector('.resource-empty');
if (emptyMsg) {
emptyMsg.style.display = (rows.length > 0 && visibleRows.length === 0) ? '' : 'none';
emptyMsg.style.display = (rows.length > 0 && visible.length === 0) ? '' : 'none';
}
});
}
@ -423,16 +429,10 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
// Apply filter on page load (shows only active by default)
applyFilter();
// === TOGGLE HANDLER ===
// When a toggle switch is flipped, POST to the server to update active status.
// On success, update the row's data-active attribute and re-apply the filter
// so the row moves to the correct section immediately.
var toggleSwitches = document.querySelectorAll('.toggle-active');
toggleSwitches.forEach(function(switchEl) {
// === TOGGLE HANDLER — AJAX POST to activate/deactivate resources ===
document.querySelectorAll('.toggle-active').forEach(function(switchEl) {
switchEl.addEventListener('change', function() {
var type = this.getAttribute('data-type');
var id = this.getAttribute('data-id');
@ -452,7 +452,6 @@ document.addEventListener('DOMContentLoaded', function() {
})
.then(function(data) {
if (data.status === 'success') {
// Update the row's data-active and re-apply filter
row.dataset.active = isChecked ? 'true' : 'false';
applyFilter();
} else {
@ -460,7 +459,7 @@ document.addEventListener('DOMContentLoaded', function() {
alert('Error updating status.');
}
})
.catch(function(error) {
.catch(function() {
switchEl.checked = !isChecked;
alert('Error updating status.');
});

View File

@ -1,17 +1,30 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Payroll Dashboard | Fox Fitt{% endblock %}
{% block title %}Payroll Dashboard | FoxFitt{% endblock %}
{% block content %}
<!-- Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
// === CHART.JS THEME DEFAULTS ===
// Read CSS variable colours so chart axes/grid lines adapt to dark mode
(function() {
var style = getComputedStyle(document.documentElement);
var textColor = style.getPropertyValue('--text-secondary').trim() || '#64748b';
var borderColor = style.getPropertyValue('--border-default').trim() || '#e2e8f0';
if (typeof Chart !== 'undefined') {
Chart.defaults.color = textColor;
Chart.defaults.borderColor = borderColor;
}
})();
</script>
<div class="container py-4">
{# === PAGE HEADER === #}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0" style="font-family: 'Poppins', sans-serif;">Payroll Dashboard</h1>
<h1 class="page-title"><i class="fas fa-wallet me-2" style="color: var(--accent);"></i>Payroll Dashboard</h1>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-info shadow-sm" id="workerLookupBtn">
<i class="fas fa-id-card fa-sm me-1"></i> Worker Lookup
@ -38,39 +51,37 @@
<div class="row g-3 h-100">
{# Outstanding Total — with breakdown of wages vs adjustments #}
<div class="col-sm-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between">
<div>
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #ef4444;">
Outstanding Payments</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ outstanding_total|floatformat:2 }}</div>
{# === BREAKDOWN — only shown when there are pending adjustments === #}
{% if pending_adj_add_total or pending_adj_sub_total %}
<div class="mt-2 pt-2 border-top" style="font-size: 0.75rem; color: #64748b;">
<div class="d-flex justify-content-between">
<span>Unpaid wages</span>
<span>R {{ unpaid_wages_total|floatformat:2 }}</span>
</div>
{% if pending_adj_add_total %}
<div class="d-flex justify-content-between">
<span>+ Additions</span>
<span class="text-success">R {{ pending_adj_add_total|floatformat:2 }}</span>
</div>
{% endif %}
{% if pending_adj_sub_total %}
<div class="d-flex justify-content-between">
<span>- Deductions</span>
<span class="text-danger">-R {{ pending_adj_sub_total|floatformat:2 }}</span>
</div>
{% endif %}
<div class="stat-card stat-card--danger h-100 p-3">
<div class="d-flex align-items-start justify-content-between">
<div>
<div class="stat-label">Outstanding Payments</div>
<div class="stat-value">R {{ outstanding_total|floatformat:2 }}</div>
{% if pending_adj_add_total or pending_adj_sub_total %}
<div class="mt-2 pt-2" style="border-top: 1px solid var(--border-subtle); font-size: 0.75rem; color: var(--text-secondary);">
<div class="d-flex justify-content-between">
<span>Unpaid wages</span>
<span>R {{ unpaid_wages_total|floatformat:2 }}</span>
</div>
{% if pending_adj_add_total %}
<div class="d-flex justify-content-between">
<span>+ Additions</span>
<span style="color: var(--color-success);">R {{ pending_adj_add_total|floatformat:2 }}</span>
</div>
{% endif %}
<div class="mt-1" style="font-size: 0.65rem; color: #94a3b8;">
<i class="fas fa-info-circle"></i> Loan repayments deducted at payment time
{% if pending_adj_sub_total %}
<div class="d-flex justify-content-between">
<span>- Deductions</span>
<span style="color: var(--color-danger);">-R {{ pending_adj_sub_total|floatformat:2 }}</span>
</div>
{% endif %}
</div>
<i class="fas fa-exclamation-circle fa-2x text-danger opacity-25"></i>
{% endif %}
<div class="mt-1" style="font-size: 0.65rem; color: var(--text-tertiary);">
<i class="fas fa-info-circle"></i> Loan repayments deducted at payment time
</div>
</div>
<div class="stat-icon stat-icon--danger">
<i class="fas fa-exclamation-circle"></i>
</div>
</div>
</div>
@ -78,15 +89,14 @@
{# Recent Payments #}
<div class="col-sm-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between">
<div>
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #10b981;">
Paid (Last 60 Days)</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ recent_payments_total|floatformat:2 }}</div>
</div>
<i class="fas fa-check-circle fa-2x text-success opacity-25"></i>
<div class="stat-card stat-card--success h-100 p-3">
<div class="d-flex align-items-start justify-content-between">
<div>
<div class="stat-label">Paid (Last 60 Days)</div>
<div class="stat-value">R {{ recent_payments_total|floatformat:2 }}</div>
</div>
<div class="stat-icon stat-icon--success">
<i class="fas fa-check-circle"></i>
</div>
</div>
</div>
@ -94,15 +104,14 @@
{# Active Loans — spans full width below the first two #}
<div class="col-12">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between">
<div>
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #f59e0b;">
Active Loans & Advances ({{ active_loans_count }})</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ active_loans_balance|floatformat:2 }}</div>
</div>
<i class="fas fa-hand-holding-usd fa-2x text-warning opacity-25"></i>
<div class="stat-card stat-card--warning h-100 p-3">
<div class="d-flex align-items-start justify-content-between">
<div>
<div class="stat-label">Active Loans & Advances ({{ active_loans_count }})</div>
<div class="stat-value">R {{ active_loans_balance|floatformat:2 }}</div>
</div>
<div class="stat-icon stat-icon--warning">
<i class="fas fa-hand-holding-usd"></i>
</div>
</div>
</div>
@ -112,25 +121,26 @@
{# --- Right column: project breakdown (grows to fit all projects) --- #}
<div class="col-xl-5 d-flex">
<div class="card stat-card py-2 w-100">
<div class="card-body d-flex flex-column">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="text-xs font-weight-bold text-uppercase" style="color: #3b82f6;">
Outstanding by Project</div>
<i class="fas fa-chart-pie fa-2x text-primary opacity-25"></i>
<div class="stat-card stat-card--info p-3 w-100">
<div class="d-flex flex-column h-100">
<div class="d-flex align-items-start justify-content-between mb-3">
<div class="stat-label">Outstanding by Project</div>
<div class="stat-icon stat-icon--info">
<i class="fas fa-chart-pie"></i>
</div>
</div>
{% if outstanding_project_costs %}
<div class="flex-grow-1">
{% for pc in outstanding_project_costs %}
<div class="d-flex justify-content-between align-items-center {% if not forloop.last %}mb-2 pb-2 border-bottom{% endif %}">
<span class="fw-semibold text-gray-800">{{ pc.name }}</span>
<span class="fw-bold" style="color: #3b82f6;">R {{ pc.cost|floatformat:2 }}</span>
<div class="d-flex justify-content-between align-items-center {% if not forloop.last %}mb-2 pb-2{% endif %}" {% if not forloop.last %}style="border-bottom: 1px solid var(--border-subtle);"{% endif %}>
<span class="fw-semibold">{{ pc.name }}</span>
<span class="fw-bold" style="color: var(--color-info);">R {{ pc.cost|floatformat:2 }}</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="flex-grow-1 d-flex align-items-center justify-content-center">
<span class="text-muted"><i class="fas fa-check-circle me-1"></i> No outstanding amounts</span>
<span style="color: var(--text-tertiary);"><i class="fas fa-check-circle me-1"></i> No outstanding amounts</span>
</div>
{% endif %}
</div>
@ -142,13 +152,11 @@
{# === CHARTS === #}
<div class="row mb-4">
<div class="col-lg-6 mb-4 mb-lg-0">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<div class="card h-100">
<div class="card-header py-3">
{# === CHART TOGGLE: Overall vs By Worker === #}
{# Two small buttons to switch between the total line chart #}
{# and a per-worker stacked bar chart breakdown. #}
<div class="d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold" style="color: var(--primary-dark);">Monthly Payroll</h6>
<h6 class="m-0 fw-bold">Monthly Payroll</h6>
<div class="btn-group btn-group-sm" role="group" aria-label="Chart view toggle">
<button type="button" class="btn btn-sm btn-accent" id="btnOverall">
<i class="fas fa-chart-line fa-sm me-1"></i>Overall
@ -185,9 +193,9 @@
</div>
</div>
<div class="col-lg-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Cost by Project (Monthly)</h6>
<div class="card h-100">
<div class="card-header py-3">
<h6 class="m-0 fw-bold">Cost by Project (Monthly)</h6>
</div>
<div class="card-body">
<canvas id="projectChart" height="200"></canvas>
@ -246,7 +254,7 @@
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="pendingTable">
@ -268,7 +276,7 @@
data-overdue="{{ wd.is_overdue|yesno:'true,false' }}"
data-has-loan="{{ wd.has_loan|yesno:'true,false' }}">
<td class="ps-4 align-middle">
<a href="#" class="worker-lookup-link text-decoration-none text-dark fw-bold"
<a href="#" class="worker-lookup-link fw-bold"
data-worker-id="{{ wd.worker.id }}">{{ wd.worker.name }}</a>
{% if wd.is_overdue %}
<span class="badge bg-danger ms-1" title="Has unpaid work from a completed pay period (since {{ wd.earliest_unpaid|date:'d M Y' }})">Overdue</span>
@ -348,7 +356,7 @@
{# === PAYMENT HISTORY TAB === #}
{# =============================================== #}
{% if active_tab == 'paid' %}
<div class="card shadow-sm border-0">
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
@ -366,7 +374,7 @@
{% for record in paid_records %}
<tr>
<td class="ps-4 align-middle">{{ record.date }}</td>
<td class="align-middle"><a href="#" class="worker-lookup-link text-decoration-none text-dark fw-bold"
<td class="align-middle"><a href="#" class="worker-lookup-link fw-bold"
data-worker-id="{{ record.worker.id }}">{{ record.worker.name }}</a></td>
<td class="align-middle">R {{ record.amount_paid|floatformat:2 }}</td>
<td class="align-middle">
@ -416,7 +424,7 @@
History
</a>
</div>
<div class="card shadow-sm border-0">
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
@ -434,7 +442,7 @@
<tbody>
{% for loan in loans %}
<tr>
<td class="ps-4 align-middle"><a href="#" class="worker-lookup-link text-decoration-none text-dark fw-bold"
<td class="ps-4 align-middle"><a href="#" class="worker-lookup-link fw-bold"
data-worker-id="{{ loan.worker.id }}">{{ loan.worker.name }}</a></td>
<td class="align-middle">
{% if loan.loan_type == 'advance' %}
@ -534,7 +542,7 @@
<a href="#" id="adjSelectAll" class="small text-primary text-decoration-none text-nowrap">Select All</a>
<a href="#" id="adjDeselectAll" class="small text-muted text-decoration-none text-nowrap">Clear</a>
</div>
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 4px; padding: 8px;">
<div style="max-height: 200px; overflow-y: auto; border: 1px solid var(--border-default); border-radius: var(--radius-sm); padding: 8px; background: var(--bg-inset);">
{% for w in all_workers %}
<div class="form-check">
<input class="form-check-input add-adj-worker" type="checkbox"
@ -1668,9 +1676,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.pay_period && data.pay_period.has_schedule) {
var periodInfo = document.createElement('div');
periodInfo.className = 'alert alert-info py-2 px-3 small mb-2';
periodInfo.style.backgroundColor = '#e0f2fe';
periodInfo.style.borderColor = '#7dd3fc';
periodInfo.style.color = '#0c4a6e';
periodInfo.style.backgroundColor = 'var(--color-info-bg)';
periodInfo.style.borderColor = 'var(--color-info)';
periodInfo.style.color = 'var(--color-info)';
var infoIcon = document.createElement('i');
infoIcon.className = 'fas fa-calendar-alt me-2';
@ -2000,7 +2008,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Card container for each loan/advance
var card = document.createElement('div');
card.className = 'border rounded p-2 mb-2';
card.style.backgroundColor = '#f8f9fa';
card.style.backgroundColor = 'var(--bg-inset)';
// Row 1: Type badge + Balance
var topRow = document.createElement('div');
@ -2790,23 +2798,21 @@ document.addEventListener('DOMContentLoaded', function() {
var statsRow = el('div', 'row g-2 mb-4');
var stats = [
{ label: 'Amount Payable', value: data.amount_payable, color: '#0f172a' },
{ label: 'Outstanding Loans', value: data.outstanding_loans, color: '#f59e0b' },
{ label: 'Paid This Month', value: data.paid_this_month, color: '#10b981' },
{ label: 'Loans This Year', value: data.loans_this_year, color: '#ef4444' },
{ label: 'Amount Payable', value: data.amount_payable, color: 'var(--text-primary)' },
{ label: 'Outstanding Loans', value: data.outstanding_loans, color: 'var(--color-warning)' },
{ label: 'Paid This Month', value: data.paid_this_month, color: 'var(--color-success)' },
{ label: 'Loans This Year', value: data.loans_this_year, color: 'var(--color-danger)' },
];
stats.forEach(function(stat) {
var col = el('div', 'col-6 col-md-3');
var card = el('div', 'card border-0 shadow-sm h-100');
var body = el('div', 'card-body text-center py-2 px-2');
var label = el('div', 'text-uppercase small fw-bold mb-1');
var card = el('div', 'stat-card h-100');
card.style.padding = '0.75rem';
var label = el('div', 'stat-label');
label.style.color = stat.color;
label.style.fontSize = '0.65rem';
label.textContent = stat.label;
body.appendChild(label);
body.appendChild(el('div', 'fw-bold', formatRand(stat.value)));
card.appendChild(body);
card.appendChild(label);
card.appendChild(el('div', 'fw-bold', formatRand(stat.value)));
col.appendChild(card);
statsRow.appendChild(col);
});
@ -2885,7 +2891,7 @@ document.addEventListener('DOMContentLoaded', function() {
// --- PAID THIS YEAR ---
var yearSection = el('div', 'mb-4 p-3 rounded');
yearSection.style.backgroundColor = '#f1f5f9';
yearSection.style.backgroundColor = 'var(--bg-inset)';
var yearLabel = el('span', 'text-muted small text-uppercase', 'Paid This Year: ');
var yearValue = el('span', 'fw-bold', formatRand(data.paid_this_year));
yearSection.appendChild(yearLabel);
@ -2919,7 +2925,7 @@ document.addEventListener('DOMContentLoaded', function() {
var notesLabel = el('div', 'small text-muted mt-2', 'Notes:');
infoSection.appendChild(notesLabel);
var notesText = el('div', 'small p-2 rounded');
notesText.style.backgroundColor = '#f8f9fa';
notesText.style.backgroundColor = 'var(--bg-inset)';
notesText.textContent = data.notes;
infoSection.appendChild(notesText);
}

View File

@ -1,16 +1,15 @@
{% extends 'base.html' %}
{% block title %}Payslip #{{ record.id }} | Fox Fitt{% endblock %}
{% block title %}Payslip #{{ record.id }} | FoxFitt{% endblock %}
{% block content %}
<!-- === PAYSLIP DETAIL PAGE ===
Shows a completed payment with work logs, adjustments, and totals.
Reached from the Payment History tab on the payroll dashboard.
Has a Print button that uses the browser's native print dialog. -->
Print-friendly layout. -->
<div class="container py-5">
<div class="container py-4">
<!-- Action buttons (hidden when printing) -->
<div class="d-print-none mb-4 d-grid gap-2 d-md-flex">
<div class="d-print-none mb-4 d-flex gap-2 flex-wrap">
<a href="{% url 'payroll_dashboard' %}?status=paid" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Back to Payment History
</a>
@ -20,35 +19,41 @@
</div>
<!-- Payslip card -->
<div class="card border-0 shadow-sm" id="payslip-card">
<div class="card-body p-5">
<div class="card" id="payslip-card">
<div class="card-body p-4 p-md-5">
<!-- === HEADER — worker name is the dominant element === -->
<div class="row mb-5 border-bottom pb-4 align-items-center">
<!-- === HEADER === -->
<div class="row mb-5 pb-4 align-items-center" style="border-bottom: 2px solid var(--border-default);">
<div class="col-md-6">
<h6 class="text-uppercase text-muted fw-bold small mb-1">Payment To Beneficiary:</h6>
<h2 class="fw-bold text-dark mb-0 text-uppercase">{{ record.worker.name }}</h2>
<p class="text-muted small mb-0">{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip No. #{{ record.id|stringformat:"06d" }}</p>
<div class="stat-label mb-1">Payment To Beneficiary:</div>
<h2 class="fw-bold mb-0 text-uppercase">{{ record.worker.name }}</h2>
<p class="mb-0" style="color: var(--text-tertiary); font-size: 0.85rem;">
{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip No. #{{ record.id|stringformat:"06d" }}
</p>
</div>
<div class="col-md-6 text-md-end mt-3 mt-md-0">
<h3 class="fw-bold text-uppercase text-secondary mb-1">{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip</h3>
<h3 class="fw-bold text-uppercase" style="color: var(--text-secondary);">
{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip
</h3>
<div class="fw-bold">{{ record.date|date:"F j, Y" }}</div>
<div class="text-muted small">Payer: Fox Fitt</div>
<div style="color: var(--text-tertiary); font-size: 0.85rem;">Payer: Fox Fitt</div>
</div>
</div>
<!-- === WORKER DETAILS + NET PAY === -->
<div class="row mb-5">
<div class="col-md-6">
<h6 class="text-uppercase text-muted fw-bold small mb-3">Beneficiary Details:</h6>
<div class="stat-label mb-2">Beneficiary Details:</div>
<h4 class="fw-bold">{{ record.worker.name }}</h4>
<p class="mb-0">ID Number: <strong>{{ record.worker.id_number }}</strong></p>
<p class="mb-0">Phone: {{ record.worker.phone_number|default:"—" }}</p>
<p class="mb-0" style="color: var(--text-secondary);">Phone: {{ record.worker.phone_number|default:"—" }}</p>
</div>
<div class="col-md-6 text-md-end mt-4 mt-md-0">
<h6 class="text-uppercase text-muted fw-bold small mb-3">Net Payable Amount:</h6>
<div class="display-6 fw-bold text-dark">R {{ record.amount_paid|floatformat:2 }}</div>
<p class="text-success small fw-bold mt-2">
<div class="stat-label mb-2">Net Payable Amount:</div>
<div style="font-size: 2.5rem; font-weight: 700; font-family: 'Poppins', sans-serif;">
R {{ record.amount_paid|floatformat:2 }}
</div>
<p class="fw-bold mt-2" style="color: var(--color-success); font-size: 0.85rem;">
<i class="fas fa-check-circle me-1"></i> PAID
</p>
</div>
@ -56,10 +61,10 @@
{% if is_advance %}
<!-- === ADVANCE PAYMENT DETAIL === -->
<h6 class="text-uppercase text-muted fw-bold small mb-3">Advance Details</h6>
<div class="stat-label mb-3">Advance Details</div>
<div class="table-responsive mb-4">
<table class="table table-hover mb-0">
<thead class="table-light">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
@ -70,19 +75,17 @@
<tbody>
<tr>
<td>{{ advance_adj.date|date:"M d, Y" }}</td>
<td><span class="badge bg-info text-dark text-uppercase">Advance Payment</span></td>
<td><span class="badge" style="background: var(--color-info-bg); color: var(--color-info);">ADVANCE PAYMENT</span></td>
<td>{{ advance_adj.description|default:"Salary advance" }}</td>
<td class="text-end text-success fw-bold">R {{ advance_adj.amount|floatformat:2 }}</td>
<td class="text-end fw-bold" style="color: var(--color-success);">R {{ advance_adj.amount|floatformat:2 }}</td>
</tr>
</tbody>
</table>
</div>
<!-- === ADVANCE TOTAL === -->
<div class="row justify-content-end mt-4">
<div class="col-md-5">
<table class="table table-sm border-0">
<tr class="border-top border-dark">
<tr style="border-top: 2px solid var(--text-primary);">
<td class="text-end border-0 fw-bold fs-5">Amount Advanced:</td>
<td class="text-end border-0 fw-bold fs-5">R {{ advance_adj.amount|floatformat:2 }}</td>
</tr>
@ -92,10 +95,10 @@
{% elif is_loan %}
<!-- === LOAN PAYMENT DETAIL === -->
<h6 class="text-uppercase text-muted fw-bold small mb-3">Loan Details</h6>
<div class="stat-label mb-3">Loan Details</div>
<div class="table-responsive mb-4">
<table class="table table-hover mb-0">
<thead class="table-light">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
@ -106,19 +109,17 @@
<tbody>
<tr>
<td>{{ loan_adj.date|date:"M d, Y" }}</td>
<td><span class="badge bg-warning text-dark text-uppercase">Loan Payment</span></td>
<td><span class="badge" style="background: var(--color-warning-bg); color: var(--color-warning);">LOAN PAYMENT</span></td>
<td>{{ loan_adj.description|default:"Worker loan" }}</td>
<td class="text-end text-success fw-bold">R {{ loan_adj.amount|floatformat:2 }}</td>
<td class="text-end fw-bold" style="color: var(--color-success);">R {{ loan_adj.amount|floatformat:2 }}</td>
</tr>
</tbody>
</table>
</div>
<!-- === LOAN TOTAL === -->
<div class="row justify-content-end mt-4">
<div class="col-md-5">
<table class="table table-sm border-0">
<tr class="border-top border-dark">
<tr style="border-top: 2px solid var(--text-primary);">
<td class="text-end border-0 fw-bold fs-5">Loan Amount:</td>
<td class="text-end border-0 fw-bold fs-5">R {{ loan_adj.amount|floatformat:2 }}</td>
</tr>
@ -127,11 +128,11 @@
</div>
{% else %}
<!-- === WORK LOG TABLE — each day worked === -->
<h6 class="text-uppercase text-muted fw-bold small mb-3">Work Log Details (Attendance)</h6>
<!-- === WORK LOG TABLE === -->
<div class="stat-label mb-3">Work Log Details (Attendance)</div>
<div class="table-responsive mb-4">
<table class="table table-hover mb-0">
<thead class="table-light">
<thead>
<tr>
<th>Date</th>
<th>Project</th>
@ -144,19 +145,19 @@
<tr>
<td>{{ log.date|date:"M d, Y" }}</td>
<td>{{ log.project.name }}</td>
<td>{{ log.notes|default:"—"|truncatechars:50 }}</td>
<td style="color: var(--text-secondary);">{{ log.notes|default:"—"|truncatechars:50 }}</td>
<td class="text-end">R {{ record.worker.daily_rate|floatformat:2 }}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center text-muted">
<td colspan="4" class="text-center py-3" style="color: var(--text-tertiary);">
<i class="fas fa-info-circle me-1"></i> No work logs in this period.
</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="table-light">
<tr>
<tfoot>
<tr style="background: var(--bg-inset);">
<td colspan="3" class="text-end fw-bold">Base Pay Subtotal</td>
<td class="text-end fw-bold">R {{ base_pay|floatformat:2 }}</td>
</tr>
@ -164,12 +165,12 @@
</table>
</div>
<!-- === ADJUSTMENTS TABLE — bonuses, deductions, overtime, loan repayments === -->
<!-- === ADJUSTMENTS TABLE === -->
{% if adjustments %}
<h6 class="text-uppercase text-muted fw-bold small mb-3 mt-4">Adjustments (Bonuses, Deductions, Loans)</h6>
<div class="stat-label mb-3 mt-4">Adjustments (Bonuses, Deductions, Loans)</div>
<div class="table-responsive mb-4">
<table class="table table-hover mb-0">
<thead class="table-light">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
@ -182,15 +183,13 @@
<tr>
<td>{{ adj.date|date:"M d, Y" }}</td>
<td>
<span class="badge bg-secondary text-uppercase">{{ adj.get_type_display }}</span>
<span class="badge" style="background: var(--bg-inset); color: var(--text-secondary); border: 1px solid var(--border-default);">
{{ adj.get_type_display|upper }}
</span>
</td>
<td>{{ adj.description }}</td>
<td class="text-end {% if adj.type in deductive_types %}text-danger{% else %}text-success{% endif %}">
{% if adj.type in deductive_types %}
- R {{ adj.amount|floatformat:2 }}
{% else %}
+ R {{ adj.amount|floatformat:2 }}
{% endif %}
<td class="text-end fw-semibold {% if adj.type in deductive_types %}{% else %}{% endif %}" style="color: {% if adj.type in deductive_types %}var(--color-danger){% else %}var(--color-success){% endif %};">
{% if adj.type in deductive_types %}- R {{ adj.amount|floatformat:2 }}{% else %}+ R {{ adj.amount|floatformat:2 }}{% endif %}
</td>
</tr>
{% endfor %}
@ -199,27 +198,23 @@
</div>
{% endif %}
<!-- === GRAND TOTAL SUMMARY === -->
<!-- === GRAND TOTAL === -->
<div class="row justify-content-end mt-4">
<div class="col-md-5">
<table class="table table-sm border-0">
<tr>
<td class="text-end border-0 text-muted">Base Pay:</td>
<td class="text-end border-0" style="color: var(--text-secondary);">Base Pay:</td>
<td class="text-end border-0" width="140">R {{ base_pay|floatformat:2 }}</td>
</tr>
{% if adjustments %}
<tr>
<td class="text-end border-0 text-muted">Adjustments Net:</td>
<td class="text-end border-0" style="color: var(--text-secondary);">Adjustments Net:</td>
<td class="text-end border-0">
{% if adjustments_net >= 0 %}
+ R {{ adjustments_net|floatformat:2 }}
{% else %}
- R {{ adjustments_net_abs|floatformat:2 }}
{% endif %}
{% if adjustments_net >= 0 %}+ R {{ adjustments_net|floatformat:2 }}{% else %}- R {{ adjustments_net_abs|floatformat:2 }}{% endif %}
</td>
</tr>
{% endif %}
<tr class="border-top border-dark">
<tr style="border-top: 2px solid var(--text-primary);">
<td class="text-end border-0 fw-bold fs-5">Net Payable:</td>
<td class="text-end border-0 fw-bold fs-5">R {{ record.amount_paid|floatformat:2 }}</td>
</tr>
@ -229,9 +224,9 @@
{% endif %}
<!-- === FOOTER === -->
<div class="text-center text-muted small mt-5 pt-4 border-top">
<div class="text-center mt-5 pt-4" style="border-top: 1px solid var(--border-default); color: var(--text-tertiary); font-size: 0.85rem;">
<p>This is a computer-generated document and does not require a signature.</p>
<p>Payer: Fox Fitt &copy; 2026</p>
<p>Payer: Fox Fitt &copy; {% now "Y" %}</p>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Work History | Fox Fitt{% endblock %}
{% block title %}Work History | FoxFitt{% endblock %}
{% block content %}
<!-- === WORK HISTORY PAGE ===
@ -13,9 +13,9 @@
<div class="container py-4">
{# === PAGE HEADER with view toggle and export === #}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0" style="font-family: 'Poppins', sans-serif;">Work History</h1>
<div class="d-flex gap-2">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<h1 class="page-title"><i class="fas fa-clock me-2" style="color: var(--accent);"></i>Work History</h1>
<div class="d-flex gap-2 flex-wrap">
{# View toggle — List vs Calendar #}
<div class="btn-group" role="group" aria-label="View mode">
<a href="?view=list{{ filter_params }}"
@ -27,59 +27,48 @@
<i class="fas fa-calendar-alt me-1"></i> Calendar
</a>
</div>
{# CSV Export button — keeps the current filters in the export URL #}
<a href="{% url 'export_work_log_csv' %}?worker={{ selected_worker }}&project={{ selected_project }}&status={{ selected_status }}"
class="btn btn-outline-success btn-sm shadow-sm">
class="btn btn-outline-secondary btn-sm">
<i class="fas fa-file-csv me-1"></i> Export CSV
</a>
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm shadow-sm">
<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>
{# === FILTER BAR === #}
<div class="card shadow-sm border-0 mb-4{% if has_active_filters %} border-start border-3{% endif %}" {% if has_active_filters %}style="border-left-color: var(--accent, #10b981) !important;"{% endif %}>
<div class="card mb-4{% if has_active_filters %} border-start border-3{% endif %}" {% if has_active_filters %}style="border-left-color: var(--accent) !important;"{% endif %}>
<div class="card-body py-3">
<form method="GET" action="{% url 'work_history' %}" class="row g-2 align-items-end">
{# Preserve current view mode when filtering #}
<input type="hidden" name="view" value="{{ view_mode }}">
{% if view_mode == 'calendar' %}
{# Preserve current calendar month when filtering #}
<input type="hidden" name="year" value="{{ curr_year }}">
<input type="hidden" name="month" value="{{ curr_month }}">
{% endif %}
{# Filter by Worker #}
<div class="col-md-3">
<label class="form-label small text-muted mb-1">Worker</label>
<label class="form-label">Worker</label>
<select name="worker" class="form-select form-select-sm">
<option value="">All Workers</option>
{% for w in filter_workers %}
<option value="{{ w.id }}" {% if selected_worker == w.id|stringformat:"d" %}selected{% endif %}>
{{ w.name }}
</option>
<option value="{{ w.id }}" {% if selected_worker == w.id|stringformat:"d" %}selected{% endif %}>{{ w.name }}</option>
{% endfor %}
</select>
</div>
{# Filter by Project #}
<div class="col-md-3">
<label class="form-label small text-muted mb-1">Project</label>
<label class="form-label">Project</label>
<select name="project" class="form-select form-select-sm">
<option value="">All Projects</option>
{% for p in filter_projects %}
<option value="{{ p.id }}" {% if selected_project == p.id|stringformat:"d" %}selected{% endif %}>
{{ p.name }}
</option>
<option value="{{ p.id }}" {% if selected_project == p.id|stringformat:"d" %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
</div>
{# Filter by Payment Status #}
<div class="col-md-3">
<label class="form-label small text-muted mb-1">Payment Status</label>
<label class="form-label">Payment Status</label>
<select name="status" class="form-select form-select-sm">
<option value="" {% if not selected_status %}selected{% endif %}>All</option>
<option value="paid" {% if selected_status == 'paid' %}selected{% endif %}>Paid</option>
@ -87,7 +76,6 @@
</select>
</div>
{# Filter + Clear Buttons #}
<div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-sm btn-accent">
<i class="fas fa-filter me-1"></i> Filter
@ -100,31 +88,28 @@
</div>
</form>
{# === Active Filter Feedback === #}
{# Shows a results counter when filters are active so the user can see the filter is working #}
{# Active filter feedback #}
{% if has_active_filters %}
<div class="mt-2 d-flex align-items-center flex-wrap gap-2">
<small class="text-muted">
<small style="color: var(--text-secondary);">
<i class="fas fa-info-circle me-1"></i>
Showing <strong>{{ filtered_log_count }}</strong> of {{ total_log_count }} work log{{ total_log_count|pluralize }}
</small>
{# Show which filters are active as small badges #}
{% if selected_worker %}
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25">
<span class="badge" style="background: var(--color-info-bg); color: var(--color-info); border: 1px solid var(--color-info);">
<i class="fas fa-user fa-xs me-1"></i>
{% for w in filter_workers %}{% if w.id|stringformat:"d" == selected_worker %}{{ w.name }}{% endif %}{% endfor %}
</span>
{% endif %}
{% if selected_project %}
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25">
<span class="badge" style="background: var(--color-success-bg); color: var(--color-success); border: 1px solid var(--color-success);">
<i class="fas fa-project-diagram fa-xs me-1"></i>
{% for p in filter_projects %}{% if p.id|stringformat:"d" == selected_project %}{{ p.name }}{% endif %}{% endfor %}
</span>
{% endif %}
{% if selected_status %}
<span class="badge bg-warning bg-opacity-10 text-dark border border-warning border-opacity-25">
<i class="fas fa-tag fa-xs me-1"></i>
{{ selected_status|capfirst }}
<span class="badge" style="background: var(--color-warning-bg); color: var(--color-warning); border: 1px solid var(--color-warning);">
<i class="fas fa-tag fa-xs me-1"></i>{{ selected_status|capfirst }}
</span>
{% endif %}
</div>
@ -138,17 +123,15 @@
{# === CALENDAR VIEW === #}
{# =============================================================== #}
{# Month navigation header #}
<div class="card shadow-sm border-0 mb-3">
{# Month navigation #}
<div class="card mb-3">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center">
<a href="?view=calendar&year={{ prev_year }}&month={{ prev_month }}{{ filter_params }}"
class="btn btn-sm btn-outline-secondary">
<i class="fas fa-chevron-left"></i>
</a>
<h5 class="mb-0 fw-bold" style="font-family: 'Poppins', sans-serif;">
{{ month_name }}
</h5>
<h5 class="mb-0 fw-bold" style="font-family: 'Poppins', sans-serif;">{{ month_name }}</h5>
<a href="?view=calendar&year={{ next_year }}&month={{ next_month }}{{ filter_params }}"
class="btn btn-sm btn-outline-secondary">
<i class="fas fa-chevron-right"></i>
@ -158,10 +141,10 @@
</div>
{# Calendar grid #}
<div class="card shadow-sm border-0 mb-3">
<div class="card mb-3">
<div class="card-body p-0 p-md-3">
{# Day-of-week header row #}
<div class="row g-0 d-none d-md-flex text-center fw-bold text-secondary border-bottom pb-2 mb-2" style="font-size: 0.85rem;">
{# Day-of-week header #}
<div class="row g-0 d-none d-md-flex text-center fw-bold border-bottom pb-2 mb-2" style="font-size: 0.85rem; color: var(--text-secondary);">
<div class="col">Mon</div>
<div class="col">Tue</div>
<div class="col">Wed</div>
@ -171,36 +154,32 @@
<div class="col">Sun</div>
</div>
{# Calendar weeks — each row is 7 day cells #}
{% for week in calendar_weeks %}
<div class="row g-0 g-md-1 mb-0 mb-md-1">
{% for day in week %}
<div class="col cal-day {% if not day.is_current_month %}cal-day--other{% endif %}{% if day.is_today %} cal-day--today{% endif %}{% if day.count > 0 %} cal-day--has-logs{% endif %}"
{% if day.count > 0 %}data-date="{{ day.date|date:'Y-m-d' }}"{% endif %}>
{# Day number + badge count #}
<div class="d-flex justify-content-between align-items-start">
<span class="cal-day__number {% if day.is_today %}fw-bold{% endif %}">{{ day.day }}</span>
<span class="cal-day__number">{{ day.day }}</span>
{% if day.count > 0 %}
<span class="badge bg-primary rounded-pill" style="font-size: 0.65rem;">{{ day.count }}</span>
<span class="badge rounded-pill" style="font-size: 0.65rem; background: var(--accent);">{{ day.count }}</span>
{% endif %}
</div>
{# Mini log indicators (show first 3 entries) #}
{% for log in day.records|slice:":3" %}
<div class="cal-entry text-truncate" title="{{ log.project.name }}">
<small>
{% if log.payroll_records.exists %}
<i class="fas fa-check-circle text-success" style="font-size: 0.55rem;"></i>
<i class="fas fa-check-circle" style="font-size: 0.55rem; color: var(--color-success);"></i>
{% else %}
<i class="fas fa-clock text-warning" style="font-size: 0.55rem;"></i>
<i class="fas fa-clock" style="font-size: 0.55rem; color: var(--color-warning);"></i>
{% endif %}
{{ log.project.name }}
</small>
</div>
{% endfor %}
{# "and X more" indicator #}
{% if day.count > 3 %}
<div class="cal-entry">
<small class="text-muted">+{{ day.count|add:"-3" }} more</small>
<small style="color: var(--text-tertiary);">+{{ day.count|add:"-3" }} more</small>
</div>
{% endif %}
</div>
@ -211,62 +190,49 @@
</div>
{# === Day Detail Panel === #}
{# Hidden by default. Click day cells to select them — shows combined details with totals. #}
<div class="card shadow-sm border-0 d-none" id="dayDetailPanel">
<div class="card-header py-2 bg-white">
<div class="card d-none" id="dayDetailPanel">
<div class="card-header py-2">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold" id="dayDetailTitle">
<i class="fas fa-calendar-day me-2"></i>Details
</h6>
<div class="d-flex gap-2 align-items-center">
<span class="badge bg-primary rounded-pill d-none" id="daySelectionCount"></span>
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearDaySelection"
title="Clear selection">
<span class="badge rounded-pill d-none" style="background: var(--accent);" id="daySelectionCount"></span>
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearDaySelection" title="Clear selection">
<i class="fas fa-times-circle me-1"></i> Clear
</button>
</div>
</div>
{# Hint text for multi-select #}
<small class="text-muted d-block mt-1" id="multiSelectHint">
<small class="d-block mt-1" style="color: var(--text-tertiary);" id="multiSelectHint">
<i class="fas fa-info-circle me-1"></i>Click more days to add them to the selection
</small>
</div>
<div class="card-body p-0" id="dayDetailBody">
{# Content built by JavaScript #}
</div>
{# === Totals Footer (admin only, shown when days are selected) === #}
<div class="card-body p-0" id="dayDetailBody"></div>
{% if is_admin %}
<div class="card-footer bg-white border-top d-none" id="dayDetailFooter">
<div class="card-footer border-top d-none" id="dayDetailFooter">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>Total:</strong>
<span class="text-muted ms-2" id="totalDays">0 days</span>
<span class="text-muted mx-1">·</span>
<span class="text-muted" id="totalLogs">0 logs</span>
<span class="text-muted mx-1">·</span>
<span class="text-muted" id="totalWorkers">0 unique workers</span>
<span class="ms-2" style="color: var(--text-secondary);" id="totalDays">0 days</span>
<span class="mx-1" style="color: var(--text-tertiary);">&middot;</span>
<span style="color: var(--text-secondary);" id="totalLogs">0 logs</span>
<span class="mx-1" style="color: var(--text-tertiary);">&middot;</span>
<span style="color: var(--text-secondary);" id="totalWorkers">0 unique workers</span>
</div>
<div>
<strong class="fs-5" style="color: var(--accent-color, #10b981);" id="totalAmount">R 0.00</strong>
<strong class="fs-5" style="color: var(--accent);" id="totalAmount">R 0.00</strong>
</div>
</div>
</div>
{% endif %}
</div>
{# Pass calendar detail data to JavaScript safely using json_script #}
{{ calendar_detail|json_script:"calDetailJson" }}
<script>
(function() {
'use strict';
// === 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');
@ -276,80 +242,49 @@
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];
return parseInt(parts[2], 10) + ' ' + months[parseInt(parts[1], 10) - 1];
}
// === 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];
return parseInt(parts[2], 10) + ' ' + months[parseInt(parts[1], 10) - 1] + ' ' + 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
(calDetail[dateStr] || []).forEach(function(entry) {
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;
}
entry.workers.forEach(function(w) { uniqueWorkers[w] = true; });
if (isAdmin && entry.amount !== undefined) totalAmount += entry.amount;
});
});
// === Update panel title ===
// 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)'
));
detailTitle.appendChild(document.createTextNode(formatDateLong(selectedDates[0]) + ' \u2014 ' + allEntries.length + ' log(s)'));
} else {
// Multiple days: show date range or count
detailTitle.appendChild(document.createTextNode(
selectedDates.length + ' days selected — ' + allEntries.length + ' log(s)'
));
detailTitle.appendChild(document.createTextNode(selectedDates.length + ' days selected \u2014 ' + allEntries.length + ' log(s)'));
}
// Update selection count badge
if (selectedDates.length > 1) {
selCountBadge.textContent = selectedDates.length + ' days';
selCountBadge.classList.remove('d-none');
@ -359,17 +294,13 @@
multiSelectHint.classList.remove('d-none');
}
// === Clear previous content ===
// Build table
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'];
@ -388,7 +319,6 @@
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';
@ -396,7 +326,6 @@
tr.appendChild(tdDate);
}
// Project
var tdProj = document.createElement('td');
tdProj.className = selectedDates.length === 1 ? 'ps-3' : '';
var strong = document.createElement('strong');
@ -404,53 +333,43 @@
tdProj.appendChild(strong);
tr.appendChild(tdProj);
// Workers — each name gets a small pill badge for readability
var tdWork = document.createElement('td');
entry.workers.forEach(function(name) {
var pill = document.createElement('span');
pill.className = 'badge rounded-pill bg-light text-dark fw-normal border me-1 mb-1';
pill.className = 'badge rounded-pill fw-normal me-1 mb-1';
pill.style.cssText = 'background: var(--bg-inset); color: var(--text-primary); border: 1px solid var(--border-default);';
pill.textContent = name;
tdWork.appendChild(pill);
});
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.className = 'badge';
otBadge.style.cssText = 'background: var(--color-warning-bg); color: var(--color-warning);';
otBadge.textContent = entry.overtime;
tdOt.appendChild(otBadge);
} else {
tdOt.textContent = '-';
tdOt.className = 'text-muted';
tdOt.style.color = 'var(--text-tertiary)';
}
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';
}
statusBadge.className = 'badge ' + (entry.is_paid ? 'bg-success' : 'bg-danger bg-opacity-75');
statusBadge.textContent = entry.is_paid ? 'Paid' : '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)
: '-';
tdAmt.textContent = entry.amount !== undefined ? 'R ' + entry.amount.toFixed(2) : '-';
tr.appendChild(tdAmt);
}
@ -459,137 +378,55 @@
table.appendChild(tbody);
detailBody.appendChild(table);
// === Update totals footer (admin only) ===
// Totals footer
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);
document.getElementById('totalDays').textContent = selectedDates.length + ' day' + (selectedDates.length !== 1 ? 's' : '');
document.getElementById('totalLogs').textContent = allEntries.length + ' log' + (allEntries.length !== 1 ? 's' : '');
document.getElementById('totalWorkers').textContent = uniqueCount + ' unique worker' + (uniqueCount !== 1 ? 's' : '');
document.getElementById('totalAmount').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;
// Toggle this date in the selection
if (!(calDetail[dateStr] || []).length) return;
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');
}
// Refresh the detail panel with the updated selection
updateDetailPanel();
});
});
// === Clear all selections ===
clearBtn.addEventListener('click', function() {
selectedDates = [];
document.querySelectorAll('.cal-day--selected').forEach(function(c) {
c.classList.remove('cal-day--selected');
});
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');
});
})();
</script>
{# Calendar-specific CSS #}
<style>
/* === CALENDAR GRID STYLES === */
.cal-day {
min-height: 90px;
padding: 6px 8px;
border: 1px solid #e2e8f0;
border-radius: 4px;
background: #fff;
transition: background-color 0.15s, box-shadow 0.15s;
}
.cal-day__number {
font-size: 0.85rem;
color: var(--text-main, #334155);
}
/* Days from previous/next month — faded */
.cal-day--other {
background-color: #f8fafc;
opacity: 0.5;
}
/* Today's date — accent border */
.cal-day--today {
border-color: var(--accent-color, #10b981);
border-width: 2px;
}
.cal-day--today .cal-day__number {
color: var(--accent-color, #10b981);
}
/* Days with logs — clickable */
.cal-day--has-logs {
cursor: pointer;
}
.cal-day--has-logs:hover {
background-color: #f0fdfa;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
/* Selected day */
.cal-day--selected {
background-color: #ecfdf5 !important;
border-color: var(--accent-color, #10b981) !important;
border-width: 2px;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15);
}
/* Mini log entry indicators */
.cal-entry {
line-height: 1.3;
font-size: 0.72rem;
}
/* Mobile: compact cells */
@media (max-width: 767.98px) {
.cal-day {
min-height: 55px;
padding: 4px 5px;
font-size: 0.75rem;
}
.cal-entry {
display: none; /* Hide text indicators on mobile, just show badges */
}
}
</style>
{% else %}
{# =============================================================== #}
{# === LIST VIEW (TABLE) === #}
{# =============================================================== #}
<div class="card shadow-sm border-0">
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<thead>
<tr>
<th scope="col" class="ps-4">Date</th>
<th scope="col">Project</th>
@ -606,25 +443,23 @@
<td class="ps-4 align-middle">{{ log.date }}</td>
<td class="align-middle"><strong>{{ log.project.name }}</strong></td>
<td class="align-middle">
{# When filtering by a specific worker, show only that worker. Otherwise show all workers. #}
{% if filtered_worker_obj %}
<span class="badge rounded-pill bg-light text-dark fw-normal border">{{ filtered_worker_obj.name }}</span>
<span class="badge rounded-pill fw-normal" style="background: var(--bg-inset); color: var(--text-primary); border: 1px solid var(--border-default);">{{ filtered_worker_obj.name }}</span>
{% else %}
{% for w in log.workers.all %}
<span class="badge rounded-pill bg-light text-dark fw-normal border me-1 mb-1">{{ w.name }}</span>
<span class="badge rounded-pill fw-normal me-1 mb-1" style="background: var(--bg-inset); color: var(--text-primary); border: 1px solid var(--border-default);">{{ w.name }}</span>
{% endfor %}
<span class="badge rounded-pill bg-secondary">{{ log.workers.count }}</span>
<span class="badge rounded-pill" style="background: var(--text-secondary); color: var(--text-on-accent);">{{ log.workers.count }}</span>
{% endif %}
</td>
<td class="align-middle">
{% if log.overtime_amount > 0 %}
<span class="badge bg-warning text-dark">{{ log.get_overtime_amount_display }}</span>
<span class="badge" style="background: var(--color-warning-bg); color: var(--color-warning);">{{ log.get_overtime_amount_display }}</span>
{% else %}
<span class="text-muted">-</span>
<span style="color: var(--text-tertiary);">-</span>
{% endif %}
</td>
<td class="align-middle">
{# Payment status — a WorkLog is "paid" if it has at least one PayrollRecord #}
{% if log.payroll_records.exists %}
<span class="badge bg-success"><i class="fas fa-check me-1"></i>Paid</span>
{% else %}
@ -633,11 +468,10 @@
</td>
{% if is_admin %}
<td class="align-middle">
{# Daily cost — worker's rate when filtered, otherwise total for all workers #}
{% if filtered_worker_obj %}
<span class="text-success fw-semibold">R {{ filtered_worker_obj.daily_rate }}</span>
<span class="fw-semibold" style="color: var(--color-success);">R {{ filtered_worker_obj.daily_rate }}</span>
{% else %}
<span class="text-success fw-semibold">R {{ log.display_amount }}</span>
<span class="fw-semibold" style="color: var(--color-success);">R {{ log.display_amount }}</span>
{% endif %}
</td>
{% endif %}
@ -645,14 +479,14 @@
{% if log.supervisor %}
{{ log.supervisor.get_full_name|default:log.supervisor.username }}
{% else %}
<span class="text-muted">-</span>
<span style="color: var(--text-tertiary);">-</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="{% if is_admin %}7{% else %}6{% endif %}" class="text-center py-5 text-muted">
<i class="fas fa-inbox fa-2x mb-3 d-block opacity-50"></i>
<td colspan="{% if is_admin %}7{% else %}6{% endif %}" class="text-center py-5" style="color: var(--text-tertiary);">
<i class="fas fa-inbox fa-2x mb-3 d-block"></i>
No work history found.
{% if selected_worker or selected_project or selected_status %}
<br><small>Try adjusting your filters.</small>

View File

@ -1,32 +1,92 @@
{% extends "base.html" %}
{% block content %}
<div class="container d-flex justify-content-center align-items-center min-vh-100">
<div class="card shadow-sm" style="width: 100%; max-width: 400px; border-radius: 12px; border: none;">
<div class="card-body p-5">
<h2 class="text-center mb-4" style="font-family: 'Poppins', sans-serif; font-weight: 700;">
<span style="color: #10b981;">Fox</span>Fitt
</h2>
{% block title %}Login | FoxFitt{% endblock %}
{% block content %}
<!-- === LOGIN PAGE — full-screen centred with premium orange theme === -->
<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg-body); position: relative; overflow: hidden;">
<!-- Decorative orange glow -->
<div style="position: absolute; top: -200px; right: -100px; width: 500px; height: 500px; background: radial-gradient(circle, var(--accent-glow) 0%, transparent 65%); pointer-events: none;"></div>
<div style="position: absolute; bottom: -200px; left: -100px; width: 400px; height: 400px; background: radial-gradient(circle, rgba(232, 133, 26, 0.08) 0%, transparent 70%); pointer-events: none;"></div>
<div class="card" style="width: 100%; max-width: 420px; position: relative; z-index: 1;">
<div class="card-body p-4 p-md-5">
<!-- Brand icon + name -->
<div class="text-center mb-4">
<div class="sidebar-brand__icon mx-auto mb-3" style="width: 48px; height: 48px; font-size: 1.25rem;">
<i class="fas fa-bolt"></i>
</div>
<h1 class="mb-1" style="font-size: 2rem;">
<span style="color: var(--accent); font-weight: 700;">Fox</span><span style="font-weight: 700;">Fitt</span>
</h1>
<p class="text-muted mb-0" style="font-size: 0.85rem;">Payroll Management System</p>
</div>
<!-- Error message -->
{% if form.errors %}
<div class="alert alert-danger" role="alert">
<div class="alert alert-danger d-flex align-items-center" role="alert" style="font-size: 0.875rem;">
<i class="fas fa-exclamation-circle me-2"></i>
Your username and password didn't match. Please try again.
</div>
{% endif %}
<!-- Login form -->
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<div class="mb-3">
<label for="id_username" class="form-label" style="font-family: 'Inter', sans-serif; font-weight: 500;">Username</label>
<input type="text" name="username" class="form-control form-control-lg" id="id_username" required autofocus style="border-radius: 8px;">
<label for="id_username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user" style="width: 1rem; text-align: center;"></i></span>
<input type="text" name="username" class="form-control form-control-lg" id="id_username" placeholder="Enter username" required autofocus>
</div>
</div>
<div class="mb-4">
<label for="id_password" class="form-label" style="font-family: 'Inter', sans-serif; font-weight: 500;">Password</label>
<input type="password" name="password" class="form-control form-control-lg" id="id_password" required style="border-radius: 8px;">
<label for="id_password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock" style="width: 1rem; text-align: center;"></i></span>
<input type="password" name="password" class="form-control form-control-lg" id="id_password" placeholder="Enter password" required>
</div>
</div>
<button type="submit" class="btn btn-lg w-100 text-white" style="background-color: #10b981; border: none; border-radius: 8px; font-weight: 600;">Login</button>
<button type="submit" class="btn btn-accent btn-lg w-100">
<i class="fas fa-sign-in-alt me-2"></i>Login
</button>
</form>
<!-- Theme toggle (since sidebar isn't visible on login page) -->
<div class="text-center mt-4 pt-3" style="border-top: 1px solid var(--border-subtle);">
<button type="button" class="theme-toggle" id="loginThemeToggle" style="border-color: var(--border-default); color: var(--text-secondary); margin: 0 auto;">
<i class="fas fa-moon" id="loginThemeIcon"></i>
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- Theme toggle for login page (sidebar toggle is hidden when not authenticated) -->
<script>
(function() {
var toggle = document.getElementById('loginThemeToggle');
var icon = document.getElementById('loginThemeIcon');
if (!toggle || !icon) return;
function updateLoginToggle() {
var isDark = document.documentElement.getAttribute('data-theme') !== 'light';
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
}
updateLoginToggle();
toggle.addEventListener('click', function() {
var current = document.documentElement.getAttribute('data-theme');
var next = (current === 'light') ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('foxfitt-theme', next);
updateLoginToggle();
});
})();
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff