06/08 Add inspectors' KPI, performance
This commit is contained in:
@@ -561,4 +561,138 @@ def export_issues():
|
||||
stream_with_context(generate()),
|
||||
mimetype='text/csv',
|
||||
headers={'Content-Disposition': f'attachment; filename="{filename}"'}
|
||||
)
|
||||
|
||||
|
||||
# ── Inspector Performance ─────────────────────────────────────────────────────
|
||||
|
||||
@bp.route('/inspector-performance')
|
||||
@login_required
|
||||
@supervisor_required
|
||||
def inspector_performance():
|
||||
"""KPI & performance dashboard for all inspectors — admin/director only."""
|
||||
start, end = _date_range()
|
||||
selected_id = request.args.get('inspector_id', type=int)
|
||||
|
||||
# ── Per-inspector aggregate stats ─────────────────────────────────────
|
||||
total_rows = db.session.query(
|
||||
Inspection.inspector_id,
|
||||
func.count(Inspection.id).label('total'),
|
||||
func.count(func.distinct(Inspection.facility_id)).label('facilities'),
|
||||
).filter(
|
||||
Inspection.inspection_date >= start,
|
||||
Inspection.inspection_date <= end,
|
||||
).group_by(Inspection.inspector_id).all()
|
||||
|
||||
completed_rows = db.session.query(
|
||||
Inspection.inspector_id,
|
||||
func.count(Inspection.id).label('completed'),
|
||||
func.avg(Inspection.overall_score).label('avg_score'),
|
||||
).filter(
|
||||
Inspection.inspection_date >= start,
|
||||
Inspection.inspection_date <= end,
|
||||
Inspection.status == 'completed',
|
||||
).group_by(Inspection.inspector_id).all()
|
||||
|
||||
issue_rows = db.session.query(
|
||||
Inspection.inspector_id,
|
||||
func.count(Issue.id).label('count'),
|
||||
).join(Issue, Issue.inspection_id == Inspection.id)\
|
||||
.filter(
|
||||
Inspection.inspection_date >= start,
|
||||
Inspection.inspection_date <= end,
|
||||
).group_by(Inspection.inspector_id).all()
|
||||
|
||||
followup_rows = db.session.query(
|
||||
Inspection.inspector_id,
|
||||
func.count(Inspection.id).label('count'),
|
||||
).filter(
|
||||
Inspection.inspection_date >= start,
|
||||
Inspection.inspection_date <= end,
|
||||
Inspection.follow_up_required == True,
|
||||
).group_by(Inspection.inspector_id).all()
|
||||
|
||||
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}
|
||||
issues_map = {r.inspector_id: r.count for r in issue_rows}
|
||||
followups_map = {r.inspector_id: r.count for r in followup_rows}
|
||||
|
||||
all_ids = set(total_map.keys())
|
||||
active_inspectors = (
|
||||
User.query
|
||||
.filter(User.id.in_(all_ids), User.active == True, User.role == 'inspector')
|
||||
.order_by(User.full_name, User.username)
|
||||
.all()
|
||||
) if all_ids else []
|
||||
|
||||
inspector_stats = []
|
||||
for u in active_inspectors:
|
||||
t = total_map.get(u.id, {})
|
||||
c = completed_map.get(u.id, {})
|
||||
tot = t.get('total', 0)
|
||||
comp = c.get('completed', 0)
|
||||
avg = round(float(c['avg_score']), 1) if c.get('avg_score') else None
|
||||
inspector_stats.append({
|
||||
'id': u.id,
|
||||
'display_name': u.display_name,
|
||||
'total': tot,
|
||||
'completed': comp,
|
||||
'completion_rate': round(comp / tot * 100) if tot else 0,
|
||||
'avg_score': avg,
|
||||
'issues_flagged': issues_map.get(u.id, 0),
|
||||
'follow_ups': followups_map.get(u.id, 0),
|
||||
'facilities': t.get('facilities', 0),
|
||||
})
|
||||
|
||||
inspector_stats.sort(key=lambda x: (x['avg_score'] is None, -(x['avg_score'] or 0)))
|
||||
|
||||
# ── Individual drill-down ─────────────────────────────────────────────
|
||||
selected_inspector = None
|
||||
selected_kpis = None
|
||||
trend_data = []
|
||||
recent_inspections = []
|
||||
|
||||
if selected_id:
|
||||
selected_inspector = db.session.get(User, selected_id)
|
||||
if selected_inspector and selected_inspector.role == 'inspector':
|
||||
selected_kpis = next((s for s in inspector_stats if s['id'] == selected_id), None)
|
||||
|
||||
trend_rows = db.session.query(
|
||||
func.date(Inspection.inspection_date).label('day'),
|
||||
func.avg(Inspection.overall_score).label('avg'),
|
||||
func.count(Inspection.id).label('count'),
|
||||
).filter(
|
||||
Inspection.inspector_id == selected_id,
|
||||
Inspection.inspection_date >= start,
|
||||
Inspection.inspection_date <= end,
|
||||
Inspection.status == 'completed',
|
||||
Inspection.overall_score.isnot(None),
|
||||
).group_by(func.date(Inspection.inspection_date))\
|
||||
.order_by(func.date(Inspection.inspection_date)).all()
|
||||
|
||||
trend_data = [
|
||||
{'day': str(r.day), 'avg': round(float(r.avg), 2), 'count': r.count}
|
||||
for r in trend_rows
|
||||
]
|
||||
|
||||
recent_inspections = (
|
||||
Inspection.query
|
||||
.filter(
|
||||
Inspection.inspector_id == selected_id,
|
||||
Inspection.inspection_date >= start,
|
||||
Inspection.inspection_date <= end,
|
||||
)
|
||||
.order_by(Inspection.inspection_date.desc())
|
||||
.limit(20)
|
||||
.all()
|
||||
)
|
||||
|
||||
return render_template('reports/inspector_performance.html',
|
||||
start=start, end=end,
|
||||
inspector_stats=inspector_stats,
|
||||
selected_inspector=selected_inspector,
|
||||
selected_kpis=selected_kpis,
|
||||
trend_data=trend_data,
|
||||
recent_inspections=recent_inspections,
|
||||
selected_id=selected_id,
|
||||
)
|
||||
Reference in New Issue
Block a user