Phase 2: implement template form editor
This commit is contained in:
+171
-120
@@ -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 %}
|
||||
Reference in New Issue
Block a user