Initial Activity View

Add New Colors.jsx
Allow Dark model Toggle from the Navbar
This commit is contained in:
Mo Tarbin 2024-12-28 18:52:06 -05:00
parent 4fc836a34b
commit d4c36e2057
17 changed files with 1326 additions and 310 deletions

View file

@ -5,6 +5,7 @@ import Error from '@/views/Error'
import Settings from '@/views/Settings/Settings'
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
import ForgotPasswordView from '../views/Authorization/ForgotPasswordView'
import LoginSettings from '../views/Authorization/LoginSettings'
import LoginView from '../views/Authorization/LoginView'
import SignupView from '../views/Authorization/Signup'
import UpdatePasswordView from '../views/Authorization/UpdatePasswordView'
@ -21,7 +22,7 @@ import TermsView from '../views/Terms/TermsView'
import TestView from '../views/TestView/Test'
import ThingsHistory from '../views/Things/ThingsHistory'
import ThingsView from '../views/Things/ThingsView'
import LoginSettings from '../views/Authorization/LoginSettings'
import UserActivities from '../views/User/UserActivities'
const getMainRoute = () => {
if (import.meta.env.VITE_IS_LANDING_DEFAULT === 'true') {
return <Landing />
@ -66,6 +67,10 @@ const Router = createBrowserRouter([
path: '/my/chores',
element: <MyChores />,
},
{
path: '/activities',
element: <UserActivities />,
},
{
path: '/login',
element: <LoginView />,

View file

@ -1,9 +1,12 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useQuery } from 'react-query'
import { CreateChore, GetChoresNew } from '../utils/Fetcher'
import { CreateChore, GetChoresHistory, GetChoresNew } from '../utils/Fetcher'
export const useChores = () => {
return useQuery('chores', GetChoresNew)
export const useChores = includeArchive => {
return useQuery(['chores', includeArchive], () =>
GetChoresNew(includeArchive),
)
}
export const useCreateChore = () => {
@ -15,3 +18,17 @@ export const useCreateChore = () => {
},
})
}
export const useChoresHistory = initialLimit => {
const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected
const { data, error, isLoading } = useQuery(['choresHistory', limit], () =>
GetChoresHistory(limit),
)
const handleLimitChange = newLimit => {
setLimit(newLimit)
}
return { data, error, isLoading, handleLimitChange }
}

160
src/utils/Chores.jsx Normal file
View file

@ -0,0 +1,160 @@
import { TASK_COLOR } from './Colors.jsx'
export const ChoresGrouper = (groupBy, chores) => {
// sort by priority then due date:
chores.sort((a, b) => {
// no priority is lowest priority:
if (a.priority === 0) {
return 1
}
if (a.priority !== b.priority) {
return a.priority - b.priority
}
if (a.nextDueDate === null) {
return 1
}
if (b.nextDueDate === null) {
return -1
}
return new Date(a.nextDueDate) - new Date(b.nextDueDate)
})
var groups = []
switch (groupBy) {
case 'due_date':
var groupRaw = {
Today: [],
'In a week': [],
'This month': [],
Later: [],
Overdue: [],
Anytime: [],
}
chores.forEach(chore => {
if (chore.nextDueDate === null) {
groupRaw['Anytime'].push(chore)
} else if (new Date(chore.nextDueDate) < new Date()) {
groupRaw['Overdue'].push(chore)
} else if (
new Date(chore.nextDueDate).toDateString() ===
new Date().toDateString()
) {
groupRaw['Today'].push(chore)
} else if (
new Date(chore.nextDueDate) <
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) &&
new Date(chore.nextDueDate) > new Date()
) {
groupRaw['In a week'].push(chore)
} else if (
new Date(chore.nextDueDate).getMonth() === new Date().getMonth()
) {
groupRaw['This month'].push(chore)
} else {
groupRaw['Later'].push(chore)
}
})
groups = [
{
name: 'Overdue',
content: groupRaw['Overdue'],
color: TASK_COLOR.OVERDUE,
},
{ name: 'Today', content: groupRaw['Today'], color: TASK_COLOR.TODAY },
{
name: 'In a week',
content: groupRaw['In a week'],
color: TASK_COLOR.IN_A_WEEK,
},
{
name: 'This month',
content: groupRaw['This month'],
color: TASK_COLOR.THIS_MONTH,
},
{ name: 'Later', content: groupRaw['Later'], color: TASK_COLOR.LATER },
{
name: 'Anytime',
content: groupRaw['Anytime'],
color: TASK_COLOR.ANYTIME,
},
]
break
case 'priority':
groupRaw = {
p1: [],
p2: [],
p3: [],
p4: [],
no_priority: [],
}
chores.forEach(chore => {
switch (chore.priority) {
case 1:
groupRaw['p1'].push(chore)
break
case 2:
groupRaw['p2'].push(chore)
break
case 3:
groupRaw['p3'].push(chore)
break
case 4:
groupRaw['p4'].push(chore)
break
default:
groupRaw['no_priority'].push(chore)
break
}
})
groups = [
{
name: 'Priority 1',
content: groupRaw['p1'],
color: TASK_COLOR.PRIORITY_1,
},
{
name: 'Priority 2',
content: groupRaw['p2'],
color: TASK_COLOR.PRIORITY_2,
},
{
name: 'Priority 3',
content: groupRaw['p3'],
color: TASK_COLOR.PRIORITY_3,
},
{
name: 'Priority 4',
content: groupRaw['p4'],
color: TASK_COLOR.PRIORITY_4,
},
{
name: 'No Priority',
content: groupRaw['no_priority'],
color: TASK_COLOR.NO_PRIORITY,
},
]
break
case 'labels':
groupRaw = {}
var labels = {}
chores.forEach(chore => {
chore.labelsV2.forEach(label => {
labels[label.id] = label
if (groupRaw[label.id] === undefined) {
groupRaw[label.id] = []
}
groupRaw[label.id].push(chore)
})
})
groups = Object.keys(groupRaw).map(key => {
return {
name: labels[key].name,
content: groupRaw[key],
}
})
groups.sort((a, b) => {
a.name < b.name ? 1 : -1
})
}
return groups
}

View file

@ -26,6 +26,60 @@ const LABEL_COLORS = [
{ name: 'Sand', value: '#d7ccc8' },
]
export const COLORS = {
white: '#FFFFFF',
salmon: '#ff7961',
teal: '#26a69a',
skyBlue: '#80d8ff',
grape: '#7e57c2',
sunshine: '#ffee58',
coral: '#ff7043',
lavender: '#ce93d8',
rose: '#f48fb1',
charcoal: '#616161',
sienna: '#8d6e63',
mint: '#a7ffeb',
amber: '#ffc107',
cobalt: '#3f51b5',
emerald: '#4caf50',
peach: '#ffab91',
ocean: '#0288d1',
mustard: '#ffca28',
ruby: '#d32f2f',
periwinkle: '#b39ddb',
turquoise: '#00bcd4',
lime: '#cddc39',
blush: '#f8bbd0',
ash: '#90a4ae',
sand: '#d7ccc8',
}
export const TASK_COLOR = {
COMPLETED: '#4ec1a2',
LATE: '#f6ad55',
MISSED: '#F03A47',
UPCOMING: '#AF5B5B',
SKIPPED: '#E2C2FF',
// For the calendar
OVERDUE: '#F03A47',
TODAY: '#ffc107',
IN_A_WEEK: '#4ec1a2',
THIS_MONTH: '#00bcd4',
LATER: '#d7ccc8',
ANYTIME: '#90a4ae',
// FOR ASSIGNEE:
ASSIGNED_TO_ME: '#4ec1a2',
ASSIGNED_TO_OTHER: '#b39ddb',
// FOR PRIORITY:
PRIORITY_1: '#F03A47',
PRIORITY_2: '#ffc107',
PRIORITY_3: '#00bcd4',
PRIORITY_4: '#7e57c2',
NO_PRIORITY: '#90a4ae',
}
export default LABEL_COLORS
export const getTextColorFromBackgroundColor = bgColor => {

View file

@ -1,9 +1,5 @@
import { API_URL } from '../Config'
import { Fetch, HEADERS, apiManager } from './TokenManager'
const createChore = userID => {
return Fetch(`/chores/`, {
method: 'POST',
@ -33,7 +29,7 @@ const UpdatePassword = newPassword => {
}
const login = (username, password) => {
const baseURL = apiManager.getApiURL();
const baseURL = apiManager.getApiURL()
return fetch(`${baseURL}/auth/login`, {
headers: {
'Content-Type': 'application/json',
@ -49,8 +45,13 @@ const GetAllUsers = () => {
headers: HEADERS(),
})
}
const GetChoresNew = async () => {
const resp = await Fetch(`/chores/`, {
const GetChoresNew = async includeArchived => {
var url = `/chores/`
if (includeArchived) {
url += `?includeArchived=true`
}
const resp = await Fetch(url, {
method: 'GET',
headers: HEADERS(),
})
@ -95,7 +96,7 @@ const GetChoreDetailById = id => {
})
}
const MarkChoreComplete = (id, note, completedDate, performer) => {
var markChoreURL =`/chores/${id}/do`
var markChoreURL = `/chores/${id}/do`
const body = {
note,
@ -109,15 +110,13 @@ const MarkChoreComplete = (id, note, completedDate, performer) => {
}
if (performer) {
body.performer = Number(performer)
if(completedDateFormated === ''){
markChoreURL += `&performer=${performer}`
}
else{
if (completedDateFormated === '') {
markChoreURL += `&performer=${performer}`
} else {
markChoreURL += `?performer=${performer}`
}
}
return Fetch(markChoreURL, {
method: 'POST',
headers: HEADERS(),
@ -244,13 +243,10 @@ const LeaveCircle = id => {
}
const DeleteCircleMember = (circleID, memberID) => {
return Fetch(
`/circles/${circleID}/members/delete?member_id=${memberID}`,
{
method: 'DELETE',
headers: HEADERS(),
},
)
return Fetch(`/circles/${circleID}/members/delete?member_id=${memberID}`, {
method: 'DELETE',
headers: HEADERS(),
})
}
const UpdateUserDetails = userDetails => {
@ -345,13 +341,12 @@ const GetLongLiveTokens = () => {
headers: HEADERS(),
})
}
const PutNotificationTarget = ( platform, deviceToken) => {
const PutNotificationTarget = (platform, deviceToken) => {
return Fetch(`/users/targets`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify({ platform,deviceToken }),
}
)
body: JSON.stringify({ platform, deviceToken }),
})
}
const CreateLabel = label => {
return Fetch(`/labels`, {
@ -383,22 +378,19 @@ const DeleteLabel = id => {
})
}
const ChangePassword = (verifiticationCode, password) => {
const baseURL = apiManager.getApiURL();
return fetch(
`${baseURL}/auth/password?c=${verifiticationCode}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password: password }),
const ChangePassword = (verifiticationCode, password) => {
const baseURL = apiManager.getApiURL()
return fetch(`${baseURL}/auth/password?c=${verifiticationCode}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
)
body: JSON.stringify({ password: password }),
})
}
const ResetPassword = email => {
const basedURL = apiManager.getApiURL();
const basedURL = apiManager.getApiURL()
return fetch(`${basedURL}/auth/reset`, {
method: 'POST',
headers: {
@ -422,24 +414,34 @@ const UpdateDueDate = (id, dueDate) => {
}
const RefreshToken = () => {
const basedURL = apiManager.getApiURL();
const basedURL = apiManager.getApiURL()
return fetch(basedURL + '/auth/refresh', {
method: 'GET',
headers: HEADERS(),
})
}
const GetChoresHistory = async limit => {
var url = `/chores/history`
if (limit) {
url += `?limit=${limit}`
}
const resp = await Fetch(url, {
method: 'GET',
headers: HEADERS(),
})
return resp.json()
}
export {
AcceptCircleMemberRequest,
ArchiveChore,
CancelSubscription,
createChore,
ChangePassword,
CreateChore,
CreateLabel,
CreateLongLiveToken,
CreateThing,
DeleteChore,
DeleteChoreHistory,
ChangePassword,
DeleteCircleMember,
DeleteLabel,
DeleteLongLiveToken,
@ -451,6 +453,7 @@ export {
GetChoreDetailById,
GetChoreHistory,
GetChores,
GetChoresHistory,
GetChoresNew,
GetCircleMemberRequests,
GetLabels,
@ -462,14 +465,12 @@ export {
GetUserProfile,
JoinCircle,
LeaveCircle,
login,
MarkChoreComplete,
RefreshToken,
ResetPassword,
PutNotificationTarget,
RefreshToken,
ResetPassword,
SaveChore,
SaveThing,
signUp,
SkipChore,
UnArchiveChore,
UpdateChoreAssignee,
@ -481,4 +482,7 @@ export {
UpdatePassword,
UpdateThingState,
UpdateUserDetails,
createChore,
login,
signUp,
}

View file

@ -9,6 +9,7 @@ import {
Divider,
FormControl,
FormHelperText,
FormLabel,
Input,
List,
ListItem,
@ -20,12 +21,14 @@ import {
Sheet,
Snackbar,
Stack,
Switch,
Typography,
} from '@mui/joy'
import moment from 'moment'
import { useContext, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { UserContext } from '../../contexts/UserContext'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import {
CreateChore,
DeleteChore,
@ -36,7 +39,6 @@ import {
SaveChore,
} from '../../utils/Fetcher'
import { isPlusAccount } from '../../utils/Helpers'
import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
import { useLabels } from '../Labels/LabelQueries'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import LabelModal from '../Modals/Inputs/LabelModal'
@ -72,6 +74,7 @@ const ChoreEdit = () => {
const [frequencyMetadata, setFrequencyMetadata] = useState({})
const [labels, setLabels] = useState([])
const [labelsV2, setLabelsV2] = useState([])
const [points, setPoints] = useState(-1)
const [allUserThings, setAllUserThings] = useState([])
const [thingTrigger, setThingTrigger] = useState(null)
const [isThingValid, setIsThingValid] = useState(false)
@ -196,6 +199,7 @@ const ChoreEdit = () => {
labelsV2: labelsV2,
notificationMetadata: notificationMetadata,
thingTrigger: thingTrigger,
points: points < 0 ? null : points,
}
let SaveFunction = CreateChore
if (choreId > 0) {
@ -247,6 +251,9 @@ const ChoreEdit = () => {
setFrequency(data.res.frequency)
setNotificationMetadata(JSON.parse(data.res.notificationMetadata))
setPoints(
data.res.points && data.res.points > -1 ? data.res.points : -1,
)
// setLabels(data.res.labels ? data.res.labels.split(',') : [])
setLabelsV2(data.res.labelsV2)
@ -758,23 +765,6 @@ const ChoreEdit = () => {
<Typography level='h5'>
Things to remember about this chore or to tag it
</Typography>
{/* <FreeSoloCreateOption
options={[...labels, 'test']}
selected={labels}
onSelectChange={changes => {
const newLabels = []
changes.map(change => {
// if type is string :
if (typeof change === 'string') {
// add the change to the labels array:
newLabels.push(change)
} else {
newLabels.push(change.inputValue)
}
})
setLabels(newLabels)
}}
/> */}
<Select
multiple
onChange={(event, newValue) => {
@ -839,37 +829,80 @@ const ChoreEdit = () => {
Add New Label
</MenuItem>
</Select>
{/* <Card>
<List
orientation='horizontal'
wrap
sx={{
'--List-gap': '8px',
'--ListItem-radius': '20px',
}}
>
{labels?.map((label, index) => (
<ListItem key={label}>
<Chip
onClick={() => {
setLabels(labels.filter(l => l !== label))
}}
checked={true}
overlay
variant='soft'
color='primary'
size='lg'
endDecorator={<Cancel />}
>
{label}
</Chip>
</ListItem>
))}
</List>
</Card> */}
</Box>
<Box mt={2}>
<Typography level='h4' gutterBottom>
Others :
</Typography>
<FormControl
orientation='horizontal'
sx={{ width: 400, justifyContent: 'space-between' }}
>
<div>
<FormLabel>Assign Points</FormLabel>
<FormHelperText sx={{ mt: 0 }}>
Assign points to this task and user will earn points when they
completed it
</FormHelperText>
</div>
<Switch
checked={points > -1}
onClick={event => {
event.preventDefault()
if (points > -1) {
setPoints(-1)
} else {
setPoints(1)
}
}}
color={points !== -1 ? 'success' : 'neutral'}
variant={points !== -1 ? 'solid' : 'outlined'}
// endDecorator={points !== -1 ? 'On' : 'Off'}
slotProps={{
endDecorator: {
sx: {
minWidth: 24,
},
},
}}
/>
</FormControl>
{points != -1 && (
<Card variant='outlined'>
<Box
sx={{
mt: 0,
ml: 4,
}}
>
<Typography level='body-sm'>Points:</Typography>
<Input
type='number'
value={points}
sx={{ maxWidth: 100 }}
// add min points is 0 and max is 1000
slotProps={{
input: {
min: 0,
max: 1000,
},
}}
placeholder='Points'
onChange={e => {
setPoints(parseInt(e.target.value))
}}
/>
</Box>
</Card>
)}
</Box>
{choreId > 0 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mt: 3 }}>
<Sheet
sx={{
p: 2,
@ -900,6 +933,7 @@ const ChoreEdit = () => {
</Sheet>
</Box>
)}
<Divider sx={{ mb: 9 }} />
{/* <Box mt={2} alignSelf={'flex-start'} display='flex' gap={2}>

View file

@ -36,6 +36,7 @@ import { Divider } from '@mui/material'
import moment from 'moment'
import { useEffect, useState } from 'react'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import {
GetAllUsers,
GetChoreDetailById,
@ -43,7 +44,6 @@ import {
SkipChore,
UpdateChorePriority,
} from '../../utils/Fetcher'
import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
import Priorities from '../../utils/Priorities'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
const IconCard = styled('div')({

View file

@ -42,8 +42,8 @@ import {
import moment from 'moment'
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { API_URL } from '../../Config'
import { UserContext } from '../../contexts/UserContext'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import {
ArchiveChore,
DeleteChore,
@ -53,9 +53,7 @@ import {
UpdateChoreAssignee,
UpdateDueDate,
} from '../../utils/Fetcher'
import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
import Priorities from '../../utils/Priorities'
import { Fetch } from '../../utils/TokenManager'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import DateModal from '../Modals/Inputs/DateModal'
import SelectModal from '../Modals/Inputs/SelectModal'
@ -130,8 +128,7 @@ const ChoreCard = ({
message: 'Are you sure you want to delete this chore?',
onClose: isConfirmed => {
if (isConfirmed === true) {
DeleteChore(chore.id)
.then(response => {
DeleteChore(chore.id).then(response => {
if (response.ok) {
onChoreRemove(chore)
}
@ -181,7 +178,7 @@ const ChoreCard = ({
}, 1000)
const id = setTimeout(() => {
MarkChoreComplete(chore.id, null, null,null)
MarkChoreComplete(chore.id, null, null, null)
.then(resp => {
if (resp.ok) {
return resp.json().then(data => {
@ -221,9 +218,13 @@ const ChoreCard = ({
alert('Please select a performer')
return
}
MarkChoreComplete(chore.id, null, new Date(newDate).toISOString(), null)
.then(response => {
MarkChoreComplete(
chore.id,
null,
new Date(newDate).toISOString(),
null,
).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
@ -243,9 +244,7 @@ const ChoreCard = ({
})
}
const handleCompleteWithNote = note => {
MarkChoreComplete(chore.id, note, null, null)
.then(response => {
MarkChoreComplete(chore.id, note, null, null).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res

View file

@ -1,7 +1,7 @@
import { Chip, Menu, MenuItem, Typography } from '@mui/joy'
import IconButton from '@mui/joy/IconButton'
import React, { useEffect, useRef, useState } from 'react'
import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
const IconButtonWithMenu = ({
key,

View file

@ -43,7 +43,11 @@ import { useLabels } from '../Labels/LabelQueries'
import ChoreCard from './ChoreCard'
import IconButtonWithMenu from './IconButtonWithMenu'
import { canScheduleNotification, scheduleChoreNotification } from './LocalNotificationScheduler'
import { ChoresGrouper } from '../../utils/Chores'
import {
canScheduleNotification,
scheduleChoreNotification,
} from './LocalNotificationScheduler'
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
const MyChores = () => {
@ -92,159 +96,40 @@ const MyChores = () => {
return aDueDate - bDueDate // Sort ascending by due date
}
const sectionSorter = (t, chores) => {
// sort by priority then due date:
chores.sort((a, b) => {
// no priority is lowest priority:
if (a.priority === 0) {
return 1
}
if (a.priority !== b.priority) {
return a.priority - b.priority
}
if (a.nextDueDate === null) {
return 1
}
if (b.nextDueDate === null) {
return -1
}
return new Date(a.nextDueDate) - new Date(b.nextDueDate)
})
var groups = []
switch (t) {
case 'due_date':
var groupRaw = {
Today: [],
'In a week': [],
'This month': [],
Later: [],
Overdue: [],
Anytime: [],
}
chores.forEach(chore => {
if (chore.nextDueDate === null) {
groupRaw['Anytime'].push(chore)
} else if (new Date(chore.nextDueDate) < new Date()) {
groupRaw['Overdue'].push(chore)
} else if (
new Date(chore.nextDueDate).toDateString() ===
new Date().toDateString()
) {
groupRaw['Today'].push(chore)
} else if (
new Date(chore.nextDueDate) <
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) &&
new Date(chore.nextDueDate) > new Date()
) {
groupRaw['In a week'].push(chore)
} else if (
new Date(chore.nextDueDate).getMonth() === new Date().getMonth()
) {
groupRaw['This month'].push(chore)
} else {
groupRaw['Later'].push(chore)
}
})
groups = [
{ name: 'Overdue', content: groupRaw['Overdue'] },
{ name: 'Today', content: groupRaw['Today'] },
{ name: 'In a week', content: groupRaw['In a week'] },
{ name: 'This month', content: groupRaw['This month'] },
{ name: 'Later', content: groupRaw['Later'] },
{ name: 'Anytime', content: groupRaw['Anytime'] },
]
break
case 'priority':
groupRaw = {
p1: [],
p2: [],
p3: [],
p4: [],
no_priority: [],
}
chores.forEach(chore => {
switch (chore.priority) {
case 1:
groupRaw['p1'].push(chore)
break
case 2:
groupRaw['p2'].push(chore)
break
case 3:
groupRaw['p3'].push(chore)
break
case 4:
groupRaw['p4'].push(chore)
break
default:
groupRaw['no_priority'].push(chore)
break
}
})
groups = [
{ name: 'Priority 1', content: groupRaw['p1'] },
{ name: 'Priority 2', content: groupRaw['p2'] },
{ name: 'Priority 3', content: groupRaw['p3'] },
{ name: 'Priority 4', content: groupRaw['p4'] },
{ name: 'No Priority', content: groupRaw['no_priority'] },
]
break
case 'labels':
groupRaw = {}
var labels = {}
chores.forEach(chore => {
chore.labelsV2.forEach(label => {
labels[label.id] = label
if (groupRaw[label.id] === undefined) {
groupRaw[label.id] = []
}
groupRaw[label.id].push(chore)
})
})
groups = Object.keys(groupRaw).map(key => {
return {
name: labels[key].name,
content: groupRaw[key],
}
})
groups.sort((a, b) => {
a.name < b.name ? 1 : -1
})
}
return groups
}
useEffect(() => {
Promise.all([GetChores(), GetAllUsers(),GetUserProfile()]).then(responses => {
const [choresResponse, usersResponse, userProfileResponse] = responses;
if (!choresResponse.ok) {
throw new Error(choresResponse.statusText);
}
if (!usersResponse.ok) {
throw new Error(usersResponse.statusText);
}
if (!userProfileResponse.ok) {
throw new Error(userProfileResponse.statusText);
Promise.all([GetChores(), GetAllUsers(), GetUserProfile()]).then(
responses => {
const [choresResponse, usersResponse, userProfileResponse] = responses
if (!choresResponse.ok) {
throw new Error(choresResponse.statusText)
}
Promise.all([choresResponse.json(), usersResponse.json(), userProfileResponse.json()]).then(data => {
const [choresData, usersData, userProfileData] = data;
setUserProfile(userProfileData.res);
choresData.res.sort(choreSorter);
setChores(choresData.res);
setFilteredChores(choresData.res);
setPerformers(usersData.res);
if (!usersResponse.ok) {
throw new Error(usersResponse.statusText)
}
if (!userProfileResponse.ok) {
throw new Error(userProfileResponse.statusText)
}
Promise.all([
choresResponse.json(),
usersResponse.json(),
userProfileResponse.json(),
]).then(data => {
const [choresData, usersData, userProfileData] = data
setUserProfile(userProfileData.res)
choresData.res.sort(choreSorter)
setChores(choresData.res)
setFilteredChores(choresData.res)
setPerformers(usersData.res)
if (canScheduleNotification()) {
scheduleChoreNotification(choresData.res, userProfileData.res, usersData.res);
scheduleChoreNotification(
choresData.res,
userProfileData.res,
usersData.res,
)
}
});
})
})
},
)
// GetAllUsers()
// .then(response => response.json())
@ -266,7 +151,7 @@ const MyChores = () => {
const sortedChores = choresData.res.sort(choreSorter)
setChores(sortedChores)
setFilteredChores(sortedChores)
const sections = sectionSorter('due_date', sortedChores)
const sections = ChoresGrouper('due_date', sortedChores)
setChoreSections(sections)
setOpenChoreSections(
Object.keys(sections).reduce((acc, key) => {
@ -354,7 +239,7 @@ const MyChores = () => {
}
setChores(newChores)
setFilteredChores(newFilteredChores)
setChoreSections(sectionSorter('due_date', newChores))
setChoreSections(ChoresGrouper('due_date', newChores))
switch (event) {
case 'completed':
@ -385,7 +270,7 @@ const MyChores = () => {
)
setChores(newChores)
setFilteredChores(newFilteredChores)
setChoreSections(sectionSorter('due_date', newChores))
setChoreSections(ChoresGrouper('due_date', newChores))
}
const searchOptions = {
@ -419,13 +304,13 @@ const MyChores = () => {
setSearchTerm(term)
setFilteredChores(fuse.search(term).map(result => result.item))
}
if (
userProfile === null ||
userLabelsLoading ||
performers.length === 0 ||
choresLoading
) {
) {
return <LoadingComponent />
}
@ -580,7 +465,7 @@ const MyChores = () => {
]}
selectedItem={selectedChoreSection}
onItemSelect={selected => {
const section = sectionSorter(selected.value, chores)
const section = ChoresGrouper(selected.value, chores)
setChoreSections(section)
setSelectedChoreSection(selected.value)
setFilteredChores(chores)

View file

@ -11,6 +11,49 @@ import {
} from '@mui/joy'
import moment from 'moment'
export const getCompletedChip = historyEntry => {
var text = 'No Due Date'
var color = 'info'
var icon = <CalendarViewDay />
// if completed few hours +-6 hours
if (
historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate - 1000 * 60 * 60 * 6 &&
historyEntry.completedAt < historyEntry.dueDate + 1000 * 60 * 60 * 6
) {
text = 'On Time'
color = 'success'
icon = <Check />
} else if (
historyEntry.dueDate &&
historyEntry.completedAt < historyEntry.dueDate
) {
text = 'On Time'
color = 'success'
icon = <Check />
}
// if completed after due date then it's late
else if (
historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate
) {
text = 'Late'
color = 'warning'
icon = <Timelapse />
} else {
text = 'No Due Date'
color = 'neutral'
icon = <CalendarViewDay />
}
return (
<Chip startDecorator={icon} color={color}>
{text}
</Chip>
)
}
const HistoryCard = ({
allHistory,
performers,
@ -38,49 +81,6 @@ const HistoryCard = ({
return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}`
}
const getCompletedChip = historyEntry => {
var text = 'No Due Date'
var color = 'info'
var icon = <CalendarViewDay />
// if completed few hours +-6 hours
if (
historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate - 1000 * 60 * 60 * 6 &&
historyEntry.completedAt < historyEntry.dueDate + 1000 * 60 * 60 * 6
) {
text = 'On Time'
color = 'success'
icon = <Check />
} else if (
historyEntry.dueDate &&
historyEntry.completedAt < historyEntry.dueDate
) {
text = 'On Time'
color = 'success'
icon = <Check />
}
// if completed after due date then it's late
else if (
historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate
) {
text = 'Late'
color = 'warning'
icon = <Timelapse />
} else {
text = 'No Due Date'
color = 'neutral'
icon = <CalendarViewDay />
}
return (
<Chip startDecorator={icon} color={color}>
{text}
</Chip>
)
}
return (
<>
<ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }} onClick={onClick}>

View file

@ -12,8 +12,8 @@ import {
import React, { useEffect } from 'react'
import { useQueryClient } from 'react-query'
import LABEL_COLORS from '../../../utils/Colors.jsx'
import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
import LABEL_COLORS from '../../../utils/LabelColors'
import { useLabels } from '../../Labels/LabelQueries'
function LabelModal({ isOpen, onClose, onSave, label }) {

View file

@ -0,0 +1,51 @@
import useStickyState from '@/hooks/useStickyState'
import {
BrightnessAuto,
DarkModeOutlined,
LightModeOutlined,
} from '@mui/icons-material'
import { FormControl, IconButton, useColorScheme } from '@mui/joy'
const ELEMENTID = 'select-theme-mode'
const ThemeToggleButton = ({ sx }) => {
const { mode, setMode } = useColorScheme()
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
const handleThemeModeChange = e => {
e.preventDefault()
e.stopPropagation()
let newThemeMode
switch (themeMode) {
case 'light':
newThemeMode = 'dark'
break
case 'dark':
newThemeMode = 'system'
break
case 'system':
default:
newThemeMode = 'light'
break
}
setThemeMode(newThemeMode)
setMode(newThemeMode)
}
return (
<FormControl sx={sx}>
<IconButton onClick={handleThemeModeChange}>
{themeMode === 'light' ? (
<DarkModeOutlined />
) : themeMode === 'dark' ? (
<BrightnessAuto />
) : (
<LightModeOutlined />
)}
</IconButton>
</FormControl>
)
}
export default ThemeToggleButton

View file

@ -0,0 +1,395 @@
import CancelIcon from '@mui/icons-material/Cancel'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CircleIcon from '@mui/icons-material/Circle'
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts'
import { Toll } from '@mui/icons-material'
import {
Box,
Card,
Chip,
Container,
Divider,
Grid,
Stack,
Tab,
TabList,
Tabs,
Typography,
} from '@mui/joy'
import React, { useEffect } from 'react'
import { UserContext } from '../../contexts/UserContext'
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
import { ChoresGrouper } from '../../utils/Chores'
import { TASK_COLOR } from '../../utils/Colors.jsx'
import LoadingComponent from '../components/Loading'
const groupByDate = history => {
const aggregated = {}
for (let i = 0; i < history.length; i++) {
const item = history[i]
const date = new Date(item.completedAt).toLocaleDateString()
if (!aggregated[date]) {
aggregated[date] = []
}
aggregated[date].push(item)
}
return aggregated
}
const ChoreHistoryItem = ({ time, name, points, status }) => {
const statusIcon = {
completed: <CheckCircleIcon color='success' />,
missed: <CancelIcon color='error' />,
pending: <CircleIcon color='neutral' />,
}
return (
<Stack direction='row' alignItems='center' spacing={2}>
<Typography level='body-md' sx={{ minWidth: 80 }}>
{time}
</Typography>
<Box>
{statusIcon[status] ? statusIcon[status] : statusIcon['completed']}
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
minHeight: 40,
// center vertically:
justifyContent: 'center',
}}
>
<Typography
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '50vw',
}}
level='body-md'
>
{name}
</Typography>
{points && (
<Chip size='sm' color='success' startDecorator={<Toll />}>
{`${points} points`}
</Chip>
)}
</Box>
</Stack>
)
}
const ChoreHistoryTimeline = ({ history }) => {
const groupedHistory = groupByDate(history)
return (
<Container sx={{ p: 2 }}>
<Typography level='h4' sx={{ mb: 2 }}>
Activities Timeline
</Typography>
{Object.entries(groupedHistory).map(([date, items]) => (
<Box key={date} sx={{ mb: 4 }}>
<Typography level='title-sm' sx={{ mb: 0.5 }}>
{new Date(date).toLocaleDateString([], {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</Typography>
<Divider />
<Stack spacing={1}>
{items.map(record => (
<>
<ChoreHistoryItem
key={record.id}
time={new Date(record.completedAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
name={record.choreName}
points={record.points}
status={record.status}
/>
</>
))}
</Stack>
</Box>
))}
</Container>
)
}
const renderPieChart = (data, size, isPrimary) => (
<PieChart width={size} height={size}>
<Pie
data={data}
dataKey='value'
nameKey='label'
cx='50%'
cy='50%'
innerRadius={isPrimary ? size / 4 : size / 6}
paddingAngle={5}
cornerRadius={5}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
{isPrimary && <Tooltip />}
{isPrimary && (
<Legend
layout='horizontal'
verticalAlign='bottom'
align='center'
// format as : {entry.payload.label}: {value}
iconType='circle'
formatter={(label, value) => `${label}: ${value.payload.value}`}
/>
)}
</PieChart>
)
const UserActivites = () => {
const { userProfile } = React.useContext(UserContext)
const [tabValue, setTabValue] = React.useState(30)
const [selectedHistory, setSelectedHistory] = React.useState([])
const [selectedChart, setSelectedChart] = React.useState('history')
const [historyPieChartData, setHistoryPieChartData] = React.useState([])
const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([])
const [choresAssignedChartData, setChoresAssignedChartData] = React.useState(
[],
)
const [choresPriorityChartData, setChoresPriorityChartData] = React.useState(
[],
)
const { data: choresData, isLoading: isChoresLoading } = useChores(true)
const {
data: choresHistory,
isChoresHistoryLoading,
handleLimitChange: refetchHistory,
} = useChoresHistory(tabValue ? tabValue : 30)
useEffect(() => {
if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) {
const enrichedHistory = choresHistory.res.map(item => {
const chore = choresData.res.find(chore => chore.id === item.choreId)
return {
...item,
choreName: chore?.name,
}
})
setSelectedHistory(enrichedHistory)
setHistoryPieChartData(generateHistoryPieChartData(enrichedHistory))
}
}, [isChoresHistoryLoading, isChoresLoading, choresHistory])
useEffect(() => {
if (!isChoresLoading && choresData) {
const choreDuePieChartData = generateChoreDuePieChartData(choresData.res)
setChoreDuePieChartData(choreDuePieChartData)
setChoresAssignedChartData(generateChoreAssignedChartData(choresData.res))
setChoresPriorityChartData(
generateChorePriorityPieChartData(choresData.res),
)
}
}, [isChoresLoading, choresData])
const generateChoreAssignedChartData = chores => {
var assignedToMe = 0
var assignedToOthers = 0
chores.forEach(chore => {
if (chore.assignedTo === userProfile.id) {
assignedToMe++
} else assignedToOthers++
})
const group = []
if (assignedToMe > 0) {
group.push({
label: `Assigned to me`,
value: assignedToMe,
color: TASK_COLOR.ASSIGNED_TO_ME,
id: 1,
})
}
if (assignedToOthers > 0) {
group.push({
label: `Assigned to others`,
value: assignedToOthers,
color: TASK_COLOR.ASSIGNED_TO_OTHERS,
id: 2,
})
}
return group
}
const generateChoreDuePieChartData = chores => {
const groups = ChoresGrouper('due_date', chores)
return groups
.map(group => {
return {
label: group.name,
value: group.content.length,
color: group.color,
id: group.name,
}
})
.filter(item => item.value > 0)
}
const generateChorePriorityPieChartData = chores => {
const groups = ChoresGrouper('priority', chores)
return groups
.map(group => {
return {
label: group.name,
value: group.content.length,
color: group.color,
id: group.name,
}
})
.filter(item => item.value > 0)
}
const generateHistoryPieChartData = history => {
const totalCompleted = history.filter(
item => item.dueDate > item.completedAt,
).length
const totalLate = history.filter(
item => item.dueDate < item.completedAt,
).length
return [
{
label: `On time`,
value: totalCompleted,
color: '#4ec1a2',
id: 1,
},
{
label: `Late`,
value: totalLate,
color: '#f6ad55',
id: 2,
},
]
}
if (isChoresHistoryLoading || isChoresLoading) {
return <LoadingComponent />
}
const COLORS = historyPieChartData.map(item => item.color)
const chartData = {
history: {
data: historyPieChartData,
title: 'Status',
description: 'Completed tasks status',
},
due: {
data: choreDuePieChartData,
title: 'Due Date',
description: 'Current tasks due date',
},
assigned: {
data: choresAssignedChartData,
title: 'Assignee',
description: 'Tasks assigned to you vs others',
},
priority: {
data: choresPriorityChartData,
title: 'Priority',
description: 'Tasks by priority',
},
}
return (
<Container
maxWidth='md'
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Tabs
onChange={(e, tabValue) => {
setTabValue(tabValue)
refetchHistory(tabValue)
}}
defaultValue={7}
sx={{
py: 0.5,
borderRadius: 16,
maxWidth: 400,
mb: 1,
}}
>
<TabList
disableUnderline
sx={{
borderRadius: 16,
backgroundColor: 'background.paper',
boxShadow: 1,
justifyContent: 'space-evenly',
}}
>
{[
{ label: '7 Days', value: 7 },
{ label: '30 Days', value: 30 },
{ label: '90 Days', value: 90 },
].map((tab, index) => (
<Tab
key={index}
sx={{
borderRadius: 16,
color: 'text.secondary',
'&.Mui-selected': {
color: 'text.primary',
backgroundColor: 'primary.light',
},
}}
disableIndicator
value={tab.value}
>
{tab.label}
</Tab>
))}
</TabList>
</Tabs>
<Box sx={{ mb: 4 }}>
<Typography level='h4' textAlign='center'>
{chartData[selectedChart].title}
</Typography>
<Typography level='body-xs' textAlign='center'>
{chartData[selectedChart].description}
</Typography>
{renderPieChart(chartData[selectedChart].data, 250, true)}
</Box>
<Grid container spacing={1}>
{Object.entries(chartData)
.filter(([key]) => key !== selectedChart)
.map(([key, { data, title }]) => (
<Grid item key={key} xs={4}>
<Card
onClick={() => setSelectedChart(key)}
sx={{ cursor: 'pointer', p: 1 }}
>
<Typography textAlign='center' level='body-xs' mb={-2}>
{title}
</Typography>
{renderPieChart(data, 75, false)}
</Card>
</Grid>
))}
</Grid>
<ChoreHistoryTimeline history={selectedHistory} />
</Container>
)
}
export default UserActivites

View file

@ -1,6 +1,7 @@
import Logo from '@/assets/logo.svg'
import {
AccountBox,
History,
HomeOutlined,
ListAlt,
Logout,
@ -23,6 +24,7 @@ import {
import { useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { version } from '../../../package.json'
import ThemeToggleButton from '../Settings/ThemeToggleButton'
import NavBarLink from './NavBarLink'
const links = [
{
@ -41,6 +43,11 @@ const links = [
label: 'Things',
icon: <Widgets />,
},
{
to: 'activities',
label: 'Activities',
icon: <History />,
},
{
to: 'labels',
label: 'Labels',
@ -120,7 +127,12 @@ const NavBar = () => {
tick
</span>
</Typography>
<ThemeToggleButton
sx={{
position: 'absolute',
right: 10,
}}
/>
</Box>
<Drawer
open={drawerOpen}