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' %}
+ {{ field.label }}{% if field.required %} * {% endif %}
+ {% 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 %}
+
{# ── Select / Dropdown ── #}
{% elif field.type == 'select' %}
{{ field.label }}{% if field.required %} * {% endif %}
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' %}
+
{{ field.label }}{% if field.required %}* {% endif %}
+ {% 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' %}
{{ field.label }}{% if field.required %} * {% endif %}
diff --git a/app/utils/pdf_export.py b/app/utils/pdf_export.py
index 9d88712..441a371 100644
--- a/app/utils/pdf_export.py
+++ b/app/utils/pdf_export.py
@@ -386,6 +386,8 @@ def _form_fields_section(form_fields, form_data, static_folder):
if ftype == 'checkbox':
# stored as 'true'/'false' by _collect_form_responses
return val not in ('yes', 'true')
+ if ftype == 'pass_fail':
+ return not val
if ftype == 'checkbox_group':
return not val or not isinstance(val, list) or len(val) == 0
if ftype == 'table':
@@ -429,6 +431,14 @@ def _form_fields_section(form_fields, form_data, static_folder):
elif ftype == 'checkbox':
val_p = Paragraph('Yes', STYLES['FieldValue'])
+ elif ftype == 'pass_fail':
+ is_pass = str(val).lower() in ('pass', 'yes', 'ok', 'good', 'acceptable', 'compliant')
+ colour = C_GREEN if is_pass else C_RED
+ val_p = Paragraph(
+ f'{str(val)} ',
+ STYLES['FieldValue'],
+ )
+
elif ftype == 'checkbox_group':
val_p = Paragraph(', '.join(val), STYLES['FieldValue'])