@@ -1,27 +1,22 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Save,
|
||||
Sliders,
|
||||
Bell,
|
||||
Lock,
|
||||
Shield,
|
||||
Megaphone,
|
||||
Building,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
HelpCircle,
|
||||
LogOut,
|
||||
Eye,
|
||||
EyeOff
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { List, ListItem } from '@astryxdesign/core/List';
|
||||
import { Text, Heading } from '@astryxdesign/core/Text';
|
||||
import { TextInput } from '@astryxdesign/core/TextInput';
|
||||
import { Selector } from '@astryxdesign/core/Selector';
|
||||
import { Switch } from '@astryxdesign/core/Switch';
|
||||
import { Badge } from '@astryxdesign/core/Badge';
|
||||
Grid, Card, Box, Stack, TextField, MenuItem, Switch, Button, Typography, Divider,
|
||||
Snackbar, Alert, Chip, IconButton, InputAdornment, LinearProgress, Avatar
|
||||
} from '@mui/material';
|
||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined';
|
||||
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
|
||||
import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined';
|
||||
import BusinessOutlinedIcon from '@mui/icons-material/BusinessOutlined';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded';
|
||||
import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded';
|
||||
import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
|
||||
@@ -41,9 +36,9 @@ const INITIAL_NOTIFY = {
|
||||
const INITIAL_SECURITY = { currentPassword: '', newPassword: '', confirmPassword: '', twoFactor: false };
|
||||
|
||||
const NAV = [
|
||||
{ icon: Sliders, label: 'General', desc: 'Organisation profile' },
|
||||
{ icon: Bell, label: 'Notifications', desc: 'Alerts & channels' },
|
||||
{ icon: Lock, label: 'Security', desc: 'Password & 2FA' }
|
||||
{ icon: TuneOutlinedIcon, label: 'General', desc: 'Organisation profile' },
|
||||
{ icon: NotificationsNoneIcon, label: 'Notifications', desc: 'Alerts & channels' },
|
||||
{ icon: LockOutlinedIcon, label: 'Security', desc: 'Password & 2FA' }
|
||||
];
|
||||
|
||||
const NOTIFY_ROWS = [
|
||||
@@ -54,13 +49,12 @@ const NOTIFY_ROWS = [
|
||||
];
|
||||
|
||||
const STRENGTH = [
|
||||
{ label: 'Too weak', color: '#ef4444' },
|
||||
{ label: 'Weak', color: '#ef4444' },
|
||||
{ label: 'Fair', color: '#f59e0b' },
|
||||
{ label: 'Good', color: '#3b82f6' },
|
||||
{ label: 'Strong', color: '#10b981' }
|
||||
{ label: 'Too weak', color: 'error' },
|
||||
{ label: 'Weak', color: 'error' },
|
||||
{ label: 'Fair', color: 'warning' },
|
||||
{ label: 'Good', color: 'info' },
|
||||
{ label: 'Strong', color: 'success' }
|
||||
];
|
||||
|
||||
const scorePassword = (pw) => {
|
||||
let s = 0;
|
||||
if (pw.length >= 8) s++;
|
||||
@@ -70,89 +64,63 @@ const scorePassword = (pw) => {
|
||||
return s;
|
||||
};
|
||||
|
||||
function Section({ icon: Icon, title, subtitle, danger = false, children }) {
|
||||
// Section surface with a tinted icon header band.
|
||||
function Section({ icon: Icon, title, subtitle, color = 'primary', danger = false, children }) {
|
||||
return (
|
||||
<Card style={{ padding: '0', borderRadius: '16px', border: danger ? '1px solid #fca5a5' : '1px solid rgba(5, 54, 89, 0.08)', backgroundColor: '#ffffff', overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: '18px 24px',
|
||||
borderBottom: '1px solid rgba(5, 54, 89, 0.06)',
|
||||
background: danger ? 'linear-gradient(90deg, rgba(239, 68, 68, 0.04) 0%, #ffffff 72%)' : 'linear-gradient(90deg, rgba(192, 18, 39, 0.04) 0%, #ffffff 72%)',
|
||||
gap: '12px'
|
||||
<Card sx={danger ? { borderColor: 'error.light' } : undefined}>
|
||||
<Stack
|
||||
direction="row" spacing={1.75} alignItems="center"
|
||||
sx={{
|
||||
px: { xs: 2, sm: 3 }, py: 2.25, borderBottom: 1, borderColor: 'divider',
|
||||
background: (theme) => `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, ${theme.palette.background.paper} 72%)`
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '40px', height: '40px', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: danger ? 'rgba(239, 68, 68, 0.08)' : 'rgba(192, 18, 39, 0.08)', color: danger ? '#ef4444' : '#C01227' }}>
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<Heading level={5}>{title}</Heading>
|
||||
{subtitle && <Text type="supporting" color="secondary">{subtitle}</Text>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '4px 24px' }}>{children}</div>
|
||||
<Box sx={{ width: 40, height: 40, borderRadius: 2, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: `${color}.lighter`, color: `${color}.main` }}>
|
||||
<Icon fontSize="small" />
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.3 }}>{title}</Typography>
|
||||
{subtitle && <Typography variant="caption" color="text.secondary">{subtitle}</Typography>}
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box sx={{ px: { xs: 2, sm: 3 } }}>{children}</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, description, controlAlign = 'stretch', children }) {
|
||||
// Two-column row: label + helper on the left, control on the right.
|
||||
function Row({ label, description, children, align = 'center' }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', padding: '16px 0', borderBottom: '1px solid #f1f5f9', flexWrap: 'wrap', gap: '12px' }} className="settings-row">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px', width: '240px', flexShrink: 0 }}>
|
||||
<Text type="body" weight="semibold">{label}</Text>
|
||||
{description && <Text type="supporting" color="secondary">{description}</Text>}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
minWidth: '220px',
|
||||
display: controlAlign === 'end' ? 'flex' : 'block',
|
||||
justifyContent: controlAlign === 'end' ? 'flex-end' : undefined
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<Grid
|
||||
container spacing={2} alignItems={align}
|
||||
sx={{ py: 2.5, borderRadius: 2, transition: 'background-color .15s', '&:hover': { bgcolor: 'grey.50' } }}
|
||||
>
|
||||
<Grid item xs={12} sm={5}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'grey.800' }}>{label}</Typography>
|
||||
{description && <Typography variant="caption" color="text.secondary">{description}</Typography>}
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={7}>{children}</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
function PasswordField({ label, value, onChange }) {
|
||||
const rightAlign = { display: 'flex', justifyContent: { sm: 'flex-end' } };
|
||||
|
||||
function PasswordField({ label, value, onChange, autoComplete }) {
|
||||
const [show, setShow] = useState(false);
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
<TextInput
|
||||
label={label}
|
||||
isLabelHidden
|
||||
type={show ? 'text' : 'password'}
|
||||
placeholder={label}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShow((s) => !s)}
|
||||
aria-label={show ? 'Hide password' : 'Show password'}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '10px',
|
||||
bottom: 0,
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: '#64748b',
|
||||
padding: 0,
|
||||
zIndex: 5
|
||||
}}
|
||||
>
|
||||
{show ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
<TextField
|
||||
fullWidth size="small" type={show ? 'text' : 'password'} label={label} value={value} onChange={onChange} autoComplete={autoComplete}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShow((s) => !s)} edge="end" size="small">
|
||||
{show ? <VisibilityOff fontSize="small" /> : <Visibility fontSize="small" />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,11 +133,11 @@ export default function Settings() {
|
||||
const [notify, setNotify] = useState(INITIAL_NOTIFY);
|
||||
const [security, setSecurity] = useState(INITIAL_SECURITY);
|
||||
|
||||
const setG = (k) => (val) => { setGeneral((p) => ({ ...p, [k]: val })); setDirty(true); };
|
||||
const setN = (k) => (val) => { setNotify((p) => ({ ...p, [k]: val })); setDirty(true); };
|
||||
const setSText = (k) => (val) => { setSecurity((p) => ({ ...p, [k]: val })); setDirty(true); };
|
||||
const setG = (k) => (e) => { setGeneral((p) => ({ ...p, [k]: e.target.value })); setDirty(true); };
|
||||
const setN = (k) => (e) => { setNotify((p) => ({ ...p, [k]: e.target.checked })); setDirty(true); };
|
||||
const setSText = (k) => (e) => { setSecurity((p) => ({ ...p, [k]: e.target.value })); setDirty(true); };
|
||||
|
||||
const save = () => { setToast(true); setDirty(false); setTimeout(() => setToast(false), 2500); };
|
||||
const save = () => { setToast(true); setDirty(false); };
|
||||
const discard = () => {
|
||||
setGeneral(INITIAL_GENERAL);
|
||||
setNotify(INITIAL_NOTIFY);
|
||||
@@ -182,211 +150,211 @@ export default function Settings() {
|
||||
const mismatch = security.confirmPassword && security.newPassword !== security.confirmPassword;
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif' }}>
|
||||
<>
|
||||
<PageHeader
|
||||
title="Settings"
|
||||
breadcrumbs={[{ label: 'Settings' }]}
|
||||
action={
|
||||
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
|
||||
{dirty && <Badge variant="warning" label="Unsaved changes" />}
|
||||
<Button variant="ghost" onClick={discard} label="Discard" />
|
||||
<Button variant="primary" icon={<Save size={14} />} onClick={save} label="Save Changes" />
|
||||
</div>
|
||||
<Stack
|
||||
direction="row" spacing={1.5} alignItems="center" useFlexGap flexWrap="wrap"
|
||||
sx={{ width: { xs: '100%', sm: 'auto' }, justifyContent: { xs: 'flex-start', sm: 'flex-end' } }}
|
||||
>
|
||||
{dirty && <Chip size="small" label="Unsaved changes" sx={{ bgcolor: 'warning.lighter', color: 'warning.dark', fontWeight: 600 }} />}
|
||||
<Button variant="outlined" onClick={discard} sx={{ flex: { xs: 1, sm: 'none' } }}>Discard</Button>
|
||||
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save} sx={{ flex: { xs: 1, sm: 'none' } }}>Save Changes</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', lg: 'repeat(12, 1fr)', gap: '24px', marginTop: '24px' }} className="responsive-row">
|
||||
<Grid container spacing={2.5}>
|
||||
{/* Sidebar */}
|
||||
<div style={{ gridColumn: 'span 3' }} className="col-span-3">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<Card style={{ padding: '8px' }}>
|
||||
<List density="spacious">
|
||||
{NAV.map((item, i) => (
|
||||
<ListItem
|
||||
key={item.label}
|
||||
label={item.label}
|
||||
description={item.desc}
|
||||
startContent={<item.icon size={16} />}
|
||||
isSelected={tab === i}
|
||||
onClick={() => setTab(i)}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
<Grid item xs={12} md={3}>
|
||||
<Stack spacing={2.5} sx={{ position: { md: 'sticky' }, top: { md: 88 } }}>
|
||||
<Card sx={{ p: 1.5 }}>
|
||||
<Typography variant="overline" sx={{ px: 1, color: 'text.secondary', fontWeight: 700, letterSpacing: 0.6 }}>Preferences</Typography>
|
||||
<Stack spacing={0.25} sx={{ mt: 0.5 }}>
|
||||
{NAV.map((item, i) => {
|
||||
const active = tab === i;
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Stack
|
||||
key={item.label}
|
||||
direction="row" spacing={1.5} alignItems="center"
|
||||
onClick={() => setTab(i)}
|
||||
sx={{
|
||||
px: 1.5, py: 1.25, borderRadius: 2, cursor: 'pointer', position: 'relative',
|
||||
bgcolor: active ? 'primary.lighter' : 'transparent',
|
||||
transition: 'background-color .15s',
|
||||
'&:hover': { bgcolor: active ? 'primary.lighter' : 'grey.50' },
|
||||
'&::before': active ? { content: '""', position: 'absolute', left: 0, top: 9, bottom: 9, width: 3, borderRadius: 3, bgcolor: 'primary.main' } : {}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: 34, height: 34, borderRadius: 1.5, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: active ? 'primary.main' : 'grey.100', color: active ? '#fff' : 'grey.600' }}>
|
||||
<Icon fontSize="small" />
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: active ? 'primary.main' : 'grey.800' }}>{item.label}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{item.desc}</Typography>
|
||||
</Box>
|
||||
<ChevronRightIcon sx={{ fontSize: 18, color: active ? 'primary.main' : 'grey.300' }} />
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card style={{ padding: '20px', backgroundColor: 'rgba(192, 18, 39, 0.05)', border: '1px solid rgba(192, 18, 39, 0.1)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<div style={{ width: '38px', height: '38px', borderRadius: '8px', backgroundColor: '#C01227', color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<HelpCircle size={16} />
|
||||
</div>
|
||||
<div>
|
||||
<Text type="body" weight="bold" color="accent">Need a hand?</Text>
|
||||
<div style={{ marginTop: '2px' }}>
|
||||
<Text type="supporting" color="accent">Our team is available 24/7 for operational support.</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="primary" label="Contact support" />
|
||||
</div>
|
||||
<Card sx={{ p: 2.5, bgcolor: 'primary.lighter', borderColor: 'primary.100' }}>
|
||||
<Stack spacing={1.25}>
|
||||
<Box sx={{ width: 38, height: 38, borderRadius: 2, bgcolor: 'primary.main', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<HelpOutlineRoundedIcon fontSize="small" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.dark' }}>Need a hand?</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'primary.dark', opacity: 0.85 }}>Our team is available 24/7 for operational support.</Typography>
|
||||
</Box>
|
||||
<Button size="small" variant="contained" sx={{ alignSelf: 'flex-start' }}>Contact support</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ gridColumn: 'span 9' }} className="col-span-9">
|
||||
<Grid item xs={12} md={9}>
|
||||
{tab === 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
{/* Org banner */}
|
||||
<Card style={{ padding: '0', overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '20px',
|
||||
alignItems: 'center',
|
||||
padding: '24px',
|
||||
background: 'linear-gradient(90deg, rgba(192, 18, 39, 0.05) 0%, #ffffff 75%)',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
<Stack spacing={2.5}>
|
||||
{/* Organisation identity banner */}
|
||||
<Card sx={{ overflow: 'hidden' }}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }} spacing={2.5} alignItems={{ sm: 'center' }}
|
||||
sx={{ p: 3, background: (theme) => `linear-gradient(90deg, ${theme.palette.primary.lighter}88 0%, ${theme.palette.background.paper} 75%)` }}
|
||||
>
|
||||
<div style={{ width: '64px', height: '64px', borderRadius: '8px', backgroundColor: '#C01227', color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<Building size={32} />
|
||||
</div>
|
||||
<div style={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Heading level={4}>{general.orgName}</Heading>
|
||||
<Badge variant="success" icon={<CheckCircle2 size={12} />} label="Verified" />
|
||||
</div>
|
||||
<Text type="supporting" color="secondary">{general.supportEmail} · {general.contact}</Text>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm" label="Change logo" />
|
||||
</div>
|
||||
<Avatar variant="rounded" sx={{ width: 64, height: 64, bgcolor: 'primary.main', color: '#fff' }}>
|
||||
<BusinessOutlinedIcon />
|
||||
</Avatar>
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.800' }}>{general.orgName}</Typography>
|
||||
<Chip size="small" icon={<VerifiedOutlinedIcon sx={{ fontSize: 15, ml: 0.5 }} />} label="Verified" sx={{ fontWeight: 700, bgcolor: 'success.lighter', color: 'success.dark', '& .MuiChip-icon': { color: 'inherit' } }} />
|
||||
</Stack>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.25 }}>{general.supportEmail} · {general.contact}</Typography>
|
||||
</Box>
|
||||
<Button variant="outlined" size="small">Change logo</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Section icon={Sliders} title="Organisation" subtitle="Profile and regional preferences">
|
||||
<Row label="Organisation name" description="Shown on invoices and exports">
|
||||
<TextInput label="Organisation name" isLabelHidden width="100%" value={general.orgName} onChange={setG('orgName')} />
|
||||
</Row>
|
||||
<Row label="Support email" description="Where customer replies are routed">
|
||||
<TextInput label="Support email" isLabelHidden width="100%" type="email" value={general.supportEmail} onChange={setG('supportEmail')} />
|
||||
</Row>
|
||||
<Row label="Contact number" description="Primary operations line">
|
||||
<TextInput label="Contact number" isLabelHidden width="100%" value={general.contact} onChange={setG('contact')} />
|
||||
</Row>
|
||||
<Row label="Timezone" description="Used for schedules and reports">
|
||||
<div style={{ width: '100%' }}>
|
||||
<Selector label="Timezone" isLabelHidden options={TIMEZONES} value={general.timezone} onChange={(v) => setG('timezone')(v || '')} />
|
||||
</div>
|
||||
</Row>
|
||||
<Row label="Language" description="Console display language">
|
||||
<div style={{ width: '100%' }}>
|
||||
<Selector label="Language" isLabelHidden options={LANGUAGES} value={general.language} onChange={(v) => setG('language')(v || '')} />
|
||||
</div>
|
||||
</Row>
|
||||
<Section icon={TuneOutlinedIcon} title="Organisation" subtitle="Profile and regional preferences" color="primary">
|
||||
<Stack divider={<Divider />}>
|
||||
<Row label="Organisation name" description="Shown on invoices and exports">
|
||||
<TextField fullWidth size="small" value={general.orgName} onChange={setG('orgName')} />
|
||||
</Row>
|
||||
<Row label="Support email" description="Where customer replies are routed">
|
||||
<TextField fullWidth size="small" value={general.supportEmail} onChange={setG('supportEmail')} />
|
||||
</Row>
|
||||
<Row label="Contact number" description="Primary operations line">
|
||||
<TextField fullWidth size="small" value={general.contact} onChange={setG('contact')} />
|
||||
</Row>
|
||||
<Row label="Timezone" description="Used for schedules and reports">
|
||||
<TextField select fullWidth size="small" value={general.timezone} onChange={setG('timezone')}>
|
||||
{TIMEZONES.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</TextField>
|
||||
</Row>
|
||||
<Row label="Language" description="Console display language">
|
||||
<TextField select fullWidth size="small" value={general.language} onChange={setG('language')}>
|
||||
{LANGUAGES.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
</Row>
|
||||
</Stack>
|
||||
</Section>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{tab === 1 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<Section icon={Bell} title="Notification Preferences" subtitle="Choose what you get alerted about">
|
||||
{NOTIFY_ROWS.map((row) => (
|
||||
<Row key={row.k} label={row.t} description={row.d} controlAlign="end">
|
||||
<Switch label={row.t} isLabelHidden value={notify[row.k]} onChange={setN(row.k)} />
|
||||
<Stack spacing={2.5}>
|
||||
<Section icon={NotificationsNoneIcon} title="Notification Preferences" subtitle="Choose what you get alerted about" color="primary">
|
||||
<Stack divider={<Divider />}>
|
||||
{NOTIFY_ROWS.map((row) => (
|
||||
<Row key={row.k} label={row.t} description={row.d}>
|
||||
<Box sx={rightAlign}><Switch checked={notify[row.k]} onChange={setN(row.k)} /></Box>
|
||||
</Row>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
<Section icon={CampaignOutlinedIcon} title="Delivery Channels" subtitle="How alerts reach your team" color="primary">
|
||||
<Stack divider={<Divider />}>
|
||||
<Row label="Email alerts" description="Send notifications to the support inbox">
|
||||
<Box sx={rightAlign}><Switch checked={notify.emailAlerts} onChange={setN('emailAlerts')} /></Box>
|
||||
</Row>
|
||||
))}
|
||||
<Row label="SMS alerts" description="Send notifications to the registered mobile">
|
||||
<Box sx={rightAlign}><Switch checked={notify.smsAlerts} onChange={setN('smsAlerts')} /></Box>
|
||||
</Row>
|
||||
</Stack>
|
||||
</Section>
|
||||
<Section icon={Megaphone} title="Delivery Channels" subtitle="How alerts reach your team">
|
||||
<Row label="Email alerts" description="Send notifications to the support inbox" controlAlign="end">
|
||||
<Switch label="Email alerts" isLabelHidden value={notify.emailAlerts} onChange={setN('emailAlerts')} />
|
||||
</Row>
|
||||
<Row label="SMS alerts" description="Send notifications to the registered mobile" controlAlign="end">
|
||||
<Switch label="SMS alerts" isLabelHidden value={notify.smsAlerts} onChange={setN('smsAlerts')} />
|
||||
</Row>
|
||||
</Section>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{tab === 2 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<Section icon={Lock} title="Change Password" subtitle="Use 8+ characters with a mix of letters, numbers & symbols">
|
||||
<Row label="Current password" description="Enter your existing password">
|
||||
<PasswordField label="Current password" value={security.currentPassword} onChange={setSText('currentPassword')} />
|
||||
</Row>
|
||||
<Row label="New password" description="Choose a strong, unique password">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', width: '100%' }}>
|
||||
<PasswordField label="New password" value={security.newPassword} onChange={setSText('newPassword')} />
|
||||
{security.newPassword && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginTop: '4px' }}>
|
||||
<div style={{ flexGrow: 1, height: '6px', borderRadius: '3px', backgroundColor: '#f1f5f9', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', backgroundColor: pwMeta.color, width: `${(pwScore / 4) * 100}%` }} />
|
||||
</div>
|
||||
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: pwMeta.color, minWidth: '56px' }}>{pwMeta.label}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
<Row label="Confirm new password" description="Re-enter the new password">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', width: '100%' }}>
|
||||
<PasswordField label="Confirm new password" value={security.confirmPassword} onChange={setSText('confirmPassword')} />
|
||||
{mismatch && <span style={{ fontSize: '0.75rem', color: '#ef4444', display: 'block', marginTop: '4px' }}>Passwords do not match</span>}
|
||||
</div>
|
||||
</Row>
|
||||
<Stack spacing={2.5}>
|
||||
<Section icon={LockOutlinedIcon} title="Change Password" subtitle="Use 8+ characters with a mix of letters, numbers & symbols" color="primary">
|
||||
<Stack divider={<Divider />}>
|
||||
<Row label="Current password" description="Enter your existing password" align="flex-start">
|
||||
<PasswordField label="Current password" value={security.currentPassword} onChange={setSText('currentPassword')} autoComplete="current-password" />
|
||||
</Row>
|
||||
<Row label="New password" description="Choose a strong, unique password" align="flex-start">
|
||||
<Box>
|
||||
<PasswordField label="New password" value={security.newPassword} onChange={setSText('newPassword')} autoComplete="new-password" />
|
||||
{security.newPassword && (
|
||||
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mt: 1 }}>
|
||||
<LinearProgress variant="determinate" value={(pwScore / 4) * 100} color={pwMeta.color} sx={{ flexGrow: 1, height: 6, borderRadius: 3 }} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: `${pwMeta.color}.main`, minWidth: 56 }}>{pwMeta.label}</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Row>
|
||||
<Row label="Confirm new password" description="Re-enter the new password" align="flex-start">
|
||||
<Box>
|
||||
<PasswordField label="Confirm new password" value={security.confirmPassword} onChange={setSText('confirmPassword')} autoComplete="new-password" />
|
||||
{mismatch && <Typography variant="caption" color="error.main" sx={{ mt: 0.75, display: 'block' }}>Passwords do not match</Typography>}
|
||||
</Box>
|
||||
</Row>
|
||||
</Stack>
|
||||
</Section>
|
||||
|
||||
<Section icon={Shield} title="Two-Factor Authentication" subtitle="Add an extra layer of security to your account">
|
||||
<Row label="Authenticator app" description="Require a one-time code at sign-in for extra security" controlAlign="end">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Badge variant={security.twoFactor ? 'success' : 'neutral'} label={security.twoFactor ? 'Enabled' : 'Disabled'} />
|
||||
<Switch
|
||||
label="Two-factor authentication"
|
||||
isLabelHidden
|
||||
value={security.twoFactor}
|
||||
onChange={(v) => { setSecurity((p) => ({ ...p, twoFactor: v })); setDirty(true); }}
|
||||
<Section icon={ShieldOutlinedIcon} title="Two-Factor Authentication" subtitle="Add an extra layer of security to your account" color="primary">
|
||||
<Row label="Authenticator app" description="Require a one-time code at sign-in for extra security">
|
||||
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ justifyContent: { sm: 'flex-end' } }}>
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<ShieldOutlinedIcon sx={{ fontSize: 15, ml: 0.5 }} />}
|
||||
label={security.twoFactor ? 'Enabled' : 'Disabled'}
|
||||
sx={{ fontWeight: 700, bgcolor: security.twoFactor ? 'success.lighter' : 'grey.100', color: security.twoFactor ? 'success.dark' : 'grey.600', '& .MuiChip-icon': { color: 'inherit' } }}
|
||||
/>
|
||||
</div>
|
||||
<Switch checked={security.twoFactor} onChange={(e) => { setSecurity((p) => ({ ...p, twoFactor: e.target.checked })); setDirty(true); }} />
|
||||
</Stack>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Section icon={AlertTriangle} title="Danger Zone" subtitle="Irreversible and high-impact actions" danger>
|
||||
<Row label="Sign out of all sessions" description="End every active session on all devices" controlAlign="end">
|
||||
<Button variant="destructive" onClick={() => { localStorage.removeItem('auth_token'); window.location.href = '/login'; }} label="Sign out everywhere" icon={<LogOut size={14} />} />
|
||||
<Section icon={WarningAmberRoundedIcon} title="Danger Zone" subtitle="Irreversible and high-impact actions" color="error" danger>
|
||||
<Row label="Sign out of all sessions" description="End every active session on all devices">
|
||||
<Box sx={rightAlign}>
|
||||
<Button variant="outlined" color="error" startIcon={<LogoutOutlinedIcon />} onClick={() => { localStorage.removeItem('auth_token'); window.location.href = '/login'; }}>Sign out everywhere</Button>
|
||||
</Box>
|
||||
</Row>
|
||||
</Section>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{toast && (
|
||||
<div style={{ position: 'fixed', bottom: '24px', left: '50%', transform: 'translateX(-50%)', backgroundColor: '#0f172a', color: '#ffffff', padding: '12px 24px', borderRadius: '8px', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)', zIndex: 3000, display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.875rem' }}>
|
||||
<CheckCircle2 size={16} style={{ color: '#10b981' }} />
|
||||
<Snackbar
|
||||
open={toast}
|
||||
autoHideDuration={2500}
|
||||
onClose={() => setToast(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="success" variant="filled" onClose={() => setToast(false)} sx={{ width: '100%' }}>
|
||||
Settings saved successfully.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
@media (min-width: 1024px) {
|
||||
.responsive-row {
|
||||
grid-template-columns: repeat(12, 1fr) !important;
|
||||
}
|
||||
.col-span-3 {
|
||||
grid-column: span 3 !important;
|
||||
}
|
||||
.col-span-9 {
|
||||
grid-column: span 9 !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1023px) {
|
||||
.responsive-row {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
.col-span-3, .col-span-9 {
|
||||
grid-column: span 1 !important;
|
||||
}
|
||||
}
|
||||
.settings-row:last-child {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user