Jul 7 - Implement additional recipient per contract
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Notification Recipients — {{ project.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col">
|
||||
<h2><i class="bi bi-bell"></i> Notification Recipients</h2>
|
||||
<p class="text-muted mb-0">
|
||||
Additional recipients notified for events in
|
||||
<strong>{{ project.name }}</strong> facilities — on top of the
|
||||
global {% if current_user.role == 'admin' %}<a href="{{ url_for('auth.notification_matrix') }}">notification matrix</a>{% else %}notification matrix (admin-managed){% endif %}.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-auto d-flex gap-2">
|
||||
<a href="{{ url_for('projects.view', project_id=project.id) }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Contract
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Recipient list ── #}
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">
|
||||
<i class="bi bi-bell"></i> Additional Notification Recipients
|
||||
<span class="badge bg-secondary rounded-pill ms-1">{{ recipients|length }}</span>
|
||||
</span>
|
||||
<button class="btn btn-sm btn-primary" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#addRecipientForm"
|
||||
aria-expanded="false" aria-controls="addRecipientForm">
|
||||
<i class="bi bi-plus-lg"></i> Add Recipient
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# ── Add / update form (collapsed) ── #}
|
||||
<div class="collapse border-bottom" id="addRecipientForm">
|
||||
<form method="POST" action="{{ url_for('projects.add_recipient', project_id=project.id) }}"
|
||||
class="card-body bg-light-subtle">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small fw-semibold">Recipient type</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="recipient_type"
|
||||
id="typeStaff" value="staff" checked>
|
||||
<label class="form-check-label small" for="typeStaff">Staff user</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="recipient_type"
|
||||
id="typeExternal" value="external">
|
||||
<label class="form-check-label small" for="typeExternal">External email</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4" id="staffPicker">
|
||||
<label class="form-label small fw-semibold" for="user_id">Staff user</label>
|
||||
<select class="form-select" name="user_id" id="user_id">
|
||||
{% for u in staff_users %}
|
||||
<option value="{{ u.id }}">{{ u.display_name }} ({{ u.email or 'no email' }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-none" id="emailPicker">
|
||||
<label class="form-label small fw-semibold" for="email">Email address</label>
|
||||
<input type="email" class="form-control" name="email" id="email"
|
||||
placeholder="name@example.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="form-label small fw-semibold mb-1">Notified events</label>
|
||||
<div class="row">
|
||||
{% for key, label in matrix_events.items() %}
|
||||
<div class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="events"
|
||||
value="{{ key }}" id="ev_{{ key }}">
|
||||
<label class="form-check-label small" for="ev_{{ key }}">{{ label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-check-lg"></i> Save Recipient
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="collapse" data-bs-target="#addRecipientForm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text mt-2">
|
||||
Adding an existing recipient again replaces their event list.
|
||||
Staff users receive in-app + email notifications; external
|
||||
addresses receive email only.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
{% if recipients %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="min-width:180px;">Recipient</th>
|
||||
<th style="min-width:150px;">Type</th>
|
||||
<th>Notified Events</th>
|
||||
<th width="60"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in recipients %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if r.is_staff %}
|
||||
<strong>{{ r.user.display_name }}</strong>
|
||||
<div class="text-muted small">{{ r.user.email or 'no email on file' }}</div>
|
||||
{% else %}
|
||||
<strong>{{ r.email }}</strong>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if r.is_staff %}
|
||||
<span class="badge bg-primary">Staff · in-app + email</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">External · email</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for ev in r.get_events() %}
|
||||
<span class="badge bg-light text-dark border">{{ matrix_events.get(ev, ev) }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST"
|
||||
action="{{ url_for('projects.remove_recipient', recipient_id=r.id) }}"
|
||||
onsubmit="return confirm('Remove this recipient?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
title="Remove recipient">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-3 text-muted small">
|
||||
No additional recipients configured for this contract yet.
|
||||
Events fall through to the global notification matrix only.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var staffRadio = document.getElementById('typeStaff');
|
||||
var externalRadio = document.getElementById('typeExternal');
|
||||
var staffPicker = document.getElementById('staffPicker');
|
||||
var emailPicker = document.getElementById('emailPicker');
|
||||
|
||||
function togglePickers() {
|
||||
var staff = staffRadio.checked;
|
||||
staffPicker.classList.toggle('d-none', !staff);
|
||||
emailPicker.classList.toggle('d-none', staff);
|
||||
}
|
||||
staffRadio.addEventListener('change', togglePickers);
|
||||
externalRadio.addEventListener('change', togglePickers);
|
||||
togglePickers();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -16,6 +16,9 @@
|
||||
</div>
|
||||
<div class="col-auto d-flex gap-2">
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('projects.recipients', project_id=project.id) }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-bell"></i> Notification Recipients
|
||||
</a>
|
||||
<a href="{{ url_for('projects.edit', project_id=project.id) }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user