Files
JQC_multi_tenant/app/templates/dashboard.html
T

531 lines
24 KiB
HTML

{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="row mb-3 align-items-center">
<div class="col">
<h2 class="mb-0">Welcome, {{ current_user.display_name }}!</h2>
<span class="badge bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'director' %}warning{% elif current_user.role == 'project_manager' %}primary{% elif current_user.role == 'customer' %}success{% else %}info{% endif %} mt-1">
{{ current_user.role.replace('_',' ')|title }}
</span>
</div>
</div>
{# ── Inspections section ─────────────────────────────────────────────────── #}
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-clipboard-data-fill text-primary"></i>
<span class="fw-bold text-uppercase" style="font-size:.78rem;letter-spacing:.07em;color:#64748b;">Inspections</span>
<div style="flex:1;height:1px;background:#e2e8f0;"></div>
</div>
<div class="row g-3 mb-4">
<div class="col-6 col-md">
<a href="{{ url_for('inspections.index', date_from=today_str, date_to=today_str) }}" class="text-decoration-none">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small text-white-50 fw-semibold">Today's Inspections</span>
<i class="bi bi-clipboard-data" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ today_inspections }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">All inspections started today</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
<a href="{{ url_for('inspections.index', status='completed', date_from=today_str, date_to=today_str) }}" class="text-decoration-none">
<div class="card text-white bg-success h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small text-white-50 fw-semibold">Submitted Today</span>
<i class="bi bi-check-circle" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ completed_today }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Fully completed &amp; submitted today</div>
</div>
</div>
</a>
</div>
{% if current_user.role != 'customer' %}
<div class="col-6 col-md">
<a href="{{ url_for('inspections.index', status='in_progress') }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#b45309;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Stale In-Progress</span>
<i class="bi bi-hourglass-split" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ stale_in_progress }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Started &gt;24 h ago, not yet submitted</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
<a href="{{ url_for('inspections.index', status='follow_up') }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#7c3aed;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Pending Follow-ups</span>
<i class="bi bi-arrow-repeat" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ pending_followups }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Flagged for a follow-up re-inspection</div>
</div>
</div>
</a>
</div>
{% endif %}
</div>
{# ── Issues section ──────────────────────────────────────────────────────── #}
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-exclamation-triangle-fill text-danger"></i>
<span class="fw-bold text-uppercase" style="font-size:.78rem;letter-spacing:.07em;color:#64748b;">Issues</span>
<div style="flex:1;height:1px;background:#e2e8f0;"></div>
</div>
<div class="row g-3 mb-4">
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='open') }}" class="text-decoration-none">
<div class="card text-white bg-warning h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small text-white-50 fw-semibold">Open Issues</span>
<i class="bi bi-exclamation-triangle" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ open_issues }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.75;">
Active issues not yet resolved
{% if open_issues > 0 %}
· {% if severity_breakdown.critical > 0 %}<span class="badge bg-danger">{{ severity_breakdown.critical }}C</span> {% endif %}
{% if severity_breakdown.high > 0 %}<span class="badge bg-danger">{{ severity_breakdown.high }}H</span> {% endif %}
{% if severity_breakdown.medium > 0 %}<span class="badge bg-dark">{{ severity_breakdown.medium }}M</span> {% endif %}
{% if severity_breakdown.low > 0 %}<span class="badge bg-secondary">{{ severity_breakdown.low }}L</span>{% endif %}
{% endif %}
</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', date_from=today_str, date_to=today_str) }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#ea580c;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Issues Opened Today</span>
<i class="bi bi-flag" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ issues_opened_today }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">New issues reported today</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='resolved', date_from=today_str, date_to=today_str) }}" class="text-decoration-none">
<div class="card text-white bg-info h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small text-white-50 fw-semibold">Resolved Today</span>
<i class="bi bi-check2-all" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ resolved_today }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Issues closed and resolved today</div>
</div>
</div>
</a>
</div>
{% if current_user.role != 'customer' %}
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='pending_verification') }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#2563eb;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Pending Verification</span>
<i class="bi bi-clipboard2-check" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ pending_verification }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Resolved but awaiting supervisor sign-off</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='open') }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#16a34a;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Unassigned Open</span>
<i class="bi bi-person-dash" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ unassigned_open }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Open issues with no one assigned</div>
</div>
</div>
</a>
</div>
{% endif %}
</div>
{# ── Open Issues by Handler (phase39) — staff only ──────────────────────── #}
{% if current_user.role not in ['customer'] %}
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<a href="{{ url_for('issues.index', status='open', handler_type='internal') }}" class="text-decoration-none">
<div class="card h-100 border-0" style="background:#e0f2fe;">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small fw-semibold text-muted text-uppercase mb-1" style="font-size:.7rem;">Janitorial Staff</div>
<div class="fs-2 fw-bold" style="color:#0369a1;">{{ handler_breakdown.internal }}</div>
<div class="small text-muted">Open issues — our team</div>
</div>
<i class="bi bi-people" style="font-size:1.8rem;color:#0ea5e9;opacity:.35;"></i>
</div>
</div>
</a>
</div>
<div class="col-12 col-md-4">
<a href="{{ url_for('issues.index', status='open', handler_type='facility') }}" class="text-decoration-none">
<div class="card h-100 border-0" style="background:#fef9c3;">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small fw-semibold text-muted text-uppercase mb-1" style="font-size:.7rem;">Facility Staff</div>
<div class="fs-2 fw-bold" style="color:#a16207;">{{ handler_breakdown.facility }}</div>
<div class="small text-muted">Open issues — facility contact</div>
</div>
<i class="bi bi-building" style="font-size:1.8rem;color:#eab308;opacity:.35;"></i>
</div>
</div>
</a>
</div>
<div class="col-12 col-md-4">
<a href="{{ url_for('issues.index', status='open', handler_type='vendor') }}" class="text-decoration-none">
<div class="card h-100 border-0" style="background:#fce7f3;">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small fw-semibold text-muted text-uppercase mb-1" style="font-size:.7rem;">External Vendor</div>
<div class="fs-2 fw-bold" style="color:#9d174d;">{{ handler_breakdown.vendor }}</div>
<div class="small text-muted">Open issues — contractor</div>
</div>
<i class="bi bi-person-gear" style="font-size:1.8rem;color:#ec4899;opacity:.35;"></i>
</div>
</div>
</a>
</div>
</div>
{% endif %}
{# ── SLA Summary ─────────────────────────────────────────────────────────── #}
{% if sla_breached > 0 or sla_at_risk > 0 %}
<div class="row g-3 mb-4">
{% if sla_breached > 0 %}
<div class="col-6 col-md-3">
<a href="{{ url_for('issues.index', sla='breached') }}" class="text-decoration-none">
<div class="card border-danger h-100">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small text-danger fw-semibold">SLA Breached</div>
<div class="fs-2 fw-bold text-danger">{{ sla_breached }}</div>
</div>
<i class="bi bi-alarm text-danger" style="font-size:2.5rem;opacity:.3;"></i>
</div>
</div>
</a>
</div>
{% endif %}
{% if sla_at_risk > 0 %}
<div class="col-6 col-md-3">
<a href="{{ url_for('issues.index', sla='at_risk') }}" class="text-decoration-none">
<div class="card border-warning h-100">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small text-warning fw-semibold">SLA At Risk</div>
<div class="fs-2 fw-bold text-warning">{{ sla_at_risk }}</div>
</div>
<i class="bi bi-alarm text-warning" style="font-size:2.5rem;opacity:.3;"></i>
</div>
</div>
</a>
</div>
{% endif %}
</div>
{% endif %}
{# ── Recent activity ─────────────────────────────────────────────────────── #}
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-semibold">
<i class="bi bi-clock-history me-1"></i>Recent Activity
</div>
<div class="card-body p-0">
{% if recent_inspections %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Facility</th>
<th>Area</th>
{% if current_user.role != 'inspector' %}<th>Inspector</th>{% endif %}
<th>Score</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for insp in recent_inspections %}
<tr style="cursor:pointer;" onclick="window.location='{{ url_for('inspections.view', inspection_id=insp.id) }}'">
<td><small>{{ insp.inspection_date.strftime('%Y-%m-%d %H:%M') }}</small></td>
<td>{{ insp.facility.name }}</td>
<td>{{ insp.area.name if insp.area else '—' }}</td>
{% if current_user.role != 'inspector' %}<td>{{ insp.inspector.display_name }}</td>{% endif %}
<td>
{% if insp.overall_score %}
<span class="badge bg-{% if insp.overall_score >= 90 %}success{% elif insp.overall_score >= 70 %}warning{% else %}danger{% endif %}">
{{ insp.overall_score }}%
</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
<span class="badge bg-{% if insp.status == 'completed' %}success{% elif insp.status == 'flagged' %}danger{% else %}secondary{% endif %}">
{{ 'Submitted' if insp.status == 'completed' else insp.status|title }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4 text-muted">
<i class="bi bi-inbox fs-2 d-block mb-2"></i>No recent inspections.
</div>
{% endif %}
</div>
</div>
{# ── Inspector activity today (admin / director / PM) ───────────────────── #}
{% if inspector_activity %}
<div class="card shadow-sm mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<span class="fw-semibold"><i class="bi bi-people me-1"></i>Inspector Activity Today</span>
<a href="{{ url_for('inspections.index', date_from=today_str, date_to=today_str) }}"
class="btn btn-sm btn-outline-secondary">View all</a>
</div>
<div class="card-body p-0 table-responsive">
<table class="table table-sm table-hover mb-0" style="font-size:.85rem;">
<thead class="table-light">
<tr>
<th>Inspector</th>
<th class="text-center" style="width:120px;">Submitted Today</th>
<th style="width:200px;"></th>
</tr>
</thead>
<tbody>
{% for row in inspector_activity %}
<tr class="{{ 'table-success' if row.count > 0 else '' }}">
<td>{{ row.name }}</td>
<td class="text-center">
{% if row.count > 0 %}
<span class="badge bg-success">{{ row.count }}</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
<div class="progress" style="height:6px;margin-top:4px;">
{% set max_count = inspector_activity | map(attribute='count') | max %}
{% set pct = (row.count / max_count * 100) | int if max_count > 0 else 0 %}
<div class="progress-bar bg-success" style="width:{{ pct }}%;"></div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{# ── My open issues (inspector widget) ──────────────────────────────────── #}
{% if my_issues %}
<div class="card shadow-sm mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<span class="fw-semibold"><i class="bi bi-person-check me-1 text-primary"></i>My Open Issues</span>
<a href="{{ url_for('issues.index') }}" class="btn btn-sm btn-outline-secondary">View all</a>
</div>
<div class="card-body p-0 table-responsive">
<table class="table table-hover mb-0" style="font-size:.85rem;">
<thead class="table-light">
<tr>
<th width="50">ID</th>
<th width="80">Severity</th>
<th>Facility / Description</th>
<th width="90">Status</th>
<th width="110">SLA</th>
</tr>
</thead>
<tbody>
{% for issue in my_issues %}
{% set sla = sla_status(issue) %}
<tr class="{{ 'table-danger' if sla == 'breached' else 'table-warning' if sla == 'at_risk' else '' }}">
<td class="text-muted">
<a href="{{ url_for('issues.view', issue_id=issue.id) }}" class="text-decoration-none fw-semibold">#{{ issue.id }}</a>
</td>
<td>
<span class="badge bg-{{ 'danger' if issue.severity in ['critical','high'] else 'warning text-dark' if issue.severity == 'medium' else 'secondary' }}">
{{ issue.severity|title }}
</span>
</td>
<td>
<div>{{ issue.resolved_facility.name if issue.resolved_facility else '—' }}</div>
<div class="text-muted small">{{ issue.description[:60] }}{% if issue.description|length > 60 %}…{% endif %}</div>
</td>
<td>
<span class="badge bg-{{ 'warning text-dark' if issue.status == 'in_progress' else 'danger' }}">
{{ issue.status|replace('_',' ')|title }}
</span>
</td>
<td>
{% if sla == 'breached' %}
<span class="badge bg-danger"><i class="bi bi-alarm me-1"></i>Breached</span>
{% elif sla == 'at_risk' %}
<span class="badge bg-warning text-dark"><i class="bi bi-hourglass-split me-1"></i>{{ sla_hours_remaining(issue)|abs|round(1) }}h left</span>
{% else %}
<span class="badge bg-secondary">OK</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{# ── Customer portal: scoped facilities panel ───────────────────────────── #}
{% if current_user.role == 'customer' %}
<div class="row g-3 mt-2">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light fw-semibold d-flex justify-content-between align-items-center"
role="button" data-bs-toggle="collapse" data-bs-target="#facilitiesBody"
aria-expanded="true" aria-controls="facilitiesBody" style="cursor:pointer;">
<span><i class="bi bi-building me-1"></i> Your Facilities
<i class="bi bi-chevron-down ms-1 small" id="facilitiesChevron"></i>
</span>
{% if customer_facilities %}
<span class="badge bg-secondary rounded-pill">{{ customer_facilities|length }}</span>
{% endif %}
</div>
<div class="collapse show" id="facilitiesBody">
{% if customer_facilities %}
<div class="card-body pb-2">
{% if customer_facilities|length > 6 %}
<div class="mb-3">
<input type="text" id="facilitySearch" class="form-control form-control-sm"
placeholder="Search facilities…">
</div>
{% endif %}
<div class="row g-2" id="facilityGrid">
{% for f in customer_facilities %}
<div class="col-12 col-sm-6 col-lg-4 facility-col">
<div class="border rounded p-2 h-100 d-flex flex-column facility-card">
<div class="fw-semibold mb-1 small">{{ f.name }}</div>
<div class="text-muted" style="font-size:.8rem;">{{ f.address or '—' }}</div>
<div class="my-1">
<span class="badge bg-light text-dark border" style="font-size:.75rem;">
{{ f.project.name if f.project else '—' }}
</span>
</div>
<div class="mt-auto pt-1">
<a href="{{ url_for('facilities.view_facility', facility_id=f.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> View
</a>
<a href="{{ url_for('reports.facility_report', facility_id=f.id) }}"
class="btn btn-sm btn-outline-secondary ms-1">
<i class="bi bi-graph-up"></i> Report
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% if customer_facilities|length > 9 %}
<div id="facilityShowMore" class="text-center mt-3">
<button class="btn btn-sm btn-link text-muted" id="toggleFacilities">
Show all {{ customer_facilities|length }} facilities <i class="bi bi-chevron-down"></i>
</button>
</div>
{% endif %}
</div>
{% else %}
<div class="card-body text-muted small">
<i class="bi bi-info-circle me-1"></i>
No facilities have been assigned to your account yet. Please contact your administrator.
</div>
{% endif %}
</div>
</div>
</div>
</div>
<script>
(function () {
var collapseEl = document.getElementById('facilitiesBody');
var chevron = document.getElementById('facilitiesChevron');
collapseEl.addEventListener('hide.bs.collapse', function () {
chevron.classList.replace('bi-chevron-down', 'bi-chevron-up');
});
collapseEl.addEventListener('show.bs.collapse', function () {
chevron.classList.replace('bi-chevron-up', 'bi-chevron-down');
});
{% if customer_facilities and customer_facilities|length > 9 %}
var VISIBLE = 9;
var cols = document.querySelectorAll('#facilityGrid .facility-col');
var btn = document.getElementById('toggleFacilities');
var more = document.getElementById('facilityShowMore');
var expanded = false;
cols.forEach(function (c, i) { if (i >= VISIBLE) c.style.display = 'none'; });
btn.addEventListener('click', function () {
expanded = !expanded;
cols.forEach(function (c, i) {
if (i >= VISIBLE) c.style.display = expanded ? '' : 'none';
});
btn.innerHTML = expanded
? 'Show fewer <i class="bi bi-chevron-up"></i>'
: 'Show all {{ customer_facilities|length }} facilities <i class="bi bi-chevron-down"></i>';
});
{% endif %}
{% if customer_facilities and customer_facilities|length > 6 %}
document.getElementById('facilitySearch').addEventListener('input', function () {
var q = this.value.toLowerCase();
document.querySelectorAll('#facilityGrid .facility-col').forEach(function (col) {
var match = col.querySelector('.facility-card').textContent.toLowerCase().includes(q);
col.style.display = match ? '' : 'none';
});
var more2 = document.getElementById('facilityShowMore');
if (more2) more2.style.display = this.value ? 'none' : '';
});
{% endif %}
})();
</script>
{% endif %}
{% endblock %}