Files
Doormilexpress_console/src/pages/nearle/viewProfile.js

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;