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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { authenticate } from '../../plugins/auth.js'
|
||||
import { getMitraByFirebaseUid } from '../../services/mitra.service.js'
|
||||
import { acceptPairingRequest, declinePairingRequest } from '../../services/pairing.service.js'
|
||||
import { acceptPairingRequest, declinePairingRequest, getSessionStatus } from '../../services/pairing.service.js'
|
||||
import { getActiveSessionsByMitra, getActiveSessionsByMitraWithUnread, endSession, getMitraHistory } from '../../services/session.service.js'
|
||||
import { respondToExtension } from '../../services/extension.service.js'
|
||||
import { EndedBy } from '../../constants.js'
|
||||
@@ -23,6 +23,15 @@ const resolveMitra = async (request, reply) => {
|
||||
}
|
||||
|
||||
export const mitraChatRoutes = async (app) => {
|
||||
// Check if a session is still pending acceptance (for notification validation)
|
||||
app.get('/:sessionId/status', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
||||
const session = await getSessionStatus(request.params.sessionId)
|
||||
if (!session) {
|
||||
return reply.code(404).send({ success: false, error: { code: 'NOT_FOUND', message: 'Session not found' } })
|
||||
}
|
||||
return reply.send({ success: true, data: { status: session.status } })
|
||||
})
|
||||
|
||||
app.post('/:sessionId/accept', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
||||
const session = await acceptPairingRequest(request.params.sessionId, request.mitra.id)
|
||||
return reply.send({ success: true, data: session })
|
||||
|
||||
@@ -130,19 +130,26 @@ class ChatRequest extends _$ChatRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when user taps a chat_request notification. Sets the incoming state
|
||||
/// with the given session and validates it's still pending.
|
||||
Future<void> setIncomingFromNotification(String sessionId) async {
|
||||
state = ChatRequestIncomingData(sessionId);
|
||||
await validateIncomingRequest();
|
||||
}
|
||||
|
||||
/// Check if the current incoming request is still valid (pending_acceptance).
|
||||
/// If stale, reset to listening state.
|
||||
Future<void> validateIncomingRequest() async {
|
||||
if (state is! ChatRequestIncomingData) return;
|
||||
final sessionId = (state as ChatRequestIncomingData).sessionId;
|
||||
try {
|
||||
final response = await _apiClient.get('/api/shared/chat/$sessionId/info');
|
||||
final response = await _apiClient.get('/api/mitra/chat-requests/$sessionId/status');
|
||||
final status = response['data']?['status'] as String?;
|
||||
if (status != 'pending_acceptance') {
|
||||
state = const ChatRequestListeningData();
|
||||
}
|
||||
} catch (_) {
|
||||
state = const ChatRequestListeningData();
|
||||
// On error, keep current state — don't dismiss valid requests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ class NotificationService {
|
||||
static final _localNotifications = FlutterLocalNotificationsPlugin();
|
||||
static GoRouter? _router;
|
||||
|
||||
/// Callback for when a chat request notification is tapped.
|
||||
/// Set this from the app to bridge notifications → Riverpod state.
|
||||
static void Function(String sessionId)? onChatRequestTapped;
|
||||
|
||||
static const _channel = AndroidNotificationChannel(
|
||||
'chat_messages',
|
||||
'Chat Messages',
|
||||
@@ -119,8 +123,9 @@ class NotificationService {
|
||||
final type = data['type'] as String?;
|
||||
final action = data['action'] as String?;
|
||||
|
||||
if (type == 'chat_request' && action == 'open_accept') {
|
||||
// Navigate to home where incoming request sheet will show
|
||||
if (type == 'chat_request' && action == 'open_accept' && sessionId != null) {
|
||||
// Update the notifier state with this session, then navigate
|
||||
onChatRequestTapped?.call(sessionId);
|
||||
_router!.go('/home');
|
||||
} else if (type == 'session_closing' || type == 'session_expired') {
|
||||
// Navigate to the chat session closure screen
|
||||
|
||||
@@ -68,16 +68,10 @@ class _HomeScreenState extends ConsumerState<HomeScreen> with WidgetsBindingObse
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for incoming chat requests
|
||||
// Listen for incoming chat requests — fresh from WS, show immediately
|
||||
ref.listen(chatRequestProvider, (prev, next) {
|
||||
if (next is ChatRequestIncomingData) {
|
||||
// Validate request is still pending before showing sheet
|
||||
ref.read(chatRequestProvider.notifier).validateIncomingRequest().then((_) {
|
||||
final current = ref.read(chatRequestProvider);
|
||||
if (current is ChatRequestIncomingData) {
|
||||
_showIncomingRequest(current.sessionId);
|
||||
}
|
||||
});
|
||||
_showIncomingRequest(next.sessionId);
|
||||
} else if (next is ChatRequestAcceptedData) {
|
||||
final session = next.session;
|
||||
final sessionId = session['session_id'] as String? ?? session['id'] as String;
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'core/api/api_client_provider.dart';
|
||||
import 'core/auth/auth_notifier.dart';
|
||||
import 'core/status/status_notifier.dart';
|
||||
import 'core/chat/chat_request_notifier.dart';
|
||||
import 'core/notifications/notification_service.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'router.dart';
|
||||
@@ -78,6 +79,9 @@ class _AppState extends ConsumerState<App> with WidgetsBindingObserver {
|
||||
|
||||
final router = ref.watch(routerProvider);
|
||||
NotificationService.initialize(router);
|
||||
NotificationService.onChatRequestTapped = (sessionId) {
|
||||
ref.read(chatRequestProvider.notifier).setIncomingFromNotification(sessionId);
|
||||
};
|
||||
|
||||
return MaterialApp.router(
|
||||
title: 'Halo Bestie Mitra',
|
||||
|
||||
258
requirement/phase3.2-plan.md
Normal file
258
requirement/phase3.2-plan.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# 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 |
|
||||
137
requirement/phase3.2.md
Normal file
137
requirement/phase3.2.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# PRD: Mitra Chat Request Overlay & Pairing UX
|
||||
|
||||
# Overview
|
||||
|
||||
**Goal:** Reliable incoming chat request experience for Mitra — always visible, non-blocking, works in all app states (foreground, background, killed)
|
||||
|
||||
**Success looks like:** When a customer requests a chat, the mitra is always notified and can accept/reject without leaving their current screen. The notification works regardless of whether the app is in foreground, background, or opened from a push notification.
|
||||
|
||||
## Background
|
||||
- Current bottom sheet approach (`showModalBottomSheet`) fails silently when the app is backgrounded
|
||||
- Flutter UI cannot render when the app is not in foreground
|
||||
- Push notifications work in background but the in-app response (bottom sheet) doesn't show reliably when the user taps the notification
|
||||
- The state change from WebSocket gets "consumed" by listeners while backgrounded, so returning to foreground shows stale data
|
||||
- Need a solution that is non-blocking (doesn't interrupt active chat sessions) and works on any page
|
||||
|
||||
|
||||
# Functional Requirement
|
||||
|
||||
## Incoming Chat Request Overlay
|
||||
|
||||
### Trigger
|
||||
- Overlay watches `chatRequestProvider` state — it does NOT depend on any specific page or lifecycle event
|
||||
- Three sources can trigger the overlay:
|
||||
1. **WebSocket** — real-time delivery when app is in foreground
|
||||
2. **FCM notification tap** — user taps push notification, app opens, state is set from notification payload
|
||||
3. **App resume** — app returns to foreground, validates pending request with backend
|
||||
|
||||
### Appearance
|
||||
- Slides up from the bottom of the screen, like a bottom sheet
|
||||
- Rounded top corners
|
||||
- Page behind is partially dimmed to get mitra's attention
|
||||
- Shows on top of ANY page (home, chat, history, settings, etc.)
|
||||
|
||||
### Content
|
||||
- Session information (duration, etc.)
|
||||
- Accept button
|
||||
- Reject button
|
||||
- Swipe down to close (= ignore, NOT reject)
|
||||
|
||||
### Behavior
|
||||
- **Accept** → overlay dismisses, navigate to chat session with customer
|
||||
- **Reject** → overlay dismisses, mitra continues what they were doing
|
||||
- **Swipe down / close** → overlay dismisses, request is ignored (no explicit reject sent to backend)
|
||||
- **Request cancelled by customer** → overlay updates to show "Permintaan dibatalkan oleh customer"
|
||||
- **Request accepted by other mitra** → overlay updates to show "Permintaan diterima oleh Bestie lain"
|
||||
- **Request expired (60s timeout)** → overlay updates to show "Permintaan kedaluwarsa"
|
||||
- Stale messages do NOT auto-dismiss — mitra must acknowledge by tapping OK or swiping down
|
||||
|
||||
### Multiple Requests
|
||||
- Show one request at a time
|
||||
- Requests are queued — when current request is resolved (accepted/rejected/ignored/expired), next queued request appears
|
||||
- Future enhancement: dedicated `/chat-requests` page with full list
|
||||
|
||||
## Push Notification (Background/Killed)
|
||||
|
||||
### When App is Backgrounded
|
||||
- Local notification with sound + vibration (already implemented via WebSocket listener)
|
||||
- FCM push notification as fallback when WebSocket is disconnected
|
||||
|
||||
### When Notification is Tapped
|
||||
- App opens → `chatRequestProvider` state is set from notification payload (`session_id`)
|
||||
- Overlay appears with the request detail
|
||||
- Backend validation confirms request is still pending before showing accept/reject
|
||||
|
||||
### Stale Notification Handling
|
||||
- If user taps a notification for an already-resolved request, overlay shows appropriate message ("dibatalkan" / "diterima Bestie lain" / "kedaluwarsa") then auto-dismisses
|
||||
|
||||
|
||||
## Mitra Request Activity Log
|
||||
|
||||
### Goal
|
||||
Track every mitra's response to incoming chat requests for QC measurement — how many accepted, rejected, ignored (expired without action), and how many active sessions they had at the time.
|
||||
|
||||
### Logged Data
|
||||
For each incoming request delivered to a mitra, log:
|
||||
- `mitra_id` — which mitra received the request
|
||||
- `session_id` — which chat request
|
||||
- `response` — `accepted`, `rejected`, `ignored` (expired without action), `missed` (taken by other mitra before response)
|
||||
- `response_time_seconds` — how long it took the mitra to respond (null if ignored)
|
||||
- `active_session_count` — how many active sessions the mitra had at the time of the request
|
||||
- `notified_at` — when the request was delivered to the mitra
|
||||
- `responded_at` — when the mitra responded (null if ignored)
|
||||
|
||||
### When to Log
|
||||
All logging is **backend-side and event-driven** — no polling or constant monitoring needed. The frontend only triggers `accepted` and `rejected` through existing API calls.
|
||||
|
||||
| Response | Triggered by | Frontend action? |
|
||||
|---|---|---|
|
||||
| `accepted` | Backend — when mitra calls `POST /:sessionId/accept` | Yes — mitra taps Accept |
|
||||
| `rejected` | Backend — when mitra calls `POST /:sessionId/decline` | Yes — mitra taps Reject |
|
||||
| `missed` | Backend — when `acceptPairingRequest` marks other mitras' notifications (another mitra accepted first) | No — fully backend |
|
||||
| `ignored` | Backend — when `expirePairingRequest` fires after 60s timeout with no response | No — fully backend |
|
||||
|
||||
### Existing Infrastructure
|
||||
The `chat_request_notifications` table already tracks `notified_at`, `response`, `responded_at`. Current changes needed:
|
||||
- Distinguish `missed` from `ignored` (currently both stored as `ignored`)
|
||||
- Add `active_session_count` column — recorded when the notification is created
|
||||
- `response_time_seconds` can be calculated from `responded_at - notified_at` (no new column needed)
|
||||
|
||||
### Control Center
|
||||
- New dedicated page: Mitra Request Activity
|
||||
- Dashboard: mitra acceptance rate, average response time, rejection count, ignore count per mitra
|
||||
- Filter by date range and mitra
|
||||
- Auto-flagging mitras with high rejection/ignore rate is **out of scope** for this phase (planned for future)
|
||||
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Overlay Component
|
||||
- Single `OverlayEntry` managed from `main.dart`
|
||||
- Wrapped around `MaterialApp.router` in a `Stack`
|
||||
- Watches `chatRequestProvider` — shows/hides based on state
|
||||
- Uses `AnimatedPositioned` or `SlideTransition` for bottom-up animation
|
||||
- `GestureDetector` for swipe-to-dismiss
|
||||
|
||||
### State Flow
|
||||
```
|
||||
WebSocket ──→
|
||||
FCM tap ──→ chatRequestProvider ──→ Overlay shows/hides
|
||||
App resume ──→
|
||||
```
|
||||
|
||||
### No Changes Required
|
||||
- Backend pairing service (already sends WS + FCM)
|
||||
- Push notification payload (already contains session_id + type)
|
||||
- Chat request notifier (already manages state from WS + FCM)
|
||||
|
||||
### Cleanup from Phase 3.1
|
||||
- Remove `showModalBottomSheet` for incoming requests from home screen
|
||||
- Remove `IncomingRequestSheet` widget
|
||||
- Remove `didChangeAppLifecycleState` incoming request check from home screen
|
||||
|
||||
|
||||
# Tech Stack
|
||||
- Flutter `Overlay` / `OverlayEntry` or `Stack` with `AnimatedPositioned`
|
||||
- Existing Riverpod `chatRequestProvider`
|
||||
- No backend changes
|
||||
Reference in New Issue
Block a user