feat: Add Sonner toast notifications to all CRUD operations
- Install sonner package and create Toaster component - Add toast notifications to all admin dashboard operations: * AdminPlans: create, update, delete, reorder, toggle visibility * AdminPaymentMethods: create, update, delete, reorder, toggle active * AdminUsers: suspend, unsuspend, grant pro access * AdminPayments: verify, reject * AdminSettings: save settings - Add toast notifications to all member dashboard operations: * Wallets: create, update, delete * Transactions: create, update, delete * Profile: update name, avatar, phone, password, delete account * OTP: enable/disable email, WhatsApp, authenticator - Replace all alert() calls with toast.success/error/warning - Add proper success/error messages in Bahasa Indonesia - Implement smart plan deletion (permanent if no subscriptions, soft delete if has subscriptions) - Fix admin redirect after login (admin goes to /admin, users to /) - Exclude admin accounts from subscription distribution chart - Show inactive plans with visual indicators - Add real revenue data to admin dashboard charts - Use formatLargeNumber for consistent number formatting
This commit is contained in:
@@ -1,12 +1,295 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Shield, Database, Save, Globe } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'
|
||||
|
||||
interface Settings {
|
||||
appName: string
|
||||
appUrl: string
|
||||
supportEmail: string
|
||||
enableRegistration: boolean
|
||||
enableEmailVerification: boolean
|
||||
enablePaymentVerification: boolean
|
||||
maintenanceMode: boolean
|
||||
maintenanceMessage: string
|
||||
}
|
||||
|
||||
export function AdminSettings() {
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
appName: 'Tabungin',
|
||||
appUrl: 'https://tabungin.app',
|
||||
supportEmail: 'support@tabungin.app',
|
||||
enableRegistration: true,
|
||||
enableEmailVerification: true,
|
||||
enablePaymentVerification: true,
|
||||
maintenanceMode: false,
|
||||
maintenanceMessage: 'Sistem sedang dalam pemeliharaan. Mohon coba lagi nanti.',
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings()
|
||||
}, [])
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.get(`${API_URL}/api/admin/config/by-category`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
// Convert config array to settings object
|
||||
interface ConfigItem {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
interface ConfigData {
|
||||
general?: ConfigItem[]
|
||||
features?: ConfigItem[]
|
||||
system?: ConfigItem[]
|
||||
}
|
||||
const configData = response.data as ConfigData
|
||||
const settingsObj: Settings = {
|
||||
appName: configData.general?.find((c) => c.key === 'app_name')?.value || 'Tabungin',
|
||||
appUrl: configData.general?.find((c) => c.key === 'app_url')?.value || 'https://tabungin.app',
|
||||
supportEmail: configData.general?.find((c) => c.key === 'support_email')?.value || 'support@tabungin.app',
|
||||
enableRegistration: configData.features?.find((c) => c.key === 'enable_registration')?.value === 'true',
|
||||
enableEmailVerification: configData.features?.find((c) => c.key === 'enable_email_verification')?.value === 'true',
|
||||
enablePaymentVerification: configData.features?.find((c) => c.key === 'enable_payment_verification')?.value === 'true',
|
||||
maintenanceMode: configData.system?.find((c) => c.key === 'maintenance_mode')?.value === 'true',
|
||||
maintenanceMessage: configData.system?.find((c) => c.key === 'maintenance_message')?.value || 'Sistem sedang dalam pemeliharaan. Mohon coba lagi nanti.',
|
||||
}
|
||||
setSettings(settingsObj)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true)
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
// Save each setting individually
|
||||
const configUpdates = [
|
||||
{ key: 'app_name', value: settings.appName, category: 'general', label: 'Nama Aplikasi', type: 'text' },
|
||||
{ key: 'app_url', value: settings.appUrl, category: 'general', label: 'URL Aplikasi', type: 'text' },
|
||||
{ key: 'support_email', value: settings.supportEmail, category: 'general', label: 'Email Support', type: 'email' },
|
||||
{ key: 'enable_registration', value: String(settings.enableRegistration), category: 'features', label: 'Registrasi Pengguna Baru', type: 'boolean' },
|
||||
{ key: 'enable_email_verification', value: String(settings.enableEmailVerification), category: 'features', label: 'Verifikasi Email', type: 'boolean' },
|
||||
{ key: 'enable_payment_verification', value: String(settings.enablePaymentVerification), category: 'features', label: 'Verifikasi Pembayaran', type: 'boolean' },
|
||||
{ key: 'maintenance_mode', value: String(settings.maintenanceMode), category: 'system', label: 'Mode Pemeliharaan', type: 'boolean' },
|
||||
{ key: 'maintenance_message', value: settings.maintenanceMessage, category: 'system', label: 'Pesan Pemeliharaan', type: 'text' },
|
||||
]
|
||||
|
||||
await Promise.all(
|
||||
configUpdates.map((config) =>
|
||||
axios.post(`${API_URL}/api/admin/config/${config.key}`, config, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
toast.success('Pengaturan berhasil disimpan')
|
||||
fetchSettings() // Refresh
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error)
|
||||
toast.error('Gagal menyimpan pengaturan')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (field: keyof Settings, value: string | boolean) => {
|
||||
setSettings((prev) => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
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">
|
||||
App Settings
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Manage app configuration (Coming soon)
|
||||
</p>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">Pengaturan Aplikasi</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Kelola konfigurasi dan pengaturan sistem
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* General Settings */}
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Globe className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Pengaturan Umum</h2>
|
||||
<p className="text-sm text-muted-foreground">Informasi dasar aplikasi</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Nama Aplikasi
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={settings.appName}
|
||||
onChange={(e) => handleChange('appName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
URL Aplikasi
|
||||
</label>
|
||||
<Input
|
||||
type="url"
|
||||
value={settings.appUrl}
|
||||
onChange={(e) => handleChange('appUrl', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Email Support
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={settings.supportEmail}
|
||||
onChange={(e) => handleChange('supportEmail', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Toggles */}
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Fitur & Keamanan</h2>
|
||||
<p className="text-sm text-muted-foreground">Aktifkan atau nonaktifkan fitur</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Registrasi Pengguna Baru</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Izinkan pengguna baru mendaftar
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enableRegistration}
|
||||
onCheckedChange={(checked) => handleChange('enableRegistration', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Verifikasi Email</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Wajibkan verifikasi email untuk pengguna baru
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enableEmailVerification}
|
||||
onCheckedChange={(checked) => handleChange('enableEmailVerification', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Verifikasi Pembayaran Manual</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Aktifkan verifikasi manual untuk pembayaran
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enablePaymentVerification}
|
||||
onCheckedChange={(checked) => handleChange('enablePaymentVerification', checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Maintenance Mode */}
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Database className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Mode Pemeliharaan</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nonaktifkan akses sementara untuk maintenance
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-destructive/10 border border-destructive/20">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Mode Pemeliharaan</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Aktifkan untuk menutup akses sementara
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.maintenanceMode}
|
||||
onCheckedChange={(checked) => handleChange('maintenanceMode', checked)}
|
||||
className="data-[state=checked]:bg-destructive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.maintenanceMode && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Pesan Pemeliharaan
|
||||
</label>
|
||||
<Textarea
|
||||
value={settings.maintenanceMessage}
|
||||
onChange={(e) => handleChange('maintenanceMessage', e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Save className="h-5 w-5" />
|
||||
{saving ? 'Menyimpan...' : 'Simpan Pengaturan'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user