Phase 3 scaffold: chat engine (WebSocket, FCM, pricing, timer, extension, history)
- Backend: WebSocket plugin, chat/pricing/timer/extension/closure/notification services - Client app: ChatBloc, pricing dialog, chat screen with message status, extension/goodbye flow, history - Mitra app: MitraChatBloc, ExtensionBloc, chat screen, extension accept/reject, history - Control center: free trial, extension timeout, early end config toggles - DB migration: chat_messages, session_closures, session_extensions, customer_transactions tables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,37 @@ const updateMaxCustomersConfig = async (max_customers_per_mitra) => {
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
// Phase 3 config fetchers
|
||||
const fetchFreeTrialConfig = async () => {
|
||||
const res = await apiClient.get('/internal/config/free-trial')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
const updateFreeTrialConfig = async (data) => {
|
||||
const res = await apiClient.patch('/internal/config/free-trial', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
const fetchExtensionTimeoutConfig = async () => {
|
||||
const res = await apiClient.get('/internal/config/extension-timeout')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
const updateExtensionTimeoutConfig = async (extension_timeout_seconds) => {
|
||||
const res = await apiClient.patch('/internal/config/extension-timeout', { extension_timeout_seconds })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
const fetchEarlyEndConfig = async () => {
|
||||
const res = await apiClient.get('/internal/config/early-end')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
const updateEarlyEndConfig = async (data) => {
|
||||
const res = await apiClient.patch('/internal/config/early-end', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const { data, isLoading } = useQuery({ queryKey: ['config-anonymity'], queryFn: fetchAnonymityConfig })
|
||||
@@ -40,7 +71,37 @@ export default function SettingsPage() {
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-max-customers'] }),
|
||||
})
|
||||
|
||||
if (isLoading || maxLoading) return <div>Loading...</div>
|
||||
// Phase 3: Free Trial
|
||||
const { data: ftData, isLoading: ftLoading } = useQuery({
|
||||
queryKey: ['config-free-trial'],
|
||||
queryFn: fetchFreeTrialConfig,
|
||||
})
|
||||
const ftMutation = useMutation({
|
||||
mutationFn: updateFreeTrialConfig,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-free-trial'] }),
|
||||
})
|
||||
|
||||
// Phase 3: Extension Timeout
|
||||
const { data: etData, isLoading: etLoading } = useQuery({
|
||||
queryKey: ['config-extension-timeout'],
|
||||
queryFn: fetchExtensionTimeoutConfig,
|
||||
})
|
||||
const etMutation = useMutation({
|
||||
mutationFn: updateExtensionTimeoutConfig,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-extension-timeout'] }),
|
||||
})
|
||||
|
||||
// Phase 3: Early End
|
||||
const { data: eeData, isLoading: eeLoading } = useQuery({
|
||||
queryKey: ['config-early-end'],
|
||||
queryFn: fetchEarlyEndConfig,
|
||||
})
|
||||
const eeMutation = useMutation({
|
||||
mutationFn: updateEarlyEndConfig,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-early-end'] }),
|
||||
})
|
||||
|
||||
if (isLoading || maxLoading || ftLoading || etLoading || eeLoading) return <div>Loading...</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -80,6 +141,80 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
{maxMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 24 }}>
|
||||
<h2>Free Trial</h2>
|
||||
<p>Aktifkan free trial untuk customer baru yang belum pernah bertransaksi.</p>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={ftData?.enabled ?? false}
|
||||
onChange={e => ftMutation.mutate({ enabled: e.target.checked })}
|
||||
disabled={ftMutation.isPending}
|
||||
/>
|
||||
Aktifkan Free Trial
|
||||
</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<label>Durasi:</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={ftData?.duration_minutes ?? 5}
|
||||
onChange={e => {
|
||||
const val = parseInt(e.target.value, 10)
|
||||
if (val >= 1) ftMutation.mutate({ duration_minutes: val })
|
||||
}}
|
||||
disabled={ftMutation.isPending}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
<span>menit</span>
|
||||
</div>
|
||||
{ftMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 24 }}>
|
||||
<h2>Extension Timeout</h2>
|
||||
<p>Waktu tunggu untuk customer memutuskan dan mitra mengkonfirmasi perpanjangan sesi.</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input
|
||||
type="number"
|
||||
min="10"
|
||||
value={etData?.extension_timeout_seconds ?? 60}
|
||||
onChange={e => {
|
||||
const val = parseInt(e.target.value, 10)
|
||||
if (val >= 10) etMutation.mutate(val)
|
||||
}}
|
||||
disabled={etMutation.isPending}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
<span>detik</span>
|
||||
</div>
|
||||
{etMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 24 }}>
|
||||
<h2>Akhiri Sesi Lebih Awal</h2>
|
||||
<p>Izinkan mitra dan/atau customer untuk mengakhiri sesi sebelum waktu habis.</p>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={eeData?.mitra_enabled ?? false}
|
||||
onChange={e => eeMutation.mutate({ mitra_enabled: e.target.checked })}
|
||||
disabled={eeMutation.isPending}
|
||||
/>
|
||||
Izinkan Mitra mengakhiri lebih awal
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={eeData?.customer_enabled ?? false}
|
||||
onChange={e => eeMutation.mutate({ customer_enabled: e.target.checked })}
|
||||
disabled={eeMutation.isPending}
|
||||
/>
|
||||
Izinkan Customer mengakhiri lebih awal
|
||||
</label>
|
||||
{eeMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user