import {
Box,
Button,
Card,
Checkbox,
Chip,
Container,
Divider,
FormControl,
FormHelperText,
Input,
List,
ListItem,
Option,
Radio,
RadioGroup,
Select,
Sheet,
Snackbar,
Stack,
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 {
CreateChore,
DeleteChore,
GetAllCircleMembers,
GetChoreByID,
GetChoreHistory,
GetThings,
SaveChore,
} from '../../utils/Fetcher'
import { isPlusAccount } from '../../utils/Helpers'
import FreeSoloCreateOption from '../components/AutocompleteSelect'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import RepeatSection from './RepeatSection'
const ASSIGN_STRATEGIES = [
'random',
'least_assigned',
'least_completed',
'keep_last_assigned',
]
const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month']
const NO_DUE_DATE_REQUIRED_TYPE = ['no_repeat', 'once']
const NO_DUE_DATE_ALLOWED_TYPE = ['trigger']
const ChoreEdit = () => {
const { userProfile, setUserProfile } = useContext(UserContext)
const [chore, setChore] = useState([])
const [choresHistory, setChoresHistory] = useState([])
const [userHistory, setUserHistory] = useState({})
const { choreId } = useParams()
const [name, setName] = useState('')
const [confirmModelConfig, setConfirmModelConfig] = useState({})
const [assignees, setAssignees] = useState([])
const [performers, setPerformers] = useState([])
const [assignStrategy, setAssignStrategy] = useState(ASSIGN_STRATEGIES[2])
const [dueDate, setDueDate] = useState(null)
const [completed, setCompleted] = useState(false)
const [completedDate, setCompletedDate] = useState('')
const [assignedTo, setAssignedTo] = useState(-1)
const [frequencyType, setFrequencyType] = useState('once')
const [frequency, setFrequency] = useState(1)
const [frequencyMetadata, setFrequencyMetadata] = useState({})
const [labels, setLabels] = useState([])
const [allUserThings, setAllUserThings] = useState([])
const [thingTrigger, setThingTrigger] = useState(null)
const [isThingValid, setIsThingValid] = useState(false)
const [notificationMetadata, setNotificationMetadata] = useState({})
const [isRolling, setIsRolling] = useState(false)
const [isNotificable, setIsNotificable] = useState(false)
const [isActive, setIsActive] = useState(true)
const [updatedBy, setUpdatedBy] = useState(0)
const [createdBy, setCreatedBy] = useState(0)
const [errors, setErrors] = useState({})
const [attemptToSave, setAttemptToSave] = useState(false)
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
const [snackbarMessage, setSnackbarMessage] = useState('')
const [snackbarColor, setSnackbarColor] = useState('warning')
const Navigate = useNavigate()
const HandleValidateChore = () => {
const errors = {}
if (name.trim() === '') {
errors.name = 'Name is required'
}
if (assignees.length === 0) {
errors.assignees = 'At least 1 assignees is required'
}
if (assignedTo < 0) {
errors.assignedTo = 'Assigned to is required'
}
if (frequencyType === 'interval' && frequency < 1) {
errors.frequency = 'Frequency is required'
}
if (
frequencyType === 'days_of_the_week' &&
frequencyMetadata['days']?.length === 0
) {
errors.frequency = 'At least 1 day is required'
}
if (
frequencyType === 'day_of_the_month' &&
frequencyMetadata['months']?.length === 0
) {
errors.frequency = 'At least 1 month is required'
}
if (
dueDate === null &&
!NO_DUE_DATE_REQUIRED_TYPE.includes(frequencyType) &&
!NO_DUE_DATE_ALLOWED_TYPE.includes(frequencyType)
) {
if (REPEAT_ON_TYPE.includes(frequencyType)) {
errors.dueDate = 'Start date is required'
} else {
errors.dueDate = 'Due date is required'
}
}
if (frequencyType === 'trigger') {
if (!isThingValid) {
errors.thingTrigger = 'Thing trigger is invalid'
}
}
// if there is any error then return false:
setErrors(errors)
if (Object.keys(errors).length > 0) {
// generate a list with error and set it in snackbar:
const errorList = Object.keys(errors).map(key => (
{errors[key]}
))
setSnackbarMessage(
Please resolve the following errors:
{errorList}
,
)
setSnackbarColor('danger')
setIsSnackbarOpen(true)
return false
}
return true
}
const HandleSaveChore = () => {
setAttemptToSave(true)
if (!HandleValidateChore()) {
console.log('validation failed')
console.log(errors)
return
}
const chore = {
id: Number(choreId),
name: name,
assignees: assignees,
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
frequencyType: frequencyType,
frequency: Number(frequency),
frequencyMetadata: frequencyMetadata,
assignedTo: assignedTo,
assignStrategy: assignStrategy,
isRolling: isRolling,
isActive: isActive,
notification: isNotificable,
labels: labels,
notificationMetadata: notificationMetadata,
thingTrigger: thingTrigger,
}
let SaveFunction = CreateChore
if (choreId > 0) {
SaveFunction = SaveChore
}
SaveFunction(chore).then(response => {
if (response.status === 200) {
Navigate(`/my/chores`)
} else {
alert('Failed to save chore')
}
})
}
useEffect(() => {
//fetch performers:
GetAllCircleMembers()
.then(response => response.json())
.then(data => {
setPerformers(data.res)
})
GetThings().then(response => {
response.json().then(data => {
setAllUserThings(data.res)
})
})
// fetch chores:
if (choreId > 0) {
GetChoreByID(choreId)
.then(response => {
if (response.status !== 200) {
alert('You are not authorized to view this chore.')
Navigate('/my/chores')
return null
} else {
return response.json()
}
})
.then(data => {
setChore(data.res)
setName(data.res.name ? data.res.name : '')
setAssignees(data.res.assignees ? data.res.assignees : [])
setAssignedTo(data.res.assignedTo)
setFrequencyType(
data.res.frequencyType ? data.res.frequencyType : 'once',
)
setFrequencyMetadata(JSON.parse(data.res.frequencyMetadata))
setFrequency(data.res.frequency)
setNotificationMetadata(JSON.parse(data.res.notificationMetadata))
setLabels(data.res.labels ? data.res.labels.split(',') : [])
setAssignStrategy(
data.res.assignStrategy
? data.res.assignStrategy
: ASSIGN_STRATEGIES[2],
)
setIsRolling(data.res.isRolling)
setIsActive(data.res.isActive)
// parse the due date to a string from this format "2021-10-10T00:00:00.000Z"
// use moment.js or date-fns to format the date for to be usable in the input field:
setDueDate(
data.res.nextDueDate
? moment(data.res.nextDueDate).format('YYYY-MM-DDTHH:mm:ss')
: null,
)
setUpdatedBy(data.res.updatedBy)
setCreatedBy(data.res.createdBy)
setIsNotificable(data.res.notification)
setThingTrigger(data.res.thingChore)
// setDueDate(data.res.dueDate)
// setCompleted(data.res.completed)
// setCompletedDate(data.res.completedDate)
})
// fetch chores history:
GetChoreHistory(choreId)
.then(response => response.json())
.then(data => {
setChoresHistory(data.res)
const newUserChoreHistory = {}
data.res.forEach(choreHistory => {
if (newUserChoreHistory[choreHistory.completedBy]) {
newUserChoreHistory[choreHistory.completedBy] += 1
} else {
newUserChoreHistory[choreHistory.completedBy] = 1
}
})
setUserHistory(newUserChoreHistory)
})
}
// set focus on the first input field:
else {
// new task/ chore set focus on the first input field:
document.querySelector('input').focus()
}
}, [])
useEffect(() => {
// if frequancy type change to somthing need a due date then set it to the current date:
if (!NO_DUE_DATE_REQUIRED_TYPE.includes(frequencyType) && !dueDate) {
setDueDate(moment(new Date()).format('YYYY-MM-DDTHH:mm:00'))
}
if (NO_DUE_DATE_ALLOWED_TYPE.includes(frequencyType)) {
setDueDate(null)
}
}, [frequencyType])
useEffect(() => {
if (assignees.length === 1) {
setAssignedTo(assignees[0].userId)
}
}, [assignees])
useEffect(() => {
if (performers.length > 0 && assignees.length === 0) {
setAssignees([
{
userId: userProfile.id,
},
])
}
}, [performers])
// if user resolve the error trigger validation to remove the error message from the respective field
useEffect(() => {
if (attemptToSave) {
HandleValidateChore()
}
}, [assignees, name, frequencyMetadata, attemptToSave, dueDate])
const handleDelete = () => {
setConfirmModelConfig({
isOpen: true,
title: 'Delete Chore',
confirmText: 'Delete',
cancelText: 'Cancel',
message: 'Are you sure you want to delete this chore?',
onClose: isConfirmed => {
if (isConfirmed === true) {
DeleteChore(choreId).then(response => {
if (response.status === 200) {
Navigate('/my/chores')
} else {
alert('Failed to delete chore')
}
})
}
setConfirmModelConfig({})
},
})
}
return (
{/*
Edit Chore
*/}
Descritpion :
What is this chore about?
setName(e.target.value)} />
{errors.name}
Assignees :
Who can do this chore?
{performers?.map((item, index) => (
a.userId == item.id) != null}
onClick={() => {
if (assignees.find(a => a.userId == item.id)) {
setAssignees(assignees.filter(i => i.userId !== item.id))
} else {
setAssignees([...assignees, { userId: item.id }])
}
}}
overlay
disableIcon
variant='soft'
label={item.displayName}
/>
))}
{Boolean(errors.assignee)}
{assignees.length > 1 && (
// this wrap the details that needed if we have more than one assingee
// we need to pick the next assignedTo and also the strategy to pick the next assignee.
// if we have only one then no need to display this section
<>
Assigned :
Who is assigned the next due chore?
Picking Mode :
How to pick the next assignee for the following chore?
{ASSIGN_STRATEGIES.map((item, idx) => (
setAssignStrategy(item)}
overlay
disableIcon
variant='soft'
label={item
.split('_')
.map(x => x.charAt(0).toUpperCase() + x.slice(1))
.join(' ')}
/>
))}
>
)}
{
setFrequencyMetadata({
...frequencyMetadata,
time: t,
})
}}
frequencyError={errors?.frequency}
allUserThings={allUserThings}
onTriggerUpdate={thingUpdate => {
if (thingUpdate === null) {
setThingTrigger(null)
return
}
setThingTrigger({
triggerState: thingUpdate.triggerState,
condition: thingUpdate.condition,
thingID: thingUpdate.thing.id,
})
}}
OnTriggerValidate={setIsThingValid}
isAttemptToSave={attemptToSave}
selectedThing={thingTrigger}
/>
{REPEAT_ON_TYPE.includes(frequencyType) ? 'Start date' : 'Due date'} :
{frequencyType === 'trigger' && !dueDate && (
Due Date will be set when the trigger of the thing is met
)}
{NO_DUE_DATE_REQUIRED_TYPE.includes(frequencyType) && (
{
if (e.target.checked) {
setDueDate(moment(new Date()).format('YYYY-MM-DDTHH:mm:00'))
} else {
setDueDate(null)
}
}}
defaultChecked={dueDate !== null}
checked={dueDate !== null}
value={dueDate !== null}
overlay
label='Give this task a due date'
/>
task needs to be completed by a specific time.
)}
{dueDate && (
{REPEAT_ON_TYPE.includes(frequencyType)
? 'When does this chore start?'
: 'When is the next first time this chore is due?'}
{
setDueDate(e.target.value)
}}
/>
{errors.dueDate}
)}
{!['once', 'no_repeat'].includes(frequencyType) && (
Scheduling Preferences:
How to reschedule the next due date?
div': { p: 1 } }}>
setIsRolling(false)}
label='Reschedule from due date'
/>
the next task will be scheduled from the original due date, even
if the previous task was completed late
setIsRolling(true)}
label='Reschedule from completion date'
/>
the next task will be scheduled from the actual completion date
of the previous task
)}
Notifications :
Get Reminders when this task is due or completed
{!isPlusAccount(userProfile) && (
Not available in Basic Plan
)}
{
setIsNotificable(e.target.checked)
}}
defaultChecked={isNotificable}
checked={isNotificable}
value={isNotificable}
disabled={!isPlusAccount(userProfile)}
overlay
label='Notify for this task'
/>
When should receive notifications for this task
{isNotificable && (
div': { p: 2, borderRadius: 'md', display: 'flex' },
}}
>
What things should trigger the notification?
{[
{
title: 'Due Date/Time',
description: 'A simple reminder that a task is due',
id: 'dueDate',
},
// {
// title: 'Upon Completion',
// description: 'A notification when a task is completed',
// id: 'completion',
// },
{
title: 'Predued',
description: 'before a task is due in few hours',
id: 'predue',
},
{
title: 'Overdue',
description: 'A notification when a task is overdue',
},
{
title: 'Nagging',
description: 'Daily reminders until the task is completed',
id: 'nagging',
},
].map(item => (
{
setNotificationMetadata({
...notificationMetadata,
[item.id]: !notificationMetadata[item.id],
})
}}
checked={
notificationMetadata ? notificationMetadata[item.id] : false
}
label={item.title}
key={item.title}
/>
{item.description}
))}
What things should trigger the notification?
Notify all assignees
{
if (notificationMetadata['circleGroup']) {
delete notificationMetadata['circleGroupID']
}
setNotificationMetadata({
...notificationMetadata,
['circleGroup']: !notificationMetadata['circleGroup'],
})
}}
checked={
notificationMetadata
? notificationMetadata['circleGroup']
: false
}
label='Specific Group'
/>
Notify a specific group
{notificationMetadata['circleGroup'] && (
Telegram Group ID:
{
setNotificationMetadata({
...notificationMetadata,
['circleGroupID']: parseInt(e.target.value),
})
}}
/>
)}
)}
Labels :
Things to remember about this chore or to tag it
{
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)
}}
/>
{choreId > 0 && (
Created by{' '}
{performers.find(f => f.id === createdBy)?.displayName}
{' '}
{moment(chore.createdAt).fromNow()}
{(chore.updatedAt && updatedBy > 0 && (
<>
Updated by{' '}
{performers.find(f => f.id === updatedBy)?.displayName}
{' '}
{moment(chore.updatedAt).fromNow()}
>
)) || <>>}
)}
{/*
*/}
{choreId > 0 && (
)}
{/* */}
{
setIsSnackbarOpen(false)
setSnackbarMessage(null)
}}
color={snackbarColor}
autoHideDuration={4000}
sx={{ bottom: 70 }}
invertedColors={true}
variant='soft'
>
{snackbarMessage}
)
}
export default ChoreEdit