feat: add sub user management features including creation, deletion, and password updates
This commit is contained in:
@@ -3,6 +3,7 @@ import ChoreEdit from '@/views/ChoreEdit/ChoreEdit'
|
||||
import Error from '@/views/Error'
|
||||
import AccountSettings from '@/views/Settings/AccountSettings'
|
||||
import AdvancedSettings from '@/views/Settings/AdvancedSettings'
|
||||
import ChildUserSettings from '@/views/Settings/ChildUserSettings'
|
||||
import CircleSettings from '@/views/Settings/CircleSettings'
|
||||
import Settings from '@/views/Settings/Settings'
|
||||
import SettingsOverview from '@/views/Settings/SettingsOverview'
|
||||
@@ -83,6 +84,10 @@ const Router = createBrowserRouter([
|
||||
path: 'account',
|
||||
element: <AccountSettings />,
|
||||
},
|
||||
{
|
||||
path: 'subaccounts',
|
||||
element: <ChildUserSettings />,
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
element: <NotificationSetting />,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
GetAllCircleMembers,
|
||||
GetAllUsers,
|
||||
GetChildUsers,
|
||||
GetDeviceTokens,
|
||||
GetUserProfile,
|
||||
} from '../utils/Fetcher'
|
||||
@@ -40,6 +41,13 @@ export const useUserProfile = () => {
|
||||
}
|
||||
const resp = await GetUserProfile()
|
||||
const result = await resp.json()
|
||||
// if we got 403 then user probably deleted their account and token is still valid. navigate to login
|
||||
if (resp.status === 403) {
|
||||
localStorage.removeItem('ca_token')
|
||||
localStorage.removeItem('ca_expiration')
|
||||
window.location.href = '/login'
|
||||
return null
|
||||
}
|
||||
|
||||
return result.res // Return the actual user profile data
|
||||
},
|
||||
@@ -78,3 +86,28 @@ export const useDeviceTokens = () => {
|
||||
refetch: () => queryClient.invalidateQueries(['deviceTokens']),
|
||||
}
|
||||
}
|
||||
|
||||
export const useChildUsers = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ['childUsers'],
|
||||
queryFn: async () => {
|
||||
if (!isTokenValid()) {
|
||||
return null
|
||||
}
|
||||
const resp = await GetChildUsers()
|
||||
const result = await resp.json()
|
||||
return result.res || []
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
isLoading,
|
||||
refetch: () => queryClient.invalidateQueries(['childUsers']),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useState } from 'react'
|
||||
import { createContext, useState } from 'react'
|
||||
|
||||
const AuthenticationContext = createContext({})
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
IconButton,
|
||||
Input,
|
||||
Sheet,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
@@ -28,6 +32,7 @@ import { useResource } from '../../queries/ResourceQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { GetUserProfile, login } from '../../utils/Fetcher'
|
||||
import { apiManager, isTokenValid } from '../../utils/TokenManager'
|
||||
import { buildChildUsername, getUserDisplayInfo } from '../../utils/UserHelpers'
|
||||
import MFAVerificationModal from './MFAVerificationModal'
|
||||
|
||||
const LoginView = () => {
|
||||
@@ -39,6 +44,20 @@ const LoginView = () => {
|
||||
const [mfaModalOpen, setMfaModalOpen] = useState(false)
|
||||
const [mfaSessionToken, setMfaSessionToken] = useState('')
|
||||
const [isAppleSignInSupported, setIsAppleSignInSupported] = useState(false)
|
||||
|
||||
// Child login state
|
||||
const [loginType, setLoginType] = useState('primary')
|
||||
const [parentUsername, setParentUsername] = useState('')
|
||||
const [childName, setChildName] = useState('')
|
||||
|
||||
// Clear fields when switching login modes
|
||||
const handleLoginModeChange = (event, newValue) => {
|
||||
setLoginType(newValue)
|
||||
setUsername('')
|
||||
setParentUsername('')
|
||||
setChildName('')
|
||||
setPassword('')
|
||||
}
|
||||
const { data: resource } = useResource()
|
||||
const { showError } = useNotification()
|
||||
const Navigate = useNavigate()
|
||||
@@ -84,7 +103,48 @@ const LoginView = () => {
|
||||
}, [])
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
login(username, password)
|
||||
|
||||
// Validation for child login
|
||||
if (loginType === 'sub') {
|
||||
if (!parentUsername.trim()) {
|
||||
showError({
|
||||
title: 'Validation Error',
|
||||
message: 'Primary username is required for sub account login',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!childName.trim()) {
|
||||
showError({
|
||||
title: 'Validation Error',
|
||||
message: 'Sub account name is required for sub account login',
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (!username.trim()) {
|
||||
showError({
|
||||
title: 'Validation Error',
|
||||
message: 'Username is required',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
showError({
|
||||
title: 'Validation Error',
|
||||
message: 'Password is required',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Determine the actual username to send
|
||||
const actualUsername =
|
||||
loginType === 'sub'
|
||||
? buildChildUsername(parentUsername, childName)
|
||||
: username
|
||||
|
||||
login(actualUsername, password)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json().then(data => {
|
||||
@@ -358,6 +418,16 @@ const LoginView = () => {
|
||||
<Typography level='body-md' alignSelf={'center'}>
|
||||
Welcome back,{' '}
|
||||
{userProfile?.displayName || userProfile?.username}
|
||||
{getUserDisplayInfo(userProfile).userType === 'child' && (
|
||||
<Typography
|
||||
component='span'
|
||||
level='body-xs'
|
||||
color='neutral'
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
(Sub Account)
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
@@ -394,27 +464,109 @@ const LoginView = () => {
|
||||
)}
|
||||
{!userProfile && (
|
||||
<>
|
||||
<Typography level='body2'>
|
||||
<Typography level='body2' sx={{ mb: 3 }}>
|
||||
Sign in to your account to continue
|
||||
</Typography>
|
||||
<Typography level='body2' alignSelf={'start'} mt={4}>
|
||||
Username
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
id='email'
|
||||
label='Email Address'
|
||||
name='email'
|
||||
autoComplete='email'
|
||||
autoFocus
|
||||
value={username}
|
||||
onChange={e => {
|
||||
setUsername(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Typography level='body2' alignSelf={'start'}>
|
||||
|
||||
{/* Login Type Tabs */}
|
||||
<Tabs
|
||||
value={loginType}
|
||||
onChange={handleLoginModeChange}
|
||||
sx={{ width: '100%', mb: 3 }}
|
||||
>
|
||||
<TabList
|
||||
sx={{
|
||||
width: '100%',
|
||||
p: 0.5,
|
||||
borderBottom: 'none',
|
||||
boxShadow: 'none',
|
||||
'&::after': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
value='primary'
|
||||
variant='plain'
|
||||
sx={{
|
||||
flex: 1,
|
||||
borderRadius: '6px',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Primary Account
|
||||
</Tab>
|
||||
<Tab
|
||||
value='sub'
|
||||
variant='plain'
|
||||
sx={{
|
||||
flex: 1,
|
||||
borderRadius: '6px',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Sub Account
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel value='primary' sx={{ p: 0, mt: 2 }}>
|
||||
<Typography level='body2' alignSelf={'start'} mb={1}>
|
||||
Username
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
id='email'
|
||||
label='Email Address'
|
||||
name='email'
|
||||
autoComplete='email'
|
||||
autoFocus
|
||||
value={username}
|
||||
onChange={e => {
|
||||
setUsername(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value='sub' sx={{ p: 0, mt: 2 }}>
|
||||
<Typography level='body2' alignSelf={'start'} mb={1}>
|
||||
Primary Account Username
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
id='parentUsername'
|
||||
name='parentUsername'
|
||||
placeholder='Enter primary account username'
|
||||
autoFocus
|
||||
value={parentUsername}
|
||||
onChange={e => {
|
||||
setParentUsername(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Typography level='body2' alignSelf={'start'} mt={1} mb={1}>
|
||||
Sub Account Username
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
id='childName'
|
||||
name='childName'
|
||||
placeholder='Enter sub account name'
|
||||
value={childName}
|
||||
onChange={e => {
|
||||
setChildName(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Typography level='body2' alignSelf={'start'} mb={1}>
|
||||
Password:
|
||||
</Typography>
|
||||
<Input
|
||||
@@ -446,7 +598,7 @@ const LoginView = () => {
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Sign In
|
||||
{loginType === 'sub' ? 'Sign In as Sub Account' : 'Sign In'}
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
|
||||
227
src/views/Modals/Inputs/CreateChildUserModal.jsx
Normal file
227
src/views/Modals/Inputs/CreateChildUserModal.jsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [childName, setChildName] = useState('')
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [errors, setErrors] = useState({})
|
||||
const [touched, setTouched] = useState({})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const newErrors = {}
|
||||
|
||||
if (touched.childName) {
|
||||
if (!childName.trim()) {
|
||||
newErrors.childName = 'Sub account name is required'
|
||||
} else if (childName.length < 2) {
|
||||
newErrors.childName = 'Sub account name must be at least 2 characters'
|
||||
} else if (childName.length > 20) {
|
||||
newErrors.childName = 'Sub account name must be less than 20 characters'
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(childName)) {
|
||||
newErrors.childName =
|
||||
'Sub account name can only contain letters, numbers, and underscores'
|
||||
}
|
||||
}
|
||||
|
||||
if (touched.password) {
|
||||
if (!password) {
|
||||
newErrors.password = 'Password is required'
|
||||
} else if (password.length < 8) {
|
||||
newErrors.password = 'Password must be at least 8 characters'
|
||||
} else if (password.length > 45) {
|
||||
newErrors.password = 'Password must be less than 45 characters'
|
||||
}
|
||||
}
|
||||
|
||||
if (touched.confirmPassword) {
|
||||
if (password !== confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match'
|
||||
}
|
||||
}
|
||||
|
||||
if (touched.displayName && displayName.length > 50) {
|
||||
newErrors.displayName = 'Display name must be less than 50 characters'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
}, [childName, displayName, password, confirmPassword, touched])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setTouched({
|
||||
childName: true,
|
||||
password: true,
|
||||
confirmPassword: true,
|
||||
displayName: true,
|
||||
})
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await onSuccess({
|
||||
childName: childName.trim(),
|
||||
displayName: displayName.trim() || childName.trim(),
|
||||
password,
|
||||
})
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.error('Failed to create child user:', error)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setChildName('')
|
||||
setDisplayName('')
|
||||
setPassword('')
|
||||
setConfirmPassword('')
|
||||
setErrors({})
|
||||
setTouched({})
|
||||
setIsSubmitting(false)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const isValid =
|
||||
Object.keys(errors).length === 0 &&
|
||||
childName.trim() &&
|
||||
password &&
|
||||
password === confirmPassword
|
||||
|
||||
return (
|
||||
<ResponsiveModal open={isOpen} onClose={handleClose}>
|
||||
<Typography level='h4' mb={2}>
|
||||
Create Sub Account
|
||||
</Typography>
|
||||
|
||||
<Typography level='body-md' mb={3}>
|
||||
Create a new sub account. The user will be able to log in using their
|
||||
combined username and complete tasks assigned to them.
|
||||
</Typography>
|
||||
|
||||
<FormControl error={!!errors.childName} sx={{ mb: 2 }}>
|
||||
<Typography level='body2' mb={1}>
|
||||
Sub Account Name *
|
||||
</Typography>
|
||||
<Input
|
||||
required
|
||||
fullWidth
|
||||
id='childName'
|
||||
name='childName'
|
||||
placeholder='Enter sub account name (e.g., sarah)'
|
||||
value={childName}
|
||||
onChange={e => {
|
||||
setChildName(e.target.value)
|
||||
setTouched(prev => ({ ...prev, childName: true }))
|
||||
}}
|
||||
/>
|
||||
{errors.childName && (
|
||||
<FormHelperText>{errors.childName}</FormHelperText>
|
||||
)}
|
||||
<FormHelperText>
|
||||
This will create a username like: primaryname_subaccountname
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl error={!!errors.displayName} sx={{ mb: 2 }}>
|
||||
<Typography level='body2' mb={1}>
|
||||
Display Name
|
||||
</Typography>
|
||||
<Input
|
||||
fullWidth
|
||||
id='displayName'
|
||||
name='displayName'
|
||||
placeholder='Display name (optional, defaults to sub account name)'
|
||||
value={displayName}
|
||||
onChange={e => {
|
||||
setDisplayName(e.target.value)
|
||||
setTouched(prev => ({ ...prev, displayName: true }))
|
||||
}}
|
||||
/>
|
||||
{errors.displayName && (
|
||||
<FormHelperText>{errors.displayName}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl error={!!errors.password} sx={{ mb: 2 }}>
|
||||
<Typography level='body2' mb={1}>
|
||||
Password *
|
||||
</Typography>
|
||||
<Input
|
||||
required
|
||||
fullWidth
|
||||
name='password'
|
||||
type='password'
|
||||
id='password'
|
||||
placeholder='Enter password (8-45 characters)'
|
||||
value={password}
|
||||
onChange={e => {
|
||||
setPassword(e.target.value)
|
||||
setTouched(prev => ({ ...prev, password: true }))
|
||||
}}
|
||||
/>
|
||||
{errors.password && <FormHelperText>{errors.password}</FormHelperText>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl error={!!errors.confirmPassword} sx={{ mb: 3 }}>
|
||||
<Typography level='body2' mb={1}>
|
||||
Confirm Password *
|
||||
</Typography>
|
||||
<Input
|
||||
required
|
||||
fullWidth
|
||||
name='confirmPassword'
|
||||
type='password'
|
||||
id='confirmPassword'
|
||||
placeholder='Confirm password'
|
||||
value={confirmPassword}
|
||||
onChange={e => {
|
||||
setConfirmPassword(e.target.value)
|
||||
setTouched(prev => ({ ...prev, confirmPassword: true }))
|
||||
}}
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<FormHelperText>{errors.confirmPassword}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<Box display='flex' justifyContent='space-between' gap={2}>
|
||||
<Button
|
||||
size='lg'
|
||||
variant='outlined'
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={handleSubmit}
|
||||
disabled={!isValid || isSubmitting}
|
||||
loading={isSubmitting}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateChildUserModal
|
||||
297
src/views/Settings/ChildUserSettings.jsx
Normal file
297
src/views/Settings/ChildUserSettings.jsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Divider,
|
||||
IconButton,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
||||
import { useChildUsers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import {
|
||||
CreateChildUser,
|
||||
DeleteChildUser,
|
||||
UpdateChildPassword,
|
||||
} from '../../utils/Fetcher'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import CreateChildUserModal from '../Modals/Inputs/CreateChildUserModal'
|
||||
import PasswordChangeModal from '../Modals/Inputs/PasswordChangeModal'
|
||||
import SettingsLayout from './SettingsLayout'
|
||||
|
||||
const ChildUserSettings = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { data: childUsers, isLoading, refetch } = useChildUsers()
|
||||
const { showNotification } = useNotification()
|
||||
const queryClient = useQueryClient()
|
||||
const { confirmModalConfig, showConfirmation } = useConfirmationModal()
|
||||
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false)
|
||||
const [passwordModalOpen, setPasswordModalOpen] = useState(false)
|
||||
const [selectedChildId, setSelectedChildId] = useState(null)
|
||||
const [deletingChildId, setDeletingChildId] = useState(null)
|
||||
|
||||
// Check if user is a parent (not a child user)
|
||||
const isParentUser = userProfile?.userType === 0 && !userProfile?.parentUserId
|
||||
|
||||
const handleCreateChild = async childData => {
|
||||
try {
|
||||
const response = await CreateChildUser(
|
||||
childData.childName,
|
||||
childData.displayName,
|
||||
childData.password,
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: `Child account "${result.res.displayName}" created successfully!`,
|
||||
})
|
||||
refetch()
|
||||
queryClient.invalidateQueries(['childUsers'])
|
||||
} else {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to create child user')
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: `Failed to create child account: ${error.message}`,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdatePassword = async newPassword => {
|
||||
if (!selectedChildId || !newPassword) return
|
||||
|
||||
try {
|
||||
const response = await UpdateChildPassword(selectedChildId, newPassword)
|
||||
|
||||
if (response.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Child password updated successfully',
|
||||
})
|
||||
} else {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to update password')
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: `Failed to update password: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteChild = async (childId, childName) => {
|
||||
showConfirmation(
|
||||
`Are you sure you want to delete the child account "${childName}"? This action cannot be undone.`,
|
||||
'Delete Sub Account',
|
||||
async () => {
|
||||
setDeletingChildId(childId)
|
||||
try {
|
||||
const response = await DeleteChildUser(childId)
|
||||
|
||||
if (response.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: `Sub account "${childName}" deleted successfully`,
|
||||
})
|
||||
refetch()
|
||||
queryClient.invalidateQueries(['childUsers'])
|
||||
} else {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to delete Sub user')
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: `Failed to delete Sub account: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setDeletingChildId(null)
|
||||
}
|
||||
},
|
||||
'Delete',
|
||||
'Cancel',
|
||||
'danger',
|
||||
)
|
||||
}
|
||||
|
||||
if (!isParentUser) {
|
||||
return (
|
||||
<SettingsLayout title='Sub Account Management'>
|
||||
<Typography level='body-md' color='warning'>
|
||||
Only primary users can manage sub accounts.
|
||||
</Typography>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayout title='Managed Accounts'>
|
||||
<div className='grid gap-4'>
|
||||
<Typography level='body-md'>
|
||||
Manage sub accounts. Sub account users can log in and complete
|
||||
assigned tasks.
|
||||
</Typography>
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||
Sub account limited to 1 on Free plan. Upgrade to Plus to have up to
|
||||
5 sub accounts.
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography level='title-lg'>
|
||||
Sub Accounts ({childUsers?.length || 0})
|
||||
</Typography>
|
||||
<Button
|
||||
startDecorator={<PersonAddIcon />}
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
>
|
||||
Add Sub Account
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{isLoading ? (
|
||||
<Typography>Loading sub accounts...</Typography>
|
||||
) : childUsers?.length === 0 ? (
|
||||
<Card variant='soft' sx={{ textAlign: 'center', py: 4 }}>
|
||||
<CardContent>
|
||||
<Typography level='title-md' mb={1}>
|
||||
No Sub Accounts
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={3}>
|
||||
Create sub accounts so team members can log in and complete
|
||||
their assigned tasks.
|
||||
</Typography>
|
||||
<Button
|
||||
startDecorator={<PersonAddIcon />}
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
>
|
||||
Add Your First Sub Account
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className='grid gap-3'>
|
||||
{childUsers?.map(child => (
|
||||
<Card key={child.id} variant='outlined'>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar size='lg'>
|
||||
{child.displayName?.[0]?.toUpperCase() ||
|
||||
child.username?.[0]?.toUpperCase()}
|
||||
</Avatar>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography level='title-md'>
|
||||
{child.displayName || child.username}
|
||||
</Typography>
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Username: {child.username}
|
||||
</Typography>
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Created:{' '}
|
||||
{new Date(child.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='soft'
|
||||
onClick={() => {
|
||||
setSelectedChildId(child.id)
|
||||
setPasswordModalOpen(true)
|
||||
}}
|
||||
title='Change Password'
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='danger'
|
||||
onClick={() =>
|
||||
handleDeleteChild(
|
||||
child.id,
|
||||
child.displayName || child.username,
|
||||
)
|
||||
}
|
||||
loading={deletingChildId === child.id}
|
||||
title='Delete Account'
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Box>
|
||||
<Typography level='title-md' mb={2}>
|
||||
How Managed Accounts Work
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
• Managed accounts created by the primary user, these specific for
|
||||
user you want to have ability to delete and reset password.
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
• Sub accounts can log in with their own username and password.
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
• Managed accounts can complete tasks but have limited
|
||||
administrative permissions
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
• Managed accounts automatically added to your circle
|
||||
</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
<CreateChildUserModal
|
||||
isOpen={createModalOpen}
|
||||
onClose={() => setCreateModalOpen(false)}
|
||||
onSuccess={handleCreateChild}
|
||||
/>
|
||||
|
||||
<PasswordChangeModal
|
||||
isOpen={passwordModalOpen}
|
||||
onClose={newPassword => {
|
||||
if (newPassword) {
|
||||
handleUpdatePassword(newPassword)
|
||||
}
|
||||
setPasswordModalOpen(false)
|
||||
setSelectedChildId(null)
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal config={confirmModalConfig} />
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChildUserSettings
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Api,
|
||||
ChevronRight,
|
||||
Circle,
|
||||
FamilyRestroom,
|
||||
Notifications,
|
||||
Palette,
|
||||
Person,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import { isParentUser } from '../../utils/UserHelpers'
|
||||
|
||||
const SettingsOverview = () => {
|
||||
const navigate = useNavigate()
|
||||
@@ -57,6 +59,13 @@ const SettingsOverview = () => {
|
||||
'Manage your subscription, change password, and account deletion options.',
|
||||
icon: <AccountCircle />,
|
||||
},
|
||||
{
|
||||
id: 'subaccounts',
|
||||
title: 'Managed Accounts',
|
||||
description:
|
||||
'Create and manage sub accounts to log in and complete assigned tasks.',
|
||||
icon: <FamilyRestroom />,
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
title: 'Notifications',
|
||||
@@ -112,6 +121,27 @@ const SettingsOverview = () => {
|
||||
navigate(`/settings/${settingId}`)
|
||||
}
|
||||
|
||||
// Filter settings based on user type
|
||||
const getAvailableSettings = () => {
|
||||
const parentOnlySettings = [
|
||||
'children',
|
||||
'mfa',
|
||||
'apitokens',
|
||||
'circle',
|
||||
'account',
|
||||
]
|
||||
|
||||
if (isParentUser(userProfile)) {
|
||||
// Parent users can access all settings
|
||||
return settingsCards
|
||||
} else {
|
||||
// Child users can only access basic settings
|
||||
return settingsCards.filter(
|
||||
setting => !parentOnlySettings.includes(setting.id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth='lg' sx={{ py: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
@@ -257,7 +287,7 @@ const SettingsOverview = () => {
|
||||
'--ListItem-paddingX': '20px',
|
||||
}}
|
||||
>
|
||||
{settingsCards.map((setting, index) => (
|
||||
{getAvailableSettings().map((setting, index) => (
|
||||
<ListItem key={setting.id} sx={{ p: 0 }}>
|
||||
<ListItemButton
|
||||
onClick={() => handleCardClick(setting.id)}
|
||||
|
||||
Reference in New Issue
Block a user