import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { apiClient } from '../../core/api/api-client' // ─────────────────────────── data fetching ─────────────────────────── const fetchGroups = async () => { const res = await apiClient.get('/internal/payment-groups') return res.data.data.groups } const fetchMethods = async () => { const res = await apiClient.get('/internal/payment-methods') return res.data.data.methods } const errorMessage = (err) => { const code = err?.response?.data?.error?.code const msg = err?.response?.data?.error?.message if (code === 'CONFLICT') return msg || 'Konflik data.' if (code === 'VALIDATION') return msg || 'Input tidak valid.' if (code === 'NOT_FOUND') return msg || 'Data tidak ditemukan.' return msg || 'Gagal menyimpan.' } // ─────────────────────────── page ─────────────────────────── export default function PaymentCatalogPage() { const qc = useQueryClient() const groupsQ = useQuery({ queryKey: ['payment-groups'], queryFn: fetchGroups }) const methodsQ = useQuery({ queryKey: ['payment-methods'], queryFn: fetchMethods }) const [selectedGroupId, setSelectedGroupId] = useState(null) const [groupForm, setGroupForm] = useState(null) // null | {id?, name, display_order, is_active} const [methodForm, setMethodForm] = useState(null) const [error, setError] = useState(null) const invalidate = () => { qc.invalidateQueries({ queryKey: ['payment-groups'] }) qc.invalidateQueries({ queryKey: ['payment-methods'] }) } // ─────────────── mutations ─────────────── const groupCreate = useMutation({ mutationFn: (body) => apiClient.post('/internal/payment-groups', body), onSuccess: () => { invalidate(); setGroupForm(null); setError(null) }, onError: (e) => setError(errorMessage(e)), }) const groupUpdate = useMutation({ mutationFn: ({ id, body }) => apiClient.patch(`/internal/payment-groups/${id}`, body), onSuccess: () => { invalidate(); setGroupForm(null); setError(null) }, onError: (e) => setError(errorMessage(e)), }) const groupDelete = useMutation({ mutationFn: (id) => apiClient.delete(`/internal/payment-groups/${id}`), onSuccess: () => { invalidate(); setError(null) }, onError: (e) => setError(errorMessage(e)), }) const groupsReorder = useMutation({ mutationFn: (orderedIds) => apiClient.post('/internal/payment-groups/reorder', { ordered_ids: orderedIds }), onSuccess: invalidate, onError: (e) => setError(errorMessage(e)), }) const methodCreate = useMutation({ mutationFn: (body) => apiClient.post('/internal/payment-methods', body), onSuccess: () => { invalidate(); setMethodForm(null); setError(null) }, onError: (e) => setError(errorMessage(e)), }) const methodUpdate = useMutation({ mutationFn: ({ id, body }) => apiClient.patch(`/internal/payment-methods/${id}`, body), onSuccess: () => { invalidate(); setMethodForm(null); setError(null) }, onError: (e) => setError(errorMessage(e)), }) const methodDelete = useMutation({ mutationFn: (id) => apiClient.delete(`/internal/payment-methods/${id}`), onSuccess: () => { invalidate(); setError(null) }, onError: (e) => setError(errorMessage(e)), }) const methodsReorder = useMutation({ mutationFn: (orderedIds) => apiClient.post('/internal/payment-methods/reorder', { ordered_ids: orderedIds }), onSuccess: invalidate, onError: (e) => setError(errorMessage(e)), }) // ─────────────── helpers ─────────────── const moveGroup = (id, delta) => { const ordered = (groupsQ.data ?? []).map((g) => g.id) const idx = ordered.indexOf(id) const newIdx = idx + delta if (idx < 0 || newIdx < 0 || newIdx >= ordered.length) return ;[ordered[idx], ordered[newIdx]] = [ordered[newIdx], ordered[idx]] groupsReorder.mutate(ordered) } const moveMethod = (id, delta, groupId) => { const inGroup = (methodsQ.data ?? []) .filter((m) => m.group_id === groupId) .map((m) => m.id) const idx = inGroup.indexOf(id) const newIdx = idx + delta if (idx < 0 || newIdx < 0 || newIdx >= inGroup.length) return ;[inGroup[idx], inGroup[newIdx]] = [inGroup[newIdx], inGroup[idx]] methodsReorder.mutate(inGroup) } if (groupsQ.isLoading || methodsQ.isLoading) return
Loading…
if (groupsQ.error || methodsQ.error) return
Failed to load payment catalog.
const groups = groupsQ.data ?? [] const methods = methodsQ.data ?? [] const filteredMethods = selectedGroupId ? methods.filter((m) => m.group_id === selectedGroupId) : methods return (

Payment Catalog

Groups and methods rendered by the customer app on the "cara bayar" screen. Reorder controls the visible order. payment_code must match the Xendit channel code exactly (uppercase, e.g. OVO, BCA_VA).

{error && (
{error}
)} {/* ───────────── groups ───────────── */}

Groups

{groups.map((g, i) => { const methodsInGroup = methods.filter((m) => m.group_id === g.id).length return ( ) })} {groups.length === 0 && ( )}
Order Name Active Methods Actions
{g.display_order}
{g.name} {g.is_active ? '✓' : '✗'} {methodsInGroup}
No groups yet.
{/* ───────────── methods ───────────── */}

Methods{selectedGroupId && groups.find(g => g.id === selectedGroupId) ? ` — ${groups.find(g => g.id === selectedGroupId).name}` : ''}

{filteredMethods.map((m, i, arr) => ( ))} {filteredMethods.length === 0 && ( )}
Order Group Display name Code Icon slug Active Actions
{m.display_order}
{groups.find((g) => g.id === m.group_id)?.name ?? '—'} {m.display_name} {m.payment_code} {m.icon || } {m.is_active ? '✓' : '✗'}
{selectedGroupId ? 'No methods in this group yet.' : 'No methods yet.'}
{/* ───────────── modals ───────────── */} {groupForm && ( { setGroupForm(null); setError(null) }} onSubmit={() => { const body = { name: groupForm.name?.trim(), display_order: Number(groupForm.display_order) || 0, is_active: !!groupForm.is_active, } if (groupForm.id) { groupUpdate.mutate({ id: groupForm.id, body }) } else { groupCreate.mutate(body) } }} /> )} {methodForm && ( { setMethodForm(null); setError(null) }} onSubmit={() => { const body = { group_id: methodForm.group_id, display_name: methodForm.display_name?.trim(), payment_code: methodForm.payment_code?.trim()?.toUpperCase(), display_order: Number(methodForm.display_order) || 0, icon: methodForm.icon?.trim() || null, is_active: !!methodForm.is_active, } if (methodForm.id) { methodUpdate.mutate({ id: methodForm.id, body }) } else { methodCreate.mutate(body) } }} /> )}
) } // ─────────────────────────── modals ─────────────────────────── const GroupModal = ({ form, onChange, onSubmit, onClose }) => ( onChange({ ...form, name: e.target.value })} style={inputStyle} /> onChange({ ...form, display_order: e.target.value })} style={inputStyle} /> onChange({ ...form, is_active: e.target.checked })} />