Files
tabungin/apps/web/src/components/admin/pages/AdminSettings.tsx
dwindown 89f881e7cf feat: reorganize admin settings with tabbed interface and documentation
- Reorganized admin settings into tabbed interface (General, Security, Payment Methods)
- Vertical tabs on desktop, horizontal scrollable on mobile
- Moved Payment Methods from separate menu to Settings tab
- Fixed admin profile reuse and dashboard blocking
- Fixed maintenance mode guard to use AppConfig model
- Added admin auto-redirect after login (admins → /admin, users → /)
- Reorganized documentation into docs/ folder structure
- Created comprehensive README and documentation index
- Added PWA and Web Push notifications to to-do list
2025-10-13 09:28:12 +07:00

296 lines
11 KiB
TypeScript

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: 'System is under maintenance. Please try again later.',
})
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 || 'System is under maintenance. Please try again later.',
}
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: 'Application Name', type: 'text' },
{ key: 'app_url', value: settings.appUrl, category: 'general', label: 'Application URL', type: 'text' },
{ key: 'support_email', value: settings.supportEmail, category: 'general', label: 'Support Email', type: 'email' },
{ key: 'enable_registration', value: String(settings.enableRegistration), category: 'features', label: 'New User Registration', type: 'boolean' },
{ key: 'enable_email_verification', value: String(settings.enableEmailVerification), category: 'features', label: 'Email Verification', type: 'boolean' },
{ key: 'enable_payment_verification', value: String(settings.enablePaymentVerification), category: 'features', label: 'Payment Verification', type: 'boolean' },
{ key: 'maintenance_mode', value: String(settings.maintenanceMode), category: 'system', label: 'Maintenance Mode', type: 'boolean' },
{ key: 'maintenance_message', value: settings.maintenanceMessage, category: 'system', label: 'Maintenance Message', type: 'text' },
]
await Promise.all(
configUpdates.map((config) =>
axios.post(`${API_URL}/api/admin/config/${config.key}`, config, {
headers: { Authorization: `Bearer ${token}` },
})
)
)
toast.success('Settings saved successfully')
fetchSettings() // Refresh
} catch (error) {
console.error('Failed to save settings:', error)
toast.error('Failed to save settings')
} 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">Loading...</div>
</div>
)
}
return (
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground">Application Settings</h1>
<p className="mt-2 text-muted-foreground">
Manage system configuration and settings
</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">General Settings</h2>
<p className="text-sm text-muted-foreground">Basic application information</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Application Name
</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">
Application URL
</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">Features & Security</h2>
<p className="text-sm text-muted-foreground">Enable or disable features</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">New User Registration</p>
<p className="text-sm text-muted-foreground">
Allow new users to register
</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">Email Verification</p>
<p className="text-sm text-muted-foreground">
Require email verification for new users
</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">Manual Payment Verification</p>
<p className="text-sm text-muted-foreground">
Enable manual verification for payments
</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">Maintenance Mode</h2>
<p className="text-sm text-muted-foreground">
Temporarily disable access for 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">Maintenance Mode</p>
<p className="text-sm text-muted-foreground">
Enable to temporarily close access
</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">
Maintenance Message
</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 ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
</div>
)
}