Your commit message
This commit is contained in:
13
src/App.jsx
13
src/App.jsx
@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { Outlet, useNavigate } from 'react-router-dom'
|
import { Outlet, useNavigate } from 'react-router-dom'
|
||||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||||
import { registerCapacitorListeners } from './CapacitorListener'
|
import { registerCapacitorListeners } from './CapacitorListener'
|
||||||
|
import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
|
||||||
import { UserContext } from './contexts/UserContext'
|
import { UserContext } from './contexts/UserContext'
|
||||||
import { useResource } from './queries/ResourceQueries'
|
import { useResource } from './queries/ResourceQueries'
|
||||||
import { AuthenticationProvider } from './service/AuthenticationService'
|
import { AuthenticationProvider } from './service/AuthenticationService'
|
||||||
@@ -93,14 +94,18 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<NetworkBanner />
|
<NetworkBanner />
|
||||||
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthenticationProvider />
|
<AuthenticationProvider />
|
||||||
<ErrorProvider>
|
<ErrorProvider>
|
||||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
<ImpersonateUserProvider>
|
||||||
<NavBar />
|
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||||
<Outlet />
|
<NavBar />
|
||||||
</UserContext.Provider>
|
<Outlet />
|
||||||
|
</UserContext.Provider>
|
||||||
|
</ImpersonateUserProvider>
|
||||||
</ErrorProvider>
|
</ErrorProvider>
|
||||||
|
|
||||||
{needRefresh && (
|
{needRefresh && (
|
||||||
<Snackbar open={showUpdateSnackbar}>
|
<Snackbar open={showUpdateSnackbar}>
|
||||||
<Typography level='body-md'>
|
<Typography level='body-md'>
|
||||||
|
|||||||
16
src/contexts/ImpersonateUserContext.jsx
Normal file
16
src/contexts/ImpersonateUserContext.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { createContext, useContext, useState } from 'react'
|
||||||
|
|
||||||
|
const ImpersonateUserContext = createContext()
|
||||||
|
|
||||||
|
export const useImpersonateUser = () => useContext(ImpersonateUserContext)
|
||||||
|
|
||||||
|
export const ImpersonateUserProvider = ({ children }) => {
|
||||||
|
const [impersonatedUser, setImpersonatedUser] = useState(null)
|
||||||
|
return (
|
||||||
|
<ImpersonateUserContext.Provider
|
||||||
|
value={{ impersonatedUser, setImpersonatedUser }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ImpersonateUserContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -97,12 +97,9 @@ const GetChoreDetailById = id => {
|
|||||||
headers: HEADERS(),
|
headers: HEADERS(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const MarkChoreComplete = (id, note, completedDate, performer) => {
|
const MarkChoreComplete = (id, body, completedDate, performer) => {
|
||||||
var markChoreURL = `/chores/${id}/do`
|
var markChoreURL = `/chores/${id}/do`
|
||||||
|
|
||||||
const body = {
|
|
||||||
note,
|
|
||||||
}
|
|
||||||
let completedDateFormated = ''
|
let completedDateFormated = ''
|
||||||
if (completedDate) {
|
if (completedDate) {
|
||||||
completedDateFormated = `?completedDate=${new Date(
|
completedDateFormated = `?completedDate=${new Date(
|
||||||
@@ -220,6 +217,14 @@ const GetAllCircleMembers = async () => {
|
|||||||
return resp.json()
|
return resp.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UpdateMemberRole = async (memberId, role) => {
|
||||||
|
return Fetch(`/circles/members/role`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: HEADERS(),
|
||||||
|
body: JSON.stringify({ role, memberId }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const GetUserProfile = () => {
|
const GetUserProfile = () => {
|
||||||
return Fetch(`/users/profile`, {
|
return Fetch(`/users/profile`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -540,6 +545,7 @@ export {
|
|||||||
UpdateChoreStatus,
|
UpdateChoreStatus,
|
||||||
UpdateDueDate,
|
UpdateDueDate,
|
||||||
UpdateLabel,
|
UpdateLabel,
|
||||||
|
UpdateMemberRole,
|
||||||
UpdateNotificationTarget,
|
UpdateNotificationTarget,
|
||||||
UpdatePassword,
|
UpdatePassword,
|
||||||
UpdateThingState,
|
UpdateThingState,
|
||||||
|
|||||||
@@ -43,6 +43,28 @@ class ApiManager {
|
|||||||
|
|
||||||
export const apiManager = new ApiManager()
|
export const apiManager = new ApiManager()
|
||||||
|
|
||||||
|
export const getAssetURL = path => {
|
||||||
|
const baseURL = apiManager.getApiURL()
|
||||||
|
return `${baseURL}/assets/${path}`
|
||||||
|
}
|
||||||
|
export async function UploadFile(url, options) {
|
||||||
|
if (!isTokenValid()) {
|
||||||
|
Cookies.set('ca_redirect', window.location.pathname)
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
options = {}
|
||||||
|
}
|
||||||
|
const headers = HEADERS()
|
||||||
|
options.headers = { Authorization: headers['Authorization'] }
|
||||||
|
|
||||||
|
const baseURL = apiManager.getApiURL()
|
||||||
|
const fullURL = `${baseURL}${url}`
|
||||||
|
|
||||||
|
return fetch(fullURL, options)
|
||||||
|
}
|
||||||
|
|
||||||
export async function Fetch(url, options) {
|
export async function Fetch(url, options) {
|
||||||
if (!isTokenValid()) {
|
if (!isTokenValid()) {
|
||||||
Cookies.set('ca_redirect', window.location.pathname)
|
Cookies.set('ca_redirect', window.location.pathname)
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import { Divider } from '@mui/material'
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||||
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
|
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
|
||||||
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
||||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||||
@@ -85,6 +87,7 @@ const ChoreView = () => {
|
|||||||
isLoading: isCircleMembersLoading,
|
isLoading: isCircleMembersLoading,
|
||||||
handleRefetch: handleCircleMembersRefetch,
|
handleRefetch: handleCircleMembersRefetch,
|
||||||
} = useCircleMembers()
|
} = useCircleMembers()
|
||||||
|
const { impersonatedUser } = useImpersonateUser()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: choreData,
|
data: choreData,
|
||||||
@@ -181,7 +184,14 @@ const ChoreView = () => {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
const id = setTimeout(() => {
|
const id = setTimeout(() => {
|
||||||
MarkChoreComplete(choreId, note, completedDate, null)
|
MarkChoreComplete(
|
||||||
|
choreId,
|
||||||
|
impersonatedUser
|
||||||
|
? { completedBy: impersonatedUser.userId, note }
|
||||||
|
: { note },
|
||||||
|
completedDate,
|
||||||
|
null,
|
||||||
|
)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
return resp.json().then(data => {
|
return resp.json().then(data => {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||||
import { UserContext } from '../../contexts/UserContext'
|
import { UserContext } from '../../contexts/UserContext'
|
||||||
import { useError } from '../../service/ErrorProvider'
|
import { useError } from '../../service/ErrorProvider'
|
||||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||||
@@ -93,6 +94,8 @@ const ChoreCard = ({
|
|||||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||||
const { userProfile } = React.useContext(UserContext)
|
const { userProfile } = React.useContext(UserContext)
|
||||||
|
const { impersonatedUser } = useImpersonateUser()
|
||||||
|
|
||||||
const { showError } = useError()
|
const { showError } = useError()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('mousedown', handleMenuOutsideClick)
|
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||||
@@ -187,7 +190,12 @@ const ChoreCard = ({
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
const id = setTimeout(() => {
|
const id = setTimeout(() => {
|
||||||
MarkChoreComplete(chore.id, null, null, null)
|
MarkChoreComplete(
|
||||||
|
chore.id,
|
||||||
|
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
return resp.json().then(data => {
|
return resp.json().then(data => {
|
||||||
@@ -249,7 +257,7 @@ const ChoreCard = ({
|
|||||||
|
|
||||||
MarkChoreComplete(
|
MarkChoreComplete(
|
||||||
chore.id,
|
chore.id,
|
||||||
null,
|
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||||
new Date(newDate).toISOString(),
|
new Date(newDate).toISOString(),
|
||||||
null,
|
null,
|
||||||
).then(response => {
|
).then(response => {
|
||||||
@@ -272,7 +280,14 @@ const ChoreCard = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const handleCompleteWithNote = note => {
|
const handleCompleteWithNote = note => {
|
||||||
MarkChoreComplete(chore.id, note, null, null).then(response => {
|
MarkChoreComplete(
|
||||||
|
chore.id,
|
||||||
|
impersonatedUser
|
||||||
|
? { note, completedBy: impersonatedUser.userId }
|
||||||
|
: { note },
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
).then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
response.json().then(data => {
|
response.json().then(data => {
|
||||||
const newChore = data.res
|
const newChore = data.res
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useMediaQuery } from '@mui/material'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { ChoresGrouper } from '../../utils/Chores'
|
import { ChoresGrouper } from '../../utils/Chores'
|
||||||
import CalendarView from '../components/CalendarView'
|
import CalendarView from '../components/CalendarView'
|
||||||
|
import WelcomeCard from './WelcomeCard'
|
||||||
|
|
||||||
const Sidepanel = ({ chores }) => {
|
const Sidepanel = ({ chores }) => {
|
||||||
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
||||||
@@ -30,61 +31,28 @@ const Sidepanel = ({ chores }) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Sheet
|
<Box>
|
||||||
variant='plain'
|
<WelcomeCard chores={chores} />
|
||||||
sx={{
|
<Sheet
|
||||||
p: 2,
|
variant='plain'
|
||||||
// borderRadius: 'sm',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
mr: 10,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
boxShadow: 'sm',
|
|
||||||
borderRadius: 20,
|
|
||||||
|
|
||||||
// minimum height to fit the content:
|
|
||||||
height: '80vh',
|
|
||||||
width: '290px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* <Box
|
|
||||||
sx={{
|
sx={{
|
||||||
|
p: 2,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
mr: 10,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxShadow: 'sm',
|
||||||
|
borderRadius: 20,
|
||||||
|
height: '80vh',
|
||||||
|
width: '290px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PieChart width={200} height={200}>
|
<Box sx={{ width: '100%' }}>
|
||||||
<Pie
|
<CalendarView chores={chores} />
|
||||||
data={dueDatePieChartData}
|
</Box>
|
||||||
dataKey='value'
|
</Sheet>
|
||||||
nameKey='label'
|
</Box>
|
||||||
innerRadius={30}
|
|
||||||
paddingAngle={5}
|
|
||||||
cornerRadius={5}
|
|
||||||
>
|
|
||||||
{dueDatePieChartData.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
|
|
||||||
<Legend
|
|
||||||
layout='horizontal'
|
|
||||||
align='center'
|
|
||||||
iconType='circle'
|
|
||||||
iconSize={8}
|
|
||||||
fontSize={12}
|
|
||||||
formatter={(label, value) => `${label}: ${value.payload.value}`}
|
|
||||||
wrapperStyle={{ paddingTop: 0, marginTop: 0 }} // Adjust padding and margin
|
|
||||||
/>
|
|
||||||
<Tooltip />
|
|
||||||
</PieChart>
|
|
||||||
</Box> */}
|
|
||||||
<Box sx={{ width: '100%' }}>
|
|
||||||
<CalendarView chores={chores} />
|
|
||||||
</Box>
|
|
||||||
</Sheet>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
169
src/views/Chores/WelcomeCard.jsx
Normal file
169
src/views/Chores/WelcomeCard.jsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { Avatar, Box, Button, Sheet, Typography } from '@mui/joy'
|
||||||
|
|
||||||
|
import { useContext, useEffect, useState } from 'react'
|
||||||
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
||||||
|
import { UserContext } from '../../contexts/UserContext'
|
||||||
|
import { useCircleMembers } from '../../queries/UserQueries'
|
||||||
|
import UserModal from '../Modals/Inputs/UserModal'
|
||||||
|
const WelcomeCard = chores => {
|
||||||
|
const { impersonatedUser, setImpersonatedUser } = useImpersonateUser()
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
|
const { userProfile } = useContext(UserContext)
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [summarytext, setSummaryText] = useState('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: circleMembersData,
|
||||||
|
isLoading: isCircleMembersLoading,
|
||||||
|
handleRefetch: handleCircleMembersRefetch,
|
||||||
|
} = useCircleMembers()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userProfile && userProfile?.id) {
|
||||||
|
const members = circleMembersData?.res || []
|
||||||
|
const isUserAdmin = members.some(
|
||||||
|
member =>
|
||||||
|
member.userId === userProfile?.id &&
|
||||||
|
(member.role === 'admin' || member.role === 'manager'),
|
||||||
|
)
|
||||||
|
members.forEach(m =>
|
||||||
|
console.log('Member:', userProfile?.id, m.id, m.role),
|
||||||
|
)
|
||||||
|
console.log('Circle Members:', members)
|
||||||
|
console.log(isUserAdmin)
|
||||||
|
|
||||||
|
setIsAdmin(isUserAdmin)
|
||||||
|
}
|
||||||
|
}, [userProfile, circleMembersData])
|
||||||
|
if (!isAdmin) {
|
||||||
|
return null
|
||||||
|
} else if (isCircleMembersLoading || impersonatedUser === null) {
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
variant='plain'
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
mr: 10,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxShadow: 'sm',
|
||||||
|
borderRadius: 20,
|
||||||
|
width: '290px',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ textAlign: 'center', width: '100%' }}>
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography level='title-md' sx={{ mb: 0.5 }}>
|
||||||
|
Who's checking in?
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant='plain'
|
||||||
|
color='primary'
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
Select User
|
||||||
|
</Button>
|
||||||
|
<UserModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
performers={circleMembersData?.res}
|
||||||
|
onSelect={user => {
|
||||||
|
setImpersonatedUser(user)
|
||||||
|
setIsModalOpen(false)
|
||||||
|
}}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Sheet>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
variant='plain'
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
mr: 10,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxShadow: 'sm',
|
||||||
|
borderRadius: 20,
|
||||||
|
width: '290px',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||||
|
<Box sx={{ mr: 2 }}>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
src={impersonatedUser?.image || impersonatedUser?.avatar}
|
||||||
|
alt={impersonatedUser?.displayName || impersonatedUser?.name}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography level='title-md' sx={{ mb: 1, ml: 0.5 }}>
|
||||||
|
{impersonatedUser?.displayName || impersonatedUser?.name}
|
||||||
|
</Typography>
|
||||||
|
{/* <Box sx={{ fontSize: 14, color: 'text.secondary', mb: 0.5 }}>
|
||||||
|
5 chores assigned, 2 due soon
|
||||||
|
</Box> */}
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
variant='plain'
|
||||||
|
color='neutral'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => {
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Change User
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='plain'
|
||||||
|
color='neutral'
|
||||||
|
size='sm'
|
||||||
|
sx={{ ml: 0.5 }}
|
||||||
|
onClick={() => {
|
||||||
|
setImpersonatedUser(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<UserModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
performers={circleMembersData?.res}
|
||||||
|
onSelect={user => {
|
||||||
|
setImpersonatedUser(user)
|
||||||
|
setIsModalOpen(false)
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Sheet>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default WelcomeCard
|
||||||
58
src/views/Modals/Inputs/UserModal.jsx
Normal file
58
src/views/Modals/Inputs/UserModal.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
ModalOverflow,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
|
||||||
|
const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverflow>
|
||||||
|
<ModalDialog size='md' sx={{ minWidth: 360 }}>
|
||||||
|
<Typography level='h4' sx={{ mb: 2 }}>
|
||||||
|
Select User
|
||||||
|
</Typography>
|
||||||
|
<List sx={{ mb: 2 }}>
|
||||||
|
{performers.map(user => (
|
||||||
|
<ListItem
|
||||||
|
key={user.id}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(user)
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Avatar
|
||||||
|
size='lg'
|
||||||
|
src={user.image || user.avatar}
|
||||||
|
alt={user.displayName || user.name}
|
||||||
|
/>
|
||||||
|
<Typography>{user.displayName || user.name}</Typography>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
|
<Button variant='outlined' color='neutral' onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</ModalDialog>
|
||||||
|
</ModalOverflow>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserModal
|
||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
FormHelperText,
|
FormHelperText,
|
||||||
Input,
|
Input,
|
||||||
|
ListItem,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
@@ -28,6 +31,7 @@ import {
|
|||||||
JoinCircle,
|
JoinCircle,
|
||||||
LeaveCircle,
|
LeaveCircle,
|
||||||
PutWebhookURL,
|
PutWebhookURL,
|
||||||
|
UpdateMemberRole,
|
||||||
UpdatePassword,
|
UpdatePassword,
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
import { isPlusAccount } from '../../utils/Helpers'
|
import { isPlusAccount } from '../../utils/Helpers'
|
||||||
@@ -46,6 +50,7 @@ const Settings = () => {
|
|||||||
const [circleMembers, setCircleMembers] = useState([])
|
const [circleMembers, setCircleMembers] = useState([])
|
||||||
const [webhookURL, setWebhookURL] = useState(null)
|
const [webhookURL, setWebhookURL] = useState(null)
|
||||||
const [webhookError, setWebhookError] = useState(null)
|
const [webhookError, setWebhookError] = useState(null)
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
|
|
||||||
const [changePasswordModal, setChangePasswordModal] = useState(false)
|
const [changePasswordModal, setChangePasswordModal] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,6 +75,16 @@ const Settings = () => {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// useEffect when circleMembers and userprofile:
|
||||||
|
useEffect(() => {
|
||||||
|
if (userProfile && userProfile.id) {
|
||||||
|
const isUserAdmin = circleMembers.some(
|
||||||
|
member => member.userId === userProfile.id && member.role === 'admin',
|
||||||
|
)
|
||||||
|
setIsAdmin(isUserAdmin)
|
||||||
|
}
|
||||||
|
}, [circleMembers, userProfile])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hash = window.location.hash
|
const hash = window.location.hash
|
||||||
if (hash) {
|
if (hash) {
|
||||||
@@ -222,33 +237,110 @@ const Settings = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{member.userId !== userProfile.id && member.isActive && (
|
|
||||||
<Button
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
disabled={
|
{member.userId !== userProfile.id && isAdmin && (
|
||||||
circleMembers.find(m => userProfile.id == m.userId).role !==
|
<Select
|
||||||
'admin'
|
size='sm'
|
||||||
}
|
sx={{ mr: 1 }}
|
||||||
variant='outlined'
|
value={member.role}
|
||||||
color='danger'
|
renderValue={() => (
|
||||||
size='sm'
|
<Typography>
|
||||||
onClick={() => {
|
{member.role.charAt(0).toUpperCase() +
|
||||||
const confirmed = confirm(
|
member.role.slice(1)}
|
||||||
`Are you sure you want to remove ${member.displayName} from your circle?`,
|
</Typography>
|
||||||
)
|
)}
|
||||||
if (confirmed) {
|
onChange={(e, value) => {
|
||||||
DeleteCircleMember(member.circleId, member.userId).then(
|
UpdateMemberRole(member.userId, value).then(resp => {
|
||||||
resp => {
|
if (resp.ok) {
|
||||||
if (resp.ok) {
|
const newCircleMembers = circleMembers.map(m => {
|
||||||
alert('Removed member successfully.')
|
if (m.userId === member.userId) {
|
||||||
}
|
m.role = value
|
||||||
},
|
}
|
||||||
)
|
return m
|
||||||
}
|
})
|
||||||
}}
|
setCircleMembers(newCircleMembers)
|
||||||
>
|
} else {
|
||||||
Remove
|
alert('Failed to update role')
|
||||||
</Button>
|
}
|
||||||
)}
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
value: 'member',
|
||||||
|
description: 'Just a regular member of the circle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'manager',
|
||||||
|
description:
|
||||||
|
'Can impersonate users and perform actions on their behalf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'admin',
|
||||||
|
description: 'Full access to the circle',
|
||||||
|
},
|
||||||
|
].map((option, index) => (
|
||||||
|
<Option value={option.value} key={index}>
|
||||||
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'start',
|
||||||
|
alignItems: 'start',
|
||||||
|
width: '100%',
|
||||||
|
gap: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
level='title-sm'
|
||||||
|
sx={{ mb: 0, mt: 0, lineHeight: 1.1 }}
|
||||||
|
>
|
||||||
|
{option.value.charAt(0).toUpperCase() +
|
||||||
|
option.value.slice(1)}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
level='body-sm'
|
||||||
|
sx={{ mt: 0, mb: 0, lineHeight: 1.1 }}
|
||||||
|
>
|
||||||
|
{option.description}
|
||||||
|
</Typography>
|
||||||
|
</ListItem>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{userProfile.role === 'admin' &&
|
||||||
|
member.userId !== userProfile.id &&
|
||||||
|
member.isActive && (
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
circleMembers.find(m => userProfile.id == m.userId)
|
||||||
|
.role !== 'admin'
|
||||||
|
}
|
||||||
|
variant='outlined'
|
||||||
|
color='danger'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => {
|
||||||
|
const confirmed = confirm(
|
||||||
|
`Are you sure you want to remove ${member.displayName} from your circle?`,
|
||||||
|
)
|
||||||
|
if (confirmed) {
|
||||||
|
DeleteCircleMember(
|
||||||
|
member.circleId,
|
||||||
|
member.userId,
|
||||||
|
).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
alert('Removed member successfully.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ const RichTextEditor = ({
|
|||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Define compression options based on entity type
|
// Define compression options based on entity type ( this need a revist later)
|
||||||
const compressionOptions = {
|
const compressionOptions = {
|
||||||
maxSizeMB: entityType === 'profile' ? 0.5 : 1, // Smaller size for profile images
|
maxSizeMB: entityType === 'profile' ? 0.5 : 1, // Smaller size for profile images
|
||||||
maxWidthOrHeight: entityType === 'profile' ? 320 : 1200, // Smaller dimensions for profile images
|
maxWidthOrHeight: entityType === 'profile' ? 320 : 1200, // Smaller dimensions for profile images
|
||||||
useWebWorker: true,
|
useWebWorker: true,
|
||||||
fileType: 'image/jpeg', // Always convert to JPEG
|
fileType: 'image/jpeg',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compress the image
|
// Compress the image
|
||||||
@@ -59,6 +59,7 @@ const RichTextEditor = ({
|
|||||||
formData.append('file', compressedJpegFile)
|
formData.append('file', compressedJpegFile)
|
||||||
formData.append('entityId', entityId)
|
formData.append('entityId', entityId)
|
||||||
formData.append('entityType', entityType)
|
formData.append('entityType', entityType)
|
||||||
|
|
||||||
const response = await UploadFile('/assets/chore', {
|
const response = await UploadFile('/assets/chore', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
@@ -83,7 +84,6 @@ const RichTextEditor = ({
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
const url = resolvePhotoURL(data.url || data.sign)
|
const url = resolvePhotoURL(data.url || data.sign)
|
||||||
// Insert image into Quill
|
// Insert image into Quill
|
||||||
|
|||||||
Reference in New Issue
Block a user