diff --git a/src/App.jsx b/src/App.jsx index dec5fa5..6aa369c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,6 +6,7 @@ import { useEffect, useState } from 'react' import { Outlet, useNavigate } from 'react-router-dom' import { useRegisterSW } from 'virtual:pwa-register/react' import { registerCapacitorListeners } from './CapacitorListener' +import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext' import { UserContext } from './contexts/UserContext' import { useResource } from './queries/ResourceQueries' import { AuthenticationProvider } from './service/AuthenticationService' @@ -93,14 +94,18 @@ function App() { return (
+ - - - - + + + + + + + {needRefresh && ( diff --git a/src/contexts/ImpersonateUserContext.jsx b/src/contexts/ImpersonateUserContext.jsx new file mode 100644 index 0000000..23f5503 --- /dev/null +++ b/src/contexts/ImpersonateUserContext.jsx @@ -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 ( + + {children} + + ) +} diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx index d36f132..7313f58 100644 --- a/src/utils/Fetcher.jsx +++ b/src/utils/Fetcher.jsx @@ -97,12 +97,9 @@ const GetChoreDetailById = id => { headers: HEADERS(), }) } -const MarkChoreComplete = (id, note, completedDate, performer) => { +const MarkChoreComplete = (id, body, completedDate, performer) => { var markChoreURL = `/chores/${id}/do` - const body = { - note, - } let completedDateFormated = '' if (completedDate) { completedDateFormated = `?completedDate=${new Date( @@ -220,6 +217,14 @@ const GetAllCircleMembers = async () => { return resp.json() } +const UpdateMemberRole = async (memberId, role) => { + return Fetch(`/circles/members/role`, { + method: 'PUT', + headers: HEADERS(), + body: JSON.stringify({ role, memberId }), + }) +} + const GetUserProfile = () => { return Fetch(`/users/profile`, { method: 'GET', @@ -540,6 +545,7 @@ export { UpdateChoreStatus, UpdateDueDate, UpdateLabel, + UpdateMemberRole, UpdateNotificationTarget, UpdatePassword, UpdateThingState, diff --git a/src/utils/TokenManager.jsx b/src/utils/TokenManager.jsx index ad8378a..eb10a51 100644 --- a/src/utils/TokenManager.jsx +++ b/src/utils/TokenManager.jsx @@ -43,6 +43,28 @@ class 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) { if (!isTokenValid()) { Cookies.set('ca_redirect', window.location.pathname) diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx index b1cf44b..1e3f80b 100644 --- a/src/views/ChoreEdit/ChoreView.jsx +++ b/src/views/ChoreEdit/ChoreView.jsx @@ -37,6 +37,8 @@ import { Divider } from '@mui/material' import moment from 'moment' import { useEffect, useState } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' + +import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' import { useChoreDetails } from '../../queries/ChoreQueries.jsx' import { useCircleMembers } from '../../queries/UserQueries.jsx' import { notInCompletionWindow } from '../../utils/Chores.jsx' @@ -85,6 +87,7 @@ const ChoreView = () => { isLoading: isCircleMembersLoading, handleRefetch: handleCircleMembersRefetch, } = useCircleMembers() + const { impersonatedUser } = useImpersonateUser() const { data: choreData, @@ -181,7 +184,14 @@ const ChoreView = () => { }, 1000) const id = setTimeout(() => { - MarkChoreComplete(choreId, note, completedDate, null) + MarkChoreComplete( + choreId, + impersonatedUser + ? { completedBy: impersonatedUser.userId, note } + : { note }, + completedDate, + null, + ) .then(resp => { if (resp.ok) { return resp.json().then(data => { diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index bc7e5ec..64a5da2 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -44,6 +44,7 @@ import { import moment from 'moment' import React, { useEffect } from 'react' import { useNavigate } from 'react-router-dom' +import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' import { UserContext } from '../../contexts/UserContext' import { useError } from '../../service/ErrorProvider' import { notInCompletionWindow } from '../../utils/Chores.jsx' @@ -93,6 +94,8 @@ const ChoreCard = ({ const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null) const [timeoutId, setTimeoutId] = React.useState(null) const { userProfile } = React.useContext(UserContext) + const { impersonatedUser } = useImpersonateUser() + const { showError } = useError() useEffect(() => { document.addEventListener('mousedown', handleMenuOutsideClick) @@ -187,7 +190,12 @@ const ChoreCard = ({ }, 1000) const id = setTimeout(() => { - MarkChoreComplete(chore.id, null, null, null) + MarkChoreComplete( + chore.id, + impersonatedUser ? { completedBy: impersonatedUser.userId } : null, + null, + null, + ) .then(resp => { if (resp.ok) { return resp.json().then(data => { @@ -249,7 +257,7 @@ const ChoreCard = ({ MarkChoreComplete( chore.id, - null, + impersonatedUser ? { completedBy: impersonatedUser.userId } : null, new Date(newDate).toISOString(), null, ).then(response => { @@ -272,7 +280,14 @@ const ChoreCard = ({ }) } 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) { response.json().then(data => { const newChore = data.res diff --git a/src/views/Chores/Sidepanel.jsx b/src/views/Chores/Sidepanel.jsx index 1e83e99..6bb292d 100644 --- a/src/views/Chores/Sidepanel.jsx +++ b/src/views/Chores/Sidepanel.jsx @@ -3,6 +3,7 @@ import { useMediaQuery } from '@mui/material' import { useEffect, useState } from 'react' import { ChoresGrouper } from '../../utils/Chores' import CalendarView from '../components/CalendarView' +import WelcomeCard from './WelcomeCard' const Sidepanel = ({ chores }) => { const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md')) @@ -30,61 +31,28 @@ const Sidepanel = ({ chores }) => { return null } return ( - - {/* + + - - - {dueDatePieChartData.map((entry, index) => ( - - ))} - - - `${label}: ${value.payload.value}`} - wrapperStyle={{ paddingTop: 0, marginTop: 0 }} // Adjust padding and margin - /> - - - */} - - - - + + + + + ) } diff --git a/src/views/Chores/WelcomeCard.jsx b/src/views/Chores/WelcomeCard.jsx new file mode 100644 index 0000000..323f794 --- /dev/null +++ b/src/views/Chores/WelcomeCard.jsx @@ -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 ( + + + + + Who's checking in? + + + + { + setImpersonatedUser(user) + setIsModalOpen(false) + }} + onClose={() => setIsModalOpen(false)} + /> + + + ) + } + + return ( + + + + + + + + + {impersonatedUser?.displayName || impersonatedUser?.name} + + {/* + 5 chores assigned, 2 due soon + */} + + + + + + + + { + setImpersonatedUser(user) + setIsModalOpen(false) + }} + onClose={() => { + setIsModalOpen(false) + }} + /> + + ) +} +export default WelcomeCard diff --git a/src/views/Modals/Inputs/UserModal.jsx b/src/views/Modals/Inputs/UserModal.jsx new file mode 100644 index 0000000..f617307 --- /dev/null +++ b/src/views/Modals/Inputs/UserModal.jsx @@ -0,0 +1,58 @@ +import { + Avatar, + Box, + Button, + List, + ListItem, + Modal, + ModalDialog, + ModalOverflow, + Typography, +} from '@mui/joy' + +const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => { + return ( + + + + + Select User + + + {performers.map(user => ( + { + onSelect(user) + onClose() + }} + > + + + {user.displayName || user.name} + + + ))} + + + + + + + + ) +} + +export default UserModal diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx index e115f7a..58c98f2 100644 --- a/src/views/Settings/Settings.jsx +++ b/src/views/Settings/Settings.jsx @@ -10,6 +10,9 @@ import { FormControl, FormHelperText, Input, + ListItem, + Option, + Select, Typography, } from '@mui/joy' import moment from 'moment' @@ -28,6 +31,7 @@ import { JoinCircle, LeaveCircle, PutWebhookURL, + UpdateMemberRole, UpdatePassword, } from '../../utils/Fetcher' import { isPlusAccount } from '../../utils/Helpers' @@ -46,6 +50,7 @@ const Settings = () => { const [circleMembers, setCircleMembers] = useState([]) const [webhookURL, setWebhookURL] = useState(null) const [webhookError, setWebhookError] = useState(null) + const [isAdmin, setIsAdmin] = useState(false) const [changePasswordModal, setChangePasswordModal] = useState(false) 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(() => { const hash = window.location.hash if (hash) { @@ -222,33 +237,110 @@ const Settings = () => { )} - {member.userId !== userProfile.id && member.isActive && ( - - )} + + + {member.userId !== userProfile.id && isAdmin && ( + + )} + {userProfile.role === 'admin' && + member.userId !== userProfile.id && + member.isActive && ( + + )} + ))} diff --git a/src/views/components/RichTextEditor.jsx b/src/views/components/RichTextEditor.jsx index 5e49c44..f538f95 100644 --- a/src/views/components/RichTextEditor.jsx +++ b/src/views/components/RichTextEditor.jsx @@ -31,12 +31,12 @@ const RichTextEditor = ({ if (!file) return try { - // Define compression options based on entity type + // Define compression options based on entity type ( this need a revist later) const compressionOptions = { maxSizeMB: entityType === 'profile' ? 0.5 : 1, // Smaller size for profile images maxWidthOrHeight: entityType === 'profile' ? 320 : 1200, // Smaller dimensions for profile images useWebWorker: true, - fileType: 'image/jpeg', // Always convert to JPEG + fileType: 'image/jpeg', } // Compress the image @@ -59,6 +59,7 @@ const RichTextEditor = ({ formData.append('file', compressedJpegFile) formData.append('entityId', entityId) formData.append('entityType', entityType) + const response = await UploadFile('/assets/chore', { method: 'POST', body: formData, @@ -83,7 +84,6 @@ const RichTextEditor = ({ }) return } - const data = await response.json() const url = resolvePhotoURL(data.url || data.sign) // Insert image into Quill