**Display labels are perspective-neutral** (they read the same for staff and customers) with a descriptor line under the selector and a tooltip on badges: `internal` → **"Janitorial Staff"** ("Our janitorial crew handles it."), `facility` → **"Facility Staff"** ("The facility's own on-site staff handle it."), `vendor` → **"External Vendor"** ("An outside contractor handles it."). Labels/descriptions live in `Issue.HANDLER_LABELS` / `HANDLER_DESCRIPTIONS`, the WTForms `handler_type` choices, and the `HANDLER_DESC` JS map in both issue templates — keep these in sync. Do **not** use viewer-relative words like "Our"/"Your" for the stored categories.
A free-text **`internal_handler_name`** (Phase 41) captures the janitorial crew member's name when `handler_type == 'internal'` — the actual person doing the work, who may not be a system User. It is distinct from `assigned_to` (the follow-up owner) and is revealed by the same "Handled By" selector JS as the facility/vendor blocks (`#internal_handler_block`). Displayed under a **"Staff"** row on the issue detail when set.
Free-text **`internal_handler_name`** (Phase 41) + **`internal_handler_contact`** (Phase 42, phone/email) capture the janitorial crew member's name and contact when `handler_type == 'internal'` — the actual person doing the work, who may not be a system User. They are distinct from `assigned_to` (the follow-up owner) and are revealed by the same "Handled By" selector JS as the facility/vendor blocks (`#internal_handler_block`). Displayed under a **"Staff"** row (name + contact) on the issue detail when set.
`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 janitorial/facility/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`).
@@ -649,7 +650,7 @@ No migration was needed for either feature: the `scheduled_inspections` table (p
Revision id `phase42_internal_contact` (file `phase42_internal_handler_contact.py`). Adds `issues.internal_handler_contact VARCHAR(200) NULL` — phone/email for the janitorial staff member handling the issue when `handler_type == 'internal'`; parallels `internal_handler_name`. `INFORMATION_SCHEMA` column-existence check — safe to re-run.
**Deploy order:**
```bash
flask db upgrade
sudo systemctl restart gunicorn
```
### phase40_auditor_role
Revision id `phase40_auditor_role`. Adds the `auditor` value to the `users.role` ENUM (`ALTER TABLE users MODIFY COLUMN role ENUM(...,'auditor') NOT NULL`). This is a **pure ENUM expansion** (adds a value, removes/migrates nothing), so the 3-step ENUM protocol does not apply and the `MODIFY` is idempotent — safe to re-run. `downgrade()` reassigns any `auditor` rows to `project_manager` before contracting the ENUM. Backs the new Auditor role — see the `auditor` note in §5 and the `@issue_manager_required` decorator in §6.
@@ -1331,7 +1343,7 @@ timeout = 30
| 75 | **Email is stored lowercased; look it up case-insensitively** | User/customer email is normalized to `.strip().lower()` at every write site (`auth.py` profile/create/edit, `customers.py` invite/edit). Forgot-password lookup uses `db.func.lower(User.email) == input` so a mixed-case legacy row still matches — a plain `filter_by(email=...)` silently missed them and sent no reset (the failure was invisible because of the generic "if an account exists…" message). Keep both halves: normalize on write, case-insensitive on lookup. |
| 76 | **Transactional email `From` must be an SMTP-authorized identity, per-domain branding via display name only** | Reset-password sends from `MAIL_DEFAULT_SENDER`; customer invite sends from `branded_sender()` = `(per-domain display name, authorized address)`. A per-host `noreply@<subdomain>` sender is accepted by the relay then dropped by SPF/DMARC. See rule 64 and §8 `mail_utils.py`. |
| 77 | **`GET /api/v1/scheduled-inspections` is inspector-scoped by `inspector_id`, admin/director/PM see all** | New `app/api/scheduled.py` blueprint. Register in `app/api/__init__.py` AND `csrf.exempt(_api_scheduled_bp)` in `app/__init__.py` — the child-blueprint CSRF exemption never cascades from the parent. Read-only; do not add write/fulfil endpoints here (the schedule lifecycle stays in `routes/scheduled_inspections.py`). |
| 78 | **`PATCH /api/v1/issues/<id>/handler` allows the inspector on purpose — do NOT align it to the web form's admin/director/PM restriction** | The iPad lets the assigned inspector set "Handled By" from the field, scoped via `get_inspector_scope()` (403 if the issue's facility isn't contracted). This is a deliberate divergence from the web form. `_issue_payload()` must keep returning all handler fields (`handler_type`, `handler_label`, `facility_handler_*`, `vendor_*`, `internal_handler_name`) or the iPad's "Handled By" panel silently blanks — same failure mode as rule 40. |
| 78 | **`PATCH /api/v1/issues/<id>/handler` allows the inspector on purpose — do NOT align it to the web form's admin/director/PM restriction** | The iPad lets the assigned inspector set "Handled By" from the field, scoped via `get_inspector_scope()` (403 if the issue's facility isn't contracted). This is a deliberate divergence from the web form. `_issue_payload()` must keep returning all handler fields (`handler_type`, `handler_label`, `facility_handler_*`, `vendor_*`, `internal_handler_name`, `internal_handler_contact`) or the iPad's "Handled By" panel silently blanks — same failure mode as rule 40. |
| 79 | **`auditor` = `project_manager` access + issue management, minus delete — keep the two decorators distinct** | Auditor is added to `@project_manager_required` (PM baseline) and to every `project_manager` role check in routes/templates. Its *extra* issue powers (verify/bulk-verify/verification-queue) go through the separate `@issue_manager_required` (admin/director/auditor). Issue **delete** stays `@supervisor_required` — never add auditor there. When adding a new PM-level gate, include `auditor`; when adding a director-only or delete-level gate, do not. The three issue **delete** template gates (spaced `['admin', 'director']` in `issues/list.html` + `issues/view.html`) are deliberately left without auditor. Auditor is also in the `_ALLOWED_ROLES` set of every `app/api/*` module — a **new** API blueprint's `_ALLOWED_ROLES` must include `auditor` for PM parity. |
| 80 | **Assignee dropdowns are `director`/`inspector`/`auditor` (admin removed, auditor added)** | The issue/inspection assignee `<select>`s query `User.role.in_([...])` — admin was removed and auditor added (the inspection flag-issue list also keeps `project_manager`). These lists control who can be *assigned*, distinct from who can *edit*. The issue-update route (`issues.view`) defensively appends any current `assigned_to` who is not in the set (e.g. a legacy admin assignment) to `form.assigned_to.choices` so saving the form never silently unassigns them. Do not remove that guard. |
op.execute(sa.text("ALTER TABLE issues DROP COLUMN internal_handler_contact"))
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.