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