- 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>
259 lines
9.8 KiB
Markdown
259 lines
9.8 KiB
Markdown
# 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 1–2 |
|
||
| 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 8–9 |
|
||
| 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 |
|