188 lines
7.2 KiB
JavaScript
188 lines
7.2 KiB
JavaScript
import { useState } from 'react';
|
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
import { MdBadge, MdPerson, MdLocationOn, MdMail, MdShield, MdVerifiedUser, MdLock } from 'react-icons/md';
|
|
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 { PageShell } from 'components/nearle_components/PageLayout';
|
|
import { getusers } from 'pages/api/api';
|
|
import { updateProfilePassword } from 'pages/api/doormileApi';
|
|
import { DT } from 'themes/dt/tokens';
|
|
import { AccentAvatar } from 'themes/dt/primitives';
|
|
|
|
const ROLE_LABELS = { 1: 'Admin', 3: 'Manager', 4: 'Executive' };
|
|
|
|
const BRAND = DT.brand;
|
|
|
|
// A single read-only field rendered as an icon + uppercase label + value.
|
|
const InfoField = ({ icon: Icon, label, value, accent = BRAND }) => (
|
|
<HStack gap={3} vAlign="start">
|
|
<AccentAvatar color={accent} selected size={36}>
|
|
<Icon size={18} />
|
|
</AccentAvatar>
|
|
<VStack gap={0} padding={0}>
|
|
<Text type="label" weight="semibold" color="secondary">
|
|
{label}
|
|
</Text>
|
|
<Text weight="semibold" title={value || ''}>
|
|
{value || '—'}
|
|
</Text>
|
|
</VStack>
|
|
</HStack>
|
|
);
|
|
|
|
// A titled grouping card.
|
|
const SectionCard = ({ title, icon: Icon, children }) => (
|
|
<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} />
|
|
</AccentAvatar>
|
|
<Text weight="bold">{title}</Text>
|
|
</HStack>
|
|
<VStack gap={0} padding={3}>
|
|
{children}
|
|
</VStack>
|
|
</Card>
|
|
);
|
|
|
|
const ViewProfile = () => {
|
|
const { data: userData, isLoading } = useQuery({
|
|
queryKey: ['getuser'],
|
|
queryFn: getusers
|
|
});
|
|
|
|
const [currentPassword, setCurrentPassword] = useState('');
|
|
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) {
|
|
opentoast('Password updated successfully', 'success');
|
|
setCurrentPassword('');
|
|
setNewPassword('');
|
|
setConfirmPassword('');
|
|
} else {
|
|
opentoast(res.message || 'Failed to update password');
|
|
}
|
|
},
|
|
onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to update password')
|
|
});
|
|
|
|
const handleChangePassword = () => {
|
|
if (!currentPassword || !newPassword) {
|
|
opentoast('Fill current and new password');
|
|
return;
|
|
}
|
|
if (newPassword !== confirmPassword) {
|
|
opentoast('New password and confirmation do not match');
|
|
return;
|
|
}
|
|
changePasswordMutation.mutate();
|
|
};
|
|
|
|
// GET /admin/profile has no documented schema, but the console's own login
|
|
// response ({ success, token, user: { id, name, email, role, tenantid } })
|
|
// is confirmed — /admin/profile almost certainly returns the same "user"
|
|
// shape. Fields like address/suburb/city/postcode were jupiter-era guesses
|
|
// with no basis in the new API and have been dropped rather than shown blank.
|
|
const fullname = userData?.name || userData?.fullname || 'User Profile';
|
|
const roleLabel = ROLE_LABELS[userData?.role] || ROLE_LABELS[userData?.roleid] || userData?.role || '—';
|
|
const tenantLabel = userData?.tenantid ? `Tenant #${userData.tenantid}` : 'Doormile staff (all tenants)';
|
|
|
|
return (
|
|
<>
|
|
{isLoading && <Loader />}
|
|
|
|
<PageShell>
|
|
<PageHeader title="My Profile" subtitle="Account & Security" />
|
|
|
|
<Card padding={4} elevation="none">
|
|
<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 columns={{ minWidth: 320, repeat: 'fit' }} 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>
|
|
</PageShell>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ViewProfile;
|