From 33166bbc6b0a45410a74b7698d3c479da7f9fb10 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Tue, 17 Mar 2026 12:09:40 -0400 Subject: [PATCH] March 17 2026: form editor - add pass/fail element --- app/routes/inspections.py | 17 +++++++++++++--- app/routes/templates.py | 2 +- app/templates/inspections/execute.html | 24 +++++++++++++++++++++++ app/templates/inspections/view.html | 12 ++++++++++++ app/templates/templates/form_editor.html | 15 ++++++++++---- app/templates/templates/form_preview.html | 13 ++++++++++++ app/utils/pdf_export.py | 10 ++++++++++ 7 files changed, 85 insertions(+), 8 deletions(-) diff --git a/app/routes/inspections.py b/app/routes/inspections.py index 399526d..6c55bb7 100644 --- a/app/routes/inspections.py +++ b/app/routes/inspections.py @@ -30,7 +30,7 @@ ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'} INPUT_FIELD_TYPES = { 'text', 'textarea', 'number', 'date', 'email', 'checkbox', 'checkbox_group', 'radio', 'select', - 'rating', 'signature', 'image', 'table' + 'rating', 'pass_fail', 'signature', 'image', 'table' } @@ -115,7 +115,7 @@ def _compute_score_from_form(form_fields, responses): Derive an overall score from rating fields and checkbox pass/fail fields. Returns a float 0–100 or None if the form has no scoreable fields. """ - scoreable = [f for f in form_fields if f['type'] in ('rating', 'checkbox', 'radio')] + scoreable = [f for f in form_fields if f['type'] in ('rating', 'checkbox', 'radio', 'pass_fail')] if not scoreable: return None @@ -144,6 +144,13 @@ def _compute_score_from_form(form_fields, responses): if val.lower() in ('pass', 'yes', 'ok', 'good', 'acceptable', 'compliant'): earned += 1 + elif field['type'] == 'pass_fail': + if not val: + continue + total += 1 + if val.lower() in ('pass', 'yes', 'ok', 'good', 'acceptable', 'compliant'): + earned += 1 + return round((earned / total) * 100, 2) if total else None @@ -478,7 +485,7 @@ def view(inspection_id): except (json.JSONDecodeError, TypeError): pass - scoreable_types = ('rating', 'checkbox', 'radio') + scoreable_types = ('rating', 'checkbox', 'radio', 'pass_fail') # Collect all text/textarea fields per row, keyed by (col, fid) # so we can pick the leftmost non-empty value as the item name. @@ -557,6 +564,10 @@ def view(inspection_id): return 100.0 if val == 'true' else 0.0 if ft == 'radio': return None if not val else 100.0 + if ft == 'pass_fail': + if not val: + return None + return 100.0 if val.lower() in ('pass', 'yes', 'ok', 'good', 'acceptable', 'compliant') else 0.0 return None cur_pct = _field_pct(cur_val, ftype) diff --git a/app/routes/templates.py b/app/routes/templates.py index 86917ee..1c079da 100644 --- a/app/routes/templates.py +++ b/app/routes/templates.py @@ -229,7 +229,7 @@ def save_form_schema(template_id): '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 [], + 'options': field.get('options', []) if ftype in ('radio', 'checkbox_group', 'select', 'pass_fail') else [], 'help_text': str(field.get('help_text', ''))[:500], 'order': int(field.get('order', 0)), # Grid position & size diff --git a/app/templates/inspections/execute.html b/app/templates/inspections/execute.html index eef6a9b..b8dcd1b 100644 --- a/app/templates/inspections/execute.html +++ b/app/templates/inspections/execute.html @@ -330,6 +330,30 @@ {% endfor %} {% if field.help_text %}
{{ field.help_text }}
{% endif %} + {# ── Pass / Fail ── #} + {% elif field.type == 'pass_fail' %} + + {% set pf_options = field.options if field.options else ['Pass', 'Fail'] %} +
+ {% for opt in pf_options %} + {% set is_pass = opt.lower() in ('pass','yes','ok','good') %} + + {% endfor %} +
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %} + {# ── Select / Dropdown ── #} {% elif field.type == 'select' %} diff --git a/app/templates/inspections/view.html b/app/templates/inspections/view.html index a228fc3..d843592 100644 --- a/app/templates/inspections/view.html +++ b/app/templates/inspections/view.html @@ -559,6 +559,18 @@ {% endif %} + {% elif ftype == 'pass_fail' %} + {{ field.label }} +
+ {% if val and val.lower() in ('pass','yes','ok','good','acceptable','compliant') %} + {{ val }} + {% elif val %} + {{ val }} + {% else %} + Not answered + {% endif %} +
+ {% elif ftype == 'checkbox' %} {{ field.label }}
diff --git a/app/templates/templates/form_editor.html b/app/templates/templates/form_editor.html index 0c4bdde..49befd9 100644 --- a/app/templates/templates/form_editor.html +++ b/app/templates/templates/form_editor.html @@ -402,6 +402,7 @@ ('checkbox_group', 'bi-ui-checks', 'Checkbox Group'), ('radio', 'bi-ui-radios', 'Radio Group'), ('select', 'bi-menu-button-wide', 'Dropdown'), + ('pass_fail', 'bi-check2-circle', 'Pass / Fail'), ] %}
{{ lb }} @@ -480,7 +481,7 @@ const DEF_SIZE = { number: [3, 2], date: [4, 2], email: [6, 2], checkbox: [4, 2], checkbox_group: [5, 4], radio: [5, 4], - select: [5, 2], image: [6, 4], + select: [5, 2], pass_fail: [4, 2], image: [6, 4], signature: [6, 3], rating: [5, 2], section: [12, 1], table: [12, 5], label: [6, 1], @@ -490,7 +491,7 @@ const DEF_SIZE = { const LABELS = { text:'Text Input', textarea:'Text Area', number:'Number', date:'Date', email:'Email', checkbox:'Checkbox', checkbox_group:'Checkbox Group', - radio:'Radio Group', select:'Dropdown', image:'Image Upload', + radio:'Radio Group', select:'Dropdown', pass_fail:'Pass / Fail', image:'Image Upload', signature:'Signature', rating:'Rating (1–5)', section:'Section Header', table:'Table', label:'Label / Text', button_submit:'Submit Button', button_print:'Print Button', button_email:'Email Button', @@ -586,7 +587,7 @@ function mkField(type, col, row) { placeholder: '', required: false, help_text: '', - options: ['radio','checkbox_group','select'].includes(type) ? ['Option 1','Option 2'] : [], + options: type === 'pass_fail' ? ['Pass','Fail'] : ['radio','checkbox_group','select'].includes(type) ? ['Option 1','Option 2'] : [], table_cols: isTable ? 3 : undefined, table_rows: isTable ? 3 : undefined, col_headers: isTable ? ['Column 1','Column 2','Column 3'] : undefined, @@ -745,6 +746,12 @@ function mkPreview(f) { return `${lbl}
Sign here…
`; case 'rating': return `${lbl}
★★★★★
`; + case 'pass_fail': { + const pfOpts = f.options || ['Pass','Fail']; + return `${lbl}
${pfOpts.map(o => + `${esc(o)}` + ).join('')}
`; + } case 'section': return `
${esc(f.label)} @@ -815,7 +822,7 @@ function renderProperties() { const f = fields.find(f => f.id === selectedId); if (!f) return; empty.style.display = 'none'; content.style.display = ''; - const hasOpts = ['radio','checkbox_group','select'].includes(f.type); + const hasOpts = ['radio','checkbox_group','select','pass_fail'].includes(f.type); const isSec = f.type === 'section'; const isLabel = f.type === 'label'; const isBtn = f.type.startsWith('button_'); diff --git a/app/templates/templates/form_preview.html b/app/templates/templates/form_preview.html index a539161..8bc37df 100644 --- a/app/templates/templates/form_preview.html +++ b/app/templates/templates/form_preview.html @@ -248,6 +248,19 @@ {% endfor %} {% if field.help_text %}
{{ field.help_text }}
{% endif %} + {% elif field.type == 'pass_fail' %} + + {% set pf_options = field.options if field.options else ['Pass', 'Fail'] %} +
+ {% for opt in pf_options %} + {% set is_pass = opt.lower() in ('pass','yes','ok','good') %} + {{ opt }} + {% endfor %} +
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %} + {% elif field.type == 'select' %}