Phase 2: implement template form editor

This commit is contained in:
2026-02-20 12:57:44 -05:00
parent 225ec9000a
commit b62df442b5
8 changed files with 2419 additions and 284 deletions
+171 -120
View File
@@ -2,127 +2,178 @@
{% 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="row mb-4">
<div class="col-md-8">
<h2><i class="bi bi-file-earmark-text"></i> {{ template.name }}</h2>
<p class="text-muted">{{ template.description or 'No description provided' }}</p>
</div>
<div class="col-md-4 text-end">
{% if current_user.role in ['admin', 'supervisor'] %}
<a href="{{ url_for('templates.edit_template', template_id=template.id) }}" class="btn btn-primary">
<i class="bi bi-pencil"></i> Edit Template
</a>
<form method="POST" action="{{ url_for('templates.delete_template', template_id=template.id) }}" class="d-inline" onsubmit="return confirm('Delete this template? This action cannot be undone.');">
<button type="submit" class="btn btn-outline-danger">
<i class="bi bi-trash"></i> Delete
</button>
</form>
{% endif %}
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0">Template Details</h5>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tr>
<th width="40%">Frequency:</th>
<td>
<span class="badge bg-info">{{ template.frequency|title }}</span>
</td>
</tr>
<tr>
<th>Total Items:</th>
<td>{{ template.checklist_items.count() }}</td>
</tr>
<tr>
<th>Created:</th>
<td>{{ template.created_at.strftime('%Y-%m-%d %H:%M') if template.created_at else 'N/A' }}</td>
</tr>
<tr>
<th>Total Inspections:</th>
<td>{{ template.inspections.count() }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0">Statistics</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<h3 class="text-primary">{{ items_by_category|length }}</h3>
<small class="text-muted">Categories</small>
</div>
<div class="col-6">
<h3 class="text-success">{{ template.checklist_items.count() }}</h3>
<small class="text-muted">Checklist Items</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-check2-square"></i> Checklist Preview</h5>
</div>
<div class="card-body">
{% if items_by_category %}
{% for category, items in items_by_category.items() %}
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-folder"></i> {{ category }}
<span class="badge bg-secondary">{{ items|length }} items</span>
</h5>
<div class="list-group">
{% for item in items %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="mb-2">
<span class="badge bg-info me-2">{{ item.scoring_type.replace('_', ' ')|title }}</span>
{% if item.requires_photo %}
<span class="badge bg-warning text-dark">
<i class="bi bi-camera"></i> Photo Required
</span>
{% endif %}
<span class="badge bg-secondary">Weight: {{ item.weight }}</span>
</div>
<p class="mb-0">{{ item.item_description }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i> No checklist items defined for this template yet.
{% if current_user.role in ['admin', 'supervisor'] %}
<a href="{{ url_for('templates.edit_template', template_id=template.id) }}" class="alert-link">Click here to add items.</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="mt-3">
<a href="{{ url_for('templates.index') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to Templates
<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', 'supervisor'] %}
<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', 'supervisor'] %}
<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 %}