345 lines
27 KiB
Markdown
345 lines
27 KiB
Markdown
# Appreciations — Workflow & Lifecycle
|
|
|
|
> Source of truth: `apps/appreciation/`. This is an **outbound recognition/recognition** module (recognition sent *to* staff/physicians), fundamentally different from inbound case-management. This document describes the **current implementation only**. Every claim is cited as `file:line`.
|
|
|
|
> **⚠ Read first — two important findings:**
|
|
> 1. `VALID_APPRECIATION_TRANSITIONS` (`models.py:28-34`) is **defined but never referenced** anywhere (dead code). The state machine is enforced **only** by runtime `ValueError` guards inside the model transition methods.
|
|
> 2. The REST API `create` and complaint→appreciation conversion call `appreciation.send()` on a freshly-created DRAFT, but `send()` requires ACTIVATED/AI_ANALYZED — **both would raise `ValueError`** (likely bugs). The **working path is the UI activation-gate flow**.
|
|
|
|
---
|
|
|
|
## 1. Purpose
|
|
|
|
**Appreciations = BOTH inbound patient feedback AND outbound staff recognition, but the dominant designed workflow is OUTBOUND recognition FROM PX/hospital staff TO staff/physicians.**
|
|
|
|
Evidence:
|
|
- `apps/appreciation/models.py:2`, `__init__.py:2`, `README.md:3`, `IMPLEMENTATION_SUMMARY.md:4`: *"Send and track appreciation to users and physicians."*
|
|
- The **outbound** character: `appreciation_create` is gated to PX/hospital staff (`ui_views.py:155`); the email subject is *"Staff Appreciation"* (`signals.py:72`); the body *"A staff member in your department has received an appreciation. Please acknowledge their good work."* (`signals.py:179`); `tasks.py:18` *"Send email + SMS to champion, manager, and appreciated staff."*
|
|
- The **inbound patient-feedback** channel: `public_appreciation_submit` (`ui_views.py:1196`) — patients/public submit a message, created as DRAFT, then PX staff triage and activate/send. Plus complaint→appreciation conversion (`complaints/views.py:2076`).
|
|
|
|
So: **a recognition engine whose originating actors can be (a) PX/hospital staff sending peer/leadership recognition, (b) public/patient submissions awaiting staff review, or (c) appreciation-type complaints being formally converted.** The terminal recipient is always a staff member or physician.
|
|
|
|
---
|
|
|
|
## 2. How a Case Starts
|
|
|
|
### 2.1 Who can create
|
|
|
|
| Channel | View/Serializer | Permission | File:line |
|
|
|---|---|---|---|
|
|
| Internal UI form | `appreciation_create` + `AppreciationForm` | `@login_required`; PX/Hosp-Admin/PX-Mgmt/PX-Employee | `forms.py:8`; `ui_views.py:153` |
|
|
| Public portal | `public_appreciation_submit` | **No auth** (`@csrf_exempt`); rate-limited (5/5min/IP) | `ui_views.py:1196` |
|
|
| External API (HIS integration) | `integrations/api_views.py` create | API-key scoped to a hospital | `integrations/api_views.py:606` |
|
|
| REST API create | `AppreciationCreateSerializer` | `IsAuthenticated` | `views.py:55`, `serializers.py:132` |
|
|
| Complaint → Appreciation conversion | `convert_to_appreciation` | Edit permission on the complaint | `complaints/views.py:2075` |
|
|
|
|
### 2.2 Creation channels in detail
|
|
|
|
**(a) Internal UI form** (`forms.py:8`): ModelForm exposing only `["hospital", "department", "message_en"]` (`forms.py:13`); `department` restricted to active departments (`forms.py:34`). On POST: `sender = request.user`, `status = DRAFT` (`ui_views.py:173`). **Category/recipient/visibility are NOT set here** — decided later during activate/send.
|
|
|
|
**(b) Public portal** (`ui_views.py:1196`): Inputs `contact_name`, `contact_phone`, `message`, `hospital`, optional `staff_name`/`department`/`section`. Required: name, phone, message, hospital (`ui_views.py:1224`). Creates with `status=DRAFT`, `visibility=PUBLIC`, `is_anonymous=False`, `category=None`, metadata `{source:"public_form", ...}` (`ui_views.py:1239`). Fires `notify_staff_new_item.delay("appreciation", ...)` (`ui_views.py:1258`). Returns `reference_number`.
|
|
|
|
**(c) External API** (`integrations/api_views.py:606`): Creates DRAFT, visibility PUBLIC, metadata `{source:"external_api", ...}`. Builds its own `APR-YYYYMMDD-<random>` (`api_views.py:602`) — informational, since `save()` also generates the canonical `reference_number`. Fires `notify_staff_new_item`.
|
|
|
|
**(d) REST API create** (`views.py:143`, serializer `serializers.py:132`):
|
|
Required: `recipient_type` (`['user','staff']`), `recipient_id` (UUID), `message_en`, `hospital_id`. Optional: `category_id`, `message_ar`, `visibility` (default PRIVATE), `is_anonymous`, `department_id`. Validation: recipient must belong to hospital; category hospital-compatible. `AppreciationViewSet.create` resolves a `User`/`Staff` via `ContentType`, creates the record (status DRAFT), then **immediately calls `appreciation.send()`** (`views.py:198`).
|
|
|
|
**(e) Complaint → Appreciation conversion** (`complaints/views.py:2076`): Guards — complaint active (`is_active_status`, `complaints/views.py:2087`), `complaint_type == "appreciation"` (`:2096`), not already converted (`:2103`). Defaults: `message_en` ← `complaint.description`; `message_ar` ← `complaint.short_description_ar`; `visibility="private"`; `is_anonymous=True` (`:2113`). Creates DRAFT (`:2171`), then `appreciation.send()` (`:2183`). Links back via `metadata.appreciation_id` (`:2186`); optionally closes complaint (`:2196`). Default category "Patient Feedback Appreciation" (code `patient_feedback`, `create_patient_feedback_category.py`).
|
|
|
|
### 2.3 What happens immediately after submission
|
|
- **Reference number** generated in `Appreciation.save()` via `generate_reference("APR", hospital)` (`models.py:243`) → `APR-YYYYMM-NNNN` (global monthly sequence). Stored on `reference_number` (`models.py:198`), comment *"internal-only, not publicly trackable"*.
|
|
- **Default status = DRAFT** (`models.py:193`).
|
|
- **`post_save` signal `handle_appreciation_sent`** (`signals.py:27`) only acts when `status == SENT and not notification_sent` — does **nothing** at creation (DRAFT).
|
|
- For public/external: **PX-admin notification task** `notify_staff_new_item("appreciation", id)` fired (`ui_views.py:1259`, `api_views.py:625`) → `emails/new_appreciation_notification.html`.
|
|
|
|
### 2.4 Why DRAFT + activation gate?
|
|
Mirrors the complaints activation gate. A DRAFT appreciation is **untriaged**: no recipient set (internal form), no AI analysis, no decision to publish at chosen visibility. **Activation** (`ui_views.py:195`) is the PX-staff gate that confirms content, triggers AI analysis, stamps `activated_at`/`activated_by`, emits audit. Only after activation may it be sent (`models.py:335`).
|
|
|
|
The draft count is surfaced to reviewers via `core/context_processors.py:74` (`draft_appreciation_count`) for PX/Hosp-Admin/PX-Mgmt.
|
|
|
|
---
|
|
|
|
## 3. Complete Lifecycle
|
|
|
|
### 3.1 The declared state machine — `VALID_APPRECIATION_TRANSITIONS` (`models.py:28-34`) — verified
|
|
```
|
|
DRAFT → {ACTIVATED}
|
|
ACTIVATED → {AI_ANALYZED, SENT}
|
|
AI_ANALYZED → {SENT}
|
|
SENT → {ACKNOWLEDGED}
|
|
ACKNOWLEDGED → set() (terminal)
|
|
```
|
|
|
|
### 3.2 Where enforced — NOWHERE via the constant
|
|
`VALID_APPRECIATION_TRANSITIONS` is **never imported or referenced** anywhere else (grep confirmed). The **model transition methods** each re-check the current status and raise `ValueError`:
|
|
- `activate()` (`models.py:309`) — requires DRAFT.
|
|
- `mark_ai_analyzed(analysis_data)` (`models.py:320`) — requires ACTIVATED.
|
|
- `send()` (`models.py:331`) — requires ACTIVATED or AI_ANALYZED.
|
|
- `acknowledge()` (`models.py:344`) — requires SENT.
|
|
|
|
### 3.3 `clean()` and DB CheckConstraints
|
|
- **No `clean()` method** on `Appreciation`.
|
|
- **No `CheckConstraint`s** in any migration. Only `unique_together` on category (`models.py:84`) and `AppreciationStats` (`models.py:517`), `unique=True` on `reference_number`/badge `code`.
|
|
|
|
**Conclusion:** status integrity enforced **only at runtime in Python methods**, not at DB layer.
|
|
|
|
---
|
|
|
|
## 4. Status Definitions
|
|
|
|
| Status | Constant | When entered | Who advances | Next | Key code |
|
|
|---|---|---|---|---|---|
|
|
| **DRAFT** | `AppreciationStatus.DRAFT` (`models.py:21`) | On creation (model default) | PX/hospital staff via `appreciation_activate` | ACTIVATED | created; ref number generated; PX admins notified (public/external only) |
|
|
| **ACTIVATED** | `ACTIVATED` (`models.py:22`) | Staff approves/triages draft; runs AI | AI step (auto) or staff `appreciation_send`/`send_to` | AI_ANALYZED, SENT | `activated_at`/`activated_by` stamped (`models.py:316`, `ui_views.py:211`) |
|
|
| **AI_ANALYZED** | `AI_ANALYZED` (`models.py:23`) | `mark_ai_analyzed()` called by activate view after a successful AI call | Staff via `send`/`send_to` | SENT | `ai_analyzed_at` + `ai_analysis` JSON (`models.py:326`, `ui_views.py:254`) |
|
|
| **SENT** | `SENT` (`models.py:24`) | `send()` invoked | The recipient (User/Staff) via `acknowledge` | ACKNOWLEDGED | `sent_at` stamped (`models.py:339`); triggers `post_save` cascade (notifications, stats, badges) |
|
|
| **ACKNOWLEDGED** | `ACKNOWLEDGED` (`models.py:25`) | Recipient acknowledges | (terminal) | none | `acknowledged_at` (`models.py:352`) |
|
|
|
|
### 4.1 The AI_ANALYZED status — what AI analysis happens
|
|
Performed **synchronously inside `appreciation_activate`** (`ui_views.py:230-258`), immediately after DRAFT→ACTIVATED. Uses `apps.core.ai_service.AIService.chat_completion` (NOT `analyze_complaint`):
|
|
- Prompt asks for JSON: `summary_en`, `summary_ar`, `themes` (list), `tone` (`warm|formal|casual`), `suggested_response_en`, `suggested_response_ar` (`ui_views.py:234-246`).
|
|
- Parsed and passed to `appreciation.mark_ai_analyzed(analysis_data)` (`ui_views.py:251`).
|
|
- On failure: logged + swallowed; appreciation stays ACTIVATED (does NOT auto-advance) — still sendable (`ui_views.py:255`).
|
|
- `AIService.chat_completion` (`ai_service.py:685`) → OpenRouter `/chat/completions`, model `google/gemini-2.5-flash-lite` default (`ai_service.py:41`).
|
|
|
|
**So AI_ANALYZED is a "content enrichment" step (summary/themes/tone/draft reply), not a sentiment/severity gate.** The appreciation can still be SENT directly from ACTIVATED without ever becoming AI_ANALYZED (`models.py:30`).
|
|
|
|
---
|
|
|
|
## 5. Workflow Actions
|
|
|
|
### 5.1 UI actions (`ui_views.py`)
|
|
|
|
| Action | Function | URL name | Status gate | Permission |
|
|
|---|---|---|---|---|
|
|
| List | `appreciation_list` `:37` | `appreciation_list` | — | `@login_required`; hospital-filtered |
|
|
| Detail | `appreciation_detail` `:92` | `appreciation_detail` | — | login; PX/Hosp-Admin or same-hospital |
|
|
| Create (internal) | `appreciation_create` `:153` | `appreciation_create` | creates DRAFT | PX/Hosp-Admin/PX-Mgmt/PX-Employee |
|
|
| **Activate** (DRAFT→ACTIVATED + AI) | `appreciation_activate` `:193` | `appreciation_activate` | must be DRAFT (`:198`) | PX/Hosp-Admin/PX-Mgmt/PX-Employee |
|
|
| Send (form) | `appreciation_send` `:264` | `appreciation_send` | ACTIVATED or AI_ANALYZED (`:269`) | PX/Hosp-Admin/PX-Mgmt/PX-Employee |
|
|
| **Send-to** (AJAX, person/department) | `appreciation_send_to` `:320` | `appreciation_send_to` | ACTIVATED or AI_ANALYZED (`:344`) | PX/Hosp-Admin/Dept-Manager/PX-Mgmt/PX-Employee |
|
|
| **Acknowledge** (SENT→ACKNOWLEDGED) | `appreciation_acknowledge` `:485` | `appreciation_acknowledge` | must be SENT (`:500`) | **recipient only** (User ContentType match, `:492`) |
|
|
| Public submit | `public_appreciation_submit` `:1196` | `public_appreciation_submit` | creates DRAFT | **none** |
|
|
| Restore deleted | `appreciation_restore` `:1036` | `admin_appreciation_restore` | — | PX/Hosp-Admin |
|
|
| PDF | `appreciation_pdf` `:1276` | `appreciation_pdf` | — | `@login_required` |
|
|
| Leaderboard | `leaderboard_view` `:515` | `leaderboard_view` | — | login |
|
|
| My Badges | `my_badges_view` `:580` | `my_badges_view` | — | login |
|
|
| Category CRUD | `:666-843` | `category_*` | — | PX/Hosp-Admin only |
|
|
| Badge CRUD | `:850-1033` | `badge_*` | — | PX/Hosp-Admin only |
|
|
| AJAX: users/staff/physicians/departments | `:1057-1147` | `ajax/*` | — | login |
|
|
|
|
Detail view exposes `can_activate` and `can_send` flags (`ui_views.py:142`).
|
|
|
|
### 5.2 REST API (`views.py`)
|
|
|
|
`AppreciationCategoryViewSet` (`:33`), `AppreciationViewSet` (`:55`, actions: `acknowledge` `:210`, `my_appreciations` `:232`, `sent_by_me` `:263`, `summary` `:277`), `AppreciationStatsViewSet` (`:363`), `AppreciationBadgeViewSet` (`:394`, PX/Hosp-Admin), `UserBadgeViewSet` (`:416`), `LeaderboardView` (`:445`).
|
|
|
|
`acknowledge` verifies the requester is the recipient via ContentType+id (`views.py:216`), else HTTP 403.
|
|
|
|
> **⚠ Note:** there is **no `activate` or `ai_analyze` REST action** — those are UI-only.
|
|
|
|
> **⚠ Gap — REST `create` vs status:** `AppreciationViewSet.create` (`views.py:143`) creates the record and immediately calls `appreciation.send()` (`views.py:198`). Because `send()` requires ACTIVATED/AI_ANALYZED (`models.py:335`), this raises `ValueError` on a DRAFT.
|
|
|
|
---
|
|
|
|
## 6. Decision Points
|
|
|
|
1. **Activation gate (DRAFT→ACTIVATED)** (`ui_views.py:195`). Mirrors the complaints activation gate.
|
|
2. **AI analysis path vs direct-send** — AI attempted at `appreciation_activate` (`ui_views.py:230`). If it succeeds → AI_ANALYZED; if it throws → stays ACTIVATED and is still sendable. From ACTIVATED the send step is allowed (`models.py:30`), so **AI is optional** to progressing.
|
|
3. **Visibility/audience decision** — `AppreciationVisibility` (`models.py:37`): PRIVATE/DEPARTMENT/HOSPITAL/PUBLIC. Default PRIVATE (`models.py:190`). Public portal forces PUBLIC (`ui_views.py:1248`); external API forces PUBLIC (`api_views.py:613`); complaint conversion defaults `"private"` (`complaints/views.py:2115`). Read-access filtered in `AppreciationViewSet.get_queryset` (`views.py:78`).
|
|
4. **Recipient/target selection** — done at send time, not creation, in `appreciation_send_to`: `recipient_type ∈ {person, department}` (`ui_views.py:350`). If department: auto-targets champion+manager via `get_champion_and_manager`. Optional `staff_id` sets recipient to a `Staff`.
|
|
5. **Badge awarding decision** — `signals.check_and_award_badges` (`signals.py:306`) runs whenever an appreciation reaches SENT.
|
|
|
|
---
|
|
|
|
## 7. Assignment Flow — RECIPIENTS
|
|
|
|
There is **no "case assignment to an agent"**. The analogous concept is the **recipient** (who the recognition is for) plus the **visibility audience**.
|
|
|
|
### 7.1 Recipient model
|
|
- Recipient is a **GenericForeignKey** (`recipient_content_type` + `recipient_object_id` + `recipient`) (`models.py:132`). Eligible concrete types: `accounts.User` and `organizations.Staff` (incl. physicians as `staff_type='physician'`, `ui_views.py:1110`).
|
|
- Sender: `models.ForeignKey("accounts.User")` (`models.py:127`).
|
|
- `AppreciationForm` (internal) does **not** set a recipient — only `hospital`, `department`, `message_en`. Recipient bound at send step (`ui_views.py:427`) for the department path, or via `recipient_type`/`recipient_id` in REST/conversion.
|
|
- The "owner" (for routing) is computed by `get_owner()` (`models.py:250`) — a cascade over section/department role-holders. **⚠ Gap:** `get_owner()` is defined but not invoked by any code in the appreciation app (verify external callers).
|
|
|
|
### 7.2 Visibility
|
|
`AppreciationVisibility` (`models.py:37`): PRIVATE, DEPARTMENT, HOSPITAL, PUBLIC. Read-side enforcement in `AppreciationViewSet.get_queryset` (`views.py:78`): a user sees their sent items, those received by their User/Staff profile, DEPARTMENT-vis in their dept, HOSPITAL-vis in their hospital, and all PUBLIC.
|
|
|
|
### 7.3 Organization context
|
|
`hospital` (required, `models.py:139`), `department` (nullable, `models.py:173`), `section` (nullable, `models.py:165`), plus three **DEPRECATED** legacy fields (`legacy_location`, `legacy_main_section`, `legacy_subsection`, `models.py:140-163`).
|
|
|
|
---
|
|
|
|
## 8. Investigation Process — N/A
|
|
|
|
Appreciations have **no investigation phase**. No investigator field, no investigation status, no evidence model. The closest analog is the **AI_ANALYZED** step, which is automated content enrichment (summary/themes/tone/suggested reply), not a human investigation. **Why N/A:** appreciations are positive recognition, not problems requiring root-cause analysis.
|
|
|
|
---
|
|
|
|
## 9. Communication Flow — notifications
|
|
|
|
### 9.1 On reaching SENT — `post_save` cascade
|
|
`signals.handle_appreciation_sent` (`signals.py:27`) fires when `status == SENT and not notification_sent`. It calls:
|
|
- **`send_appreciation_notification`** (`signals.py:49`): builds HTML email ("Staff Appreciation"); resolves recipient email/phone via `get_recipient_email`/`get_recipient_phone` (`models.py:287`); sends email + SMS; sender display "Anonymous" if `is_anonymous`; calls `_send_department_head_notification` (`signals.py:140`) emailing `department.manager` + `Staff(department=dept, is_head=True)`; calls `_send_cc_notifications` (`signals.py:203`); sets `notification_sent=True`/`notification_sent_at`.
|
|
- **`update_appreciation_stats`** (`signals.py:227`) — §12.
|
|
- **`check_and_award_badges`** (`signals.py:306`) — §Gamification.
|
|
|
|
### 9.2 The `send_appreciation_notifications` Celery task
|
|
`tasks.py:9` (`@shared_task`) — a **separate, richer** notification path used by the UI `appreciation_send_to` department branch (`ui_views.py:442`). Requires a `department`; `get_champion_and_manager(department)`; per target sends email + SMS; if `staff_id` provided, emails/SMS the Staff *"You've Been Appreciated!"*.
|
|
|
|
> **⚠ Two notification codepaths** — `signals.py` (sync, fires on every SENT) and `tasks.py` (async, fires only from UI `send_to` department branch). Both can run for the same appreciation if sent via `send_to`, potentially double-notifying.
|
|
|
|
### 9.3 On creation of a public/external DRAFT
|
|
`notify_staff_new_item("appreciation", id)` emails PX admins/on-call roster (`complaints/tasks.py:2489`), template `emails/new_appreciation_notification.html`.
|
|
|
|
### 9.4 On badge award
|
|
`signals.check_and_award_badges` emails the recipient user *"You earned a badge: <name>!"* (`signals.py:357`).
|
|
|
|
---
|
|
|
|
## 10. Escalation Flow — N/A
|
|
|
|
**No SLA timers, no escalation levels, no overdue fields** in `Appreciation` (`models.py:110-237`). No celery beat task references appreciation SLA. **Why N/A:** appreciation is non-urgent recognition; urgency/SLA semantics belong to complaints.
|
|
|
|
---
|
|
|
|
## 11. Resolution Process — "acknowledgment"
|
|
|
|
The analog of resolution is the **acknowledge** transition (SENT→ACKNOWLEDGED).
|
|
- Model method: `acknowledge()` (`models.py:344`) — sets `status=ACKNOWLEDGED`, `acknowledged_at=now()`.
|
|
- UI: `appreciation_acknowledge` (`ui_views.py:485`) — POST; **only the recipient** (matched by User ContentType + id, `ui_views.py:492`); rejects if not SENT (`ui_views.py:500`).
|
|
- REST: `AppreciationViewSet.acknowledge` (`views.py:210`) — same recipient check, HTTP 403 otherwise.
|
|
|
|
"Acknowledgment" = the recipient has confirmed/thanked — it is the **recipient's terminal action**. There is **no sender-side resolution**.
|
|
|
|
---
|
|
|
|
## 12. Closure Process
|
|
|
|
### 12.1 Terminal state & reopen
|
|
ACKNOWLEDGED is terminal: `VALID_APPRECIATION_TRANSITIONS[ACKNOWLEDGED] = set()` (`models.py:33`), and `acknowledge()` leaves no forward method. There is **no reopen** action/view anywhere. Once ACKNOWLEDGED, permanently complete.
|
|
|
|
### 12.2 Soft delete / restore
|
|
`Appreciation` extends `SoftDeleteModel` (`models.py:110`); soft-deletable + restorable via `appreciation_restore` (`ui_views.py:1036`, uses `Appreciation.all_objects` + `restore()`). Restore does not change status.
|
|
|
|
### 12.3 Statistics aggregation
|
|
`AppreciationStats` (`models.py:476`) — **monthly per recipient**. Unique key `(recipient_content_type, recipient_object_id, year, month)` (`models.py:517`). Fields: `received_count`, `sent_count`, `acknowledged_count`, `hospital_rank`, `department_rank`, `category_breakdown` (JSON). Aggregated in `signals.update_appreciation_stats` (`signals.py:227`) when an appreciation reaches SENT: `get_or_create` the month row; atomically increment counts via `F('...') + 1`; update category JSON; `recalculate_rankings` (`signals.py:269`) re-numerates ranks by `-received_count`.
|
|
|
|
### 12.4 Leaderboard
|
|
`LeaderboardView` (`views.py:445`): reads `year`/`month` (defaults current month); filters `AppreciationStats` by hospital; enumerates rank; attaches earned badges. UI equivalent `leaderboard_view` (`ui_views.py:515`).
|
|
|
|
### 12.5 Summary
|
|
`AppreciationViewSet.summary` (`views.py:277`) and `appreciation_summary_ajax` (`ui_views.py:1150`) compute total/this-month received & sent, badges earned, top category, hospital rank.
|
|
|
|
---
|
|
|
|
## 13. Exception Flows
|
|
|
|
### 13.1 Complaint → Appreciation conversion
|
|
`complaints/views.py:2076`. Guards: `is_active_status`, `complaint_type == "appreciation"`, `metadata.appreciation_id` absent. Default category "Patient Feedback Appreciation" (code `patient_feedback`).
|
|
|
|
### 13.2 ⚠ INCONSISTENCY (likely bug): conversion and REST create call `send()` on DRAFT
|
|
- Conversion: creates `status=DRAFT` (`complaints/views.py:2171`) then `appreciation.send()` (`:2183`).
|
|
- REST create: creates DRAFT then `appreciation.send()` (`views.py:198`).
|
|
- `send()` (`models.py:335`) requires ACTIVATED/AI_ANALYZED, else `ValueError`.
|
|
|
|
**Both paths would raise `ValueError` at runtime.** Neither wraps `send()` in try/except. This strongly indicates the **primary, exercised path is the UI activation-gate flow**, and these two callers are out of sync with the gate model.
|
|
|
|
### 13.3 Draft abandonment
|
|
A DRAFT has no expiry task and no cleanup. Remains queryable as DRAFT (counted in `core/context_processors.py:79`, `ui_views.py:71`). No auto-close/activation.
|
|
|
|
### 13.4 Visibility changes
|
|
`visibility` is a plain CharField (`models.py:190`) with no transition guard — editable at any status via admin or direct mutation. Public/external forced PUBLIC at creation, not otherwise change-protected.
|
|
|
|
### 13.5 Badge revocation
|
|
**No revoke/un-award path.** `UserBadge` (`models.py:432`) has only create + read. `badge_delete` (`ui_views.py:1004`) refuses to delete a badge with any `UserBadge` (`ui_views.py:1018`) — earned badges are protected. A badge, once awarded, persists.
|
|
|
|
### 13.6 Duplicate appreciation
|
|
**No deduplication** on creation. The only dedup is per-badge-award (`signals.py:326`) and per-stats-month (`unique_together` `models.py:517`, guarded by `get_or_create`).
|
|
|
|
### 13.7 Restore from soft-delete
|
|
`appreciation_restore` (`ui_views.py:1036`) — PX/Hosp-Admin only; redirects to `config:deleted_items`.
|
|
|
|
### 13.8 AI-failure handling
|
|
In `appreciation_activate`, an AI exception is logged but swallowed (`ui_views.py:255`) — appreciation stays ACTIVATED and is still sendable. At infra level, `AIService._notify_ai_failure` (`ai_service.py:172`) emails PX admins (debounced hourly) on HTTP 401/402.
|
|
|
|
---
|
|
|
|
## 14. Department-Response Sub-Flow — NOT used (confirmed)
|
|
|
|
Appreciations **do not** use the inbound send-to-department → department-responds → resolve cycle. There is no `responded`/`department_action` status, no department-side response model, no SLA on department reply. The `send_to` step (`ui_views.py:320`) is **outbound only** — it delivers the appreciation to a department's champion+manager+staff and marks the appreciation SENT; the department does not "respond back" through the system (their only return action is the recipient's `acknowledge`).
|
|
|
|
**Why:** this module is outbound recognition. The "send to department" verb here means *deliver recognition to that department*, not *open a case for the department to handle*.
|
|
|
|
---
|
|
|
|
## 15. End-to-End Example
|
|
|
|
```
|
|
Patient submits appreciation via public form (mentions Nurse A)
|
|
↓ (ui_views.py:1196) → status=DRAFT, ref APR-202607-0001,
|
|
visibility=PUBLIC, PX admins notified
|
|
PX staff reviews + activates
|
|
↓ (ui_views.py:193) → status=ACTIVATED, activated_at/by
|
|
AI analysis runs synchronously
|
|
[DECISION: AI succeeds → AI_ANALYZED, or stays ACTIVATED]
|
|
↓ (ui_views.py:230) → ai_analysis JSON stored (summary/themes/tone/suggested reply)
|
|
PX sends to Department X (target Nurse A / champion / manager)
|
|
↓ (ui_views.py:320) → recipient=Staff(Nurse A), status=SENT, sent_at
|
|
[DECISION: visibility PRIVATE/DEPARTMENT/HOSPITAL/PUBLIC]
|
|
post_save cascade fires:
|
|
- email+SMS to recipient ("Staff Appreciation")
|
|
- email department manager + dept heads
|
|
- cc notifications
|
|
- update_appreciation_stats (received/sent count, rank)
|
|
- check_and_award_badges (e.g. "First Appreciation")
|
|
→ badge email to recipient
|
|
Nurse A (the recipient) acknowledges
|
|
↓ (ui_views.py:485) → status=ACKNOWLEDGED, acknowledged_at
|
|
[TERMINAL — no reopen, no further actions]
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix A — Gamification (Badges)
|
|
|
|
### A.1 `AppreciationBadge` (`models.py:360`) — definition
|
|
Fields: `hospital` (null = system-wide), `code` (unique), bilingual `name_en/ar`/`description_en/ar`, `icon`, `color`, `order`. **`criteria_type`** (`models.py:389`): `received_count`, `received_month`, `streak_weeks`, `diverse_senders`. `criteria_value` (int threshold). `is_active`.
|
|
|
|
### A.2 Seed data (`seed_appreciation_data.py`)
|
|
**Categories:** `excellent_care`, `team_player`, `innovation`, `leadership`, `mentorship`, `going_extra_mile`, `reliability`, `positive_attitude`.
|
|
|
|
**Badges** (seed file is authoritative — `README.md` differs):
|
|
|
|
| code | name_en | criteria_type | value |
|
|
|---|---|---|---|
|
|
| first_appreciation | First Appreciation | received_count | 1 |
|
|
| appreciated_5 | Rising Star | received_count | 5 |
|
|
| appreciated_10 | Shining Star | received_count | 10 |
|
|
| appreciated_25 | Super Star | received_count | 25 |
|
|
| appreciated_50 | Legendary | received_count | 50 |
|
|
| monthly_champion | Monthly Champion | received_month | 10 |
|
|
| streak_4_weeks | Consistent | streak_weeks | 4 |
|
|
| diverse_appreciation | Well-Loved | diverse_senders | 10 |
|
|
|
|
### A.3 `UserBadge` (`models.py:432`) — award record
|
|
GenericFK recipient, FK `badge`, `earned_at` (`auto_now_add`), `appreciation_count` (count-at-award), `metadata` JSON. No unique constraint — awarder dedups manually (`signals.py:326`).
|
|
|
|
### A.4 Awarding logic — `signals.check_and_award_badges` (`signals.py:306`)
|
|
Triggered from `handle_appreciation_sent` when SENT. Iterates active badges for the hospital/system-wide; skip if already earned; `check_badge_criteria` (`signals.py:383`): `received_count` (total SENT to recipient), `received_month` (this month), `streak_weeks` (`check_appreciation_streak` `signals.py:498` — Mon-start weeks; gap breaks streak), `diverse_senders` (distinct senders). If qualifies: `UserBadge.objects.create`, email recipient.
|
|
|
|
### A.5 Badge progress UI — `my_badges_view` (`ui_views.py:580`)
|
|
Computes per-badge progress % toward threshold. Template `appreciation/my_badges.html`.
|
|
|
|
---
|
|
|
|
## Appendix B — Flagged gaps / ambiguities
|
|
|
|
1. **`VALID_APPRECIATION_TRANSITIONS` is dead code** — defined but never enforced via lookup; transitions rely solely on per-method `ValueError`. Constant and methods agree today but nothing keeps them in sync.
|
|
2. **REST `create` and complaint conversion call `send()` on DRAFT** — would raise `ValueError` (`views.py:198`, `complaints/views.py:2183` vs guard `models.py:335`). Likely bug/stale code; UI activation flow is the working path.
|
|
3. **`AppreciationForm` does not capture recipient/category/visibility** — decided later (at send-time for UI path). An internally-created DRAFT has no recipient until `send_to` runs.
|
|
4. **Two notification codepaths** — `signals.py` (sync, every SENT) and `tasks.py` (async, UI `send_to` dept branch). Potential double-notification.
|
|
5. **No badge revocation, no reopen, no draft expiry** — by design.
|
|
6. **README badge list vs seed file badge list differ** — seed file is authoritative.
|
|
7. **`get_owner()` (`models.py:250`) defined but not invoked** in the appreciation app — verify external callers.
|