diff --git a/CLAUDE.md b/CLAUDE.md
index e0bb94e..6f55503 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -260,7 +260,11 @@ issues: id, inspection_id (nullable), area_id, facility_id (nullable), severity
| `facility` | The facility's own staff | `facility_handler_name/contact/notes` (free text) | internal **follow-up owner** |
| `vendor` | External contractor | `vendor_name/contact/notes` (Phase 26) | internal **follow-up owner** |
-`assigned_to` (a JQC User) is **always** available: it is the handler for `internal`, and the internal follow-up owner (e.g. the inspector who verifies/updates) for `facility`/`vendor`. Set via the **Update Issue** panel on the issue detail page — the "Handled By" selector reveals the facility or vendor sub-fields via JS. Triage of `handler_type` + facility/vendor detail fields is **admin/director/project_manager only** (same gate as vendor fields); `assigned_to` remains admin/director. Issue list is filterable by `?handler_type=` and shows a Facility/Vendor badge. `Issue.handler_label` gives the display string. Not yet exposed in the mobile API.
+`assigned_to` (a JQC User) is **always** available: it is the handler for `internal`, and the internal follow-up owner (e.g. the inspector who verifies/updates) for `facility`/`vendor`. Settable in **two places**, both with a "Handled By" selector that reveals the facility or vendor sub-fields via JS:
+- **Log New Issue** form (`issues/form.html`) — at creation, for non-customer staff. Customer-created issues stay `internal` (the handler UI is hidden for them, same as `assigned_to`).
+- **Update Issue** panel on the issue detail page (`issues/view.html`) — triage after creation.
+
+Triage of `handler_type` + facility/vendor detail fields on the **update** panel is **admin/director/project_manager only** (same gate as vendor fields); on the **create** form it follows the form's own access (admin/director create for staff). `assigned_to` editing on update remains admin/director. Issue list is filterable by `?handler_type=` and shows a Facility/Vendor badge. `Issue.handler_label` gives the display string. Not yet exposed in the mobile API.
**Photo columns — three distinct fields with different semantics:**
diff --git a/app/routes/issues.py b/app/routes/issues.py
index d993c1a..15f4a73 100644
--- a/app/routes/issues.py
+++ b/app/routes/issues.py
@@ -732,6 +732,20 @@ def create():
reported_at = now_eastern(),
reported_by = current_user.id,
)
+
+ # "Handled By" — staff only; customer-created issues stay internal.
+ if current_user.role != 'customer':
+ handler = form.handler_type.data or 'internal'
+ if handler not in ('internal', 'facility', 'vendor'):
+ handler = 'internal'
+ issue.handler_type = handler
+ issue.facility_handler_name = (form.facility_handler_name.data or '').strip() or None
+ issue.facility_handler_contact = (form.facility_handler_contact.data or '').strip() or None
+ issue.facility_handler_notes = (form.facility_handler_notes.data or '').strip() or None
+ issue.vendor_name = (form.vendor_name.data or '').strip() or None
+ issue.vendor_contact = (form.vendor_contact.data or '').strip() or None
+ issue.vendor_notes = (form.vendor_notes.data or '').strip() or None
+
db.session.add(issue)
db.session.commit()
current_app.logger.info(
diff --git a/app/templates/issues/form.html b/app/templates/issues/form.html
index 817ba11..757dc01 100644
--- a/app/templates/issues/form.html
+++ b/app/templates/issues/form.html
@@ -39,10 +39,55 @@
{% endfor %}
{% if current_user.role != 'customer' %}
+ The facility/vendor does the work; this is our follow-up owner.
+
{% for e in form.assigned_to.errors %}
{{ e }}
{% endfor %}
+
+ {# Facility-staff handler — shown when Handled By = Facility Staff #}
+
+
+ {{ form.facility_handler_name.label(class="form-label small fw-semibold mb-1") }}
+ {{ form.facility_handler_name(class="form-control form-control-sm",
+ placeholder="Facility staff / point of contact") }}
+
+
+ {{ form.facility_handler_contact.label(class="form-label small fw-semibold mb-1") }}
+ {{ form.facility_handler_contact(class="form-control form-control-sm",
+ placeholder="Phone or email") }}
+
+
+ {{ form.facility_handler_notes.label(class="form-label small fw-semibold mb-1") }}
+ {{ form.facility_handler_notes(class="form-control form-control-sm", rows=2,
+ placeholder="What the facility staff are handling…") }}
+
+
+
+ {# External vendor — shown when Handled By = External Vendor #}
+
+
+ {{ form.vendor_name.label(class="form-label small fw-semibold mb-1") }}
+ {{ form.vendor_name(class="form-control form-control-sm",
+ placeholder="Contractor or vendor name") }}
+
+
+ {{ form.vendor_contact.label(class="form-label small fw-semibold mb-1") }}
+ {{ form.vendor_contact(class="form-control form-control-sm",
+ placeholder="Phone or email") }}
+
+
+ {{ form.vendor_notes.label(class="form-label small fw-semibold mb-1") }}
+ {{ form.vendor_notes(class="form-control form-control-sm", rows=2,
+ placeholder="Notes about what the contractor is handling…") }}
+
+
{% endif %}
@@ -109,6 +154,29 @@
} else {
setPlaceholder();
}
+
+ // ── "Handled By" — reveal the facility/vendor sub-block + relabel assignee ──
+ var handlerSelect = document.getElementById('handler_type_select');
+ function syncHandlerUI() {
+ if (!handlerSelect) { return; }
+ var v = handlerSelect.value;
+ var facBlock = document.getElementById('facility_handler_block');
+ var venBlock = document.getElementById('vendor_block');
+ if (facBlock) { facBlock.style.display = (v === 'facility') ? '' : 'none'; }
+ if (venBlock) { venBlock.style.display = (v === 'vendor') ? '' : 'none'; }
+ var label = document.getElementById('assigned_to_label');
+ var help = document.getElementById('assigned_to_help');
+ if (label) {
+ label.textContent = (v === 'facility' || v === 'vendor') ? 'Follow-up Owner' : 'Assign To';
+ }
+ if (help) {
+ help.style.display = (v === 'facility' || v === 'vendor') ? '' : 'none';
+ }
+ }
+ if (handlerSelect) {
+ handlerSelect.addEventListener('change', syncHandlerUI);
+ syncHandlerUI();
+ }
}());
{% endblock %}
diff --git a/app/utils/forms.py b/app/utils/forms.py
index f23ef49..456512a 100644
--- a/app/utils/forms.py
+++ b/app/utils/forms.py
@@ -171,6 +171,18 @@ class IssueForm(FlaskForm):
FileAllowed(['jpg','jpeg','png','gif'], 'Images only.')
])
assigned_to = SelectField('Assign To', coerce=int, validators=[Optional()])
+ # Who handles the issue (phase35) — set at creation by staff
+ handler_type = SelectField('Handled By', choices=[
+ ('internal', 'Our Staff'),
+ ('facility', 'Facility Staff'),
+ ('vendor', 'External Vendor'),
+ ], validators=[Optional()])
+ facility_handler_name = StringField('Facility Contact Name', validators=[Optional(), Length(max=100)])
+ facility_handler_contact = StringField('Facility Contact', validators=[Optional(), Length(max=200)])
+ facility_handler_notes = TextAreaField('Facility Handling Notes', validators=[Optional(), Length(max=1000)])
+ vendor_name = StringField('Contractor Name', validators=[Optional(), Length(max=100)])
+ vendor_contact = StringField('Contractor Contact', validators=[Optional(), Length(max=200)])
+ vendor_notes = TextAreaField('Contractor Notes', validators=[Optional(), Length(max=1000)])
class IssueUpdateForm(FlaskForm):