Complete working state of the session. Will be split into two deploy phases (safety scaffolding then feature release) before merging to ai-dev. Includes: - Security fixes (email creds / SECRET_KEY / DEBUG / CSRF) - Backup + restore management commands and browser endpoints - WeasyPrint migration (replaces xhtml2pdf) - New Worker fields + WorkerCertificate + WorkerWarning models - Worker / Team / Project friendly management UIs - Dashboard cert-expiry card + Manage All buttons - Bootstrap tooltips (global init + theme-aware CSS) - Django admin template override (taller M2M pickers) - Money filter for ZAR currency formatting - Resources dropdown nav - Massive CLAUDE.md expansion + deploy plan docs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
608 lines
30 KiB
HTML
608 lines
30 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Dashboard | FoxFitt{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container py-4">
|
|
|
|
<!-- === DASHBOARD HEADER — welcome + CTA button === -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
|
<div>
|
|
<h1 class="page-title"><i class="fas fa-th-large me-2" style="color: var(--accent);"></i>Dashboard</h1>
|
|
<p class="mb-0" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
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
|
|
</a>
|
|
</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>
|
|
{# 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>
|
|
|
|
<!-- 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 class="stat-icon stat-icon--success">
|
|
<i class="fas fa-check-circle"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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="stat-icon stat-icon--warning">
|
|
<i class="fas fa-hand-holding-usd"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Certifications Expiring — shown ONLY when count > 0
|
|
Clicking it goes to the Worker Batch Report which shows per-worker cert columns. -->
|
|
{% if certs_alert_total %}
|
|
<div class="col-xl-3 col-md-6">
|
|
<a href="{% url 'worker_batch_report' %}?status=active" class="stat-card stat-card--danger h-100 p-3 d-block" style="text-decoration: none; color: inherit;">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="stat-label">Certifications Need Attention</div>
|
|
<div class="stat-value" style="font-size: 1.5rem;">{{ certs_alert_total }}</div>
|
|
<div style="font-size: 0.75rem; margin-top: 0.35rem; color: var(--text-secondary);">
|
|
{% if certs_expired_count %}{{ certs_expired_count }} expired{% endif %}
|
|
{% if certs_expired_count and certs_expiring_count %} | {% endif %}
|
|
{% if certs_expiring_count %}{{ certs_expiring_count }} expiring in 30 days{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="stat-icon stat-icon--danger">
|
|
<i class="fas fa-certificate"></i>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- 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 class="stat-icon stat-icon--info ms-2">
|
|
<i class="fas fa-chart-pie"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- === ROW 2: This Week + Quick Actions === -->
|
|
<div class="row g-3 mb-4">
|
|
<!-- This Week Summary -->
|
|
<div class="col-lg-4 mb-3 mb-lg-0">
|
|
<div class="card h-100">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 fw-bold"><i class="fas fa-calendar-week me-2" style="color: var(--accent);"></i>This Week</h6>
|
|
</div>
|
|
<div class="card-body text-center d-flex flex-column justify-content-center">
|
|
<div class="stat-value" style="font-size: 2.5rem; color: var(--accent);">{{ this_week_logs }}</div>
|
|
<div style="color: var(--text-secondary); font-size: 0.85rem;">Work Logs Created</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="col-lg-8">
|
|
<div class="card h-100">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 fw-bold"><i class="fas fa-bolt me-2" style="color: var(--accent);"></i>Quick Actions</h6>
|
|
</div>
|
|
<div class="card-body d-flex align-items-center justify-content-center gap-3 flex-wrap">
|
|
<a href="{% url 'attendance_log' %}" class="quick-action">
|
|
<i class="fas fa-clipboard-list"></i>
|
|
<span>Log Work</span>
|
|
</a>
|
|
<a href="{% url 'payroll_dashboard' %}" class="quick-action">
|
|
<i class="fas fa-money-check-alt"></i>
|
|
<span>Run Payroll</span>
|
|
</a>
|
|
<a href="{% url 'work_history' %}" class="quick-action">
|
|
<i class="fas fa-history"></i>
|
|
<span>View History</span>
|
|
</a>
|
|
<a href="{% url 'create_receipt' %}" class="quick-action">
|
|
<i class="fas fa-receipt"></i>
|
|
<span>New Receipt</span>
|
|
</a>
|
|
<a href="#" class="quick-action" data-bs-toggle="modal" data-bs-target="#reportConfigModal">
|
|
<i class="fas fa-file-alt"></i>
|
|
<span>Generate Report</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 -->
|
|
<!-- Note: the worker CSV export lives on the Workers page now
|
|
(nav: Workers → Export CSV). Dashboard card stays focused on
|
|
toggling active/inactive status, not on data export. -->
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card h-100">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 fw-bold"><i class="fas fa-sliders-h me-2" style="color: var(--accent);"></i>Manage Resources</h6>
|
|
</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 class="text-center py-2 border-top">
|
|
<a href="{% url 'worker_list' %}" class="small fw-semibold" style="color: var(--accent); text-decoration: none;">
|
|
<i class="fas fa-arrow-right me-1"></i>Manage All Workers
|
|
</a>
|
|
</div>
|
|
</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 class="text-center py-2 border-top">
|
|
<a href="{% url 'project_list' %}" class="small fw-semibold" style="color: var(--accent); text-decoration: none;">
|
|
<i class="fas fa-arrow-right me-1"></i>Manage All Projects
|
|
</a>
|
|
</div>
|
|
</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 class="text-center py-2 border-top">
|
|
<a href="{% url 'team_list' %}" class="small fw-semibold" style="color: var(--accent); text-decoration: none;">
|
|
<i class="fas fa-arrow-right me-1"></i>Manage All Teams
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# === CONFIRM DEACTIVATION MODAL (Bootstrap — works in all browsers unlike confirm()) === #}
|
|
<div class="modal fade" id="confirmDeactivateModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-sm modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-body text-center py-4">
|
|
<i class="fas fa-exclamation-triangle fa-2x mb-3" style="color: var(--color-warning);"></i>
|
|
<p class="mb-1 fw-bold" id="deactivateTitle">Deactivate?</p>
|
|
<p class="text-muted small mb-0">This will hide it from forms and dropdowns.</p>
|
|
</div>
|
|
<div class="modal-footer justify-content-center border-0 pt-0">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-sm btn-danger" id="confirmDeactivateBtn">Deactivate</button>
|
|
</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) ===
|
|
var currentFilter = 'active';
|
|
var filterBtns = document.querySelectorAll('#resourceFilter button');
|
|
|
|
function applyFilter() {
|
|
document.querySelectorAll('.resource-row').forEach(function(row) {
|
|
var isActive = row.dataset.active === 'true';
|
|
var show = false;
|
|
if (currentFilter === 'all') show = true;
|
|
else if (currentFilter === 'active') show = isActive;
|
|
else if (currentFilter === 'inactive') show = !isActive;
|
|
if (show) {
|
|
row.classList.remove('resource-hidden');
|
|
} else {
|
|
row.classList.add('resource-hidden');
|
|
}
|
|
});
|
|
// 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 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 && visible.length === 0) ? '' : 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
filterBtns.forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
filterBtns.forEach(function(b) { b.classList.remove('active'); });
|
|
this.classList.add('active');
|
|
currentFilter = this.dataset.filter;
|
|
applyFilter();
|
|
});
|
|
});
|
|
|
|
applyFilter();
|
|
|
|
// === TOGGLE HANDLER — AJAX POST to activate/deactivate resources ===
|
|
//
|
|
// How this works — and why the old code had a bug:
|
|
// ---------------------------------------------------
|
|
// The authoritative state of a resource lives on the row's
|
|
// `data-active` attribute (written by the server). The checkbox's
|
|
// `checked` property is just a visual mirror that the browser
|
|
// flips when the user clicks.
|
|
//
|
|
// We determine intent by comparing the row's CURRENT state
|
|
// (data-active) against the user's CLICK (which flips it):
|
|
// wasActive=false + click → intent is "activate" (no confirm)
|
|
// wasActive=true + click → intent is "deactivate" (confirm first)
|
|
//
|
|
// The old code read `this.checked` directly, which works in the
|
|
// happy path but got confused after a confirmed-deactivation
|
|
// because the hidden.bs.modal handler was re-setting checked=true
|
|
// whether the modal was cancelled OR confirmed. That desynced the
|
|
// UI from the server, and subsequent clicks fired deactivate
|
|
// modals on what looked to the user like "reactivation".
|
|
//
|
|
// Uses a Bootstrap modal for deactivation confirmation (native
|
|
// confirm() is blocked by Chrome in some popup-blocker configs).
|
|
var deactivateModal = new bootstrap.Modal(document.getElementById('confirmDeactivateModal'));
|
|
var deactivateTitle = document.getElementById('deactivateTitle');
|
|
var confirmBtn = document.getElementById('confirmDeactivateBtn');
|
|
var pendingSwitch = null; // toggle awaiting confirmation
|
|
var userConfirmed = false; // true if the user clicked "Deactivate" (not "Cancel")
|
|
|
|
// User confirmed deactivation — do the AJAX call
|
|
confirmBtn.addEventListener('click', function() {
|
|
userConfirmed = true;
|
|
deactivateModal.hide();
|
|
if (pendingSwitch) {
|
|
doToggle(pendingSwitch, false);
|
|
}
|
|
});
|
|
|
|
// Modal closed — either via Confirm, Cancel, Esc, or backdrop click.
|
|
// Only revert the toggle visually if the user CANCELLED.
|
|
document.getElementById('confirmDeactivateModal').addEventListener('hidden.bs.modal', function() {
|
|
if (pendingSwitch && !userConfirmed) {
|
|
// User cancelled — snap toggle back to checked (its pre-click state)
|
|
pendingSwitch.checked = true;
|
|
}
|
|
pendingSwitch = null;
|
|
userConfirmed = false;
|
|
});
|
|
|
|
// Shared function that performs the actual AJAX toggle.
|
|
// `wantsActive` is the desired NEW state (true = activate, false = deactivate).
|
|
function doToggle(switchEl, wantsActive) {
|
|
var type = switchEl.getAttribute('data-type');
|
|
var id = switchEl.getAttribute('data-id');
|
|
var row = switchEl.closest('.resource-row');
|
|
|
|
fetch('/toggle/' + type + '/' + id + '/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': '{{ csrf_token }}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(function(response) {
|
|
if (!response.ok) throw new Error('Network error');
|
|
return response.json();
|
|
})
|
|
.then(function(data) {
|
|
if (data.status === 'success') {
|
|
// Keep data-active and the checkbox visual in sync with the server
|
|
row.dataset.active = wantsActive ? 'true' : 'false';
|
|
switchEl.checked = wantsActive;
|
|
applyFilter();
|
|
} else {
|
|
// Server rejected — revert the checkbox to its pre-click state
|
|
switchEl.checked = !wantsActive;
|
|
}
|
|
})
|
|
.catch(function() {
|
|
// Network error — revert the checkbox to its pre-click state
|
|
switchEl.checked = !wantsActive;
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.toggle-active').forEach(function(switchEl) {
|
|
switchEl.addEventListener('change', function() {
|
|
var row = this.closest('.resource-row');
|
|
// Use data-active (server truth) as the "before" state, not
|
|
// this.checked (which has already been flipped by the browser).
|
|
var wasActive = row.dataset.active === 'true';
|
|
var wantsActive = !wasActive; // user's intent = flip it
|
|
|
|
if (wantsActive) {
|
|
// Reactivating — no confirmation needed
|
|
doToggle(this, true);
|
|
} else {
|
|
// Deactivating — show Bootstrap confirmation modal
|
|
var type = this.getAttribute('data-type');
|
|
var name = row.querySelector('.fw-medium').textContent.trim();
|
|
deactivateTitle.textContent = 'Deactivate ' + type + ' "' + name + '"?';
|
|
pendingSwitch = this;
|
|
userConfirmed = false;
|
|
deactivateModal.show();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<!-- === REPORT CONFIGURATION MODAL === -->
|
|
<!-- Extracted to a shared partial so the report page can use the same
|
|
modal without duplicating the HTML or the toggle script. -->
|
|
{% include 'core/_report_config_modal.html' %}
|
|
|
|
{% endblock %}
|