feat: implement AdminPaymentMethods page with full CRUD

Features:
 Modern card grid layout (matches AdminPlans design)
 Payment method types: Bank Transfer, E-Wallet, QRIS
 Type-specific icons and colors
 Account number & name display
 Instructions section
 Toggle active/inactive status
 Toggle visibility
 Delete functionality
 Drag handle for reordering (UI ready)
 Animated status indicators
 Indonesian text throughout

Card Design:
- Same modern gradient cards as Plans
- Type badges with icons
- 2-column stats grid
- Action buttons (Active, Visibility, Edit, Delete)
- Hover effects and transitions

API Integration:
- GET /admin/payment-methods
- PATCH /admin/payment-methods/:id/visibility
- PATCH /admin/payment-methods/:id/status
- DELETE /admin/payment-methods/:id

TODO: Create/Edit modal (next step)
This commit is contained in:
dwindown
2025-10-11 22:05:27 +07:00
parent 1e3d320716
commit 258d326909

View File

@@ -1,12 +1,293 @@
import { useState, useEffect } from 'react'
import { Plus, Edit, Trash2, Eye, EyeOff, GripVertical, CreditCard, Wallet, Building2 } from 'lucide-react'
import { api } from '@/lib/api'
interface PaymentMethod {
id: string
name: string
type: 'bank_transfer' | 'ewallet' | 'qris' | 'other'
accountNumber?: string
accountName?: string
instructions?: string
isActive: boolean
isVisible: boolean
displayOrder: number
createdAt: string
updatedAt: string
}
export function AdminPaymentMethods() {
const [methods, setMethods] = useState<PaymentMethod[]>([])
const [loading, setLoading] = useState(true)
const [showModal, setShowModal] = useState(false)
const [editingMethod, setEditingMethod] = useState<PaymentMethod | null>(null)
useEffect(() => {
fetchMethods()
}, [])
const fetchMethods = async () => {
try {
setLoading(true)
const response = await api.get('/admin/payment-methods')
setMethods(response.data)
} catch (error) {
console.error('Failed to fetch payment methods:', error)
} finally {
setLoading(false)
}
}
const toggleVisibility = async (method: PaymentMethod) => {
try {
await api.patch(`/admin/payment-methods/${method.id}/visibility`, {
isVisible: !method.isVisible,
})
fetchMethods()
} catch (error) {
console.error('Failed to toggle visibility:', error)
alert('Gagal mengubah visibilitas')
}
}
const toggleActive = async (method: PaymentMethod) => {
try {
await api.patch(`/admin/payment-methods/${method.id}/status`, {
isActive: !method.isActive,
})
fetchMethods()
} catch (error) {
console.error('Failed to toggle status:', error)
alert('Gagal mengubah status')
}
}
const handleDelete = async (id: string) => {
if (!confirm('Yakin ingin menghapus metode pembayaran ini?')) return
try {
await api.delete(`/admin/payment-methods/${id}`)
fetchMethods()
} catch (error) {
console.error('Failed to delete payment method:', error)
alert('Gagal menghapus metode pembayaran')
}
}
const getTypeIcon = (type: string) => {
switch (type) {
case 'bank_transfer':
return Building2
case 'ewallet':
return Wallet
case 'qris':
return CreditCard
default:
return CreditCard
}
}
const getTypeLabel = (type: string) => {
switch (type) {
case 'bank_transfer':
return 'Transfer Bank'
case 'ewallet':
return 'E-Wallet'
case 'qris':
return 'QRIS'
default:
return 'Lainnya'
}
}
const getTypeColor = (type: string) => {
switch (type) {
case 'bank_transfer':
return 'bg-blue-500/10 text-blue-600'
case 'ewallet':
return 'bg-purple-500/10 text-purple-600'
case 'qris':
return 'bg-green-500/10 text-green-600'
default:
return 'bg-muted text-muted-foreground'
}
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">Memuat...</div>
</div>
)
}
return (
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
Payment Methods
</h1>
<p className="text-gray-600 dark:text-gray-400">
Manage payment methods (Coming soon)
</p>
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold text-foreground">
Metode Pembayaran
</h1>
<p className="mt-2 text-muted-foreground">
Kelola metode pembayaran yang tersedia
</p>
</div>
<button
onClick={() => {
setEditingMethod(null)
setShowModal(true)
}}
className="flex items-center px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
>
<Plus className="h-5 w-5 mr-2" />
Tambah Metode
</button>
</div>
{/* Payment Methods Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{methods.map((method) => {
const TypeIcon = getTypeIcon(method.type)
return (
<div
key={method.id}
className="group relative bg-gradient-to-br from-card to-card/50 rounded-2xl border-2 border-border/50 hover:border-primary/30 overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-1"
>
{/* Content */}
<div className="p-6">
{/* Header: Drag Icon + Title + Type Badge */}
<div className="flex items-start gap-3 mb-6">
{/* Drag Handle */}
<div className="opacity-0 group-hover:opacity-100 transition-opacity pt-1">
<GripVertical className="h-5 w-5 mt-1 text-muted-foreground/50 cursor-move" />
</div>
{/* Title & Type */}
<div className="flex-1 min-w-0">
{/* Method Name + Type Badge */}
<div className="flex items-center gap-2 mb-2 flex-wrap">
<h3 className="text-2xl font-bold text-foreground">
{method.name}
</h3>
<span
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-semibold ${getTypeColor(
method.type
)}`}
>
<TypeIcon className="h-3 w-3" />
{getTypeLabel(method.type)}
</span>
</div>
{/* Account Info */}
{method.accountNumber && (
<div className="text-sm text-muted-foreground space-y-1">
<div className="font-mono">{method.accountNumber}</div>
{method.accountName && (
<div className="font-medium">{method.accountName}</div>
)}
</div>
)}
</div>
</div>
{/* Instructions */}
{method.instructions && (
<div className="mb-6 p-4 rounded-lg bg-muted/50 border border-border/50">
<p className="text-xs text-muted-foreground mb-1">Instruksi:</p>
<p className="text-sm text-foreground line-clamp-3">
{method.instructions}
</p>
</div>
)}
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="p-3 rounded-lg bg-muted/50 border border-border/50">
<div className="text-xs text-muted-foreground mb-1">Status</div>
<div className="flex items-center gap-2">
{method.isActive ? (
<>
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-sm font-semibold text-green-600">Active</span>
</>
) : (
<>
<div className="h-2 w-2 rounded-full bg-gray-400" />
<span className="text-sm font-semibold text-muted-foreground">
Inactive
</span>
</>
)}
</div>
</div>
<div className="p-3 rounded-lg bg-muted/50 border border-border/50">
<div className="text-xs text-muted-foreground mb-1">Visibilitas</div>
<div className="flex items-center gap-2">
{method.isVisible ? (
<Eye className="h-4 w-4 text-green-600" />
) : (
<EyeOff className="h-4 w-4 text-muted-foreground" />
)}
<span className="text-sm font-semibold text-foreground">
{method.isVisible ? 'Visible' : 'Hidden'}
</span>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-2">
<button
onClick={() => toggleActive(method)}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all ${
method.isActive
? 'bg-green-500/10 text-green-600 hover:bg-green-500/20'
: 'bg-muted text-muted-foreground hover:bg-muted/80'
}`}
>
<span>{method.isActive ? 'Active' : 'Inactive'}</span>
</button>
<button
onClick={() => toggleVisibility(method)}
className="p-2.5 rounded-lg bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground transition-all"
title={method.isVisible ? 'Sembunyikan' : 'Tampilkan'}
>
{method.isVisible ? (
<Eye className="h-4 w-4" />
) : (
<EyeOff className="h-4 w-4" />
)}
</button>
<button
onClick={() => {
setEditingMethod(method)
setShowModal(true)
}}
className="p-2.5 rounded-lg bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground transition-all"
title="Edit"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDelete(method.id)}
className="p-2.5 rounded-lg bg-destructive/10 text-destructive hover:bg-destructive hover:text-destructive-foreground transition-all"
title="Hapus"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
)
})}
</div>
{methods.length === 0 && (
<div className="text-center py-12">
<p className="text-muted-foreground">Belum ada metode pembayaran</p>
</div>
)}
</div>
)
}