06/09 Update documents

This commit is contained in:
2026-06-09 16:59:43 -04:00
parent f69ad4f805
commit a2d465b3d2
2 changed files with 110 additions and 7 deletions
+101 -4
View File
@@ -2,7 +2,7 @@
> **Audience:** AI assistants and developers working on this codebase.
> **Purpose:** Authoritative reference for architecture, conventions, gotchas, and decisions.
> **Last reviewed:** May 2026 (Phase 19 complete + mobile API gap-fill Phases AE + customer UI refinements: contract filter scoping on Inspections/Issues list, dynamic invitation email domain from `request.host_url`, "Your Facilities" card-grid dashboard panel)
> **Last reviewed:** June 2026 (Phase 19 complete + mobile API gap-fill Phases AE + customer UI refinements + Phase 22 comment visibility + Phase 23 support chat/tickets + inspector performance Excel export + inspection list filters + customer issue logging + AI chatbot)
---
@@ -43,6 +43,7 @@
- **Notification** system (in-app + email) driven by an admin-controlled matrix
- **Reports** — on-demand PDF/CSV scorecards and scheduled email digests
- **Audit trail** — immutable log of every create/update/delete action
- **Support chat** — Groq AI chatbot for customers with preset FAQ chips; escalation to admin via ticketing system; customers can view and reply to their own tickets; admins manage tickets at `/support/admin/tickets`
- **Mobile API** — JWT-authenticated REST layer for the iPad native app
- **iPad native app** — SwiftUI + SwiftData offline-first inspection tool (Phase A + B + C complete)
@@ -98,8 +99,11 @@ lt_janitorial_quality_control/
│ ├── models/
│ │ ├── inspection.py # Inspection — mobile_local_id column (Phase B)
│ │ ├── issue.py # Issue — mobile_local_id (Phase B), reported_by (Phase 18), mobile_photo_paths (Phase 19)
│ │ ├── support.py # SupportTicket, SupportTicketReply (Phase 23)
│ │ └── ...
│ ├── routes/
│ │ ├── support.py # /support/* — AI chat, ticket submit/list/detail (Phase 23)
│ │ └── ...
│ ├── static/
│ │ └── uploads/ # UPLOAD_FOLDER root
│ │ ├── inspection_photos/
@@ -109,10 +113,17 @@ lt_janitorial_quality_control/
│ │ └── issues/
│ │ ├── view.html # Shows photo_path + mobile_photo_paths under "Photo Evidence"
│ │ └── issues_view.html # Same photo evidence logic
│ ├── templates/
│ │ └── support/
│ │ ├── chat.html # Customer AI chatbot + FAQ chips + submit-ticket modal
│ │ ├── my_tickets.html # Customer: list of own tickets
│ │ ├── my_ticket_detail.html # Customer: ticket detail + staff replies + follow-up form
│ │ ├── admin_tickets.html # Admin: paginated ticket list with status filter tabs
│ │ └── admin_ticket_detail.html # Admin: ticket detail + reply form + status controls
│ └── utils/
├── migrations/
│ └── versions/
│ └── phase19_issue_mobile_photos.py ← HEAD
│ └── phase23_support_tickets.py ← HEAD
└── ...
```
@@ -134,6 +145,8 @@ lt_janitorial_quality_control/
| `MAIL_DEFAULT_SENDER` | From address |
| `DIGEST_SECRET` | Authenticates all cron endpoints |
| `REDIS_URL` | Optional. When set, Flask-Limiter uses Redis for shared rate-limit counters across Gunicorn workers. |
| `GROQ_API_KEY` | Optional. When set, enables the AI chatbot at `/support/chat`. Absent → chat input disabled; customers see a "Submit to Support" fallback only. |
| `GROQ_MODEL` | Optional. Groq model ID. Defaults to `llama-3.3-70b-versatile`. |
### Email SSL Auto-Detection
@@ -232,6 +245,34 @@ notifications: id, user_id, title, body, link, is_read, created_at, issue_id,
notification_preferences: id, user_id, event_type, email_enabled, digest_mode, digest_frequency
```
### IssueComment
```
issue_comments: id, issue_id (FK), user_id (FK), body, created_at,
status_at_time, is_customer_visible (BOOLEAN, default False) ← Phase 22
```
**`is_customer_visible`:** Staff comments are hidden from customers by default (`False`). Staff can tick "Share with customer" at post time to set `True`. Customer-authored comments are always stored as `True`. Customers see only `is_customer_visible=True` comments; staff see all.
### SupportTicket / SupportTicketReply
```
support_tickets: id, customer_id (FK→users SET NULL), facility_id (FK→facilities SET NULL),
subject VARCHAR(200), body TEXT, status VARCHAR(20) DEFAULT 'open',
created_at DATETIME
status values: open / answered / closed
support_ticket_replies: id, ticket_id (FK→support_tickets CASCADE), user_id (FK→users SET NULL),
body TEXT, created_at DATETIME
```
**Flow:**
- Customer submits ticket via chat page modal → status `open` → admins notified (in-app + email)
- Admin replies → status auto-advances to `answered` → customer notified (in-app + email, link to `/support/my-tickets/<id>`)
- Customer adds follow-up → status reverts to `open` → admins notified again
- Admin can manually set: `open` / `answered` / `closed`
- Closed tickets cannot receive new replies from customers
### NotificationMatrix
```
@@ -269,9 +310,12 @@ api_device_tokens: id, user_id, device_id, apns_token, device_name, app_version
| Contracts | ✅ | ✅ | ✅ | read | scoped |
| Templates | ✅ | ✅ | ❌ | ❌ | ❌ |
| Inspections (execute) | ✅ | ✅ | ✅ | ✅ | read |
| Issues (create/assign) | ✅ | ✅ | ✅ | ✅ | read |
| Issues (create/assign) | ✅ | ✅ | ✅ | ✅ | ✅ create own |
| Issues (quick-assign) | ✅ | ✅ | ❌ | ❌ | ❌ |
| Issue verification | ✅ | ✅ | ❌ | ❌ | ❌ |
| Issue comments | ✅ | ✅ | ✅ | ✅ | followed/reported issues only |
| Support Chat (AI) | ❌ | ❌ | ❌ | ❌ | ✅ |
| Support Tickets (manage) | ✅ | ✅ | ❌ | ❌ | own only |
| Reports | ✅ | ✅ | ✅ | ✅ | scoped |
| Scheduled Reports | ✅ | ✅ | ✅ | ❌ | ❌ |
| Audit Trail | ✅ only | ❌ | ❌ | ❌ | ❌ |
@@ -304,6 +348,7 @@ api_device_tokens: id, user_id, device_id, apns_token, device_name, app_version
| `audit` | `/audit` | list (admin only), view, purge |
| `reports` | `/reports` | index, facility report, scorecard, CSV/PDF export |
| `scheduled_reports` | `/scheduled-reports` | CRUD + manual trigger |
| `support` | `/support` | `GET /chat`, `POST /chat/message` (AJAX→Groq), `POST /tickets`, `GET /my-tickets`, `GET/POST /my-tickets/<id>`, `GET /admin/tickets`, `GET/POST /admin/tickets/<id>` |
| `api` | `/api/v1` | parent blueprint |
| `api_auth` | `/api/v1` | `/auth/login`, `/auth/refresh`, `/auth/logout`, `/auth/me`, `/devices/register` |
| `api_facilities` | `/api/v1` | `/facilities`, `/facilities/<id>/areas` |
@@ -561,7 +606,9 @@ phase1_projects_roles → phase6_features → phase7_mobile_api → phase8_notif
→ phase19_issue_mobile_photos
→ phase20_inspector_assignments
→ phase21_template_active
→ phase21_performance_indexes ← HEAD
→ phase21_performance_indexes
→ phase22_comment_visibility
→ phase23_support_tickets ← HEAD
```
### phase21_performance_indexes
@@ -578,6 +625,22 @@ sudo systemctl restart gunicorn
Adds `active` boolean column to `inspection_templates` so templates can be deactivated without deletion. Inactive templates are hidden from the inspection-start form but remain accessible in the template management UI. Uses `INFORMATION_SCHEMA` column existence check — safe to re-run.
### phase23_support_tickets
Creates `support_tickets` and `support_ticket_replies` tables. Uses table existence check — safe to re-run.
**Deploy order:**
```bash
flask db upgrade
pip install groq # if not already installed
# Set GROQ_API_KEY in environment / systemd unit
sudo systemctl restart gunicorn
```
### phase22_comment_visibility
Adds `is_customer_visible BOOLEAN NOT NULL DEFAULT FALSE` to `issue_comments`. Existing comments default to staff-only visibility. Uses `INFORMATION_SCHEMA` column existence check — safe to re-run.
### phase19_issue_mobile_photos
Adds `mobile_photo_paths JSON NULL` to `issues` table. Stores extra evidence photos submitted from the iPad at issue-creation time, separate from `result_photos` (resolution photos) so they appear under "Photo Evidence" on the web. Uses `INFORMATION_SCHEMA` existence check — safe to re-run.
@@ -665,6 +728,35 @@ Rendered in `dashboard.html` for `current_user.role == 'customer'`. Uses a Boots
- **Count badge:** The card header always shows the total facility count as a `badge bg-secondary rounded-pill`.
- The JS block is only emitted when `customer_facilities|length > 9`; the search input is only emitted when `customer_facilities|length > 6`.
### Issue Comments — Visibility & Authorship
- Comments live in the left column of `issues/view.html` under the issue description, rendered as chat bubbles.
- Each bubble shows: colored avatar circle (color keyed to `author.id % 7`), display name, role badge (Staff / Customer), `is_customer_visible` badge (staff-only view), status-at-time badge, timestamp, and body.
- **Staff commenting:** A hidden checkbox `name="is_customer_visible"` in the Add Comment form defaults to unchecked (staff-only). Checking it marks the comment visible to customers.
- **Customer commenting:** Only shown when `can_customer_comment = is_following or issue.reported_by == current_user.id`. Customer POST bypasses `IssueUpdateForm`; the route sets `is_customer_visible=True` unconditionally.
- **Read filtering:** `GET issues/view` passes `filter_by(is_customer_visible=True)` to customers; staff receive all comments.
### Inspection List Filters
`inspections.index()` accepts five additional query params: `date_from`, `date_to` (ISO date strings), `score_min`, `score_max` (0100 floats), `inspector_id` (int). Inspector filter is suppressed when the viewer has the `inspector` role (they always see their own only). The `inspectors` variable is passed to the template only for non-inspector roles so the dropdown is conditionally rendered.
### Inspector Performance — Excel Export
`GET /reports/export/inspector-performance` generates a `.xlsx` with two sheets:
- **Performance Summary** — all inspector KPIs, color-coded cells, totals row
- **Inspection Detail** — individual inspection records for the period
Accepts `date_from`, `date_to`, `inspector_id` query params matching the HTML report page. Logs an `EXPORT` audit action. Uses `openpyxl`.
### Support Chat — Customer UX
`GET /support/chat` — customer only. Renders:
- Greeting message with `current_user.display_name` (injected via `var userName = {{ current_user.display_name | tojson }}` — use `tojson` not inline interpolation to prevent XSS/quote breaks).
- FAQ quick-reply chips: text stored in `data-faq="..."` HTML attribute (HTML-escaped with `| e`), read in JS via `btn.dataset.faq`. **Never use `| tojson` in an `onclick=""` attribute** — it emits double-quoted JSON inside a double-quoted attribute, breaking HTML parsing and truncating the `<script>` tag.
- Chat history kept client-side in `let history = []`, sent with each AJAX `POST /support/chat/message`. Server caps at last 20 turns.
- If `GROQ_API_KEY` is absent, input is disabled and a fallback "Submit to Support" link is shown.
- "Submit to Support" modal POSTs to `POST /support/tickets`; subject pre-filled from last user message in history.
### Inspection Execute Page — UX Patterns
- **Photo upload-on-select**: `uploadPhotoField(input)` fires immediately on `<input type="file">` change. XHR to `POST /<id>/upload-photo`. On success, the server path is written to `<input type="hidden" id="field_<fid>_server_path">` and a `<img id="thumb_<fid>">` is shown.
@@ -766,6 +858,11 @@ timeout = 30
| 63 | **Customer Contract filter scoped to assigned contracts only** | `inspections.index()` and `issues.index()` build the `projects` list differently for `customer` role: query `CustomerAssignment.query.filter_by(user_id=current_user.id)` to get assigned `project_id` values, then filter `Project` to that set. All other roles still receive all active projects. Pattern mirrors the existing inspector scoping in `inspections.start()`. |
| 64 | **Invitation email sender and link domain are derived from `request.host_url`** | `_send_invite_email(user, token, base_url=None)` in `customers.py` accepts an optional `base_url`. Both call sites (`invite` and `resend_invite`) pass `request.host_url`. Inside the function, `effective_base` is built from that value (falling back to `APP_BASE_URL`); `setup_link` uses `effective_base`; `sender` is `noreply@<netloc>` parsed from `effective_base`. The SMTP server and credentials are unchanged — only the `From` address and link URL vary per domain. |
| 65 | **Customer "Your Facilities" uses a card grid, not a table** | See §18 "Customer Dashboard — Your Facilities Panel". Never revert to a full-width table for this section. The show-more threshold is `VISIBLE = 9`; the search input threshold is `> 6`. Both thresholds live as JS/Jinja constants in `dashboard.html` and can be adjusted together if needed. |
| 66 | **FAQ chip text must use `data-faq` attribute, not `onclick` with `\| tojson`** | `\| tojson` emits `"text"` (double-quoted) inside `onclick="..."` (also double-quoted), breaking HTML parsing and silently truncating the `<script>` block. Use `data-faq="{{ text \| e }}"` and read via `btn.dataset.faq` in JS. |
| 67 | **`display_name` in JS must use `\| tojson`, not inline Jinja interpolation** | `"Hi {{ name }}"` in a JS string literal breaks if `name` contains `"` or `\`. Use `var name = {{ name \| tojson }};` then concatenate. |
| 68 | **Support ticket customer replies revert status from `answered` → `open`** | When a customer posts a follow-up on an answered ticket, the route sets `ticket.status = 'open'` so admins see it in their open queue. Admin must manually close or re-answer. |
| 69 | **Customer issue create: `assigned_to` field hidden, `facility_id` scoped to `get_customer_scope()`** | `issues.create()` detects `role == 'customer'`, scopes facilities to the customer's assigned set, sets `staff = []` for the assigned_to dropdown, and hides the field in `form.html`. `IssueForm.facility_id.choices` must still include all active facilities so POST validation passes. |
| 70 | **`notify()` does NOT commit — caller must `db.session.commit()` after all `notify()` calls** | `notify()` adds a `Notification` row to the session but leaves the commit to the caller. The support helpers (`_notify_admins_new_ticket`, `_notify_customer_reply`, `_notify_admins_customer_reply`) each call `db.session.commit()` after the `notify()` loop. |
---
+9 -3
View File
@@ -12,7 +12,9 @@ A production-grade web application for managing janitorial service contracts, fa
- **Notification System** — In-app + email notifications driven by an admin-controlled routing matrix; per-user preferences including digest mode and a one-click "Pause All Emails" toggle
- **Reports** — On-demand PDF/CSV scorecards and scheduled recurring email reports (daily/weekly/monthly)
- **Audit Trail** — Immutable log of every create, update, and delete action with actor and IP capture
- **Mobile API** — JWT-authenticated REST API (Phase 7) for the companion React Native / Expo mobile application; rate-limited login and refresh endpoints
- **Support Chat** — Groq AI-powered chatbot for customers with preset FAQ quick-replies; automatic escalation to a ticketing system when the AI cannot resolve the issue; admins manage and reply to tickets at `/support/admin/tickets` with in-app and email notifications on every state change
- **Inspector Performance Export** — Excel (`.xlsx`) export of the inspector performance summary with a color-coded KPI sheet and a detailed inspection log sheet
- **Mobile API** — JWT-authenticated REST API (Phase 7) for the companion iPad native app; rate-limited login and refresh endpoints
- **Contract Hierarchy** — Facilities grouped into Contracts (internally "Projects") with optional Contract Manager assignment and per-contract customer access control
---
@@ -30,8 +32,10 @@ A production-grade web application for managing janitorial service contracts, fa
| Email | Flask-Mail (SMTP) |
| PDF | ReportLab |
| Frontend | Bootstrap 5, Chart.js, Jinja2 |
| Excel export | openpyxl |
| AI chatbot | Groq API (`llama-3.3-70b-versatile`) |
| Server | Gunicorn + Nginx |
| Mobile | React Native + Expo |
| Mobile | SwiftUI + SwiftData (iOS 17+) |
---
@@ -199,7 +203,7 @@ Four background tasks require scheduled execution. All endpoints that require a
| **director** | Broad access equivalent to admin, excluding Audit Trail and Notification Matrix |
| **project_manager** | Manages contracts, facilities, and reports; cannot manage users or system settings |
| **inspector** | Executes inspections and manages assigned issues |
| **customer** | Read-only portal scoped to assigned facilities; receives notifications on their facilities |
| **customer** | Portal scoped to assigned facilities; can create issues, comment on followed/reported issues, use the AI support chat, and submit/reply to support tickets |
### Creating the First Admin Account
@@ -291,6 +295,8 @@ flask db downgrade
| `DIGEST_SECRET` | — | Authenticates all cron endpoints |
| `MAX_CONTENT_LENGTH` | `50MB` | Maximum upload size per request |
| `REDIS_URL` | — | Redis connection URI for shared rate-limit storage; optional but recommended in production |
| `GROQ_API_KEY` | — | Groq API key. When absent the AI chatbot is disabled; customers can still submit support tickets. |
| `GROQ_MODEL` | `llama-3.3-70b-versatile` | Groq model ID override |
---