updates on the astryx desing
This commit is contained in:
@@ -1,78 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
import { Avatar, Box, Button, Chip, Grid, InputLabel, Paper, Stack, TextField, Typography } from '@mui/material';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { MdBadge, MdPerson, MdLocationOn, MdMail, MdShield, MdVerifiedUser, MdLock } from 'react-icons/md';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
import { Grid } from '@astryxdesign/core/Grid';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { TextInput } from '@astryxdesign/core/TextInput';
|
||||
import { Badge } from '@astryxdesign/core/Badge';
|
||||
import { Avatar } from '@astryxdesign/core/Avatar';
|
||||
import { Divider } from '@astryxdesign/core/Divider';
|
||||
|
||||
import Loader from 'components/Loader';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import { getusers } from 'pages/api/api';
|
||||
import { updateProfilePassword } from 'pages/api/doormileApi';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { DT, tint, soft } from 'themes/dt/tokens';
|
||||
import { DT } from 'themes/dt/tokens';
|
||||
import { AccentAvatar } from 'themes/dt/primitives';
|
||||
|
||||
const ROLE_LABELS = { 1: 'Admin', 3: 'Manager', 4: 'Executive' };
|
||||
|
||||
const BRAND = '#000000';
|
||||
const BRAND_LIGHT = '#3A3A3A';
|
||||
const initialsOf = (name) =>
|
||||
(name || '')
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0])
|
||||
.join('')
|
||||
.toUpperCase() || '–';
|
||||
const BRAND = DT.brand;
|
||||
|
||||
// A single read-only field rendered as an icon + uppercase label + value.
|
||||
const InfoField = ({ icon: Icon, label, value, accent = BRAND }) => (
|
||||
<Stack direction="row" spacing={1.5} alignItems="flex-start" sx={{ minWidth: 0 }}>
|
||||
<Avatar sx={{ width: 36, height: 36, bgcolor: soft(accent), color: accent, flexShrink: 0 }}>
|
||||
<HStack gap={3} vAlign="start">
|
||||
<AccentAvatar color={accent} selected size={36}>
|
||||
<Icon size={18} />
|
||||
</Avatar>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
sx={{ display: 'block', color: DT.textMuted, lineHeight: 1.6, letterSpacing: '0.06em' }}
|
||||
>
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text type="label" weight="semibold" color="secondary">
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ fontWeight: 600, color: DT.textPrimary, wordBreak: 'break-word' }}
|
||||
title={value || ''}
|
||||
>
|
||||
</Text>
|
||||
<Text weight="semibold" title={value || ''}>
|
||||
{value || '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
// A titled grouping card.
|
||||
const SectionCard = ({ title, icon: Icon, children }) => (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: DT.radiusCard + 'px',
|
||||
border: `1px solid ${DT.borderSubtle}`,
|
||||
boxShadow: DT.shadowSoft,
|
||||
overflow: 'hidden',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1.25}
|
||||
sx={{ px: 2.5, py: 1.75, borderBottom: `1px solid ${DT.divider}`, bgcolor: DT.surfaceAlt }}
|
||||
>
|
||||
<Avatar sx={{ width: 28, height: 28, bgcolor: soft(BRAND), color: BRAND }}>
|
||||
<Card padding={0} elevation="none">
|
||||
<HStack gap={2} vAlign="center" padding={3} style={{ borderBottom: `1px solid ${DT.divider}`, backgroundColor: DT.surfaceAlt }}>
|
||||
<AccentAvatar color={BRAND} selected size={28}>
|
||||
<Icon size={16} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
{title}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Box sx={{ p: 2.5 }}>{children}</Box>
|
||||
</Paper>
|
||||
</AccentAvatar>
|
||||
<Text weight="bold">{title}</Text>
|
||||
</HStack>
|
||||
<VStack gap={0} padding={3}>
|
||||
{children}
|
||||
</VStack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const ViewProfile = () => {
|
||||
@@ -85,50 +67,31 @@ const ViewProfile = () => {
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
|
||||
const opentoast = (message, variant = 'error') =>
|
||||
enqueueSnackbar(message, { variant, anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 });
|
||||
|
||||
const changePasswordMutation = useMutation({
|
||||
mutationFn: () => updateProfilePassword(currentPassword, newPassword),
|
||||
onSuccess: (res) => {
|
||||
if (res.success) {
|
||||
enqueueSnackbar('Password updated successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
opentoast('Password updated successfully', 'success');
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
} else {
|
||||
enqueueSnackbar(res.message || 'Failed to update password', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
opentoast(res.message || 'Failed to update password');
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to update password', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to update password')
|
||||
});
|
||||
|
||||
const handleChangePassword = () => {
|
||||
if (!currentPassword || !newPassword) {
|
||||
enqueueSnackbar('Fill current and new password', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
opentoast('Fill current and new password');
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
enqueueSnackbar('New password and confirmation do not match', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
opentoast('New password and confirmation do not match');
|
||||
return;
|
||||
}
|
||||
changePasswordMutation.mutate();
|
||||
@@ -145,146 +108,75 @@ const ViewProfile = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<>
|
||||
<Loader />
|
||||
<CircularLoader />
|
||||
</>
|
||||
)}
|
||||
{isLoading && <Loader />}
|
||||
|
||||
<Box sx={{ p: { xs: 1.5, md: 0 } }}>
|
||||
{/* ---- Gradient brand header ---- */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: DT.radiusCard + 'px',
|
||||
border: `1px solid ${DT.borderSubtle}`,
|
||||
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||||
boxShadow: DT.shadowSoft,
|
||||
p: { xs: 2.5, md: 3.5 },
|
||||
mb: { xs: 2.5, md: 3 }
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={{ xs: 2, sm: 3 }}
|
||||
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: { xs: 64, md: 76 },
|
||||
height: { xs: 64, md: 76 },
|
||||
bgcolor: BRAND,
|
||||
color: '#fff',
|
||||
fontSize: { xs: 24, md: 28 },
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 10px 24px rgba(0, 0, 0, 0.35)'
|
||||
}}
|
||||
>
|
||||
{initialsOf(fullname)}
|
||||
</Avatar>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography variant="h3" sx={{ color: DT.textPrimary, fontWeight: 700, mb: 0.5 }}>
|
||||
{fullname}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap alignItems="center">
|
||||
{roleLabel && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<MdVerifiedUser size={14} style={{ color: BRAND }} />}
|
||||
label={roleLabel}
|
||||
sx={{
|
||||
bgcolor: soft(BRAND),
|
||||
color: BRAND,
|
||||
fontWeight: 700,
|
||||
border: `1px solid ${BRAND}40`,
|
||||
'& .MuiChip-icon': { ml: '6px' }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(userData?.id != null || userData?.userid != null) && (
|
||||
<Typography variant="body2" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||
ID #{userData.id ?? userData.userid}
|
||||
</Typography>
|
||||
)}
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
||||
<MdLocationOn size={15} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{tenantLabel}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<PageHeader title="My Profile" subtitle="Account & Security" />
|
||||
|
||||
{/* ---- Grouped detail cards ---- */}
|
||||
<Grid container spacing={{ xs: 2.5, md: 3 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<SectionCard title="Account" icon={MdBadge}>
|
||||
<Stack spacing={2.5}>
|
||||
<InfoField icon={MdPerson} label="User Name" value={userData?.name || userData?.fullname} />
|
||||
<InfoField icon={MdBadge} label="User ID" value={userData?.id ?? userData?.userid} />
|
||||
<InfoField icon={MdMail} label="E-Mail" value={userData?.email} accent="#0ea5e9" />
|
||||
<InfoField icon={MdVerifiedUser} label="Role" value={roleLabel} />
|
||||
<InfoField icon={MdLocationOn} label="Tenant" value={tenantLabel} accent="#14b8a6" />
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
</Grid>
|
||||
<Card padding={4} elevation="none" style={{ marginTop: 16, marginBottom: 20 }}>
|
||||
<HStack gap={3} vAlign="center" wrap="wrap">
|
||||
<Avatar name={fullname} size={72} tooltip={false} />
|
||||
<VStack gap={2} padding={0}>
|
||||
<Text type="heading" weight="bold">
|
||||
{fullname}
|
||||
</Text>
|
||||
<HStack gap={2} vAlign="center" wrap="wrap">
|
||||
{roleLabel !== '—' && <Badge variant="neutral" icon={<MdVerifiedUser size={14} />} label={roleLabel} />}
|
||||
{(userData?.id != null || userData?.userid != null) && (
|
||||
<Text color="secondary" weight="semibold">
|
||||
ID #{userData.id ?? userData.userid}
|
||||
</Text>
|
||||
)}
|
||||
<HStack gap={1} vAlign="center">
|
||||
<MdLocationOn size={15} color={DT.textSecondary} />
|
||||
<Text color="secondary" weight="semibold">
|
||||
{tenantLabel}
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Card>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<SectionCard title="Security" icon={MdShield}>
|
||||
<Stack spacing={2}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel htmlFor="current-password">Current Password</InputLabel>
|
||||
<TextField
|
||||
id="current-password"
|
||||
type="password"
|
||||
fullWidth
|
||||
placeholder="Current password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel htmlFor="new-password">New Password</InputLabel>
|
||||
<TextField
|
||||
id="new-password"
|
||||
type="password"
|
||||
fullWidth
|
||||
placeholder="New password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel htmlFor="confirm-password">Confirm New Password</InputLabel>
|
||||
<TextField
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
fullWidth
|
||||
placeholder="Confirm new password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<MdLock size={16} />}
|
||||
onClick={handleChangePassword}
|
||||
disabled={changePasswordMutation.isPending}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Update Password
|
||||
</Button>
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Grid columns={{ minWidth: 320 }} gap={3}>
|
||||
<SectionCard title="Account" icon={MdBadge}>
|
||||
<VStack gap={4} padding={0}>
|
||||
<InfoField icon={MdPerson} label="User Name" value={userData?.name || userData?.fullname} />
|
||||
<InfoField icon={MdBadge} label="User ID" value={userData?.id ?? userData?.userid} />
|
||||
<InfoField icon={MdMail} label="E-Mail" value={userData?.email} accent="#0ea5e9" />
|
||||
<InfoField icon={MdVerifiedUser} label="Role" value={roleLabel} />
|
||||
<InfoField icon={MdLocationOn} label="Tenant" value={tenantLabel} accent="#14b8a6" />
|
||||
</VStack>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Security" icon={MdShield}>
|
||||
<VStack gap={3} padding={0}>
|
||||
<TextInput
|
||||
label="Current Password"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={setCurrentPassword}
|
||||
placeholder="Current password"
|
||||
/>
|
||||
<TextInput label="New Password" type="password" value={newPassword} onChange={setNewPassword} placeholder="New password" />
|
||||
<TextInput
|
||||
label="Confirm New Password"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={setConfirmPassword}
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
<Divider />
|
||||
<Button
|
||||
label="Update Password"
|
||||
variant="primary"
|
||||
icon={<MdLock size={16} />}
|
||||
onClick={handleChangePassword}
|
||||
isLoading={changePasswordMutation.isPending}
|
||||
style={{ alignSelf: 'flex-start' }}
|
||||
/>
|
||||
</VStack>
|
||||
</SectionCard>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user