Files
doormile_crm/src/pages/Settings.jsx
2026-07-14 12:13:46 +05:30

361 lines
19 KiB
JavaScript

import { useState } from 'react';
import {
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';
const TIMEZONES = ['Asia/Kolkata (IST)', 'Asia/Dubai (GST)', 'UTC', 'America/New_York (EST)'];
const LANGUAGES = ['English', 'हिन्दी (Hindi)', 'العربية (Arabic)'];
const INITIAL_GENERAL = {
orgName: 'Doormile Technologies',
supportEmail: 'support@doormile.in',
contact: '+91 63749 46729',
timezone: TIMEZONES[0],
language: LANGUAGES[0]
};
const INITIAL_NOTIFY = {
newClient: true, contractStatus: true, teamAccess: false, databaseSync: true, emailAlerts: true, smsAlerts: false
};
const INITIAL_SECURITY = { currentPassword: '', newPassword: '', confirmPassword: '', twoFactor: false };
const NAV = [
{ icon: TuneOutlinedIcon, label: 'General', desc: 'Organisation profile' },
{ icon: NotificationsNoneIcon, label: 'Notifications', desc: 'Alerts & channels' },
{ icon: LockOutlinedIcon, label: 'Security', desc: 'Password & 2FA' }
];
const NOTIFY_ROWS = [
{ k: 'newClient', t: 'New client onboarded', d: 'Notify when a new client is added to the system' },
{ k: 'contractStatus', t: 'Contract status', d: 'When a client contract becomes active or expires' },
{ k: 'teamAccess', t: 'Team access', d: 'When a new team member is granted or revoked access' },
{ k: 'databaseSync', t: 'Database sync', d: 'Alerts for vector database synchronization events' }
];
const STRENGTH = [
{ 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++;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++;
if (/\d/.test(pw)) s++;
if (/[^A-Za-z0-9]/.test(pw)) s++;
return s;
};
// Section surface with a tinted icon header band.
function Section({ icon: Icon, title, subtitle, color = 'primary', danger = false, children }) {
return (
<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%)`
}}
>
<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>
);
}
// Two-column row: label + helper on the left, control on the right.
function Row({ label, description, children, align = 'center' }) {
return (
<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>
);
}
const rightAlign = { display: 'flex', justifyContent: { sm: 'flex-end' } };
function PasswordField({ label, value, onChange, autoComplete }) {
const [show, setShow] = useState(false);
return (
<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>
)
}}
/>
);
}
export default function Settings() {
const [tab, setTab] = useState(0);
const [toast, setToast] = useState(false);
const [dirty, setDirty] = useState(false);
const [general, setGeneral] = useState(INITIAL_GENERAL);
const [notify, setNotify] = useState(INITIAL_NOTIFY);
const [security, setSecurity] = useState(INITIAL_SECURITY);
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); };
const discard = () => {
setGeneral(INITIAL_GENERAL);
setNotify(INITIAL_NOTIFY);
setSecurity(INITIAL_SECURITY);
setDirty(false);
};
const pwScore = scorePassword(security.newPassword);
const pwMeta = STRENGTH[pwScore];
const mismatch = security.confirmPassword && security.newPassword !== security.confirmPassword;
return (
<>
<PageHeader
title="Settings"
breadcrumbs={[{ label: 'Settings' }]}
action={
<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>
}
/>
<Grid container spacing={2.5}>
{/* Sidebar */}
<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 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>
</Stack>
</Grid>
{/* Content */}
<Grid item xs={12} md={9}>
{tab === 0 && (
<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%)` }}
>
<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={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>
</Stack>
)}
{tab === 1 && (
<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>
</Stack>
)}
{tab === 2 && (
<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={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' } }}
/>
<Switch checked={security.twoFactor} onChange={(e) => { setSecurity((p) => ({ ...p, twoFactor: e.target.checked })); setDirty(true); }} />
</Stack>
</Row>
</Section>
<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>
</Stack>
)}
</Grid>
</Grid>
<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.
</Alert>
</Snackbar>
</>
);
}