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 %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}FoxFitt{% endblock %}</title> <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 --> <!-- 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"> <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 --> <!-- 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"> <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 --> <!-- Font Awesome 6 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <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' }}"> <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 */ {% block extra_css %}{% endblock %}
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>
</head> </head>
<body> <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 %} {% 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> APP LAYOUT — sidebar (desktop) + top bar (mobile) + content
</button> =================================================================== -->
<div class="collapse navbar-collapse" id="navbarNav"> <div class="app-layout">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"> <!-- === SIDEBAR (desktop only, hidden on mobile via CSS) === -->
<a class="nav-link {% if request.resolver_match.url_name == 'home' %}active{% endif %}" href="{% url 'home' %}"> <aside class="app-sidebar d-print-none">
<i class="fas fa-home me-1"></i> Dashboard
</a> <!-- Brand / Logo -->
</li> <div class="sidebar-brand">
<li class="nav-item"> <div class="sidebar-brand__icon">
<a class="nav-link {% if request.resolver_match.url_name == 'attendance_log' %}active{% endif %}" href="{% url 'attendance_log' %}"> <i class="fas fa-bolt"></i>
<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> </div>
{% endif %} <a href="{% url 'home' %}" class="sidebar-brand__text">
<span>Fox</span>Fitt
</a>
</div> </div>
</nav>
<div class="container mt-4"> <!-- Navigation Links -->
<!-- Messages Block --> <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 %} {% if messages %}
<div class="container-fluid px-3 px-lg-4 mt-3">
{% for message in messages %} {% 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 }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% 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> </div>
</footer> {% endif %}
<!-- Bootstrap 5.3 JS Bundle --> <!-- === Page Content === -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> <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> </body>
</html> </html>

View File

@ -1,27 +1,29 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% block title %}Log Work | Fox Fitt{% endblock %} {% block title %}Log Work | FoxFitt{% endblock %}
{% block content %} {% block content %}
<div class="container py-4"> <div class="container py-4">
<!-- === Page Header === -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0" style="font-family: 'Poppins', sans-serif;">Log Daily Attendance</h1> <div>
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm shadow-sm"> <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 <i class="fas fa-arrow-left fa-sm me-1"></i> Back
</a> </a>
</div> </div>
<div class="row"> <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="{% 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"> <div class="card-body p-4 p-md-5">
{# --- Conflict Warning --- #} {# --- Conflict Warning --- #}
{# If we found workers already logged on selected dates, show this warning #}
{% if conflicts %} {% 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"> <h6 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>Conflicts Found <i class="fas fa-exclamation-triangle me-2"></i>Conflicts Found
</h6> </h6>
@ -34,15 +36,11 @@
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<form method="POST" class="d-inline"> <form method="POST" class="d-inline">
{% csrf_token %} {% 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 %} {% for key, value in form.data.items %}
{% if key != 'csrfmiddlewaretoken' and key != 'conflict_action' and key != 'workers' %} {% if key != 'csrfmiddlewaretoken' and key != 'conflict_action' and key != 'workers' %}
<input type="hidden" name="{{ key }}" value="{{ value }}"> <input type="hidden" name="{{ key }}" value="{{ value }}">
{% endif %} {% endif %}
{% endfor %} {% 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 %} {% for wid in selected_worker_ids %}
<input type="hidden" name="workers" value="{{ wid }}"> <input type="hidden" name="workers" value="{{ wid }}">
{% endfor %} {% endfor %}
@ -72,7 +70,7 @@
{# --- Form Errors --- #} {# --- Form Errors --- #}
{% if 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> <strong><i class="fas fa-exclamation-circle me-1"></i> Please fix the following:</strong>
<ul class="mb-0 mt-2"> <ul class="mb-0 mt-2">
{% for field, errors in form.errors.items %} {% for field, errors in form.errors.items %}
@ -87,7 +85,7 @@
<form method="POST" id="attendanceForm"> <form method="POST" id="attendanceForm">
{% csrf_token %} {% csrf_token %}
{# --- Date Range Section --- #} {# --- Date Range --- #}
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-semibold">Start Date</label> <label class="form-label fw-semibold">Start Date</label>
@ -95,10 +93,10 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-semibold"> <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> </label>
{{ form.end_date }} {{ 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>
</div> </div>
@ -126,7 +124,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-semibold"> <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> </label>
{{ form.team }} {{ form.team }}
</div> </div>
@ -135,7 +133,7 @@
{# --- Worker Checkboxes --- #} {# --- Worker Checkboxes --- #}
<div class="mb-4"> <div class="mb-4">
<label class="form-label d-block mb-3 fw-semibold">Workers Present</label> <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"> <div class="row">
{% for worker in form.workers %} {% for worker in form.workers %}
<div class="col-md-6 mb-2"> <div class="col-md-6 mb-2">
@ -165,9 +163,9 @@
{{ form.notes }} {{ form.notes }}
</div> </div>
{# --- Submit Button --- #} {# --- Submit --- #}
<div class="d-grid mt-5"> <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 <i class="fas fa-save me-2"></i>Log Work
</button> </button>
</div> </div>
@ -179,22 +177,22 @@
{# --- Estimated Cost Card (Admin Only) --- #} {# --- Estimated Cost Card (Admin Only) --- #}
{% if is_admin %} {% if is_admin %}
<div class="col-lg-4 mt-4 mt-lg-0"> <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"> <div class="card-body p-4">
<h6 class="fw-bold mb-3"> <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> </h6>
<div class="text-center py-3"> <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 R 0.00
</div> </div>
<small class="text-muted"> <small style="color: var(--text-secondary);">
<span id="selectedWorkerCount">0</span> worker(s) &times; <span id="selectedWorkerCount">0</span> worker(s) &times;
<span id="selectedDayCount">1</span> day(s) <span id="selectedDayCount">1</span> day(s)
</small> </small>
</div> </div>
<hr> <hr style="border-color: var(--border-default);">
<small class="text-muted"> <small style="color: var(--text-tertiary);">
This estimate is based on each worker's daily rate multiplied by the This estimate is based on each worker's daily rate multiplied by the
number of working days selected. Overtime is not included. number of working days selected. Overtime is not included.
</small> </small>
@ -205,50 +203,33 @@
</div> </div>
</div> </div>
{# --- JavaScript for dynamic features --- #} <!-- === JavaScript: Team auto-select + Cost estimator === -->
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// === TEAM AUTO-SELECT === // === 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 teamWorkersMap = JSON.parse('{{ team_workers_json|escapejs }}');
var teamSelect = document.querySelector('[name="team"]'); var teamSelect = document.querySelector('[name="team"]');
if (teamSelect) { if (teamSelect) {
teamSelect.addEventListener('change', function() { teamSelect.addEventListener('change', function() {
var teamId = this.value; var teamId = this.value;
// First, uncheck ALL worker checkboxes
var allBoxes = document.querySelectorAll('input[name="workers"]'); var allBoxes = document.querySelectorAll('input[name="workers"]');
allBoxes.forEach(function(cb) { allBoxes.forEach(function(cb) { cb.checked = false; });
cb.checked = false;
});
// Then check workers that belong to the selected team
if (teamId && teamWorkersMap[teamId]) { if (teamId && teamWorkersMap[teamId]) {
var workerIds = teamWorkersMap[teamId]; teamWorkersMap[teamId].forEach(function(id) {
workerIds.forEach(function(id) {
var checkbox = document.querySelector('input[name="workers"][value="' + id + '"]'); var checkbox = document.querySelector('input[name="workers"][value="' + id + '"]');
if (checkbox) { if (checkbox) checkbox.checked = true;
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 %} {% if is_admin %}
// === ESTIMATED COST CALCULATOR (Admin Only) === // === 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 workerRates = {{ worker_rates_json|safe }};
const startDateInput = document.querySelector('[name="date"]'); const startDateInput = document.querySelector('[name="date"]');
const endDateInput = document.querySelector('[name="end_date"]'); const endDateInput = document.querySelector('[name="end_date"]');
const satCheckbox = document.querySelector('[name="include_saturday"]'); const satCheckbox = document.querySelector('[name="include_saturday"]');
@ -259,26 +240,18 @@ document.addEventListener('DOMContentLoaded', function() {
const dayCountDisplay = document.getElementById('selectedDayCount'); const dayCountDisplay = document.getElementById('selectedDayCount');
function countWorkingDays() { function countWorkingDays() {
// Count how many working days are in the selected date range
const startDate = startDateInput ? new Date(startDateInput.value) : null; const startDate = startDateInput ? new Date(startDateInput.value) : null;
const endDateVal = endDateInput ? endDateInput.value : ''; const endDateVal = endDateInput ? endDateInput.value : '';
const endDate = endDateVal ? new Date(endDateVal) : startDate; const endDate = endDateVal ? new Date(endDateVal) : startDate;
if (!startDate || isNaN(startDate)) return 1; if (!startDate || isNaN(startDate)) return 1;
if (!endDate || isNaN(endDate)) return 1; if (!endDate || isNaN(endDate)) return 1;
let count = 0; let count = 0;
let current = new Date(startDate); let current = new Date(startDate);
while (current <= endDate) { while (current <= endDate) {
const day = current.getDay(); // 0=Sun, 6=Sat const day = current.getDay();
if (day === 6 && !(satCheckbox && satCheckbox.checked)) { if (day === 6 && !(satCheckbox && satCheckbox.checked)) { current.setDate(current.getDate() + 1); continue; }
current.setDate(current.getDate() + 1); if (day === 0 && !(sunCheckbox && sunCheckbox.checked)) { current.setDate(current.getDate() + 1); continue; }
continue;
}
if (day === 0 && !(sunCheckbox && sunCheckbox.checked)) {
current.setDate(current.getDate() + 1);
continue;
}
count++; count++;
current.setDate(current.getDate() + 1); current.setDate(current.getDate() + 1);
} }
@ -286,39 +259,27 @@ document.addEventListener('DOMContentLoaded', function() {
} }
function updateEstimatedCost() { function updateEstimatedCost() {
// Add up daily rates of all checked workers, multiply by number of days
let totalDailyRate = 0; let totalDailyRate = 0;
let selectedCount = 0; let selectedCount = 0;
workerCheckboxes.forEach(function(cb) { workerCheckboxes.forEach(function(cb) {
if (cb.checked) { if (cb.checked) {
const workerId = cb.value; const workerId = cb.value;
if (workerRates[workerId]) { if (workerRates[workerId]) totalDailyRate += parseFloat(workerRates[workerId]);
totalDailyRate += parseFloat(workerRates[workerId]);
}
selectedCount++; selectedCount++;
} }
}); });
const days = countWorkingDays(); const days = countWorkingDays();
const totalCost = totalDailyRate * days; const totalCost = totalDailyRate * days;
// Update the display
if (costDisplay) costDisplay.textContent = 'R ' + totalCost.toLocaleString('en-ZA', {minimumFractionDigits: 2, maximumFractionDigits: 2}); if (costDisplay) costDisplay.textContent = 'R ' + totalCost.toLocaleString('en-ZA', {minimumFractionDigits: 2, maximumFractionDigits: 2});
if (workerCountDisplay) workerCountDisplay.textContent = selectedCount; if (workerCountDisplay) workerCountDisplay.textContent = selectedCount;
if (dayCountDisplay) dayCountDisplay.textContent = days; 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 (startDateInput) startDateInput.addEventListener('change', updateEstimatedCost);
if (endDateInput) endDateInput.addEventListener('change', updateEstimatedCost); if (endDateInput) endDateInput.addEventListener('change', updateEstimatedCost);
if (satCheckbox) satCheckbox.addEventListener('change', updateEstimatedCost); if (satCheckbox) satCheckbox.addEventListener('change', updateEstimatedCost);
if (sunCheckbox) sunCheckbox.addEventListener('change', updateEstimatedCost); if (sunCheckbox) sunCheckbox.addEventListener('change', updateEstimatedCost);
// Run once on page load in case of pre-selected values
updateEstimatedCost(); updateEstimatedCost();
{% endif %} {% endif %}
}); });

View File

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

View File

@ -4,15 +4,13 @@
{% block title %}Dashboard | FoxFitt{% endblock %} {% block title %}Dashboard | FoxFitt{% endblock %}
{% block content %} {% block content %}
<style> <!-- === DASHBOARD HEADER — gradient banner with welcome + CTA === -->
{# Hide resource rows — needs !important to override Bootstrap's d-flex !important #} <div class="dashboard-header mb-5 rounded-0 p-4 d-flex justify-content-between align-items-center d-print-none">
.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">
<div> <div>
<h1 class="h3 mb-0 text-white" style="font-family: 'Poppins', sans-serif;">Dashboard</h1> <h1 class="h3 mb-1 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> <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> </div>
<a href="{% url 'attendance_log' %}" class="btn btn-accent shadow-sm"> <a href="{% url 'attendance_log' %}" class="btn btn-accent shadow-sm">
<i class="fas fa-plus fa-sm me-1"></i> Log Daily Work <i class="fas fa-plus fa-sm me-1"></i> Log Daily Work
@ -20,377 +18,385 @@
</div> </div>
<div class="container py-2" style="margin-top: -3rem;"> <div class="container py-2" style="margin-top: -3rem;">
{% if is_admin %}
<!-- Admin View --> {% if is_admin %}
<div class="row g-4 mb-4 position-relative"> <!-- ===================================================================
<!-- Outstanding Payments Card --> ADMIN VIEW — stats, quick actions, activity, resources
<!-- 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"> <!-- === STAT CARDS (4 columns) === -->
<div class="card-body"> <div class="row g-3 mb-4">
<div class="row no-gutters align-items-center">
<div class="col me-2"> <!-- Outstanding Payments -->
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #ef4444;"> <div class="col-xl-3 col-md-6">
Outstanding Payments</div> <div class="stat-card stat-card--danger h-100 p-3">
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ outstanding_payments|floatformat:2 }}</div> <div class="d-flex justify-content-between align-items-start">
{# === BREAKDOWN — only shown when there are pending adjustments === #} <div>
{% if pending_adjustments_add or pending_adjustments_sub %} <div class="stat-label">Outstanding Payments</div>
<div class="mt-2 pt-2 border-top" style="font-size: 0.75rem; color: #64748b;"> <div class="stat-value">R {{ outstanding_payments|floatformat:2 }}</div>
<div class="d-flex justify-content-between"> </div>
<span>Unpaid wages</span> <div class="stat-icon stat-icon--danger">
<span>R {{ unpaid_wages|floatformat:2 }}</span> <i class="fas fa-exclamation-circle"></i>
</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>
</div> </div>
</div> </div>
</div> {# Breakdown — wages + adjustments (shown when adjustments exist) #}
{% if pending_adjustments_add or pending_adjustments_sub %}
<!-- Paid This Month Card --> <div class="mt-2 pt-2" style="border-top: 1px solid var(--border-subtle); font-size: 0.75rem; color: var(--text-secondary);">
<div class="col-xl-3 col-md-6"> <div class="d-flex justify-content-between">
<div class="card stat-card h-100 py-2"> <span>Unpaid wages</span>
<div class="card-body"> <span>R {{ unpaid_wages|floatformat:2 }}</span>
<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>
</div> </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> </div>
</div>
<!-- Active Loans Card --> <!-- Paid This Month -->
<div class="col-xl-3 col-md-6"> <div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2"> <div class="stat-card stat-card--success h-100 p-3">
<div class="card-body"> <div class="d-flex justify-content-between align-items-start">
<div class="row no-gutters align-items-center"> <div>
<div class="col me-2"> <div class="stat-label">Paid This Month</div>
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #f59e0b;"> <div class="stat-value">R {{ paid_this_month|floatformat:2 }}</div>
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>
</div> </div>
</div> <div class="stat-icon stat-icon--success">
</div> <i class="fas fa-check-circle"></i>
<!-- 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> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Quick Actions and This Week --> <!-- Active Loans -->
<div class="row mb-4"> <div class="col-xl-3 col-md-6">
<!-- This Week --> <div class="stat-card stat-card--warning h-100 p-3">
<div class="col-lg-4 mb-4 mb-lg-0"> <div class="d-flex justify-content-between align-items-start">
<div class="card shadow-sm border-0 h-100"> <div>
<div class="card-header py-3 bg-white"> <div class="stat-label">Active Loans ({{ active_loans_count }})</div>
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">This Week Summary</h6> <div class="stat-value">R {{ active_loans_balance|floatformat:2 }}</div>
</div> </div>
<div class="card-body text-center d-flex flex-column justify-content-center"> <div class="stat-icon stat-icon--warning">
<div class="h1 mb-0 font-weight-bold text-primary">{{ this_week_logs }}</div> <i class="fas fa-hand-holding-usd"></i>
<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> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <!-- Outstanding by Project -->
<!-- Recent Activity --> <div class="col-xl-3 col-md-6">
<div class="col-lg-6 mb-4"> <div class="stat-card stat-card--info h-100 p-3">
<div class="card shadow-sm border-0 h-100"> <div class="d-flex justify-content-between align-items-start">
<div class="card-header py-3 bg-white"> <div style="flex: 1;">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Recent Activity</h6> <div class="stat-label">Outstanding by Project</div>
</div> {% if outstanding_by_project %}
<div class="card-body p-0"> <div style="font-size: 0.85rem; margin-top: 0.35rem;">
<div class="list-group list-group-flush"> {% for proj, amount in outstanding_by_project.items %}
{% for log in recent_activity %} <div class="d-flex justify-content-between" style="color: var(--text-primary);">
<div class="list-group-item px-4 py-3"> <span class="text-truncate me-2">{{ proj }}</span>
<div class="d-flex justify-content-between align-items-center"> <span class="fw-semibold" style="white-space: nowrap;">R {{ amount|floatformat:2 }}</span>
<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.
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %}
<span style="font-size: 0.85rem; color: var(--text-tertiary);">None</span>
{% endif %}
</div> </div>
</div> <div class="stat-icon stat-icon--info ms-2">
</div> <i class="fas fa-chart-pie"></i>
<!-- 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> </div>
</div> </div>
</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> </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> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// === RESOURCE FILTER (Active / Inactive / All) === // === 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 currentFilter = 'active';
var filterBtns = document.querySelectorAll('#resourceFilter button'); var filterBtns = document.querySelectorAll('#resourceFilter button');
function applyFilter() { 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) { document.querySelectorAll('.resource-row').forEach(function(row) {
var isActive = row.dataset.active === 'true'; var isActive = row.dataset.active === 'true';
var show = false; var show = false;
@ -403,13 +409,13 @@ document.addEventListener('DOMContentLoaded', function() {
row.classList.add('resource-hidden'); 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) { document.querySelectorAll('.tab-pane').forEach(function(pane) {
var rows = pane.querySelectorAll('.resource-row'); 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'); var emptyMsg = pane.querySelector('.resource-empty');
if (emptyMsg) { 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(); applyFilter();
// === TOGGLE HANDLER === // === TOGGLE HANDLER — AJAX POST to activate/deactivate resources ===
// When a toggle switch is flipped, POST to the server to update active status. document.querySelectorAll('.toggle-active').forEach(function(switchEl) {
// 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) {
switchEl.addEventListener('change', function() { switchEl.addEventListener('change', function() {
var type = this.getAttribute('data-type'); var type = this.getAttribute('data-type');
var id = this.getAttribute('data-id'); var id = this.getAttribute('data-id');
@ -452,7 +452,6 @@ document.addEventListener('DOMContentLoaded', function() {
}) })
.then(function(data) { .then(function(data) {
if (data.status === 'success') { if (data.status === 'success') {
// Update the row's data-active and re-apply filter
row.dataset.active = isChecked ? 'true' : 'false'; row.dataset.active = isChecked ? 'true' : 'false';
applyFilter(); applyFilter();
} else { } else {
@ -460,7 +459,7 @@ document.addEventListener('DOMContentLoaded', function() {
alert('Error updating status.'); alert('Error updating status.');
} }
}) })
.catch(function(error) { .catch(function() {
switchEl.checked = !isChecked; switchEl.checked = !isChecked;
alert('Error updating status.'); alert('Error updating status.');
}); });

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% block title %}Work History | Fox Fitt{% endblock %} {% block title %}Work History | FoxFitt{% endblock %}
{% block content %} {% block content %}
<!-- === WORK HISTORY PAGE === <!-- === WORK HISTORY PAGE ===
@ -13,9 +13,9 @@
<div class="container py-4"> <div class="container py-4">
{# === PAGE HEADER with view toggle and export === #} {# === PAGE HEADER with view toggle and export === #}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<h1 class="h3 mb-0" style="font-family: 'Poppins', sans-serif;">Work History</h1> <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"> <div class="d-flex gap-2 flex-wrap">
{# View toggle — List vs Calendar #} {# View toggle — List vs Calendar #}
<div class="btn-group" role="group" aria-label="View mode"> <div class="btn-group" role="group" aria-label="View mode">
<a href="?view=list{{ filter_params }}" <a href="?view=list{{ filter_params }}"
@ -27,59 +27,48 @@
<i class="fas fa-calendar-alt me-1"></i> Calendar <i class="fas fa-calendar-alt me-1"></i> Calendar
</a> </a>
</div> </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 }}" <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 <i class="fas fa-file-csv me-1"></i> Export CSV
</a> </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 <i class="fas fa-arrow-left fa-sm me-1"></i> Back
</a> </a>
</div> </div>
</div> </div>
{# === FILTER BAR === #} {# === 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"> <div class="card-body py-3">
<form method="GET" action="{% url 'work_history' %}" class="row g-2 align-items-end"> <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 }}"> <input type="hidden" name="view" value="{{ view_mode }}">
{% if view_mode == 'calendar' %} {% if view_mode == 'calendar' %}
{# Preserve current calendar month when filtering #}
<input type="hidden" name="year" value="{{ curr_year }}"> <input type="hidden" name="year" value="{{ curr_year }}">
<input type="hidden" name="month" value="{{ curr_month }}"> <input type="hidden" name="month" value="{{ curr_month }}">
{% endif %} {% endif %}
{# Filter by Worker #}
<div class="col-md-3"> <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"> <select name="worker" class="form-select form-select-sm">
<option value="">All Workers</option> <option value="">All Workers</option>
{% for w in filter_workers %} {% for w in filter_workers %}
<option value="{{ w.id }}" {% if selected_worker == w.id|stringformat:"d" %}selected{% endif %}> <option value="{{ w.id }}" {% if selected_worker == w.id|stringformat:"d" %}selected{% endif %}>{{ w.name }}</option>
{{ w.name }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
{# Filter by Project #}
<div class="col-md-3"> <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"> <select name="project" class="form-select form-select-sm">
<option value="">All Projects</option> <option value="">All Projects</option>
{% for p in filter_projects %} {% for p in filter_projects %}
<option value="{{ p.id }}" {% if selected_project == p.id|stringformat:"d" %}selected{% endif %}> <option value="{{ p.id }}" {% if selected_project == p.id|stringformat:"d" %}selected{% endif %}>{{ p.name }}</option>
{{ p.name }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
{# Filter by Payment Status #}
<div class="col-md-3"> <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"> <select name="status" class="form-select form-select-sm">
<option value="" {% if not selected_status %}selected{% endif %}>All</option> <option value="" {% if not selected_status %}selected{% endif %}>All</option>
<option value="paid" {% if selected_status == 'paid' %}selected{% endif %}>Paid</option> <option value="paid" {% if selected_status == 'paid' %}selected{% endif %}>Paid</option>
@ -87,7 +76,6 @@
</select> </select>
</div> </div>
{# Filter + Clear Buttons #}
<div class="col-md-3 d-flex gap-2"> <div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-sm btn-accent"> <button type="submit" class="btn btn-sm btn-accent">
<i class="fas fa-filter me-1"></i> Filter <i class="fas fa-filter me-1"></i> Filter
@ -100,31 +88,28 @@
</div> </div>
</form> </form>
{# === Active Filter Feedback === #} {# Active filter feedback #}
{# Shows a results counter when filters are active so the user can see the filter is working #}
{% if has_active_filters %} {% if has_active_filters %}
<div class="mt-2 d-flex align-items-center flex-wrap gap-2"> <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> <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 }} Showing <strong>{{ filtered_log_count }}</strong> of {{ total_log_count }} work log{{ total_log_count|pluralize }}
</small> </small>
{# Show which filters are active as small badges #}
{% if selected_worker %} {% 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> <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 %} {% for w in filter_workers %}{% if w.id|stringformat:"d" == selected_worker %}{{ w.name }}{% endif %}{% endfor %}
</span> </span>
{% endif %} {% endif %}
{% if selected_project %} {% 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> <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 %} {% for p in filter_projects %}{% if p.id|stringformat:"d" == selected_project %}{{ p.name }}{% endif %}{% endfor %}
</span> </span>
{% endif %} {% endif %}
{% if selected_status %} {% if selected_status %}
<span class="badge bg-warning bg-opacity-10 text-dark border border-warning border-opacity-25"> <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> <i class="fas fa-tag fa-xs me-1"></i>{{ selected_status|capfirst }}
{{ selected_status|capfirst }}
</span> </span>
{% endif %} {% endif %}
</div> </div>
@ -138,17 +123,15 @@
{# === CALENDAR VIEW === #} {# === CALENDAR VIEW === #}
{# =============================================================== #} {# =============================================================== #}
{# Month navigation header #} {# Month navigation #}
<div class="card shadow-sm border-0 mb-3"> <div class="card mb-3">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<a href="?view=calendar&year={{ prev_year }}&month={{ prev_month }}{{ filter_params }}" <a href="?view=calendar&year={{ prev_year }}&month={{ prev_month }}{{ filter_params }}"
class="btn btn-sm btn-outline-secondary"> class="btn btn-sm btn-outline-secondary">
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
</a> </a>
<h5 class="mb-0 fw-bold" style="font-family: 'Poppins', sans-serif;"> <h5 class="mb-0 fw-bold" style="font-family: 'Poppins', sans-serif;">{{ month_name }}</h5>
{{ month_name }}
</h5>
<a href="?view=calendar&year={{ next_year }}&month={{ next_month }}{{ filter_params }}" <a href="?view=calendar&year={{ next_year }}&month={{ next_month }}{{ filter_params }}"
class="btn btn-sm btn-outline-secondary"> class="btn btn-sm btn-outline-secondary">
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
@ -158,10 +141,10 @@
</div> </div>
{# Calendar grid #} {# 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"> <div class="card-body p-0 p-md-3">
{# Day-of-week header row #} {# Day-of-week header #}
<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;"> <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">Mon</div>
<div class="col">Tue</div> <div class="col">Tue</div>
<div class="col">Wed</div> <div class="col">Wed</div>
@ -171,36 +154,32 @@
<div class="col">Sun</div> <div class="col">Sun</div>
</div> </div>
{# Calendar weeks — each row is 7 day cells #}
{% for week in calendar_weeks %} {% for week in calendar_weeks %}
<div class="row g-0 g-md-1 mb-0 mb-md-1"> <div class="row g-0 g-md-1 mb-0 mb-md-1">
{% for day in week %} {% 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 %}" <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 %}> {% 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"> <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 %} {% 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 %} {% endif %}
</div> </div>
{# Mini log indicators (show first 3 entries) #}
{% for log in day.records|slice:":3" %} {% for log in day.records|slice:":3" %}
<div class="cal-entry text-truncate" title="{{ log.project.name }}"> <div class="cal-entry text-truncate" title="{{ log.project.name }}">
<small> <small>
{% if log.payroll_records.exists %} {% 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 %} {% 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 %} {% endif %}
{{ log.project.name }} {{ log.project.name }}
</small> </small>
</div> </div>
{% endfor %} {% endfor %}
{# "and X more" indicator #}
{% if day.count > 3 %} {% if day.count > 3 %}
<div class="cal-entry"> <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> </div>
{% endif %} {% endif %}
</div> </div>
@ -211,62 +190,49 @@
</div> </div>
{# === Day Detail Panel === #} {# === Day Detail Panel === #}
{# Hidden by default. Click day cells to select them — shows combined details with totals. #} <div class="card d-none" id="dayDetailPanel">
<div class="card shadow-sm border-0 d-none" id="dayDetailPanel"> <div class="card-header py-2">
<div class="card-header py-2 bg-white">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold" id="dayDetailTitle"> <h6 class="mb-0 fw-bold" id="dayDetailTitle">
<i class="fas fa-calendar-day me-2"></i>Details <i class="fas fa-calendar-day me-2"></i>Details
</h6> </h6>
<div class="d-flex gap-2 align-items-center"> <div class="d-flex gap-2 align-items-center">
<span class="badge bg-primary rounded-pill d-none" id="daySelectionCount"></span> <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" <button type="button" class="btn btn-sm btn-outline-secondary" id="clearDaySelection" title="Clear selection">
title="Clear selection">
<i class="fas fa-times-circle me-1"></i> Clear <i class="fas fa-times-circle me-1"></i> Clear
</button> </button>
</div> </div>
</div> </div>
{# Hint text for multi-select #} <small class="d-block mt-1" style="color: var(--text-tertiary);" id="multiSelectHint">
<small class="text-muted d-block mt-1" id="multiSelectHint">
<i class="fas fa-info-circle me-1"></i>Click more days to add them to the selection <i class="fas fa-info-circle me-1"></i>Click more days to add them to the selection
</small> </small>
</div> </div>
<div class="card-body p-0" id="dayDetailBody"> <div class="card-body p-0" id="dayDetailBody"></div>
{# Content built by JavaScript #}
</div>
{# === Totals Footer (admin only, shown when days are selected) === #}
{% if is_admin %} {% 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 class="d-flex justify-content-between align-items-center">
<div> <div>
<strong>Total:</strong> <strong>Total:</strong>
<span class="text-muted ms-2" id="totalDays">0 days</span> <span class="ms-2" style="color: var(--text-secondary);" id="totalDays">0 days</span>
<span class="text-muted mx-1">·</span> <span class="mx-1" style="color: var(--text-tertiary);">&middot;</span>
<span class="text-muted" id="totalLogs">0 logs</span> <span style="color: var(--text-secondary);" id="totalLogs">0 logs</span>
<span class="text-muted mx-1">·</span> <span class="mx-1" style="color: var(--text-tertiary);">&middot;</span>
<span class="text-muted" id="totalWorkers">0 unique workers</span> <span style="color: var(--text-secondary);" id="totalWorkers">0 unique workers</span>
</div> </div>
<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> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{# Pass calendar detail data to JavaScript safely using json_script #}
{{ calendar_detail|json_script:"calDetailJson" }} {{ calendar_detail|json_script:"calDetailJson" }}
<script> <script>
(function() { (function() {
'use strict'; '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 calDetail = JSON.parse(document.getElementById('calDetailJson').textContent);
var detailPanel = document.getElementById('dayDetailPanel'); var detailPanel = document.getElementById('dayDetailPanel');
var detailTitle = document.getElementById('dayDetailTitle'); var detailTitle = document.getElementById('dayDetailTitle');
@ -276,80 +242,49 @@
var multiSelectHint = document.getElementById('multiSelectHint'); var multiSelectHint = document.getElementById('multiSelectHint');
var isAdmin = {{ is_admin|yesno:"true,false" }}; var isAdmin = {{ is_admin|yesno:"true,false" }};
var detailFooter = document.getElementById('dayDetailFooter'); var detailFooter = document.getElementById('dayDetailFooter');
// Track which dates are currently selected (array of date strings)
var selectedDates = []; var selectedDates = [];
// Short month names for formatting dates
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; 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) { function formatDateShort(dateStr) {
var parts = dateStr.split('-'); var parts = dateStr.split('-');
var day = parseInt(parts[2], 10); return parseInt(parts[2], 10) + ' ' + months[parseInt(parts[1], 10) - 1];
var monthIdx = parseInt(parts[1], 10) - 1;
return day + ' ' + months[monthIdx];
} }
// === Format a date string for longer display (e.g. "22 Feb 2026") ===
function formatDateLong(dateStr) { function formatDateLong(dateStr) {
var parts = dateStr.split('-'); var parts = dateStr.split('-');
var day = parseInt(parts[2], 10); return parseInt(parts[2], 10) + ' ' + months[parseInt(parts[1], 10) - 1] + ' ' + parts[0];
var monthIdx = parseInt(parts[1], 10) - 1;
return day + ' ' + months[monthIdx] + ' ' + parts[0];
} }
// === Update the detail panel with data from all selected dates ===
function updateDetailPanel() { function updateDetailPanel() {
if (selectedDates.length === 0) { if (selectedDates.length === 0) {
// Nothing selected — hide the panel
detailPanel.classList.add('d-none'); detailPanel.classList.add('d-none');
return; return;
} }
// Sort selected dates chronologically
selectedDates.sort(); selectedDates.sort();
// Collect all entries from all selected dates
var allEntries = []; var allEntries = [];
var totalAmount = 0; var totalAmount = 0;
var uniqueWorkers = {}; var uniqueWorkers = {};
selectedDates.forEach(function(dateStr) { selectedDates.forEach(function(dateStr) {
var entries = calDetail[dateStr] || []; (calDetail[dateStr] || []).forEach(function(entry) {
entries.forEach(function(entry) {
// Tag each entry with its date for display
allEntries.push({ date: dateStr, entry: entry }); allEntries.push({ date: dateStr, entry: entry });
// Track unique workers entry.workers.forEach(function(w) { uniqueWorkers[w] = true; });
entry.workers.forEach(function(w) { if (isAdmin && entry.amount !== undefined) totalAmount += entry.amount;
uniqueWorkers[w] = true;
});
// Sum amounts (admin only)
if (isAdmin && entry.amount !== undefined) {
totalAmount += entry.amount;
}
}); });
}); });
// === Update panel title === // Title
detailTitle.textContent = ''; detailTitle.textContent = '';
var icon = document.createElement('i'); var icon = document.createElement('i');
icon.className = 'fas fa-calendar-day me-2'; icon.className = 'fas fa-calendar-day me-2';
detailTitle.appendChild(icon); detailTitle.appendChild(icon);
if (selectedDates.length === 1) { if (selectedDates.length === 1) {
// Single day: show full date detailTitle.appendChild(document.createTextNode(formatDateLong(selectedDates[0]) + ' \u2014 ' + allEntries.length + ' log(s)'));
detailTitle.appendChild(document.createTextNode(
formatDateLong(selectedDates[0]) + ' — ' + allEntries.length + ' log(s)'
));
} else { } else {
// Multiple days: show date range or count detailTitle.appendChild(document.createTextNode(selectedDates.length + ' days selected \u2014 ' + allEntries.length + ' log(s)'));
detailTitle.appendChild(document.createTextNode(
selectedDates.length + ' days selected — ' + allEntries.length + ' log(s)'
));
} }
// Update selection count badge
if (selectedDates.length > 1) { if (selectedDates.length > 1) {
selCountBadge.textContent = selectedDates.length + ' days'; selCountBadge.textContent = selectedDates.length + ' days';
selCountBadge.classList.remove('d-none'); selCountBadge.classList.remove('d-none');
@ -359,17 +294,13 @@
multiSelectHint.classList.remove('d-none'); multiSelectHint.classList.remove('d-none');
} }
// === Clear previous content === // Build table
while (detailBody.firstChild) detailBody.removeChild(detailBody.firstChild); while (detailBody.firstChild) detailBody.removeChild(detailBody.firstChild);
// === Build detail table ===
var table = document.createElement('table'); var table = document.createElement('table');
table.className = 'table table-sm table-hover mb-0'; table.className = 'table table-sm table-hover mb-0';
var thead = document.createElement('thead'); var thead = document.createElement('thead');
thead.className = 'table-light';
var headRow = document.createElement('tr'); var headRow = document.createElement('tr');
// Show Date column when multiple days are selected
var headers = selectedDates.length > 1 var headers = selectedDates.length > 1
? ['Date', 'Project', 'Workers', 'Supervisor', 'OT', 'Status'] ? ['Date', 'Project', 'Workers', 'Supervisor', 'OT', 'Status']
: ['Project', 'Workers', 'Supervisor', 'OT', 'Status']; : ['Project', 'Workers', 'Supervisor', 'OT', 'Status'];
@ -388,7 +319,6 @@
var entry = item.entry; var entry = item.entry;
var tr = document.createElement('tr'); var tr = document.createElement('tr');
// Date column (only for multi-day selection)
if (selectedDates.length > 1) { if (selectedDates.length > 1) {
var tdDate = document.createElement('td'); var tdDate = document.createElement('td');
tdDate.className = 'ps-3'; tdDate.className = 'ps-3';
@ -396,7 +326,6 @@
tr.appendChild(tdDate); tr.appendChild(tdDate);
} }
// Project
var tdProj = document.createElement('td'); var tdProj = document.createElement('td');
tdProj.className = selectedDates.length === 1 ? 'ps-3' : ''; tdProj.className = selectedDates.length === 1 ? 'ps-3' : '';
var strong = document.createElement('strong'); var strong = document.createElement('strong');
@ -404,53 +333,43 @@
tdProj.appendChild(strong); tdProj.appendChild(strong);
tr.appendChild(tdProj); tr.appendChild(tdProj);
// Workers — each name gets a small pill badge for readability
var tdWork = document.createElement('td'); var tdWork = document.createElement('td');
entry.workers.forEach(function(name) { entry.workers.forEach(function(name) {
var pill = document.createElement('span'); 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; pill.textContent = name;
tdWork.appendChild(pill); tdWork.appendChild(pill);
}); });
tr.appendChild(tdWork); tr.appendChild(tdWork);
// Supervisor
var tdSup = document.createElement('td'); var tdSup = document.createElement('td');
tdSup.textContent = entry.supervisor; tdSup.textContent = entry.supervisor;
tr.appendChild(tdSup); tr.appendChild(tdSup);
// Overtime
var tdOt = document.createElement('td'); var tdOt = document.createElement('td');
if (entry.overtime) { if (entry.overtime) {
var otBadge = document.createElement('span'); 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; otBadge.textContent = entry.overtime;
tdOt.appendChild(otBadge); tdOt.appendChild(otBadge);
} else { } else {
tdOt.textContent = '-'; tdOt.textContent = '-';
tdOt.className = 'text-muted'; tdOt.style.color = 'var(--text-tertiary)';
} }
tr.appendChild(tdOt); tr.appendChild(tdOt);
// Status
var tdStatus = document.createElement('td'); var tdStatus = document.createElement('td');
var statusBadge = document.createElement('span'); var statusBadge = document.createElement('span');
if (entry.is_paid) { statusBadge.className = 'badge ' + (entry.is_paid ? 'bg-success' : 'bg-danger bg-opacity-75');
statusBadge.className = 'badge bg-success'; statusBadge.textContent = entry.is_paid ? 'Paid' : 'Unpaid';
statusBadge.textContent = 'Paid';
} else {
statusBadge.className = 'badge bg-danger bg-opacity-75';
statusBadge.textContent = 'Unpaid';
}
tdStatus.appendChild(statusBadge); tdStatus.appendChild(statusBadge);
tr.appendChild(tdStatus); tr.appendChild(tdStatus);
// Amount (admin only)
if (isAdmin) { if (isAdmin) {
var tdAmt = document.createElement('td'); var tdAmt = document.createElement('td');
tdAmt.textContent = entry.amount !== undefined tdAmt.textContent = entry.amount !== undefined ? 'R ' + entry.amount.toFixed(2) : '-';
? 'R ' + entry.amount.toFixed(2)
: '-';
tr.appendChild(tdAmt); tr.appendChild(tdAmt);
} }
@ -459,137 +378,55 @@
table.appendChild(tbody); table.appendChild(tbody);
detailBody.appendChild(table); detailBody.appendChild(table);
// === Update totals footer (admin only) === // Totals footer
if (isAdmin && detailFooter) { 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; var uniqueCount = Object.keys(uniqueWorkers).length;
document.getElementById('totalDays').textContent = selectedDates.length + ' day' + (selectedDates.length !== 1 ? 's' : '');
totalDaysEl.textContent = selectedDates.length + ' day' + (selectedDates.length !== 1 ? 's' : ''); document.getElementById('totalLogs').textContent = allEntries.length + ' log' + (allEntries.length !== 1 ? 's' : '');
totalLogsEl.textContent = allEntries.length + ' log' + (allEntries.length !== 1 ? 's' : ''); document.getElementById('totalWorkers').textContent = uniqueCount + ' unique worker' + (uniqueCount !== 1 ? 's' : '');
totalWorkersEl.textContent = uniqueCount + ' unique worker' + (uniqueCount !== 1 ? 's' : ''); document.getElementById('totalAmount').textContent = 'R ' + totalAmount.toFixed(2);
totalAmountEl.textContent = 'R ' + totalAmount.toFixed(2);
detailFooter.classList.remove('d-none'); detailFooter.classList.remove('d-none');
} }
// Show the panel and scroll to it
detailPanel.classList.remove('d-none'); detailPanel.classList.remove('d-none');
detailPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 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) { document.querySelectorAll('.cal-day--has-logs').forEach(function(cell) {
cell.addEventListener('click', function() { cell.addEventListener('click', function() {
var dateStr = this.dataset.date; var dateStr = this.dataset.date;
var entries = calDetail[dateStr] || []; if (!(calDetail[dateStr] || []).length) return;
if (entries.length === 0) return;
// Toggle this date in the selection
var idx = selectedDates.indexOf(dateStr); var idx = selectedDates.indexOf(dateStr);
if (idx !== -1) { if (idx !== -1) {
// Already selected — remove it
selectedDates.splice(idx, 1); selectedDates.splice(idx, 1);
this.classList.remove('cal-day--selected'); this.classList.remove('cal-day--selected');
} else { } else {
// Not selected — add it
selectedDates.push(dateStr); selectedDates.push(dateStr);
this.classList.add('cal-day--selected'); this.classList.add('cal-day--selected');
} }
// Refresh the detail panel with the updated selection
updateDetailPanel(); updateDetailPanel();
}); });
}); });
// === Clear all selections ===
clearBtn.addEventListener('click', function() { clearBtn.addEventListener('click', function() {
selectedDates = []; selectedDates = [];
document.querySelectorAll('.cal-day--selected').forEach(function(c) { document.querySelectorAll('.cal-day--selected').forEach(function(c) { c.classList.remove('cal-day--selected'); });
c.classList.remove('cal-day--selected');
});
detailPanel.classList.add('d-none'); detailPanel.classList.add('d-none');
if (detailFooter) detailFooter.classList.add('d-none'); if (detailFooter) detailFooter.classList.add('d-none');
}); });
})(); })();
</script> </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 %} {% else %}
{# =============================================================== #} {# =============================================================== #}
{# === LIST VIEW (TABLE) === #} {# === LIST VIEW (TABLE) === #}
{# =============================================================== #} {# =============================================================== #}
<div class="card shadow-sm border-0"> <div class="card">
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover mb-0"> <table class="table table-hover mb-0">
<thead class="table-light"> <thead>
<tr> <tr>
<th scope="col" class="ps-4">Date</th> <th scope="col" class="ps-4">Date</th>
<th scope="col">Project</th> <th scope="col">Project</th>
@ -606,25 +443,23 @@
<td class="ps-4 align-middle">{{ log.date }}</td> <td class="ps-4 align-middle">{{ log.date }}</td>
<td class="align-middle"><strong>{{ log.project.name }}</strong></td> <td class="align-middle"><strong>{{ log.project.name }}</strong></td>
<td class="align-middle"> <td class="align-middle">
{# When filtering by a specific worker, show only that worker. Otherwise show all workers. #}
{% if filtered_worker_obj %} {% 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 %} {% else %}
{% for w in log.workers.all %} {% 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 %} {% 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 %} {% endif %}
</td> </td>
<td class="align-middle"> <td class="align-middle">
{% if log.overtime_amount > 0 %} {% 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 %} {% else %}
<span class="text-muted">-</span> <span style="color: var(--text-tertiary);">-</span>
{% endif %} {% endif %}
</td> </td>
<td class="align-middle"> <td class="align-middle">
{# Payment status — a WorkLog is "paid" if it has at least one PayrollRecord #}
{% if log.payroll_records.exists %} {% if log.payroll_records.exists %}
<span class="badge bg-success"><i class="fas fa-check me-1"></i>Paid</span> <span class="badge bg-success"><i class="fas fa-check me-1"></i>Paid</span>
{% else %} {% else %}
@ -633,11 +468,10 @@
</td> </td>
{% if is_admin %} {% if is_admin %}
<td class="align-middle"> <td class="align-middle">
{# Daily cost — worker's rate when filtered, otherwise total for all workers #}
{% if filtered_worker_obj %} {% 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 %} {% 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 %} {% endif %}
</td> </td>
{% endif %} {% endif %}
@ -645,14 +479,14 @@
{% if log.supervisor %} {% if log.supervisor %}
{{ log.supervisor.get_full_name|default:log.supervisor.username }} {{ log.supervisor.get_full_name|default:log.supervisor.username }}
{% else %} {% else %}
<span class="text-muted">-</span> <span style="color: var(--text-tertiary);">-</span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="{% if is_admin %}7{% else %}6{% endif %}" class="text-center py-5 text-muted"> <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 opacity-50"></i> <i class="fas fa-inbox fa-2x mb-3 d-block"></i>
No work history found. No work history found.
{% if selected_worker or selected_project or selected_status %} {% if selected_worker or selected_project or selected_status %}
<br><small>Try adjusting your filters.</small> <br><small>Try adjusting your filters.</small>

View File

@ -1,32 +1,92 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block title %}Login | FoxFitt{% endblock %}
<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 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 %} {% 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. Your username and password didn't match. Please try again.
</div> </div>
{% endif %} {% endif %}
<!-- Login form -->
<form method="post" action="{% url 'login' %}"> <form method="post" action="{% url 'login' %}">
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
<label for="id_username" class="form-label" style="font-family: 'Inter', sans-serif; font-weight: 500;">Username</label> <label for="id_username" class="form-label">Username</label>
<input type="text" name="username" class="form-control form-control-lg" id="id_username" required autofocus style="border-radius: 8px;"> <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>
<div class="mb-4"> <div class="mb-4">
<label for="id_password" class="form-label" style="font-family: 'Inter', sans-serif; font-weight: 500;">Password</label> <label for="id_password" class="form-label">Password</label>
<input type="password" name="password" class="form-control form-control-lg" id="id_password" required style="border-radius: 8px;"> <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> </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> </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> </div>
</div> </div>
{% endblock %} {% 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