feat: Add real system settings to admin dashboard
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
Backend: - Added GET /admin/dashboard/settings endpoint - Added POST /admin/dashboard/settings endpoint - Returns settings from environment variables and defaults - Includes General, Security, Notification, Email, Storage, and API settings - Password fields are masked for security Frontend: - Removed deprecated MUI Grid import and components - Replaced all Grid layouts with CSS Grid - Connected settings page to real backend API - Added loading state during initial fetch - Added saving state with disabled button during save - Proper error handling for fetch and save operations - Settings now fetched from /admin/dashboard/settings on load - Form spans 2 columns on desktop, single column on mobile All 6 settings tabs now use real data from backend with proper responsive layout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
Divider,
|
||||
Grid,
|
||||
Alert,
|
||||
Select,
|
||||
MenuItem,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
Tabs,
|
||||
Tab,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Save,
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
Api,
|
||||
} from '@mui/icons-material';
|
||||
import AdminLayout from '@/components/AdminLayout';
|
||||
import apiClient from '@/lib/api-client';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -51,61 +52,49 @@ function TabPanel(props: TabPanelProps) {
|
||||
export default function SettingsPage() {
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
const [settings, setSettings] = useState({
|
||||
// General Settings
|
||||
siteName: 'ParentFlow',
|
||||
adminEmail: 'admin@parentflowapp.com',
|
||||
supportEmail: 'support@parentflowapp.com',
|
||||
timezone: 'America/New_York',
|
||||
language: 'en',
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<any>({});
|
||||
|
||||
// Security Settings
|
||||
enforcePasswordPolicy: true,
|
||||
minPasswordLength: 8,
|
||||
requireUppercase: true,
|
||||
requireNumbers: true,
|
||||
requireSpecialChars: true,
|
||||
sessionTimeout: 30,
|
||||
maxLoginAttempts: 5,
|
||||
enableTwoFactor: false,
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
// Notification Settings
|
||||
enableEmailNotifications: true,
|
||||
enablePushNotifications: true,
|
||||
adminNotifications: true,
|
||||
errorAlerts: true,
|
||||
newUserAlerts: true,
|
||||
systemHealthAlerts: true,
|
||||
|
||||
// Email Settings
|
||||
smtpHost: 'smtp.gmail.com',
|
||||
smtpPort: 587,
|
||||
smtpUser: 'noreply@parentflowapp.com',
|
||||
smtpPassword: '********',
|
||||
emailFrom: 'ParentFlow <noreply@parentflowapp.com>',
|
||||
|
||||
// Storage Settings
|
||||
maxFileSize: 10,
|
||||
allowedFileTypes: 'jpg,jpeg,png,pdf,doc,docx',
|
||||
storageProvider: 'minio',
|
||||
s3Bucket: 'parentflow-files',
|
||||
retentionDays: 90,
|
||||
|
||||
// API Settings
|
||||
rateLimit: 100,
|
||||
rateLimitWindow: 60,
|
||||
apiTimeout: 30,
|
||||
enableGraphQL: true,
|
||||
enableWebSockets: true,
|
||||
corsOrigins: 'https://web.parentflowapp.com',
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
// Save settings logic here
|
||||
setSaveSuccess(true);
|
||||
setTimeout(() => setSaveSuccess(false), 3000);
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await apiClient.get('/admin/dashboard/settings');
|
||||
setSettings(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
await apiClient.post('/admin/dashboard/settings', settings);
|
||||
setSaveSuccess(true);
|
||||
setTimeout(() => setSaveSuccess(false), 3000);
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '400px' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
@@ -140,16 +129,15 @@ export default function SettingsPage() {
|
||||
<Box sx={{ p: 3 }}>
|
||||
{/* General Settings */}
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Site Name"
|
||||
value={settings.siteName}
|
||||
onChange={(e) => setSettings({ ...settings, siteName: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Admin Email"
|
||||
@@ -157,8 +145,8 @@ export default function SettingsPage() {
|
||||
value={settings.adminEmail}
|
||||
onChange={(e) => setSettings({ ...settings, adminEmail: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Support Email"
|
||||
@@ -166,8 +154,8 @@ export default function SettingsPage() {
|
||||
value={settings.supportEmail}
|
||||
onChange={(e) => setSettings({ ...settings, supportEmail: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Timezone</InputLabel>
|
||||
<Select
|
||||
@@ -184,8 +172,8 @@ export default function SettingsPage() {
|
||||
<MenuItem value="Asia/Tokyo">Tokyo</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Language</InputLabel>
|
||||
<Select
|
||||
@@ -200,8 +188,8 @@ export default function SettingsPage() {
|
||||
<MenuItem value="zh">Chinese</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
</TabPanel>
|
||||
|
||||
{/* Security Settings */}
|
||||
@@ -220,8 +208,7 @@ export default function SettingsPage() {
|
||||
/>
|
||||
{settings.enforcePasswordPolicy && (
|
||||
<Box sx={{ ml: 4, mt: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, mb: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
@@ -231,8 +218,7 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, minPasswordLength: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
</Box>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -268,13 +254,12 @@ export default function SettingsPage() {
|
||||
label="Require Special Characters"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
</Box>
|
||||
)}
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
@@ -284,8 +269,8 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, sessionTimeout: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
@@ -295,8 +280,7 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, maxLoginAttempts: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
@@ -390,16 +374,15 @@ export default function SettingsPage() {
|
||||
|
||||
{/* Email Settings */}
|
||||
<TabPanel value={tabValue} index={3}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="SMTP Host"
|
||||
value={settings.smtpHost}
|
||||
onChange={(e) => setSettings({ ...settings, smtpHost: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
@@ -409,16 +392,16 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, smtpPort: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="SMTP Username"
|
||||
value={settings.smtpUser}
|
||||
onChange={(e) => setSettings({ ...settings, smtpUser: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="password"
|
||||
@@ -426,25 +409,21 @@ export default function SettingsPage() {
|
||||
value={settings.smtpPassword}
|
||||
onChange={(e) => setSettings({ ...settings, smtpPassword: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="From Address"
|
||||
value={settings.emailFrom}
|
||||
onChange={(e) => setSettings({ ...settings, emailFrom: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Button variant="outlined">Test Email Configuration</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Button variant="outlined" sx={{ gridColumn: { xs: '1', md: 'span 2' } }}>Test Email Configuration</Button>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* Storage Settings */}
|
||||
<TabPanel value={tabValue} index={4}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
@@ -454,8 +433,8 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, maxFileSize: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Allowed File Types"
|
||||
@@ -463,8 +442,8 @@ export default function SettingsPage() {
|
||||
onChange={(e) => setSettings({ ...settings, allowedFileTypes: e.target.value })}
|
||||
helperText="Comma-separated list of extensions"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Storage Provider</InputLabel>
|
||||
<Select
|
||||
@@ -478,16 +457,16 @@ export default function SettingsPage() {
|
||||
<MenuItem value="gcs">Google Cloud Storage</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="S3 Bucket Name"
|
||||
value={settings.s3Bucket}
|
||||
onChange={(e) => setSettings({ ...settings, s3Bucket: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
@@ -497,14 +476,12 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, retentionDays: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* API Settings */}
|
||||
<TabPanel value={tabValue} index={5}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
@@ -514,8 +491,8 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, rateLimit: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
@@ -525,8 +502,8 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, rateLimitWindow: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
@@ -536,9 +513,7 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, apiTimeout: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormGroup>
|
||||
<FormGroup sx={{ gridColumn: { xs: '1', md: 'span 2' } }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
@@ -562,8 +537,6 @@ export default function SettingsPage() {
|
||||
label="Enable WebSockets"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
@@ -572,9 +545,9 @@ export default function SettingsPage() {
|
||||
value={settings.corsOrigins}
|
||||
onChange={(e) => setSettings({ ...settings, corsOrigins: e.target.value })}
|
||||
helperText="One origin per line"
|
||||
sx={{ gridColumn: { xs: '1', md: 'span 2' } }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* Save Button */}
|
||||
@@ -582,10 +555,11 @@ export default function SettingsPage() {
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<Save />}
|
||||
startIcon={saving ? <CircularProgress size={20} /> : <Save />}
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
Save Settings
|
||||
{saving ? 'Saving...' : 'Save Settings'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user