Move payment gateway toggles to a dedicated Finance page

Keep settlements and refunds on Finance; enable/disable lives at /finance/gateways.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
kirukib 2026-08-06 19:12:49 +03:00
parent f2aff8c377
commit 81731dc945
4 changed files with 100 additions and 26 deletions

View File

@ -21,6 +21,7 @@
| 2026-08-06 | Data migration import jobs | | 2026-08-06 | Data migration import jobs |
| 2026-08-06 | Deployed Admin SPA: https://gishen-admin.vercel.app | | 2026-08-06 | Deployed Admin SPA: https://gishen-admin.vercel.app |
| 2026-08-06 | List UX: filters + create pages for orders, Rx, orgs, stock, team, procurement; `POST` create sketches below | | 2026-08-06 | List UX: filters + create pages for orders, Rx, orgs, stock, team, procurement; `POST` create sketches below |
| 2026-08-06 | Finance: gateway enable/disable moved to `/admin/finance/gateways` UI route `/finance/gateways` |
## Conventions ## Conventions

View File

@ -12,6 +12,7 @@ import { CreateStockPage } from '@/pages/stock/CreateStockPage'
import { ProcurementPage } from '@/pages/procurement/ProcurementPage' import { ProcurementPage } from '@/pages/procurement/ProcurementPage'
import { CreateProcurementRequestPage } from '@/pages/procurement/CreateProcurementRequestPage' import { CreateProcurementRequestPage } from '@/pages/procurement/CreateProcurementRequestPage'
import { FinancePage } from '@/pages/finance/FinancePage' import { FinancePage } from '@/pages/finance/FinancePage'
import { PaymentGatewaysPage } from '@/pages/finance/PaymentGatewaysPage'
import { OrganisationsPage } from '@/pages/organisations/OrganisationsPage' import { OrganisationsPage } from '@/pages/organisations/OrganisationsPage'
import { CreateOrganisationPage } from '@/pages/organisations/CreateOrganisationPage' import { CreateOrganisationPage } from '@/pages/organisations/CreateOrganisationPage'
import { MarketingPage } from '@/pages/marketing/MarketingPage' import { MarketingPage } from '@/pages/marketing/MarketingPage'
@ -53,6 +54,7 @@ export function AppRoutes() {
<Route path="procurement" element={<Guard module="procurement"><ProcurementPage /></Guard>} /> <Route path="procurement" element={<Guard module="procurement"><ProcurementPage /></Guard>} />
<Route path="procurement/new" element={<Guard module="procurement"><CreateProcurementRequestPage /></Guard>} /> <Route path="procurement/new" element={<Guard module="procurement"><CreateProcurementRequestPage /></Guard>} />
<Route path="finance" element={<Guard module="finance"><FinancePage /></Guard>} /> <Route path="finance" element={<Guard module="finance"><FinancePage /></Guard>} />
<Route path="finance/gateways" element={<Guard module="finance"><PaymentGatewaysPage /></Guard>} />
<Route path="organisations" element={<Guard module="organisations"><OrganisationsPage /></Guard>} /> <Route path="organisations" element={<Guard module="organisations"><OrganisationsPage /></Guard>} />
<Route path="organisations/new" element={<Guard module="organisations"><CreateOrganisationPage /></Guard>} /> <Route path="organisations/new" element={<Guard module="organisations"><CreateOrganisationPage /></Guard>} />
<Route path="marketing" element={<Guard module="marketing"><MarketingPage /></Guard>} /> <Route path="marketing" element={<Guard module="marketing"><MarketingPage /></Guard>} />

View File

@ -1,28 +1,31 @@
import { useState } from 'react' import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Settings2 } from 'lucide-react'
import { PageShell } from '@/layouts/PageShell' import { PageShell } from '@/layouts/PageShell'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { SwitchRow } from '@/pages/finance/SwitchRow'
import { settlements } from '@/mocks/data' import { settlements } from '@/mocks/data'
import { formatEtb } from '@/lib/utils' import { formatEtb } from '@/lib/utils'
const gateways = [
{ id: 'chapa', label: 'Chapa', enabled: true },
{ id: 'arifpay', label: 'ArifPay', enabled: true },
{ id: 'telebirr', label: 'Telebirr', enabled: false },
{ id: 'mpesa', label: 'M-Pesa', enabled: false },
]
export function FinancePage() { export function FinancePage() {
const { t } = useTranslation() const { t } = useTranslation()
const [gw, setGw] = useState(gateways)
return ( return (
<PageShell title={t('finance.title')} subtitle={t('finance.subtitle')}> <PageShell
title={t('finance.title')}
subtitle={t('finance.subtitle')}
actions={
<Button asChild variant="outline" className="h-10 gap-2 shadow-none">
<Link to="/finance/gateways">
<Settings2 className="h-4 w-4" />
Manage gateways
</Link>
</Button>
}
>
<div className="grid gap-4 lg:grid-cols-2"> <div className="grid gap-4 lg:grid-cols-2">
<Card> <Card>
<CardHeader> <CardHeader>
@ -55,26 +58,27 @@ export function FinancePage() {
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Payment gateways</CardTitle> <CardTitle>Operations</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3 p-4 pt-0"> <CardContent className="space-y-3 p-4 pt-0">
{gw.map((g) => (
<SwitchRow
key={g.id}
label={g.label}
enabled={g.enabled}
onToggle={() => {
setGw((prev) => prev.map((x) => (x.id === g.id ? { ...x, enabled: !x.enabled } : x)))
toast.success(`${g.label} ${g.enabled ? 'disabled' : 'enabled'} (no code release)`)
}}
/>
))}
<div className="rounded-xl border bg-muted/40 p-3 text-sm"> <div className="rounded-xl border bg-muted/40 p-3 text-sm">
Loyalty points liability (Aug): <strong>{formatEtb(182400)}</strong> Loyalty points liability (Aug): <strong>{formatEtb(182400)}</strong>
</div> </div>
<Button variant="outline" onClick={() => toast.message('Refund queued (mock)')}> <p className="text-sm text-muted-foreground">
Gateway enable / disable lives on a dedicated settings page so day-to-day finance work stays focused on
settlements and refunds.
</p>
<div className="flex flex-wrap gap-2">
<Button variant="outline" className="h-10" onClick={() => toast.message('Refund queued (mock)')}>
Process refund Process refund
</Button> </Button>
<Button asChild variant="secondary" className="h-10 gap-2">
<Link to="/finance/gateways">
<Settings2 className="h-4 w-4" />
Payment gateways
</Link>
</Button>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -0,0 +1,67 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { toast } from 'sonner'
import { ArrowLeft } from 'lucide-react'
import { PageShell } from '@/layouts/PageShell'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { SwitchRow } from '@/pages/finance/SwitchRow'
const initialGateways = [
{ id: 'chapa', label: 'Chapa', enabled: true, type: 'Aggregator' },
{ id: 'arifpay', label: 'ArifPay', enabled: true, type: 'Aggregator' },
{ id: 'telebirr', label: 'Telebirr', enabled: false, type: 'Direct wallet' },
{ id: 'mpesa', label: 'M-Pesa', enabled: false, type: 'Direct wallet' },
]
export function PaymentGatewaysPage() {
const [gw, setGw] = useState(initialGateways)
return (
<PageShell
title="Payment gateways"
subtitle="Enable, disable, or swap channels without a code release"
actions={
<Button asChild variant="outline" className="h-10 gap-2">
<Link to="/finance">
<ArrowLeft className="h-4 w-4" />
Back to Finance
</Link>
</Button>
}
>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Channel controls</CardTitle>
<CardDescription>
Changes apply immediately in the demo. Telebirr / M-Pesa may require INSA clearance before go-live.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{gw.map((g) => (
<div key={g.id} className="rounded-xl border p-3">
<div className="mb-2 flex items-center justify-between gap-2">
<div>
<p className="text-sm font-semibold">{g.label}</p>
<p className="text-xs text-muted-foreground">{g.type}</p>
</div>
<Badge variant={g.enabled ? 'success' : 'muted'}>{g.enabled ? 'Live' : 'Off'}</Badge>
</div>
<SwitchRow
label={g.enabled ? 'Disable channel' : 'Enable channel'}
enabled={g.enabled}
onToggle={() => {
setGw((prev) =>
prev.map((x) => (x.id === g.id ? { ...x, enabled: !x.enabled } : x)),
)
toast.success(`${g.label} ${g.enabled ? 'disabled' : 'enabled'} (no code release)`)
}}
/>
</div>
))}
</CardContent>
</Card>
</PageShell>
)
}