feat: Add Multi-Gateway Configuration Module with Payment Settings Tab
This commit is contained in:
158
src/features/settings/components/tabs/OrganizationSettings.tsx
Normal file
158
src/features/settings/components/tabs/OrganizationSettings.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { OrganizationConfig, SecurityConfig } from '@/lib/types/settings';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { updateSystemSetting } from '@/lib/actions/settings';
|
||||
import { toast } from 'sonner';
|
||||
import { Building2, ShieldCheck, Mail, Globe } from 'lucide-react';
|
||||
|
||||
interface OrganizationSettingsProps {
|
||||
orgConfig: OrganizationConfig;
|
||||
securityConfig: SecurityConfig;
|
||||
onUpdate: (section: 'organization' | 'security', data: any) => void;
|
||||
}
|
||||
|
||||
export function OrganizationSettings({ orgConfig, securityConfig, onUpdate }: OrganizationSettingsProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Internal state for form usage
|
||||
const [orgState, setOrgState] = useState(orgConfig);
|
||||
const [secState, setSecState] = useState(securityConfig);
|
||||
|
||||
const handleSaveOrg = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await updateSystemSetting('organization', orgState);
|
||||
if (res.success) {
|
||||
toast.success('Organization profile updated');
|
||||
onUpdate('organization', res.updatedSettings.organization);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update organization settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveSecurity = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await updateSystemSetting('security', secState);
|
||||
if (res.success) {
|
||||
toast.success('Security policy updated');
|
||||
onUpdate('security', res.updatedSettings.security);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update security settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Building2 className="h-5 w-5 text-primary" />
|
||||
Organization Profile
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
General branding and contact details for the control center and emails.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Brand Name</Label>
|
||||
<Input
|
||||
value={orgState.brandName}
|
||||
onChange={(e) => setOrgState({ ...orgState, brandName: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Support Email</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-9"
|
||||
value={orgState.supportEmail}
|
||||
onChange={(e) => setOrgState({ ...orgState, supportEmail: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Legal Address</Label>
|
||||
<Input
|
||||
value={orgState.legalAddress}
|
||||
onChange={(e) => setOrgState({ ...orgState, legalAddress: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button onClick={handleSaveOrg} disabled={loading}>
|
||||
{loading ? 'Saving...' : 'Save Profile'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-orange-200 bg-orange-50/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-5 w-5 text-orange-600" />
|
||||
Security Policy
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enforce security rules for all Control Center staff members.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg bg-card">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-base">Enforce 2FA</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Require Two-Factor Authentication for all admin accounts.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={secState.enforce2FA}
|
||||
onCheckedChange={(checked) => setSecState({ ...secState, enforce2FA: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Session Timeout (Minutes)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={secState.sessionTimeoutMinutes}
|
||||
onChange={(e) => setSecState({ ...secState, sessionTimeoutMinutes: parseInt(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Password Expiration (Days)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={secState.passwordExpirationDays}
|
||||
onChange={(e) => setSecState({ ...secState, passwordExpirationDays: parseInt(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" onClick={handleSaveSecurity} disabled={loading} className="border-orange-200 text-orange-700 hover:bg-orange-50 hover:text-orange-800">
|
||||
{loading ? 'Saving...' : 'Update Security Policy'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
173
src/features/settings/components/tabs/PartnerGovernance.tsx
Normal file
173
src/features/settings/components/tabs/PartnerGovernance.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PartnerConfig } from '@/lib/types/settings';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { updateSystemSetting } from '@/lib/actions/settings';
|
||||
import { toast } from 'sonner';
|
||||
import { Handshake, Banknote, FileCheck } from 'lucide-react';
|
||||
|
||||
interface PartnerGovernanceProps {
|
||||
config: PartnerConfig;
|
||||
onUpdate: (data: PartnerConfig) => void;
|
||||
}
|
||||
|
||||
export function PartnerGovernanceTab({ config, onUpdate }: PartnerGovernanceProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [state, setState] = useState(config);
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await updateSystemSetting('partner', state);
|
||||
if (res.success && res.updatedSettings.partner) {
|
||||
toast.success('Partner governance rules updated');
|
||||
onUpdate(res.updatedSettings.partner);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update partner settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleKycDoc = (doc: 'pan' | 'gst' | 'aadhaar' | 'cheque') => {
|
||||
const current = state.allowedKycDocs;
|
||||
const updated = current.includes(doc)
|
||||
? current.filter(d => d !== doc)
|
||||
: [...current, doc];
|
||||
|
||||
setState({ ...state, allowedKycDocs: updated });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
|
||||
{/* Onboarding Rules */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Handshake className="h-5 w-5 text-indigo-500" />
|
||||
Onboarding & Approval
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Set rules for new organizer accounts and events.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between py-2 border-b">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Require KYC Approval</Label>
|
||||
<p className="text-xs text-muted-foreground">Block payouts until verified.</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={state.requireKyc}
|
||||
onCheckedChange={(c) => setState({ ...state, requireKyc: c })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Manual Event Approval</Label>
|
||||
<p className="text-xs text-muted-foreground">Admins must approve live events.</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={state.manualEventApproval}
|
||||
onCheckedChange={(c) => setState({ ...state, manualEventApproval: c })}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Commission Model */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Banknote className="h-5 w-5 text-green-600" />
|
||||
Commission & Payouts
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Define the default revenue share model.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Default Commission (%)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={state.defaultCommissionPercent}
|
||||
onChange={(e) => setState({ ...state, defaultCommissionPercent: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Payout Schedule</Label>
|
||||
<Select
|
||||
value={state.payoutSchedule}
|
||||
onValueChange={(v: any) => setState({ ...state, payoutSchedule: v })}
|
||||
>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="daily">Daily (T+1)</SelectItem>
|
||||
<SelectItem value="weekly">Weekly</SelectItem>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="manual">Manual Request</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Min. Payout Amount (₹)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={state.minPayoutAmount}
|
||||
onChange={(e) => setState({ ...state, minPayoutAmount: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* KYC Requirements */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileCheck className="h-5 w-5 text-slate-500" />
|
||||
KYC Requirements
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Select documents required for organizer verification.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{['pan', 'gst', 'aadhaar', 'cheque'].map((doc) => (
|
||||
<div key={doc} className="flex items-center space-x-2 border p-3 rounded-md">
|
||||
<Checkbox
|
||||
id={`doc-${doc}`}
|
||||
checked={state.allowedKycDocs.includes(doc as any)}
|
||||
onCheckedChange={() => toggleKycDoc(doc as any)}
|
||||
/>
|
||||
<Label htmlFor={`doc-${doc}`} className="uppercase text-xs font-semibold">
|
||||
{doc}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave} disabled={loading}>
|
||||
{loading ? 'Saving...' : 'Update Governance Rules'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
196
src/features/settings/components/tabs/PaymentConfig.tsx
Normal file
196
src/features/settings/components/tabs/PaymentConfig.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PaymentConfig, GatewayProvider, GatewayCredentials } from '@/lib/types/settings';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { GatewayConfigSheet } from '@/features/settings/components/GatewayConfigSheet';
|
||||
import { updateRoutingRules } from '@/lib/actions/payment-settings';
|
||||
import { toast } from 'sonner';
|
||||
import { CreditCard, Globe, Settings2, ShieldCheck } from 'lucide-react';
|
||||
|
||||
interface PaymentConfigTabProps {
|
||||
config: PaymentConfig;
|
||||
onUpdate: (data: PaymentConfig) => void;
|
||||
}
|
||||
|
||||
const PROVIDERS: { id: GatewayProvider; name: string }[] = [
|
||||
{ id: 'razorpay', name: 'Razorpay' },
|
||||
{ id: 'stripe', name: 'Stripe' },
|
||||
{ id: 'payu', name: 'PayU' },
|
||||
{ id: 'easebuzz', name: 'Easebuzz' },
|
||||
{ id: 'worldline', name: 'Worldline' },
|
||||
];
|
||||
|
||||
export function PaymentConfigTab({ config, onUpdate }: PaymentConfigTabProps) {
|
||||
const [selectedProvider, setSelectedProvider] = useState<GatewayProvider | null>(null);
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const [routingState, setRoutingState] = useState(config);
|
||||
|
||||
const handleOpenConfig = (provider: GatewayProvider) => {
|
||||
setSelectedProvider(provider);
|
||||
setSheetOpen(true);
|
||||
};
|
||||
|
||||
const handleRoutingUpdate = async (key: keyof PaymentConfig, value: string) => {
|
||||
const newState = { ...routingState, [key]: value };
|
||||
setRoutingState(newState);
|
||||
|
||||
try {
|
||||
const res = await updateRoutingRules({
|
||||
defaultGateway: newState.defaultGateway,
|
||||
fallbackGateway: newState.fallbackGateway,
|
||||
internationalGateway: newState.internationalGateway
|
||||
});
|
||||
if (res.success) {
|
||||
toast.success('Routing rules updated');
|
||||
// Optimistic update handled by local state, but we should propagate up
|
||||
onUpdate(newState);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update routing');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
|
||||
{/* Gateway Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{PROVIDERS.map((provider) => {
|
||||
const gwConfig = config.gateways[provider.id];
|
||||
const isActive = gwConfig?.enabled;
|
||||
const isLive = gwConfig?.mode === 'live';
|
||||
|
||||
return (
|
||||
<Card key={provider.id} className={`group hover:shadow-md transition-shadow ${isActive ? 'border-primary/50 bg-primary/5' : 'opacity-75'}`}>
|
||||
<CardHeader className="flex flex-row items-start justify-between pb-2">
|
||||
<CardTitle className="text-lg font-bold">{provider.name}</CardTitle>
|
||||
{isActive ? (
|
||||
<Badge variant={isLive ? 'default' : 'secondary'} className={isLive ? 'bg-emerald-600' : ''}>
|
||||
{isLive ? 'LIVE' : 'TEST'}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">Disabled</Badge>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
{isActive ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4 text-emerald-500" />
|
||||
Verified
|
||||
</div>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{gwConfig.features.upi && <Badge variant="outline" className="text-[10px]">UPI</Badge>}
|
||||
{gwConfig.features.cards && <Badge variant="outline" className="text-[10px]">Cards</Badge>}
|
||||
{gwConfig.features.emi && <Badge variant="outline" className="text-[10px]">EMI</Badge>}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
Not configured
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
variant={isActive ? "outline" : "secondary"}
|
||||
className="w-full"
|
||||
onClick={() => handleOpenConfig(provider.id)}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 mr-2" />
|
||||
Configure
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Routing Logic */}
|
||||
<Card className="border-indigo-200 bg-indigo-50/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-indigo-700">
|
||||
<Globe className="h-5 w-5" />
|
||||
Smart Routing Logic
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Define how transactions are routed across active gateways.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid md:grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label>Default Gateway</Label>
|
||||
<Select
|
||||
value={routingState.defaultGateway}
|
||||
onValueChange={(v) => handleRoutingUpdate('defaultGateway', v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROVIDERS.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">Used for 90% of traffic.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Fallback Gateway</Label>
|
||||
<Select
|
||||
value={routingState.fallbackGateway}
|
||||
onValueChange={(v) => handleRoutingUpdate('fallbackGateway', v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROVIDERS.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">Used if default fails.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>International (Non-INR)</Label>
|
||||
<Select
|
||||
value={routingState.internationalGateway}
|
||||
onValueChange={(v) => handleRoutingUpdate('internationalGateway', v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROVIDERS.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">Cards issued outside India.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Config Sheet Instance */}
|
||||
{selectedProvider && config.gateways[selectedProvider] && (
|
||||
<GatewayConfigSheet
|
||||
open={sheetOpen}
|
||||
onOpenChange={setSheetOpen}
|
||||
provider={selectedProvider}
|
||||
initialConfig={config.gateways[selectedProvider]}
|
||||
onSave={() => {
|
||||
// Refresh parent data? Handled via Actions and upper state update usually,
|
||||
// but for now we rely on the parent's polling or manual refresh,
|
||||
// effectively we might want to trigger a callback here.
|
||||
// Ideally onUpdate would trigger a re-fetch.
|
||||
window.location.reload(); // Quick dirty refresh to sync state in this demo
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
193
src/features/settings/components/tabs/PublicAppConfig.tsx
Normal file
193
src/features/settings/components/tabs/PublicAppConfig.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PublicAppConfig } from '@/lib/types/settings';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { updateSystemSetting } from '@/lib/actions/settings';
|
||||
import { toast } from 'sonner';
|
||||
import { Smartphone, Zap, Percent, Link as LinkIcon } from 'lucide-react';
|
||||
|
||||
interface PublicAppConfigProps {
|
||||
config: PublicAppConfig;
|
||||
onUpdate: (data: PublicAppConfig) => void;
|
||||
}
|
||||
|
||||
export function PublicAppConfigTab({ config, onUpdate }: PublicAppConfigProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [state, setState] = useState(config);
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await updateSystemSetting('publicApp', state);
|
||||
if (res.success && res.updatedSettings.publicApp) {
|
||||
toast.success('Public App configuration updated');
|
||||
onUpdate(res.updatedSettings.publicApp);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update app config');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to update deeply nested feature flags
|
||||
const toggleFeature = (key: keyof PublicAppConfig['betaFeatures']) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
betaFeatures: {
|
||||
...prev.betaFeatures,
|
||||
[key]: !prev.betaFeatures[key]
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
|
||||
{/* Feature Flags */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-yellow-500" />
|
||||
Feature Flags
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Toggle beta features for end-users instantly.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between py-2 border-b">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Crypto Payments</Label>
|
||||
<p className="text-xs text-muted-foreground">Accept ETH/SOL via Solana Pay</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={state.betaFeatures.cryptoPayments}
|
||||
onCheckedChange={() => toggleFeature('cryptoPayments')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2 border-b">
|
||||
<div className="space-y-0.5">
|
||||
<Label>AI Recommendations</Label>
|
||||
<p className="text-xs text-muted-foreground">Show "Events you might like"</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={state.betaFeatures.aiRecommendations}
|
||||
onCheckedChange={() => toggleFeature('aiRecommendations')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Social Login</Label>
|
||||
<p className="text-xs text-muted-foreground">Enable Google/Apple Auth</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={state.betaFeatures.socialLogin}
|
||||
onCheckedChange={() => toggleFeature('socialLogin')}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Commercials */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Percent className="h-5 w-5 text-emerald-600" />
|
||||
Booking Fees & Tax
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Set the global platform fee charged to users.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Platform Fee (Flat ₹)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={state.fees.platformFeeFlat}
|
||||
onChange={(e) => setState({
|
||||
...state,
|
||||
fees: { ...state.fees, platformFeeFlat: Number(e.target.value) }
|
||||
})}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Added to every ticket purchase.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>GST / Tax Rate (%)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={state.fees.taxRatePercent}
|
||||
onChange={(e) => setState({
|
||||
...state,
|
||||
fees: { ...state.fees, taxRatePercent: Number(e.target.value) }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<LinkIcon className="h-5 w-5 text-blue-500" />
|
||||
Support Links
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Update external URLs for help, terms, and privacy policies.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Help Center URL</Label>
|
||||
<Input
|
||||
value={state.links.helpCenterUrl}
|
||||
onChange={(e) => setState({
|
||||
...state,
|
||||
links: { ...state.links, helpCenterUrl: e.target.value }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Terms & Conditions URL</Label>
|
||||
<Input
|
||||
value={state.links.termsUrl}
|
||||
onChange={(e) => setState({
|
||||
...state,
|
||||
links: { ...state.links, termsUrl: e.target.value }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Privacy Policy URL</Label>
|
||||
<Input
|
||||
value={state.links.privacyUrl}
|
||||
onChange={(e) => setState({
|
||||
...state,
|
||||
links: { ...state.links, privacyUrl: e.target.value }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end sticky bottom-6 z-10">
|
||||
<Button size="lg" onClick={handleSave} disabled={loading} className="shadow-lg">
|
||||
{loading ? 'Publishing Updates...' : 'Publish Public App Config'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
244
src/features/settings/components/tabs/SystemHealth.tsx
Normal file
244
src/features/settings/components/tabs/SystemHealth.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { SystemConfig } from '@/lib/types/settings';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { updateSystemSetting, purgeSystemCache } from '@/lib/actions/settings';
|
||||
import { toast } from 'sonner';
|
||||
import { Server, CreditCard, Radio, Trash2, AlertTriangle, Activity } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface SystemHealthProps {
|
||||
config: SystemConfig;
|
||||
onUpdate: (data: SystemConfig) => void;
|
||||
}
|
||||
|
||||
export function SystemHealthTab({ config, onUpdate }: SystemHealthProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [state, setState] = useState(config);
|
||||
const [purgeLoading, setPurgeLoading] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await updateSystemSetting('system', state);
|
||||
if (res.success && res.updatedSettings.system) {
|
||||
toast.success('System configuration updated');
|
||||
onUpdate(res.updatedSettings.system);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update system config');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurgeCache = async () => {
|
||||
setPurgeLoading(true);
|
||||
try {
|
||||
const res = await purgeSystemCache();
|
||||
if (res.success) {
|
||||
toast.success('Cache purged successfully', {
|
||||
description: 'Changes are propagating to edge locations.'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to purge cache');
|
||||
} finally {
|
||||
setPurgeLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* System Status Banner */}
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">API Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span className="text-2xl font-bold">Operational</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Uptime: 99.99%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Cache Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-2xl font-bold">Healthy</span>
|
||||
<Badge variant="outline">TTL: {state.cache.ttl}s</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Last Purged: {state.cache.lastPurgedAt ? new Date(state.cache.lastPurgedAt).toLocaleTimeString() : 'Never'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Database</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<span className="text-2xl font-bold">Connected</span>
|
||||
<p className="text-xs text-muted-foreground mt-1">Latency: 24ms</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Payment Gateways */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CreditCard className="h-5 w-5 text-primary" />
|
||||
Payment Gateways
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage gateway keys and active modes (Test vs Live).
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Stripe */}
|
||||
<div className="p-4 border rounded-lg bg-card/50">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-semibold">Stripe</h4>
|
||||
{state.gateways.stripe.mode === 'live' ?
|
||||
<Badge className="bg-emerald-500">Live</Badge> :
|
||||
<Badge variant="secondary">Test Mode</Badge>
|
||||
}
|
||||
</div>
|
||||
<Switch
|
||||
checked={state.gateways.stripe.enabled}
|
||||
onCheckedChange={(c) => setState({
|
||||
...state,
|
||||
gateways: { ...state.gateways, stripe: { ...state.gateways.stripe, enabled: c } }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Public Key</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={state.gateways.stripe.publicKey}
|
||||
readOnly // For demo safety
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<Label className="text-xs">Mode:</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={state.gateways.stripe.mode === 'live'}
|
||||
onCheckedChange={(c) => setState({
|
||||
...state,
|
||||
gateways: { ...state.gateways, stripe: { ...state.gateways.stripe, mode: c ? 'live' : 'test' } }
|
||||
})}
|
||||
/>
|
||||
<span className={state.gateways.stripe.mode === 'live' ? 'font-bold text-red-600' : 'text-muted-foreground'}>
|
||||
{state.gateways.stripe.mode === 'live' ? 'LIVE TRAFFIC' : 'Test Mode'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Razorpay */}
|
||||
<div className="p-4 border rounded-lg bg-card/50">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-semibold">Razorpay</h4>
|
||||
{state.gateways.razorpay.mode === 'live' ?
|
||||
<Badge className="bg-emerald-500">Live</Badge> :
|
||||
<Badge variant="secondary">Test Mode</Badge>
|
||||
}
|
||||
</div>
|
||||
<Switch
|
||||
checked={state.gateways.razorpay.enabled}
|
||||
onCheckedChange={(c) => setState({
|
||||
...state,
|
||||
gateways: { ...state.gateways, razorpay: { ...state.gateways.razorpay, enabled: c } }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Key ID</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={state.gateways.razorpay.keyId}
|
||||
readOnly
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end border-t pt-4">
|
||||
<Button onClick={handleSave} disabled={loading}>Save Gateway Config</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<h3 className="text-lg font-semibold text-red-600 flex items-center gap-2 mt-8">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
Danger Zone
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card className="border-red-200 bg-red-50/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base text-red-700">Maintenance Mode</CardTitle>
|
||||
<CardDescription>
|
||||
Disable the public facing app temporarily. Only admins can access.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
<Label>Status</Label>
|
||||
<p className="text-sm font-medium">
|
||||
{false ? <span className="text-red-600">Active - App Offline</span> : <span className="text-emerald-600">Inactive - App Live</span>}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="destructive">Enable Maintenance</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-orange-200 bg-orange-50/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base text-orange-700">Cache Control</CardTitle>
|
||||
<CardDescription>
|
||||
Force purge the CDN cache for all static pages.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
<Label>Metrics</Label>
|
||||
<p className="text-xs text-muted-foreground">Items cached: ~14.2k</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-orange-300 text-orange-700 hover:bg-orange-100"
|
||||
onClick={handlePurgeCache}
|
||||
disabled={purgeLoading}
|
||||
>
|
||||
{purgeLoading ? (
|
||||
<Activity className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Purge Cache
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user