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:
parent
a1ac8540ab
commit
82c1906607
@ -1,10 +1,21 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}FoxFitt{% endblock %}</title>
|
||||
|
||||
<!-- === THEME: Apply saved preference BEFORE first paint (prevents flash) === -->
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('foxfitt-theme');
|
||||
if (saved === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Bootstrap 5.3 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<!-- Google Fonts -->
|
||||
@ -13,112 +24,221 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Font Awesome 6 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<!-- Custom CSS (cache-busted with deployment timestamp) -->
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ request.timestamp|default:'1.0' }}">
|
||||
<style>
|
||||
/* Layout helpers — keep body full-height so footer sticks to bottom */
|
||||
body { display: flex; flex-direction: column; min-height: 100vh; }
|
||||
main { flex-grow: 1; }
|
||||
/* Branding — Fox in green, Fitt in white */
|
||||
.navbar-brand-fox { color: #10b981; font-weight: 700; }
|
||||
.navbar-brand-fitt { color: #ffffff; font-weight: 700; }
|
||||
.nav-link { font-weight: 500; }
|
||||
.dropdown-menu { border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
|
||||
</style>
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark sticky-top shadow-sm">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{% url 'home' %}">
|
||||
<span class="navbar-brand-fox">Fox</span>
|
||||
<span class="navbar-brand-fitt">Fitt</span>
|
||||
</a>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'home' %}active{% endif %}" href="{% url 'home' %}">
|
||||
<i class="fas fa-home me-1"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'attendance_log' %}active{% endif %}" href="{% url 'attendance_log' %}">
|
||||
<i class="fas fa-clipboard-list me-1"></i> Log Work
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'work_history' %}active{% endif %}" href="{% url 'work_history' %}">
|
||||
<i class="fas fa-clock me-1"></i> Work History
|
||||
</a>
|
||||
</li>
|
||||
{% if user.is_staff %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'payroll_dashboard' %}active{% endif %}" href="{% url 'payroll_dashboard' %}">
|
||||
<i class="fas fa-wallet me-1"></i> Payroll
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'create_receipt' %}active{% endif %}" href="{% url 'create_receipt' %}">
|
||||
<i class="fas fa-receipt me-1"></i> Receipts
|
||||
</a>
|
||||
</li>
|
||||
{% if user.is_staff %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:index' %}">
|
||||
<i class="fas fa-cog me-1"></i> Admin
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item d-flex align-items-center">
|
||||
<span class="nav-link text-light pe-2">
|
||||
<i class="fas fa-user-circle me-1"></i> {{ user.username }}
|
||||
</span>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="fas fa-sign-out-alt me-1"></i> Logout
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<!-- Messages Block -->
|
||||
{% if user.is_authenticated %}
|
||||
<!-- ===================================================================
|
||||
APP LAYOUT — sidebar (desktop) + top bar (mobile) + content
|
||||
=================================================================== -->
|
||||
<div class="app-layout">
|
||||
|
||||
<!-- === SIDEBAR (desktop only, hidden on mobile via CSS) === -->
|
||||
<aside class="app-sidebar d-print-none">
|
||||
|
||||
<!-- Brand / Logo -->
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand__icon">
|
||||
<i class="fas fa-bolt"></i>
|
||||
</div>
|
||||
<a href="{% url 'home' %}" class="sidebar-brand__text">
|
||||
<span>Fox</span>Fitt
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<nav class="sidebar-nav">
|
||||
<a href="{% url 'home' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'home' %}active{% endif %}">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="{% url 'attendance_log' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'attendance_log' %}active{% endif %}">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
<span>Log Work</span>
|
||||
</a>
|
||||
<a href="{% url 'work_history' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'work_history' %}active{% endif %}">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>History</span>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'payroll_dashboard' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'payroll_dashboard' %}active{% endif %}">
|
||||
<i class="fas fa-wallet"></i>
|
||||
<span>Payroll</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'create_receipt' %}" class="sidebar-nav__link {% if request.resolver_match.url_name == 'create_receipt' %}active{% endif %}">
|
||||
<i class="fas fa-receipt"></i>
|
||||
<span>Receipts</span>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'admin:index' %}" class="sidebar-nav__link">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Admin</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<!-- Sidebar Footer: theme toggle + user -->
|
||||
<div class="sidebar-footer">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<button type="button" class="theme-toggle" id="themeToggle" title="Toggle dark/light mode">
|
||||
<i class="fas fa-moon" id="themeIcon"></i>
|
||||
</button>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="theme-toggle" title="Sign out" style="color: var(--color-danger);">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user__avatar">
|
||||
{{ user.username|make_list|first|upper }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="sidebar-user__name">{{ user.first_name|default:user.username }}</div>
|
||||
<div class="sidebar-user__role">{% if user.is_staff %}Administrator{% else %}Supervisor{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- === MAIN CONTENT AREA === -->
|
||||
<div class="app-main">
|
||||
|
||||
<!-- === TOP BAR (mobile only, hidden on desktop via CSS) === -->
|
||||
<div class="app-topbar d-print-none">
|
||||
<a href="{% url 'home' %}" style="text-decoration: none; font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 1.2rem;">
|
||||
<span style="color: var(--accent);">Fox</span><span style="color: var(--text-on-nav);">Fitt</span>
|
||||
</a>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button type="button" class="theme-toggle" id="themeToggleMobile" title="Toggle dark/light mode">
|
||||
<i class="fas fa-moon" id="themeIconMobile"></i>
|
||||
</button>
|
||||
<form method="post" action="{% url 'logout' %}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="theme-toggle" title="Sign out" style="color: var(--color-danger);">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === Flash messages (Django messages framework) === -->
|
||||
{% if messages %}
|
||||
<div class="container-fluid px-3 px-lg-4 mt-3">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show shadow-sm" role="alert">
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{% if message.tags == 'success' %}<i class="fas fa-check-circle me-2"></i>
|
||||
{% elif message.tags == 'error' or message.tags == 'danger' %}<i class="fas fa-exclamation-circle me-2"></i>
|
||||
{% elif message.tags == 'warning' %}<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{% elif message.tags == 'info' %}<i class="fas fa-info-circle me-2"></i>
|
||||
{% endif %}
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="py-4 mt-auto border-top border-secondary">
|
||||
<div class="container text-center">
|
||||
<p class="mb-0 small">© {% now "Y" %} FoxFitt Construction. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bootstrap 5.3 JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
<!-- === Page Content === -->
|
||||
<div class="app-content">
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
</div>
|
||||
|
||||
<!-- === Footer (inside main area) === -->
|
||||
<footer class="app-footer d-print-none">
|
||||
<div class="container">
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-center">
|
||||
<p class="mb-1 mb-sm-0">
|
||||
<span style="color: var(--accent); font-weight: 600;">Fox</span><span style="font-weight: 600;">Fitt</span>
|
||||
<span class="ms-1">Construction</span>
|
||||
</p>
|
||||
<p class="mb-0">© {% now "Y" %} All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- === BOTTOM TAB BAR (mobile only, hidden on desktop via CSS) === -->
|
||||
<nav class="app-bottom-nav d-print-none">
|
||||
<div class="bottom-nav-inner">
|
||||
<a href="{% url 'home' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'home' %}active{% endif %}">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="{% url 'attendance_log' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'attendance_log' %}active{% endif %}">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
<span>Log Work</span>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'payroll_dashboard' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'payroll_dashboard' %}active{% endif %}">
|
||||
<i class="fas fa-wallet"></i>
|
||||
<span>Payroll</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'work_history' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'work_history' %}active{% endif %}">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>History</span>
|
||||
</a>
|
||||
<a href="{% url 'create_receipt' %}" class="bottom-nav__link {% if request.resolver_match.url_name == 'create_receipt' %}active{% endif %}">
|
||||
<i class="fas fa-receipt"></i>
|
||||
<span>Receipts</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bootstrap 5.3 JS Bundle (includes Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- === THEME TOGGLE — switches dark/light and persists to localStorage === -->
|
||||
<script>
|
||||
(function() {
|
||||
// Both desktop sidebar and mobile top bar toggle buttons
|
||||
var toggles = [
|
||||
{ btn: document.getElementById('themeToggle'), icon: document.getElementById('themeIcon') },
|
||||
{ btn: document.getElementById('themeToggleMobile'), icon: document.getElementById('themeIconMobile') }
|
||||
];
|
||||
|
||||
function updateIcons() {
|
||||
var isDark = document.documentElement.getAttribute('data-theme') !== 'light';
|
||||
toggles.forEach(function(t) {
|
||||
if (t.icon) {
|
||||
t.icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
if (t.btn) {
|
||||
t.btn.title = isDark ? 'Switch to light mode' : 'Switch to dark mode';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateIcons();
|
||||
|
||||
toggles.forEach(function(t) {
|
||||
if (t.btn) {
|
||||
t.btn.addEventListener('click', function() {
|
||||
var current = document.documentElement.getAttribute('data-theme');
|
||||
var next = (current === 'light') ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('foxfitt-theme', next);
|
||||
updateIcons();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -1,27 +1,29 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Log Work | Fox Fitt{% endblock %}
|
||||
{% block title %}Log Work | FoxFitt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<!-- === Page Header === -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0" style="font-family: 'Poppins', sans-serif;">Log Daily Attendance</h1>
|
||||
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm shadow-sm">
|
||||
<div>
|
||||
<h1 class="page-title"><i class="fas fa-clipboard-list me-2" style="color: var(--accent);"></i>Log Daily Attendance</h1>
|
||||
</div>
|
||||
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left fa-sm me-1"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Main Form Column -->
|
||||
<!-- === Main Form Column === -->
|
||||
<div class="{% if is_admin %}col-lg-8{% else %}col-lg-8 mx-auto{% endif %}">
|
||||
<div class="card shadow-sm border-0" style="border-radius: 12px;">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
|
||||
{# --- Conflict Warning --- #}
|
||||
{# If we found workers already logged on selected dates, show this warning #}
|
||||
{% if conflicts %}
|
||||
<div class="alert alert-warning border-0 shadow-sm mb-4" role="alert">
|
||||
<div class="alert alert-warning mb-4" role="alert">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Conflicts Found
|
||||
</h6>
|
||||
@ -34,15 +36,11 @@
|
||||
<div class="d-flex gap-2">
|
||||
<form method="POST" class="d-inline">
|
||||
{% csrf_token %}
|
||||
{# Re-submit all form data with a conflict_action flag #}
|
||||
{# Non-multi-value fields from form.data #}
|
||||
{% for key, value in form.data.items %}
|
||||
{% if key != 'csrfmiddlewaretoken' and key != 'conflict_action' and key != 'workers' %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{# Workers is a multi-value field — use the explicit list #}
|
||||
{# passed from the view (QueryDict.getlist) to avoid losing values #}
|
||||
{% for wid in selected_worker_ids %}
|
||||
<input type="hidden" name="workers" value="{{ wid }}">
|
||||
{% endfor %}
|
||||
@ -72,7 +70,7 @@
|
||||
|
||||
{# --- Form Errors --- #}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger border-0 shadow-sm mb-4">
|
||||
<div class="alert alert-danger mb-4">
|
||||
<strong><i class="fas fa-exclamation-circle me-1"></i> Please fix the following:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
{% for field, errors in form.errors.items %}
|
||||
@ -87,7 +85,7 @@
|
||||
<form method="POST" id="attendanceForm">
|
||||
{% csrf_token %}
|
||||
|
||||
{# --- Date Range Section --- #}
|
||||
{# --- Date Range --- #}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Start Date</label>
|
||||
@ -95,10 +93,10 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
End Date <span class="text-muted fw-normal">(optional)</span>
|
||||
End Date <span style="color: var(--text-tertiary); font-weight: 400;">(optional)</span>
|
||||
</label>
|
||||
{{ form.end_date }}
|
||||
<small class="text-muted">Leave blank to log a single day</small>
|
||||
<small style="color: var(--text-tertiary);">Leave blank to log a single day</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -126,7 +124,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
Team <span class="text-muted fw-normal">(optional — selects all team workers)</span>
|
||||
Team <span style="color: var(--text-tertiary); font-weight: 400;">(optional — selects all team workers)</span>
|
||||
</label>
|
||||
{{ form.team }}
|
||||
</div>
|
||||
@ -135,7 +133,7 @@
|
||||
{# --- Worker Checkboxes --- #}
|
||||
<div class="mb-4">
|
||||
<label class="form-label d-block mb-3 fw-semibold">Workers Present</label>
|
||||
<div class="worker-selection border rounded p-3" style="max-height: 300px; overflow-y: auto; background-color: #f8fafc; border-color: #e2e8f0 !important;">
|
||||
<div class="worker-selection border rounded p-3" style="max-height: 300px; overflow-y: auto; background-color: var(--bg-inset); border-color: var(--border-default) !important;">
|
||||
<div class="row">
|
||||
{% for worker in form.workers %}
|
||||
<div class="col-md-6 mb-2">
|
||||
@ -165,9 +163,9 @@
|
||||
{{ form.notes }}
|
||||
</div>
|
||||
|
||||
{# --- Submit Button --- #}
|
||||
{# --- Submit --- #}
|
||||
<div class="d-grid mt-5">
|
||||
<button type="submit" class="btn btn-lg btn-accent shadow-sm" style="border-radius: 8px;">
|
||||
<button type="submit" class="btn btn-lg btn-accent">
|
||||
<i class="fas fa-save me-2"></i>Log Work
|
||||
</button>
|
||||
</div>
|
||||
@ -179,22 +177,22 @@
|
||||
{# --- Estimated Cost Card (Admin Only) --- #}
|
||||
{% if is_admin %}
|
||||
<div class="col-lg-4 mt-4 mt-lg-0">
|
||||
<div class="card shadow-sm border-0 sticky-top" style="border-radius: 12px; top: 80px;">
|
||||
<div class="card sticky-top" style="top: 80px;">
|
||||
<div class="card-body p-4">
|
||||
<h6 class="fw-bold mb-3">
|
||||
<i class="fas fa-calculator me-2 text-success"></i>Estimated Cost
|
||||
<i class="fas fa-calculator me-2" style="color: var(--accent);"></i>Estimated Cost
|
||||
</h6>
|
||||
<div class="text-center py-3">
|
||||
<div class="display-6 fw-bold" id="estimatedCost" style="color: var(--accent-color, #10b981);">
|
||||
<div id="estimatedCost" style="font-size: 2rem; font-weight: 700; font-family: 'Poppins', sans-serif; color: var(--accent);">
|
||||
R 0.00
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<small style="color: var(--text-secondary);">
|
||||
<span id="selectedWorkerCount">0</span> worker(s) ×
|
||||
<span id="selectedDayCount">1</span> day(s)
|
||||
</small>
|
||||
</div>
|
||||
<hr>
|
||||
<small class="text-muted">
|
||||
<hr style="border-color: var(--border-default);">
|
||||
<small style="color: var(--text-tertiary);">
|
||||
This estimate is based on each worker's daily rate multiplied by the
|
||||
number of working days selected. Overtime is not included.
|
||||
</small>
|
||||
@ -205,50 +203,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- JavaScript for dynamic features --- #}
|
||||
<!-- === JavaScript: Team auto-select + Cost estimator === -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// === TEAM AUTO-SELECT ===
|
||||
// When a team is chosen from the dropdown, automatically check all workers
|
||||
// that belong to that team. Uses team_workers_json passed from the view.
|
||||
var teamWorkersMap = JSON.parse('{{ team_workers_json|escapejs }}');
|
||||
var teamSelect = document.querySelector('[name="team"]');
|
||||
if (teamSelect) {
|
||||
teamSelect.addEventListener('change', function() {
|
||||
var teamId = this.value;
|
||||
|
||||
// First, uncheck ALL worker checkboxes
|
||||
var allBoxes = document.querySelectorAll('input[name="workers"]');
|
||||
allBoxes.forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
allBoxes.forEach(function(cb) { cb.checked = false; });
|
||||
|
||||
// Then check workers that belong to the selected team
|
||||
if (teamId && teamWorkersMap[teamId]) {
|
||||
var workerIds = teamWorkersMap[teamId];
|
||||
workerIds.forEach(function(id) {
|
||||
teamWorkersMap[teamId].forEach(function(id) {
|
||||
var checkbox = document.querySelector('input[name="workers"][value="' + id + '"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
if (checkbox) checkbox.checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Recalculate estimated cost if the admin cost calculator exists
|
||||
if (typeof updateEstimatedCost === 'function') {
|
||||
updateEstimatedCost();
|
||||
}
|
||||
if (typeof updateEstimatedCost === 'function') updateEstimatedCost();
|
||||
});
|
||||
}
|
||||
|
||||
{% if is_admin %}
|
||||
// === ESTIMATED COST CALCULATOR (Admin Only) ===
|
||||
// Updates the cost card in real-time as workers and dates are selected.
|
||||
|
||||
// Worker daily rates passed from the view
|
||||
const workerRates = {{ worker_rates_json|safe }};
|
||||
|
||||
const startDateInput = document.querySelector('[name="date"]');
|
||||
const endDateInput = document.querySelector('[name="end_date"]');
|
||||
const satCheckbox = document.querySelector('[name="include_saturday"]');
|
||||
@ -259,26 +240,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const dayCountDisplay = document.getElementById('selectedDayCount');
|
||||
|
||||
function countWorkingDays() {
|
||||
// Count how many working days are in the selected date range
|
||||
const startDate = startDateInput ? new Date(startDateInput.value) : null;
|
||||
const endDateVal = endDateInput ? endDateInput.value : '';
|
||||
const endDate = endDateVal ? new Date(endDateVal) : startDate;
|
||||
|
||||
if (!startDate || isNaN(startDate)) return 1;
|
||||
if (!endDate || isNaN(endDate)) return 1;
|
||||
|
||||
let count = 0;
|
||||
let current = new Date(startDate);
|
||||
while (current <= endDate) {
|
||||
const day = current.getDay(); // 0=Sun, 6=Sat
|
||||
if (day === 6 && !(satCheckbox && satCheckbox.checked)) {
|
||||
current.setDate(current.getDate() + 1);
|
||||
continue;
|
||||
}
|
||||
if (day === 0 && !(sunCheckbox && sunCheckbox.checked)) {
|
||||
current.setDate(current.getDate() + 1);
|
||||
continue;
|
||||
}
|
||||
const day = current.getDay();
|
||||
if (day === 6 && !(satCheckbox && satCheckbox.checked)) { current.setDate(current.getDate() + 1); continue; }
|
||||
if (day === 0 && !(sunCheckbox && sunCheckbox.checked)) { current.setDate(current.getDate() + 1); continue; }
|
||||
count++;
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
@ -286,39 +259,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
function updateEstimatedCost() {
|
||||
// Add up daily rates of all checked workers, multiply by number of days
|
||||
let totalDailyRate = 0;
|
||||
let selectedCount = 0;
|
||||
|
||||
workerCheckboxes.forEach(function(cb) {
|
||||
if (cb.checked) {
|
||||
const workerId = cb.value;
|
||||
if (workerRates[workerId]) {
|
||||
totalDailyRate += parseFloat(workerRates[workerId]);
|
||||
}
|
||||
if (workerRates[workerId]) totalDailyRate += parseFloat(workerRates[workerId]);
|
||||
selectedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const days = countWorkingDays();
|
||||
const totalCost = totalDailyRate * days;
|
||||
|
||||
// Update the display
|
||||
if (costDisplay) costDisplay.textContent = 'R ' + totalCost.toLocaleString('en-ZA', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
||||
if (workerCountDisplay) workerCountDisplay.textContent = selectedCount;
|
||||
if (dayCountDisplay) dayCountDisplay.textContent = days;
|
||||
}
|
||||
|
||||
// Listen for changes on all relevant inputs
|
||||
workerCheckboxes.forEach(function(cb) {
|
||||
cb.addEventListener('change', updateEstimatedCost);
|
||||
});
|
||||
workerCheckboxes.forEach(function(cb) { cb.addEventListener('change', updateEstimatedCost); });
|
||||
if (startDateInput) startDateInput.addEventListener('change', updateEstimatedCost);
|
||||
if (endDateInput) endDateInput.addEventListener('change', updateEstimatedCost);
|
||||
if (satCheckbox) satCheckbox.addEventListener('change', updateEstimatedCost);
|
||||
if (sunCheckbox) sunCheckbox.addEventListener('change', updateEstimatedCost);
|
||||
|
||||
// Run once on page load in case of pre-selected values
|
||||
updateEstimatedCost();
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Create Receipt | Fox Fitt{% endblock %}
|
||||
{% block title %}Create Receipt | FoxFitt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- === CREATE EXPENSE RECEIPT ===
|
||||
@ -9,182 +9,164 @@
|
||||
- Live VAT calculation (Included / Excluded / None)
|
||||
- On submit: saves to database + emails HTML + PDF to Spark Receipt -->
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10 col-xl-8">
|
||||
|
||||
<!-- Card header -->
|
||||
<div class="card-header py-3" style="background-color: var(--primary-color);">
|
||||
<h4 class="mb-0 text-white fw-bold">
|
||||
<i class="fas fa-file-invoice-dollar me-2"></i> Create Expense Receipt
|
||||
</h4>
|
||||
</div>
|
||||
<!-- Page header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="page-title"><i class="fas fa-receipt me-2" style="color: var(--accent);"></i>Create Expense Receipt</h1>
|
||||
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left fa-sm me-1"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
<form method="post" id="receipt-form">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<form method="post" id="receipt-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- === RECEIPT HEADER FIELDS === -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-secondary">Date</label>
|
||||
{{ form.date }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-secondary">Vendor Name</label>
|
||||
{{ form.vendor_name }}
|
||||
<div class="form-text text-muted small">
|
||||
<i class="fas fa-info-circle"></i> Will appear as the main title on the receipt.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-secondary">Payment Method</label>
|
||||
{{ form.payment_method }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-bold text-secondary">Description</label>
|
||||
{{ form.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- === LINE ITEMS SECTION ===
|
||||
Each row is a product name + amount.
|
||||
The "Add Line" button adds new rows via JavaScript.
|
||||
The X button hides the row and checks a hidden DELETE checkbox. -->
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="fw-bold text-dark m-0">Items</h5>
|
||||
<button type="button" id="add-item" class="btn btn-sm btn-outline-secondary rounded-pill">
|
||||
<i class="fas fa-plus me-1"></i> Add Line
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Django formset management form — tracks how many item forms exist -->
|
||||
{{ items.management_form }}
|
||||
|
||||
<div id="items-container">
|
||||
{% for item_form in items %}
|
||||
<div class="item-row row g-2 align-items-center mb-2">
|
||||
<!-- Hidden ID field (used by Django to track existing items) -->
|
||||
{{ item_form.id }}
|
||||
|
||||
<!-- Product name (takes most of the row) -->
|
||||
<div class="col-12 col-md-7">
|
||||
{{ item_form.product_name }}
|
||||
</div>
|
||||
|
||||
<!-- Amount with "R" prefix -->
|
||||
<div class="col-10 col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">R</span>
|
||||
{{ item_form.amount }}
|
||||
<!-- === RECEIPT HEADER FIELDS === -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Date</label>
|
||||
{{ form.date }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Vendor Name</label>
|
||||
{{ form.vendor_name }}
|
||||
<small style="color: var(--text-tertiary);">
|
||||
<i class="fas fa-info-circle"></i> Will appear as the main title on the receipt.
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Payment Method</label>
|
||||
{{ form.payment_method }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Description</label>
|
||||
{{ form.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete button — hides the row and checks the DELETE checkbox -->
|
||||
<div class="col-2 col-md-1 text-center">
|
||||
{% if items.can_delete %}
|
||||
<div class="form-check d-none">
|
||||
{{ item_form.DELETE }}
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm delete-row rounded-circle" title="Remove">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<hr style="border-color: var(--border-default);">
|
||||
|
||||
<!-- === LINE ITEMS === -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 mt-4">
|
||||
<h5 class="fw-bold m-0">Items</h5>
|
||||
<button type="button" id="add-item" class="btn btn-sm btn-outline-secondary rounded-pill">
|
||||
<i class="fas fa-plus me-1"></i> Add Line
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
{{ items.management_form }}
|
||||
|
||||
<!-- === VAT CONFIGURATION + LIVE TOTALS === -->
|
||||
<div class="row">
|
||||
<!-- Left: VAT type radio buttons -->
|
||||
<div class="col-md-6 mb-3 mb-md-0">
|
||||
<label class="form-label d-block fw-bold text-secondary mb-2">VAT Configuration (15%)</label>
|
||||
<div class="card bg-light border-0 p-3">
|
||||
{% for radio in form.vat_type %}
|
||||
<div class="form-check mb-2">
|
||||
{{ radio.tag }}
|
||||
<label class="form-check-label" for="{{ radio.id_for_label }}">
|
||||
{{ radio.choice_label }}
|
||||
</label>
|
||||
<div id="items-container">
|
||||
{% for item_form in items %}
|
||||
<div class="item-row row g-2 align-items-center mb-2">
|
||||
{{ item_form.id }}
|
||||
<div class="col-12 col-md-7">
|
||||
{{ item_form.product_name }}
|
||||
</div>
|
||||
<div class="col-10 col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">R</span>
|
||||
{{ item_form.amount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2 col-md-1 text-center">
|
||||
{% if items.can_delete %}
|
||||
<div class="form-check d-none">
|
||||
{{ item_form.DELETE }}
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm delete-row rounded-circle" title="Remove">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Live-updating totals panel -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label d-block fw-bold text-secondary mb-2">Receipt Totals</label>
|
||||
<div class="p-3 rounded" style="background-color: #f8fafc; border: 1px solid #e2e8f0;">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-secondary">Subtotal (Excl. VAT):</span>
|
||||
<span class="fw-bold">R <span id="display-subtotal">0.00</span></span>
|
||||
<hr style="border-color: var(--border-default);">
|
||||
|
||||
<!-- === VAT + TOTALS === -->
|
||||
<div class="row mt-4">
|
||||
<!-- VAT radio buttons -->
|
||||
<div class="col-md-6 mb-3 mb-md-0">
|
||||
<label class="form-label d-block fw-semibold mb-2">VAT Configuration (15%)</label>
|
||||
<div class="p-3 rounded" style="background: var(--bg-inset); border: 1px solid var(--border-default);">
|
||||
{% for radio in form.vat_type %}
|
||||
<div class="form-check mb-2">
|
||||
{{ radio.tag }}
|
||||
<label class="form-check-label" for="{{ radio.id_for_label }}">
|
||||
{{ radio.choice_label }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-secondary">VAT (15%):</span>
|
||||
<span class="fw-bold">R <span id="display-vat">0.00</span></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between border-top pt-2 mt-2">
|
||||
<span class="h5 mb-0 fw-bold">Total:</span>
|
||||
<span class="h5 mb-0" style="color: var(--accent-color);">
|
||||
R <span id="display-total">0.00</span>
|
||||
</span>
|
||||
|
||||
<!-- Live totals -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label d-block fw-semibold mb-2">Receipt Totals</label>
|
||||
<div class="p-3 rounded" style="background: var(--bg-inset); border: 1px solid var(--border-default);">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span style="color: var(--text-secondary);">Subtotal (Excl. VAT):</span>
|
||||
<span class="fw-bold">R <span id="display-subtotal">0.00</span></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span style="color: var(--text-secondary);">VAT (15%):</span>
|
||||
<span class="fw-bold">R <span id="display-vat">0.00</span></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between pt-2 mt-2" style="border-top: 1px solid var(--border-default);">
|
||||
<span class="h5 mb-0 fw-bold">Total:</span>
|
||||
<span class="h5 mb-0" style="color: var(--accent); font-family: 'Poppins', sans-serif;">
|
||||
R <span id="display-total">0.00</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === SUBMIT BUTTON === -->
|
||||
<div class="text-end mt-4">
|
||||
<button type="submit" class="btn btn-accent btn-lg">
|
||||
<i class="fas fa-paper-plane me-2"></i> Create & Send Receipt
|
||||
</button>
|
||||
<!-- Submit -->
|
||||
<div class="text-end mt-4">
|
||||
<button type="submit" class="btn btn-accent btn-lg">
|
||||
<i class="fas fa-paper-plane me-2"></i> Create & Send Receipt
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==========================================================================
|
||||
JAVASCRIPT — Dynamic line items + live VAT calculation
|
||||
========================================================================== -->
|
||||
<!-- === JavaScript: Dynamic line items + live VAT calculation === -->
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// --- DOM REFERENCES ---
|
||||
var itemsContainer = document.getElementById('items-container');
|
||||
var addItemBtn = document.getElementById('add-item');
|
||||
var totalForms = document.querySelector('#id_line_items-TOTAL_FORMS');
|
||||
var displaySubtotal = document.getElementById('display-subtotal');
|
||||
var displayVat = document.getElementById('display-vat');
|
||||
var displayTotal = document.getElementById('display-total');
|
||||
|
||||
// All VAT radio buttons — we listen for changes on these
|
||||
var vatRadios = document.querySelectorAll('input[name="vat_type"]');
|
||||
|
||||
// === ADD NEW LINE ITEM ROW ===
|
||||
// When "Add Line" is clicked, build a new blank row using DOM methods.
|
||||
// We increment TOTAL_FORMS so Django knows there's an extra form.
|
||||
// === ADD NEW LINE ITEM ===
|
||||
addItemBtn.addEventListener('click', function() {
|
||||
var formIdx = parseInt(totalForms.value);
|
||||
|
||||
// Create the row container
|
||||
var row = document.createElement('div');
|
||||
row.className = 'item-row row g-2 align-items-center mb-2';
|
||||
|
||||
// Hidden ID input (required by Django formset)
|
||||
var hiddenId = document.createElement('input');
|
||||
hiddenId.type = 'hidden';
|
||||
hiddenId.name = 'line_items-' + formIdx + '-id';
|
||||
hiddenId.id = 'id_line_items-' + formIdx + '-id';
|
||||
row.appendChild(hiddenId);
|
||||
|
||||
// Product name column
|
||||
var prodCol = document.createElement('div');
|
||||
prodCol.className = 'col-12 col-md-7';
|
||||
var prodInput = document.createElement('input');
|
||||
@ -196,13 +178,12 @@
|
||||
prodCol.appendChild(prodInput);
|
||||
row.appendChild(prodCol);
|
||||
|
||||
// Amount column with "R" prefix
|
||||
var amtCol = document.createElement('div');
|
||||
amtCol.className = 'col-10 col-md-4';
|
||||
var inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group';
|
||||
var prefix = document.createElement('span');
|
||||
prefix.className = 'input-group-text bg-light border-end-0';
|
||||
prefix.className = 'input-group-text';
|
||||
prefix.textContent = 'R';
|
||||
var amtInput = document.createElement('input');
|
||||
amtInput.type = 'number';
|
||||
@ -216,7 +197,6 @@
|
||||
amtCol.appendChild(inputGroup);
|
||||
row.appendChild(amtCol);
|
||||
|
||||
// Delete button column
|
||||
var delCol = document.createElement('div');
|
||||
delCol.className = 'col-2 col-md-1 text-center';
|
||||
var delBtn = document.createElement('button');
|
||||
@ -229,98 +209,60 @@
|
||||
delCol.appendChild(delBtn);
|
||||
row.appendChild(delCol);
|
||||
|
||||
// Add to DOM and update form count
|
||||
itemsContainer.appendChild(row);
|
||||
totalForms.value = formIdx + 1;
|
||||
|
||||
// Recalculate totals
|
||||
updateCalculations();
|
||||
});
|
||||
|
||||
// === DELETE LINE ITEM ROW ===
|
||||
// Uses event delegation — listens on the container for any delete button click.
|
||||
// If the row has a DELETE checkbox (existing saved item), checks it and hides the row.
|
||||
// If the row is brand new (no DELETE checkbox), just removes it from the DOM.
|
||||
// === DELETE LINE ITEM ===
|
||||
itemsContainer.addEventListener('click', function(e) {
|
||||
var deleteBtn = e.target.closest('.delete-row');
|
||||
if (!deleteBtn) return;
|
||||
|
||||
var row = deleteBtn.closest('.item-row');
|
||||
var deleteCheckbox = row.querySelector('input[name$="-DELETE"]');
|
||||
|
||||
if (deleteCheckbox) {
|
||||
// Existing item — check DELETE and hide (Django will delete on save)
|
||||
deleteCheckbox.checked = true;
|
||||
row.classList.add('d-none', 'deleted');
|
||||
} else {
|
||||
// New item — just remove from DOM
|
||||
row.remove();
|
||||
}
|
||||
|
||||
updateCalculations();
|
||||
});
|
||||
|
||||
// === LIVE AMOUNT INPUT CHANGES ===
|
||||
// Recalculate whenever an amount field changes
|
||||
// === LIVE AMOUNT CHANGES ===
|
||||
itemsContainer.addEventListener('input', function(e) {
|
||||
if (e.target.classList.contains('item-amount')) {
|
||||
updateCalculations();
|
||||
}
|
||||
if (e.target.classList.contains('item-amount')) updateCalculations();
|
||||
});
|
||||
|
||||
// === VAT TYPE RADIO CHANGES ===
|
||||
vatRadios.forEach(function(radio) {
|
||||
radio.addEventListener('change', updateCalculations);
|
||||
});
|
||||
|
||||
// === VAT CALCULATION LOGIC ===
|
||||
// Mirrors the backend Python calculation exactly.
|
||||
// Three modes: Included (reverse 15%), Excluded (add 15%), None (no VAT).
|
||||
// === VAT CALCULATION ===
|
||||
function updateCalculations() {
|
||||
// Sum all visible (non-deleted) item amounts
|
||||
var sum = 0;
|
||||
var amounts = document.querySelectorAll('.item-row:not(.deleted) .item-amount');
|
||||
amounts.forEach(function(input) {
|
||||
var val = parseFloat(input.value) || 0;
|
||||
sum += val;
|
||||
document.querySelectorAll('.item-row:not(.deleted) .item-amount').forEach(function(input) {
|
||||
sum += parseFloat(input.value) || 0;
|
||||
});
|
||||
|
||||
// Find which VAT radio is selected
|
||||
var vatType = 'None';
|
||||
vatRadios.forEach(function(r) {
|
||||
if (r.checked) vatType = r.value;
|
||||
});
|
||||
|
||||
var subtotal = 0;
|
||||
var vat = 0;
|
||||
var total = 0;
|
||||
vatRadios.forEach(function(r) { if (r.checked) vatType = r.value; });
|
||||
|
||||
var subtotal = 0, vat = 0, total = 0;
|
||||
if (vatType === 'Included') {
|
||||
// Entered amounts include VAT — reverse it out
|
||||
total = sum;
|
||||
subtotal = total / 1.15;
|
||||
vat = total - subtotal;
|
||||
total = sum; subtotal = total / 1.15; vat = total - subtotal;
|
||||
} else if (vatType === 'Excluded') {
|
||||
// Entered amounts are pre-VAT — add 15% on top
|
||||
subtotal = sum;
|
||||
vat = subtotal * 0.15;
|
||||
total = subtotal + vat;
|
||||
subtotal = sum; vat = subtotal * 0.15; total = subtotal + vat;
|
||||
} else {
|
||||
// No VAT
|
||||
subtotal = sum;
|
||||
vat = 0;
|
||||
total = sum;
|
||||
subtotal = sum; total = sum;
|
||||
}
|
||||
|
||||
// Update the display using textContent (safe, no HTML injection)
|
||||
displaySubtotal.textContent = subtotal.toFixed(2);
|
||||
displayVat.textContent = vat.toFixed(2);
|
||||
displayTotal.textContent = total.toFixed(2);
|
||||
}
|
||||
|
||||
// Run once on page load (in case form has pre-filled values)
|
||||
updateCalculations();
|
||||
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -4,15 +4,13 @@
|
||||
{% block title %}Dashboard | FoxFitt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
{# Hide resource rows — needs !important to override Bootstrap's d-flex !important #}
|
||||
.resource-hidden { display: none !important; }
|
||||
</style>
|
||||
<!-- Gradient Header -->
|
||||
<div class="dashboard-header mb-5 rounded shadow-sm p-4 d-flex justify-content-between align-items-center">
|
||||
<!-- === DASHBOARD HEADER — gradient banner with welcome + CTA === -->
|
||||
<div class="dashboard-header mb-5 rounded-0 p-4 d-flex justify-content-between align-items-center d-print-none">
|
||||
<div>
|
||||
<h1 class="h3 mb-0 text-white" style="font-family: 'Poppins', sans-serif;">Dashboard</h1>
|
||||
<p class="text-white-50 mb-0">Welcome back, {{ user.first_name|default:user.username }}!</p>
|
||||
<h1 class="h3 mb-1 text-white" style="font-family: 'Poppins', sans-serif;">Dashboard</h1>
|
||||
<p class="mb-0" style="color: rgba(255,255,255,0.6); font-size: 0.9rem;">
|
||||
Welcome back, {{ user.first_name|default:user.username }}
|
||||
</p>
|
||||
</div>
|
||||
<a href="{% url 'attendance_log' %}" class="btn btn-accent shadow-sm">
|
||||
<i class="fas fa-plus fa-sm me-1"></i> Log Daily Work
|
||||
@ -20,377 +18,385 @@
|
||||
</div>
|
||||
|
||||
<div class="container py-2" style="margin-top: -3rem;">
|
||||
{% if is_admin %}
|
||||
<!-- Admin View -->
|
||||
<div class="row g-4 mb-4 position-relative">
|
||||
<!-- Outstanding Payments Card -->
|
||||
<!-- Shows the total owed to workers, with a breakdown of wages vs adjustments -->
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col me-2">
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #ef4444;">
|
||||
Outstanding Payments</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ outstanding_payments|floatformat:2 }}</div>
|
||||
{# === BREAKDOWN — only shown when there are pending adjustments === #}
|
||||
{% if pending_adjustments_add or pending_adjustments_sub %}
|
||||
<div class="mt-2 pt-2 border-top" style="font-size: 0.75rem; color: #64748b;">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Unpaid wages</span>
|
||||
<span>R {{ unpaid_wages|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% if pending_adjustments_add %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>+ Additions</span>
|
||||
<span class="text-success">R {{ pending_adjustments_add|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if pending_adjustments_sub %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>- Deductions</span>
|
||||
<span class="text-danger">-R {{ pending_adjustments_sub|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-1" style="font-size: 0.65rem; color: #94a3b8;">
|
||||
<i class="fas fa-info-circle"></i> Loan repayments deducted at payment time
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto align-self-start">
|
||||
<i class="fas fa-exclamation-circle fa-2x text-danger opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_admin %}
|
||||
<!-- ===================================================================
|
||||
ADMIN VIEW — stats, quick actions, activity, resources
|
||||
=================================================================== -->
|
||||
|
||||
<!-- === STAT CARDS (4 columns) === -->
|
||||
<div class="row g-3 mb-4">
|
||||
|
||||
<!-- Outstanding Payments -->
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="stat-card stat-card--danger h-100 p-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="stat-label">Outstanding Payments</div>
|
||||
<div class="stat-value">R {{ outstanding_payments|floatformat:2 }}</div>
|
||||
</div>
|
||||
<div class="stat-icon stat-icon--danger">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paid This Month Card -->
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col me-2">
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #10b981;">
|
||||
Paid This Month</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ paid_this_month|floatformat:2 }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-check-circle fa-2x text-success opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
{# Breakdown — wages + adjustments (shown when adjustments exist) #}
|
||||
{% if pending_adjustments_add or pending_adjustments_sub %}
|
||||
<div class="mt-2 pt-2" style="border-top: 1px solid var(--border-subtle); font-size: 0.75rem; color: var(--text-secondary);">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Unpaid wages</span>
|
||||
<span>R {{ unpaid_wages|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% if pending_adjustments_add %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>+ Additions</span>
|
||||
<span style="color: var(--color-success);">R {{ pending_adjustments_add|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if pending_adjustments_sub %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>- Deductions</span>
|
||||
<span style="color: var(--color-danger);">-R {{ pending_adjustments_sub|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-1" style="font-size: 0.65rem; color: var(--text-tertiary);">
|
||||
<i class="fas fa-info-circle"></i> Loan repayments deducted at payment time
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Loans Card -->
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col me-2">
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #f59e0b;">
|
||||
Active Loans ({{ active_loans_count }})</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ active_loans_balance|floatformat:2 }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-hand-holding-usd fa-2x text-warning opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Paid This Month -->
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="stat-card stat-card--success h-100 p-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="stat-label">Paid This Month</div>
|
||||
<div class="stat-value">R {{ paid_this_month|floatformat:2 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outstanding by Project -->
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col me-2">
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #3b82f6;">
|
||||
Outstanding by Project</div>
|
||||
<div class="mb-0 text-gray-800" style="font-size: 0.85rem;">
|
||||
{% if outstanding_by_project %}
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for proj, amount in outstanding_by_project.items %}
|
||||
<li><strong>{{ proj }}:</strong> R {{ amount|floatformat:2 }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-chart-pie fa-2x text-primary opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-icon stat-icon--success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions and This Week -->
|
||||
<div class="row mb-4">
|
||||
<!-- This Week -->
|
||||
<div class="col-lg-4 mb-4 mb-lg-0">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header py-3 bg-white">
|
||||
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">This Week Summary</h6>
|
||||
<!-- Active Loans -->
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="stat-card stat-card--warning h-100 p-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="stat-label">Active Loans ({{ active_loans_count }})</div>
|
||||
<div class="stat-value">R {{ active_loans_balance|floatformat:2 }}</div>
|
||||
</div>
|
||||
<div class="card-body text-center d-flex flex-column justify-content-center">
|
||||
<div class="h1 mb-0 font-weight-bold text-primary">{{ this_week_logs }}</div>
|
||||
<div class="text-muted">Work Logs Created This Week</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header py-3 bg-white">
|
||||
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Quick Actions</h6>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-around flex-wrap">
|
||||
<a href="{% url 'attendance_log' %}" class="btn btn-lg btn-outline-primary mb-2">
|
||||
<i class="fas fa-clipboard-list mb-2 d-block fa-2x"></i> Log Work
|
||||
</a>
|
||||
<a href="{% url 'payroll_dashboard' %}" class="btn btn-lg btn-outline-success mb-2">
|
||||
<i class="fas fa-money-check-alt mb-2 d-block fa-2x"></i> Run Payroll
|
||||
</a>
|
||||
<a href="{% url 'work_history' %}" class="btn btn-lg btn-outline-secondary mb-2">
|
||||
<i class="fas fa-history mb-2 d-block fa-2x"></i> View History
|
||||
</a>
|
||||
<div class="stat-icon stat-icon--warning">
|
||||
<i class="fas fa-hand-holding-usd"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Recent Activity -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header py-3 bg-white">
|
||||
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Recent Activity</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for log in recent_activity %}
|
||||
<div class="list-group-item px-4 py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">{{ log.project.name }}</h6>
|
||||
<small class="text-muted">{{ log.date }} · {{ log.workers.count }} workers</small>
|
||||
</div>
|
||||
<span class="badge bg-light text-dark border">{{ log.supervisor.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="p-4 text-center text-muted">
|
||||
No recent activity.
|
||||
<!-- Outstanding by Project -->
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="stat-card stat-card--info h-100 p-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div style="flex: 1;">
|
||||
<div class="stat-label">Outstanding by Project</div>
|
||||
{% if outstanding_by_project %}
|
||||
<div style="font-size: 0.85rem; margin-top: 0.35rem;">
|
||||
{% for proj, amount in outstanding_by_project.items %}
|
||||
<div class="d-flex justify-content-between" style="color: var(--text-primary);">
|
||||
<span class="text-truncate me-2">{{ proj }}</span>
|
||||
<span class="fw-semibold" style="white-space: nowrap;">R {{ amount|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span style="font-size: 0.85rem; color: var(--text-tertiary);">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manage Resources -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header py-3 bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Manage Resources</h6>
|
||||
<a href="{% url 'export_workers_csv' %}" class="btn btn-outline-success btn-sm">
|
||||
<i class="fas fa-file-csv me-1"></i> Export Workers
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<p class="text-muted small mb-0 px-3 pt-3">Toggle active status. Inactive items are hidden from forms.</p>
|
||||
|
||||
<ul class="nav nav-tabs px-3 pt-2" id="resourceTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="workers-tab" data-bs-toggle="tab" data-bs-target="#workers" type="button" role="tab" aria-selected="true">Workers</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="projects-tab" data-bs-toggle="tab" data-bs-target="#projects" type="button" role="tab" aria-selected="false">Projects</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="teams-tab" data-bs-toggle="tab" data-bs-target="#teams" type="button" role="tab" aria-selected="false">Teams</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{# Filter bar — Active / Inactive / All (defaults to Active) #}
|
||||
<div class="btn-group btn-group-sm w-100 px-3 mt-2" id="resourceFilter" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" data-filter="active">Active</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-filter="inactive">Inactive</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-filter="all">All</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content px-0 mt-2" id="resourceTabsContent" style="max-height: 350px; overflow-y: auto;">
|
||||
|
||||
{# === WORKERS TAB === #}
|
||||
<div class="tab-pane fade show active" id="workers" role="tabpanel">
|
||||
{% for item in workers %}
|
||||
<div class="resource-row d-flex justify-content-between align-items-center py-2 px-3 border-bottom {% if not item.active %}resource-hidden{% endif %}" data-active="{% if item.active %}true{% else %}false{% endif %}">
|
||||
<strong class="small">{{ item.name }}</strong>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="worker" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-muted small px-3 py-2">No workers found.</p>
|
||||
{% endfor %}
|
||||
<p class="text-muted small text-center mt-2 resource-empty" style="display:none;">No matching items.</p>
|
||||
</div>
|
||||
|
||||
{# === PROJECTS TAB === #}
|
||||
<div class="tab-pane fade" id="projects" role="tabpanel">
|
||||
{% for item in projects %}
|
||||
<div class="resource-row d-flex justify-content-between align-items-center py-2 px-3 border-bottom {% if not item.active %}resource-hidden{% endif %}" data-active="{% if item.active %}true{% else %}false{% endif %}">
|
||||
<strong class="small">{{ item.name }}</strong>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="project" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-muted small px-3 py-2">No projects found.</p>
|
||||
{% endfor %}
|
||||
<p class="text-muted small text-center mt-2 resource-empty" style="display:none;">No matching items.</p>
|
||||
</div>
|
||||
|
||||
{# === TEAMS TAB === #}
|
||||
<div class="tab-pane fade" id="teams" role="tabpanel">
|
||||
{% for item in teams %}
|
||||
<div class="resource-row d-flex justify-content-between align-items-center py-2 px-3 border-bottom {% if not item.active %}resource-hidden{% endif %}" data-active="{% if item.active %}true{% else %}false{% endif %}">
|
||||
<strong class="small">{{ item.name }}</strong>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="team" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-muted small px-3 py-2">No teams found.</p>
|
||||
{% endfor %}
|
||||
<p class="text-muted small text-center mt-2 resource-empty" style="display:none;">No matching items.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="stat-icon stat-icon--info ms-2">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Supervisor View -->
|
||||
<!-- Stat Cards — how many projects, teams, and workers this supervisor manages -->
|
||||
<div class="row g-4 mb-4 position-relative">
|
||||
<div class="col-md-4">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col me-2">
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #8b5cf6;">
|
||||
My Projects</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ my_projects_count }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-project-diagram fa-2x opacity-50" style="color: #8b5cf6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col me-2">
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #3b82f6;">
|
||||
My Teams</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ my_teams_count }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-users fa-2x opacity-50" style="color: #3b82f6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col me-2">
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #10b981;">
|
||||
My Workers</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ my_workers_count }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-hard-hat fa-2x opacity-50" style="color: #10b981;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- This Week + Recent Activity -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-4 mb-4 mb-lg-0">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header py-3 bg-white">
|
||||
<h6 class="m-0 fw-bold" style="color: #0f172a;">This Week Summary</h6>
|
||||
</div>
|
||||
<div class="card-body text-center d-flex flex-column justify-content-center">
|
||||
<div class="h1 mb-0 fw-bold text-primary">{{ this_week_logs }}</div>
|
||||
<div class="text-muted">Work Logs Created This Week</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header py-3 bg-white">
|
||||
<h6 class="m-0 fw-bold" style="color: #0f172a;">Recent Activity</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for log in recent_activity %}
|
||||
<div class="list-group-item px-4 py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">{{ log.project.name }}</h6>
|
||||
<small class="text-muted">{{ log.date }} · {{ log.workers.count }} workers</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="p-4 text-center text-muted">
|
||||
<i class="fas fa-inbox fa-2x mb-2 d-block opacity-50"></i>
|
||||
No recent activity.
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- === ROW 2: This Week + Quick Actions === -->
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- This Week Summary -->
|
||||
<div class="col-lg-4 mb-3 mb-lg-0">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 fw-bold"><i class="fas fa-calendar-week me-2" style="color: var(--accent);"></i>This Week</h6>
|
||||
</div>
|
||||
<div class="card-body text-center d-flex flex-column justify-content-center">
|
||||
<div class="stat-value" style="font-size: 2.5rem; color: var(--accent);">{{ this_week_logs }}</div>
|
||||
<div style="color: var(--text-secondary); font-size: 0.85rem;">Work Logs Created</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 fw-bold"><i class="fas fa-bolt me-2" style="color: var(--accent);"></i>Quick Actions</h6>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center gap-3 flex-wrap">
|
||||
<a href="{% url 'attendance_log' %}" class="quick-action">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
<span>Log Work</span>
|
||||
</a>
|
||||
<a href="{% url 'payroll_dashboard' %}" class="quick-action">
|
||||
<i class="fas fa-money-check-alt"></i>
|
||||
<span>Run Payroll</span>
|
||||
</a>
|
||||
<a href="{% url 'work_history' %}" class="quick-action">
|
||||
<i class="fas fa-history"></i>
|
||||
<span>View History</span>
|
||||
</a>
|
||||
<a href="{% url 'create_receipt' %}" class="quick-action">
|
||||
<i class="fas fa-receipt"></i>
|
||||
<span>New Receipt</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === ROW 3: Recent Activity + Manage Resources === -->
|
||||
<div class="row g-3">
|
||||
<!-- Recent Activity -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 fw-bold"><i class="fas fa-stream me-2" style="color: var(--accent);"></i>Recent Activity</h6>
|
||||
<a href="{% url 'work_history' %}" class="btn btn-sm btn-outline-secondary">View All</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for log in recent_activity %}
|
||||
<div class="list-group-item px-4 py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1 fw-semibold" style="font-size: 0.9rem;">{{ log.project.name }}</h6>
|
||||
<small style="color: var(--text-secondary);">
|
||||
<i class="fas fa-calendar-day me-1"></i>{{ log.date }}
|
||||
<span class="mx-1">·</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">·</span>
|
||||
<i class="fas fa-users me-1"></i>{{ log.workers.count }} workers
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="p-4 text-center" style="color: var(--text-tertiary);">
|
||||
<i class="fas fa-inbox fa-2x mb-2 d-block"></i>
|
||||
No recent activity
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- === JAVASCRIPT: Resource filter + AJAX toggle === -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// === RESOURCE FILTER (Active / Inactive / All) ===
|
||||
// Hides/shows resource rows based on their data-active attribute.
|
||||
// Starts on "Active" so only current items are visible by default.
|
||||
var currentFilter = 'active';
|
||||
var filterBtns = document.querySelectorAll('#resourceFilter button');
|
||||
|
||||
function applyFilter() {
|
||||
// Use the resource-hidden CLASS (not inline display:none) because
|
||||
// Bootstrap's d-flex has !important which overrides inline styles.
|
||||
// Our .resource-hidden also has !important, so it wins.
|
||||
document.querySelectorAll('.resource-row').forEach(function(row) {
|
||||
var isActive = row.dataset.active === 'true';
|
||||
var show = false;
|
||||
@ -403,13 +409,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
row.classList.add('resource-hidden');
|
||||
}
|
||||
});
|
||||
// Show "No matching items" if a tab has rows but none are visible
|
||||
// Show "No matching items" if tab has rows but none visible
|
||||
document.querySelectorAll('.tab-pane').forEach(function(pane) {
|
||||
var rows = pane.querySelectorAll('.resource-row');
|
||||
var visibleRows = Array.from(rows).filter(function(r) { return !r.classList.contains('resource-hidden'); });
|
||||
var visible = Array.from(rows).filter(function(r) { return !r.classList.contains('resource-hidden'); });
|
||||
var emptyMsg = pane.querySelector('.resource-empty');
|
||||
if (emptyMsg) {
|
||||
emptyMsg.style.display = (rows.length > 0 && visibleRows.length === 0) ? '' : 'none';
|
||||
emptyMsg.style.display = (rows.length > 0 && visible.length === 0) ? '' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -423,16 +429,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Apply filter on page load (shows only active by default)
|
||||
applyFilter();
|
||||
|
||||
// === TOGGLE HANDLER ===
|
||||
// When a toggle switch is flipped, POST to the server to update active status.
|
||||
// On success, update the row's data-active attribute and re-apply the filter
|
||||
// so the row moves to the correct section immediately.
|
||||
var toggleSwitches = document.querySelectorAll('.toggle-active');
|
||||
|
||||
toggleSwitches.forEach(function(switchEl) {
|
||||
// === TOGGLE HANDLER — AJAX POST to activate/deactivate resources ===
|
||||
document.querySelectorAll('.toggle-active').forEach(function(switchEl) {
|
||||
switchEl.addEventListener('change', function() {
|
||||
var type = this.getAttribute('data-type');
|
||||
var id = this.getAttribute('data-id');
|
||||
@ -452,7 +452,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.status === 'success') {
|
||||
// Update the row's data-active and re-apply filter
|
||||
row.dataset.active = isChecked ? 'true' : 'false';
|
||||
applyFilter();
|
||||
} else {
|
||||
@ -460,7 +459,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
alert('Error updating status.');
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
.catch(function() {
|
||||
switchEl.checked = !isChecked;
|
||||
alert('Error updating status.');
|
||||
});
|
||||
|
||||
@ -1,17 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Payroll Dashboard | Fox Fitt{% endblock %}
|
||||
{% block title %}Payroll Dashboard | FoxFitt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Chart.js CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
// === CHART.JS THEME DEFAULTS ===
|
||||
// Read CSS variable colours so chart axes/grid lines adapt to dark mode
|
||||
(function() {
|
||||
var style = getComputedStyle(document.documentElement);
|
||||
var textColor = style.getPropertyValue('--text-secondary').trim() || '#64748b';
|
||||
var borderColor = style.getPropertyValue('--border-default').trim() || '#e2e8f0';
|
||||
if (typeof Chart !== 'undefined') {
|
||||
Chart.defaults.color = textColor;
|
||||
Chart.defaults.borderColor = borderColor;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div class="container py-4">
|
||||
|
||||
{# === PAGE HEADER === #}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0" style="font-family: 'Poppins', sans-serif;">Payroll Dashboard</h1>
|
||||
<h1 class="page-title"><i class="fas fa-wallet me-2" style="color: var(--accent);"></i>Payroll Dashboard</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-info shadow-sm" id="workerLookupBtn">
|
||||
<i class="fas fa-id-card fa-sm me-1"></i> Worker Lookup
|
||||
@ -38,39 +51,37 @@
|
||||
<div class="row g-3 h-100">
|
||||
{# Outstanding Total — with breakdown of wages vs adjustments #}
|
||||
<div class="col-sm-6">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #ef4444;">
|
||||
Outstanding Payments</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ outstanding_total|floatformat:2 }}</div>
|
||||
{# === BREAKDOWN — only shown when there are pending adjustments === #}
|
||||
{% if pending_adj_add_total or pending_adj_sub_total %}
|
||||
<div class="mt-2 pt-2 border-top" style="font-size: 0.75rem; color: #64748b;">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Unpaid wages</span>
|
||||
<span>R {{ unpaid_wages_total|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% if pending_adj_add_total %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>+ Additions</span>
|
||||
<span class="text-success">R {{ pending_adj_add_total|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if pending_adj_sub_total %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>- Deductions</span>
|
||||
<span class="text-danger">-R {{ pending_adj_sub_total|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="stat-card stat-card--danger h-100 p-3">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="stat-label">Outstanding Payments</div>
|
||||
<div class="stat-value">R {{ outstanding_total|floatformat:2 }}</div>
|
||||
{% if pending_adj_add_total or pending_adj_sub_total %}
|
||||
<div class="mt-2 pt-2" style="border-top: 1px solid var(--border-subtle); font-size: 0.75rem; color: var(--text-secondary);">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Unpaid wages</span>
|
||||
<span>R {{ unpaid_wages_total|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% if pending_adj_add_total %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>+ Additions</span>
|
||||
<span style="color: var(--color-success);">R {{ pending_adj_add_total|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-1" style="font-size: 0.65rem; color: #94a3b8;">
|
||||
<i class="fas fa-info-circle"></i> Loan repayments deducted at payment time
|
||||
{% if pending_adj_sub_total %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>- Deductions</span>
|
||||
<span style="color: var(--color-danger);">-R {{ pending_adj_sub_total|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<i class="fas fa-exclamation-circle fa-2x text-danger opacity-25"></i>
|
||||
{% endif %}
|
||||
<div class="mt-1" style="font-size: 0.65rem; color: var(--text-tertiary);">
|
||||
<i class="fas fa-info-circle"></i> Loan repayments deducted at payment time
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-icon stat-icon--danger">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -78,15 +89,14 @@
|
||||
|
||||
{# Recent Payments #}
|
||||
<div class="col-sm-6">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #10b981;">
|
||||
Paid (Last 60 Days)</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ recent_payments_total|floatformat:2 }}</div>
|
||||
</div>
|
||||
<i class="fas fa-check-circle fa-2x text-success opacity-25"></i>
|
||||
<div class="stat-card stat-card--success h-100 p-3">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="stat-label">Paid (Last 60 Days)</div>
|
||||
<div class="stat-value">R {{ recent_payments_total|floatformat:2 }}</div>
|
||||
</div>
|
||||
<div class="stat-icon stat-icon--success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,15 +104,14 @@
|
||||
|
||||
{# Active Loans — spans full width below the first two #}
|
||||
<div class="col-12">
|
||||
<div class="card stat-card h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #f59e0b;">
|
||||
Active Loans & Advances ({{ active_loans_count }})</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ active_loans_balance|floatformat:2 }}</div>
|
||||
</div>
|
||||
<i class="fas fa-hand-holding-usd fa-2x text-warning opacity-25"></i>
|
||||
<div class="stat-card stat-card--warning h-100 p-3">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="stat-label">Active Loans & Advances ({{ active_loans_count }})</div>
|
||||
<div class="stat-value">R {{ active_loans_balance|floatformat:2 }}</div>
|
||||
</div>
|
||||
<div class="stat-icon stat-icon--warning">
|
||||
<i class="fas fa-hand-holding-usd"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -112,25 +121,26 @@
|
||||
|
||||
{# --- Right column: project breakdown (grows to fit all projects) --- #}
|
||||
<div class="col-xl-5 d-flex">
|
||||
<div class="card stat-card py-2 w-100">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div class="text-xs font-weight-bold text-uppercase" style="color: #3b82f6;">
|
||||
Outstanding by Project</div>
|
||||
<i class="fas fa-chart-pie fa-2x text-primary opacity-25"></i>
|
||||
<div class="stat-card stat-card--info p-3 w-100">
|
||||
<div class="d-flex flex-column h-100">
|
||||
<div class="d-flex align-items-start justify-content-between mb-3">
|
||||
<div class="stat-label">Outstanding by Project</div>
|
||||
<div class="stat-icon stat-icon--info">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
</div>
|
||||
</div>
|
||||
{% if outstanding_project_costs %}
|
||||
<div class="flex-grow-1">
|
||||
{% for pc in outstanding_project_costs %}
|
||||
<div class="d-flex justify-content-between align-items-center {% if not forloop.last %}mb-2 pb-2 border-bottom{% endif %}">
|
||||
<span class="fw-semibold text-gray-800">{{ pc.name }}</span>
|
||||
<span class="fw-bold" style="color: #3b82f6;">R {{ pc.cost|floatformat:2 }}</span>
|
||||
<div class="d-flex justify-content-between align-items-center {% if not forloop.last %}mb-2 pb-2{% endif %}" {% if not forloop.last %}style="border-bottom: 1px solid var(--border-subtle);"{% endif %}>
|
||||
<span class="fw-semibold">{{ pc.name }}</span>
|
||||
<span class="fw-bold" style="color: var(--color-info);">R {{ pc.cost|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex-grow-1 d-flex align-items-center justify-content-center">
|
||||
<span class="text-muted"><i class="fas fa-check-circle me-1"></i> No outstanding amounts</span>
|
||||
<span style="color: var(--text-tertiary);"><i class="fas fa-check-circle me-1"></i> No outstanding amounts</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -142,13 +152,11 @@
|
||||
{# === CHARTS === #}
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-6 mb-4 mb-lg-0">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header py-3 bg-white">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-3">
|
||||
{# === CHART TOGGLE: Overall vs By Worker === #}
|
||||
{# Two small buttons to switch between the total line chart #}
|
||||
{# and a per-worker stacked bar chart breakdown. #}
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold" style="color: var(--primary-dark);">Monthly Payroll</h6>
|
||||
<h6 class="m-0 fw-bold">Monthly Payroll</h6>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Chart view toggle">
|
||||
<button type="button" class="btn btn-sm btn-accent" id="btnOverall">
|
||||
<i class="fas fa-chart-line fa-sm me-1"></i>Overall
|
||||
@ -185,9 +193,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header py-3 bg-white">
|
||||
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Cost by Project (Monthly)</h6>
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 fw-bold">Cost by Project (Monthly)</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="projectChart" height="200"></canvas>
|
||||
@ -246,7 +254,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="pendingTable">
|
||||
@ -268,7 +276,7 @@
|
||||
data-overdue="{{ wd.is_overdue|yesno:'true,false' }}"
|
||||
data-has-loan="{{ wd.has_loan|yesno:'true,false' }}">
|
||||
<td class="ps-4 align-middle">
|
||||
<a href="#" class="worker-lookup-link text-decoration-none text-dark fw-bold"
|
||||
<a href="#" class="worker-lookup-link fw-bold"
|
||||
data-worker-id="{{ wd.worker.id }}">{{ wd.worker.name }}</a>
|
||||
{% if wd.is_overdue %}
|
||||
<span class="badge bg-danger ms-1" title="Has unpaid work from a completed pay period (since {{ wd.earliest_unpaid|date:'d M Y' }})">Overdue</span>
|
||||
@ -348,7 +356,7 @@
|
||||
{# === PAYMENT HISTORY TAB === #}
|
||||
{# =============================================== #}
|
||||
{% if active_tab == 'paid' %}
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
@ -366,7 +374,7 @@
|
||||
{% for record in paid_records %}
|
||||
<tr>
|
||||
<td class="ps-4 align-middle">{{ record.date }}</td>
|
||||
<td class="align-middle"><a href="#" class="worker-lookup-link text-decoration-none text-dark fw-bold"
|
||||
<td class="align-middle"><a href="#" class="worker-lookup-link fw-bold"
|
||||
data-worker-id="{{ record.worker.id }}">{{ record.worker.name }}</a></td>
|
||||
<td class="align-middle">R {{ record.amount_paid|floatformat:2 }}</td>
|
||||
<td class="align-middle">
|
||||
@ -416,7 +424,7 @@
|
||||
History
|
||||
</a>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
@ -434,7 +442,7 @@
|
||||
<tbody>
|
||||
{% for loan in loans %}
|
||||
<tr>
|
||||
<td class="ps-4 align-middle"><a href="#" class="worker-lookup-link text-decoration-none text-dark fw-bold"
|
||||
<td class="ps-4 align-middle"><a href="#" class="worker-lookup-link fw-bold"
|
||||
data-worker-id="{{ loan.worker.id }}">{{ loan.worker.name }}</a></td>
|
||||
<td class="align-middle">
|
||||
{% if loan.loan_type == 'advance' %}
|
||||
@ -534,7 +542,7 @@
|
||||
<a href="#" id="adjSelectAll" class="small text-primary text-decoration-none text-nowrap">Select All</a>
|
||||
<a href="#" id="adjDeselectAll" class="small text-muted text-decoration-none text-nowrap">Clear</a>
|
||||
</div>
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 4px; padding: 8px;">
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid var(--border-default); border-radius: var(--radius-sm); padding: 8px; background: var(--bg-inset);">
|
||||
{% for w in all_workers %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input add-adj-worker" type="checkbox"
|
||||
@ -1668,9 +1676,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (data.pay_period && data.pay_period.has_schedule) {
|
||||
var periodInfo = document.createElement('div');
|
||||
periodInfo.className = 'alert alert-info py-2 px-3 small mb-2';
|
||||
periodInfo.style.backgroundColor = '#e0f2fe';
|
||||
periodInfo.style.borderColor = '#7dd3fc';
|
||||
periodInfo.style.color = '#0c4a6e';
|
||||
periodInfo.style.backgroundColor = 'var(--color-info-bg)';
|
||||
periodInfo.style.borderColor = 'var(--color-info)';
|
||||
periodInfo.style.color = 'var(--color-info)';
|
||||
|
||||
var infoIcon = document.createElement('i');
|
||||
infoIcon.className = 'fas fa-calendar-alt me-2';
|
||||
@ -2000,7 +2008,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Card container for each loan/advance
|
||||
var card = document.createElement('div');
|
||||
card.className = 'border rounded p-2 mb-2';
|
||||
card.style.backgroundColor = '#f8f9fa';
|
||||
card.style.backgroundColor = 'var(--bg-inset)';
|
||||
|
||||
// Row 1: Type badge + Balance
|
||||
var topRow = document.createElement('div');
|
||||
@ -2790,23 +2798,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var statsRow = el('div', 'row g-2 mb-4');
|
||||
|
||||
var stats = [
|
||||
{ label: 'Amount Payable', value: data.amount_payable, color: '#0f172a' },
|
||||
{ label: 'Outstanding Loans', value: data.outstanding_loans, color: '#f59e0b' },
|
||||
{ label: 'Paid This Month', value: data.paid_this_month, color: '#10b981' },
|
||||
{ label: 'Loans This Year', value: data.loans_this_year, color: '#ef4444' },
|
||||
{ label: 'Amount Payable', value: data.amount_payable, color: 'var(--text-primary)' },
|
||||
{ label: 'Outstanding Loans', value: data.outstanding_loans, color: 'var(--color-warning)' },
|
||||
{ label: 'Paid This Month', value: data.paid_this_month, color: 'var(--color-success)' },
|
||||
{ label: 'Loans This Year', value: data.loans_this_year, color: 'var(--color-danger)' },
|
||||
];
|
||||
|
||||
stats.forEach(function(stat) {
|
||||
var col = el('div', 'col-6 col-md-3');
|
||||
var card = el('div', 'card border-0 shadow-sm h-100');
|
||||
var body = el('div', 'card-body text-center py-2 px-2');
|
||||
var label = el('div', 'text-uppercase small fw-bold mb-1');
|
||||
var card = el('div', 'stat-card h-100');
|
||||
card.style.padding = '0.75rem';
|
||||
var label = el('div', 'stat-label');
|
||||
label.style.color = stat.color;
|
||||
label.style.fontSize = '0.65rem';
|
||||
label.textContent = stat.label;
|
||||
body.appendChild(label);
|
||||
body.appendChild(el('div', 'fw-bold', formatRand(stat.value)));
|
||||
card.appendChild(body);
|
||||
card.appendChild(label);
|
||||
card.appendChild(el('div', 'fw-bold', formatRand(stat.value)));
|
||||
col.appendChild(card);
|
||||
statsRow.appendChild(col);
|
||||
});
|
||||
@ -2885,7 +2891,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// --- PAID THIS YEAR ---
|
||||
var yearSection = el('div', 'mb-4 p-3 rounded');
|
||||
yearSection.style.backgroundColor = '#f1f5f9';
|
||||
yearSection.style.backgroundColor = 'var(--bg-inset)';
|
||||
var yearLabel = el('span', 'text-muted small text-uppercase', 'Paid This Year: ');
|
||||
var yearValue = el('span', 'fw-bold', formatRand(data.paid_this_year));
|
||||
yearSection.appendChild(yearLabel);
|
||||
@ -2919,7 +2925,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var notesLabel = el('div', 'small text-muted mt-2', 'Notes:');
|
||||
infoSection.appendChild(notesLabel);
|
||||
var notesText = el('div', 'small p-2 rounded');
|
||||
notesText.style.backgroundColor = '#f8f9fa';
|
||||
notesText.style.backgroundColor = 'var(--bg-inset)';
|
||||
notesText.textContent = data.notes;
|
||||
infoSection.appendChild(notesText);
|
||||
}
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Payslip #{{ record.id }} | Fox Fitt{% endblock %}
|
||||
{% block title %}Payslip #{{ record.id }} | FoxFitt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- === PAYSLIP DETAIL PAGE ===
|
||||
Shows a completed payment with work logs, adjustments, and totals.
|
||||
Reached from the Payment History tab on the payroll dashboard.
|
||||
Has a Print button that uses the browser's native print dialog. -->
|
||||
Print-friendly layout. -->
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="container py-4">
|
||||
<!-- Action buttons (hidden when printing) -->
|
||||
<div class="d-print-none mb-4 d-grid gap-2 d-md-flex">
|
||||
<div class="d-print-none mb-4 d-flex gap-2 flex-wrap">
|
||||
<a href="{% url 'payroll_dashboard' %}?status=paid" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> Back to Payment History
|
||||
</a>
|
||||
@ -20,35 +19,41 @@
|
||||
</div>
|
||||
|
||||
<!-- Payslip card -->
|
||||
<div class="card border-0 shadow-sm" id="payslip-card">
|
||||
<div class="card-body p-5">
|
||||
<div class="card" id="payslip-card">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
|
||||
<!-- === HEADER — worker name is the dominant element === -->
|
||||
<div class="row mb-5 border-bottom pb-4 align-items-center">
|
||||
<!-- === HEADER === -->
|
||||
<div class="row mb-5 pb-4 align-items-center" style="border-bottom: 2px solid var(--border-default);">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-1">Payment To Beneficiary:</h6>
|
||||
<h2 class="fw-bold text-dark mb-0 text-uppercase">{{ record.worker.name }}</h2>
|
||||
<p class="text-muted small mb-0">{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip No. #{{ record.id|stringformat:"06d" }}</p>
|
||||
<div class="stat-label mb-1">Payment To Beneficiary:</div>
|
||||
<h2 class="fw-bold mb-0 text-uppercase">{{ record.worker.name }}</h2>
|
||||
<p class="mb-0" style="color: var(--text-tertiary); font-size: 0.85rem;">
|
||||
{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip No. #{{ record.id|stringformat:"06d" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end mt-3 mt-md-0">
|
||||
<h3 class="fw-bold text-uppercase text-secondary mb-1">{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip</h3>
|
||||
<h3 class="fw-bold text-uppercase" style="color: var(--text-secondary);">
|
||||
{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip
|
||||
</h3>
|
||||
<div class="fw-bold">{{ record.date|date:"F j, Y" }}</div>
|
||||
<div class="text-muted small">Payer: Fox Fitt</div>
|
||||
<div style="color: var(--text-tertiary); font-size: 0.85rem;">Payer: Fox Fitt</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === WORKER DETAILS + NET PAY === -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-3">Beneficiary Details:</h6>
|
||||
<div class="stat-label mb-2">Beneficiary Details:</div>
|
||||
<h4 class="fw-bold">{{ record.worker.name }}</h4>
|
||||
<p class="mb-0">ID Number: <strong>{{ record.worker.id_number }}</strong></p>
|
||||
<p class="mb-0">Phone: {{ record.worker.phone_number|default:"—" }}</p>
|
||||
<p class="mb-0" style="color: var(--text-secondary);">Phone: {{ record.worker.phone_number|default:"—" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end mt-4 mt-md-0">
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-3">Net Payable Amount:</h6>
|
||||
<div class="display-6 fw-bold text-dark">R {{ record.amount_paid|floatformat:2 }}</div>
|
||||
<p class="text-success small fw-bold mt-2">
|
||||
<div class="stat-label mb-2">Net Payable Amount:</div>
|
||||
<div style="font-size: 2.5rem; font-weight: 700; font-family: 'Poppins', sans-serif;">
|
||||
R {{ record.amount_paid|floatformat:2 }}
|
||||
</div>
|
||||
<p class="fw-bold mt-2" style="color: var(--color-success); font-size: 0.85rem;">
|
||||
<i class="fas fa-check-circle me-1"></i> PAID
|
||||
</p>
|
||||
</div>
|
||||
@ -56,10 +61,10 @@
|
||||
|
||||
{% if is_advance %}
|
||||
<!-- === ADVANCE PAYMENT DETAIL === -->
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-3">Advance Details</h6>
|
||||
<div class="stat-label mb-3">Advance Details</div>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
@ -70,19 +75,17 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ advance_adj.date|date:"M d, Y" }}</td>
|
||||
<td><span class="badge bg-info text-dark text-uppercase">Advance Payment</span></td>
|
||||
<td><span class="badge" style="background: var(--color-info-bg); color: var(--color-info);">ADVANCE PAYMENT</span></td>
|
||||
<td>{{ advance_adj.description|default:"Salary advance" }}</td>
|
||||
<td class="text-end text-success fw-bold">R {{ advance_adj.amount|floatformat:2 }}</td>
|
||||
<td class="text-end fw-bold" style="color: var(--color-success);">R {{ advance_adj.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- === ADVANCE TOTAL === -->
|
||||
<div class="row justify-content-end mt-4">
|
||||
<div class="col-md-5">
|
||||
<table class="table table-sm border-0">
|
||||
<tr class="border-top border-dark">
|
||||
<tr style="border-top: 2px solid var(--text-primary);">
|
||||
<td class="text-end border-0 fw-bold fs-5">Amount Advanced:</td>
|
||||
<td class="text-end border-0 fw-bold fs-5">R {{ advance_adj.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
@ -92,10 +95,10 @@
|
||||
|
||||
{% elif is_loan %}
|
||||
<!-- === LOAN PAYMENT DETAIL === -->
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-3">Loan Details</h6>
|
||||
<div class="stat-label mb-3">Loan Details</div>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
@ -106,19 +109,17 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ loan_adj.date|date:"M d, Y" }}</td>
|
||||
<td><span class="badge bg-warning text-dark text-uppercase">Loan Payment</span></td>
|
||||
<td><span class="badge" style="background: var(--color-warning-bg); color: var(--color-warning);">LOAN PAYMENT</span></td>
|
||||
<td>{{ loan_adj.description|default:"Worker loan" }}</td>
|
||||
<td class="text-end text-success fw-bold">R {{ loan_adj.amount|floatformat:2 }}</td>
|
||||
<td class="text-end fw-bold" style="color: var(--color-success);">R {{ loan_adj.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- === LOAN TOTAL === -->
|
||||
<div class="row justify-content-end mt-4">
|
||||
<div class="col-md-5">
|
||||
<table class="table table-sm border-0">
|
||||
<tr class="border-top border-dark">
|
||||
<tr style="border-top: 2px solid var(--text-primary);">
|
||||
<td class="text-end border-0 fw-bold fs-5">Loan Amount:</td>
|
||||
<td class="text-end border-0 fw-bold fs-5">R {{ loan_adj.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
@ -127,11 +128,11 @@
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- === WORK LOG TABLE — each day worked === -->
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-3">Work Log Details (Attendance)</h6>
|
||||
<!-- === WORK LOG TABLE === -->
|
||||
<div class="stat-label mb-3">Work Log Details (Attendance)</div>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Project</th>
|
||||
@ -144,19 +145,19 @@
|
||||
<tr>
|
||||
<td>{{ log.date|date:"M d, Y" }}</td>
|
||||
<td>{{ log.project.name }}</td>
|
||||
<td>{{ log.notes|default:"—"|truncatechars:50 }}</td>
|
||||
<td style="color: var(--text-secondary);">{{ log.notes|default:"—"|truncatechars:50 }}</td>
|
||||
<td class="text-end">R {{ record.worker.daily_rate|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">
|
||||
<td colspan="4" class="text-center py-3" style="color: var(--text-tertiary);">
|
||||
<i class="fas fa-info-circle me-1"></i> No work logs in this period.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<tfoot>
|
||||
<tr style="background: var(--bg-inset);">
|
||||
<td colspan="3" class="text-end fw-bold">Base Pay Subtotal</td>
|
||||
<td class="text-end fw-bold">R {{ base_pay|floatformat:2 }}</td>
|
||||
</tr>
|
||||
@ -164,12 +165,12 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- === ADJUSTMENTS TABLE — bonuses, deductions, overtime, loan repayments === -->
|
||||
<!-- === ADJUSTMENTS TABLE === -->
|
||||
{% if adjustments %}
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-3 mt-4">Adjustments (Bonuses, Deductions, Loans)</h6>
|
||||
<div class="stat-label mb-3 mt-4">Adjustments (Bonuses, Deductions, Loans)</div>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
@ -182,15 +183,13 @@
|
||||
<tr>
|
||||
<td>{{ adj.date|date:"M d, Y" }}</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary text-uppercase">{{ adj.get_type_display }}</span>
|
||||
<span class="badge" style="background: var(--bg-inset); color: var(--text-secondary); border: 1px solid var(--border-default);">
|
||||
{{ adj.get_type_display|upper }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ adj.description }}</td>
|
||||
<td class="text-end {% if adj.type in deductive_types %}text-danger{% else %}text-success{% endif %}">
|
||||
{% if adj.type in deductive_types %}
|
||||
- R {{ adj.amount|floatformat:2 }}
|
||||
{% else %}
|
||||
+ R {{ adj.amount|floatformat:2 }}
|
||||
{% endif %}
|
||||
<td class="text-end fw-semibold {% if adj.type in deductive_types %}{% else %}{% endif %}" style="color: {% if adj.type in deductive_types %}var(--color-danger){% else %}var(--color-success){% endif %};">
|
||||
{% if adj.type in deductive_types %}- R {{ adj.amount|floatformat:2 }}{% else %}+ R {{ adj.amount|floatformat:2 }}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -199,27 +198,23 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- === GRAND TOTAL SUMMARY === -->
|
||||
<!-- === GRAND TOTAL === -->
|
||||
<div class="row justify-content-end mt-4">
|
||||
<div class="col-md-5">
|
||||
<table class="table table-sm border-0">
|
||||
<tr>
|
||||
<td class="text-end border-0 text-muted">Base Pay:</td>
|
||||
<td class="text-end border-0" style="color: var(--text-secondary);">Base Pay:</td>
|
||||
<td class="text-end border-0" width="140">R {{ base_pay|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% if adjustments %}
|
||||
<tr>
|
||||
<td class="text-end border-0 text-muted">Adjustments Net:</td>
|
||||
<td class="text-end border-0" style="color: var(--text-secondary);">Adjustments Net:</td>
|
||||
<td class="text-end border-0">
|
||||
{% if adjustments_net >= 0 %}
|
||||
+ R {{ adjustments_net|floatformat:2 }}
|
||||
{% else %}
|
||||
- R {{ adjustments_net_abs|floatformat:2 }}
|
||||
{% endif %}
|
||||
{% if adjustments_net >= 0 %}+ R {{ adjustments_net|floatformat:2 }}{% else %}- R {{ adjustments_net_abs|floatformat:2 }}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="border-top border-dark">
|
||||
<tr style="border-top: 2px solid var(--text-primary);">
|
||||
<td class="text-end border-0 fw-bold fs-5">Net Payable:</td>
|
||||
<td class="text-end border-0 fw-bold fs-5">R {{ record.amount_paid|floatformat:2 }}</td>
|
||||
</tr>
|
||||
@ -229,9 +224,9 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- === FOOTER === -->
|
||||
<div class="text-center text-muted small mt-5 pt-4 border-top">
|
||||
<div class="text-center mt-5 pt-4" style="border-top: 1px solid var(--border-default); color: var(--text-tertiary); font-size: 0.85rem;">
|
||||
<p>This is a computer-generated document and does not require a signature.</p>
|
||||
<p>Payer: Fox Fitt © 2026</p>
|
||||
<p>Payer: Fox Fitt © {% now "Y" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Work History | Fox Fitt{% endblock %}
|
||||
{% block title %}Work History | FoxFitt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- === WORK HISTORY PAGE ===
|
||||
@ -13,9 +13,9 @@
|
||||
<div class="container py-4">
|
||||
|
||||
{# === PAGE HEADER with view toggle and export === #}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0" style="font-family: 'Poppins', sans-serif;">Work History</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h1 class="page-title"><i class="fas fa-clock me-2" style="color: var(--accent);"></i>Work History</h1>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
{# View toggle — List vs Calendar #}
|
||||
<div class="btn-group" role="group" aria-label="View mode">
|
||||
<a href="?view=list{{ filter_params }}"
|
||||
@ -27,59 +27,48 @@
|
||||
<i class="fas fa-calendar-alt me-1"></i> Calendar
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# CSV Export button — keeps the current filters in the export URL #}
|
||||
<a href="{% url 'export_work_log_csv' %}?worker={{ selected_worker }}&project={{ selected_project }}&status={{ selected_status }}"
|
||||
class="btn btn-outline-success btn-sm shadow-sm">
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-file-csv me-1"></i> Export CSV
|
||||
</a>
|
||||
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm shadow-sm">
|
||||
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left fa-sm me-1"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# === FILTER BAR === #}
|
||||
<div class="card shadow-sm border-0 mb-4{% if has_active_filters %} border-start border-3{% endif %}" {% if has_active_filters %}style="border-left-color: var(--accent, #10b981) !important;"{% endif %}>
|
||||
<div class="card mb-4{% if has_active_filters %} border-start border-3{% endif %}" {% if has_active_filters %}style="border-left-color: var(--accent) !important;"{% endif %}>
|
||||
<div class="card-body py-3">
|
||||
<form method="GET" action="{% url 'work_history' %}" class="row g-2 align-items-end">
|
||||
{# Preserve current view mode when filtering #}
|
||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||
{% if view_mode == 'calendar' %}
|
||||
{# Preserve current calendar month when filtering #}
|
||||
<input type="hidden" name="year" value="{{ curr_year }}">
|
||||
<input type="hidden" name="month" value="{{ curr_month }}">
|
||||
{% endif %}
|
||||
|
||||
{# Filter by Worker #}
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted mb-1">Worker</label>
|
||||
<label class="form-label">Worker</label>
|
||||
<select name="worker" class="form-select form-select-sm">
|
||||
<option value="">All Workers</option>
|
||||
{% for w in filter_workers %}
|
||||
<option value="{{ w.id }}" {% if selected_worker == w.id|stringformat:"d" %}selected{% endif %}>
|
||||
{{ w.name }}
|
||||
</option>
|
||||
<option value="{{ w.id }}" {% if selected_worker == w.id|stringformat:"d" %}selected{% endif %}>{{ w.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Filter by Project #}
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted mb-1">Project</label>
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project" class="form-select form-select-sm">
|
||||
<option value="">All Projects</option>
|
||||
{% for p in filter_projects %}
|
||||
<option value="{{ p.id }}" {% if selected_project == p.id|stringformat:"d" %}selected{% endif %}>
|
||||
{{ p.name }}
|
||||
</option>
|
||||
<option value="{{ p.id }}" {% if selected_project == p.id|stringformat:"d" %}selected{% endif %}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Filter by Payment Status #}
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted mb-1">Payment Status</label>
|
||||
<label class="form-label">Payment Status</label>
|
||||
<select name="status" class="form-select form-select-sm">
|
||||
<option value="" {% if not selected_status %}selected{% endif %}>All</option>
|
||||
<option value="paid" {% if selected_status == 'paid' %}selected{% endif %}>Paid</option>
|
||||
@ -87,7 +76,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Filter + Clear Buttons #}
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-sm btn-accent">
|
||||
<i class="fas fa-filter me-1"></i> Filter
|
||||
@ -100,31 +88,28 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# === Active Filter Feedback === #}
|
||||
{# Shows a results counter when filters are active so the user can see the filter is working #}
|
||||
{# Active filter feedback #}
|
||||
{% if has_active_filters %}
|
||||
<div class="mt-2 d-flex align-items-center flex-wrap gap-2">
|
||||
<small class="text-muted">
|
||||
<small style="color: var(--text-secondary);">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Showing <strong>{{ filtered_log_count }}</strong> of {{ total_log_count }} work log{{ total_log_count|pluralize }}
|
||||
</small>
|
||||
{# Show which filters are active as small badges #}
|
||||
{% if selected_worker %}
|
||||
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25">
|
||||
<span class="badge" style="background: var(--color-info-bg); color: var(--color-info); border: 1px solid var(--color-info);">
|
||||
<i class="fas fa-user fa-xs me-1"></i>
|
||||
{% for w in filter_workers %}{% if w.id|stringformat:"d" == selected_worker %}{{ w.name }}{% endif %}{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if selected_project %}
|
||||
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25">
|
||||
<span class="badge" style="background: var(--color-success-bg); color: var(--color-success); border: 1px solid var(--color-success);">
|
||||
<i class="fas fa-project-diagram fa-xs me-1"></i>
|
||||
{% for p in filter_projects %}{% if p.id|stringformat:"d" == selected_project %}{{ p.name }}{% endif %}{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if selected_status %}
|
||||
<span class="badge bg-warning bg-opacity-10 text-dark border border-warning border-opacity-25">
|
||||
<i class="fas fa-tag fa-xs me-1"></i>
|
||||
{{ selected_status|capfirst }}
|
||||
<span class="badge" style="background: var(--color-warning-bg); color: var(--color-warning); border: 1px solid var(--color-warning);">
|
||||
<i class="fas fa-tag fa-xs me-1"></i>{{ selected_status|capfirst }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -138,17 +123,15 @@
|
||||
{# === CALENDAR VIEW === #}
|
||||
{# =============================================================== #}
|
||||
|
||||
{# Month navigation header #}
|
||||
<div class="card shadow-sm border-0 mb-3">
|
||||
{# Month navigation #}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="?view=calendar&year={{ prev_year }}&month={{ prev_month }}{{ filter_params }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
<h5 class="mb-0 fw-bold" style="font-family: 'Poppins', sans-serif;">
|
||||
{{ month_name }}
|
||||
</h5>
|
||||
<h5 class="mb-0 fw-bold" style="font-family: 'Poppins', sans-serif;">{{ month_name }}</h5>
|
||||
<a href="?view=calendar&year={{ next_year }}&month={{ next_month }}{{ filter_params }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
@ -158,10 +141,10 @@
|
||||
</div>
|
||||
|
||||
{# Calendar grid #}
|
||||
<div class="card shadow-sm border-0 mb-3">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body p-0 p-md-3">
|
||||
{# Day-of-week header row #}
|
||||
<div class="row g-0 d-none d-md-flex text-center fw-bold text-secondary border-bottom pb-2 mb-2" style="font-size: 0.85rem;">
|
||||
{# Day-of-week header #}
|
||||
<div class="row g-0 d-none d-md-flex text-center fw-bold border-bottom pb-2 mb-2" style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
<div class="col">Mon</div>
|
||||
<div class="col">Tue</div>
|
||||
<div class="col">Wed</div>
|
||||
@ -171,36 +154,32 @@
|
||||
<div class="col">Sun</div>
|
||||
</div>
|
||||
|
||||
{# Calendar weeks — each row is 7 day cells #}
|
||||
{% for week in calendar_weeks %}
|
||||
<div class="row g-0 g-md-1 mb-0 mb-md-1">
|
||||
{% for day in week %}
|
||||
<div class="col cal-day {% if not day.is_current_month %}cal-day--other{% endif %}{% if day.is_today %} cal-day--today{% endif %}{% if day.count > 0 %} cal-day--has-logs{% endif %}"
|
||||
{% if day.count > 0 %}data-date="{{ day.date|date:'Y-m-d' }}"{% endif %}>
|
||||
{# Day number + badge count #}
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<span class="cal-day__number {% if day.is_today %}fw-bold{% endif %}">{{ day.day }}</span>
|
||||
<span class="cal-day__number">{{ day.day }}</span>
|
||||
{% if day.count > 0 %}
|
||||
<span class="badge bg-primary rounded-pill" style="font-size: 0.65rem;">{{ day.count }}</span>
|
||||
<span class="badge rounded-pill" style="font-size: 0.65rem; background: var(--accent);">{{ day.count }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{# Mini log indicators (show first 3 entries) #}
|
||||
{% for log in day.records|slice:":3" %}
|
||||
<div class="cal-entry text-truncate" title="{{ log.project.name }}">
|
||||
<small>
|
||||
{% if log.payroll_records.exists %}
|
||||
<i class="fas fa-check-circle text-success" style="font-size: 0.55rem;"></i>
|
||||
<i class="fas fa-check-circle" style="font-size: 0.55rem; color: var(--color-success);"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-clock text-warning" style="font-size: 0.55rem;"></i>
|
||||
<i class="fas fa-clock" style="font-size: 0.55rem; color: var(--color-warning);"></i>
|
||||
{% endif %}
|
||||
{{ log.project.name }}
|
||||
</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{# "and X more" indicator #}
|
||||
{% if day.count > 3 %}
|
||||
<div class="cal-entry">
|
||||
<small class="text-muted">+{{ day.count|add:"-3" }} more</small>
|
||||
<small style="color: var(--text-tertiary);">+{{ day.count|add:"-3" }} more</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -211,62 +190,49 @@
|
||||
</div>
|
||||
|
||||
{# === Day Detail Panel === #}
|
||||
{# Hidden by default. Click day cells to select them — shows combined details with totals. #}
|
||||
<div class="card shadow-sm border-0 d-none" id="dayDetailPanel">
|
||||
<div class="card-header py-2 bg-white">
|
||||
<div class="card d-none" id="dayDetailPanel">
|
||||
<div class="card-header py-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0 fw-bold" id="dayDetailTitle">
|
||||
<i class="fas fa-calendar-day me-2"></i>Details
|
||||
</h6>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<span class="badge bg-primary rounded-pill d-none" id="daySelectionCount"></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearDaySelection"
|
||||
title="Clear selection">
|
||||
<span class="badge rounded-pill d-none" style="background: var(--accent);" id="daySelectionCount"></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearDaySelection" title="Clear selection">
|
||||
<i class="fas fa-times-circle me-1"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{# Hint text for multi-select #}
|
||||
<small class="text-muted d-block mt-1" id="multiSelectHint">
|
||||
<small class="d-block mt-1" style="color: var(--text-tertiary);" id="multiSelectHint">
|
||||
<i class="fas fa-info-circle me-1"></i>Click more days to add them to the selection
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-body p-0" id="dayDetailBody">
|
||||
{# Content built by JavaScript #}
|
||||
</div>
|
||||
{# === Totals Footer (admin only, shown when days are selected) === #}
|
||||
<div class="card-body p-0" id="dayDetailBody"></div>
|
||||
{% if is_admin %}
|
||||
<div class="card-footer bg-white border-top d-none" id="dayDetailFooter">
|
||||
<div class="card-footer border-top d-none" id="dayDetailFooter">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>Total:</strong>
|
||||
<span class="text-muted ms-2" id="totalDays">0 days</span>
|
||||
<span class="text-muted mx-1">·</span>
|
||||
<span class="text-muted" id="totalLogs">0 logs</span>
|
||||
<span class="text-muted mx-1">·</span>
|
||||
<span class="text-muted" id="totalWorkers">0 unique workers</span>
|
||||
<span class="ms-2" style="color: var(--text-secondary);" id="totalDays">0 days</span>
|
||||
<span class="mx-1" style="color: var(--text-tertiary);">·</span>
|
||||
<span style="color: var(--text-secondary);" id="totalLogs">0 logs</span>
|
||||
<span class="mx-1" style="color: var(--text-tertiary);">·</span>
|
||||
<span style="color: var(--text-secondary);" id="totalWorkers">0 unique workers</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="fs-5" style="color: var(--accent-color, #10b981);" id="totalAmount">R 0.00</strong>
|
||||
<strong class="fs-5" style="color: var(--accent);" id="totalAmount">R 0.00</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Pass calendar detail data to JavaScript safely using json_script #}
|
||||
{{ calendar_detail|json_script:"calDetailJson" }}
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// === CALENDAR MULTI-DAY SELECTION ===
|
||||
// Click a day to add it to the selection. Click again to deselect.
|
||||
// The detail panel shows combined data from ALL selected days.
|
||||
// Admin users see a total amount across all selected days.
|
||||
|
||||
// Parse calendar detail data (keyed by date string, e.g. "2026-02-22")
|
||||
var calDetail = JSON.parse(document.getElementById('calDetailJson').textContent);
|
||||
var detailPanel = document.getElementById('dayDetailPanel');
|
||||
var detailTitle = document.getElementById('dayDetailTitle');
|
||||
@ -276,80 +242,49 @@
|
||||
var multiSelectHint = document.getElementById('multiSelectHint');
|
||||
var isAdmin = {{ is_admin|yesno:"true,false" }};
|
||||
var detailFooter = document.getElementById('dayDetailFooter');
|
||||
|
||||
// Track which dates are currently selected (array of date strings)
|
||||
var selectedDates = [];
|
||||
|
||||
// Short month names for formatting dates
|
||||
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
|
||||
// === Format a date string (YYYY-MM-DD) for display (e.g. "22 Feb") ===
|
||||
function formatDateShort(dateStr) {
|
||||
var parts = dateStr.split('-');
|
||||
var day = parseInt(parts[2], 10);
|
||||
var monthIdx = parseInt(parts[1], 10) - 1;
|
||||
return day + ' ' + months[monthIdx];
|
||||
return parseInt(parts[2], 10) + ' ' + months[parseInt(parts[1], 10) - 1];
|
||||
}
|
||||
|
||||
// === Format a date string for longer display (e.g. "22 Feb 2026") ===
|
||||
function formatDateLong(dateStr) {
|
||||
var parts = dateStr.split('-');
|
||||
var day = parseInt(parts[2], 10);
|
||||
var monthIdx = parseInt(parts[1], 10) - 1;
|
||||
return day + ' ' + months[monthIdx] + ' ' + parts[0];
|
||||
return parseInt(parts[2], 10) + ' ' + months[parseInt(parts[1], 10) - 1] + ' ' + parts[0];
|
||||
}
|
||||
|
||||
// === Update the detail panel with data from all selected dates ===
|
||||
function updateDetailPanel() {
|
||||
if (selectedDates.length === 0) {
|
||||
// Nothing selected — hide the panel
|
||||
detailPanel.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort selected dates chronologically
|
||||
selectedDates.sort();
|
||||
|
||||
// Collect all entries from all selected dates
|
||||
var allEntries = [];
|
||||
var totalAmount = 0;
|
||||
var uniqueWorkers = {};
|
||||
|
||||
selectedDates.forEach(function(dateStr) {
|
||||
var entries = calDetail[dateStr] || [];
|
||||
entries.forEach(function(entry) {
|
||||
// Tag each entry with its date for display
|
||||
(calDetail[dateStr] || []).forEach(function(entry) {
|
||||
allEntries.push({ date: dateStr, entry: entry });
|
||||
// Track unique workers
|
||||
entry.workers.forEach(function(w) {
|
||||
uniqueWorkers[w] = true;
|
||||
});
|
||||
// Sum amounts (admin only)
|
||||
if (isAdmin && entry.amount !== undefined) {
|
||||
totalAmount += entry.amount;
|
||||
}
|
||||
entry.workers.forEach(function(w) { uniqueWorkers[w] = true; });
|
||||
if (isAdmin && entry.amount !== undefined) totalAmount += entry.amount;
|
||||
});
|
||||
});
|
||||
|
||||
// === Update panel title ===
|
||||
// Title
|
||||
detailTitle.textContent = '';
|
||||
var icon = document.createElement('i');
|
||||
icon.className = 'fas fa-calendar-day me-2';
|
||||
detailTitle.appendChild(icon);
|
||||
|
||||
if (selectedDates.length === 1) {
|
||||
// Single day: show full date
|
||||
detailTitle.appendChild(document.createTextNode(
|
||||
formatDateLong(selectedDates[0]) + ' — ' + allEntries.length + ' log(s)'
|
||||
));
|
||||
detailTitle.appendChild(document.createTextNode(formatDateLong(selectedDates[0]) + ' \u2014 ' + allEntries.length + ' log(s)'));
|
||||
} else {
|
||||
// Multiple days: show date range or count
|
||||
detailTitle.appendChild(document.createTextNode(
|
||||
selectedDates.length + ' days selected — ' + allEntries.length + ' log(s)'
|
||||
));
|
||||
detailTitle.appendChild(document.createTextNode(selectedDates.length + ' days selected \u2014 ' + allEntries.length + ' log(s)'));
|
||||
}
|
||||
|
||||
// Update selection count badge
|
||||
if (selectedDates.length > 1) {
|
||||
selCountBadge.textContent = selectedDates.length + ' days';
|
||||
selCountBadge.classList.remove('d-none');
|
||||
@ -359,17 +294,13 @@
|
||||
multiSelectHint.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// === Clear previous content ===
|
||||
// Build table
|
||||
while (detailBody.firstChild) detailBody.removeChild(detailBody.firstChild);
|
||||
|
||||
// === Build detail table ===
|
||||
var table = document.createElement('table');
|
||||
table.className = 'table table-sm table-hover mb-0';
|
||||
|
||||
var thead = document.createElement('thead');
|
||||
thead.className = 'table-light';
|
||||
var headRow = document.createElement('tr');
|
||||
// Show Date column when multiple days are selected
|
||||
var headers = selectedDates.length > 1
|
||||
? ['Date', 'Project', 'Workers', 'Supervisor', 'OT', 'Status']
|
||||
: ['Project', 'Workers', 'Supervisor', 'OT', 'Status'];
|
||||
@ -388,7 +319,6 @@
|
||||
var entry = item.entry;
|
||||
var tr = document.createElement('tr');
|
||||
|
||||
// Date column (only for multi-day selection)
|
||||
if (selectedDates.length > 1) {
|
||||
var tdDate = document.createElement('td');
|
||||
tdDate.className = 'ps-3';
|
||||
@ -396,7 +326,6 @@
|
||||
tr.appendChild(tdDate);
|
||||
}
|
||||
|
||||
// Project
|
||||
var tdProj = document.createElement('td');
|
||||
tdProj.className = selectedDates.length === 1 ? 'ps-3' : '';
|
||||
var strong = document.createElement('strong');
|
||||
@ -404,53 +333,43 @@
|
||||
tdProj.appendChild(strong);
|
||||
tr.appendChild(tdProj);
|
||||
|
||||
// Workers — each name gets a small pill badge for readability
|
||||
var tdWork = document.createElement('td');
|
||||
entry.workers.forEach(function(name) {
|
||||
var pill = document.createElement('span');
|
||||
pill.className = 'badge rounded-pill bg-light text-dark fw-normal border me-1 mb-1';
|
||||
pill.className = 'badge rounded-pill fw-normal me-1 mb-1';
|
||||
pill.style.cssText = 'background: var(--bg-inset); color: var(--text-primary); border: 1px solid var(--border-default);';
|
||||
pill.textContent = name;
|
||||
tdWork.appendChild(pill);
|
||||
});
|
||||
tr.appendChild(tdWork);
|
||||
|
||||
// Supervisor
|
||||
var tdSup = document.createElement('td');
|
||||
tdSup.textContent = entry.supervisor;
|
||||
tr.appendChild(tdSup);
|
||||
|
||||
// Overtime
|
||||
var tdOt = document.createElement('td');
|
||||
if (entry.overtime) {
|
||||
var otBadge = document.createElement('span');
|
||||
otBadge.className = 'badge bg-warning text-dark';
|
||||
otBadge.className = 'badge';
|
||||
otBadge.style.cssText = 'background: var(--color-warning-bg); color: var(--color-warning);';
|
||||
otBadge.textContent = entry.overtime;
|
||||
tdOt.appendChild(otBadge);
|
||||
} else {
|
||||
tdOt.textContent = '-';
|
||||
tdOt.className = 'text-muted';
|
||||
tdOt.style.color = 'var(--text-tertiary)';
|
||||
}
|
||||
tr.appendChild(tdOt);
|
||||
|
||||
// Status
|
||||
var tdStatus = document.createElement('td');
|
||||
var statusBadge = document.createElement('span');
|
||||
if (entry.is_paid) {
|
||||
statusBadge.className = 'badge bg-success';
|
||||
statusBadge.textContent = 'Paid';
|
||||
} else {
|
||||
statusBadge.className = 'badge bg-danger bg-opacity-75';
|
||||
statusBadge.textContent = 'Unpaid';
|
||||
}
|
||||
statusBadge.className = 'badge ' + (entry.is_paid ? 'bg-success' : 'bg-danger bg-opacity-75');
|
||||
statusBadge.textContent = entry.is_paid ? 'Paid' : 'Unpaid';
|
||||
tdStatus.appendChild(statusBadge);
|
||||
tr.appendChild(tdStatus);
|
||||
|
||||
// Amount (admin only)
|
||||
if (isAdmin) {
|
||||
var tdAmt = document.createElement('td');
|
||||
tdAmt.textContent = entry.amount !== undefined
|
||||
? 'R ' + entry.amount.toFixed(2)
|
||||
: '-';
|
||||
tdAmt.textContent = entry.amount !== undefined ? 'R ' + entry.amount.toFixed(2) : '-';
|
||||
tr.appendChild(tdAmt);
|
||||
}
|
||||
|
||||
@ -459,137 +378,55 @@
|
||||
table.appendChild(tbody);
|
||||
detailBody.appendChild(table);
|
||||
|
||||
// === Update totals footer (admin only) ===
|
||||
// Totals footer
|
||||
if (isAdmin && detailFooter) {
|
||||
var totalDaysEl = document.getElementById('totalDays');
|
||||
var totalLogsEl = document.getElementById('totalLogs');
|
||||
var totalWorkersEl = document.getElementById('totalWorkers');
|
||||
var totalAmountEl = document.getElementById('totalAmount');
|
||||
|
||||
var uniqueCount = Object.keys(uniqueWorkers).length;
|
||||
|
||||
totalDaysEl.textContent = selectedDates.length + ' day' + (selectedDates.length !== 1 ? 's' : '');
|
||||
totalLogsEl.textContent = allEntries.length + ' log' + (allEntries.length !== 1 ? 's' : '');
|
||||
totalWorkersEl.textContent = uniqueCount + ' unique worker' + (uniqueCount !== 1 ? 's' : '');
|
||||
totalAmountEl.textContent = 'R ' + totalAmount.toFixed(2);
|
||||
|
||||
document.getElementById('totalDays').textContent = selectedDates.length + ' day' + (selectedDates.length !== 1 ? 's' : '');
|
||||
document.getElementById('totalLogs').textContent = allEntries.length + ' log' + (allEntries.length !== 1 ? 's' : '');
|
||||
document.getElementById('totalWorkers').textContent = uniqueCount + ' unique worker' + (uniqueCount !== 1 ? 's' : '');
|
||||
document.getElementById('totalAmount').textContent = 'R ' + totalAmount.toFixed(2);
|
||||
detailFooter.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// Show the panel and scroll to it
|
||||
detailPanel.classList.remove('d-none');
|
||||
detailPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
// === Click handler for day cells with logs ===
|
||||
// Toggle selection: click to add, click again to remove
|
||||
document.querySelectorAll('.cal-day--has-logs').forEach(function(cell) {
|
||||
cell.addEventListener('click', function() {
|
||||
var dateStr = this.dataset.date;
|
||||
var entries = calDetail[dateStr] || [];
|
||||
if (entries.length === 0) return;
|
||||
|
||||
// Toggle this date in the selection
|
||||
if (!(calDetail[dateStr] || []).length) return;
|
||||
var idx = selectedDates.indexOf(dateStr);
|
||||
if (idx !== -1) {
|
||||
// Already selected — remove it
|
||||
selectedDates.splice(idx, 1);
|
||||
this.classList.remove('cal-day--selected');
|
||||
} else {
|
||||
// Not selected — add it
|
||||
selectedDates.push(dateStr);
|
||||
this.classList.add('cal-day--selected');
|
||||
}
|
||||
|
||||
// Refresh the detail panel with the updated selection
|
||||
updateDetailPanel();
|
||||
});
|
||||
});
|
||||
|
||||
// === Clear all selections ===
|
||||
clearBtn.addEventListener('click', function() {
|
||||
selectedDates = [];
|
||||
document.querySelectorAll('.cal-day--selected').forEach(function(c) {
|
||||
c.classList.remove('cal-day--selected');
|
||||
});
|
||||
document.querySelectorAll('.cal-day--selected').forEach(function(c) { c.classList.remove('cal-day--selected'); });
|
||||
detailPanel.classList.add('d-none');
|
||||
if (detailFooter) detailFooter.classList.add('d-none');
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
{# Calendar-specific CSS #}
|
||||
<style>
|
||||
/* === CALENDAR GRID STYLES === */
|
||||
.cal-day {
|
||||
min-height: 90px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
transition: background-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.cal-day__number {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-main, #334155);
|
||||
}
|
||||
/* Days from previous/next month — faded */
|
||||
.cal-day--other {
|
||||
background-color: #f8fafc;
|
||||
opacity: 0.5;
|
||||
}
|
||||
/* Today's date — accent border */
|
||||
.cal-day--today {
|
||||
border-color: var(--accent-color, #10b981);
|
||||
border-width: 2px;
|
||||
}
|
||||
.cal-day--today .cal-day__number {
|
||||
color: var(--accent-color, #10b981);
|
||||
}
|
||||
/* Days with logs — clickable */
|
||||
.cal-day--has-logs {
|
||||
cursor: pointer;
|
||||
}
|
||||
.cal-day--has-logs:hover {
|
||||
background-color: #f0fdfa;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
/* Selected day */
|
||||
.cal-day--selected {
|
||||
background-color: #ecfdf5 !important;
|
||||
border-color: var(--accent-color, #10b981) !important;
|
||||
border-width: 2px;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15);
|
||||
}
|
||||
/* Mini log entry indicators */
|
||||
.cal-entry {
|
||||
line-height: 1.3;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
/* Mobile: compact cells */
|
||||
@media (max-width: 767.98px) {
|
||||
.cal-day {
|
||||
min-height: 55px;
|
||||
padding: 4px 5px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.cal-entry {
|
||||
display: none; /* Hide text indicators on mobile, just show badges */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
{% else %}
|
||||
{# =============================================================== #}
|
||||
{# === LIST VIEW (TABLE) === #}
|
||||
{# =============================================================== #}
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="ps-4">Date</th>
|
||||
<th scope="col">Project</th>
|
||||
@ -606,25 +443,23 @@
|
||||
<td class="ps-4 align-middle">{{ log.date }}</td>
|
||||
<td class="align-middle"><strong>{{ log.project.name }}</strong></td>
|
||||
<td class="align-middle">
|
||||
{# When filtering by a specific worker, show only that worker. Otherwise show all workers. #}
|
||||
{% if filtered_worker_obj %}
|
||||
<span class="badge rounded-pill bg-light text-dark fw-normal border">{{ filtered_worker_obj.name }}</span>
|
||||
<span class="badge rounded-pill fw-normal" style="background: var(--bg-inset); color: var(--text-primary); border: 1px solid var(--border-default);">{{ filtered_worker_obj.name }}</span>
|
||||
{% else %}
|
||||
{% for w in log.workers.all %}
|
||||
<span class="badge rounded-pill bg-light text-dark fw-normal border me-1 mb-1">{{ w.name }}</span>
|
||||
<span class="badge rounded-pill fw-normal me-1 mb-1" style="background: var(--bg-inset); color: var(--text-primary); border: 1px solid var(--border-default);">{{ w.name }}</span>
|
||||
{% endfor %}
|
||||
<span class="badge rounded-pill bg-secondary">{{ log.workers.count }}</span>
|
||||
<span class="badge rounded-pill" style="background: var(--text-secondary); color: var(--text-on-accent);">{{ log.workers.count }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{% if log.overtime_amount > 0 %}
|
||||
<span class="badge bg-warning text-dark">{{ log.get_overtime_amount_display }}</span>
|
||||
<span class="badge" style="background: var(--color-warning-bg); color: var(--color-warning);">{{ log.get_overtime_amount_display }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
<span style="color: var(--text-tertiary);">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{# Payment status — a WorkLog is "paid" if it has at least one PayrollRecord #}
|
||||
{% if log.payroll_records.exists %}
|
||||
<span class="badge bg-success"><i class="fas fa-check me-1"></i>Paid</span>
|
||||
{% else %}
|
||||
@ -633,11 +468,10 @@
|
||||
</td>
|
||||
{% if is_admin %}
|
||||
<td class="align-middle">
|
||||
{# Daily cost — worker's rate when filtered, otherwise total for all workers #}
|
||||
{% if filtered_worker_obj %}
|
||||
<span class="text-success fw-semibold">R {{ filtered_worker_obj.daily_rate }}</span>
|
||||
<span class="fw-semibold" style="color: var(--color-success);">R {{ filtered_worker_obj.daily_rate }}</span>
|
||||
{% else %}
|
||||
<span class="text-success fw-semibold">R {{ log.display_amount }}</span>
|
||||
<span class="fw-semibold" style="color: var(--color-success);">R {{ log.display_amount }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
@ -645,14 +479,14 @@
|
||||
{% if log.supervisor %}
|
||||
{{ log.supervisor.get_full_name|default:log.supervisor.username }}
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
<span style="color: var(--text-tertiary);">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="{% if is_admin %}7{% else %}6{% endif %}" class="text-center py-5 text-muted">
|
||||
<i class="fas fa-inbox fa-2x mb-3 d-block opacity-50"></i>
|
||||
<td colspan="{% if is_admin %}7{% else %}6{% endif %}" class="text-center py-5" style="color: var(--text-tertiary);">
|
||||
<i class="fas fa-inbox fa-2x mb-3 d-block"></i>
|
||||
No work history found.
|
||||
{% if selected_worker or selected_project or selected_status %}
|
||||
<br><small>Try adjusting your filters.</small>
|
||||
|
||||
@ -1,32 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container d-flex justify-content-center align-items-center min-vh-100">
|
||||
<div class="card shadow-sm" style="width: 100%; max-width: 400px; border-radius: 12px; border: none;">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="text-center mb-4" style="font-family: 'Poppins', sans-serif; font-weight: 700;">
|
||||
<span style="color: #10b981;">Fox</span>Fitt
|
||||
</h2>
|
||||
{% block title %}Login | FoxFitt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- === LOGIN PAGE — full-screen centred with premium orange theme === -->
|
||||
<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg-body); position: relative; overflow: hidden;">
|
||||
|
||||
<!-- Decorative orange glow -->
|
||||
<div style="position: absolute; top: -200px; right: -100px; width: 500px; height: 500px; background: radial-gradient(circle, var(--accent-glow) 0%, transparent 65%); pointer-events: none;"></div>
|
||||
<div style="position: absolute; bottom: -200px; left: -100px; width: 400px; height: 400px; background: radial-gradient(circle, rgba(232, 133, 26, 0.08) 0%, transparent 70%); pointer-events: none;"></div>
|
||||
|
||||
<div class="card" style="width: 100%; max-width: 420px; position: relative; z-index: 1;">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
|
||||
<!-- Brand icon + name -->
|
||||
<div class="text-center mb-4">
|
||||
<div class="sidebar-brand__icon mx-auto mb-3" style="width: 48px; height: 48px; font-size: 1.25rem;">
|
||||
<i class="fas fa-bolt"></i>
|
||||
</div>
|
||||
<h1 class="mb-1" style="font-size: 2rem;">
|
||||
<span style="color: var(--accent); font-weight: 700;">Fox</span><span style="font-weight: 700;">Fitt</span>
|
||||
</h1>
|
||||
<p class="text-muted mb-0" style="font-size: 0.85rem;">Payroll Management System</p>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<div class="alert alert-danger d-flex align-items-center" role="alert" style="font-size: 0.875rem;">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
Your username and password didn't match. Please try again.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Login form -->
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="id_username" class="form-label" style="font-family: 'Inter', sans-serif; font-weight: 500;">Username</label>
|
||||
<input type="text" name="username" class="form-control form-control-lg" id="id_username" required autofocus style="border-radius: 8px;">
|
||||
<label for="id_username" class="form-label">Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-user" style="width: 1rem; text-align: center;"></i></span>
|
||||
<input type="text" name="username" class="form-control form-control-lg" id="id_username" placeholder="Enter username" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="id_password" class="form-label" style="font-family: 'Inter', sans-serif; font-weight: 500;">Password</label>
|
||||
<input type="password" name="password" class="form-control form-control-lg" id="id_password" required style="border-radius: 8px;">
|
||||
<label for="id_password" class="form-label">Password</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-lock" style="width: 1rem; text-align: center;"></i></span>
|
||||
<input type="password" name="password" class="form-control form-control-lg" id="id_password" placeholder="Enter password" required>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-lg w-100 text-white" style="background-color: #10b981; border: none; border-radius: 8px; font-weight: 600;">Login</button>
|
||||
<button type="submit" class="btn btn-accent btn-lg w-100">
|
||||
<i class="fas fa-sign-in-alt me-2"></i>Login
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Theme toggle (since sidebar isn't visible on login page) -->
|
||||
<div class="text-center mt-4 pt-3" style="border-top: 1px solid var(--border-subtle);">
|
||||
<button type="button" class="theme-toggle" id="loginThemeToggle" style="border-color: var(--border-default); color: var(--text-secondary); margin: 0 auto;">
|
||||
<i class="fas fa-moon" id="loginThemeIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- Theme toggle for login page (sidebar toggle is hidden when not authenticated) -->
|
||||
<script>
|
||||
(function() {
|
||||
var toggle = document.getElementById('loginThemeToggle');
|
||||
var icon = document.getElementById('loginThemeIcon');
|
||||
if (!toggle || !icon) return;
|
||||
|
||||
function updateLoginToggle() {
|
||||
var isDark = document.documentElement.getAttribute('data-theme') !== 'light';
|
||||
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
|
||||
updateLoginToggle();
|
||||
|
||||
toggle.addEventListener('click', function() {
|
||||
var current = document.documentElement.getAttribute('data-theme');
|
||||
var next = (current === 'light') ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('foxfitt-theme', next);
|
||||
updateLoginToggle();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user