import { CancelScheduleSend, Check, Repeat, TimesOneMobiledata, Webhook, } from '@mui/icons-material' import { Box, Button, Checkbox, Chip, CircularProgress, IconButton, Snackbar, Typography, } from '@mui/joy' import moment from 'moment' import React from 'react' import { useNavigate } from 'react-router-dom' import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' import { useUserProfile } from '../../queries/UserQueries.jsx' import { useNotification } from '../../service/NotificationProvider' import { notInCompletionWindow } from '../../utils/Chores.jsx' import { getTextColorFromBackgroundColor, TASK_COLOR, } from '../../utils/Colors.jsx' import { DeleteChore, MarkChoreComplete, UpdateChoreAssignee, UpdateDueDate, } from '../../utils/Fetcher' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import DateModal from '../Modals/Inputs/DateModal' import SelectModal from '../Modals/Inputs/SelectModal' import TextModal from '../Modals/Inputs/TextModal' import WriteNFCModal from '../Modals/Inputs/WriteNFCModal' import ChoreActionMenu from '../components/ChoreActionMenu' const CompactChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx, viewOnly, onChipClick, // Multi-select props isMultiSelectMode = false, isSelected = false, onSelectionToggle, }) => { const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] = React.useState(false) const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] = React.useState(false) const [isChangeAssigneeModalOpen, setIsChangeAssigneeModalOpen] = React.useState(false) const [isCompleteWithNoteModalOpen, setIsCompleteWithNoteModalOpen] = React.useState(false) const [confirmModelConfig, setConfirmModelConfig] = React.useState({}) const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false) const navigate = useNavigate() const [isPendingCompletion, setIsPendingCompletion] = React.useState(false) const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null) const [timeoutId, setTimeoutId] = React.useState(null) const { data: userProfile } = useUserProfile() const { impersonatedUser } = useImpersonateUser() const { showError } = useNotification() // All the existing handler methods (same as original ChoreCard) 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(chore.id).then(response => { if (response.ok) { onChoreRemove(chore) } }) } setConfirmModelConfig({}) }, }) } const handleTaskCompletion = () => { setIsPendingCompletion(true) let seconds = 3 setSecondsLeftToCancel(seconds) const countdownInterval = setInterval(() => { seconds -= 1 setSecondsLeftToCancel(seconds) if (seconds <= 0) { clearInterval(countdownInterval) setIsPendingCompletion(false) } }, 1000) const id = setTimeout(() => { MarkChoreComplete( chore.id, impersonatedUser ? { completedBy: impersonatedUser.userId } : null, null, null, ) .then(resp => { if (resp.ok) { return resp.json().then(data => { onChoreUpdate(data.res, 'completed') }) } }) .then(() => { setIsPendingCompletion(false) clearTimeout(id) clearInterval(countdownInterval) setTimeoutId(null) setSecondsLeftToCancel(null) }) .catch(error => { if (error?.queued) { showError({ title: 'Update Failed', message: 'Request will be reattempt when you are online', }) } else { showError({ title: 'Failed to update', message: error, }) } setIsPendingCompletion(false) clearTimeout(id) clearInterval(countdownInterval) setTimeoutId(null) setSecondsLeftToCancel(null) }) }, 2000) setTimeoutId(id) } const handleChangeDueDate = newDate => { UpdateDueDate(chore.id, newDate).then(response => { if (response.ok) { response.json().then(data => { const newChore = data.res onChoreUpdate(newChore, 'rescheduled') }) } }) } const handleCompleteWithPastDate = newDate => { MarkChoreComplete( chore.id, impersonatedUser ? { completedBy: impersonatedUser.userId } : null, new Date(newDate).toISOString(), null, ).then(response => { if (response.ok) { response.json().then(data => { const newChore = data.res onChoreUpdate(newChore, 'completed') }) } }) } const handleAssigneChange = assigneeId => { UpdateChoreAssignee(chore.id, assigneeId).then(response => { if (response.ok) { response.json().then(data => { const newChore = data.res onChoreUpdate(newChore, 'assigned') }) } }) } const handleCompleteWithNote = note => { MarkChoreComplete( chore.id, impersonatedUser ? { note, completedBy: impersonatedUser.userId } : { note }, null, null, ).then(response => { if (response.ok) { response.json().then(data => { const newChore = data.res onChoreUpdate(newChore, 'completed') }) } }) } // Utility functions const getDueDateText = nextDueDate => { if (chore.nextDueDate === null) return 'No Due Date' // if due in next 48 hours, we should it in this format : Tomorrow 11:00 AM const diff = moment(nextDueDate).diff(moment(), 'hours') if (diff < 48 && diff > 0) { return moment(nextDueDate).calendar().replace(' at', '') } return moment(nextDueDate).fromNow() } const getDueDateColor = nextDueDate => { if (chore.nextDueDate === null) return 'neutral' const diff = moment(nextDueDate).diff(moment(), 'hours') if (diff < 48 && diff > 0) { return 'warning' } if (diff < 0) { return 'danger' } return 'neutral' } const getRecurrentText = chore => { // if chore.frequencyMetadata is type string then parse it otherwise assigned to the metadata: const metadata = typeof chore.frequencyMetadata === 'string' ? JSON.parse(chore.frequencyMetadata) : chore.frequencyMetadata const dayOfMonthSuffix = n => { if (n >= 11 && n <= 13) { return 'th' } switch (n % 10) { case 1: return 'st' case 2: return 'nd' case 3: return 'rd' default: return 'th' } } if (chore.frequencyType === 'once') { return 'Once' } else if (chore.frequencyType === 'trigger') { return 'Trigger' } else if (chore.frequencyType === 'daily') { return 'Daily' } else if (chore.frequencyType === 'adaptive') { return 'Adaptive' } else if (chore.frequencyType === 'weekly') { return 'Weekly' } else if (chore.frequencyType === 'monthly') { return 'Monthly' } else if (chore.frequencyType === 'yearly') { return 'Yearly' } else if (chore.frequencyType === 'days_of_the_week') { let days = metadata.days if (days.length > 4) { const allDays = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ] const selectedDays = days.map(d => moment().day(d).format('dddd')) const notSelectedDay = allDays.filter( day => !selectedDays.includes(day), ) const notSelectedShortdays = notSelectedDay.map(d => moment().day(d).format('ddd'), ) return `Daily except ${notSelectedShortdays.join(', ')}` } else { days = days.map(d => moment().day(d).format('ddd')) return days.join(', ') } } else if (chore.frequencyType === 'day_of_the_month') { let months = metadata.months if (months.length > 6) { const allMonths = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ] const selectedMonths = months.map(m => moment().month(m).format('MMMM')) const notSelectedMonth = allMonths.filter( month => !selectedMonths.includes(month), ) const notSelectedShortMonths = notSelectedMonth.map(m => moment().month(m).format('MMM'), ) let result = `Monthly ${chore.frequency}${dayOfMonthSuffix( chore.frequency, )}` if (notSelectedShortMonths.length > 0) result += ` except ${notSelectedShortMonths.join(', ')}` return result } else { let freqData = metadata const months = freqData.months.map(m => moment().month(m).format('MMM')) return `${chore.frequency}${dayOfMonthSuffix( chore.frequency, )} of ${months.join(', ')}` } } else if (chore.frequencyType === 'interval') { return `Every ${chore.frequency} ${metadata.unit}` } else { return chore.frequencyType } } const getFrequencyIcon = chore => { if (['once', 'no_repeat'].includes(chore.frequencyType)) { return } else if (chore.frequencyType === 'trigger') { return } else { return } } const formatMetadata = () => { const parts = [] // Frequency parts.push(getRecurrentText(chore)) // Assignee (if not current user) if (userProfile && chore.assignedTo !== userProfile.id) { const assignee = performers.find( p => p.id === chore.assignedTo, )?.displayName if (assignee) parts.push(assignee) } // Points if (chore.points > 0) { parts.push(`${chore.points}pts`) } return parts.join(' • ') } const getPriorityColor = priority => { switch (priority) { case 1: return TASK_COLOR.PRIORITY_1 case 2: return TASK_COLOR.PRIORITY_2 case 3: return TASK_COLOR.PRIORITY_3 case 4: return TASK_COLOR.PRIORITY_4 default: return TASK_COLOR.NO_PRIORITY } } return ( { if (isMultiSelectMode) { onSelectionToggle() } else { navigate(`/chores/${chore.id}`) } }} > {/* Priority bar clickable area */} {chore.priority > 0 && ( { e.stopPropagation() onChipClick({ priority: chore.priority }) }} /> )} {/* Animated transition container for Complete Button / Multi-select checkbox */} {/* Complete Button */} { e.stopPropagation() handleTaskCompletion() }} disabled={isPendingCompletion || notInCompletionWindow(chore)} sx={{ width: 32, height: 32, borderRadius: '50%', transition: 'all 0.2s ease', '&:hover': { transform: 'scale(1.05)', }, '&:active': { transform: 'scale(0.95)', }, '&:disabled': { opacity: 0.5, transform: 'none', }, }} > {isPendingCompletion ? ( ) : ( )} {/* Multi-select Checkbox */} e.stopPropagation()} /> {/* Content - Center */} {/* Line 1: Name + Due Date */} {/* Chore Name */} {chore.name} {/* Due Date - Inline with name */} {getDueDateText(chore.nextDueDate)} {/* Line 2: Metadata */} {getFrequencyIcon(chore)} {formatMetadata()} {/* Labels - Priority chip removed, now shown as vertical bar */} {chore.labelsV2?.map(l => (
{ e.stopPropagation() onChipClick({ label: l }) }} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation() onChipClick({ label: l }) } }} style={{ cursor: 'pointer', padding: 0, margin: 0, display: 'flex', alignItems: 'center', }} key={`compact-chorecard-${chore.id}-label-${l.id}`} > {l?.name}
))}
{/* Right side - Action Menu with animation */} setIsCompleteWithNoteModalOpen(true)} onCompleteWithPastDate={() => setIsCompleteWithPastDateModalOpen(true) } onChangeAssignee={() => setIsChangeAssigneeModalOpen(true)} onChangeDueDate={() => setIsChangeDueDateModalOpen(true)} onWriteNFC={() => setIsNFCModalOpen(true)} onDelete={handleDelete} sx={{ width: 32, height: 32, color: 'text.tertiary', flexShrink: 0, '&:hover': { color: 'text.secondary', bgcolor: 'background.level1', }, }} />
{/* All modals (same as original) */} setIsChangeDueDateModalOpen(false)} onSave={handleChangeDueDate} /> setIsCompleteWithPastDateModalOpen(false)} onSave={handleCompleteWithPastDate} /> setIsChangeAssigneeModalOpen(false)} onSave={selected => handleAssigneChange(selected.id)} /> {confirmModelConfig?.isOpen && ( )} setIsCompleteWithNoteModalOpen(false)} okText={'Complete'} onSave={handleCompleteWithNote} /> setIsNFCModalOpen(false), }} /> {/* Snackbar for pending completion */} { if (timeoutId) { clearTimeout(timeoutId) setIsPendingCompletion(false) setTimeoutId(null) setSecondsLeftToCancel(null) } }} size='sm' variant='outlined' color='primary' startDecorator={} > Cancel } > Task will be marked as completed in {secondsLeftToCancel} seconds
) } export default CompactChoreCard