First commit

This commit is contained in:
2026-06-26 09:04:34 -04:00
commit 77678ed724
166 changed files with 34842 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
{% extends "base.html" %}
{% block title %}Edit Template — {{ template.name }}{% endblock %}
{% block extra_css %}
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
body { font-family: 'DM Sans', sans-serif; }
.page-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 1.5rem; gap: 1rem;
}
.page-title { font-size: 1.3rem; font-weight: 700; color: #0f172a; margin: 0; }
/* two-column layout */
.edit-layout {
display: grid;
grid-template-columns: 340px 1fr;
gap: 1.5rem;
align-items: start;
}
/* left: settings card */
.settings-card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
position: sticky;
top: 1rem;
}
.settings-card-header {
background: #1a1d23;
color: #fff;
padding: .85rem 1.25rem;
font-weight: 600;
font-size: .9rem;
display: flex;
align-items: center;
gap: .5rem;
}
.settings-card-body { padding: 1.25rem; }
/* right: form canvas preview */
.canvas-card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
}
.canvas-card-header {
display: flex; align-items: center; justify-content: space-between;
padding: .85rem 1.25rem;
border-bottom: 1px solid #e2e8f0;
background: #fafbfc;
}
.canvas-card-header h6 { margin: 0; font-weight: 700; font-size: .88rem; color: #0f172a; }
.canvas-card-body { padding: 1.25rem; overflow-x: auto; }
/* grid minimap — same constants as editor */
.form-grid {
display: grid;
grid-template-columns: repeat(12, 72px);
grid-auto-rows: 52px;
gap: 8px;
width: calc(12 * 72px + 11 * 8px);
}
.fg-cell {
overflow: hidden; display: flex; flex-direction: column;
background: #f8fafc; border: 1px solid #e2e8f0;
border-radius: 8px; padding: .4rem .6rem;
cursor: default;
transition: border-color .15s, box-shadow .15s;
}
.fg-cell:hover { border-color: #93c5fd; box-shadow: 0 2px 8px rgba(37,99,235,.1); }
.fg-cell .fl {
font-size: .72rem; font-weight: 600; color: #374151;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-bottom: .15rem;
}
.fg-cell .ft {
font-size: .6rem; font-family: monospace;
color: #2563eb; background: #eff6ff;
padding: .05rem .28rem; border-radius: 10px;
display: inline-block; width: fit-content;
text-transform: uppercase; letter-spacing: .04em;
}
.req-dot { color: #dc2626; }
.fg-section {
border-top: 2px solid #e2e8f0; padding-top: .35rem;
display: flex; align-items: center; height: 100%;
}
.fg-section span { font-weight: 700; font-size: .82rem; color: #374151; }
/* empty / open-editor CTA */
.editor-cta {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 3rem 2rem; text-align: center; color: #94a3b8;
gap: .75rem;
}
.editor-cta i { font-size: 2.2rem; opacity: .3; }
.editor-cta p { margin: 0; font-size: .85rem; }
/* field count badge */
.field-count {
font-size: .72rem; font-weight: 600;
background: #eff6ff; color: #2563eb;
padding: .2rem .6rem; border-radius: 20px;
}
/* open editor banner */
.editor-banner {
margin: 1rem 1.25rem 0;
padding: .75rem 1rem;
background: #eff6ff; border: 1px solid #bfdbfe;
border-radius: 8px;
display: flex; align-items: center; justify-content: space-between; gap: 1rem;
font-size: .83rem; color: #1e40af;
}
.editor-banner .left { display: flex; align-items: center; gap: .5rem; }
</style>
{% endblock %}
{% block content %}
<div class="page-header">
<h2 class="page-title"><i class="bi bi-file-earmark-text text-primary"></i> Edit Template</h2>
<a href="{{ url_for('templates.view_template', template_id=template.id) }}"
class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> Back
</a>
</div>
<div class="edit-layout">
<!-- LEFT: template metadata settings -->
<div class="settings-card">
<div class="settings-card-header">
<i class="bi bi-sliders"></i> Template Settings
</div>
<div class="settings-card-body">
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label fw-semibold small") }}
{{ form.name(class="form-control form-control-sm") }}
{% if form.name.errors %}
<div class="text-danger small mt-1">{{ form.name.errors[0] }}</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label fw-semibold small") }}
{{ form.description(class="form-control form-control-sm", rows=3) }}
</div>
<div class="mb-4">
{{ form.frequency.label(class="form-label fw-semibold small") }}
{{ form.frequency(class="form-select form-select-sm") }}
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-save"></i> Save Settings
</button>
</div>
</form>
</div>
<!-- open form editor banner -->
<div class="editor-banner">
<div class="left">
<i class="bi bi-grid-3x3-gap"></i>
<span>Edit fields &amp; layout in the Form Editor</span>
</div>
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}"
class="btn btn-primary btn-sm flex-shrink-0">
<i class="bi bi-pencil-square"></i> Open Editor
</a>
</div>
<div style="height:.85rem;"></div>
</div>
<!-- RIGHT: form canvas preview -->
<div class="canvas-card">
<div class="canvas-card-header">
<h6><i class="bi bi-grid-3x3-gap me-1 text-primary"></i> Form Layout</h6>
<div class="d-flex align-items: center; gap: .75rem;">
<span class="field-count">{{ form_fields|length }} field{{ 's' if form_fields|length != 1 else '' }}</span>
<a href="{{ url_for('templates.form_preview', template_id=template.id) }}"
target="_blank" class="btn btn-outline-secondary btn-sm ms-2">
<i class="bi bi-eye"></i> Preview
</a>
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}"
class="btn btn-primary btn-sm">
<i class="bi bi-pencil-square"></i> Edit Form
</a>
</div>
</div>
<div class="canvas-card-body">
{% if form_fields %}
<div class="form-grid">
{% for f in form_fields %}
<div class="fg-cell"
style="grid-column: {{ f.col }} / span {{ f.colSpan }};
grid-row: {{ f.row }} / span {{ f.rowSpan }};"
title="{{ f.label }} ({{ f.type.replace('_', ' ') }})">
{% if f.type == 'section' %}
<div class="fg-section"><span>{{ f.label }}</span></div>
{% elif f.type == 'table' %}
<div class="fl"><i class="bi bi-table" style="color:#2563eb;margin-right:.2rem;"></i>{{ f.label }}</div>
<span class="ft">table · {{ f.col_headers|length if f.col_headers else 3 }} cols × {{ f.table_rows or 3 }} rows</span>
{% elif f.type == 'label' %}
<div class="fl" style="font-size:.7rem;color:#374151;overflow:hidden;line-height:1.3;white-space:nowrap;text-overflow:ellipsis;">{{ f.text_content or 'Label text' }}</div>
<span class="ft">label</span>
{% elif f.type.startswith('button_') %}
<div class="fl"><i class="bi bi-{% if f.type == 'button_submit' %}send-fill{% elif f.type == 'button_print' %}printer-fill{% else %}envelope-fill{% endif %}" style="color:#2563eb;margin-right:.2rem;"></i>{{ f.btn_label or f.type.replace('button_','') | title }}</div>
<span class="ft">{{ f.type.replace('_',' ') }}</span>
{% else %}
<div class="fl">
{{ f.label }}{% if f.required %}<span class="req-dot"> *</span>{% endif %}
</div>
<span class="ft">{{ f.type.replace('_', ' ') }}</span>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="editor-cta">
<i class="bi bi-layout-text-sidebar-reverse"></i>
<p>No fields yet. Open the Form Editor to start building your inspection form.</p>
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}"
class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle"></i> Open Form Editor
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+44
View File
@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">{{ title }}</h4>
</div>
<div class="card-body">
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label") }}
{{ form.description(class="form-control", rows=3) }}
</div>
<div class="mb-4">
{{ form.frequency.label(class="form-label") }}
{{ form.frequency(class="form-select") }}
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Save Template
</button>
<a href="{{ url_for('templates.index') }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
File diff suppressed because it is too large Load Diff
+406
View File
@@ -0,0 +1,406 @@
{% extends "base.html" %}
{% block title %}Preview — {{ template.name }}{% endblock %}
{% block extra_css %}
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
body { font-family: 'DM Sans', sans-serif; background: #eef0f4; }
.preview-wrap { max-width: 960px; margin: 2rem auto; padding: 0 1rem 3rem; }
.preview-header {
background: #1a1d23; color: #fff;
padding: 1.25rem 1.75rem;
border-radius: 12px 12px 0 0;
display: flex; align-items: center; justify-content: space-between;
}
.preview-header h4 { margin: 0; font-weight: 600; font-size: 1.05rem; }
.preview-header .sub { font-size: .78rem; color: #94a3b8; margin-top: .2rem; }
.freq-badge {
font-size: .72rem;
background: rgba(255,255,255,.15);
padding: .28rem .65rem; border-radius: 20px; font-weight: 500;
}
.preview-notice {
background: #eff6ff; border: 1px solid #bfdbfe;
border-radius: 8px; padding: .65rem 1rem;
font-size: .82rem; color: #1d4ed8;
margin-bottom: 1.5rem;
display: flex; align-items: center; gap: .45rem;
}
.preview-body {
background: #fff;
border: 1px solid #e2e8f0; border-top: none;
border-radius: 0 0 12px 12px;
padding: 1.75rem;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 12-col grid — same columns as editor, rows shrink to content in preview */
.form-grid {
display: grid;
grid-template-columns: repeat(12, 72px);
grid-auto-rows: auto; /* no fixed row height — rows are only as tall as their content */
gap: 4px 8px; /* 4px row-gap (tight), 8px col-gap (matches editor) */
width: max-content;
}
/* —— iPad / touch-device fluid grid override —— */
@media (max-width: 1194px) {
.form-grid {
--_cell: calc((min(calc(100vw - 2rem), 900px) - 11 * 8px) / 12);
grid-template-columns: repeat(12, var(--_cell));
width: 100%;
}
.preview-body { padding: 1rem; }
}
/* ── Cell ── */
.fg-cell {
overflow: hidden;
display: flex; flex-direction: column;
padding: .22rem .55rem; /* top/bottom halved from .45rem */
}
/* Label shown above the input — same style as editor card header label */
.fg-cell .field-lbl {
font-size: .74rem; font-weight: 500;
color: #64748b; margin-bottom: .2rem; display: block;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
flex-shrink: 0;
}
.required-mark { color: #dc2626; }
.help-text { font-size: .72rem; color: #64748b; margin-top: .18rem; flex-shrink: 0; }
/* Inputs/selects: compact to match editor body sizing */
.fg-cell .form-control,
.fg-cell .form-select {
font-size: .76rem;
padding: .2rem .4rem;
border-color: #e2e8f0;
background: #f8fafc;
/* NO flex/height stretch — inputs must be their natural intrinsic height,
exactly as in the editor where no flex is applied to .fcard-body inputs */
}
.fg-cell textarea.form-control { resize: vertical; flex: 1; min-height: 4; }
.fg-cell .upload-zone,
.fg-cell .signature-box { flex: 1; min-height: 0; }
.fg-cell .form-check-label { font-size: .76rem; color: #64748b; }
.fg-cell .form-check-input { margin-top: .18rem; }
.fg-cell .form-check { margin-bottom: .1rem; }
.upload-zone {
border: 2px dashed #cbd5e1; border-radius: 6px;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: #64748b; background: #f8fafc;
cursor: pointer; font-size: .74rem; gap: .2rem;
transition: border-color .18s, background .18s;
}
.upload-zone:hover { border-color: #2563eb; background: #eff6ff; color: #2563eb; }
.upload-zone i { font-size: 1.1rem; }
.signature-box {
border-bottom: 2px solid #e2e8f0;
flex: 1; min-height: 0;
display: flex; align-items: flex-end;
padding: .75rem .4rem .15rem; /* same as editor .mock-sig */
color: #94a3b8; font-size: .72rem; font-style: italic;
}
.rating-stars { display: flex; gap: .25rem; align-items: center; padding-top: .08rem; }
.rating-stars button {
border: none; background: none;
font-size: 1.45rem; color: #cbd5e1;
cursor: pointer; padding: 0; line-height: 1;
transition: color .12s;
}
.rating-stars button:hover,
.rating-stars button.on { color: #f59e0b; }
.section-divider {
border-top: 2px solid #e2e8f0; padding-top: .4rem;
height: 100%; display: flex; align-items: center;
padding: 0; /* cell already has .45rem .55rem padding */
}
.section-divider h5 { font-weight: 700; color: #374151; margin: 0; font-size: .95rem; }
.preview-footer {
display: flex; align-items: center; justify-content: space-between;
margin-top: 1.5rem; padding-top: 1.25rem;
border-top: 1px solid #e2e8f0;
}
.btn-back {
display: inline-flex; align-items: center; gap: .4rem;
padding: .42rem .95rem; border-radius: 7px;
background: #1a1d23; color: #fff;
font-size: .83rem; font-weight: 600;
text-decoration: none; transition: background .18s;
}
.btn-back:hover { background: #374151; color: #fff; }
.empty-form {
text-align: center; padding: 3rem 1rem; color: #94a3b8;
}
.empty-form i { font-size: 2.25rem; display: block; margin-bottom: .65rem; opacity: .38; }
/* table field */
.tbl-field {
width: 100%; border-collapse: collapse; font-size: .8rem;
}
.tbl-field th {
background: #f1f5f9; font-weight: 600; color: #374151;
padding: .3rem .5rem; border: 1px solid #e2e8f0;
white-space: nowrap; font-size: .78rem;
}
.tbl-field td {
border: 1px solid #e2e8f0; padding: .15rem .25rem;
}
.tbl-input {
width: 100%; border: none; outline: none;
font-size: .8rem; padding: .1rem .2rem;
background: transparent; color: #0f172a;
}
.tbl-input:focus { background: #eff6ff; }
</style>
{% endblock %}
{% block content %}
<div class="preview-wrap">
<div class="preview-header">
<div>
<h4><i class="bi bi-file-earmark-check"></i> {{ template.name }}</h4>
<div class="sub">{{ template.description or 'Inspection form preview' }}</div>
</div>
<span class="freq-badge">{{ template.frequency|title }}</span>
</div>
<div class="preview-body">
<div class="preview-notice">
<i class="bi bi-eye"></i>
<span>This is a <strong>read-only preview</strong>. Layout matches the grid editor exactly.</span>
</div>
{% if form_fields %}
<div class="form-grid">
{% for field in form_fields %}
<div class="fg-cell"
style="grid-column: {{ field.col }} / span {{ field.colSpan }};
grid-row: {{ field.row }} / span {{ field.rowSpan }};">
{% if field.type == 'section' %}
<div class="section-divider"><strong>{{ field.label }}</strong></div>
{% elif field.type == 'text' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<input type="text" class="form-control" placeholder="{{ field.placeholder or '' }}">
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'textarea' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<textarea class="form-control" style="flex:1;resize:vertical;" placeholder="{{ field.placeholder or '' }}"></textarea>
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'number' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<input type="number" class="form-control" placeholder="{{ field.placeholder or '' }}">
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'date' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<input type="date" class="form-control">
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'email' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<input type="email" class="form-control" placeholder="{{ field.placeholder or 'name@example.com' }}">
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'checkbox' %}
<div class="form-check">
<input class="form-check-input" type="checkbox" id="f_{{ loop.index }}">
<label class="form-check-label" for="f_{{ loop.index }}">
{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}
</label>
</div>
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'checkbox_group' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
{% for opt in field.options %}
<div class="form-check">
<input class="form-check-input" type="checkbox" id="cg_{{ loop.index }}_{{ loop.index0 }}">
<label class="form-check-label" for="cg_{{ loop.index }}_{{ loop.index0 }}">{{ opt }}</label>
</div>
{% endfor %}
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'radio' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
{% for opt in field.options %}
<div class="form-check">
<input class="form-check-input" type="radio" name="rg_{{ loop.index }}" id="rg_{{ loop.index }}_{{ loop.index0 }}">
<label class="form-check-label" for="rg_{{ loop.index }}_{{ loop.index0 }}">{{ opt }}</label>
</div>
{% endfor %}
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'pass_fail' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="text-danger">*</span>{% endif %}</label>
{% set pf_options = field.options if field.options else ['Pass', 'Fail'] %}
<div style="display:flex;gap:.4rem;margin-top:.2rem;">
{% for opt in pf_options %}
{% set is_pass = opt.lower() in ('pass','yes','ok','good') %}
<span style="padding:.25rem .65rem;border-radius:20px;font-size:.78rem;font-weight:600;
border:1.5px solid {{ '#16a34a' if is_pass else '#dc2626' }};
color:{{ '#16a34a' if is_pass else '#dc2626' }};">{{ opt }}</span>
{% endfor %}
</div>
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'select' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<select class="form-select">
<option value="">-- Select --</option>
{% for opt in field.options %}<option>{{ opt }}</option>{% endfor %}
</select>
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'image' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<div class="upload-zone">
<i class="bi bi-cloud-upload"></i>
<span>Upload photo</span>
</div>
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'signature' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<div class="signature-box">Sign here…</div>
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'rating' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<div class="rating-stars" data-rating="0">
{% for i in range(1, 6) %}
<button type="button" data-val="{{ i }}" onclick="setRating(this)"></button>
{% endfor %}
</div>
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% elif field.type == 'label' %}
{% set fs_map = {'small':'0.78rem','normal':'0.9rem','large':'1.05rem','x-large':'1.25rem'} %}
<div style="font-size:{{ fs_map.get(field.font_size or 'normal', '0.9rem') }};
font-weight:{{ field.font_weight or 'normal' }};
color:#374151;line-height:1.5;white-space:pre-wrap;overflow:hidden;height:100%;">{{ field.text_content or 'Label text' }}</div>
{% elif field.type == 'button_submit' %}
<button type="button" class="btn btn-primary w-100" onclick="handleSubmit(this)">
<i class="bi bi-send-fill"></i> {{ field.btn_label or 'Submit Form' }}
</button>
{% elif field.type == 'button_print' %}
<button type="button" class="btn btn-outline-secondary w-100" onclick="window.print()">
<i class="bi bi-printer-fill"></i> {{ field.btn_label or 'Print Form' }}
</button>
{% elif field.type == 'button_email' %}
<button type="button" class="btn btn-outline-primary w-100" onclick="handleEmail(this)">
<i class="bi bi-envelope-fill"></i> {{ field.btn_label or 'Email Form' }}
</button>
{% elif field.type == 'table' %}
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
<div style="overflow:auto;flex:1;min-height:0;">
<table class="tbl-field">
<thead>
<tr>
{% for hdr in (field.col_headers or ['Column 1','Column 2','Column 3']) %}
<th>{{ hdr }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for r in range(field.table_rows or 3) %}
<tr>
{% for hdr in (field.col_headers or ['Column 1','Column 2','Column 3']) %}
<td><input type="text" class="tbl-input"></td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
<div class="preview-footer">
<button onclick="window.close()" class="btn-back">
<i class="bi bi-x-lg"></i> Close Preview
</button>
<button class="btn btn-primary" disabled>
<i class="bi bi-send"></i> Submit Inspection
</button>
</div>
{% else %}
<div class="empty-form">
<i class="bi bi-layout-text-sidebar-reverse"></i>
<strong>No fields yet</strong>
<p class="mb-3" style="font-size:.84rem;">
This template has no form fields. Open the editor to add some.
</p>
<button onclick="window.close()" class="btn-back">
<i class="bi bi-x-lg"></i> Close Preview
</button>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function setRating(btn) {
const group = btn.closest('.rating-stars');
const val = parseInt(btn.dataset.val);
group.dataset.rating = val;
group.querySelectorAll('button').forEach(b => {
b.classList.toggle('on', parseInt(b.dataset.val) <= val);
});
}
function handleSubmit(btn) {
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-check-circle-fill"></i> Submitted!';
btn.classList.replace('btn-primary', 'btn-success');
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = btn.dataset.orig || btn.innerHTML;
btn.classList.replace('btn-success', 'btn-primary');
}, 2500);
}
function handleEmail(btn) {
// In a real inspection this would POST and email; here it shows a toast.
const toast = document.getElementById('emailToast');
toast.style.display = 'flex';
setTimeout(() => { toast.style.display = 'none'; }, 3000);
}
</script>
<!-- email toast -->
<div id="emailToast" style="display:none;position:fixed;bottom:1.5rem;right:1.5rem;
background:#16a34a;color:#fff;padding:.65rem 1.1rem;border-radius:8px;
align-items:center;gap:.5rem;font-size:.85rem;font-weight:600;
box-shadow:0 4px 16px rgba(0,0,0,.15);z-index:9999;">
<i class="bi bi-check-circle-fill"></i> Form emailed successfully
</div>
{% endblock %}
+61
View File
@@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">{{ title }}</h4>
<small>Template: {{ template.name }}</small>
</div>
<div class="card-body">
<form method="POST">
{{ form.hidden_tag() }}
<div class="row">
<div class="col-md-6 mb-3">
{{ form.category.label(class="form-label") }}
{{ form.category(class="form-control", placeholder="e.g., Restrooms, Floors, etc.") }}
</div>
<div class="col-md-6 mb-3">
{{ form.scoring_type.label(class="form-label") }}
{{ form.scoring_type(class="form-select") }}
</div>
</div>
<div class="mb-3">
{{ form.item_description.label(class="form-label") }}
{{ form.item_description(class="form-control", rows=3) }}
</div>
<div class="row">
<div class="col-md-6 mb-3">
{{ form.weight.label(class="form-label") }}
{{ form.weight(class="form-control") }}
<small class="text-muted">1.0 = standard weight</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">&nbsp;</label>
<div class="form-check">
{{ form.requires_photo(class="form-check-input") }}
{{ form.requires_photo.label(class="form-check-label") }}
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Save Item
</button>
<a href="{{ url_for('templates.edit_template', template_id=template.id) }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+269
View File
@@ -0,0 +1,269 @@
{% extends "base.html" %}
{% block title %}Inspection Templates{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-md-6">
<h2><i class="bi bi-file-earmark-text"></i> Inspection Templates</h2>
</div>
<div class="col-md-6 text-end">
{% if current_user.role in ['admin', 'director'] %}
<a href="{{ url_for('templates.create_template') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Create Template
</a>
{% endif %}
</div>
</div>
<div class="row">
{% for template in templates %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 {% if not template.active %}opacity-75 border-secondary{% endif %}">
<div class="card-body">
<h5 class="card-title d-flex align-items-start gap-2">
<a href="{{ url_for('templates.view_template', template_id=template.id) }}" class="text-decoration-none flex-grow-1">
{{ template.name }}
</a>
{% if template.active %}
<span class="badge bg-success flex-shrink-0">Active</span>
{% else %}
<span class="badge bg-secondary flex-shrink-0">Inactive</span>
{% endif %}
</h5>
<p class="card-text text-muted small">{{ template.description or 'No description' }}</p>
<div class="mt-3">
<span class="badge bg-info">{{ template.frequency|title }}</span>
<small class="text-muted ms-2">
<i class="bi bi-check2-square"></i> {{ template.checklist_items.count() }} items
</small>
</div>
</div>
<div class="card-footer bg-transparent d-flex gap-2 align-items-center flex-wrap">
<a href="{{ url_for('templates.view_template', template_id=template.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> View
</a>
{% if current_user.role in ['admin', 'director'] %}
<!-- Rename -->
<button type="button"
class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#renameModal"
data-template-id="{{ template.id }}"
data-template-name="{{ template.name }}"
data-template-description="{{ template.description or '' }}"
data-template-frequency="{{ template.frequency or 'daily' }}"
title="Edit template details">
<i class="bi bi-pencil"></i> Edit
</button>
<!-- Duplicate -->
<form method="POST"
action="{{ url_for('templates.duplicate_template', template_id=template.id) }}"
class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-secondary"
title="Duplicate template">
<i class="bi bi-copy"></i> Duplicate
</button>
</form>
<!-- Toggle active/inactive -->
<form method="POST"
action="{{ url_for('templates.toggle_active', template_id=template.id) }}"
class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if template.active %}
<button type="submit" class="btn btn-sm btn-outline-warning"
title="Deactivate template">
<i class="bi bi-pause-circle"></i> Deactivate
</button>
{% else %}
<button type="submit" class="btn btn-sm btn-outline-success"
title="Activate template">
<i class="bi bi-play-circle"></i> Activate
</button>
{% endif %}
</form>
<!-- Delete -->
<button type="button"
class="btn btn-sm btn-outline-danger ms-auto"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-template-id="{{ template.id }}"
data-template-name="{{ template.name }}"
data-inspection-count="{{ template.inspections.count() }}"
title="Delete template">
<i class="bi bi-trash"></i> Delete
</button>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> No templates created yet.
</div>
</div>
{% endfor %}
</div>
{% if current_user.role in ['admin', 'director'] %}
<!-- Edit Template Modal -->
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="renameModalLabel">
<i class="bi bi-pencil"></i> Edit Template
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form id="renameTemplateForm" method="POST" action="">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="renameInput" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
<input type="text" id="renameInput" name="name"
class="form-control" maxlength="255"
placeholder="Enter template name" required>
</div>
<div class="mb-3">
<label for="editDescription" class="form-label fw-semibold">Description</label>
<textarea id="editDescription" name="description"
class="form-control" rows="3"
placeholder="Optional description"></textarea>
</div>
<div class="mb-1">
<label for="editFrequency" class="form-label fw-semibold">Frequency</label>
<select id="editFrequency" name="frequency" class="form-select">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="quarterly">Quarterly</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Cancel
</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteModalLabel">
<i class="bi bi-exclamation-triangle-fill"></i> Confirm Deletion
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="mb-1">You are about to permanently delete:</p>
<p class="fw-bold fs-5 mb-3" id="modalTemplateName"></p>
<!-- Shown when template has inspections -->
<div id="modalWarningBlock" class="alert alert-danger d-none mb-0">
<i class="bi bi-x-circle-fill"></i>
<strong>Cannot delete this template.</strong>
It has existing inspection records linked to it.
Remove all associated inspections first.
</div>
<!-- Shown when safe to delete -->
<div id="modalConfirmBlock">
<p class="text-muted mb-0">
This action is <strong>irreversible</strong>.
All checklist items and form schema for this template will be permanently removed.
</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Cancel
</button>
<form id="deleteTemplateForm" method="POST" action="" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" id="confirmDeleteBtn" class="btn btn-danger">
<i class="bi bi-trash-fill"></i> Delete Permanently
</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
{% if current_user.role in ['admin', 'director'] %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Edit template modal
const renameModal = document.getElementById('renameModal');
renameModal.addEventListener('show.bs.modal', function (event) {
const btn = event.relatedTarget;
const templateId = btn.getAttribute('data-template-id');
const templateName = btn.getAttribute('data-template-name');
const templateDesc = btn.getAttribute('data-template-description');
const templateFreq = btn.getAttribute('data-template-frequency');
document.getElementById('renameInput').value = templateName;
document.getElementById('editDescription').value = templateDesc;
document.getElementById('editFrequency').value = templateFreq;
document.getElementById('renameTemplateForm').action =
'/templates/' + templateId + '/rename';
renameModal.addEventListener('shown.bs.modal', function focusInput() {
const input = document.getElementById('renameInput');
input.select();
renameModal.removeEventListener('shown.bs.modal', focusInput);
});
});
const deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function (event) {
const btn = event.relatedTarget;
const templateId = btn.getAttribute('data-template-id');
const templateName = btn.getAttribute('data-template-name');
const inspectionCount = parseInt(btn.getAttribute('data-inspection-count'), 10);
document.getElementById('modalTemplateName').textContent = templateName;
document.getElementById('deleteTemplateForm').action =
'/templates/' + templateId + '/delete';
const warningBlock = document.getElementById('modalWarningBlock');
const confirmBlock = document.getElementById('modalConfirmBlock');
const confirmBtn = document.getElementById('confirmDeleteBtn');
if (inspectionCount > 0) {
warningBlock.classList.remove('d-none');
confirmBlock.classList.add('d-none');
confirmBtn.disabled = true;
} else {
warningBlock.classList.add('d-none');
confirmBlock.classList.remove('d-none');
confirmBtn.disabled = false;
}
});
});
</script>
{% endif %}
{% endblock %}
+179
View File
@@ -0,0 +1,179 @@
{% extends "base.html" %}
{% block title %}{{ template.name }}{% endblock %}
{% block extra_css %}
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
body { font-family: 'DM Sans', sans-serif; }
.tpl-header {
display: flex; align-items: flex-start;
justify-content: space-between; gap: 1rem;
margin-bottom: 1.5rem;
}
.tpl-title { font-size: 1.45rem; font-weight: 700; color: #0f172a; margin: 0; }
.tpl-desc { color: #64748b; font-size: .9rem; margin-top: .25rem; }
/* meta card */
.meta-card {
background: #fff; border: 1px solid #e2e8f0;
border-radius: 10px; padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
display: flex; gap: 2rem; flex-wrap: wrap;
align-items: center;
}
.meta-item { display: flex; align-items: center; gap: .45rem; font-size: .85rem; color: #374151; }
.meta-item i { color: #2563eb; font-size: 1rem; }
.meta-item strong { color: #0f172a; }
/* form grid preview */
.grid-preview-wrap {
background: #fff; border: 1px solid #e2e8f0;
border-radius: 10px; padding: 1.5rem;
overflow-x: auto;
}
.grid-preview-title {
font-size: .75rem; font-weight: 700; letter-spacing: .08em;
text-transform: uppercase; color: #64748b;
margin-bottom: 1rem;
}
/* mirrors editor constants */
.form-grid {
display: grid;
grid-template-columns: repeat(12, 72px);
grid-auto-rows: 52px;
gap: 8px;
width: calc(12 * 72px + 11 * 8px);
}
.fg-cell {
overflow: hidden; display: flex; flex-direction: column;
background: #f8fafc; border: 1px solid #e2e8f0;
border-radius: 8px; padding: .4rem .6rem;
}
.fg-cell .fl {
font-size: .72rem; font-weight: 600; color: #374151;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-bottom: .15rem;
}
.fg-cell .ft {
font-size: .62rem; font-family: monospace;
color: #2563eb; background: #eff6ff;
padding: .05rem .3rem; border-radius: 10px;
display: inline-block; width: fit-content;
text-transform: uppercase; letter-spacing: .04em;
}
.req-dot { color: #dc2626; }
.fg-section {
border-top: 2px solid #e2e8f0; padding-top: .35rem;
display: flex; align-items: center; height: 100%;
}
.fg-section span { font-weight: 700; font-size: .82rem; color: #374151; }
/* empty state */
.empty-state {
text-align: center; padding: 2.5rem 1rem; color: #94a3b8;
}
.empty-state i { font-size: 2rem; display: block; margin-bottom: .6rem; opacity: .35; }
.empty-state p { font-size: .85rem; margin: 0; }
</style>
{% endblock %}
{% block content %}
<div class="tpl-header">
<div>
<h2 class="tpl-title"><i class="bi bi-file-earmark-text text-primary"></i> {{ template.name }}</h2>
{% if template.description %}
<p class="tpl-desc">{{ template.description }}</p>
{% endif %}
</div>
<div class="d-flex gap-2 flex-shrink-0 mt-1">
{% if current_user.role in ['admin', 'director'] %}
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}" class="btn btn-primary btn-sm">
<i class="bi bi-pencil-square"></i> Edit Form
</a>
<form method="POST" action="{{ url_for('templates.delete_template', template_id=template.id) }}"
class="d-inline" onsubmit="return confirm('Delete this template? This cannot be undone.');">
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="bi bi-trash"></i> Delete
</button>
</form>
{% endif %}
<a href="{{ url_for('templates.form_preview', template_id=template.id) }}"
target="_blank" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-eye"></i> Preview
</a>
<a href="{{ url_for('templates.index') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> Back
</a>
</div>
</div>
<!-- Meta strip -->
<div class="meta-card">
<div class="meta-item">
<i class="bi bi-arrow-repeat"></i>
<span>Frequency: <strong>{{ template.frequency|title }}</strong></span>
</div>
<div class="meta-item">
<i class="bi bi-layout-text-sidebar-reverse"></i>
<span>Fields: <strong>{{ form_fields|length }}</strong></span>
</div>
<div class="meta-item">
<i class="bi bi-clipboard-check"></i>
<span>Inspections: <strong>{{ template.inspections.count() }}</strong></span>
</div>
{% if template.created_at %}
<div class="meta-item">
<i class="bi bi-calendar3"></i>
<span>Created: <strong>{{ template.created_at.strftime('%Y-%m-%d') }}</strong></span>
</div>
{% endif %}
</div>
<!-- Form layout preview -->
<div class="grid-preview-wrap">
<div class="grid-preview-title"><i class="bi bi-grid-3x3-gap"></i> Form Layout</div>
{% if form_fields %}
<div class="form-grid">
{% for f in form_fields %}
<div class="fg-cell"
style="grid-column: {{ f.col }} / span {{ f.colSpan }};
grid-row: {{ f.row }} / span {{ f.rowSpan }};">
{% if f.type == 'section' %}
<div class="fg-section"><span>{{ f.label }}</span></div>
{% elif f.type == 'table' %}
<div class="fl"><i class="bi bi-table" style="color:#2563eb;margin-right:.2rem;"></i>{{ f.label }}</div>
<span class="ft">table · {{ f.col_headers|length if f.col_headers else 3 }} cols × {{ f.table_rows or 3 }} rows</span>
{% elif f.type == 'label' %}
<div class="fl" style="font-size:.7rem;color:#374151;overflow:hidden;line-height:1.3;white-space:nowrap;text-overflow:ellipsis;">{{ f.text_content or 'Label text' }}</div>
<span class="ft">label</span>
{% elif f.type.startswith('button_') %}
<div class="fl"><i class="bi bi-{% if f.type == 'button_submit' %}send-fill{% elif f.type == 'button_print' %}printer-fill{% else %}envelope-fill{% endif %}" style="color:#2563eb;margin-right:.2rem;"></i>{{ f.btn_label or f.type.replace('button_','') | title }}</div>
<span class="ft">{{ f.type.replace('_',' ') }}</span>
{% else %}
<div class="fl">
{{ f.label }}{% if f.required %}<span class="req-dot"> *</span>{% endif %}
</div>
<span class="ft">{{ f.type.replace('_', ' ') }}</span>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<i class="bi bi-layout-text-sidebar-reverse"></i>
<p>No fields defined yet.</p>
{% if current_user.role in ['admin', 'director'] %}
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}"
class="btn btn-primary btn-sm mt-3">
<i class="bi bi-plus-circle"></i> Open Form Editor
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}