diff --git a/app/add_form_schema.py b/app/add_form_schema.py new file mode 100644 index 0000000..fbac276 --- /dev/null +++ b/app/add_form_schema.py @@ -0,0 +1,40 @@ +"""Add form_schema column to inspection_templates + +Run this once on your server: + python add_form_schema.py + +Or via Flask-Migrate: + flask db migrate -m "add form_schema to inspection_templates" + flask db upgrade +""" + +# If you prefer to run this as a standalone script: +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app import create_app, db +from sqlalchemy import text + +app = create_app() + +with app.app_context(): + with db.engine.connect() as conn: + # Check if column already exists + result = conn.execute(text(""" + SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'inspection_templates' + AND column_name = 'form_schema' + """)) + exists = result.scalar() + + if not exists: + conn.execute(text(""" + ALTER TABLE inspection_templates + ADD COLUMN form_schema JSON NULL COMMENT 'JSON schema for the dynamic form builder' + """)) + conn.commit() + print("✓ Column 'form_schema' added to inspection_templates.") + else: + print("✓ Column 'form_schema' already exists — no changes made.") \ No newline at end of file diff --git a/app/models/inspection.py b/app/models/inspection.py index 13478e3..a8c9bb9 100644 --- a/app/models/inspection.py +++ b/app/models/inspection.py @@ -1,5 +1,7 @@ from app import db from datetime import datetime +import json + class InspectionTemplate(db.Model): __tablename__ = 'inspection_templates' @@ -11,13 +13,28 @@ class InspectionTemplate(db.Model): created_by = db.Column(db.Integer, db.ForeignKey('users.id')) created_at = db.Column(db.DateTime, default=datetime.utcnow) + # Form builder schema — stores the full dynamic field layout as JSON + form_schema = db.Column(db.JSON, nullable=True) + # Relationships checklist_items = db.relationship('ChecklistItem', backref='template', lazy='dynamic', cascade='all, delete-orphan') inspections = db.relationship('Inspection', backref='template', lazy='dynamic') + def get_form_schema(self): + """Return the form schema as a Python list, or empty list if not set.""" + if self.form_schema is None: + return [] + if isinstance(self.form_schema, str): + try: + return json.loads(self.form_schema) + except (json.JSONDecodeError, TypeError): + return [] + return self.form_schema + def __repr__(self): return f'' + class ChecklistItem(db.Model): __tablename__ = 'checklist_items' @@ -36,6 +53,7 @@ class ChecklistItem(db.Model): def __repr__(self): return f'' + class Inspection(db.Model): __tablename__ = 'inspections' @@ -57,6 +75,7 @@ class Inspection(db.Model): def __repr__(self): return f'' + class InspectionResult(db.Model): __tablename__ = 'inspection_results' @@ -69,4 +88,4 @@ class InspectionResult(db.Model): photo_path = db.Column(db.String(255)) def __repr__(self): - return f'' + return f'' \ No newline at end of file diff --git a/app/routes/templates.py b/app/routes/templates.py index af8197b..cc17ca7 100644 --- a/app/routes/templates.py +++ b/app/routes/templates.py @@ -1,24 +1,32 @@ from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask_login import login_required, current_user +from flask_wtf.csrf import generate_csrf from app import db from app.models.inspection import InspectionTemplate, ChecklistItem from app.utils.forms import InspectionTemplateForm, ChecklistItemForm from app.utils.decorators import supervisor_required +import json bp = Blueprint('templates', __name__, url_prefix='/templates') + +# --------------------------------------------------------------------------- +# Template CRUD +# --------------------------------------------------------------------------- + @bp.route('/') @login_required def index(): templates = InspectionTemplate.query.order_by(InspectionTemplate.name).all() return render_template('templates/list.html', templates=templates) + @bp.route('/new', methods=['GET', 'POST']) @login_required @supervisor_required def create_template(): form = InspectionTemplateForm() - + if form.validate_on_submit(): template = InspectionTemplate( name=form.name.data, @@ -26,30 +34,26 @@ def create_template(): frequency=form.frequency.data, created_by=current_user.id ) - db.session.add(template) db.session.commit() - + flash(f'Template "{template.name}" created successfully.', 'success') - return redirect(url_for('templates.edit_template', template_id=template.id)) - + return redirect(url_for('templates.form_editor', template_id=template.id)) + return render_template('templates/form.html', form=form, title='Create Inspection Template') + @bp.route('/') @login_required def view_template(template_id): template = InspectionTemplate.query.get_or_404(template_id) - checklist_items = template.checklist_items.order_by(ChecklistItem.display_order, ChecklistItem.category).all() - - # Group items by category - items_by_category = {} - for item in checklist_items: - category = item.category or 'General' - if category not in items_by_category: - items_by_category[category] = [] - items_by_category[category].append(item) - - return render_template('templates/view.html', template=template, items_by_category=items_by_category) + form_fields = template.get_form_schema() + return render_template( + 'templates/view.html', + template=template, + form_fields=form_fields + ) + @bp.route('//edit', methods=['GET', 'POST']) @login_required @@ -57,39 +61,204 @@ def view_template(template_id): def edit_template(template_id): template = InspectionTemplate.query.get_or_404(template_id) form = InspectionTemplateForm(obj=template) - + if form.validate_on_submit(): template.name = form.name.data template.description = form.description.data template.frequency = form.frequency.data - db.session.commit() flash(f'Template "{template.name}" updated successfully.', 'success') return redirect(url_for('templates.view_template', template_id=template.id)) - - checklist_items = template.checklist_items.order_by(ChecklistItem.display_order, ChecklistItem.category).all() - - return render_template('templates/edit.html', form=form, template=template, checklist_items=checklist_items) + + form_fields = template.get_form_schema() + return render_template( + 'templates/edit.html', + form=form, + template=template, + form_fields=form_fields + ) + + +@bp.route('//rename', methods=['POST']) +@login_required +@supervisor_required +def rename_template(template_id): + template = InspectionTemplate.query.get_or_404(template_id) + + new_name = request.form.get('name', '').strip() + if not new_name: + flash('Template name cannot be empty.', 'danger') + return redirect(url_for('templates.index')) + if len(new_name) > 255: + flash('Template name is too long (max 255 characters).', 'danger') + return redirect(url_for('templates.index')) + + valid_frequencies = {'daily', 'weekly', 'monthly', 'quarterly'} + new_frequency = request.form.get('frequency', '').strip() + if new_frequency not in valid_frequencies: + flash('Invalid frequency value.', 'danger') + return redirect(url_for('templates.index')) + + template.name = new_name + template.description = request.form.get('description', '').strip() or None + template.frequency = new_frequency + + db.session.commit() + flash(f'Template "{template.name}" updated successfully.', 'success') + return redirect(url_for('templates.index')) + @bp.route('//delete', methods=['POST']) @login_required @supervisor_required def delete_template(template_id): template = InspectionTemplate.query.get_or_404(template_id) - - # Check if template has inspections + if template.inspections.count() > 0: flash('Cannot delete template with existing inspections.', 'danger') - return redirect(url_for('templates.view_template', template_id=template.id)) - + return redirect(url_for('templates.index')) + template_name = template.name db.session.delete(template) db.session.commit() - + flash(f'Template "{template_name}" deleted successfully.', 'success') return redirect(url_for('templates.index')) -# Checklist Item Management + +@bp.route('//duplicate', methods=['POST']) +@login_required +@supervisor_required +def duplicate_template(template_id): + src = InspectionTemplate.query.get_or_404(template_id) + + # Duplicate the template header + new_tpl = InspectionTemplate( + name=f'{src.name} (Copy)', + description=src.description, + frequency=src.frequency, + created_by=current_user.id + ) + db.session.add(new_tpl) + db.session.flush() # get new_tpl.id before committing + + # Duplicate all checklist items + for item in src.checklist_items.order_by(ChecklistItem.display_order).all(): + new_item = ChecklistItem( + template_id=new_tpl.id, + category=item.category, + item_description=item.item_description, + scoring_type=item.scoring_type, + weight=item.weight, + requires_photo=item.requires_photo, + display_order=item.display_order + ) + db.session.add(new_item) + + # Duplicate form schema if present + if src.form_schema: + new_tpl.form_schema = src.form_schema + + db.session.commit() + + flash(f'Template "{src.name}" duplicated successfully.', 'success') + return redirect(url_for('templates.index')) + + +# --------------------------------------------------------------------------- +# Form Editor +# --------------------------------------------------------------------------- + +@bp.route('//form-editor') +@login_required +@supervisor_required +def form_editor(template_id): + template = InspectionTemplate.query.get_or_404(template_id) + form_schema = template.get_form_schema() + return render_template( + 'templates/form_editor.html', + template=template, + form_schema_json=json.dumps(form_schema), + csrf_token=generate_csrf() + ) + + +@bp.route('//form-editor/save', methods=['POST']) +@login_required +@supervisor_required +def save_form_schema(template_id): + """AJAX endpoint — receives the full form schema as JSON and persists it.""" + template = InspectionTemplate.query.get_or_404(template_id) + + data = request.get_json(silent=True) + if data is None: + return jsonify({'success': False, 'error': 'Invalid JSON payload'}), 400 + + fields = data.get('fields', []) + + # Basic sanitisation — ensure each field has the minimum required keys + sanitised = [] + for field in fields: + if not isinstance(field, dict): + continue + if not field.get('id') or not field.get('type'): + continue + ftype = str(field.get('type', 'text')) + entry = { + 'id': str(field.get('id', '')), + 'type': ftype, + 'label': str(field.get('label', 'Untitled'))[:255], + 'placeholder': str(field.get('placeholder', ''))[:255], + 'required': bool(field.get('required', False)), + 'options': field.get('options', []) if ftype in ('radio', 'checkbox_group', 'select') else [], + 'help_text': str(field.get('help_text', ''))[:500], + 'order': int(field.get('order', 0)), + # Grid position & size + 'col': max(1, min(12, int(field.get('col', 1)))), + 'row': max(1, min(9999, int(field.get('row', 1)))), + 'colSpan': max(1, min(12, int(field.get('colSpan', 6)))), + 'rowSpan': max(1, min(20, int(field.get('rowSpan', 2)))), + } + # Table-specific fields + if ftype == 'table': + raw_hdrs = field.get('col_headers', ['Column 1', 'Column 2', 'Column 3']) + col_headers = [str(h)[:100] for h in raw_hdrs if isinstance(h, str)][:20] or ['Column 1'] + entry['col_headers'] = col_headers + entry['table_cols'] = len(col_headers) + entry['table_rows'] = max(1, min(30, int(field.get('table_rows', 3)))) + # Label-specific fields + if ftype == 'label': + entry['text_content'] = str(field.get('text_content', 'Label text'))[:2000] + entry['font_size'] = field.get('font_size', 'normal') if field.get('font_size') in ('small','normal','large','x-large') else 'normal' + entry['font_weight'] = 'bold' if field.get('font_weight') == 'bold' else 'normal' + # Button-specific fields + if ftype in ('button_submit', 'button_print', 'button_email'): + defaults = {'button_submit':'Submit Form','button_print':'Print Form','button_email':'Email Form'} + entry['btn_label'] = str(field.get('btn_label', defaults[ftype]))[:100] + sanitised.append(entry) + + template.form_schema = sanitised + db.session.commit() + + return jsonify({'success': True, 'field_count': len(sanitised)}) + + +@bp.route('//form-editor/preview') +@login_required +def form_preview(template_id): + """Renders a read-only preview of the dynamic form.""" + template = InspectionTemplate.query.get_or_404(template_id) + form_fields = template.get_form_schema() + return render_template( + 'templates/form_preview.html', + template=template, + form_fields=form_fields + ) + + +# --------------------------------------------------------------------------- +# Checklist Item Management (legacy, kept for backwards compatibility) +# --------------------------------------------------------------------------- @bp.route('//items/new', methods=['GET', 'POST']) @login_required @@ -97,12 +266,11 @@ def delete_template(template_id): def create_checklist_item(template_id): template = InspectionTemplate.query.get_or_404(template_id) form = ChecklistItemForm() - + if form.validate_on_submit(): - # Get the highest display order max_order = db.session.query(db.func.max(ChecklistItem.display_order))\ .filter_by(template_id=template.id).scalar() or 0 - + item = ChecklistItem( template_id=template.id, category=form.category.data, @@ -112,14 +280,19 @@ def create_checklist_item(template_id): requires_photo=form.requires_photo.data, display_order=max_order + 1 ) - db.session.add(item) db.session.commit() - + flash('Checklist item added successfully.', 'success') return redirect(url_for('templates.edit_template', template_id=template.id)) - - return render_template('templates/item_form.html', form=form, template=template, title='Add Checklist Item') + + return render_template( + 'templates/item_form.html', + form=form, + template=template, + title='Add Checklist Item' + ) + @bp.route('/items//edit', methods=['GET', 'POST']) @login_required @@ -127,19 +300,25 @@ def create_checklist_item(template_id): def edit_checklist_item(item_id): item = ChecklistItem.query.get_or_404(item_id) form = ChecklistItemForm(obj=item) - + if form.validate_on_submit(): item.category = form.category.data item.item_description = form.item_description.data item.scoring_type = form.scoring_type.data item.weight = form.weight.data item.requires_photo = form.requires_photo.data - db.session.commit() flash('Checklist item updated successfully.', 'success') return redirect(url_for('templates.edit_template', template_id=item.template_id)) - - return render_template('templates/item_form.html', form=form, item=item, template=item.template, title='Edit Checklist Item') + + return render_template( + 'templates/item_form.html', + form=form, + item=item, + template=item.template, + title='Edit Checklist Item' + ) + @bp.route('/items//delete', methods=['POST']) @login_required @@ -147,24 +326,23 @@ def edit_checklist_item(item_id): def delete_checklist_item(item_id): item = ChecklistItem.query.get_or_404(item_id) template_id = item.template_id - db.session.delete(item) db.session.commit() - flash('Checklist item deleted successfully.', 'success') return redirect(url_for('templates.edit_template', template_id=template_id)) + @bp.route('//items/reorder', methods=['POST']) @login_required @supervisor_required def reorder_items(template_id): template = InspectionTemplate.query.get_or_404(template_id) item_order = request.json.get('item_order', []) - + for index, item_id in enumerate(item_order): item = ChecklistItem.query.get(item_id) if item and item.template_id == template.id: item.display_order = index - + db.session.commit() return jsonify({'success': True}) \ No newline at end of file diff --git a/app/templates/templates/edit.html b/app/templates/templates/edit.html index 137a8df..386b1d0 100644 --- a/app/templates/templates/edit.html +++ b/app/templates/templates/edit.html @@ -1,132 +1,246 @@ {% extends "base.html" %} -{% block title %}Edit Template - {{ template.name }}{% endblock %} +{% block title %}Edit Template — {{ template.name }}{% endblock %} {% block extra_css %} + {% endblock %} {% block content %} -
-
-

{{ template.name }}

+ + +
+ + +
+
+ Template Settings
- + + +
+
+
Form Layout
+
+ {{ form_fields|length }} field{{ 's' if form_fields|length != 1 else '' }} + + Preview + + Edit Form + +
-
-
-
-
-
-
Template Settings
-
-
-
- {{ form.hidden_tag() }} - -
- {{ form.name.label(class="form-label") }} - {{ form.name(class="form-control") }} -
- -
- {{ form.description.label(class="form-label") }} - {{ form.description(class="form-control", rows=3) }} -
- -
- {{ form.frequency.label(class="form-label") }} - {{ form.frequency(class="form-select") }} -
- - -
+
+ {% if form_fields %} +
+ {% for f in form_fields %} +
+ {% if f.type == 'section' %} +
{{ f.label }}
+ {% elif f.type == 'table' %} +
{{ f.label }}
+ table · {{ f.col_headers|length if f.col_headers else 3 }} cols × {{ f.table_rows or 3 }} rows + {% elif f.type == 'label' %} +
{{ f.text_content or 'Label text' }}
+ label + {% elif f.type.startswith('button_') %} +
{{ f.btn_label or f.type.replace('button_','') | title }}
+ {{ f.type.replace('_',' ') }} + {% else %} +
+ {{ f.label }}{% if f.required %} *{% endif %}
+ {{ f.type.replace('_', ' ') }} + {% endif %}
-
- -
-
-
-
Checklist Items ({{ checklist_items|length }})
-
-
- {% if checklist_items %} -
- {% for item in checklist_items %} -
-
-
-
- - {{ item.category }} - {{ item.scoring_type.replace('_', ' ')|title }} - {% if item.requires_photo %} - - {% endif %} -
-

{{ item.item_description }}

- Weight: {{ item.weight }} -
-
- - - -
- -
-
-
-
- {% endfor %} -
- {% else %} -
- No checklist items yet. Click "Add Item" to get started. -
- {% endif %} -
-
-
-
-{% endblock %} + {% endfor %} +
-{% block extra_js %} - - + {% else %} +
+ +

No fields yet. Open the Form Editor to start building your inspection form.

+ + Open Form Editor + +
+ {% endif %} +
+
+ +
{% endblock %} \ No newline at end of file diff --git a/app/templates/templates/form_editor.html b/app/templates/templates/form_editor.html new file mode 100644 index 0000000..d653ce9 --- /dev/null +++ b/app/templates/templates/form_editor.html @@ -0,0 +1,1159 @@ +{% extends "base.html" %} + +{% block title %}Form Editor — {{ template.name }}{% endblock %} + +{% block extra_css %} + + + +{% endblock %} + +{% block content %} +
+ + +
+ + Back + +
+
{{ template.name }}
+
Form Editor · {{ template.frequency|title }}
+
+
+ All changes saved + Preview + +
+ + +
+
Basic
+ {% for t, ic, lb in [ + ('text', 'bi-input-cursor-text', 'Text Input'), + ('textarea', 'bi-text-left', 'Text Area'), + ('number', 'bi-hash', 'Number'), + ('date', 'bi-calendar3', 'Date'), + ('email', 'bi-envelope', 'Email'), + ] %} +
+ {{ lb }} +
+ {% endfor %} +
Choice
+ {% for t, ic, lb in [ + ('checkbox', 'bi-check-square', 'Checkbox'), + ('checkbox_group', 'bi-ui-checks', 'Checkbox Group'), + ('radio', 'bi-ui-radios', 'Radio Group'), + ('select', 'bi-menu-button-wide', 'Dropdown'), + ] %} +
+ {{ lb }} +
+ {% endfor %} +
Media & Other
+ {% for t, ic, lb in [ + ('image', 'bi-image', 'Image Upload'), + ('signature', 'bi-pen', 'Signature'), + ('rating', 'bi-star', 'Rating (1–5)'), + ('section', 'bi-dash-lg', 'Section Header'), + ('table', 'bi-table', 'Table'), + ] %} +
+ {{ lb }} +
+ {% endfor %} +
Text & Actions
+ {% for t, ic, lb in [ + ('label', 'bi-type', 'Label / Text'), + ('button_submit', 'bi-send-fill', 'Submit Button'), + ('button_print', 'bi-printer-fill', 'Print Button'), + ('button_email', 'bi-envelope-fill', 'Email Button'), + ] %} +
+ {{ lb }} +
+ {% endfor %} +
+ + +
+
+
+ +

Drag fields onto the canvas

+

Fields snap to a 12-column grid.

+
+
+
+
+ + +
+
+ +

Select a field to edit its properties.

+
+ +
+ +
+ +
+{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/templates/form_preview.html b/app/templates/templates/form_preview.html new file mode 100644 index 0000000..a1a7a9a --- /dev/null +++ b/app/templates/templates/form_preview.html @@ -0,0 +1,382 @@ +{% extends "base.html" %} + +{% block title %}Preview — {{ template.name }}{% endblock %} + +{% block extra_css %} + + +{% endblock %} + +{% block content %} +
+
+
+

{{ template.name }}

+
{{ template.description or 'Inspection form preview' }}
+
+ {{ template.frequency|title }} +
+ +
+
+ + This is a read-only preview. Layout matches the grid editor exactly. +
+ + {% if form_fields %} +
+ {% for field in form_fields %} +
+ + {% if field.type == 'section' %} +
{{ field.label }}
+ + {% elif field.type == 'text' %} + + + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'textarea' %} + + + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'number' %} + + + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'date' %} + + + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'email' %} + + + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'checkbox' %} +
+ + +
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'checkbox_group' %} + + {% for opt in field.options %} +
+ + +
+ {% endfor %} + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'radio' %} + + {% for opt in field.options %} +
+ + +
+ {% endfor %} + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'select' %} + + + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'image' %} + +
+ + Upload photo +
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'signature' %} + +
Sign here…
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'rating' %} + +
+ {% for i in range(1, 6) %} + + {% endfor %} +
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% elif field.type == 'label' %} + {% set fs_map = {'small':'0.78rem','normal':'0.9rem','large':'1.05rem','x-large':'1.25rem'} %} +
{{ field.text_content or 'Label text' }}
+ + {% elif field.type == 'button_submit' %} + + + {% elif field.type == 'button_print' %} + + + {% elif field.type == 'button_email' %} + + + {% elif field.type == 'table' %} + +
+ + + + {% for hdr in (field.col_headers or ['Column 1','Column 2','Column 3']) %} + + {% endfor %} + + + + {% for r in range(field.table_rows or 3) %} + + {% for hdr in (field.col_headers or ['Column 1','Column 2','Column 3']) %} + + {% endfor %} + + {% endfor %} + +
{{ hdr }}
+
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {% endif %} +
+ {% endfor %} +
+ + + + {% else %} +
+ + No fields yet +

+ This template has no form fields. Open the editor to add some. +

+ +
+ {% endif %} +
+
+{% endblock %} + +{% block extra_js %} + + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/templates/list.html b/app/templates/templates/list.html index dbfe933..a85d0e2 100644 --- a/app/templates/templates/list.html +++ b/app/templates/templates/list.html @@ -26,9 +26,7 @@ {{ template.name }} -

{{ template.description or 'No description' }}

-
{{ template.frequency|title }} @@ -36,10 +34,50 @@
-
@@ -51,4 +89,158 @@
{% endfor %}
+ +{% if current_user.role in ['admin', 'supervisor'] %} + + + + + +{% endif %} +{% endblock %} + +{% block extra_js %} +{% if current_user.role in ['admin', 'supervisor'] %} + +{% endif %} {% endblock %} \ No newline at end of file diff --git a/app/templates/templates/view.html b/app/templates/templates/view.html index a504314..73c4004 100644 --- a/app/templates/templates/view.html +++ b/app/templates/templates/view.html @@ -2,127 +2,178 @@ {% block title %}{{ template.name }}{% endblock %} +{% block extra_css %} + + +{% endblock %} + {% block content %} -
-
-

{{ template.name }}

-

{{ template.description or 'No description provided' }}

-
-
- {% if current_user.role in ['admin', 'supervisor'] %} - - Edit Template - -
- -
- {% endif %} -
-
- -
-
-
-
-
Template Details
-
-
- - - - - - - - - - - - - - - - - -
Frequency: - {{ template.frequency|title }} -
Total Items:{{ template.checklist_items.count() }}
Created:{{ template.created_at.strftime('%Y-%m-%d %H:%M') if template.created_at else 'N/A' }}
Total Inspections:{{ template.inspections.count() }}
-
-
-
- -
-
-
-
Statistics
-
-
-
-
-

{{ items_by_category|length }}

- Categories -
-
-

{{ template.checklist_items.count() }}

- Checklist Items -
-
-
-
-
-
- -
-
-
Checklist Preview
-
-
- {% if items_by_category %} - {% for category, items in items_by_category.items() %} -
-
- {{ category }} - {{ items|length }} items -
- -
- {% for item in items %} -
-
-
-
- {{ item.scoring_type.replace('_', ' ')|title }} - {% if item.requires_photo %} - - Photo Required - - {% endif %} - Weight: {{ item.weight }} -
-

{{ item.item_description }}

-
-
-
- {% endfor %} -
-
- {% endfor %} - {% else %} -
- No checklist items defined for this template yet. - {% if current_user.role in ['admin', 'supervisor'] %} - Click here to add items. - {% endif %} -
- {% endif %} -
-
- -
- - Back to Templates + + + +
+
+ + Frequency: {{ template.frequency|title }} +
+
+ + Fields: {{ form_fields|length }} +
+
+ + Inspections: {{ template.inspections.count() }} +
+ {% if template.created_at %} +
+ + Created: {{ template.created_at.strftime('%Y-%m-%d') }} +
+ {% endif %} +
+ + +
+
Form Layout
+ + {% if form_fields %} +
+ {% for f in form_fields %} +
+ {% if f.type == 'section' %} +
{{ f.label }}
+ {% elif f.type == 'table' %} +
{{ f.label }}
+ table · {{ f.col_headers|length if f.col_headers else 3 }} cols × {{ f.table_rows or 3 }} rows + {% elif f.type == 'label' %} +
{{ f.text_content or 'Label text' }}
+ label + {% elif f.type.startswith('button_') %} +
{{ f.btn_label or f.type.replace('button_','') | title }}
+ {{ f.type.replace('_',' ') }} + {% else %} +
+ {{ f.label }}{% if f.required %} *{% endif %} +
+ {{ f.type.replace('_', ' ') }} + {% endif %} +
+ {% endfor %} +
+ + {% else %} +
+ +

No fields defined yet.

+ {% if current_user.role in ['admin', 'supervisor'] %} + + Open Form Editor + + {% endif %} +
+ {% endif %}
{% endblock %} \ No newline at end of file