06/08 Optimize app
This commit is contained in:
+41
-3
@@ -6,7 +6,7 @@ from app.utils.time_utils import now_eastern
|
|||||||
from flask import (Blueprint, render_template, request,
|
from flask import (Blueprint, render_template, request,
|
||||||
Response, stream_with_context, abort)
|
Response, stream_with_context, abort)
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func, literal_column
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from app import db
|
from app import db
|
||||||
from app.models.inspection import Inspection, InspectionTemplate
|
from app.models.inspection import Inspection, InspectionTemplate
|
||||||
@@ -22,6 +22,16 @@ bp = Blueprint('reports', __name__, url_prefix='/reports')
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_minutes(mins):
|
||||||
|
"""Format an integer minute value as '45m' or '1h 30m'. Returns None if input is None."""
|
||||||
|
if mins is None:
|
||||||
|
return None
|
||||||
|
mins = int(round(float(mins)))
|
||||||
|
if mins >= 60:
|
||||||
|
return f'{mins // 60}h {mins % 60}m'
|
||||||
|
return f'{mins}m'
|
||||||
|
|
||||||
|
|
||||||
def _date_range():
|
def _date_range():
|
||||||
"""Parse ?start= and ?end= query params; default to last 30 days."""
|
"""Parse ?start= and ?end= query params; default to last 30 days."""
|
||||||
end_default = now_eastern()
|
end_default = now_eastern()
|
||||||
@@ -589,10 +599,18 @@ def inspector_performance():
|
|||||||
Inspection.inspector_id,
|
Inspection.inspector_id,
|
||||||
func.count(Inspection.id).label('completed'),
|
func.count(Inspection.id).label('completed'),
|
||||||
func.avg(Inspection.overall_score).label('avg_score'),
|
func.avg(Inspection.overall_score).label('avg_score'),
|
||||||
|
func.avg(
|
||||||
|
func.timestampdiff(
|
||||||
|
literal_column('MINUTE'),
|
||||||
|
Inspection.inspection_date,
|
||||||
|
Inspection.completed_at,
|
||||||
|
)
|
||||||
|
).label('avg_minutes'),
|
||||||
).filter(
|
).filter(
|
||||||
Inspection.inspection_date >= start,
|
Inspection.inspection_date >= start,
|
||||||
Inspection.inspection_date <= end,
|
Inspection.inspection_date <= end,
|
||||||
Inspection.status == 'completed',
|
Inspection.status == 'completed',
|
||||||
|
Inspection.completed_at.isnot(None),
|
||||||
).group_by(Inspection.inspector_id).all()
|
).group_by(Inspection.inspector_id).all()
|
||||||
|
|
||||||
issue_rows = db.session.query(
|
issue_rows = db.session.query(
|
||||||
@@ -614,7 +632,14 @@ def inspector_performance():
|
|||||||
).group_by(Inspection.inspector_id).all()
|
).group_by(Inspection.inspector_id).all()
|
||||||
|
|
||||||
total_map = {r.inspector_id: {'total': r.total, 'facilities': r.facilities} for r in total_rows}
|
total_map = {r.inspector_id: {'total': r.total, 'facilities': r.facilities} for r in total_rows}
|
||||||
completed_map = {r.inspector_id: {'completed': r.completed, 'avg_score': r.avg_score} for r in completed_rows}
|
completed_map = {
|
||||||
|
r.inspector_id: {
|
||||||
|
'completed': r.completed,
|
||||||
|
'avg_score': r.avg_score,
|
||||||
|
'avg_minutes': r.avg_minutes,
|
||||||
|
}
|
||||||
|
for r in completed_rows
|
||||||
|
}
|
||||||
issues_map = {r.inspector_id: r.count for r in issue_rows}
|
issues_map = {r.inspector_id: r.count for r in issue_rows}
|
||||||
followups_map = {r.inspector_id: r.count for r in followup_rows}
|
followups_map = {r.inspector_id: r.count for r in followup_rows}
|
||||||
|
|
||||||
@@ -630,9 +655,10 @@ def inspector_performance():
|
|||||||
for u in active_inspectors:
|
for u in active_inspectors:
|
||||||
t = total_map.get(u.id, {})
|
t = total_map.get(u.id, {})
|
||||||
c = completed_map.get(u.id, {})
|
c = completed_map.get(u.id, {})
|
||||||
tot = t.get('total', 0)
|
tot = t.get('total', 0)
|
||||||
comp = c.get('completed', 0)
|
comp = c.get('completed', 0)
|
||||||
avg = round(float(c['avg_score']), 1) if c.get('avg_score') else None
|
avg = round(float(c['avg_score']), 1) if c.get('avg_score') else None
|
||||||
|
avg_mins = round(float(c['avg_minutes'])) if c.get('avg_minutes') else None
|
||||||
inspector_stats.append({
|
inspector_stats.append({
|
||||||
'id': u.id,
|
'id': u.id,
|
||||||
'display_name': u.display_name,
|
'display_name': u.display_name,
|
||||||
@@ -640,6 +666,8 @@ def inspector_performance():
|
|||||||
'completed': comp,
|
'completed': comp,
|
||||||
'completion_rate': round(comp / tot * 100) if tot else 0,
|
'completion_rate': round(comp / tot * 100) if tot else 0,
|
||||||
'avg_score': avg,
|
'avg_score': avg,
|
||||||
|
'avg_minutes': avg_mins,
|
||||||
|
'avg_time': _fmt_minutes(avg_mins),
|
||||||
'issues_flagged': issues_map.get(u.id, 0),
|
'issues_flagged': issues_map.get(u.id, 0),
|
||||||
'follow_ups': followups_map.get(u.id, 0),
|
'follow_ups': followups_map.get(u.id, 0),
|
||||||
'facilities': t.get('facilities', 0),
|
'facilities': t.get('facilities', 0),
|
||||||
@@ -647,6 +675,15 @@ def inspector_performance():
|
|||||||
|
|
||||||
inspector_stats.sort(key=lambda x: (x['avg_score'] is None, -(x['avg_score'] or 0)))
|
inspector_stats.sort(key=lambda x: (x['avg_score'] is None, -(x['avg_score'] or 0)))
|
||||||
|
|
||||||
|
# Team average score for benchmarking (vs. avg column)
|
||||||
|
_scores = [s['avg_score'] for s in inspector_stats if s['avg_score'] is not None]
|
||||||
|
team_avg_score = round(sum(_scores) / len(_scores), 1) if _scores else None
|
||||||
|
for s in inspector_stats:
|
||||||
|
if s['avg_score'] is not None and team_avg_score is not None:
|
||||||
|
s['vs_avg'] = round(s['avg_score'] - team_avg_score, 1)
|
||||||
|
else:
|
||||||
|
s['vs_avg'] = None
|
||||||
|
|
||||||
# ── Individual drill-down ─────────────────────────────────────────────
|
# ── Individual drill-down ─────────────────────────────────────────────
|
||||||
selected_inspector = None
|
selected_inspector = None
|
||||||
selected_kpis = None
|
selected_kpis = None
|
||||||
@@ -696,6 +733,7 @@ def inspector_performance():
|
|||||||
return render_template('reports/inspector_performance.html',
|
return render_template('reports/inspector_performance.html',
|
||||||
start=start, end=end,
|
start=start, end=end,
|
||||||
inspector_stats=inspector_stats,
|
inspector_stats=inspector_stats,
|
||||||
|
team_avg_score=team_avg_score,
|
||||||
selected_inspector=selected_inspector,
|
selected_inspector=selected_inspector,
|
||||||
selected_kpis=selected_kpis,
|
selected_kpis=selected_kpis,
|
||||||
trend_data=trend_data,
|
trend_data=trend_data,
|
||||||
|
|||||||
@@ -100,6 +100,11 @@
|
|||||||
<th class="text-center">Completed</th>
|
<th class="text-center">Completed</th>
|
||||||
<th class="text-center">Completion Rate</th>
|
<th class="text-center">Completion Rate</th>
|
||||||
<th class="text-center">Avg Score</th>
|
<th class="text-center">Avg Score</th>
|
||||||
|
<th class="text-center">
|
||||||
|
vs. Avg
|
||||||
|
{% if team_avg_score %}<span class="text-muted fw-normal small">({{ '%.1f'|format(team_avg_score) }}%)</span>{% endif %}
|
||||||
|
</th>
|
||||||
|
<th class="text-center">Avg Time</th>
|
||||||
<th class="text-center">Issues Flagged</th>
|
<th class="text-center">Issues Flagged</th>
|
||||||
<th class="text-center">Follow-ups</th>
|
<th class="text-center">Follow-ups</th>
|
||||||
<th class="text-center">Facilities</th>
|
<th class="text-center">Facilities</th>
|
||||||
@@ -133,6 +138,22 @@
|
|||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% if s.vs_avg is not none %}
|
||||||
|
{% if s.vs_avg > 0 %}
|
||||||
|
<span class="badge bg-success">+{{ '%.1f'|format(s.vs_avg) }}%</span>
|
||||||
|
{% elif s.vs_avg < 0 %}
|
||||||
|
<span class="badge bg-danger">{{ '%.1f'|format(s.vs_avg) }}%</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">0.0%</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="small text-muted">{{ s.avg_time or '—' }}</span>
|
||||||
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if s.issues_flagged > 0 %}
|
{% if s.issues_flagged > 0 %}
|
||||||
<span class="badge bg-danger">{{ s.issues_flagged }}</span>
|
<span class="badge bg-danger">{{ s.issues_flagged }}</span>
|
||||||
@@ -184,11 +205,12 @@
|
|||||||
('Total Inspections', selected_kpis.total, 'primary', 'bi-clipboard-data'),
|
('Total Inspections', selected_kpis.total, 'primary', 'bi-clipboard-data'),
|
||||||
('Completed', selected_kpis.completed, 'success', 'bi-check-circle'),
|
('Completed', selected_kpis.completed, 'success', 'bi-check-circle'),
|
||||||
('Avg Score', (('%.1f'|format(selected_kpis.avg_score)) + '%') if selected_kpis.avg_score else '—', 'info', 'bi-graph-up'),
|
('Avg Score', (('%.1f'|format(selected_kpis.avg_score)) + '%') if selected_kpis.avg_score else '—', 'info', 'bi-graph-up'),
|
||||||
|
('Avg Completion Time',selected_kpis.avg_time or '—', 'info', 'bi-stopwatch'),
|
||||||
('Issues Flagged', selected_kpis.issues_flagged, 'danger', 'bi-flag'),
|
('Issues Flagged', selected_kpis.issues_flagged, 'danger', 'bi-flag'),
|
||||||
('Follow-ups Req.', selected_kpis.follow_ups, 'warning', 'bi-arrow-repeat'),
|
('Follow-ups Req.', selected_kpis.follow_ups, 'warning', 'bi-arrow-repeat'),
|
||||||
('Facilities Covered', selected_kpis.facilities, 'primary', 'bi-building'),
|
('Facilities Covered', selected_kpis.facilities, 'primary', 'bi-building'),
|
||||||
] %}
|
] %}
|
||||||
<div class="col-6 col-md-4 col-lg-2">
|
<div class="col-6 col-md-4 col-lg-3 col-xl-auto" style="min-width:130px;">
|
||||||
<div class="card shadow-sm stat-card {{ color }} h-100">
|
<div class="card shadow-sm stat-card {{ color }} h-100">
|
||||||
<div class="card-body py-2 px-3">
|
<div class="card-body py-2 px-3">
|
||||||
<p class="text-muted small mb-1">{{ label }}</p>
|
<p class="text-muted small mb-1">{{ label }}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user