06/12 Redesign dashboard 3
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from app.utils.time_utils import now_eastern
|
||||
from flask import (Blueprint, render_template, redirect, url_for,
|
||||
flash, request, current_app, jsonify, abort, Response)
|
||||
@@ -108,6 +109,8 @@ def export_list_pdf():
|
||||
sla_filter = request.args.get('sla', '')
|
||||
facility_filter = request.args.get('facility_id', '')
|
||||
contract_filter = request.args.get('contract_id', '')
|
||||
date_from_filter = request.args.get('date_from', '')
|
||||
date_to_filter = request.args.get('date_to', '')
|
||||
|
||||
if issue_id_filter.isdigit():
|
||||
q = q.filter(Issue.id == int(issue_id_filter))
|
||||
@@ -115,6 +118,17 @@ def export_list_pdf():
|
||||
q = q.filter(Issue.severity == severity_filter)
|
||||
if status_filter:
|
||||
q = q.filter(Issue.status == status_filter)
|
||||
if date_from_filter:
|
||||
try:
|
||||
q = q.filter(Issue.reported_at >= datetime.strptime(date_from_filter, '%Y-%m-%d'))
|
||||
except ValueError:
|
||||
date_from_filter = ''
|
||||
if date_to_filter:
|
||||
try:
|
||||
_dt = datetime.strptime(date_to_filter, '%Y-%m-%d').replace(hour=23, minute=59, second=59)
|
||||
q = q.filter(Issue.reported_at <= _dt)
|
||||
except ValueError:
|
||||
date_to_filter = ''
|
||||
if contract_filter.isdigit():
|
||||
_contract_fids = [
|
||||
f.id for f in Facility.query.filter_by(project_id=int(contract_filter)).all()
|
||||
@@ -139,6 +153,10 @@ def export_list_pdf():
|
||||
filter_parts.append(f'Severity: {severity_filter.title()}')
|
||||
if status_filter:
|
||||
filter_parts.append(f'Status: {status_filter.replace("_", " ").title()}')
|
||||
if date_from_filter:
|
||||
filter_parts.append(f'From: {date_from_filter}')
|
||||
if date_to_filter:
|
||||
filter_parts.append(f'To: {date_to_filter}')
|
||||
if sla_filter:
|
||||
filter_parts.append(f'SLA: {sla_filter.replace("_", " ").title()}')
|
||||
if contract_filter.isdigit():
|
||||
@@ -215,6 +233,8 @@ def index():
|
||||
sla_filter = request.args.get('sla', '')
|
||||
facility_filter = request.args.get('facility_id', '')
|
||||
contract_filter = request.args.get('contract_id', '')
|
||||
date_from_filter = request.args.get('date_from', '')
|
||||
date_to_filter = request.args.get('date_to', '')
|
||||
|
||||
if issue_id_filter.isdigit():
|
||||
q = q.filter(Issue.id == int(issue_id_filter))
|
||||
@@ -222,6 +242,17 @@ def index():
|
||||
q = q.filter(Issue.severity == severity_filter)
|
||||
if status_filter:
|
||||
q = q.filter(Issue.status == status_filter)
|
||||
if date_from_filter:
|
||||
try:
|
||||
q = q.filter(Issue.reported_at >= datetime.strptime(date_from_filter, '%Y-%m-%d'))
|
||||
except ValueError:
|
||||
date_from_filter = ''
|
||||
if date_to_filter:
|
||||
try:
|
||||
_dt = datetime.strptime(date_to_filter, '%Y-%m-%d').replace(hour=23, minute=59, second=59)
|
||||
q = q.filter(Issue.reported_at <= _dt)
|
||||
except ValueError:
|
||||
date_to_filter = ''
|
||||
if contract_filter.isdigit():
|
||||
_contract_fids = [
|
||||
f.id for f in Facility.query.filter_by(project_id=int(contract_filter)).all()
|
||||
@@ -302,6 +333,8 @@ def index():
|
||||
sla_filter=sla_filter,
|
||||
facility_filter=facility_filter,
|
||||
contract_filter=contract_filter,
|
||||
date_from_filter=date_from_filter,
|
||||
date_to_filter=date_to_filter,
|
||||
facilities=facilities,
|
||||
projects=projects,
|
||||
staff=staff,
|
||||
|
||||
+115
-87
@@ -11,143 +11,171 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Top stat cards ─────────────────────────────────────────────────────── #}
|
||||
{# ── 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 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="small text-white-50 fw-semibold">Today's Inspections</div>
|
||||
<div class="fs-2 fw-bold">{{ today_inspections }}</div>
|
||||
<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>
|
||||
<i class="bi bi-clipboard-data" style="font-size:2.5rem;opacity:.25;"></i>
|
||||
<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 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="small text-white-50 fw-semibold">Submitted Today</div>
|
||||
<div class="fs-2 fw-bold">{{ completed_today }}</div>
|
||||
<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>
|
||||
<i class="bi bi-check-circle" style="font-size:2.5rem;opacity:.25;"></i>
|
||||
<div class="fs-1 fw-bold lh-1">{{ completed_today }}</div>
|
||||
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Fully completed & 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 >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 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="small text-white-50 fw-semibold">Open Issues</div>
|
||||
<div class="fs-2 fw-bold">{{ open_issues }}</div>
|
||||
<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 %}
|
||||
<div class="mt-1" style="font-size:.7rem;line-height:1.6;">
|
||||
{% if severity_breakdown.critical > 0 %}<span class="badge bg-danger me-1">{{ severity_breakdown.critical }} crit</span>{% endif %}
|
||||
{% if severity_breakdown.high > 0 %}<span class="badge bg-danger me-1">{{ severity_breakdown.high }} high</span>{% endif %}
|
||||
{% if severity_breakdown.medium > 0 %}<span class="badge bg-dark me-1">{{ severity_breakdown.medium }} med</span>{% endif %}
|
||||
{% if severity_breakdown.low > 0 %}<span class="badge bg-secondary me-1">{{ severity_breakdown.low }} low</span>{% endif %}
|
||||
</div>
|
||||
· {% 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>
|
||||
<i class="bi bi-exclamation-triangle" style="font-size:2.5rem;opacity:.25;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-6 col-md">
|
||||
<a href="{{ url_for('issues.index', status='resolved') }}" class="text-decoration-none">
|
||||
<div class="card text-white bg-info h-100">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="small text-white-50 fw-semibold">Resolved Today</div>
|
||||
<div class="fs-2 fw-bold">{{ resolved_today }}</div>
|
||||
</div>
|
||||
<i class="bi bi-check2-all" style="font-size:2.5rem;opacity:.25;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% if current_user.role != 'customer' %}
|
||||
<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-color:#7c3aed;">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="small fw-semibold" style="color:rgba(255,255,255,.6);">Pending Follow-ups</div>
|
||||
<div class="fs-2 fw-bold">{{ pending_followups }}</div>
|
||||
</div>
|
||||
<i class="bi bi-arrow-repeat" style="font-size:2.5rem;opacity:.25;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ── Second stat row ────────────────────────────────────────────────────── #}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md">
|
||||
<a href="{{ url_for('issues.index') }}" class="text-decoration-none">
|
||||
<div class="card h-100 border-0 shadow-sm" style="background:#fff7ed;">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="small fw-semibold" style="color:#c2410c;">Issues Opened Today</div>
|
||||
<div class="fs-2 fw-bold" style="color:#ea580c;">{{ issues_opened_today }}</div>
|
||||
<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>
|
||||
<i class="bi bi-flag" style="font-size:2.5rem;opacity:.2;color:#ea580c;"></i>
|
||||
<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 h-100 border-0 shadow-sm" style="background:#eff6ff;">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="small fw-semibold" style="color:#1d4ed8;">Pending Verification</div>
|
||||
<div class="fs-2 fw-bold" style="color:#2563eb;">{{ pending_verification }}</div>
|
||||
<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>
|
||||
<i class="bi bi-clipboard2-check" style="font-size:2.5rem;opacity:.2;color:#2563eb;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-6 col-md">
|
||||
<a href="{{ url_for('inspections.index', status='in_progress') }}" class="text-decoration-none">
|
||||
<div class="card h-100 border-0 shadow-sm" style="background:#fefce8;">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="small fw-semibold" style="color:#a16207;">Stale In-Progress</div>
|
||||
<div class="fs-2 fw-bold" style="color:#ca8a04;">{{ stale_in_progress }}</div>
|
||||
<div class="small" style="color:#a16207;font-size:.7rem;">started >24h ago</div>
|
||||
</div>
|
||||
<i class="bi bi-hourglass-split" style="font-size:2.5rem;opacity:.2;color:#ca8a04;"></i>
|
||||
<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 h-100 border-0 shadow-sm" style="background:#f0fdf4;">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="small fw-semibold" style="color:#166534;">Unassigned Open</div>
|
||||
<div class="fs-2 fw-bold" style="color:#16a34a;">{{ unassigned_open }}</div>
|
||||
<div class="small" style="color:#166534;font-size:.7rem;">no assignee</div>
|
||||
<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>
|
||||
<i class="bi bi-person-dash" style="font-size:2.5rem;opacity:.2;color:#16a34a;"></i>
|
||||
<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>
|
||||
|
||||
{# ── SLA Summary ─────────────────────────────────────────────────────────── #}
|
||||
|
||||
@@ -63,6 +63,16 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small mb-1">Reported From</label>
|
||||
<input type="date" name="date_from" class="form-control form-control-sm"
|
||||
value="{{ date_from_filter }}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small mb-1">Reported To</label>
|
||||
<input type="date" name="date_to" class="form-control form-control-sm"
|
||||
value="{{ date_to_filter }}">
|
||||
</div>
|
||||
<div class="col-auto d-flex align-items-end gap-2 flex-wrap">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">Filter</button>
|
||||
<a href="{{ url_for('issues.index') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
|
||||
@@ -157,7 +167,7 @@
|
||||
class="d-inline"
|
||||
title="Unfollow this issue">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="next" value="{{ url_for('issues.index', page=issues.page, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, contract_id=contract_filter, facility_id=facility_filter) }}">
|
||||
<input type="hidden" name="next" value="{{ url_for('issues.index', page=issues.page, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter) }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary p-0 px-1 me-1"
|
||||
title="Unfollow">
|
||||
<i class="bi bi-bell-slash" style="font-size:.75rem;"></i>
|
||||
@@ -198,7 +208,7 @@
|
||||
{% if p %}
|
||||
<li class="page-item {{ 'active' if p == issues.page }}">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('issues.index', page=p, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, sla=sla_filter, contract_id=contract_filter, facility_id=facility_filter) }}">{{ p }}</a>
|
||||
href="{{ url_for('issues.index', page=p, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, sla=sla_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}<li class="page-item disabled"><span class="page-link">…</span></li>{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user