06/23 Implement broadcast function for admin

This commit is contained in:
Nguyen Ngo
2026-06-23 18:55:53 -04:00
parent ed74e5064c
commit 056690e6da
7 changed files with 366 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
{% extends "base.html" %}
{% block title %}Broadcast Notifications{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col">
<h2><i class="bi bi-megaphone-fill me-2"></i>Broadcast Notification</h2>
<p class="text-muted mb-0">
Send an in-app notification to all active iOS app users in the selected roles.
Messages are delivered within 60 seconds via the app's background poll.
</p>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endwith %}
<div class="row g-4">
{# ── Compose Form ─────────────────────────────────────────────────── #}
<div class="col-lg-5">
<div class="card shadow-sm h-100">
<div class="card-header bg-primary text-white">
<i class="bi bi-send-fill me-2"></i><strong>Compose Message</strong>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('broadcast.send') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="title" class="form-label fw-semibold">
Title <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="title" name="title"
maxlength="255" required placeholder="e.g. App Update Available">
<div class="form-text">Appears as the notification banner title on iPad.</div>
</div>
<div class="mb-3">
<label for="body" class="form-label fw-semibold">
Message <span class="text-danger">*</span>
</label>
<textarea class="form-control" id="body" name="body"
rows="4" required
placeholder="e.g. A new version of JanitorialQC is available. Please update to v1.3 from the App Store."></textarea>
<div class="form-text">The full message body shown in the notification and in-app inbox.</div>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
Target Roles <span class="text-danger">*</span>
</label>
<div class="d-flex flex-wrap gap-3">
{% for role in roles %}
<div class="form-check">
<input class="form-check-input" type="checkbox"
name="roles" value="{{ role }}"
id="role_{{ role }}"
{% if role == 'inspector' %}checked{% endif %}>
<label class="form-check-label" for="role_{{ role }}">
{{ role_labels[role] }}
</label>
</div>
{% endfor %}
</div>
<div class="form-text">Only active users in the selected roles will receive this message.</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg"
onclick="return confirm('Send this broadcast to all selected users?')">
<i class="bi bi-send-fill me-2"></i>Send Broadcast
</button>
</div>
</form>
</div>
</div>
</div>
{# ── Broadcast History ─────────────────────────────────────────────── #}
<div class="col-lg-7">
<div class="card shadow-sm">
<div class="card-header">
<i class="bi bi-clock-history me-2"></i><strong>Recent Broadcasts</strong>
<span class="text-muted fw-normal ms-2">(last 50)</span>
</div>
<div class="card-body p-0">
{% if history %}
<div class="table-responsive">
<table class="table table-hover table-sm mb-0">
<thead class="table-light">
<tr>
<th>Sent</th>
<th>Title</th>
<th>Roles</th>
<th class="text-center">Recipients</th>
<th>By</th>
</tr>
</thead>
<tbody>
{% for b in history %}
<tr>
<td class="text-nowrap text-muted small">
{{ b.sent_at.strftime('%b %-d, %Y') }}<br>
<span class="text-muted" style="font-size:.75rem;">
{{ b.sent_at.strftime('%I:%M %p') }}
</span>
</td>
<td>
<div class="fw-semibold">{{ b.title }}</div>
<div class="text-muted small text-truncate" style="max-width:220px;"
title="{{ b.body }}">{{ b.body }}</div>
</td>
<td>
{% for role in b.target_roles %}
<span class="badge bg-secondary me-1">
{{ role_labels.get(role, role) }}
</span>
{% endfor %}
</td>
<td class="text-center">
<span class="badge bg-primary rounded-pill">
{{ b.recipient_count }}
</span>
</td>
<td class="small text-muted">
{{ b.sent_by.display_name if b.sent_by else '—' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center text-muted py-5">
<i class="bi bi-megaphone fs-1 d-block mb-2 opacity-25"></i>
No broadcasts sent yet.
</div>
{% endif %}
</div>
</div>
</div>
</div><!-- /row -->
{% endblock %}