First commit
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="font-family:Arial,sans-serif;color:#333;max-width:640px;margin:auto;">
|
||||
<div style="background:#1a1d23;padding:20px 28px;border-radius:8px 8px 0 0;">
|
||||
<h2 style="color:#fff;margin:0;font-size:1.1rem;">
|
||||
<span style="color:#93c5fd;">📋</span>
|
||||
{{ report.frequency|title }} Report — {{ report.name }}
|
||||
</h2>
|
||||
<p style="color:#94a3b8;font-size:.8rem;margin:4px 0 0;">
|
||||
{{ start.strftime('%b %d, %Y') }} — {{ end.strftime('%b %d, %Y') }}
|
||||
{% if facility %} · {{ facility.name }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background:#fff;border:1px solid #e2e8f0;border-top:none;
|
||||
border-radius:0 0 8px 8px;padding:24px;">
|
||||
|
||||
{# ── Summary / Facility type ── #}
|
||||
{% if report.report_type in ('summary','facility') %}
|
||||
|
||||
{# KPI row #}
|
||||
<table width="100%" cellspacing="0" cellpadding="0" style="margin-bottom:24px;">
|
||||
<tr>
|
||||
<td align="center" style="padding:12px;background:#f1f5f9;border-radius:8px;">
|
||||
<div style="font-size:1.6rem;font-weight:700;color:#1d4ed8;">{{ total_inspections }}</div>
|
||||
<div style="font-size:.75rem;color:#64748b;">Inspections</div>
|
||||
</td>
|
||||
<td width="12"></td>
|
||||
<td align="center" style="padding:12px;background:#f0fdf4;border-radius:8px;">
|
||||
<div style="font-size:1.6rem;font-weight:700;color:#15803d;">{{ completed }}</div>
|
||||
<div style="font-size:.75rem;color:#64748b;">Completed</div>
|
||||
</td>
|
||||
<td width="12"></td>
|
||||
<td align="center" style="padding:12px;background:#fef3c7;border-radius:8px;">
|
||||
<div style="font-size:1.6rem;font-weight:700;color:#b45309;">{{ open_issues }}</div>
|
||||
<div style="font-size:.75rem;color:#64748b;">Open Issues</div>
|
||||
</td>
|
||||
<td width="12"></td>
|
||||
<td align="center" style="padding:12px;background:#f0f9ff;border-radius:8px;">
|
||||
<div style="font-size:1.6rem;font-weight:700;color:#0369a1;">
|
||||
{{ '%.1f'|format(avg_score) ~ '%' if avg_score else '—' }}
|
||||
</div>
|
||||
<div style="font-size:.75rem;color:#64748b;">Avg Score</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if facility_scores %}
|
||||
<h3 style="font-size:.9rem;border-bottom:1px solid #e2e8f0;padding-bottom:6px;">
|
||||
Facility Scores
|
||||
</h3>
|
||||
<table width="100%" style="border-collapse:collapse;font-size:.85rem;margin-bottom:20px;">
|
||||
<thead>
|
||||
<tr style="background:#f8fafc;">
|
||||
<th style="text-align:left;padding:6px 8px;">Facility</th>
|
||||
<th style="text-align:center;padding:6px 8px;">Inspections</th>
|
||||
<th style="text-align:center;padding:6px 8px;">Avg Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in facility_scores %}
|
||||
<tr style="border-bottom:1px solid #f1f5f9;">
|
||||
<td style="padding:6px 8px;">{{ row.name }}</td>
|
||||
<td style="padding:6px 8px;text-align:center;">{{ row.count }}</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<span style="font-weight:600;color:{{ '#15803d' if row.avg >= 90 else '#b45309' if row.avg >= 70 else '#dc2626' }}">
|
||||
{{ '%.1f'|format(row.avg) }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if critical_issues %}
|
||||
<h3 style="font-size:.9rem;border-bottom:1px solid #e2e8f0;padding-bottom:6px;color:#dc2626;">
|
||||
⚠ Open Critical / High Issues
|
||||
</h3>
|
||||
<table width="100%" style="border-collapse:collapse;font-size:.82rem;margin-bottom:20px;">
|
||||
<thead>
|
||||
<tr style="background:#fef2f2;">
|
||||
<th style="text-align:left;padding:6px 8px;">Issue</th>
|
||||
<th style="text-align:left;padding:6px 8px;">Severity</th>
|
||||
<th style="text-align:left;padding:6px 8px;">Facility / Area</th>
|
||||
<th style="text-align:left;padding:6px 8px;">Reported</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in critical_issues %}
|
||||
<tr style="border-bottom:1px solid #fee2e2;">
|
||||
<td style="padding:6px 8px;">
|
||||
<a href="{{ base_url }}/issues/{{ i.id }}" style="color:#1d4ed8;">#{{ i.id }}</a>
|
||||
— {{ i.description[:60] }}{% if i.description|length > 60 %}…{% endif %}
|
||||
</td>
|
||||
<td style="padding:6px 8px;font-weight:600;color:#dc2626;">{{ i.severity|title }}</td>
|
||||
<td style="padding:6px 8px;">
|
||||
{% set rf = i.resolved_facility %}
|
||||
{{ rf.name if rf else '—' }} / {{ i.area.name if i.area else '—' }}
|
||||
</td>
|
||||
<td style="padding:6px 8px;color:#64748b;">{{ i.reported_at.strftime('%b %d') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% elif report.report_type == 'issues' %}
|
||||
|
||||
{# SLA summary bar #}
|
||||
{% if issues %}
|
||||
<table width="100%" cellspacing="0" cellpadding="0" style="margin-bottom:20px;">
|
||||
<tr>
|
||||
<td align="center" style="padding:10px;background:#fef2f2;border-radius:8px;">
|
||||
<div style="font-size:1.4rem;font-weight:700;color:#dc2626;">{{ sla_breached }}</div>
|
||||
<div style="font-size:.75rem;color:#64748b;">SLA Breached</div>
|
||||
</td>
|
||||
<td width="12"></td>
|
||||
<td align="center" style="padding:10px;background:#fefce8;border-radius:8px;">
|
||||
<div style="font-size:1.4rem;font-weight:700;color:#b45309;">{{ sla_at_risk }}</div>
|
||||
<div style="font-size:.75rem;color:#64748b;">At Risk</div>
|
||||
</td>
|
||||
<td width="12"></td>
|
||||
<td align="center" style="padding:10px;background:#f0f9ff;border-radius:8px;">
|
||||
<div style="font-size:1.4rem;font-weight:700;color:#0369a1;">{{ issues|length }}</div>
|
||||
<div style="font-size:.75rem;color:#64748b;">Total Open</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{# Per-facility sections #}
|
||||
{% for facility_name, fac_issues in issues_by_facility %}
|
||||
<h3 style="font-size:.85rem;border-bottom:1px solid #e2e8f0;padding-bottom:4px;margin-top:20px;">
|
||||
🏢 {{ facility_name }}
|
||||
<span style="font-weight:400;color:#64748b;font-size:.78rem;">({{ fac_issues|length }} issue{{ 's' if fac_issues|length != 1 else '' }})</span>
|
||||
</h3>
|
||||
<table width="100%" style="border-collapse:collapse;font-size:.82rem;margin-bottom:8px;">
|
||||
<thead>
|
||||
<tr style="background:#f8fafc;">
|
||||
<th style="padding:5px 8px;text-align:left;">#</th>
|
||||
<th style="padding:5px 8px;text-align:left;">Severity</th>
|
||||
<th style="padding:5px 8px;text-align:left;">Area</th>
|
||||
<th style="padding:5px 8px;text-align:left;">Description</th>
|
||||
<th style="padding:5px 8px;text-align:left;">Status</th>
|
||||
<th style="padding:5px 8px;text-align:left;">SLA</th>
|
||||
<th style="padding:5px 8px;text-align:left;">Reported</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i, sla in fac_issues %}
|
||||
{% set row_bg = '#fef2f2' if sla == 'breached' else '#fefce8' if sla == 'at_risk' else '#fff' %}
|
||||
<tr style="border-bottom:1px solid #f1f5f9;background:{{ row_bg }};">
|
||||
<td style="padding:5px 8px;">
|
||||
<a href="{{ base_url }}/issues/{{ i.id }}" style="color:#1d4ed8;">#{{ i.id }}</a>
|
||||
</td>
|
||||
<td style="padding:5px 8px;font-weight:600;color:{{ '#dc2626' if i.severity in ['critical','high'] else '#b45309' if i.severity == 'medium' else '#64748b' }}">
|
||||
{{ i.severity|title }}
|
||||
</td>
|
||||
<td style="padding:5px 8px;color:#64748b;">{{ i.area.name if i.area else '—' }}</td>
|
||||
<td style="padding:5px 8px;">{{ i.description[:70] }}{% if i.description|length > 70 %}…{% endif %}</td>
|
||||
<td style="padding:5px 8px;">{{ i.status|replace('_',' ')|title }}</td>
|
||||
<td style="padding:5px 8px;font-weight:600;color:{{ '#dc2626' if sla == 'breached' else '#b45309' if sla == 'at_risk' else '#64748b' }}">
|
||||
{{ 'Breached' if sla == 'breached' else 'At Risk' if sla == 'at_risk' else 'OK' }}
|
||||
</td>
|
||||
<td style="padding:5px 8px;color:#64748b;">{{ i.reported_at.strftime('%b %d') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
<p style="color:#64748b;">No open issues at this time.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<hr style="border:none;border-top:1px solid #e2e8f0;margin:28px 0 16px;">
|
||||
<p style="font-size:.75rem;color:#94a3b8;">
|
||||
Janitorial QC System — scheduled report. Do not reply to this email.<br>
|
||||
<a href="{{ base_url }}/reports" style="color:#94a3b8;">View full reports dashboard</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
{{ report.frequency|title }} Report — {{ report.name }}
|
||||
{{ start.strftime('%b %d, %Y') }} — {{ end.strftime('%b %d, %Y') }}{% if facility %} · {{ facility.name }}{% endif %}
|
||||
|
||||
{% if report.report_type in ('summary','facility') %}
|
||||
SUMMARY
|
||||
-------
|
||||
Inspections: {{ total_inspections }}
|
||||
Completed: {{ completed }}
|
||||
Open Issues: {{ open_issues }}
|
||||
Avg Score: {{ '%.1f'|format(avg_score) ~ '%' if avg_score else '—' }}
|
||||
|
||||
{% if facility_scores %}
|
||||
FACILITY SCORES
|
||||
---------------
|
||||
{% for row in facility_scores %}
|
||||
{{ row.name }}: {{ '%.1f'|format(row.avg) }}% ({{ row.count }} inspection{{ 's' if row.count != 1 else '' }})
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if critical_issues %}
|
||||
OPEN CRITICAL / HIGH ISSUES
|
||||
----------------------------
|
||||
{% for i in critical_issues %}
|
||||
{% set rf = i.resolved_facility %}#{{ i.id }} [{{ i.severity|title }}] {{ rf.name if rf else '—' }} — {{ i.description[:80] }}
|
||||
Link: {{ base_url }}/issues/{{ i.id }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% elif report.report_type == 'issues' %}
|
||||
OPEN ISSUES ({{ issues|length }}) — Breached: {{ sla_breached }} At Risk: {{ sla_at_risk }}
|
||||
{% if issues %}
|
||||
{% for facility_name, fac_issues in issues_by_facility %}
|
||||
|
||||
{{ facility_name }} ({{ fac_issues|length }} issue{{ 's' if fac_issues|length != 1 else '' }})
|
||||
{% for i, sla in fac_issues %} #{{ i.id }} [{{ i.severity|title }}] [SLA: {{ 'Breached' if sla == 'breached' else 'At Risk' if sla == 'at_risk' else 'OK' }}] {{ i.area.name if i.area else '—' }}
|
||||
Status: {{ i.status|replace('_',' ')|title }} | {{ i.description[:70] }}
|
||||
Link: {{ base_url }}/issues/{{ i.id }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
No open issues.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
--
|
||||
Janitorial QC System — automated scheduled report.
|
||||
View dashboard: {{ base_url }}/reports
|
||||
@@ -0,0 +1,102 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0"><i class="bi bi-calendar-check me-2"></i>{{ title }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Report Name</label>
|
||||
<input type="text" name="name" class="form-control"
|
||||
value="{{ report.name if report else '' }}" required
|
||||
placeholder="e.g. Weekly Facility Summary">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-semibold">Report Type</label>
|
||||
<select name="report_type" class="form-select">
|
||||
{% for val, label in [('summary','Summary KPIs'),('facility','Facility Detail'),('issues','Open Issues')] %}
|
||||
<option value="{{ val }}" {{ 'selected' if report and report.report_type == val else '' }}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-semibold">Frequency</label>
|
||||
<select name="frequency" class="form-select">
|
||||
{% for val in ['daily','weekly','monthly'] %}
|
||||
<option value="{{ val }}" {{ 'selected' if report and report.frequency == val else '' }}>
|
||||
{{ val|title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-semibold">Facility <span class="text-muted small">(optional)</span></label>
|
||||
<select name="facility_id" class="form-select">
|
||||
<option value="">— All Facilities —</option>
|
||||
{% for f in facilities %}
|
||||
<option value="{{ f.id }}"
|
||||
{{ 'selected' if report and report.facility_id == f.id else '' }}>
|
||||
{{ f.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">Leave blank to include all facilities.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Recipients</label>
|
||||
<input type="text" name="recipients" class="form-control"
|
||||
value="{{ report.recipient_list()|join(', ') if report else '' }}"
|
||||
placeholder="email1@example.com, email2@example.com"
|
||||
required>
|
||||
<div class="form-text">Comma-separated list of email addresses.</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="include_csv"
|
||||
id="include_csv" value="1"
|
||||
{{ 'checked' if report and report.include_csv else '' }}>
|
||||
<label class="form-check-label" for="include_csv">Attach CSV export</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if report %}
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="active"
|
||||
id="active" value="1"
|
||||
{{ 'checked' if report.active else '' }}>
|
||||
<label class="form-check-label" for="active">Active (send on schedule)</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save me-1"></i>
|
||||
{{ 'Save Changes' if report else 'Create Schedule' }}
|
||||
</button>
|
||||
<a href="{{ url_for('scheduled_reports.index') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle me-1"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,96 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Scheduled Reports{% endblock %}
|
||||
{% block content %}
|
||||
{% include 'reports/_subnav.html' %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-calendar-check"></i> Scheduled Reports</h2>
|
||||
<a href="{{ url_for('scheduled_reports.create') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> New Schedule
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if reports %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th><th>Type</th><th>Frequency</th><th>Facility</th>
|
||||
<th>Recipients</th><th>Next Send</th><th>Last Sent</th>
|
||||
<th>Status</th><th width="160"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in reports %}
|
||||
<tr class="{{ 'text-muted' if not r.active else '' }}">
|
||||
<td><strong>{{ r.name }}</strong></td>
|
||||
<td><span class="badge bg-secondary">{{ r.report_type|title }}</span></td>
|
||||
<td>{{ r.frequency|title }}</td>
|
||||
<td>{{ r.facility.name if r.facility else '— All —' }}</td>
|
||||
<td>
|
||||
<span title="{{ r.recipient_list()|join(', ') }}">
|
||||
{{ r.recipient_list()|length }} recipient{{ 's' if r.recipient_list()|length != 1 else '' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
{{ r.next_send_at.strftime('%Y-%m-%d %H:%M') if r.next_send_at else '—' }}
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
{{ r.last_sent_at.strftime('%Y-%m-%d %H:%M') if r.last_sent_at else 'Never' }}
|
||||
</td>
|
||||
<td>
|
||||
{% if r.active %}<span class="badge bg-success">Active</span>
|
||||
{% else %}<span class="badge bg-secondary">Paused</span>{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ url_for('scheduled_reports.edit', report_id=r.id) }}"
|
||||
class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('scheduled_reports.preview', report_id=r.id) }}"
|
||||
class="btn btn-sm btn-outline-info" title="Preview Email" target="_blank">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('scheduled_reports.preview_pdf', report_id=r.id) }}"
|
||||
class="btn btn-sm btn-outline-info" title="Preview PDF" target="_blank">
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
</a>
|
||||
<form method="POST"
|
||||
action="{{ url_for('scheduled_reports.send_now', report_id=r.id) }}"
|
||||
class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary" title="Send Now"
|
||||
onclick="return confirm('Send this report now?')">
|
||||
<i class="bi bi-send"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST"
|
||||
action="{{ url_for('scheduled_reports.delete', report_id=r.id) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Delete this scheduled report?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
<i class="bi bi-calendar-x fs-1 d-block mb-3 opacity-25"></i>
|
||||
<p class="mb-3">No scheduled reports configured yet.</p>
|
||||
<a href="{{ url_for('scheduled_reports.create') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create First Schedule
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user