Files
JQC_multi_tenant/app/templates/inspection_schedules/form.html
T

298 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<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="card-body">
<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="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>
{% for t in templates %}
<option value="{{ t.id }}"
{{ 'selected' if schedule and schedule.template_id == t.id }}>{{ t.name }}</option>
{% endfor %}
</select>
</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="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>
</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>
</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">
{% 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>
{% endfor %}
</select>
</div>
<div class="col-6">
<label class="form-label">Weekday</label>
<select name="nth_weekday" class="form-select">
{% 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>
{% endfor %}
</select>
</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' }}>
Auto — create the inspection automatically each period</option>
<option value="plan" {{ 'selected' if schedule and schedule.mode == 'plan' }}>
Plan — inspector presses Start (with due/overdue reminders)</option>
</select>
<div class="form-text">
Auto drops an in-progress inspection into the inspector's queue on
schedule. Plan assigns a due date and reminds them the day before, on
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>
<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>
</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>
</form>
</div>
</div>
<script>
// Recurrence blocks follow the chosen frequency (phase46/47).
// Display only — the server re-validates and clears the unused blocks on save.
(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'];
function syncMonthMode() {
if (!monthMode || !domField || !nthField) return;
var nth = monthMode.value === 'nth_weekday';
domField.style.display = nth ? 'none' : '';
nthField.style.display = nth ? '' : 'none';
}
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))
.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);
});
})
.catch(function () { /* leave the whole-facility default in place */ });
}
facilitySelect.addEventListener('change', function () {
preselectedAreaId = 0;
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);
})();
</script>
{% endblock %}