Phase 3: initiatal code
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Execute Inspection{% endblock %}
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.item-card { border-left: 4px solid #dee2e6; transition: border-color .2s; }
|
||||
.item-card.answered { border-left-color: #198754; }
|
||||
.item-card.flagged { border-left-color: #dc3545; }
|
||||
.pass-fail-group .btn-check:checked + .btn-outline-success { background:#198754; color:#fff; }
|
||||
.pass-fail-group .btn-check:checked + .btn-outline-danger { background:#dc3545; color:#fff; }
|
||||
.progress-bar-label { font-size:.75rem; font-weight:600; }
|
||||
.sticky-toolbar { position:sticky; top:56px; z-index:20; background:#fff; border-bottom:1px solid #dee2e6; padding:.6rem 1rem; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data" id="inspectionForm">
|
||||
{{ csrf_token() | safe }}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
{# Sticky toolbar #}
|
||||
<div class="sticky-toolbar d-flex align-items-center gap-3 mb-4">
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mb-0">{{ inspection.template.name }}</h5>
|
||||
<small class="text-muted">{{ inspection.facility.name }}{% if inspection.area %} · {{ inspection.area.name }}{% endif %}</small>
|
||||
</div>
|
||||
<div class="text-center" style="min-width:120px;">
|
||||
<div class="progress" style="height:8px;">
|
||||
<div class="progress-bar bg-success" style="width:{{ (answered / results|length * 100)|int if results else 0 }}%"></div>
|
||||
</div>
|
||||
<span class="progress-bar-label text-muted">{{ answered }}/{{ results|length }} answered</span>
|
||||
</div>
|
||||
<a href="{{ url_for('inspections.flag_issue', inspection_id=inspection.id) }}"
|
||||
class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-exclamation-triangle"></i> Flag Issue
|
||||
</a>
|
||||
<button type="submit" name="action" value="save" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-floppy"></i> Save
|
||||
</button>
|
||||
<button type="submit" name="action" value="complete" class="btn btn-success"
|
||||
onclick="return confirm('Mark this inspection as complete?')">
|
||||
<i class="bi bi-check-circle"></i> Complete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if inspection.notes %}
|
||||
<div class="alert alert-info py-2"><i class="bi bi-sticky"></i> <strong>Notes:</strong> {{ inspection.notes }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% set ns = namespace(current_cat='') %}
|
||||
{% for result in results %}
|
||||
{% set item = result.checklist_item %}
|
||||
{% if item.category != ns.current_cat %}
|
||||
{% set ns.current_cat = item.category %}
|
||||
<h5 class="text-primary border-bottom pb-1 mt-4 mb-3">
|
||||
<i class="bi bi-folder2"></i> {{ item.category or 'General' }}
|
||||
</h5>
|
||||
{% endif %}
|
||||
|
||||
{% set is_answered = result.passed is not none or result.score is not none %}
|
||||
<div class="card shadow-sm mb-3 item-card {{ 'answered' if is_answered }}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<span class="fw-semibold">{{ item.item_description }}</span>
|
||||
{% if item.requires_photo %}
|
||||
<span class="badge bg-warning text-dark ms-1"><i class="bi bi-camera"></i> Photo required</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="badge bg-secondary text-uppercase" style="font-size:.65rem;">{{ item.scoring_type|replace('_',' ') }}</span>
|
||||
</div>
|
||||
|
||||
{# ── Pass/Fail ── #}
|
||||
{% if item.scoring_type == 'pass_fail' %}
|
||||
<div class="pass-fail-group d-flex gap-2 mb-2">
|
||||
<input class="btn-check" type="radio" name="item_{{ result.id }}_passed"
|
||||
id="pass_{{ result.id }}" value="pass" {{ 'checked' if result.passed == true }}>
|
||||
<label class="btn btn-outline-success btn-sm" for="pass_{{ result.id }}">
|
||||
<i class="bi bi-check-lg"></i> Pass
|
||||
</label>
|
||||
<input class="btn-check" type="radio" name="item_{{ result.id }}_passed"
|
||||
id="fail_{{ result.id }}" value="fail" {{ 'checked' if result.passed == false }}>
|
||||
<label class="btn btn-outline-danger btn-sm" for="fail_{{ result.id }}">
|
||||
<i class="bi bi-x-lg"></i> Fail
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{# ── Rating 5 ── #}
|
||||
{% elif item.scoring_type == 'rating_5' %}
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Rating (1–5)</label>
|
||||
<div class="d-flex gap-2">
|
||||
{% for v in [1,2,3,4,5] %}
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="item_{{ result.id }}_score"
|
||||
id="r5_{{ result.id }}_{{ v }}" value="{{ v }}"
|
||||
{{ 'checked' if result.score == v }}>
|
||||
<label class="form-check-label" for="r5_{{ result.id }}_{{ v }}">{{ v }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Rating 10 ── #}
|
||||
{% elif item.scoring_type == 'rating_10' %}
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Rating (1–10)</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for v in range(1,11) %}
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="item_{{ result.id }}_score"
|
||||
id="r10_{{ result.id }}_{{ v }}" value="{{ v }}"
|
||||
{{ 'checked' if result.score == v }}>
|
||||
<label class="form-check-label" for="r10_{{ result.id }}_{{ v }}">{{ v }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<textarea class="form-control form-control-sm" name="item_{{ result.id }}_comments"
|
||||
rows="2" placeholder="Comments (optional)">{{ result.comments or '' }}</textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if result.photo_path %}
|
||||
<div class="mb-1">
|
||||
<img src="{{ url_for('static', filename=result.photo_path) }}"
|
||||
class="img-thumbnail" style="max-height:80px;" alt="Photo">
|
||||
</div>
|
||||
{% endif %}
|
||||
<input type="file" class="form-control form-control-sm"
|
||||
name="item_{{ result.id }}_photo" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{# Bottom submit bar #}
|
||||
<div class="d-flex justify-content-end gap-2 mt-4 pb-4">
|
||||
<button type="submit" name="action" value="save" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-floppy"></i> Save Progress
|
||||
</button>
|
||||
<button type="submit" name="action" value="complete" class="btn btn-success btn-lg"
|
||||
onclick="return confirm('Mark this inspection as complete? This cannot be undone.')">
|
||||
<i class="bi bi-check-circle-fill"></i> Complete Inspection
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Auto-mark item cards as answered on interaction
|
||||
document.querySelectorAll('input[type=radio]').forEach(r => {
|
||||
r.addEventListener('change', () => {
|
||||
const card = r.closest('.item-card');
|
||||
if (card) card.classList.add('answered');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Flag Issue{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-exclamation-triangle"></i> Flag Issue During Inspection</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-light border mb-3">
|
||||
<strong>Inspection:</strong> {{ inspection.template.name }}<br>
|
||||
<strong>Facility:</strong> {{ inspection.facility.name }}
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.area_id.label(class="form-label fw-semibold") }}
|
||||
{{ form.area_id(class="form-select") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.severity.label(class="form-label fw-semibold") }}
|
||||
{{ form.severity(class="form-select") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.description.label(class="form-label fw-semibold") }}
|
||||
{{ form.description(class="form-control", rows=4, placeholder="Describe the issue in detail…") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.photo.label(class="form-label fw-semibold") }}
|
||||
{{ form.photo(class="form-control") }}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
{{ form.assigned_to.label(class="form-label fw-semibold") }}
|
||||
{{ form.assigned_to(class="form-select") }}
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-danger"><i class="bi bi-flag"></i> Log Issue</button>
|
||||
<a href="{{ url_for('inspections.execute', inspection_id=inspection.id) }}"
|
||||
class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,106 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Inspections{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-clipboard-data"></i> Inspections</h2>
|
||||
<a href="{{ url_for('inspections.start') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Start Inspection
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Filters #}
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body py-2">
|
||||
<form method="get" class="row g-2 align-items-end">
|
||||
<div class="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 %}>{{ s|replace('_',' ')|title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small mb-1">Facility</label>
|
||||
<select name="facility_id" 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 class="col-auto">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">Filter</button>
|
||||
<a href="{{ url_for('inspections.index') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
{% if inspections.items %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</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>{{ ins.inspection_date.strftime('%Y-%m-%d %H:%M') }}</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 }}</td>
|
||||
<td>{{ ins.inspector.username }}</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' }}">
|
||||
{{ ins.status|replace('_',' ')|title }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% 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">Continue</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('inspections.view', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-secondary">View</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{# Pagination #}
|
||||
{% if inspections.pages > 1 %}
|
||||
<div class="d-flex justify-content-center py-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, status=status_filter, facility_id=facility_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="alert alert-info m-3"><i class="bi bi-info-circle"></i> No inspections found.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Start Inspection{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-play-circle"></i> Start New Inspection</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.template_id.label(class="form-label fw-semibold") }}
|
||||
{{ form.template_id(class="form-select" + (" is-invalid" if form.template_id.errors else "")) }}
|
||||
{% for e in form.template_id.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.facility_id.label(class="form-label fw-semibold") }}
|
||||
{{ form.facility_id(class="form-select", id="facilitySelect") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.area_id.label(class="form-label fw-semibold") }}
|
||||
<select name="area_id" id="areaSelect" class="form-select">
|
||||
<option value="0">— No specific area —</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.notes.label(class="form-label fw-semibold") }}
|
||||
{{ form.notes(class="form-control", rows=3, placeholder="Optional notes for this inspection…") }}
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-play-fill"></i> Begin Inspection
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
const AREAS_URL = "{{ url_for('inspections.areas_for_facility', facility_id=0) }}".replace('/0', '/');
|
||||
const facilitySelect = document.getElementById('facilitySelect');
|
||||
const areaSelect = document.getElementById('areaSelect');
|
||||
|
||||
function loadAreas(facilityId) {
|
||||
fetch(AREAS_URL + facilityId)
|
||||
.then(r => r.json())
|
||||
.then(areas => {
|
||||
areaSelect.innerHTML = '<option value="0">— No specific area —</option>';
|
||||
areas.forEach(a => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = a.id; opt.textContent = a.name;
|
||||
areaSelect.appendChild(opt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
facilitySelect.addEventListener('change', () => loadAreas(facilitySelect.value));
|
||||
if (facilitySelect.value) loadAreas(facilitySelect.value);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,141 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Inspection #{{ inspection.id }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2>Inspection Report</h2>
|
||||
<p class="text-muted mb-0">{{ inspection.template.name }} · {{ inspection.inspection_date.strftime('%B %d, %Y %H:%M') }}</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if current_user.role in ['admin','supervisor'] %}
|
||||
<form method="post" action="{{ url_for('inspections.delete', inspection_id=inspection.id) }}"
|
||||
onsubmit="return confirm('Delete this inspection?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash3"></i> Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('inspections.index') }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Summary cards #}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Overall Score</p>
|
||||
{% if inspection.overall_score %}
|
||||
<h2 class="fw-bold text-{{ 'success' if inspection.overall_score >= 90 else 'warning' if inspection.overall_score >= 70 else 'danger' }}">
|
||||
{{ inspection.overall_score }}%
|
||||
</h2>
|
||||
{% else %}<h2 class="text-muted">—</h2>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Status</p>
|
||||
<span class="badge fs-6 bg-{{ 'success' if inspection.status == 'completed' else 'danger' if inspection.status == 'flagged' else 'secondary' }}">
|
||||
{{ inspection.status|replace('_',' ')|title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Facility / Area</p>
|
||||
<p class="fw-semibold mb-0">{{ inspection.facility.name }}</p>
|
||||
<small class="text-muted">{{ inspection.area.name if inspection.area else '—' }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Inspector</p>
|
||||
<p class="fw-semibold mb-0">{{ inspection.inspector.username }}</p>
|
||||
{% if inspection.completed_at %}
|
||||
<small class="text-muted">Completed {{ inspection.completed_at.strftime('%Y-%m-%d %H:%M') }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if inspection.notes %}
|
||||
<div class="alert alert-light border mb-4"><strong>Notes:</strong> {{ inspection.notes }}</div>
|
||||
{% endif %}
|
||||
|
||||
{# Checklist results by category #}
|
||||
{% for category, results in categories.items() %}
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-folder2"></i> {{ category }}</h6>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr><th>Item</th><th>Type</th><th>Result</th><th>Comments</th><th>Photo</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in results %}
|
||||
<tr>
|
||||
<td>{{ r.checklist_item.item_description }}</td>
|
||||
<td><span class="badge bg-secondary" style="font-size:.65rem;">{{ r.checklist_item.scoring_type|replace('_',' ') }}</span></td>
|
||||
<td>
|
||||
{% if r.checklist_item.scoring_type == 'pass_fail' %}
|
||||
{% if r.passed is none %}<span class="text-muted">—</span>
|
||||
{% elif r.passed %}<span class="badge bg-success">Pass</span>
|
||||
{% else %}<span class="badge bg-danger">Fail</span>{% endif %}
|
||||
{% else %}
|
||||
{% if r.score is not none %}<strong>{{ r.score }}</strong>
|
||||
{% else %}<span class="text-muted">—</span>{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><small class="text-muted">{{ r.comments or '—' }}</small></td>
|
||||
<td>
|
||||
{% if r.photo_path %}
|
||||
<a href="{{ url_for('static', filename=r.photo_path) }}" target="_blank">
|
||||
<img src="{{ url_for('static', filename=r.photo_path) }}" style="max-height:40px;" class="img-thumbnail">
|
||||
</a>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{# Issues #}
|
||||
{% if issues %}
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h6 class="mb-0"><i class="bi bi-exclamation-triangle"></i> Issues Logged ({{ issues|length }})</h6>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr><th>Severity</th><th>Area</th><th>Description</th><th>Status</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for issue in issues %}
|
||||
<tr>
|
||||
<td><span class="badge bg-{{ 'danger' if issue.severity in ['critical','high'] else 'warning text-dark' if issue.severity == 'medium' else 'secondary' }}">{{ issue.severity|title }}</span></td>
|
||||
<td>{{ issue.area.name }}</td>
|
||||
<td>{{ issue.description[:80] }}{% if issue.description|length > 80 %}…{% endif %}</td>
|
||||
<td><span class="badge bg-{{ 'success' if issue.status == 'resolved' else 'warning text-dark' if issue.status == 'in_progress' else 'secondary' }}">{{ issue.status|replace('_',' ')|title }}</span></td>
|
||||
<td><a href="{{ url_for('issues.view', issue_id=issue.id) }}" class="btn btn-sm btn-outline-secondary">View</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user