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' %}
- {{ form.assigned_to.label(class="form-label fw-semibold") }} + {{ form.handler_type.label(class="form-label fw-semibold") }} + {{ form.handler_type(class="form-select", id="handler_type_select") }} +
+
+ {{ form.assigned_to(class="form-select") }} + {% for e in form.assigned_to.errors %}
{{ e }}
{% endfor %}
+ + {# Facility-staff handler — shown when Handled By = Facility Staff #} + + + {# External vendor — shown when Handled By = External Vendor #} + {% 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):