06/09 Update documents
This commit is contained in:
@@ -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 A–E + 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 A–E + 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` (0–100 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. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user