From a560b0936c6832c94426ba10c9f6b53f7bdd35c5 Mon Sep 17 00:00:00 2001 From: ramadhan sjamsani Date: Mon, 27 Apr 2026 14:09:19 +0800 Subject: [PATCH] PRDs: phase3.5 (mitra chat request history) + phase3.6 (force-close re-enable) Phase 3.5: replace _PendingRequestsBanner with a Riwayat Permintaan card on the mitra home, plus a screen listing the last 20 entries from chat_request_notifications. Backend endpoint TBD. Phase 3.6: plan to re-enable mitra force-close (Akhiri) once the moderation / accountability story is in place. Backend route and config flag are already preserved from Phase 3. Co-Authored-By: Claude Opus 4.7 (1M context) --- requirement/phase3.5.md | 182 ++++++++++++++++++++++++++++++++++++++++ requirement/phase3.6.md | 57 +++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 requirement/phase3.5.md create mode 100644 requirement/phase3.6.md diff --git a/requirement/phase3.5.md b/requirement/phase3.5.md new file mode 100644 index 0000000..246eb3a --- /dev/null +++ b/requirement/phase3.5.md @@ -0,0 +1,182 @@ +# PRD: Mitra Chat Request History + +# Overview + +**Goal:** Replace the home-screen "Pending Requests" banner on the mitra app with a single **"Riwayat Permintaan"** CTA that opens a list of the mitra's last 20 chat requests (regardless of status), and surfaces a count badge on the CTA whenever there are pending requests waiting for a response. + +**Success looks like:** A mitra can open one screen from the home and see, at a glance, every chat request that was blasted to them recently — who requested, when, the topic flag, and what happened (accepted / declined / missed / ignored, or still pending). Pending requests appear at the top, can be acted on directly from this screen, and are visually obvious from the home thanks to a count badge on the CTA. + +**Affects:** `mitra_app`, `backend` + +## Background + +- The mitra home today shows a `_PendingRequestsBanner` Card ([mitra_app/lib/features/home/home_screen.dart:128](mitra_app/lib/features/home/home_screen.dart#L128)) that displays the live count of pending requests and re-opens the incoming overlay on tap. +- The backend already stores a per-mitra log row in `chat_request_notifications` for every blast, with `response` ∈ {`accepted`, `declined`, `missed`, `ignored`, `NULL`}. +- That log is currently exposed only to the control center (`/internal/mitra-activity/log`); the mitra app sees only currently-pending pings via `GET /api/mitra/chat/pending`. +- The system-wide "ringing phone" overlay that auto-pops on every incoming `CHAT_REQUEST` WebSocket message is **out of scope** for this change and stays as-is. + +--- + +# Functional Requirement + +## 1. Home Screen CTA + +### Replaces +- The existing `_PendingRequestsBanner` Card is removed. +- A new **"Riwayat Permintaan"** Card replaces it in the same slot. + +### Appearance +- Card style consistent with the existing `Sesi Aktif` and `Riwayat Chat` Cards. +- **Title:** "Riwayat Permintaan" +- **Subtitle (no pending):** "Lihat permintaan chat sebelumnya" +- **Subtitle (with pending):** "{N} permintaan baru" — same copy as today's banner. +- **Trailing:** chevron (`>`) plus the badge below. + +### Badge +- A red **count** badge (matches existing pattern, not a dot) is shown on the trailing edge whenever pending count > 0. +- Pending = `response IS NULL` for this mitra in `chat_request_notifications`, with the corresponding session still in `pending_acceptance`. +- Badge disappears as soon as that count returns to zero. + +### Tap +- Navigates to the **Chat Request History screen** (Section 2). + +--- + +## 2. Chat Request History Screen + +### Route +- New GoRouter route: `/chat/requests/history`. +- Reachable only from the home CTA. + +### Data +- Last **20** rows from `chat_request_notifications` for the calling mitra, ordered by `notified_at DESC`. +- No pagination, no filters, no search in this phase. + +### List ordering +- **Pending rows** (`response IS NULL` AND session still `pending_acceptance`) are pinned to the top, newest-first within the group. +- All other rows follow, sorted by `notified_at DESC`. + +### Row content +Each row shows: + +- **Customer call name** — always whatever the customer chose as their call sign (`call_name` / display name). If empty or if global `anonymity_enabled = false`, show **"Anonim"**. **Never display phone, email, social id, or internal user id.** +- **Sensitive topic flag** — when `topic_sensitivity = 'sensitive'`, show the same warning-yellow **"Topik sensitif"** badge used on the incoming overlay (Phase 3.3). +- **Status badge** with localized label and color: + - Pending → yellow, **"Menunggu respon"** + - Accepted → green, **"Diterima"** + - Declined → gray, **"Ditolak"** + - Missed → gray, **"Terlewat"** (another mitra accepted) + - Ignored + session `cancelled` → gray, **"Dibatalkan"** + - Ignored + session `expired` → gray, **"Kedaluwarsa"** +- **Relative timestamp** of `notified_at` — "Baru saja", "2 menit lalu", "Kemarin", "3 hari lalu", etc. + +### Empty state +- Copy: **"Belum ada permintaan chat"**. + +### Refresh +- Pull-to-refresh (Material `RefreshIndicator`) re-calls the endpoint. +- The screen does **not** auto-update when WebSocket events arrive while open. Pull-to-refresh is the only manual refresh affordance. +- The home-screen badge count, however, stays live (driven by the same WS-backed provider that powers today's banner). + +--- + +## 3. Tap Behavior on a Row + +### Pending row (`response IS NULL` AND `session_status = 'pending_acceptance'`) +- Re-opens the **existing incoming-request accept/decline overlay** for that session. +- If the request is no longer actionable by the time the overlay opens (another mitra accepted, customer cancelled, 60s expired), the overlay opens in its standard closed state ("Permintaan sudah tidak tersedia") — no special handling needed. + +### Non-pending row (any non-NULL `response`) +- Opens a **read-only detail screen** showing: + - Customer call name (or "Anonim") + - Topic sensitivity + - Status (localized label per Section 2) + - `notified_at` (absolute date/time) + - `responded_at` if present + - For `accepted` rows only: a **"Lihat percakapan"** CTA that navigates to the existing chat-history transcript screen for that `session_id`. + - For all other non-pending rows: no transcript link. + +--- + +## 4. Live Behavior + +### Incoming auto-overlay — unchanged +- When a `CHAT_REQUEST` WebSocket message arrives, the system-wide overlay continues to pop up no matter which screen the mitra is on. This remains the primary "you have a new request" UX. + +### Home CTA badge — live +- Driven by the same Riverpod provider that powers today's `_PendingRequestsBanner` count. +- Increments on `CHAT_REQUEST`, decrements on `CHAT_REQUEST_CLOSED` (accept-by-other / cancel / expiry) and on this mitra's own accept/decline. + +### History screen — pull-only +- No WS subscription. Refreshes only when the screen is opened or pulled-to-refresh. +- Race conditions on accept are handled by the server's existing atomic `UPDATE … WHERE status = 'pending_acceptance'` — a stale "pending" row that's actually been taken simply fails the accept gracefully via the existing overlay flow. + +--- + +## 5. Backend Changes + +### New route +- **`GET /api/mitra/chat/requests/recent`** (default and max `limit = 20`) +- Auth: existing mitra JWT. +- Strictly per-mitra-scoped — the calling mitra never sees other mitras' rows. + +### Response shape (per row) +```jsonc +{ + "notification_id": "uuid", + "session_id": "uuid", + "notified_at": "ISO-8601", + "responded_at": "ISO-8601 | null", + "response": "accepted | declined | missed | ignored | null", + "session_status": "pending_acceptance | active | closing | completed | cancelled | expired | ...", + "customer_call_name": "string (already masked to 'Anonim' if anonymity rule applies)", + "topic_sensitivity": "regular | sensitive" +} +``` + +### Implementation +- Reuse the SQL shape of `getMitraActivityLog` ([backend/src/services/mitra-activity.service.js:6-40](backend/src/services/mitra-activity.service.js#L6-L40)), scoped to `WHERE crn.mitra_id = `, `ORDER BY crn.notified_at DESC`, `LIMIT 20`. +- Apply the anonymity mask **server-side** so the mitra app never has to know the raw value: if `app_config.anonymity_enabled = false` OR the customer's call name is empty, return `"Anonim"`; never return phone, email, social id, or user id under any circumstance. + +### No new tables, columns, or migrations. + +--- + +## 6. Mitra App Changes + +| Item | Detail | +|---|---| +| Removed | `_PendingRequestsBanner` Card in `home_screen.dart` | +| Added | `_RequestHistoryButton` Card in `home_screen.dart`, with count badge | +| Added | Chat Request History screen — `features/chat/screens/request_history_screen.dart` | +| Added | Read-only detail screen — `features/chat/screens/request_history_detail_screen.dart` | +| Reused | Incoming accept/decline overlay; existing chat-history transcript screen | +| Routes | `/chat/requests/history`, `/chat/requests/history/:notificationId` | +| Providers | New `requestHistoryProvider` (list state); reuses `chatRequestProvider` for the live badge count | + +--- + +## 7. Edge Cases + +- **Stale pending row tapped after the request is gone** — overlay handles via existing closed-state UX. +- **Anonymity flipped mid-fetch** — display rule is evaluated server-side at fetch time. Open lists do not retroactively update; next fetch reflects the new value. +- **More than 20 rows** — older rows simply don't appear in this view. The control center retains full history. +- **Mitra has zero notifications** — empty state per Section 2. +- **Mitra offline** — list shows last fetched data (or empty if never fetched); pull-to-refresh required to recover. + +--- + +## 8. Non-Goals (this phase) + +- Pagination, infinite scroll, "load more" beyond 20 rows. +- Filters by status / date / topic. +- Search. +- Push notification when a non-current request changes status. +- Aggregated stats (acceptance rate, avg response time) — those stay in the control center. +- Per-mitra notification preferences. + +--- + +# Open Questions + +_None — ready for plan document._ diff --git a/requirement/phase3.6.md b/requirement/phase3.6.md new file mode 100644 index 0000000..27a191d --- /dev/null +++ b/requirement/phase3.6.md @@ -0,0 +1,57 @@ +# PRD: Mitra Force-Close — Re-enable Plan + +# Overview + +**Goal:** Restore the mitra-side ability to force-close (early-end) an active session, gated behind the existing backend `early_end_mitra_enabled` config flag. + +**Status:** Currently **disabled in the mitra app** (button hidden). Backend support already exists and is gated by `app_config.early_end_mitra_enabled` (default `false`). + +**Affects:** `mitra_app`, `control_center` (config UI), backend (no functional change — already implemented) + +## Background + +- Backend has `initiateEarlyEnd(sessionId, UserType.MITRA)` ([closure.service.js:88-137](backend/src/services/closure.service.js#L88-L137)) which moves an `active`/`extending` session to `closing` (with the 5-min grace + goodbye composer flow), guarded by the `early_end_mitra_enabled` flag. +- The mitra app previously surfaced an **"Akhiri"** button on the **Sesi Aktif** screen ([mitra_app/lib/features/chat/screens/active_sessions_screen.dart](mitra_app/lib/features/chat/screens/active_sessions_screen.dart)) which called `POST /api/mitra/chat-requests/sessions/:id/end`. +- The button (and the surrounding confirmation dialog) was **removed** during testing because the UX was abruptly terminating sessions without enough guardrails. The endpoint and its config flag were intentionally left in place on the backend. + +--- + +# Functional Requirement + +## 1. Visibility + +- The "Akhiri" CTA on **Sesi Aktif** is rendered **only when** `app_config.early_end_mitra_enabled = true` (read at app startup via `/api/shared/config/...` or whichever config-fetch path the mitra app already uses). +- When the flag is `false`, the row's trailing affordance is the chevron-only state (current state after this round of cleanup). + +## 2. Confirmation UX + +When re-enabling, restore the existing confirmation dialog: +- **Title:** "Akhiri Sesi?" +- **Body:** "Apakah kamu yakin ingin mengakhiri sesi ini?" +- **Buttons:** Batal / Ya, Akhiri. + +Open question for the next phase: do we want to require a **reason** from the mitra (free-text or radio list — e.g., "Customer abusive", "Customer left", "Out of scope")? If yes, capture it in `session_closures.reason` (new column) for downstream QC/auto-flag analytics. + +## 3. Backend + +- No new endpoint or service work required — `initiateEarlyEnd` already handles this. +- If a reason field is added (Section 2 open question), extend the request body and persist it. + +## 4. Control Center + +- The existing config screen already exposes the toggle; no UI change needed unless reason capture is added. + +--- + +## Non-Goals (this phase) + +- Adding a force-close from the chat screen (only Sesi Aktif). +- Auto-flagging mitras with high force-close counts (covered by separate "Mitra QC Auto-Flag" memory item). +- Customer-side force-close (separate `early_end_customer_enabled` flag, not in scope here). + +--- + +# Open Questions + +- Capture a reason on force-close? (Recommended yes — feeds into QC.) +- Should there be a per-day cap on mitra force-closes before triggering CC review?