{% extends "base.html" %} {% block title %}Issue #{{ issue.id }}{% endblock %} {% block extra_css %} {% endblock %} {% block content %} {% set can_edit = current_user.role in ['admin','director','auditor'] or issue.assigned_to == current_user.id %} {# The filtered list URL this page was opened from (phase: filter preservation). Threaded into every action so an update or delete returns to the same filtered page, and used by the Back button. #} {% set back_url = request.args.get('next') or url_for('issues.index') %} {# Is the viewer OUR staff? Drives the internal-only chrome on this page: the "comments are visible to everyone" warning and the per-comment "Customer visible" / "Staff only" badges. Both are instructions about how WE work and must never reach a customer account. Written as an explicit ALLOWLIST of our own roles, deliberately: * It FAILS CLOSED. The obvious form, `not current_user.is_customer_account`, fails OPEN — if the attribute is missing for any reason (a process still running an older models/user.py after a template-only reload, say) Jinja yields Undefined, `not Undefined` is true, and the internal text is shown to exactly the people it must be hidden from. An allowlist of literal role strings can only ever be true for a role we listed. * `external_inspector` is absent ON PURPOSE. This is NOT the rule-87 case: rule 87 is about capability/scoping, where a Customer Inspector must behave exactly like our own inspector. Here the question is "does this person work for us?", which is the one place the two genuinely differ. Do not "fix" this by adding external_inspector to the list. #} {% set viewer_is_our_staff = current_user.role in ['admin', 'director', 'project_manager', 'auditor', 'inspector'] %}
{# ══════════════════════════════════ LEFT COLUMN ══════════════════════════════════ #}
{# ── Issue details ──────────────────────────────────────────────────── #}
Issue #{{ issue.id }} — {{ issue.severity|title }} Severity
{{ issue.status|replace('_',' ')|title }}
Reported
{{ issue.reported_at.strftime('%Y-%m-%d %H:%M') }}
Reported By
{% if issue.reporter %} {{ issue.reporter.display_name }} {% if issue.reporter.role == 'customer' %} Customer {% else %} {{ issue.reporter.role|replace('_',' ')|title }} {% endif %} {% else %} {% endif %}
Contract
{% if issue.resolved_facility and issue.resolved_facility.project %} {{ issue.resolved_facility.project.name }} {% else %}—{% endif %}
Facility
{{ issue.resolved_facility.name if issue.resolved_facility else '—' }}
Area
{{ issue.area.name if issue.area else '—' }}
{% if issue.inspection %}
Inspection
#{{ issue.inspection_id }}
{% endif %}
Assigned To
{{ issue.assigned_user.display_name if issue.assigned_user else '— Unassigned —' }}
{% set _ht = issue.handler_type or 'internal' %} {% if _ht != 'internal' or issue.internal_handler_name or issue.internal_handler_contact %}
Handled By
{% if _ht == 'facility' %} Facility Staff {% if issue.facility_handler_name %} {{ issue.facility_handler_name }} {% if issue.facility_handler_contact %} {{ issue.facility_handler_contact }} {% endif %} {% endif %} {% if issue.facility_handler_notes %}
{{ issue.facility_handler_notes }}
{% endif %} {% elif _ht == 'vendor' %} External Vendor {% else %} Janitorial Staff {% if issue.internal_handler_name %} {{ issue.internal_handler_name }} {% endif %} {% if issue.internal_handler_contact %} {{ issue.internal_handler_contact }} {% endif %} {% endif %}
{% endif %} {% if issue.vendor_name %}
Contractor
{{ issue.vendor_name }} {% if issue.vendor_contact %} {{ issue.vendor_contact }} {% endif %} {% if issue.vendor_notes %}
{{ issue.vendor_notes }}
{% endif %}
{% endif %} {% if issue.resolved_at %}
Resolved
{{ issue.resolved_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %}

Description

{{ issue.description }}

{% if issue.photo_path or issue.mobile_photo_paths %}
Photo Evidence
{% if issue.photo_path %} {% endif %} {% for photo in (issue.mobile_photo_paths or []) %} {% endfor %}
{% endif %} {% if issue.result_notes or issue.result_photos %}
Resolution Details
{% if issue.result_notes %}

{{ issue.result_notes }}

{% endif %} {% if issue.result_photos %}
{% for photo in issue.result_photos %} Result photo {% endfor %}
{% endif %} {% endif %} {# ── Verification panel ── #} {% if issue.verified_at %}
Verified by {{ issue.verifier.display_name if issue.verifier else 'unknown' }} on {{ issue.verified_at.strftime('%Y-%m-%d %H:%M') }}. {% if issue.verification_note %}
{{ issue.verification_note }}{% endif %}
{% elif issue.status == 'pending_verification' %}
Awaiting director verification. {% if current_user.role in ['admin','director','auditor'] %}
{% endif %}
{% endif %}
{# ── Linked issues ────────────────────────────────────────────────────── Duplicates and related issues, so whoever picks this one up can reach the others. `issue_links` arrives already filtered to links whose far end this viewer may open (_readable_links) — do NOT add links from the model directly here, or a customer sees an issue at a facility they have no assignment to. Links are navigational only: nothing here changes status, SLA, assignee or followers on either issue. #}
Linked Issues {{ issue_links|length }}
{% if can_manage_links %} {% endif %}
{% if issue_links %}
{% for link, other, label in issue_links %}
{{ label }}
#{{ other.id }} {{ other.area.name if other.area else other.resolved_facility.name if other.resolved_facility else '—' }}
{{ other.description }}
{{ other.severity|title }} {{ other.status|replace('_',' ')|title }} {% if can_manage_links %}
{% endif %}
{% endfor %}
{% else %}

No linked issues. {% if can_manage_links %} Use Link an Issue to point at a duplicate or a related issue. {% endif %}

{% endif %}
{# ── Comments ───────────────────────────────────────────────────────── #}
Comments {{ comments|length }}
{# Comment list #} {% if comments %}
{% set avatar_colors = ['#4f46e5','#0891b2','#059669','#d97706','#dc2626','#7c3aed','#db2777'] %} {% for c in comments %} {% set avatar_color = avatar_colors[c.author.id % (avatar_colors | length)] %}
{{ c.author.display_name[0] | upper }}
{{ c.author.display_name }} {% if c.author.role == 'customer' %} Customer {% else %} {{ c.author.role|replace('_',' ')|title }} {% endif %} {# Visibility indicator — OUR staff only, and only while the per-comment flag still decides anything. While comments_open is set EVERY comment reaches the customer, so a "Staff only" badge would be a lie; it is suppressed rather than shown incorrectly. #} {% if viewer_is_our_staff and not comments_open %} {% if c.is_customer_visible %} Customer visible {% else %} Staff only {% endif %} {% endif %}
{{ c.status_at_time|replace('_',' ')|title }} {{ c.created_at.strftime('%b %d, %Y %H:%M') }}

{{ c.body }}

{% endfor %}

{% elif current_user.role == 'customer' %}

No comments yet.


{% endif %} {# ── Add Comment form ─────────────────────────────────────────────── #} {% set can_customer_comment = current_user.role == 'customer' and (is_following or issue.reported_by == current_user.id) %} {% if can_edit %} {# Staff comment form with visibility checkbox #}

Add Comment

{# While comments_open is set, every comment reaches the customer, so the "Share with customer" tick decides nothing. Saying so plainly matters: a staff member must not write something they believe is private. The checkbox is still posted and recorded, so turning the setting off restores its meaning immediately. OUR STAFF ONLY. `can_edit` is also true for a Customer Inspector assigned to the issue, and this banner is an internal-process warning ("do not post internal-only notes") — showing it to a customer account exposes how we work and reads as nonsense to them, since nothing they write was ever private. #} {% if comments_open and viewer_is_our_staff %}
Comments are currently visible to everyone, including the customer, regardless of the tick below. Do not post internal-only notes here.
{% endif %}
{% elif can_customer_comment %} {# Customer comment form — visible to all by design #}

Add Comment

{% elif current_user.role == 'customer' %}

Follow this issue to add comments.

{% else %}

Only assigned staff can add comments.

{% endif %}
{# /col-lg-8 #} {# ══════════════════════════════════ RIGHT COLUMN ═════════════════════════════════ #}
{# ── Follow / Unfollow ──────────────────────────────────────────────── #}
{% if is_following %}Following{% else %}Not following{% endif %} {{ issue.followers.count() }} follower{{ 's' if issue.followers.count() != 1 else '' }}
{% if is_following %}
{% else %}
{% endif %}
{# ── Update Form ────────────────────────────────────────────────────── #} {% if can_edit %}
Update Issue
{{ form.status.label(class="form-label fw-semibold") }} {{ form.status(class="form-select") }}
{% if current_user.role in ['admin','director','auditor'] %}
{{ form.assigned_to.label(class="form-label fw-semibold") }} {{ form.assigned_to(class="form-select") }}
{% endif %}
{{ form.result_notes.label(class="form-label fw-semibold") }} {{ form.result_notes(class="form-control", rows=3, placeholder="Describe what was done to resolve this issue…", value=issue.result_notes or '') }}
Attach one or more photos showing the resolution.
{% if issue.result_photos %}
{{ issue.result_photos|length }} photo(s) already uploaded
{% endif %}
{% if current_user.role in ['admin','director','project_manager','auditor'] %}

Handler / Ownership

{{ form.handler_type.label(class="form-label small fw-semibold mb-1") }} {{ form.handler_type(class="form-select form-select-sm", id="handlerTypeSelect") }}
{{ form.facility_handler_name.label(class="form-label small fw-semibold mb-1") }} {{ form.facility_handler_name(class="form-control form-control-sm", placeholder="Contact name at the facility", value=issue.facility_handler_name or '') }}
{{ 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", value=issue.facility_handler_contact or '') }}
{{ 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="Notes about what they are handling…") }}
{{ form.internal_handler_name.label(class="form-label small fw-semibold mb-1") }} {{ form.internal_handler_name(class="form-control form-control-sm", placeholder="Crew member handling this", value=issue.internal_handler_name or '') }}
{{ form.internal_handler_contact.label(class="form-label small fw-semibold mb-1") }} {{ form.internal_handler_contact(class="form-control form-control-sm", placeholder="Phone or email", value=issue.internal_handler_contact or '') }}

External Contractor

{{ 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", value=issue.vendor_name or '') }}
{{ 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", value=issue.vendor_contact or '') }}
{{ 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 %}
{% if issue.status in ['in_progress', 'resolved'] and current_user.role not in ['customer'] %}
{% endif %}
{% endif %}
{# /col-lg-4 #}
Back to Issues Export PDF {% if current_user.role in ['admin', 'director'] %} {% endif %}
{# ── Link an issue ──────────────────────────────────────────────────────────── Search is scoped server-side to issues this viewer could already open, so the picker can never be used to enumerate another contract's issues. The POST re-checks access — the search is only a convenience. #} {% if can_manage_links %} {% endif %} {% if current_user.role in ['admin', 'director'] %} {% endif %} {% block extra_js %} {% endblock %} {% endblock %}