Mitra §A: pre-home (S3a/S3b/AccountInactive) + design system + Bestie Home
- Port halo_tokens + halo_theme + HaloButton to mitra_app (rose palette, Bricolage display, Poppins body, JetBrainsMono). - Build S3a Input WhatsApp (figma-bestie BestieS3 first half) with +62 chip, leading-zero/62 normalization, allow '+' in input. - Build S3b OTP verification (6-digit, 60s resend timer, attempts hint, Focus(canRequestFocus:false) for maestro inputText compat) with full error branching (CODE_MISMATCH, OTP_EXPIRED, OTP_USED, ATTEMPTS_EXCEEDED, WRONG_FLOW, ACCOUNT_INACTIVE). - Add AccountInactive terminal screen for is_active=false mitras. - Typed MitraAuthError with Indonesian-first localized messages + retryAfterSeconds passthrough. - Rebuild home_screen.dart to match figma BestieHome (greeting + status card + Ganti Status CTA + Pengingat + 2-tile dark grid). - Backend: POST /internal/_test/seed-mitra (idempotent) and PATCH /internal/mitras/:id (display_name update). - Control center: inline Edit Nama on mitras row + expandable inline log table under clicked mitra (vs old below-table panel). - 5 maestro flows ts-mitra-A-01/03/04/05/06 covering invalid input, happy path, account inactive, phone-format normalization, and the back-to-S3a regression. All green. Plan + memory documented in: - requirement/phase4-mitra-prehome-plan.md - requirement/flow_mitra.md / flow_mitra.mermaid.md §A Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { apiClient } from '../../core/api/api-client'
|
||||
|
||||
@@ -22,6 +22,11 @@ const updateMitraStatus = async ({ id, is_active }) => {
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
const updateMitraName = async ({ id, display_name }) => {
|
||||
const res = await apiClient.patch(`/internal/mitras/${id}`, { display_name })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
const fetchOnlineLogs = async (mitraId) => {
|
||||
const res = await apiClient.get(`/internal/mitras/${mitraId}/online-logs`)
|
||||
return res.data.data
|
||||
@@ -39,6 +44,8 @@ export default function MitrasPage() {
|
||||
const [form, setForm] = useState({ phone: '', display_name: '' })
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [logsForMitra, setLogsForMitra] = useState(null)
|
||||
const [editingId, setEditingId] = useState(null)
|
||||
const [editName, setEditName] = useState('')
|
||||
|
||||
const { data: logsData, isLoading: logsLoading } = useQuery({
|
||||
queryKey: ['mitra-online-logs', logsForMitra],
|
||||
@@ -60,6 +67,29 @@ export default function MitrasPage() {
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['mitras'] }),
|
||||
})
|
||||
|
||||
const nameMutation = useMutation({
|
||||
mutationFn: updateMitraName,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['mitras'] })
|
||||
setEditingId(null)
|
||||
setEditName('')
|
||||
},
|
||||
})
|
||||
|
||||
const startEdit = (mitra) => {
|
||||
setEditingId(mitra.id)
|
||||
setEditName(mitra.display_name)
|
||||
}
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null)
|
||||
setEditName('')
|
||||
}
|
||||
const saveEdit = (id) => {
|
||||
const trimmed = editName.trim()
|
||||
if (!trimmed) return
|
||||
nameMutation.mutate({ id, display_name: trimmed })
|
||||
}
|
||||
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
|
||||
// Build a set of online mitra IDs for quick lookup
|
||||
@@ -106,9 +136,27 @@ export default function MitrasPage() {
|
||||
<tbody>
|
||||
{data?.items?.map((mitra) => {
|
||||
const onlineInfo = onlineMitraMap.get(mitra.id)
|
||||
const isEditing = editingId === mitra.id
|
||||
return (
|
||||
<tr key={mitra.id}>
|
||||
<td style={{ padding: 8 }}>{mitra.display_name}</td>
|
||||
<Fragment key={mitra.id}>
|
||||
<tr>
|
||||
<td style={{ padding: 8 }}>
|
||||
{isEditing ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveEdit(mitra.id)
|
||||
if (e.key === 'Escape') cancelEdit()
|
||||
}}
|
||||
disabled={nameMutation.isPending}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
) : (
|
||||
mitra.display_name
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: 8 }}>{mitra.phone}</td>
|
||||
<td style={{ padding: 8 }}>{mitra.is_active ? 'Aktif' : 'Nonaktif'}</td>
|
||||
<td style={{ padding: 8 }}>
|
||||
@@ -118,48 +166,69 @@ export default function MitrasPage() {
|
||||
</td>
|
||||
<td style={{ padding: 8 }}>{onlineInfo ? onlineInfo.active_session_count : '-'}</td>
|
||||
<td style={{ padding: 8, display: 'flex', gap: 8 }}>
|
||||
<button onClick={() => statusMutation.mutate({ id: mitra.id, is_active: !mitra.is_active })}>
|
||||
{mitra.is_active ? 'Nonaktifkan' : 'Aktifkan'}
|
||||
</button>
|
||||
<button onClick={() => setLogsForMitra(logsForMitra === mitra.id ? null : mitra.id)}>
|
||||
{logsForMitra === mitra.id ? 'Tutup Log' : 'Log Online'}
|
||||
</button>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => saveEdit(mitra.id)}
|
||||
disabled={nameMutation.isPending || !editName.trim() || editName.trim() === mitra.display_name}
|
||||
>
|
||||
{nameMutation.isPending ? 'Menyimpan...' : 'Simpan'}
|
||||
</button>
|
||||
<button onClick={cancelEdit} disabled={nameMutation.isPending}>Batal</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => startEdit(mitra)}>Edit Nama</button>
|
||||
<button onClick={() => statusMutation.mutate({ id: mitra.id, is_active: !mitra.is_active })}>
|
||||
{mitra.is_active ? 'Nonaktifkan' : 'Aktifkan'}
|
||||
</button>
|
||||
<button onClick={() => setLogsForMitra(logsForMitra === mitra.id ? null : mitra.id)}>
|
||||
{logsForMitra === mitra.id ? 'Tutup Log' : 'Log Online'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{logsForMitra === mitra.id && (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ padding: 0, background: '#fafafa', borderBottom: '1px solid #eee' }}>
|
||||
<div style={{ padding: 16 }}>
|
||||
<h4 style={{ margin: '0 0 12px' }}>Log Online/Offline · {mitra.display_name}</h4>
|
||||
{logsLoading ? (
|
||||
<p style={{ margin: 0 }}>Loading...</p>
|
||||
) : logsData?.items?.length ? (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', background: '#fff' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #eee' }}>Status</th>
|
||||
<th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #eee' }}>Waktu</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logsData.items.map((log) => (
|
||||
<tr key={log.id}>
|
||||
<td style={{ padding: 8 }}>
|
||||
<span style={{ color: log.status === 'online' ? 'green' : 'grey' }}>
|
||||
{log.status === 'online' ? '● Online' : '○ Offline'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: 8 }}>{new Date(log.timestamp).toLocaleString('id-ID')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p style={{ margin: 0, color: '#888' }}>Belum ada log.</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{logsForMitra && (
|
||||
<div style={{ marginTop: 16, padding: 16, border: '1px solid #eee' }}>
|
||||
<h3>Log Online/Offline</h3>
|
||||
{logsLoading ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #eee' }}>Status</th>
|
||||
<th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #eee' }}>Waktu</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logsData?.items?.map((log) => (
|
||||
<tr key={log.id}>
|
||||
<td style={{ padding: 8 }}>
|
||||
<span style={{ color: log.status === 'online' ? 'green' : 'grey' }}>
|
||||
{log.status === 'online' ? '● Online' : '○ Offline'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: 8 }}>{new Date(log.timestamp).toLocaleString('id-ID')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user