Jul 16 - Fill the gaps between Single-tenant mode and Multi-tenant mode - MT6

This commit is contained in:
2026-07-16 17:28:10 -04:00
parent 02b030b1d2
commit 253291d5a4
2 changed files with 79 additions and 11 deletions
+25 -3
View File
@@ -142,9 +142,11 @@ def index():
) )
avg_score = _scope_insp(avg_score).scalar() avg_score = _scope_insp(avg_score).scalar()
# Scores by facility (for bar chart) # Scores by facility (for bar chart) — includes project_id so the report
# can group/filter facilities by Contract client-side.
fac_score_q = db.session.query( fac_score_q = db.session.query(
Facility.name, Facility.name,
Facility.project_id,
func.avg(Inspection.overall_score).label('avg_score'), func.avg(Inspection.overall_score).label('avg_score'),
func.count(Inspection.id).label('count'), func.count(Inspection.id).label('count'),
).join(Inspection, Facility.id == Inspection.facility_id)\ ).join(Inspection, Facility.id == Inspection.facility_id)\
@@ -160,9 +162,16 @@ def index():
fac_score_q = fac_score_q.filter( fac_score_q = fac_score_q.filter(
Facility.id.in_(customer_facility_ids) if customer_facility_ids else False Facility.id.in_(customer_facility_ids) if customer_facility_ids else False
) )
facility_scores = fac_score_q.group_by(Facility.id, Facility.name)\ facility_scores = fac_score_q.group_by(Facility.id, Facility.name, Facility.project_id)\
.order_by(func.avg(Inspection.overall_score).desc()).all() .order_by(func.avg(Inspection.overall_score).desc()).all()
# Resolve contract names for the facilities present.
_proj_ids = {r.project_id for r in facility_scores if r.project_id}
_proj_names = (
{p.id: p.name for p in Project.query.filter(Project.id.in_(_proj_ids)).all()}
if _proj_ids else {}
)
# Prior-period facility scores for period-over-period delta badges # Prior-period facility scores for period-over-period delta badges
period_len = end - start period_len = end - start
prior_end = start prior_end = start
@@ -256,12 +265,24 @@ def index():
inspectors = User.query.filter_by(role='inspector', active=True)\ inspectors = User.query.filter_by(role='inspector', active=True)\
.order_by(User.full_name, User.username).all() .order_by(User.full_name, User.username).all()
facility_scores_list = [{'name': r.name, 'avg_score': round(float(r.avg_score), 2), 'count': r.count} for r in facility_scores] facility_scores_list = [{
'name': r.name,
'avg_score': round(float(r.avg_score), 2),
'count': r.count,
'project_id': r.project_id or 0,
'contract': _proj_names.get(r.project_id, 'No Contract'),
} for r in facility_scores]
# Attach prior avg and delta to each facility score dict for the template table # Attach prior avg and delta to each facility score dict for the template table
for row in facility_scores_list: for row in facility_scores_list:
row['prior_avg'] = prior_scores_map.get(row['name']) row['prior_avg'] = prior_scores_map.get(row['name'])
row['delta'] = facility_deltas.get(row['name']) row['delta'] = facility_deltas.get(row['name'])
# Distinct contracts present, for the "Avg Score by Facility" contract filter.
score_contracts = sorted(
{(r['project_id'], r['contract']) for r in facility_scores_list},
key=lambda t: (t[1] or '').lower(),
)
return render_template('reports/index.html', return render_template('reports/index.html',
start=start, end=end, start=start, end=end,
total_inspections=total_inspections, total_inspections=total_inspections,
@@ -269,6 +290,7 @@ def index():
flagged=flagged, flagged=flagged,
avg_score=round(float(avg_score), 2) if avg_score else None, avg_score=round(float(avg_score), 2) if avg_score else None,
facility_scores=facility_scores_list, facility_scores=facility_scores_list,
score_contracts=score_contracts,
daily_scores=[{'day': str(r.day), 'avg': round(float(r.avg), 2), 'count': r.count} for r in daily_scores], daily_scores=[{'day': str(r.day), 'avg': round(float(r.avg), 2), 'count': r.count} for r in daily_scores],
issue_severity=[{'severity': r.severity, 'count': r.count} for r in issue_severity], issue_severity=[{'severity': r.severity, 'count': r.count} for r in issue_severity],
issue_status=[{'status': r.status, 'count': r.count} for r in issue_status], issue_status=[{'status': r.status, 'count': r.count} for r in issue_status],
+54 -8
View File
@@ -105,7 +105,17 @@
<div class="row mb-4"> <div class="row mb-4">
<div class="col-lg-8 mb-3"> <div class="col-lg-8 mb-3">
<div class="card shadow-sm h-100"> <div class="card shadow-sm h-100">
<div class="card-header bg-light"><h6 class="mb-0"><i class="bi bi-building"></i> Avg Score by Facility</h6></div> <div class="card-header bg-light d-flex justify-content-between align-items-center gap-2 flex-wrap">
<h6 class="mb-0"><i class="bi bi-building"></i> Avg Score by Facility</h6>
{% if score_contracts %}
<select id="scoreContractFilter" class="form-select form-select-sm" style="max-width:230px;">
<option value="">All Contracts</option>
{% for pid, cname in score_contracts %}
<option value="{{ pid }}">{{ cname }}</option>
{% endfor %}
</select>
{% endif %}
</div>
<div class="card-body"><div class="chart-container"><canvas id="facilityChart"></canvas></div></div> <div class="card-body"><div class="chart-container"><canvas id="facilityChart"></canvas></div></div>
</div> </div>
</div> </div>
@@ -129,6 +139,7 @@
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th>Facility</th> <th>Facility</th>
<th>Contract</th>
<th class="text-end">Current Period</th> <th class="text-end">Current Period</th>
<th class="text-end">Prior Period</th> <th class="text-end">Prior Period</th>
<th class="text-end">Change</th> <th class="text-end">Change</th>
@@ -137,8 +148,9 @@
</thead> </thead>
<tbody> <tbody>
{% for row in facility_scores %} {% for row in facility_scores %}
<tr> <tr class="facility-score-row" data-project-id="{{ row.project_id }}">
<td class="fw-semibold">{{ row.name }}</td> <td class="fw-semibold">{{ row.name }}</td>
<td class="text-muted small">{{ row.contract }}</td>
<td class="text-end"> <td class="text-end">
<span class="badge bg-{{ 'success' if row.avg_score >= 90 else 'warning text-dark' if row.avg_score >= 70 else 'danger' }}"> <span class="badge bg-{{ 'success' if row.avg_score >= 90 else 'warning text-dark' if row.avg_score >= 70 else 'danger' }}">
{{ '%.1f'|format(row.avg_score|float) }}% {{ '%.1f'|format(row.avg_score|float) }}%
@@ -266,16 +278,31 @@ new Chart(document.getElementById('trendChart'), {
} }
}); });
// ── Facility bar chart ──────────────────────────────────────────────────────── // ── Facility bar chart (filterable by contract) ───────────────────────────────
new Chart(document.getElementById('facilityChart'), { const FACILITY_SCORES = {{ facility_scores | tojson }};
let facilityChartObj = null;
function _facColors(data) { return data.map(s => s >= 90 ? GREEN : s >= 70 ? AMBER : RED); }
function renderFacilityChart(pid) {
const rows = (!pid)
? FACILITY_SCORES
: FACILITY_SCORES.filter(r => String(r.project_id) === String(pid));
const labels = rows.map(r => r.name);
const data = rows.map(r => r.avg_score);
if (facilityChartObj) {
facilityChartObj.data.labels = labels;
facilityChartObj.data.datasets[0].data = data;
facilityChartObj.data.datasets[0].backgroundColor = _facColors(data);
facilityChartObj.update();
return;
}
facilityChartObj = new Chart(document.getElementById('facilityChart'), {
type: 'bar', type: 'bar',
data: { data: {
labels: {{ facility_scores | map(attribute='name') | list | tojson }}, labels: labels,
datasets: [{ datasets: [{
label: 'Avg Score (%)', label: 'Avg Score (%)',
data: {{ facility_scores | map(attribute='avg_score') | list | tojson }}, data: data,
backgroundColor: {{ facility_scores | map(attribute='avg_score') | list | tojson }} backgroundColor: _facColors(data),
.map(s => s >= 90 ? GREEN : s >= 70 ? AMBER : RED),
borderRadius: 4, borderRadius: 4,
}] }]
}, },
@@ -285,6 +312,25 @@ new Chart(document.getElementById('facilityChart'), {
plugins: { legend: { display: false } } plugins: { legend: { display: false } }
} }
}); });
}
function filterFacilityScoreTable(pid) {
document.querySelectorAll('.facility-score-row').forEach(function (tr) {
const rp = tr.getAttribute('data-project-id');
tr.style.display = (!pid || rp === String(pid)) ? '' : 'none';
});
}
(function () {
renderFacilityChart('');
const sel = document.getElementById('scoreContractFilter');
if (sel) {
sel.addEventListener('change', function () {
renderFacilityChart(this.value);
filterFacilityScoreTable(this.value);
});
}
}());
// ── Severity doughnut ───────────────────────────────────────────────────────── // ── Severity doughnut ─────────────────────────────────────────────────────────
const sevData = {{ issue_severity | tojson }}; const sevData = {{ issue_severity | tojson }};