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

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:
Andrei
2025-10-08 11:22:02 +00:00
parent 95d22fe633
commit fb03f2c2e9
3 changed files with 170 additions and 120 deletions

View File

@@ -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>