Phase 2 scaffold: mitra online status & pairing logic
Add mitra online/offline status with heartbeat-based auto-offline, customer-mitra pairing via Valkey pub/sub blast, session management, and control center dashboard with real-time stats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,16 +11,36 @@ const updateAnonymityConfig = async (anonymity_enabled) => {
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
const fetchMaxCustomersConfig = async () => {
|
||||
const res = await apiClient.get('/internal/config/max-customers-per-mitra')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
const updateMaxCustomersConfig = async (max_customers_per_mitra) => {
|
||||
const res = await apiClient.patch('/internal/config/max-customers-per-mitra', { max_customers_per_mitra })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const { data, isLoading } = useQuery({ queryKey: ['config-anonymity'], queryFn: fetchAnonymityConfig })
|
||||
|
||||
const mutation = useMutation({
|
||||
const anonymityMutation = useMutation({
|
||||
mutationFn: updateAnonymityConfig,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-anonymity'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
const { data: maxData, isLoading: maxLoading } = useQuery({
|
||||
queryKey: ['config-max-customers'],
|
||||
queryFn: fetchMaxCustomersConfig,
|
||||
})
|
||||
|
||||
const maxMutation = useMutation({
|
||||
mutationFn: updateMaxCustomersConfig,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-max-customers'] }),
|
||||
})
|
||||
|
||||
if (isLoading || maxLoading) return <div>Loading...</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -33,12 +53,32 @@ export default function SettingsPage() {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data?.anonymity_enabled ?? true}
|
||||
onChange={e => mutation.mutate(e.target.checked)}
|
||||
disabled={mutation.isPending}
|
||||
onChange={e => anonymityMutation.mutate(e.target.checked)}
|
||||
disabled={anonymityMutation.isPending}
|
||||
/>
|
||||
Izinkan pengguna anonim
|
||||
</label>
|
||||
{mutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
|
||||
{anonymityMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 24 }}>
|
||||
<h2>Maks Customer per Mitra</h2>
|
||||
<p>Jumlah maksimal customer yang bisa ditangani satu Mitra secara bersamaan. Perubahan hanya berlaku untuk chat baru.</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={maxData?.max_customers_per_mitra ?? 3}
|
||||
onChange={e => {
|
||||
const val = parseInt(e.target.value, 10)
|
||||
if (val >= 1) maxMutation.mutate(val)
|
||||
}}
|
||||
disabled={maxMutation.isPending}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
<span>customer</span>
|
||||
</div>
|
||||
{maxMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user