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) => (
- |
- ))}
-
-
-
- */}
-
-
-
-
+
+
+
+
+
)
}
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