Aug 19 - Update New Inspection Schedule page layout

This commit is contained in:
2026-08-19 14:43:16 -04:00
parent 12141c2f75
commit 88af636912
7 changed files with 482 additions and 236 deletions
+354 -224
View File
@@ -1,144 +1,234 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{# Laid out to match the single-tenant scheduled-inspection form: one narrow
card, Contract → Facility cascade at the top, then what/who, then when.
MT keeps three fields ST does not have — the schedule NAME (required by
inspection_schedules.name), the AREA (a schedule may target one area) and
the auto/plan MODE. They are placed next to the field they qualify rather
than in a block of their own. #}
{% block content %}
{# ── Sticky values ────────────────────────────────────────────────────────
This form is hand-built (no WTForms), so a re-render after a validation
error would otherwise come back blank and the user would retype everything.
On POST every field reads back from request.form; otherwise from the saved
schedule (edit) or its default (create). ST gets this free from WTForms —
this is the equivalent. #}
{% set posted = request.form if request.method == 'POST' else None %}
{% set v_name = posted.get('name') if posted else (schedule.name if schedule else '') %}
{% set v_facility = (posted.get('facility_id')|int(0)) if posted else (schedule.facility_id if schedule else 0) %}
{% set v_area = (posted.get('area_id')|int(0)) if posted else (schedule.area_id if schedule and schedule.area_id else 0) %}
{% set v_template = (posted.get('template_id')|int(0)) if posted else (schedule.template_id if schedule else 0) %}
{% set v_inspector = (posted.get('inspector_id')|int(0)) if posted else (schedule.inspector_id if schedule else 0) %}
{% set v_frequency = posted.get('frequency') if posted else (schedule.frequency if schedule else 'weekly') %}
{% set v_due = posted.get('next_due_date') if posted else (schedule.due_date.isoformat() if schedule and schedule.due_date else '') %}
{% set v_end = posted.get('end_date') if posted else (schedule.end_date.isoformat() if schedule and schedule.end_date else '') %}
{% set v_mode = posted.get('mode') if posted else (schedule.mode if schedule else 'auto') %}
{% set v_notes = posted.get('notes') if posted else (schedule.notes if schedule and schedule.notes else '') %}
{% set v_month_mode = posted.get('month_mode') if posted else (schedule.month_mode if schedule else 'day_of_month') %}
{% set v_dom = posted.get('day_of_month') if posted else (schedule.day_of_month if schedule and schedule.day_of_month else '') %}
{% set v_nth_week = (posted.get('nth_week')|int(0)) if posted else (schedule.nth_week if schedule and schedule.nth_week else 0) %}
{% set v_nth_weekday = (posted.get('nth_weekday')|int(-1)) if posted else (schedule.nth_weekday if schedule and schedule.nth_weekday is not none else -1) %}
{% set v_weekdays = (posted.getlist('weekdays')|map('int')|list) if posted else (schedule.weekday_list if schedule else []) %}
{% set v_active = (posted.get('active') is not none) if posted else (schedule.active if schedule else True) %}
<div class="row justify-content-center">
<div class="col-lg-8">
<h2 class="mb-4"><i class="bi bi-calendar2-week"></i> {{ title }}</h2>
<form method="POST" class="card shadow-sm">
<div class="col-lg-7">
<div class="card shadow-sm">
<div class="card-header bg-light"><h5 class="mb-0">{{ title }}</h5></div>
<div class="card-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<form method="POST" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">Schedule name</label>
<input type="text" name="name" class="form-control" required
value="{{ schedule.name if schedule else '' }}"
placeholder="e.g. Weekly restroom check — Main Office">
</div>
<div class="mb-3">
<label class="form-label fw-semibold" for="name">Schedule name</label>
<input type="text" name="name" id="name" class="form-control" required
value="{{ v_name }}"
placeholder="e.g. Weekly restroom check — Main Office">
<div class="form-text">Shown in the schedule list and in the inspector's reminder.</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Template</label>
<select name="template_id" class="form-select" required>
<option value="">— Choose a template —</option>
{# Contract selector — UI only; narrows the facility list via AJAX.
It carries no name attribute and is never submitted: the facility
is what the route validates (rule 61). #}
<div class="mb-3">
<label class="form-label fw-semibold" for="contract_select">Contract</label>
<select id="contract_select" class="form-select">
<option value="">— Select Contract —</option>
{% for p in projects %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold" for="facility_id">Facility</label>
<select name="facility_id" id="facility_id" class="form-select" required>
<option value="">— Select a Contract first —</option>
{% for f in facilities %}
<option value="{{ f.id }}"
{{ 'selected' if v_facility == f.id }}>{{ f.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold" for="area_id">
Area <span class="text-muted small fw-normal">(optional)</span>
</label>
<select name="area_id" id="area_id" class="form-select">
<option value="">— Whole facility —</option>
{# Refilled by JS from the chosen facility; this keeps the saved
or just-submitted area selected until that call returns. #}
{% if schedule and schedule.area %}
<option value="{{ schedule.area.id }}"
{{ 'selected' if v_area == schedule.area.id }}>{{ schedule.area.name }}</option>
{% endif %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" for="template_id">Inspection Form</label>
<select name="template_id" id="template_id" class="form-select" required>
<option value="">— Choose a form —</option>
{% for t in templates %}
<option value="{{ t.id }}"
{{ 'selected' if schedule and schedule.template_id == t.id }}>{{ t.name }}</option>
{{ 'selected' if v_template == t.id }}>{{ t.name }}</option>
{% endfor %}
</select>
<div class="form-text">
Shared forms plus any built for this contract. A form belonging to
another contract is rejected on save, not merely hidden here.
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Frequency</label>
<select name="frequency" id="frequencySelect" class="form-select" required>
{% for f in frequencies %}
<option value="{{ f }}"
{{ 'selected' if (schedule and schedule.frequency == f) or (not schedule and f == 'weekly') }}>
{{ frequency_labels.get(f, f|title) }}</option>
{% endfor %}
</select>
</div>
</div>
{# ── Recurrence detail (phase46) ────────────────────────────────────
Only the block matching the chosen frequency is shown; the server
validates the same block and clears the others on save. #}
<div class="row" id="weeklyBlock" style="display:none;">
<div class="col-12 mb-3">
<label class="form-label">Days of the Week</label>
<div class="mb-3">
<label class="form-label fw-semibold" for="inspector_id">Assign to inspector</label>
<select name="inspector_id" id="inspector_id" class="form-select" required>
<option value="">— Choose an inspector —</option>
{% for u in inspectors %}
<option value="{{ u.id }}"
{{ 'selected' if v_inspector == u.id }}>
{{ u.display_name }}{{ ' (Customer)' if u.is_external_inspector }}</option>
{% endfor %}
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold" for="frequency">Frequency</label>
<select name="frequency" id="frequency" class="form-select" required>
{% for f in frequencies %}
<option value="{{ f }}" {{ 'selected' if v_frequency == f }}>
{{ frequency_labels.get(f, f|title) }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold" id="due_date_label" for="next_due_date">
{{ 'Next Due Date' if schedule else 'Start Date' }}
</label>
<input type="date" name="next_due_date" id="next_due_date" class="form-control"
value="{{ v_due }}">
<div class="form-text">
{% if schedule %}
Snapped forward to the first matching day. Leave it unchanged
and saving will not move it.
{% else %}
Snapped forward to the first matching day. Leave blank to start
one full period from now.
{% endif %}
</div>
</div>
</div>
{# ── End date ──
Hidden for one-time schedules, which end by deactivating when
completed. syncFrequency() toggles it; the route clears the column
for 'once', so a stale DOM value cannot survive a frequency change. #}
<div class="row" id="end_date_row" hidden>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold" for="end_date">
End Date <span class="text-muted small fw-normal">(optional)</span>
</label>
<input type="date" name="end_date" id="end_date" class="form-control"
value="{{ v_end }}">
<div class="form-text">
The last date this schedule may run — leave blank to repeat indefinitely.
</div>
</div>
</div>
{# ── Weekly: which days of the week ──────────────────────────── #}
<div class="mb-3 p-3 rounded bg-light border" id="weekly_block" hidden>
<label class="form-label fw-semibold d-block">Days of the Week</label>
<div class="d-flex flex-wrap gap-3">
{% set picked = schedule.weekday_list if schedule else [] %}
{% for i, day in [(0,'Mon'),(1,'Tue'),(2,'Wed'),(3,'Thu'),(4,'Fri'),(5,'Sat'),(6,'Sun')] %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="weekdays"
value="{{ i }}" id="wd{{ i }}" {{ 'checked' if i in picked }}>
<label class="form-check-label" for="wd{{ i }}">{{ day }}</label>
id="weekday_{{ i }}" value="{{ i }}" {{ 'checked' if i in v_weekdays }}>
<label class="form-check-label" for="weekday_{{ i }}">{{ day }}</label>
</div>
{% endfor %}
</div>
<div class="form-text">
Leave the start date on any day — it snaps forward to the first
day you pick here.
<div class="form-text mb-0">
Pick every day the inspection recurs — e.g. Mon, Wed, Fri gives three
inspections a week. The due date rolls to the next selected day each
time one is submitted.
</div>
</div>
</div>
<div class="row" id="monthlyBlock" style="display:none;">
<div class="col-md-4 mb-3">
<label class="form-label">Rule</label>
<select name="month_mode" id="monthModeSelect" class="form-select">
<option value="day_of_month"
{{ 'selected' if not schedule or schedule.month_mode != 'nth_weekday' }}>
On a day of the month</option>
<option value="nth_weekday"
{{ 'selected' if schedule and schedule.month_mode == 'nth_weekday' }}>
On a weekday of the month</option>
</select>
</div>
<div class="col-md-4 mb-3" id="dayOfMonthField">
<label class="form-label">Day of Month</label>
<input type="number" name="day_of_month" class="form-control"
min="1" max="31"
value="{{ schedule.day_of_month if schedule and schedule.day_of_month else '' }}">
<div class="form-text">Clamped to the last day in shorter months.</div>
</div>
<div class="col-md-4 mb-3" id="nthWeekdayField">
<div class="row g-2">
<div class="col-6">
<label class="form-label">Week</label>
<select name="nth_week" class="form-select">
{# ── Monthly: day-of-month OR nth weekday ────────────────────── #}
<div class="mb-3 p-3 rounded bg-light border" id="monthly_block" hidden>
<label class="form-label fw-semibold d-block">Monthly rule</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="month_mode"
id="month_mode_day" value="day_of_month"
{% if v_month_mode != 'nth_weekday' %}checked{% endif %}>
<label class="form-check-label" for="month_mode_day">On a day of the month</label>
</div>
<div class="ms-4 mb-2" id="dom_row">
<div class="input-group input-group-sm" style="max-width:16rem;">
<span class="input-group-text">Day</span>
<input type="number" name="day_of_month" class="form-control"
min="1" max="31" placeholder="15"
value="{{ v_dom }}">
</div>
<div class="form-text mb-0">Months without that day use their last day.</div>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="month_mode"
id="month_mode_nth" value="nth_weekday"
{% if v_month_mode == 'nth_weekday' %}checked{% endif %}>
<label class="form-check-label" for="month_mode_nth">On a weekday of the month</label>
</div>
<div class="ms-4" id="nth_row">
<div class="d-flex gap-2 flex-wrap" style="max-width:24rem;">
<select name="nth_week" class="form-select form-select-sm" style="max-width:7rem;">
{% for v, lbl in [(1,'1st'),(2,'2nd'),(3,'3rd'),(4,'4th'),(5,'5th'),(-1,'Last')] %}
<option value="{{ v }}"
{{ 'selected' if schedule and schedule.nth_week == v }}>{{ lbl }}</option>
<option value="{{ v }}" {{ 'selected' if v_nth_week == v }}>{{ lbl }}</option>
{% endfor %}
</select>
</div>
<div class="col-6">
<label class="form-label">Weekday</label>
<select name="nth_weekday" class="form-select">
<select name="nth_weekday" class="form-select form-select-sm" style="max-width:11rem;">
{% for i, day in [(0,'Monday'),(1,'Tuesday'),(2,'Wednesday'),(3,'Thursday'),(4,'Friday'),(5,'Saturday'),(6,'Sunday')] %}
<option value="{{ i }}"
{{ 'selected' if schedule and schedule.nth_weekday == i }}>{{ day }}</option>
<option value="{{ i }}" {{ 'selected' if v_nth_weekday == i }}>{{ day }}</option>
{% endfor %}
</select>
</div>
<div class="form-text mb-0">e.g. the 2nd Tuesday of every month.</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" id="dueDateLabel">
{{ 'Next Due Date' if schedule else 'Start Date' }}
</label>
<input type="date" name="next_due_date" class="form-control"
value="{{ schedule.due_date.isoformat() if schedule and schedule.due_date else '' }}">
<div class="form-text">
{% if schedule %}
When the next occurrence is due. Leave unchanged and saving will
not move it.
{% else %}
Leave blank to start one full period from now.
{% endif %}
</div>
</div>
<div class="col-md-6 mb-3" id="endDateBlock">
<label class="form-label">End Date <span class="text-muted small">(optional)</span></label>
<input type="date" name="end_date" class="form-control"
value="{{ schedule.end_date.isoformat() if schedule and schedule.end_date else '' }}">
<div class="form-text">
Last date this schedule may produce an occurrence. Blank = repeats
indefinitely.
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Mode</label>
<select name="mode" class="form-select">
<option value="auto" {{ 'selected' if not schedule or schedule.mode != 'plan' }}>
<div class="mb-3">
<label class="form-label fw-semibold" for="mode">Mode</label>
<select name="mode" id="mode" class="form-select">
<option value="auto" {{ 'selected' if v_mode != 'plan' }}>
Auto — create the inspection automatically each period</option>
<option value="plan" {{ 'selected' if schedule and schedule.mode == 'plan' }}>
<option value="plan" {{ 'selected' if v_mode == 'plan' }}>
Plan — inspector presses Start (with due/overdue reminders)</option>
</select>
<div class="form-text">
@@ -147,151 +237,191 @@
the day, and alerts managers once it's overdue.
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Notes for the inspector <span class="text-muted small">(optional)</span></label>
<textarea name="notes" class="form-control" rows="2"
placeholder="Anything the inspector should know before starting">{{ schedule.notes if schedule and schedule.notes else '' }}</textarea>
<div class="form-text">Copied onto the inspection when it starts.</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Facility</label>
<select name="facility_id" id="facilitySelect" class="form-select" required>
<option value="">— Choose a facility —</option>
{% for f in facilities %}
<option value="{{ f.id }}"
{{ 'selected' if schedule and schedule.facility_id == f.id }}>{{ f.name }}</option>
{% endfor %}
</select>
<div class="mb-3">
<label class="form-label fw-semibold" for="notes">
Notes for the inspector <span class="text-muted small fw-normal">(optional)</span>
</label>
<textarea name="notes" id="notes" class="form-control" rows="3"
placeholder="e.g. Front lobby carpet needs extra attention. Check loading dock after 3 PM — key is at the front desk.">{{ v_notes }}</textarea>
<div class="form-text">
<i class="bi bi-info-circle"></i>
Shown to the assigned inspector when they open this inspection, on the web and on the iPad.
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Area <span class="text-muted small">(optional)</span></label>
<select name="area_id" id="areaSelect" class="form-select">
<option value="">— Whole facility —</option>
{% if schedule and schedule.area %}
<option value="{{ schedule.area.id }}" selected>{{ schedule.area.name }}</option>
{% endif %}
</select>
{% if schedule %}
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="active" id="active"
{{ 'checked' if v_active }}>
<label class="form-check-label" for="active">Active</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Assign to inspector</label>
<select name="inspector_id" class="form-select" required>
<option value="">— Choose an inspector —</option>
{% for u in inspectors %}
<option value="{{ u.id }}"
{{ 'selected' if schedule and schedule.inspector_id == u.id }}>
{{ u.display_name }} ({{ u.role }})</option>
{% endfor %}
</select>
</div>
{% if schedule %}
<div class="form-check form-switch mb-1">
<input class="form-check-input" type="checkbox" name="active" id="activeSwitch"
{{ 'checked' if schedule.active }}>
<label class="form-check-label" for="activeSwitch">Active</label>
</div>
<p class="text-muted small">
Current {{ 'due date' if schedule.mode == 'plan' else 'run' }}:
{{ schedule.next_run_at.strftime('%Y-%m-%d %H:%M') if schedule.next_run_at else '—' }}
{% if schedule.last_completed_at %}
· last completed {{ schedule.last_completed_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
{% if schedule.end_date %}· ends {{ schedule.end_date.strftime('%Y-%m-%d') }}{% endif %}
<br>
Reminders for this occurrence are only reset if the due date actually
moves, so renaming or re-noting a schedule will not re-send them.
</p>
{% endif %}
</div>
<div class="card-footer d-flex justify-content-between">
<a href="{{ url_for('inspection_schedules.index') }}" class="btn btn-outline-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Save Schedule
</button>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Save
</button>
<a href="{{ url_for('inspection_schedules.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</form>
</div>
<p class="text-muted small mt-2">
Recurring schedules automatically roll their due date forward each time the
inspection is completed. The assigned inspector is reminded the day before
and on the due date; managers are alerted if it becomes overdue.
{% if schedule %}
<br>
Current {{ 'due date' if schedule.mode == 'plan' else 'run' }}:
{{ schedule.next_run_at.strftime('%Y-%m-%d %H:%M') if schedule.next_run_at else '—' }}
{% if schedule.last_completed_at %}
· last completed {{ schedule.last_completed_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
{% if schedule.end_date %}· ends {{ schedule.end_date.strftime('%Y-%m-%d') }}{% endif %}.
Reminders for this occurrence are only reset if the due date actually moves,
so renaming or re-noting a schedule will not re-send them.
{% endif %}
</p>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Recurrence blocks follow the chosen frequency (phase46/47).
// Display only — the server re-validates and clears the unused blocks on save.
// ── Contract → Facility → Area cascade ──────────────────────────────────────
// The contract selector is UI-only (no name attribute): it never reaches the
// server, it only narrows the facility list. facilities_for_project is scoped
// to the caller, so a Customer Director asking for another contract's id gets
// an empty list rather than that customer's building names.
(function () {
var freq = document.getElementById('frequencySelect');
var weekly = document.getElementById('weeklyBlock');
var monthly = document.getElementById('monthlyBlock');
var monthMode = document.getElementById('monthModeSelect');
var domField = document.getElementById('dayOfMonthField');
var nthField = document.getElementById('nthWeekdayField');
var endBlock = document.getElementById('endDateBlock');
var dueLabel = document.getElementById('dueDateLabel');
var isEdit = {{ 'true' if schedule else 'false' }};
var MONTHLY = ['monthly', 'quarterly', 'bi-annually', 'annually'];
'use strict';
var contractSel = document.getElementById('contract_select');
var facilitySel = document.getElementById('facility_id');
var areaSel = document.getElementById('area_id');
if (!contractSel || !facilitySel) { return; }
function syncMonthMode() {
if (!monthMode || !domField || !nthField) return;
var nth = monthMode.value === 'nth_weekday';
domField.style.display = nth ? 'none' : '';
nthField.style.display = nth ? '' : 'none';
var FACILITIES_URL = '{{ url_for("inspections.facilities_for_project", project_id=0) }}'.replace('/0', '/');
var AREAS_URL = '{{ url_for("inspections.areas_for_facility", facility_id=0) }}'.replace('/0', '/');
var preProjectId = {{ selected_project_id | tojson }};
var preFacilityId = {{ v_facility | tojson }};
var preAreaId = {{ v_area | tojson }};
function setPlaceholder() {
facilitySel.innerHTML = '<option value="">— Select a Contract first —</option>';
facilitySel.disabled = true;
loadAreas('', false);
}
function sync() {
if (!freq) return;
var v = freq.value;
if (weekly) weekly.style.display = (v === 'weekly') ? '' : 'none';
if (monthly) monthly.style.display = (MONTHLY.indexOf(v) !== -1) ? '' : 'none';
// A one-time schedule has no end date — it closes when it is completed.
if (endBlock) endBlock.style.display = (v === 'once') ? 'none' : '';
if (dueLabel) {
dueLabel.textContent = isEdit ? 'Next Due Date'
: (v === 'once' ? 'Date' : 'Start Date');
}
syncMonthMode();
}
if (freq) freq.addEventListener('change', sync);
if (monthMode) monthMode.addEventListener('change', syncMonthMode);
sync();
})();
// Facility → Area cascade, reusing the existing inspections AJAX endpoint.
(function () {
var facilitySelect = document.getElementById('facilitySelect');
var areaSelect = document.getElementById('areaSelect');
if (!facilitySelect || !areaSelect) return;
var preselectedAreaId = {{ (schedule.area_id if schedule and schedule.area_id else 0) | tojson }};
function loadAreas(facilityId, keepSelection) {
areaSelect.innerHTML = '<option value="">— Whole facility —</option>';
if (!facilityId) return;
fetch('{{ url_for('inspections.areas_for_facility', facility_id=0) }}'.replace('/0', '/' + facilityId))
if (!areaSel) { return; }
areaSel.innerHTML = '<option value="">— Whole facility —</option>';
if (!facilityId) { return; }
fetch(AREAS_URL + facilityId)
.then(function (r) { return r.json(); })
.then(function (areas) {
areas.forEach(function (a) {
var opt = document.createElement('option');
opt.value = a.id;
opt.textContent = a.name;
if (keepSelection && a.id === preselectedAreaId) opt.selected = true;
areaSelect.appendChild(opt);
if (keepSelection && a.id === preAreaId) { opt.selected = true; }
areaSel.appendChild(opt);
});
})
.catch(function () { /* leave the whole-facility default in place */ });
}
facilitySelect.addEventListener('change', function () {
preselectedAreaId = 0;
function loadFacilities(projectId, restoreFacilityId) {
facilitySel.disabled = true;
facilitySel.innerHTML = '<option value="">Loading…</option>';
fetch(FACILITIES_URL + projectId)
.then(function (r) { return r.json(); })
.then(function (data) {
facilitySel.innerHTML = '<option value="">— Select Facility —</option>';
data.forEach(function (f) {
var opt = document.createElement('option');
opt.value = f.id;
opt.textContent = f.name;
if (restoreFacilityId && f.id === restoreFacilityId) { opt.selected = true; }
facilitySel.appendChild(opt);
});
facilitySel.disabled = false;
if (restoreFacilityId) { loadAreas(restoreFacilityId, true); }
})
.catch(function () {
facilitySel.innerHTML = '<option value="">Could not load facilities</option>';
});
}
contractSel.addEventListener('change', function () {
if (this.value) { loadFacilities(this.value, null); }
else { setPlaceholder(); }
});
facilitySel.addEventListener('change', function () {
preAreaId = 0; // a new facility invalidates the saved area
loadAreas(this.value, false);
});
// On edit load, refresh the area list for the saved facility and keep the saved area.
if (facilitySelect.value) loadAreas(facilitySelect.value, true);
})();
// Initial state: restore the contract, facility and area on edit / re-render.
if (preProjectId) {
contractSel.value = String(preProjectId);
loadFacilities(preProjectId, preFacilityId);
} else if (preFacilityId) {
// Facility on no contract (or one the selector cannot name): keep the
// server-rendered options and the current choice rather than clearing it.
facilitySel.disabled = false;
loadAreas(String(preFacilityId), true);
} else {
setPlaceholder();
}
}());
// ── Recurrence blocks follow the chosen frequency ───────────────────────────
// Display only — the server re-validates and clears the unused blocks on save,
// so stale values left in the DOM never take effect.
(function () {
'use strict';
var freq = document.getElementById('frequency');
var weekly = document.getElementById('weekly_block');
var monthly = document.getElementById('monthly_block');
var endRow = document.getElementById('end_date_row');
var dueLabel = document.getElementById('due_date_label');
if (!freq || !weekly || !monthly) { return; }
var domRadio = document.getElementById('month_mode_day');
var nthRadio = document.getElementById('month_mode_nth');
var domRow = document.getElementById('dom_row');
var nthRow = document.getElementById('nth_row');
var isEdit = {{ 'true' if schedule else 'false' }};
// Every frequency that repeats on a month boundary uses the monthly rule.
var MONTHLY = ['monthly', 'quarterly', 'bi-annually', 'annually'];
function syncMonthMode() {
if (!domRow || !nthRow) { return; }
var useNth = nthRadio && nthRadio.checked;
domRow.style.opacity = useNth ? '.45' : '1';
nthRow.style.opacity = useNth ? '1' : '.45';
}
function syncFrequency() {
weekly.hidden = freq.value !== 'weekly';
monthly.hidden = MONTHLY.indexOf(freq.value) === -1;
// End date is a recurring-only concept.
if (endRow) { endRow.hidden = freq.value === 'once'; }
if (dueLabel) {
dueLabel.textContent = isEdit ? 'Next Due Date'
: (freq.value === 'once' ? 'Date' : 'Start Date');
}
syncMonthMode();
}
freq.addEventListener('change', syncFrequency);
[domRadio, nthRadio].forEach(function (r) {
if (r) { r.addEventListener('change', syncMonthMode); }
});
syncFrequency();
}());
</script>
{% endblock %}
+13 -5
View File
@@ -9,11 +9,19 @@
['admin','director','project_manager','auditor','customer'] %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-calendar2-week"></i> Inspection Schedules</h2>
{% if can_manage_schedules %}
<a href="{{ url_for('inspection_schedules.create') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> New Schedule
</a>
{% endif %}
{# This page is reached from the Inspections list ("Scheduled") and has no
entry of its own in the main nav, so without this button the only way back
is the browser control. #}
<div class="d-flex gap-2">
<a href="{{ url_for('inspections.index') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Inspections
</a>
{% if can_manage_schedules %}
<a href="{{ url_for('inspection_schedules.create') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> New Schedule
</a>
{% endif %}
</div>
</div>
<p class="text-muted small mb-4">