- 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>
9.8 KiB
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
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_closedfor queued request → remove silently from queuechat_request_closedfor displayed request → transition toChatRequestStaleData
New Methods
ignore()— swipe down on active request, advance queueacknowledgeStale()— 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:
SlideTransitionfromOffset(0, 1)toOffset(0, 0)for bottom-up slide - Swipe-to-dismiss:
GestureDetectorwithonVerticalDragEnddetecting downward swipe - State listening:
ref.listen(chatRequestProvider)to show/hide:ChatRequestIncomingData→ show overlay with accept/reject/swipeChatRequestStaleData→ show overlay with reason message + OK buttonChatRequestAcceptedData→ 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:
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
_showIncomingRequestmethod - Remove
didChangeAppLifecycleStateincoming request check - Remove
ref.listen(chatRequestProvider, ...)block for bottom sheet + navigation - Remove
IncomingRequestSheetimport - Convert from
ConsumerStatefulWidgettoConsumerWidget(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
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: recordactive_session_countwhen creating notification rowsacceptPairingRequest: changeIGNORED→MISSEDwhen marking other mitras' notificationsexpirePairingRequest: keepIGNORED(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 loggetMitraActivitySummary({ 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 |