Aug 7 - Update: UI change - MT16

This commit is contained in:
2026-08-07 16:32:40 -04:00
parent 6ca30c0dea
commit 513f708ee9
18 changed files with 3992 additions and 527 deletions
+362
View File
@@ -0,0 +1,362 @@
{% extends "base.html" %}
{% block title %}Inspections{% endblock %}
{#
MODERN inspections list (design A/B test).
Same context variables, same query params, same form field names and the same
three JS blocks as templates/inspections/list.html — only the chrome differs.
Nothing was dropped: every filter, column, badge, the pagination links and the
delete modal are carried over verbatim. `insp-list-link` is preserved on the
View/Continue buttons so filter-state restore on Back still works.
#}
{% block content %}
{# Any non-empty query param other than the page number means the user has
actually filtered — used to show the match count only when it is meaningful. #}
{% set active_filters = [] %}
{% for _k, _v in request.args.items() %}
{% if _k != 'page' and _v %}{% set _ = active_filters.append(_k) %}{% endif %}
{% endfor %}
{# ── Header ───────────────────────────────────────────────────────────── #}
<div class="d-flex flex-wrap justify-content-between align-items-end mb-4 gap-2">
<div>
<div class="jqc-page-title">Inspections</div>
</div>
{% if current_user.role != 'customer' %}
<div class="d-flex gap-2">
<a href="{{ url_for('inspection_schedules.index') }}" class="btn btn-outline-primary">
<i class="bi bi-calendar-check"></i> Scheduled
</a>
<a href="{{ url_for('inspections.start') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> New Inspection
</a>
</div>
{% endif %}
</div>
{# ── Filters ──────────────────────────────────────────────────────────── #}
{# Layout: fields fill two rows on the left; the actions sit in a block on the
right that spans both rows — Filter full-height, Clear above Export PDF.
Below the md breakpoint the action block wraps underneath, full width. #}
<div class="jqc-filter-bar">
<form method="get">
<div class="d-flex flex-wrap gap-3 align-items-stretch">
{# ── Fields ──────────────────────────────────────────────────────── #}
<div class="flex-grow-1" style="min-width:min(100%, 620px);">
<div class="row g-2 align-items-end">
<div class="col-6 col-md-2">
<label class="form-label small mb-1">Inspection #</label>
<input type="number" name="inspection_id" class="form-control form-control-sm"
min="1" placeholder="ID" value="{{ inspection_id_filter }}">
</div>
<div class="col-6 col-md-3">
<label class="form-label small mb-1">Status</label>
<select name="status" class="form-select form-select-sm">
<option value="">All Statuses</option>
{% for s in ['in_progress','completed','flagged'] %}
<option value="{{ s }}" {% if status_filter == s %}selected{% endif %}>{{ 'Submitted' if s == 'completed' else s|replace('_',' ')|title }}</option>
{% endfor %}
<option value="follow_up" {% if status_filter == 'follow_up' %}selected{% endif %}>Flagged Follow-up</option>
<option value="has_issues" {% if status_filter == 'has_issues' %}selected{% endif %}>Has Logged Issues</option>
</select>
</div>
<div class="col-12 col-md-3">
<label class="form-label small mb-1">Contract</label>
<select name="contract_id" id="insp_filter_contract" class="form-select form-select-sm">
<option value="">All Contracts</option>
{% for p in projects %}
<option value="{{ p.id }}" {% if contract_filter == p.id|string %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 col-md-4">
<label class="form-label small mb-1">Facility</label>
<select name="facility_id" id="insp_filter_facility" class="form-select form-select-sm">
<option value="">All Facilities</option>
{% for f in facilities %}
<option value="{{ f.id }}" {% if facility_filter == f.id|string %}selected{% endif %}>{{ f.name }}</option>
{% endfor %}
</select>
</div>
</div>
{# Second row. The score fields absorb the Inspector column's width when
the viewer is an inspector (they only ever see their own work, so the
dropdown is not rendered for them) — the row always totals 12. #}
<div class="row g-2 align-items-end mt-2">
{% if inspectors %}
<div class="col-12 col-md-4">
<label class="form-label small mb-1">Inspector</label>
<select name="inspector_id" class="form-select form-select-sm">
<option value="">All Inspectors</option>
{% for u in inspectors %}
<option value="{{ u.id }}" {% if inspector_filter == u.id|string %}selected{% endif %}>{{ u.display_name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="col-6 col-md-{{ 3 if inspectors else 4 }}">
<label class="form-label small mb-1">Date From</label>
<input type="date" name="date_from" class="form-control form-control-sm"
value="{{ date_from_filter }}">
</div>
<div class="col-6 col-md-{{ 3 if inspectors else 4 }}">
<label class="form-label small mb-1">Date To</label>
<input type="date" name="date_to" class="form-control form-control-sm"
value="{{ date_to_filter }}">
</div>
<div class="col-6 col-md-{{ 1 if inspectors else 2 }}">
<label class="form-label small mb-1">Min Score</label>
<input type="number" name="score_min" class="form-control form-control-sm"
min="0" max="100" placeholder="0" value="{{ score_min_filter }}">
</div>
<div class="col-6 col-md-{{ 1 if inspectors else 2 }}">
<label class="form-label small mb-1">Max Score</label>
<input type="number" name="score_max" class="form-control form-control-sm"
min="0" max="100" placeholder="100" value="{{ score_max_filter }}">
</div>
</div>
</div>
{# ── Actions — spans both field rows ─────────────────────────────── #}
{# mt-md-4 drops the block by roughly one label's height, so the buttons
line up with the first row's INPUTS rather than its labels — that is
what makes them shorter, since they still stretch to the bottom of the
second row. The margin is md-only; below that the block wraps
underneath the fields and needs its full width and natural height. #}
<div class="d-flex gap-2 align-items-stretch flex-grow-1 flex-md-grow-0 mt-md-4">
<button type="submit"
class="btn btn-sm btn-primary d-flex align-items-center justify-content-center flex-grow-1 flex-md-grow-0"
style="min-width:88px;">
<span><i class="bi bi-funnel"></i> Filter</span>
</button>
<div class="d-flex flex-column gap-2 flex-grow-1 flex-md-grow-0">
<a href="{{ url_for('inspections.index') }}"
class="btn btn-sm btn-outline-secondary d-flex align-items-center justify-content-center flex-grow-1"
style="min-width:104px;">Clear</a>
<a id="exportPdfBtn"
href="{{ url_for('inspections.export_list_pdf', **request.args) }}"
class="btn btn-sm btn-outline-danger d-flex align-items-center justify-content-center flex-grow-1 text-nowrap"
style="min-width:104px;">
<span><i class="bi bi-file-earmark-pdf"></i> Export PDF</span>
</a>
</div>
</div>
</div>
</form>
</div>
{# ── Results ──────────────────────────────────────────────────────────── #}
<div class="jqc-card">
{% if active_filters %}
<div class="text-muted small mb-2">
<i class="bi bi-funnel me-1"></i>
{{ inspections.total }} inspection{{ 's' if inspections.total != 1 }} match
{{ 'es' if inspections.total == 1 }} your filters
</div>
{% endif %}
{% if inspections.items %}
<div class="jqc-table-wrap table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>#</th><th>Date</th><th>Contract</th><th>Facility</th><th>Area</th>
<th>Template</th><th>Inspector</th><th>Score</th>
<th>Status</th><th></th>
</tr>
</thead>
<tbody>
{% for ins in inspections.items %}
<tr>
<td><small class="text-muted">#{{ ins.id }}</small></td>
<td class="text-nowrap">{{ ins.inspection_date.strftime('%Y-%m-%d %H:%M') }}</td>
<td><small>{{ ins.facility.project.name if ins.facility and ins.facility.project else '—' }}</small></td>
<td>{{ ins.facility.name }}</td>
<td>{% if ins.area %}{{ ins.area.name }}{% else %}<span class="text-muted"></span>{% endif %}</td>
<td>
{{ ins.template.name }}
{% if ins.scheduled_inspection_id %}
<span class="badge bg-info text-dark ms-1" title="From a scheduled inspection">
<i class="bi bi-calendar-check"></i> Scheduled
</span>
{% endif %}
</td>
<td>{{ ins.inspector.display_name }}</td>
<td>
{% if ins.overall_score %}
<span class="badge bg-{{ 'success' if ins.overall_score >= 90 else 'warning' if ins.overall_score >= 70 else 'danger' }}">
{{ ins.overall_score }}%
</span>
{% else %}<span class="text-muted"></span>{% endif %}
</td>
<td>
<span class="badge bg-{{ 'success' if ins.status == 'completed' else 'danger' if ins.status == 'flagged' else 'secondary' }}">
{{ 'Submitted' if ins.status == 'completed' else ins.status|replace('_',' ')|title }}
</span>
{% if ins.status == 'in_progress' %}
{% set hours_open = ((now - ins.inspection_date).total_seconds() / 3600) %}
{% if hours_open > 24 %}
<span class="badge bg-warning text-dark ms-1" title="In progress for over 24 hours — may be stale">
<i class="bi bi-clock-history"></i> Stale
</span>
{% endif %}
{% endif %}
{% if ins.follow_up_required and not ins.follow_ups.count() %}
<span class="badge bg-danger ms-1" title="Follow-up re-inspection required">
<i class="bi bi-arrow-repeat"></i> Follow-up
</span>
{% endif %}
</td>
<td class="text-nowrap">
{% if ins.status == 'in_progress' or ins.status == 'flagged' %}
<a href="{{ url_for('inspections.execute', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-primary insp-list-link">Continue</a>
{% else %}
<a href="{{ url_for('inspections.view', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-secondary insp-list-link">View</a>
{% endif %}
{% if current_user.role in ['admin', 'director'] %}
<button type="button"
class="btn btn-sm btn-outline-danger ms-1"
data-bs-toggle="modal"
data-bs-target="#deleteInspectionModal"
data-inspection-id="{{ ins.id }}"
data-inspection-label="{{ ins.template.name }} — {{ ins.facility.name }} ({{ ins.inspection_date.strftime('%Y-%m-%d') }})"
title="Delete inspection">
<i class="bi bi-trash3"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# Pagination #}
{% if inspections.pages > 1 %}
<div class="d-flex justify-content-center pt-3">
<nav><ul class="pagination pagination-sm mb-0">
{% for p in inspections.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if p %}
<li class="page-item {{ 'active' if p == inspections.page }}">
<a class="page-link" href="{{ url_for('inspections.index', page=p, inspection_id=inspection_id_filter, status=status_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter, score_min=score_min_filter, score_max=score_max_filter, inspector_id=inspector_filter) }}">{{ p }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
</ul></nav>
</div>
{% endif %}
{% else %}
<div class="text-center py-5 text-muted">
<i class="bi bi-clipboard-x fs-2 d-block mb-2"></i>
No inspections found.
<div class="mt-2">
<a href="{{ url_for('inspections.index') }}" class="btn btn-sm btn-outline-secondary">Clear filters</a>
</div>
</div>
{% endif %}
</div>
{% if current_user.role in ['admin', 'director'] %}
<!-- Delete Inspection Confirmation Modal -->
<div class="modal fade" id="deleteInspectionModal" tabindex="-1" aria-labelledby="deleteInspectionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteInspectionModalLabel">
<i class="bi bi-exclamation-triangle-fill"></i> Confirm Deletion
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>You are about to permanently delete the following inspection:</p>
<p class="fw-bold" id="deleteInspectionLabel"></p>
<p class="text-muted mb-0">This will also remove all associated results, flagged issues, and uploaded photos. This action is <strong>irreversible</strong>.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Cancel
</button>
<form id="deleteInspectionForm" method="POST" action="" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash3-fill"></i> Delete Permanently
</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
(function () {
'use strict';
// Save current filtered URL so view/execute pages can restore it on Back
var links = document.querySelectorAll('.insp-list-link');
links.forEach(function (a) {
a.addEventListener('click', function () {
sessionStorage.setItem('insp_list_back_url', window.location.href);
});
});
}());
</script>
<script>
(function () {
'use strict';
var contractSel = document.getElementById('insp_filter_contract');
var facilitySel = document.getElementById('insp_filter_facility');
if (!contractSel || !facilitySel) return;
var FACILITIES_URL = '{{ url_for("inspections.facilities_for_project", project_id=0) }}'.replace('/0', '/');
contractSel.addEventListener('change', function () {
var projectId = this.value;
facilitySel.value = '';
if (!projectId) {
facilitySel.innerHTML = '<option value="">All Facilities</option>';
return;
}
facilitySel.disabled = true;
facilitySel.innerHTML = '<option value="">Loading…</option>';
fetch(FACILITIES_URL + projectId)
.then(function (r) { return r.json(); })
.then(function (data) {
var html = '<option value="">All Facilities</option>';
data.forEach(function (f) {
html += '<option value="' + f.id + '">' + f.name + '</option>';
});
facilitySel.innerHTML = html;
facilitySel.disabled = false;
})
.catch(function () { facilitySel.disabled = false; });
});
}());
</script>
{% if current_user.role in ['admin', 'director'] %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const modal = document.getElementById('deleteInspectionModal');
modal.addEventListener('show.bs.modal', function (event) {
const btn = event.relatedTarget;
const id = btn.getAttribute('data-inspection-id');
const label = btn.getAttribute('data-inspection-label');
document.getElementById('deleteInspectionLabel').textContent = label;
document.getElementById('deleteInspectionForm').action = '/inspections/' + id + '/delete';
});
});
</script>
{% endif %}
{% endblock %}