Aug 7 - Update External Inspector invitation

This commit is contained in:
2026-08-07 14:59:40 -04:00
parent c686d12e64
commit 9f32ea8286
5 changed files with 149 additions and 14 deletions
+37 -1
View File
@@ -79,7 +79,20 @@
</div>
</div>
<div class="row">
{# phase51 — an External Inspector is invited by email and chooses
their own username and password, so the admin never sets one.
The JS at the foot of this page swaps these two blocks when the
role changes; the server decides independently of the JS. #}
<div id="inviteNotice" class="alert alert-info d-none">
<i class="bi bi-envelope me-1"></i>
<strong>This account will be invited by email.</strong>
External inspectors work outside the business, so we do not set
a password for them. On save, an invitation is sent to the email
address above with a link to choose their own username and
password. The link is valid for 72 hours.
</div>
<div class="row" id="passwordFields">
<div class="col-md-6 mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control", placeholder="Leave blank to keep current" if user else "") }}
@@ -127,4 +140,27 @@
</div>
</div>
</div>
<script>
(function () {
'use strict';
var roleSel = document.getElementById('role');
var pwBlock = document.getElementById('passwordFields');
var notice = document.getElementById('inviteNotice');
if (!roleSel || !pwBlock || !notice) return; // director view has no role select
function sync() {
var invited = roleSel.value === 'external_inspector';
pwBlock.classList.toggle('d-none', invited);
notice.classList.toggle('d-none', !invited);
// Clear anything already typed so an invited account can never be created
// with an admin-chosen password sitting in the POST body.
if (invited) {
pwBlock.querySelectorAll('input').forEach(function (i) { i.value = ''; });
}
}
roleSel.addEventListener('change', sync);
sync();
})();
</script>
{% endblock %}
+21 -3
View File
@@ -55,10 +55,17 @@
</td>
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
<td>
{% if user.active %}
<span class="badge bg-success">Active</span>
{% else %}
{% if not user.active %}
<span class="badge bg-secondary">Disabled</span>
{% elif not user.password_set %}
{# phase51 — invited but has not chosen a password yet;
the login route refuses them until they do. #}
<span class="badge bg-warning text-dark"
title="Invitation sent — this person has not set their password yet">
<i class="bi bi-envelope"></i> Invite pending
</span>
{% else %}
<span class="badge bg-success">Active</span>
{% endif %}
</td>
<td>
@@ -71,6 +78,17 @@
<i class="bi bi-briefcase"></i>
</a>
{% endif %}
{% if not user.password_set %}
<form method="POST" action="{{ url_for('auth.resend_invite', user_id=user.id) }}"
class="d-inline"
onsubmit="return confirm('Resend the invitation email to {{ user.email }}? The previous link will stop working.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-warning"
title="Resend invitation email">
<i class="bi bi-envelope-arrow-up"></i>
</button>
</form>
{% endif %}
{% if user.id != current_user.id %}
<form method="POST" action="{{ url_for('auth.toggle_active', user_id=user.id) }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">