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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user