Files
halobestie-clone/requirement/phase3.2-plan.md
ramadhan sjamsani 4158fb9432 Phase 3.2 docs + Phase 3.1 testing fixes
- Add phase3.2.md requirement: overlay UX, mitra activity log
- Add phase3.2-plan.md implementation plan
- Fix stale request validation: add GET /:sessionId/status endpoint
- Fix notification tap flow: setIncomingFromNotification + onChatRequestTapped
- IncomingRequestSheet shows stale message instead of auto-dismiss
- Home screen validates on resume, shows immediately on fresh WS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:09:25 +08:00

259 lines
9.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 3.2 Implementation Plan: Incoming Chat Request Overlay & Mitra Request Activity Log
## Summary of Clarified Requirements
| Topic | Decision |
|---|---|
| Work stream order | Overlay (mitra_app) and Activity Log (backend + CC) can be parallelized |
| Overlay approach | `Stack` wrapping `MaterialApp.router` in `main.dart` + `AnimatedPositioned` |
| Overlay state source | Watches `chatRequestProvider` exclusively — no per-page listeners |
| Multiple requests | Queued in notifier. Show one at a time. Next appears when current is resolved. |
| Swipe down behavior | Dismiss = ignore (no reject sent to backend) |
| Stale request messages | Must show specific reason: cancelled, accepted by other, expired |
| Stale auto-dismiss | No auto-dismiss — mitra must acknowledge by tapping OK or swiping |
| Background dimming | Partially dimmed to get mitra's attention |
| `missed` vs `ignored` | Backend must distinguish: `missed` = another mitra accepted; `ignored` = 60s timeout |
| `active_session_count` | Recorded at notification creation time (snapshot of mitra load) |
| `response_time_seconds` | Calculated column, not stored (`responded_at - notified_at`) |
| Control center page | New `/mitra-activity` page with acceptance rate, avg response time, filters |
| Mitra QC auto-flag | Out of scope for this phase (tracked in memory for future) |
---
## Work Stream 1: Incoming Chat Request Overlay (mitra_app)
### 1.1 Backend: Add `reason` Field to `chat_request_closed` WebSocket Message
The current `chat_request_closed` message is sent identically for three different scenarios. The overlay must show a specific message for each case.
**File:** `backend/src/services/pairing.service.js`
**Change 1 — `acceptPairingRequest`:** When notifying other mitras, add `reason: 'accepted_by_other'`
**Change 2 — `cancelPairingRequest`:** Add `reason: 'cancelled_by_customer'`
**Change 3 — `expirePairingRequest`:** Add `reason: 'expired'`
### 1.2 Backend: Include Session Metadata in `chat_request` WS Message
**File:** `backend/src/services/pairing.service.js`
Add `duration_minutes` and `is_free_trial` to the `notifyMitra` call in `createPairingRequest`.
### 1.3 ChatRequestNotifier: Queue Support and Stale Reason
**File:** `mitra_app/lib/core/chat/chat_request_notifier.dart`
#### New State Classes
```dart
class ChatRequestIncomingData extends ChatRequestData {
final String sessionId;
final int? durationMinutes;
final DateTime? createdAt;
const ChatRequestIncomingData(this.sessionId, {this.durationMinutes, this.createdAt});
}
class ChatRequestStaleData extends ChatRequestData {
final String sessionId;
final StaleReason reason;
const ChatRequestStaleData(this.sessionId, this.reason);
}
enum StaleReason {
cancelledByCustomer, // "Permintaan dibatalkan oleh customer"
acceptedByOther, // "Permintaan diterima oleh Bestie lain"
expired, // "Permintaan kedaluwarsa"
}
```
#### Queue Implementation
Add `List<String> _pendingQueue` field:
- New request arrives while showing another → add to queue
- `chat_request_closed` for queued request → remove silently from queue
- `chat_request_closed` for displayed request → transition to `ChatRequestStaleData`
#### New Methods
- `ignore()` — swipe down on active request, advance queue
- `acknowledgeStale()` — OK/swipe on stale message, advance queue
### 1.4 Overlay Widget: `ChatRequestOverlay`
**New file:** `mitra_app/lib/core/chat/widgets/chat_request_overlay.dart`
A `ConsumerStatefulWidget` wrapping the app that manages the overlay:
- **Layout:** `Stack` → app child + positioned overlay at bottom + semi-transparent dim layer
- **Animation:** `SlideTransition` from `Offset(0, 1)` to `Offset(0, 0)` for bottom-up slide
- **Swipe-to-dismiss:** `GestureDetector` with `onVerticalDragEnd` detecting downward swipe
- **State listening:** `ref.listen(chatRequestProvider)` to show/hide:
- `ChatRequestIncomingData` → show overlay with accept/reject/swipe
- `ChatRequestStaleData` → show overlay with reason message + OK button
- `ChatRequestAcceptedData` → hide overlay, navigate to chat
- Any other state → hide overlay
**Active request content:**
```
[Chat icon]
"Ada permintaan chat baru!"
Durasi: 30 Menit
[Tolak] [Terima]
(swipe down to close)
```
**Stale request content:**
```
[Info icon]
"Permintaan dibatalkan oleh customer"
[OK]
```
**Navigation:** Uses `ref.read(routerProvider)` for `GoRouter` access (overlay sits above the router).
### 1.5 App Root Changes
**File:** `mitra_app/lib/main.dart`
Wrap `MaterialApp.router` with `ChatRequestOverlay`:
```dart
return ChatRequestOverlay(
child: MaterialApp.router(
title: 'Halo Bestie Mitra',
routerConfig: router,
),
);
```
### 1.6 Cleanup Old Bottom Sheet Code
**File:** `mitra_app/lib/features/home/home_screen.dart`
- Remove `_showIncomingRequest` method
- Remove `didChangeAppLifecycleState` incoming request check
- Remove `ref.listen(chatRequestProvider, ...)` block for bottom sheet + navigation
- Remove `IncomingRequestSheet` import
- Convert from `ConsumerStatefulWidget` to `ConsumerWidget` (no lifecycle observer needed)
**Delete:** `mitra_app/lib/features/chat/widgets/incoming_request_sheet.dart`
---
## Work Stream 2: Mitra Request Activity Log (Backend + CC)
### 2.1 Database Migration
**File:** `backend/src/db/migrate.js`
```sql
ALTER TABLE chat_request_notifications
ADD COLUMN IF NOT EXISTS active_session_count INT NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_chat_request_notifications_mitra_notified
ON chat_request_notifications (mitra_id, notified_at);
```
### 2.2 Constants: Add `MISSED` Response Type
**File:** `backend/src/constants.js`
Add `MISSED: 'missed'` to `NotificationResponse`.
### 2.3 Pairing Service Updates
**File:** `backend/src/services/pairing.service.js`
- `createPairingRequest`: record `active_session_count` when creating notification rows
- `acceptPairingRequest`: change `IGNORED``MISSED` when marking other mitras' notifications
- `expirePairingRequest`: keep `IGNORED` (correct — 60s timeout)
### 2.4 New Service: Mitra Activity
**New file:** `backend/src/services/mitra-activity.service.js`
- `getMitraActivityLog({ mitra_id, date_from, date_to, page, limit })` — paginated detail log
- `getMitraActivitySummary({ mitra_id, date_from, date_to })` — per-mitra aggregates: total, accepted, rejected, missed, ignored, acceptance rate %, avg response time
### 2.5 New Internal Routes
**New file:** `backend/src/routes/internal/mitra-activity.routes.js`
| Method | Path | Purpose |
|---|---|---|
| `GET` | `/internal/mitra-activity/log` | Paginated detail log |
| `GET` | `/internal/mitra-activity/summary` | Per-mitra summary stats |
Register in `backend/src/app.internal.js`.
### 2.6 Control Center: Mitra Activity Page
**New file:** `control_center/src/pages/mitra-activity/MitraActivityPage.jsx`
**Filters:** Date range (from/to), mitra dropdown (optional)
**Summary table columns:**
| Mitra | Total | Accepted | Rejected | Missed | Ignored | Rate (%) | Avg Response (s) |
**Detail log table columns:**
| Mitra | Session | Response | Response Time | Active Sessions | Notified At | Responded At |
Response values color-coded: `accepted` green, `rejected` red, `missed` orange, `ignored` grey.
Register route in `App.jsx`, add nav link "Aktivitas Mitra" in `Layout.jsx`.
---
## 3. Implementation Order
| Step | What | Apps Affected | Dependencies |
|---|---|---|---|
| **Work Stream 1: Overlay** | | | |
| 1 | Backend: add `reason` to `chat_request_closed` WS messages | Backend | None |
| 2 | Backend: include `duration_minutes` in `chat_request` WS message | Backend | None |
| 3 | ChatRequestNotifier: add `ChatRequestStaleData`, `StaleReason`, queue, `ignore()`, `acknowledgeStale()` | mitra_app | Steps 12 |
| 4 | Create `ChatRequestOverlay` widget | mitra_app | Step 3 |
| 5 | Integrate overlay into `main.dart` | mitra_app | Step 4 |
| 6 | Cleanup: remove bottom sheet code from home screen, delete `IncomingRequestSheet` | mitra_app | Step 5 |
| 7 | E2E test overlay flows | mitra_app | Step 6 |
| **Work Stream 2: Activity Log** | | | |
| 8 | DB migration: `active_session_count` column + index | Backend | None |
| 9 | Constants: add `MISSED` to `NotificationResponse` | Backend | None |
| 10 | Pairing service: record `active_session_count`, use `MISSED` | Backend | Steps 89 |
| 11 | New `mitra-activity.service.js` | Backend | Step 10 |
| 12 | New `mitra-activity.routes.js` + register | Backend | Step 11 |
| 13 | Control center: `MitraActivityPage.jsx` | Control center | Step 12 |
| 14 | Control center: register route + nav link | Control center | Step 13 |
| 15 | E2E test activity log + summary | All | Step 14 |
---
## 4. New Files
| File | Purpose |
|---|---|
| `mitra_app/lib/core/chat/widgets/chat_request_overlay.dart` | App-wide overlay for incoming chat requests |
| `backend/src/services/mitra-activity.service.js` | Mitra activity log query functions |
| `backend/src/routes/internal/mitra-activity.routes.js` | Internal API routes for activity data |
| `control_center/src/pages/mitra-activity/MitraActivityPage.jsx` | CC mitra activity page |
## 5. Deleted Files
| File | Reason |
|---|---|
| `mitra_app/lib/features/chat/widgets/incoming_request_sheet.dart` | Replaced by `ChatRequestOverlay` |
---
## 6. Risks & Mitigations
| Risk | Mitigation |
|---|---|
| Overlay wrapping `MaterialApp.router` can't use `GoRouter.of(context)` | Use `ref.read(routerProvider)` directly |
| Swipe gesture conflicts with overlay content | Overlay content is short (non-scrollable); wrap only drag-handle area |
| Multiple rapid WS messages cause queue issues | Queue ops are synchronous in Dart's single-threaded event loop |
| `chat_request_closed` arrives during slide-up animation | Transition to stale state immediately; animation handles it smoothly |
| Old `ignored` values in DB now ambiguous | Only new requests post-deploy get correct `missed` values; document in CC |