import { Add, HorizontalRule, Save } from '@mui/icons-material' import { Box, Button, Card, Checkbox, Chip, Container, Divider, FormControl, FormHelperText, Input, List, ListItem, MenuItem, Option, Radio, RadioGroup, Select, Sheet, Switch, Typography, } from '@mui/joy' import moment from 'moment' import { useEffect, useState } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import NotificationTemplate from '../../components/NotificationTemplate.jsx' import { useArchiveChore, useChore, useCreateChore, useUnArchiveChore, useUpdateChore, } from '../../queries/ChoreQueries.jsx' import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx' import { useNotification } from '../../service/NotificationProvider' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { DeleteChore, GetAllCircleMembers, GetThings, } from '../../utils/Fetcher' import { isPlusAccount } from '../../utils/Helpers' import Priorities from '../../utils/Priorities.jsx' import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js' import LoadingComponent from '../components/Loading.jsx' import RichTextEditor from '../components/RichTextEditor.jsx' import SubTasks from '../components/SubTask.jsx' import { useLabels } from '../Labels/LabelQueries' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import LabelModal from '../Modals/Inputs/LabelModal' import RepeatSection from './RepeatSection' const ASSIGN_STRATEGIES = [ 'random', 'least_assigned', 'least_completed', 'keep_last_assigned', 'random_except_last_assigned', 'round_robin', 'no_assignee', ] 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 { data: userProfile, isLoading: isUserProfileLoading } = useUserProfile() const [chore, setChore] = useState([]) const [choresHistory, setChoresHistory] = useState([]) const [userHistory, setUserHistory] = useState({}) const { choreId } = useParams() const [searchParams, setSearchParams] = useSearchParams() const [name, setName] = useState('') const [description, setDescription] = 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 [assignedTo, setAssignedTo] = useState(-1) const [frequencyType, setFrequencyType] = useState('once') const [frequency, setFrequency] = useState(1) const [frequencyMetadata, setFrequencyMetadata] = useState({}) const [labels, setLabels] = useState([]) const [labelsV2, setLabelsV2] = useState([]) const [priority, setPriority] = useState(0) const [points, setPoints] = useState(-1) const [requireApproval, setRequireApproval] = useState(false) const [isPrivate, setIsPrivate] = useState(false) const [subTasks, setSubTasks] = useState(null) const [completionWindow, setCompletionWindow] = useState(-1) 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 [addLabelModalOpen, setAddLabelModalOpen] = useState(false) const [showSavePrivacyDefault, setShowSavePrivacyDefault] = useState(false) const [privacySaved, setPrivacySaved] = useState(false) const [showSaveNotificationDefault, setShowSaveNotificationDefault] = useState(false) const [showSaveAssigneeDefault, setShowSaveAssigneeDefault] = useState(false) const { data: userLabelsRaw, isLoading: isUserLabelsLoading } = useLabels() const updateChoreMutation = useUpdateChore() const createChoreMutation = useCreateChore() const archiveChore = useArchiveChore() const unarchiveChore = useUnArchiveChore() const { data: choreData, isLoading: isChoreLoading, refetch: refetchChore, } = useChore(choreId) const { data: membersData, isLoading: isMemberDataLoading } = useCircleMembers() const { showSuccess, showError } = useNotification() const [userLabels, setUserLabels] = useState([]) useEffect(() => { if (userLabelsRaw) { setUserLabels(userLabelsRaw) } }, [userLabelsRaw]) const Navigate = useNavigate() const HandleValidateChore = () => { const errors = {} if (name.trim() === '') { errors.name = 'Name is required' } if (assignStrategy !== 'no_assignee') { if (assignees.length === 0) { errors.assignees = 'At least 1 assignees is required' } if (assignedTo === null || assignedTo < 0) { errors.assignedTo = 'Assigned to is required' } } if (frequencyType === 'interval' && !frequency > 0) { errors.frequency = `Invalid frequency, the ${frequencyMetadata.unit} should be > 0` } if ( frequencyType === 'days_of_the_week' && frequencyMetadata['days']?.length === 0 ) { errors.frequency = 'Please select at least one day of the week' } // Validate advanced scheduling patterns if ( frequencyType === 'days_of_the_week' && frequencyMetadata?.weekPattern === 'nth_day_of_month' && (!frequencyMetadata?.occurrences || frequencyMetadata.occurrences.length === 0) ) { errors.frequency = 'Please select at least one day occurrence for the month' } if ( frequencyType === 'day_of_the_month' && frequencyMetadata['months']?.length === 0 ) { errors.frequency = 'Please select at least one month' } if ( dueDate === null && !NO_DUE_DATE_REQUIRED_TYPE.includes(frequencyType) && !NO_DUE_DATE_ALLOWED_TYPE.includes(frequencyType) ) { if (REPEAT_ON_TYPE.includes(frequencyType)) { console.log('VALIDATION:', dueDate, 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]} )) showError({ title: 'Please resolve the following errors:', message: {errorList}, }) return false } return true } const handleDueDateChange = e => { setDueDate(e.target.value) } const HandleSaveChore = () => { setAttemptToSave(true) if (!HandleValidateChore()) { console.log('validation failed') console.log(errors) return } let newChoreId = choreId if (searchParams.get('clone') === 'true') { newChoreId = null } const chore = { id: Number(newChoreId), name: name, description: description, assignees: assignees, dueDate: dueDate ? new Date(dueDate).toISOString() : null, frequencyType: frequencyType, frequency: Number(frequency), frequencyMetadata: frequencyMetadata, assignedTo: assignStrategy === 'no_assignee' ? null : assignedTo, assignStrategy: assignStrategy, isRolling: isRolling, isActive: isActive, notification: isNotificable, labels: labels.map(l => l.name), labelsV2: labelsV2, subTasks: subTasks, notificationMetadata: notificationMetadata, thingTrigger: thingTrigger, points: points < 0 ? null : points, requireApproval: requireApproval, isPrivate: isPrivate, completionWindow: // if completionWindow is -1 then set it to null or dueDate is null completionWindow < 0 || dueDate === null ? null : completionWindow, priority: priority, } let SaveFunction = createChoreMutation.mutateAsync if (newChoreId > 0) { SaveFunction = updateChoreMutation.mutateAsync } SaveFunction(chore) .then(() => { showSuccess({ title: 'Chore Saved', message: 'Your task has been saved successfully!', }) Navigate('/chores') }) .catch(error => { console.error('Failed to save chore:', error) showError({ title: 'Save Failed', message: 'Failed to save chore, please try again.', }) }) } useEffect(() => { //fetch performers: GetAllCircleMembers().then(data => { setPerformers(data.res) }) GetThings().then(response => { response.json().then(data => { setAllUserThings(data.res) }) }) // Load default privacy setting for new chores if (!choreId) { const defaultPrivacySetting = localStorage.getItem( 'defaultPrivacySetting', ) if (defaultPrivacySetting !== null) { setIsPrivate(JSON.parse(defaultPrivacySetting)) } const defaultNotificationSetting = localStorage.getItem( 'defaultNotificationSetting', ) if (defaultNotificationSetting !== null) { setIsNotificable(JSON.parse(defaultNotificationSetting)) } const defaultAssigneeSetting = localStorage.getItem( 'defaultAssigneeSetting', ) if (defaultAssigneeSetting !== null) { const savedAssignees = JSON.parse(defaultAssigneeSetting) setAssignees(savedAssignees) } } }, []) useEffect(() => { if (isChoreLoading === false && choreData && choreId) { const data = choreData const isCloneMode = searchParams.get('clone') === 'true' setChore(data.res) setName(data.res.name ? data.res.name : '') setDescription(data.res.description ? data.res.description : '') setAssignees(data.res.assignees ? data.res.assignees : []) setAssignedTo(data.res.assignedTo) setFrequencyType(data.res.frequencyType ? data.res.frequencyType : 'once') setFrequencyMetadata(data.res.frequencyMetadata) setFrequency(data.res.frequency) setNotificationMetadata(data.res.notificationMetadata) setPoints(data.res.points && data.res.points > -1 ? data.res.points : -1) setRequireApproval(data.res.requireApproval || false) setIsPrivate(data.res.isPrivate || false) setCompletionWindow( data.res.completionWindow && data.res.completionWindow > -1 ? data.res.completionWindow : -1, ) setLabelsV2(data.res.labelsV2) setPriority(data.res.priority) setAssignStrategy( data.res.assignStrategy ? data.res.assignStrategy : ASSIGN_STRATEGIES[2], ) setIsRolling(data.res.isRolling) setIsActive(data.res.isActive) setSubTasks(data.res.subTasks ? data.res.subTasks : []) if (isCloneMode) { if (data.res.subTasks) { const clonedSubTasks = data.res.subTasks.map(subTask => ({ ...subTask, id: -subTask.id, // Negate ID to indicate new sub task parentId: subTask.parentId ? -subTask.parentId : null, // Negate parent ID if exists completed: false, // Reset completion status completedAt: null, // Reset completion date })) setSubTasks(clonedSubTasks) } if (data.res.name) { setName(`Copy of ${data.res.name}`) } } setIsNotificable(data.res.notification) setThingTrigger(data.res.thingChore) setDueDate( data.res.nextDueDate ? moment(data.res.nextDueDate).format('YYYY-MM-DDTHH:mm:00') : null, ) setCreatedBy(data.res.createdBy) setUpdatedBy(data.res.updatedBy) } }, [choreData, isChoreLoading, searchParams]) // useEffect(() => { // if (userLabels && userLabels.length == 0 && labelsV2.length == 0) { // return // } // const labelIds = labelsV2.map(l => l.id) // setLabelsV2(userLabels.filter(l => labelIds.indexOf(l.id) > -1)) // }, [userLabels, labelsV2]) useEffect(() => { // if frequency 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 === 0) { setAssignStrategy('no_assignee') setAssignedTo(null) } else if (assignees.length === 1) { setAssignedTo(assignees[0].userId) if (assignStrategy === 'no_assignee') { setAssignStrategy(ASSIGN_STRATEGIES[2]) // default to least_completed } } }, [assignees, assignStrategy]) // useEffect(() => { // if (performers.length > 0 && assignees.length === 0 && userProfile) { // setAssignees([ // { // userId: userProfile?.id, // }, // ]) // } // }, [performers, userProfile]) // 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('/chores') } else { alert('Failed to delete chore') } }) } setConfirmModelConfig({}) }, }) } if ( (isChoreLoading && choreId) || isUserLabelsLoading || isUserProfileLoading || isMemberDataLoading ) { return } return ( {/* Section 1: Basic Information */} {/* Basic Information */} Name What is the name of this task? setName(e.target.value)} /> {errors.name} Description What is this task about? {errors.description} Priority How important is this task? {/* Priority Chip Selection */} {/* Priority Chips P1-P4 */} {Priorities.map(priorityItem => ( setPriority(priorityItem.value)} startDecorator={priorityItem.icon} sx={{ fontWeight: 'md', cursor: 'pointer', minHeight: 34, }} > {priorityItem.name} ))} {/* No Priority Chip */} setPriority(0)} startDecorator={} sx={{ fontWeight: 'md', cursor: 'pointer', minHeight: 34, }} > No Priority Labels Things to remember about this task or to tag it Sub Tasks {/* { if (e.target.checked) { setSubTasks([]) } else { setSubTasks(null) } }} overlay checked={subTasks != null} label='Add sub tasks to this task' /> Break this task into smaller steps */} {/* Section 2: Assignment & Responsibility */} Assignees Who can do this task? {/* add one for Anyone if no specific assignee is selected */} { setAssignees([]) setIsPrivate(false) }} overlay disableIcon variant='soft' label='Anyone' /> {performers?.map((item, index) => ( a.userId == item.userId) != null } onClick={() => { if (assignees.some(a => a.userId === item.userId)) { const newAssignees = assignees.filter( a => a.userId !== item.userId, ) setAssignees(newAssignees) } else { setAssignees([...assignees, { userId: item.userId }]) } setShowSaveAssigneeDefault(true) }} overlay disableIcon variant='soft' label={item.displayName} /> ))} {Boolean(errors.assignee)} {showSaveAssigneeDefault && ( )} {assignees.length > 1 && ( <> Currently Assigned To Who is assigned the next due? Assignment Strategy How to pick the next assignee for the following task? {ASSIGN_STRATEGIES.map((item, idx) => ( setAssignStrategy(item)} overlay disableIcon variant='soft' label={item .split('_') .map(x => x.charAt(0).toUpperCase() + x.slice(1)) .join(' ')} /> ))} )} {/* Section 3: Schedule & Timing */} { 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} 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 task start?' : 'When is the next first time this task is due?'} {errors.dueDate} )} {dueDate && ( Completion Window { event.preventDefault() if (completionWindow != -1) { setCompletionWindow(-1) } else { setCompletionWindow(1) } }} color={completionWindow !== -1 ? 'success' : 'neutral'} variant={completionWindow !== -1 ? 'solid' : 'outlined'} sx={{ mr: 2, }} />
Completion window (hours) {"Set a time window that task can't be completed before"}
{completionWindow != -1 && ( Hours: { setCompletionWindow(parseInt(e.target.value)) }} /> )}
)} {!['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 )} {/* Section 3.1: Notifications */} Notifications {!isPlusAccount(userProfile) && ( Task notifications are not available in the Basic plan. Upgrade to Plus to receive reminders when tasks are due or completed. )} { setIsNotificable(e.target.checked) if (!e.target.checked) { setNotificationMetadata({}) } }} defaultChecked={isNotificable} checked={isNotificable} disabled={!isPlusAccount(userProfile)} overlay label='Notify for this task' /> When should receive notifications for this task {isNotificable && ( Notification Schedule { const newTemplates = metadata.notifications if (notificationMetadata?.templates !== newTemplates) { setNotificationMetadata({ ...notificationMetadata, templates: newTemplates, }) } }} value={notificationMetadata} /> Who to Notify 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), }) }} /> )} )}
{/* Section 4: Task Settings */} Task Settings: Points System { if (e.target.checked) { setPoints(1) } else { setPoints(-1) } }} checked={points > -1} overlay label='Assign points for completion' /> Assign points to this task and user will earn points when they completed it {points != -1 && ( Points: { setPoints(parseInt(e.target.value)) }} /> )} Approval Requirement { setRequireApproval(e.target.checked) }} checked={requireApproval} overlay label='Require admin approval' /> This task will need approval from an admin before being marked as complete Privacy Settings Who can see this task? { const newValue = event.target.value === 'true' ? true : false setIsPrivate(newValue) setShowSavePrivacyDefault(true) }} sx={{ '& > div': { py: 1 }, }} > Everyone in your circle You and others that are assigned to the task {assignees.length === 0 ? ' (No assignees selected, Limited option is disabled)' : ''} {showSavePrivacyDefault && ( )} {choreId > 0 && ( Created by{' '} {membersData.res.find(f => f.userId === createdBy)?.displayName} {' '} {moment(chore.createdAt).fromNow()} {(chore.updatedAt && updatedBy > 0 && ( <> Updated by{' '} { membersData.res.find(f => f.userId === updatedBy) ?.displayName } {' '} {moment(chore.updatedAt).fromNow()} )) || <>} )} {/* */} {choreId > 0 && ( <> {isActive ? ( ) : ( )} )} {addLabelModalOpen && ( { console.log('label', label) const newLabels = [...labelsV2] newLabels.push(label) setUserLabels([...userLabels, label]) setLabelsV2([...labelsV2, label]) setAddLabelModalOpen(false) }} onClose={() => setAddLabelModalOpen(false)} /> )} {/* */}
) } export default ChoreEdit