Replaces the home-screen pending-requests banner with a "Riwayat
Permintaan" CTA that opens a list of the mitra's last 20 chat requests
(any status). Pending rows pin to the top; non-pending rows open a
read-only detail screen with a "Lihat percakapan" CTA on accepted rows.
Backend:
- New service `getRecentRequestsForMitra(mitraId, { limit })` capped at
20, pending pinned via `(response IS NULL AND status='pending_acceptance')
DESC`. Customer call_name returned verbatim, with `'Anonim'` only as
null-safety fallback (no anonymity-flag masking — see project memory).
- New route `GET /api/mitra/chat-requests/recent`. Strictly per-mitra
scoped via the existing `resolveMitra` preHandler.
Mitra app:
- New `RequestResponse` enum in core/constants.dart.
- New Riverpod notifier `requestHistoryProvider` (AsyncValue<List<...>>,
keepAlive) — pull-to-refresh + screen-mount fetch only, no WS.
- Two new screens (history list + detail) and two new GoRoutes.
- Home screen: `_PendingRequestsBanner` removed → `_RequestHistoryButton`
Card with red count badge. Live count comes from the existing
chatRequestProvider so nothing changes about the WS-driven badge math.
Plan + acceptance criteria in requirement/phase3.5-plan.md. flutter
analyze clean (zero new issues). Backend smoke-tested against real DB.
Real-device E2E pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
4.6 KiB
JavaScript
105 lines
4.6 KiB
JavaScript
import { authenticate } from '../../plugins/auth.js'
|
|
import { getMitraById } from '../../services/mitra.service.js'
|
|
import { acceptPairingRequest, declinePairingRequest, getSessionStatus, getPendingRequestsForMitra } from '../../services/pairing.service.js'
|
|
import { getActiveSessionsByMitra, getActiveSessionsByMitraWithUnread, endSession, getMitraHistory } from '../../services/session.service.js'
|
|
import { respondToExtension } from '../../services/extension.service.js'
|
|
import { getRecentRequestsForMitra } from '../../services/mitra-activity.service.js'
|
|
import { EndedBy, UserType } from '../../constants.js'
|
|
|
|
const resolveMitra = async (request, reply) => {
|
|
if (request.auth?.userType !== UserType.MITRA) {
|
|
return reply.code(403).send({
|
|
success: false,
|
|
error: { code: 'FORBIDDEN', message: 'Mitra account required' },
|
|
})
|
|
}
|
|
const mitra = await getMitraById(request.auth.userId)
|
|
if (!mitra) {
|
|
return reply.code(404).send({
|
|
success: false,
|
|
error: { code: 'ACCOUNT_NOT_FOUND', message: 'Mitra account not found' },
|
|
})
|
|
}
|
|
if (!mitra.is_active) {
|
|
return reply.code(403).send({
|
|
success: false,
|
|
error: { code: 'ACCOUNT_INACTIVE', message: 'Account is inactive' },
|
|
})
|
|
}
|
|
request.mitra = mitra
|
|
}
|
|
|
|
export const mitraChatRoutes = async (app) => {
|
|
// Get pending chat requests for this mitra
|
|
app.get('/pending', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
|
const requests = await getPendingRequestsForMitra(request.mitra.id)
|
|
return reply.send({ success: true, data: requests })
|
|
})
|
|
|
|
// Phase 3.5: Get last N (max 20) chat requests for this mitra (any status, pending pinned first)
|
|
app.get('/recent', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
|
const { limit } = request.query
|
|
const items = await getRecentRequestsForMitra(request.mitra.id, {
|
|
limit: limit ? parseInt(limit) : 20,
|
|
})
|
|
return reply.send({ success: true, data: items })
|
|
})
|
|
|
|
// 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 })
|
|
})
|
|
|
|
app.post('/:sessionId/decline', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
|
await declinePairingRequest(request.params.sessionId, request.mitra.id)
|
|
return reply.send({ success: true })
|
|
})
|
|
|
|
app.get('/sessions/active', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
|
const sessions = await getActiveSessionsByMitra(request.mitra.id)
|
|
return reply.send({ success: true, data: sessions })
|
|
})
|
|
|
|
app.get('/sessions/active-with-unread', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
|
const sessions = await getActiveSessionsByMitraWithUnread(request.mitra.id)
|
|
return reply.send({ success: true, data: sessions })
|
|
})
|
|
|
|
app.post('/sessions/:sessionId/end', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
|
const session = await endSession(request.params.sessionId, EndedBy.MITRA, request.mitra.id)
|
|
return reply.send({ success: true, data: session })
|
|
})
|
|
|
|
// Respond to extension request
|
|
app.post('/sessions/:sessionId/extend-response', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
|
const { extension_id, accepted } = request.body || {}
|
|
if (!extension_id || accepted === undefined) {
|
|
return reply.code(400).send({
|
|
success: false,
|
|
error: { code: 'BAD_REQUEST', message: 'extension_id and accepted are required' },
|
|
})
|
|
}
|
|
const extension = await respondToExtension(extension_id, request.params.sessionId, request.mitra.id, accepted)
|
|
return reply.send({ success: true, data: extension })
|
|
})
|
|
|
|
// Chat history
|
|
app.get('/history', { preHandler: [authenticate, resolveMitra] }, async (request, reply) => {
|
|
const { page, limit } = request.query
|
|
const history = await getMitraHistory(request.mitra.id, {
|
|
page: page ? parseInt(page) : 1,
|
|
limit: limit ? parseInt(limit) : 20,
|
|
})
|
|
return reply.send({ success: true, data: history })
|
|
})
|
|
}
|