Aug 6 - Add enrollment page

This commit is contained in:
2026-08-06 11:10:26 -04:00
parent c988f8cabb
commit 633a5b865f
12 changed files with 1239 additions and 0 deletions
@@ -0,0 +1,175 @@
{% extends "base.html" %}
{% block title %}Enrollment — {{ record.project_name }}{% endblock %}
{# One submitted enrollment form, rendered through the same schema the public
page uses. The customer's answers are READ-ONLY here — only the office-use
block and the status are editable, so the file stays a faithful record of
what was actually requested. #}
{% block content %}
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3 gap-2">
<div>
<h2 class="mb-0"><i class="bi bi-clipboard-check"></i> {{ record.project_name }}</h2>
<div class="text-muted small">
Reference {{ record.id }} · submitted {{ record.submitted_at | replace('T', ' ') }}
{% if record.updated_at %}
· updated {{ record.updated_at | replace('T', ' ') }}
{% endif %}
</div>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('enrollment.admin_list') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back
</a>
<a href="{{ url_for('enrollment.admin_download', submission_id=record.id) }}"
class="btn btn-outline-primary">
<i class="bi bi-filetype-json"></i> Download JSON
</a>
</div>
</div>
<div class="row g-3">
{# ── Request details ─────────────────────────────────────────────── #}
<div class="col-12 col-lg-6">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">Request</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5">Project Name</dt><dd class="col-7">{{ record.project_name or '—' }}</dd>
<dt class="col-5">Request by</dt><dd class="col-7">{{ record.request_by or '—' }}</dd>
<dt class="col-5">Date Requested</dt><dd class="col-7">{{ record.date_requested or '—' }}</dd>
</dl>
{% if record.notes %}
<hr>
<div class="fw-semibold small text-muted mb-1">Customer notes</div>
<div style="white-space:pre-wrap;">{{ record.notes }}</div>
{% endif %}
</div>
</div>
</div>
{# ── Office use — the only editable part ─────────────────────────── #}
<div class="col-12 col-lg-6">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">For Office Use</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% for key, label in schema.OFFICE_FIELDS %}
<div class="mb-2">
<label class="form-label small mb-1">{{ label }}</label>
<input type="text" name="{{ key }}" class="form-control form-control-sm"
value="{{ record.office.get(key, '') }}">
</div>
{% endfor %}
<div class="mb-3">
<label class="form-label small mb-1">Status</label>
<select name="status" class="form-select form-select-sm">
{% for s in schema.STATUSES %}
<option value="{{ s }}" {{ 'selected' if record.status == s }}>
{{ schema.STATUS_LABELS[s] }}
</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-sm btn-primary">
<i class="bi bi-save"></i> Save
</button>
</form>
</div>
</div>
</div>
</div>
{# ── Step 2: who to set up ──────────────────────────────────────────── #}
<div class="card shadow-sm mt-3">
<div class="card-header fw-semibold">Users to Register</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table mb-0">
<thead class="table-light">
<tr>
<th>Seat</th><th>Name</th><th>Job Title</th><th>Email</th>
<th class="text-center">Mobile App</th>
</tr>
</thead>
<tbody>
{% for reg in record.registrants %}
{% if reg.name or reg.email %}
<tr>
<td>
{{ reg.label }}
{% if reg.include %}
<i class="bi bi-check-circle-fill text-success ms-1"
title="Ticked on the form"></i>
{% endif %}
</td>
<td class="fw-semibold">{{ reg.name or '—' }}</td>
<td>{{ reg.job_title or '—' }}</td>
<td>
{% if reg.email %}
<a href="mailto:{{ reg.email }}">{{ reg.email }}</a>
{% else %}—{% endif %}
</td>
<td class="text-center">
{% if record.mobile_app.get(reg.key) %}
<i class="bi bi-phone-fill text-primary" title="Wants the mobile app"></i>
{% else %}<span class="text-muted"></span>{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# ── Step 1: the requested matrix ───────────────────────────────────── #}
<div class="card shadow-sm mt-3">
<div class="card-header fw-semibold">Requested Tasks &amp; Functions</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th style="width:50px;">Ref</th>
<th>Task / Function</th>
{% for col_key, col_label in schema.COLUMNS %}
<th class="text-center" style="width:110px;">
{{ col_label.replace('\n', ' ') }}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for ref, label, scope in schema.TASKS %}
{% set row = record.matrix.get(ref|string, {}) %}
<tr>
<td class="text-center">{{ ref }}</td>
<td>{{ label }}</td>
{% for col_key, _col_label in schema.COLUMNS %}
<td class="text-center">
{% if scope != 'all' and col_key != 'admin' %}
<span class="text-muted">·</span>
{% elif row.get(col_key) %}
<i class="bi bi-check-square-fill text-success"></i>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% if record.meta %}
<div class="text-muted small mt-3">
Submitted from {{ record.meta.ip or 'unknown address' }}
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block title %}Enrollment Forms{% endblock %}
{# Admin inbox of enrollment submissions. Extends base.html so it picks up
whichever design (classic / modern) the admin has selected. #}
{% block content %}
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2">
<h2 class="mb-0"><i class="bi bi-clipboard-plus"></i> Enrollment Forms</h2>
<div class="d-flex gap-2">
<a href="{{ url_for('enrollment.form') }}" target="_blank"
class="btn btn-outline-secondary" title="Open the public form in a new tab">
<i class="bi bi-box-arrow-up-right"></i> View public form
</a>
{% if records %}
<a href="{{ url_for('enrollment.admin_export_csv') }}" class="btn btn-outline-success">
<i class="bi bi-file-earmark-spreadsheet"></i> Export CSV
</a>
{% endif %}
</div>
</div>
<div class="alert alert-info d-flex align-items-start gap-2">
<i class="bi bi-info-circle mt-1"></i>
<div>
Send customers this link to enroll:
<code>{{ url_for('enrollment.form', _external=True) }}</code><br>
<span class="small text-muted">
Submissions are stored as JSON files on the server, outside the database —
one file per form.
</span>
</div>
</div>
{% if records %}
<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>Submitted</th>
<th>Project</th>
<th>Requested By</th>
<th class="text-center">Users</th>
<th class="text-center">Mobile App</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for r in records %}
{% set named = r.registrants | selectattr('email') | selectattr('name') | list %}
{% set mobile_count = r.mobile_app.values() | select | list | length %}
<tr>
<td class="text-nowrap">
<small>{{ r.submitted_at | replace('T', ' ') }}</small>
</td>
<td class="fw-semibold">{{ r.project_name or '—' }}</td>
<td>{{ r.request_by or '—' }}</td>
<td class="text-center">
<span class="badge bg-secondary">{{ named | length }}</span>
</td>
<td class="text-center">
{% if mobile_count %}
<span class="badge bg-info text-dark">{{ mobile_count }}</span>
{% else %}<span class="text-muted"></span>{% endif %}
</td>
<td>
<span class="badge bg-{{ 'success' if r.status == 'completed'
else 'warning text-dark' if r.status == 'in_progress'
else 'danger' }}">
{{ schema.STATUS_LABELS.get(r.status, r.status) }}
</span>
</td>
<td class="text-end text-nowrap">
<a href="{{ url_for('enrollment.admin_detail', submission_id=r.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> Open
</a>
</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-inbox fs-2 d-block mb-2"></i>
No enrollment forms have been submitted yet.
</div>
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>JQC Enrollment Form — L.T. Services, Inc</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
body { background:#f1f5f9; color:#1f2937; }
.sheet { max-width:1140px; margin:24px auto 60px; background:#fff;
border:1px solid #d7dee6; border-radius:10px; padding:32px 34px 40px; }
.form-title { color:#1a6fb5; font-weight:800; font-size:2rem; text-align:center; margin:0; }
.form-sub { text-align:center; color:#6b7280; margin-bottom:26px; }
.step-head { font-weight:700; margin:30px 0 10px; }
.step-head span { font-weight:400; }
table.grid { width:100%; border-collapse:collapse; }
table.grid th, table.grid td { border:1px solid #cbd5e1; padding:6px 9px; vertical-align:middle; }
table.grid thead th { background:#dbeafe; font-weight:700; text-align:center; font-size:.86rem; line-height:1.25; }
table.grid thead th.left { text-align:left; }
.ref-col { width:52px; text-align:center; }
.chk-col { width:118px; text-align:center; }
.chk-col input { width:18px; height:18px; }
.reg-table thead th { background:#dcfce7; }
.rec-table thead th { background:#fde4d3; }
.hdr-table td { border:1px solid #cbd5e1; padding:6px 9px; }
.hdr-table .lbl { background:#f8fafc; font-weight:600; width:170px; white-space:nowrap; }
.hdr-table input { border:none; outline:none; width:100%; }
.hdr-table input:focus { background:#eff6ff; }
.cell-input { border:1px solid transparent; background:transparent; width:100%;
padding:2px 4px; border-radius:4px; }
.cell-input:focus { border-color:#1a6fb5; background:#fff; outline:none; }
.office-note { color:#6b7280; font-size:.82rem; }
.note-list { font-size:.92rem; }
/* Honeypot — hidden from humans, visible to naive bots. Not type=hidden:
some bots skip those. */
.hp { position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden; }
@media (max-width: 820px) {
.sheet { padding:18px 14px 30px; margin:10px; }
table.grid { font-size:.8rem; }
.chk-col { width:64px; }
.scroll-x { overflow-x:auto; }
}
@media print {
body { background:#fff; }
.sheet { border:none; margin:0; max-width:none; }
.no-print { display:none !important; }
}
</style>
</head>
<body>
<div class="sheet">
<h1 class="form-title">JQC Enrollment Form</h1>
<div class="form-sub">by L.T Services, Inc</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} no-print">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('enrollment.submit') }}" id="enrollForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{# Honeypot — must stay empty. #}
<div class="hp" aria-hidden="true">
<label>Website<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
</div>
{# ── Header ─────────────────────────────────────────────────────── #}
<div class="row g-3 mb-2">
<div class="col-12 col-lg-6">
<table class="hdr-table" style="width:100%;">
<tr>
<td class="lbl">Project Name</td>
<td><input type="text" name="project_name" required maxlength="200"
value="{{ submitted.project_name if submitted else '' }}"></td>
</tr>
<tr>
<td class="lbl">Request by:</td>
<td><input type="text" name="request_by" required maxlength="200"
value="{{ submitted.request_by if submitted else '' }}"></td>
</tr>
<tr>
<td class="lbl">Date Requested:</td>
<td><input type="date" name="date_requested"
value="{{ submitted.date_requested if submitted else '' }}"></td>
</tr>
</table>
</div>
<div class="col-12 col-lg-6">
<table class="hdr-table" style="width:100%;">
{% for key, label in schema.OFFICE_FIELDS %}
<tr>
<td class="lbl">{{ label }}</td>
<td class="office-note">For office use</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{# ── Step 1 ─────────────────────────────────────────────────────── #}
<div class="step-head">
Step 1: <span>Please check the task/function for each user. Refer to our
recommendations at the bottom of this page.</span>
</div>
<div class="scroll-x">
<table class="grid">
<thead>
<tr>
<th class="ref-col">Ref</th>
<th class="left">Role Descriptions: Tasks and Functions</th>
{% for col_key, col_label in schema.COLUMNS %}
<th class="chk-col">{{ col_label.replace('\n', ' ') }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for ref, label, scope in schema.TASKS %}
<tr>
<td class="ref-col">{{ ref }}</td>
<td>{{ label }}</td>
{% for col_key, _col_label in schema.COLUMNS %}
{% if scope == 'all' or col_key == 'admin' %}
<td class="chk-col">
<input type="checkbox" class="form-check-input"
name="task_{{ ref }}_{{ col_key }}"
aria-label="{{ label }} — {{ _col_label.replace('\n',' ') }}"
{% if submitted and submitted.matrix[ref|string][col_key] %}checked{% endif %}>
</td>
{% else %}
{# Ref 10 is Admin/Director only on the printed form. #}
<td class="chk-col"></td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# ── Step 2 ─────────────────────────────────────────────────────── #}
<div class="step-head">
Step 2: <span>Please provide the following information for setup. Each user
will receive a notification at the email provided below to sign in.</span>
</div>
<div class="scroll-x">
<table class="grid reg-table">
<thead>
<tr>
<th class="ref-col">No.</th>
<th class="left">Role Descriptions Register</th>
<th style="width:52px;"></th>
<th class="left">First and last name</th>
<th class="left">Job Title</th>
<th class="left">Email Address</th>
</tr>
</thead>
<tbody>
{% for key, label in schema.REGISTRANTS %}
{% set reg = (submitted.registrants[loop.index0] if submitted else none) %}
<tr>
<td class="ref-col">{{ loop.index }}</td>
<td>{{ label }}</td>
<td class="text-center">
<input type="checkbox" class="form-check-input" name="reg_{{ key }}_include"
aria-label="Register {{ label }}"
{% if reg and reg.include %}checked{% endif %}>
</td>
<td><input type="text" class="cell-input" name="reg_{{ key }}_name" maxlength="200"
value="{{ reg.name if reg else '' }}"></td>
<td><input type="text" class="cell-input" name="reg_{{ key }}_job_title" maxlength="200"
value="{{ reg.job_title if reg else '' }}"></td>
<td><input type="email" class="cell-input" name="reg_{{ key }}_email" maxlength="200"
value="{{ reg.email if reg else '' }}"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# ── Step 3 ─────────────────────────────────────────────────────── #}
<div class="step-head">
Step 3: <span>Please check the box next to the user who will receive the
app for smart devices.</span>
</div>
<div class="scroll-x">
<table class="grid">
<thead>
<tr>
<th class="ref-col">No.</th>
<th class="left">Mobile App</th>
{% for col_key, col_label in schema.COLUMNS %}
<th class="chk-col">{{ '' if col_key == 'admin' else col_label.replace('\n',' ') }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
<td class="ref-col">7</td>
<td>{{ schema.MOBILE_APP_LABEL }}</td>
{% for col_key, col_label in schema.COLUMNS %}
<td class="chk-col">
<input type="checkbox" class="form-check-input" name="mobile_{{ col_key }}"
aria-label="Mobile app — {{ col_label.replace('\n',' ') }}"
{% if submitted and submitted.mobile_app[col_key] %}checked{% endif %}>
</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
{# ── Notes from the customer ────────────────────────────────────── #}
<div class="step-head">Anything else we should know? <span>(optional)</span></div>
<textarea name="notes" class="form-control" rows="3" maxlength="2000"
placeholder="Special requirements, timing, additional users…">{{ submitted.notes if submitted else '' }}</textarea>
{# ── Recommendation (reference only) ────────────────────────────── #}
<div class="step-head">RECOMMENDATION</div>
<p class="mb-2">{{ schema.RECOMMENDATION_INTRO }}</p>
<div class="scroll-x">
<table class="grid rec-table">
<thead>
<tr>
<th class="ref-col">Ref</th>
<th class="left">Role Descriptions: Tasks and Functions</th>
<th class="chk-col">Admin / Director</th>
<th class="chk-col">User / Inspectors</th>
</tr>
</thead>
<tbody>
{% for ref, label, scope in schema.TASKS %}
{% set rec = schema.RECOMMENDATION[ref] %}
<tr>
<td class="ref-col">{{ ref }}</td>
<td>{{ label }}</td>
<td class="chk-col">
{% if rec[0] %}<i class="bi bi-check-square-fill text-success"></i>
{% else %}<i class="bi bi-square text-muted"></i>{% endif %}
</td>
<td class="chk-col">
{% if rec[1] is none %}—
{% elif rec[1] %}<i class="bi bi-check-square-fill text-success"></i>
{% else %}<i class="bi bi-square text-muted"></i>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="step-head">Note:</div>
<ol class="note-list">
{% for note in schema.NOTES %}<li>{{ note }}</li>{% endfor %}
</ol>
<div class="d-flex gap-2 mt-4 no-print">
<button type="submit" class="btn btn-primary px-4" id="submitBtn">
<i class="bi bi-send"></i> Submit Enrollment
</button>
<button type="button" class="btn btn-outline-secondary" onclick="window.print()">
<i class="bi bi-printer"></i> Print
</button>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Disable on first submit — a double tap must not file two enrollments.
document.getElementById('enrollForm').addEventListener('submit', function () {
var btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.innerHTML = 'Submitting…';
});
</script>
</body>
</html>
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Enrollment received — JQC</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
body { background:#f1f5f9; }
.card-wrap { max-width:600px; margin:80px auto; }
</style>
</head>
<body>
<div class="card-wrap">
<div class="card shadow-sm border-0">
<div class="card-body text-center p-5">
<i class="bi bi-check-circle-fill text-success" style="font-size:3.2rem;"></i>
<h1 class="h4 mt-3 mb-2">Thank you — your enrollment form has been received.</h1>
<p class="text-muted mb-4">
Our team will set up the accounts you listed. Each user will receive an
email invitation with sign-in instructions, and a quick guide for the
web portal and the mobile app.
</p>
{% if reference %}
<div class="border rounded-3 p-3 bg-light d-inline-block">
<div class="text-muted small">Your reference number</div>
<div class="fw-bold" style="letter-spacing:.02em;">{{ reference }}</div>
</div>
<p class="text-muted small mt-3 mb-0">
Please quote this reference if you contact us about your enrollment.
</p>
{% endif %}
</div>
</div>
<div class="text-center text-muted small mt-3">JQC by L.T Services, Inc</div>
</div>
</body>
</html>