06/12 Redesign dashboard 2
This commit is contained in:
@@ -207,6 +207,120 @@ def index():
|
|||||||
sla_breached = sum(1 for i in all_open_issues if sla_status(i) == 'breached')
|
sla_breached = sum(1 for i in all_open_issues if sla_status(i) == 'breached')
|
||||||
sla_at_risk = sum(1 for i in all_open_issues if sla_status(i) == 'at_risk')
|
sla_at_risk = sum(1 for i in all_open_issues if sla_status(i) == 'at_risk')
|
||||||
|
|
||||||
|
# ── Issues opened today ───────────────────────────────────────────────────
|
||||||
|
from app.models.facility import Area as _AreaT
|
||||||
|
opened_today_q = Issue.query.outerjoin(_AreaT, Issue.area_id == _AreaT.id).filter(
|
||||||
|
Issue.reported_at >= today_start,
|
||||||
|
Issue.reported_at < today_end,
|
||||||
|
)
|
||||||
|
if is_inspector:
|
||||||
|
if not inspector_facility_ids:
|
||||||
|
opened_today_q = opened_today_q.filter(False)
|
||||||
|
else:
|
||||||
|
opened_today_q = opened_today_q.filter(db.or_(
|
||||||
|
Issue.facility_id.in_(inspector_facility_ids),
|
||||||
|
_AreaT.facility_id.in_(inspector_facility_ids),
|
||||||
|
))
|
||||||
|
elif is_customer:
|
||||||
|
if not customer_facility_ids:
|
||||||
|
opened_today_q = opened_today_q.filter(False)
|
||||||
|
else:
|
||||||
|
opened_today_q = opened_today_q.filter(db.or_(
|
||||||
|
Issue.facility_id.in_(customer_facility_ids),
|
||||||
|
_AreaT.facility_id.in_(customer_facility_ids),
|
||||||
|
))
|
||||||
|
issues_opened_today = opened_today_q.count()
|
||||||
|
|
||||||
|
# ── Pending verification ──────────────────────────────────────────────────
|
||||||
|
from app.models.facility import Area as _AreaV
|
||||||
|
pv_q = Issue.query.outerjoin(_AreaV, Issue.area_id == _AreaV.id).filter(
|
||||||
|
Issue.status == 'pending_verification',
|
||||||
|
)
|
||||||
|
if is_inspector:
|
||||||
|
if not inspector_facility_ids:
|
||||||
|
pv_q = pv_q.filter(False)
|
||||||
|
else:
|
||||||
|
pv_q = pv_q.filter(db.or_(
|
||||||
|
Issue.facility_id.in_(inspector_facility_ids),
|
||||||
|
_AreaV.facility_id.in_(inspector_facility_ids),
|
||||||
|
))
|
||||||
|
elif is_customer:
|
||||||
|
if not customer_facility_ids:
|
||||||
|
pv_q = pv_q.filter(False)
|
||||||
|
else:
|
||||||
|
pv_q = pv_q.filter(db.or_(
|
||||||
|
Issue.facility_id.in_(customer_facility_ids),
|
||||||
|
_AreaV.facility_id.in_(customer_facility_ids),
|
||||||
|
))
|
||||||
|
pending_verification = pv_q.count()
|
||||||
|
|
||||||
|
# ── Stale in-progress inspections (started > 24h ago, not yet submitted) ──
|
||||||
|
stale_cutoff = now - timedelta(hours=24)
|
||||||
|
stale_q = Inspection.query.filter(
|
||||||
|
Inspection.status == 'in_progress',
|
||||||
|
Inspection.inspection_date < stale_cutoff,
|
||||||
|
)
|
||||||
|
if is_inspector:
|
||||||
|
if not inspector_facility_ids:
|
||||||
|
stale_q = stale_q.filter(False)
|
||||||
|
else:
|
||||||
|
stale_q = stale_q.filter(
|
||||||
|
Inspection.facility_id.in_(inspector_facility_ids),
|
||||||
|
Inspection.inspector_id == current_user.id,
|
||||||
|
)
|
||||||
|
elif is_customer:
|
||||||
|
if customer_facility_ids:
|
||||||
|
stale_q = stale_q.filter(Inspection.facility_id.in_(customer_facility_ids))
|
||||||
|
else:
|
||||||
|
stale_q = stale_q.filter(False)
|
||||||
|
stale_in_progress = stale_q.count()
|
||||||
|
|
||||||
|
# ── Unassigned open issues ────────────────────────────────────────────────
|
||||||
|
from app.models.facility import Area as _AreaU
|
||||||
|
unassigned_q = Issue.query.outerjoin(_AreaU, Issue.area_id == _AreaU.id).filter(
|
||||||
|
Issue.status.in_(['open', 'in_progress']),
|
||||||
|
Issue.assigned_to.is_(None),
|
||||||
|
)
|
||||||
|
if is_inspector:
|
||||||
|
if not inspector_facility_ids:
|
||||||
|
unassigned_q = unassigned_q.filter(False)
|
||||||
|
else:
|
||||||
|
unassigned_q = unassigned_q.filter(db.or_(
|
||||||
|
Issue.facility_id.in_(inspector_facility_ids),
|
||||||
|
_AreaU.facility_id.in_(inspector_facility_ids),
|
||||||
|
))
|
||||||
|
elif is_customer:
|
||||||
|
unassigned_q = unassigned_q.filter(False) # not relevant for customers
|
||||||
|
unassigned_open = unassigned_q.count()
|
||||||
|
|
||||||
|
# ── Inspector activity today (admin / director / PM only) ─────────────────
|
||||||
|
inspector_activity = []
|
||||||
|
if is_privileged or is_project_manager:
|
||||||
|
active_inspectors = (
|
||||||
|
User.query
|
||||||
|
.filter_by(role='inspector', active=True)
|
||||||
|
.order_by(User.full_name, User.username)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
today_counts = dict(
|
||||||
|
db.session.query(
|
||||||
|
Inspection.inspector_id,
|
||||||
|
func.count(Inspection.id),
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
Inspection.status == 'completed',
|
||||||
|
Inspection.inspection_date >= today_start,
|
||||||
|
Inspection.inspection_date < today_end,
|
||||||
|
)
|
||||||
|
.group_by(Inspection.inspector_id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
inspector_activity = sorted(
|
||||||
|
[{'name': u.display_name, 'count': today_counts.get(u.id, 0)}
|
||||||
|
for u in active_inspectors],
|
||||||
|
key=lambda x: (-x['count'], x['name']),
|
||||||
|
)
|
||||||
|
|
||||||
# ── My open issues (inspector dashboard widget) ───────────────────────────
|
# ── My open issues (inspector dashboard widget) ───────────────────────────
|
||||||
# Issues assigned to the current inspector that are not yet resolved,
|
# Issues assigned to the current inspector that are not yet resolved,
|
||||||
# ordered by SLA urgency (breached first, then at-risk, then ok).
|
# ordered by SLA urgency (breached first, then at-risk, then ok).
|
||||||
@@ -231,6 +345,11 @@ def index():
|
|||||||
severity_breakdown = severity_breakdown,
|
severity_breakdown = severity_breakdown,
|
||||||
resolved_today = resolved_today,
|
resolved_today = resolved_today,
|
||||||
pending_followups = pending_followups,
|
pending_followups = pending_followups,
|
||||||
|
issues_opened_today = issues_opened_today,
|
||||||
|
pending_verification = pending_verification,
|
||||||
|
stale_in_progress = stale_in_progress,
|
||||||
|
unassigned_open = unassigned_open,
|
||||||
|
inspector_activity = inspector_activity,
|
||||||
recent_inspections = recent_inspections,
|
recent_inspections = recent_inspections,
|
||||||
total_facilities = total_facilities,
|
total_facilities = total_facilities,
|
||||||
total_templates = total_templates,
|
total_templates = total_templates,
|
||||||
|
|||||||
@@ -90,6 +90,66 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-flag" style="font-size:2.5rem;opacity:.2;color:#ea580c;"></i>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
<i class="bi bi-person-dash" style="font-size:2.5rem;opacity:.2;color:#16a34a;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{# ── SLA Summary ─────────────────────────────────────────────────────────── #}
|
{# ── SLA Summary ─────────────────────────────────────────────────────────── #}
|
||||||
{% if sla_breached > 0 or sla_at_risk > 0 %}
|
{% if sla_breached > 0 or sla_at_risk > 0 %}
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
@@ -181,6 +241,49 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{# ── 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 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) ──────────────────────────────────── #}
|
{# ── My open issues (inspector widget) ──────────────────────────────────── #}
|
||||||
{% if my_issues %}
|
{% if my_issues %}
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user