From cd2fa7943bba5b0ae672bad678a86f1d6043b38e Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Mon, 23 Mar 2026 22:30:56 -0400 Subject: [PATCH 01/12] Refactor chore selection logic to pass 'chores' parameter to getSelectedChoresData in bulk actions --- src/views/Chores/MyChores.jsx | 5 +++-- src/views/Chores/hooks/useChoreActions.js | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index cd9e859..d57c099 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -529,10 +529,11 @@ const MyChores = () => { onEnableMultiSelectAndSelectAll: () => { toggleMultiSelectMode() setTimeout(() => { - selectAllVisibleChores() + selectAllVisibleChores(null, choreSections, openChoreSections) }, 0) }, - onSelectAll: () => selectAllVisibleChores(), + onSelectAll: () => + selectAllVisibleChores(null, choreSections, openChoreSections), onClearSelection: clearSelection, onBulkComplete: handleBulkComplete, onBulkSkip: handleBulkSkip, diff --git a/src/views/Chores/hooks/useChoreActions.js b/src/views/Chores/hooks/useChoreActions.js index abf781f..9cf599d 100644 --- a/src/views/Chores/hooks/useChoreActions.js +++ b/src/views/Chores/hooks/useChoreActions.js @@ -472,7 +472,7 @@ export const useChoreActions = ({ ) const handleBulkComplete = useCallback(async () => { - const selectedData = getSelectedChoresData() + const selectedData = getSelectedChoresData(chores) if (selectedData.length === 0) return setConfirmModelConfig({ @@ -532,7 +532,7 @@ export const useChoreActions = ({ }, [getSelectedChoresData, impersonatedUser, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig]) const handleBulkArchive = useCallback(async () => { - const selectedData = getSelectedChoresData() + const selectedData = getSelectedChoresData(chores) if (selectedData.length === 0) return setConfirmModelConfig({ @@ -594,7 +594,7 @@ export const useChoreActions = ({ }, [getSelectedChoresData, archiveChore, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig]) const handleBulkDelete = useCallback(async () => { - const selectedData = getSelectedChoresData() + const selectedData = getSelectedChoresData(chores) if (selectedData.length === 0) return setConfirmModelConfig({ @@ -654,7 +654,7 @@ export const useChoreActions = ({ }, [getSelectedChoresData, chores, filteredChores, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig]) const handleBulkSkip = useCallback(async () => { - const selectedData = getSelectedChoresData() + const selectedData = getSelectedChoresData(chores) if (selectedData.length === 0) return setConfirmModelConfig({ From c4b667449257845eaef277765aa6987b67115133 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Mon, 23 Mar 2026 22:31:17 -0400 Subject: [PATCH 02/12] Add '/password/update' to public pages in NavBar component to allow password reset --- src/views/components/NavBar.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx index 222a09a..91a24c7 100644 --- a/src/views/components/NavBar.jsx +++ b/src/views/components/NavBar.jsx @@ -171,6 +171,7 @@ const NavBar = () => { '/login', '/auth/oauth2', '/forgot-password', + '/password/update', '/login/settings', '/welcome', ].includes(location.pathname) From 0b19cdc95c3b18b7fe82aff3f0ad370ec6b8c403 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Mon, 23 Mar 2026 22:31:59 -0400 Subject: [PATCH 03/12] Add 'Missed' and 'Rescheduled' statuses to ChoreHistoryStatus and update related components --- src/utils/Chores.jsx | 2 ++ src/views/History/ChoreHistory.jsx | 2 +- src/views/History/HistoryCard.jsx | 8 ++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/utils/Chores.jsx b/src/utils/Chores.jsx index f30df14..81a8b7d 100644 --- a/src/utils/Chores.jsx +++ b/src/utils/Chores.jsx @@ -16,6 +16,8 @@ export const ChoreHistoryStatus = Object.freeze({ SKIPPED: 2, PENDING_APPROVAL: 3, REJECTED: 4, + MISSED: 5, + RESCHEDULED: 6, }) export const ChoreStatus = Object.freeze({ INACTIVE: 0, diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx index 506faf0..ab8e307 100644 --- a/src/views/History/ChoreHistory.jsx +++ b/src/views/History/ChoreHistory.jsx @@ -121,7 +121,7 @@ const ChoreHistory = () => { { icon: , text: 'All Completed', - subtext: `${histories.filter(h => h.status === ChoreHistoryStatus.COMPLETED).length} times`, + subtext: `${histories.filter(h => h.status === ChoreHistoryStatus.COMPLETED || h.status === ChoreHistoryStatus.SKIPPED).length} times`, }, { icon: , diff --git a/src/views/History/HistoryCard.jsx b/src/views/History/HistoryCard.jsx index 811179a..e2c8c52 100644 --- a/src/views/History/HistoryCard.jsx +++ b/src/views/History/HistoryCard.jsx @@ -8,6 +8,7 @@ import { Person, Redo, RunningWithErrors, + Schedule, ThumbDown, Timelapse, Toll, @@ -17,7 +18,7 @@ import moment from 'moment' import { TASK_COLOR } from '../../utils/Colors.jsx' const getCompletedChip = historyEntry => { - if (historyEntry.status === 0 || historyEntry.status === 5) { + if (historyEntry.status === 0 || historyEntry.status === 5 || historyEntry.status === 6) { return null } @@ -126,6 +127,7 @@ const HistoryCard = ({ 3: { icon: , color: 'neutral' }, // Pending Approval 4: { icon: , color: 'danger' }, // Rejected 5: { icon: , color: 'danger' }, // Missed + 6: { icon: , color: 'warning' }, // Rescheduled } const config = statusMap[historyEntry.status] || statusMap[1] @@ -192,7 +194,9 @@ const HistoryCard = ({ ? 'Rejected' : historyEntry.status === 5 ? 'Missed' - : 'Completed'} + : historyEntry.status === 6 + ? 'Rescheduled' + : 'Completed'} {historyEntry.performedAt && ( }> From 92e816050596b13ab7c645146c63aa64c9e310c5 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 24 Mar 2026 16:51:57 -0400 Subject: [PATCH 04/12] Fix https://github.com/donetick/donetick/issues/611 on-time check in ActivityItem to handle missing dueDate --- src/views/Chores/ActivitesCard.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/Chores/ActivitesCard.jsx b/src/views/Chores/ActivitesCard.jsx index 6f0f0e9..54c31cb 100644 --- a/src/views/Chores/ActivitesCard.jsx +++ b/src/views/Chores/ActivitesCard.jsx @@ -76,9 +76,9 @@ const ActivityItem = ({ activity, members, onViewNote }) => { icon: , } } else if (activity.status === 1) { - const wasOnTime = moment(activity.performedAt).isSameOrBefore( - moment(activity.dueDate), - ) + const wasOnTime = + !activity.dueDate || + moment(activity.performedAt).isSameOrBefore(moment(activity.dueDate)) if (wasOnTime) { return { From 4ad8b1aef8139d96643cf90221dc3a668ee7db97 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 24 Mar 2026 17:04:25 -0400 Subject: [PATCH 05/12] Fix https://github.com/donetick/donetick/issues/604 Enhance ChoreEdit component with dropdown for archive/unarchive actions and add delete option --- src/views/ChoreEdit/ChoreEdit.jsx | 61 +++++++++++++++---------------- src/views/ChoreEdit/ChoreView.jsx | 17 ++------- 2 files changed, 34 insertions(+), 44 deletions(-) diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 35b9666..f7303b8 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -1,18 +1,23 @@ -import { Add, HorizontalRule, Save } from '@mui/icons-material' +import { Add, ArrowDropDown, HorizontalRule, Save } from '@mui/icons-material' import { Avatar, Box, Button, + ButtonGroup, Card, Checkbox, Chip, Container, Divider, + Dropdown, FormControl, FormHelperText, + IconButton, Input, List, ListItem, + Menu, + MenuButton, MenuItem, Option, Radio, @@ -1638,39 +1643,33 @@ const ChoreEdit = () => { }} > {choreId > 0 && ( - <> - {isActive ? ( + + + + + + + + { - archiveChore.mutate(choreId) - }} + onClick={handleDelete} > - Archive - - ) : ( - - )} - - + Delete + + + )} - + Delete diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index f70abb6..46f109a 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -1,5 +1,14 @@ import { Add, EditNotifications } from '@mui/icons-material' -import { Box, Button, Input, Option, Select, Typography } from '@mui/joy' +import { + Box, + Button, + Checkbox, + FormHelperText, + Input, + Option, + Select, + Typography, +} from '@mui/joy' import { FormControl } from '@mui/material' import * as chrono from 'chrono-node' import moment from 'moment' @@ -92,6 +101,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { const [hasNotifications, setHasNotifications] = useState(false) const [hasDeadline, setHasDeadline] = useState(false) const [deadlineOffset, setDeadlineOffset] = useState(-1) + const [dueDateOnly, setDueDateOnly] = useState(null) + const [dueTime, setDueTime] = useState(null) + const [useCustomTime, setUseCustomTime] = useState(false) const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false) const [projectId, setProjectId] = useState(getInitialProject()) @@ -135,7 +147,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { !dueDate ) { // add due date: - setDueDate(moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00')) + const tomorrow = moment().add(1, 'day') + setDueDateOnly(tomorrow.format('YYYY-MM-DD')) + setDueDate(tomorrow.endOf('day').format('YYYY-MM-DDTHH:mm:59')) + setUseCustomTime(false) + setDueTime(null) setShowKeyboardShortcuts(false) } // Enter key to create task @@ -379,9 +395,24 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { setFrequencyHumanReadable(repeat.name) } + const syncDueDateStates = parsedDate => { + const m = moment(parsedDate) + const dateOnly = m.format('YYYY-MM-DD') + const timeOnly = m.format('HH:mm') + setDueDateOnly(dateOnly) + setDueDate(m.format('YYYY-MM-DDTHH:mm:ss')) + if (timeOnly !== '23:59') { + setUseCustomTime(true) + setDueTime(timeOnly) + } else { + setUseCustomTime(false) + setDueTime(null) + } + } + let dueDateHighlight = null if (dueDateParsed.result) { - setDueDate(moment(dueDateParsed.result).format('YYYY-MM-DDTHH:mm:ss')) + syncDueDateStates(dueDateParsed.result) dueDateHighlight = dueDateParsed.highlight[0] } @@ -390,9 +421,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { // we need to reparse the date again to get the correct due date: const dueDateParsedAgain = parseDueDate(sentence, chrono) if (dueDateParsedAgain.result) { - setDueDate( - moment(dueDateParsedAgain.result).format('YYYY-MM-DDTHH:mm:ss'), - ) + syncDueDateStates(dueDateParsedAgain.result) } } @@ -472,6 +501,47 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { processText, ]) + const handleDueDateChange = e => { + const dateValue = e.target.value + setDueDateOnly(dateValue) + if (useCustomTime && dueTime) { + setDueDate( + moment(`${dateValue}T${dueTime}`).format('YYYY-MM-DDTHH:mm:00'), + ) + } else { + setDueDate(moment(dateValue).endOf('day').format('YYYY-MM-DDTHH:mm:ss')) + } + } + + const handleDueTimeChange = e => { + const timeValue = e.target.value + setDueTime(timeValue) + if (dueDateOnly) { + setDueDate( + moment(`${dueDateOnly}T${timeValue}`).format('YYYY-MM-DDTHH:mm:00'), + ) + } + } + + const handleUseCustomTimeChange = checked => { + setUseCustomTime(checked) + if (checked) { + const defaultTime = dueTime || '18:00' + setDueTime(defaultTime) + if (dueDateOnly) { + setDueDate( + moment(`${dueDateOnly}T${defaultTime}`).format('YYYY-MM-DDTHH:mm:00'), + ) + } + } else { + if (dueDateOnly) { + setDueDate( + moment(dueDateOnly).endOf('day').format('YYYY-MM-DDTHH:mm:ss'), + ) + } + } + } + const handleEnterPressed = () => { createChore() } @@ -495,6 +565,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { setProjectId(getInitialProject()) setHasDeadline(false) setDeadlineOffset(-1) + setDueDateOnly(null) + setDueTime(null) + setUseCustomTime(false) } const createChore = () => { @@ -780,7 +853,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { variant='plain' size='sm' onClick={() => { - setDueDate(moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00')) + const tomorrow = moment().add(1, 'day') + setDueDateOnly(tomorrow.format('YYYY-MM-DD')) + setDueDate(tomorrow.endOf('day').format('YYYY-MM-DDTHH:mm:ss')) + setUseCustomTime(false) + setDueTime(null) }} endDecorator={ showKeyboardShortcuts && @@ -870,11 +947,30 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { Due Date setDueDate(e.target.value)} - sx={{ width: '100%', fontSize: '16px' }} + type='date' + value={dueDateOnly || ''} + onChange={handleDueDateChange} /> + handleUseCustomTimeChange(e.target.checked)} + label='Set a specific time' + sx={{ mt: 1 }} + /> + + {useCustomTime + ? 'Task will be due at the specified time' + : 'Task will be due at the end of the day (11:59 PM)'} + + {useCustomTime && ( + + )} )} From 629920c2b3ee2bb4ab5000d98c38cb51c52bff36 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 5 Apr 2026 20:26:51 -0400 Subject: [PATCH 10/12] Description not being send with TaskinSentence --- src/views/components/AddTaskModal.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index 46f109a..d513830 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -598,6 +598,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { const chore = { name: taskTitle, + description: description, assignees: finalAssignees, dueDate: dueDate ? new Date(dueDate).toISOString() : null, assignedTo: finalAssignedTo, From 65cba31306db960a4c867e769d29c1be5e6055c0 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 5 Apr 2026 20:27:07 -0400 Subject: [PATCH 11/12] Update due date handling to treat 23:59:59 as end-of-day and adjust parsing logic for unspecified times --- src/utils/ChoreCardHelpers.jsx | 4 ++-- src/views/components/CustomParsers.js | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/utils/ChoreCardHelpers.jsx b/src/utils/ChoreCardHelpers.jsx index 013d5e5..2a09ccd 100644 --- a/src/utils/ChoreCardHelpers.jsx +++ b/src/utils/ChoreCardHelpers.jsx @@ -25,8 +25,8 @@ export const getDueDateChipText = (nextDueDate, chore) => { const dueDate = moment(nextDueDate) const diff = moment(nextDueDate).diff(moment(), 'hours') - // if seconds and minutes set to 59, treat as no time (date only) - if (dueDate.seconds() === 59 && dueDate.minutes() === 59) { + // if time is 23:59:59, treat as end-of-day (date only, no specific time) + if (dueDate.hours() === 23 && dueDate.minutes() === 59 && dueDate.seconds() === 59) { if (diff < 0) { // For overdue dates, show calendar format for recent dates const absDiff = Math.abs(diff) diff --git a/src/views/components/CustomParsers.js b/src/views/components/CustomParsers.js index 2983ba5..0a45e2d 100644 --- a/src/views/components/CustomParsers.js +++ b/src/views/components/CustomParsers.js @@ -722,8 +722,16 @@ export const parseDueDate = (inputSentence, chrono) => { .replace(/\s+/g, ' ') // Replace multiple spaces with single space .trim() + // If no specific time was mentioned, set to end of day (23:59:59) + // to indicate the date has no specific time tied to it (same convention as ChoreEdit) + let resultDate = dueDateMatch.start.date() + if (!dueDateMatch.start.isCertain('hour')) { + resultDate = new Date(resultDate) + resultDate.setHours(23, 59, 59, 0) + } + return { - result: dueDateMatch.start.date(), + result: resultDate, highlight: [ { text: fullHighlightText, From 2973b4755518949f99548e466c8d24ae410503f3 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 5 Apr 2026 20:41:16 -0400 Subject: [PATCH 12/12] User Activities to support note viewing functionality --- src/views/User/UserActivities.jsx | 37 ++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/views/User/UserActivities.jsx b/src/views/User/UserActivities.jsx index fa12fcf..08678eb 100644 --- a/src/views/User/UserActivities.jsx +++ b/src/views/User/UserActivities.jsx @@ -4,6 +4,7 @@ import { AccessTime, Check, EventBusy, + EventNote, Group, HourglassEmpty, Redo, @@ -34,6 +35,7 @@ import { import React, { useEffect, useState } from 'react' import { useChores, useChoresHistory } from '../../queries/ChoreQueries' +import NoteViewerModal from '../Modals/Inputs/NoteViewerModal' import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx' import { ChoresGrouper } from '../../utils/Chores' import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx' @@ -55,7 +57,7 @@ const groupByDate = history => { return aggregated } -const ChoreHistoryItem = ({ time, name, points, status, performer }) => { +const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onViewNote }) => { const getStatusIcon = status => { switch (status) { case 0: @@ -120,12 +122,27 @@ const ChoreHistoryItem = ({ time, name, points, status, performer }) => { {`${points} points`} )} + {notes && ( + } + sx={{ cursor: 'pointer' }} + onClick={e => { + e.stopPropagation() + onViewNote?.(notes) + }} + > + Note + + )} ) } -const ChoreHistoryTimeline = ({ history }) => { +const ChoreHistoryTimeline = ({ history, onViewNote }) => { const groupedHistory = groupByDate(history) const sortedEntries = Object.entries(groupedHistory).sort( @@ -166,6 +183,8 @@ const ChoreHistoryTimeline = ({ history }) => { name={record.choreName} points={record.points} status={record.status} + notes={record.notes} + onViewNote={onViewNote} /> ))} @@ -385,6 +404,7 @@ const UserActivites = () => { const [selectedHistory, setSelectedHistory] = React.useState([]) const [enrichedHistory, setEnrichedHistory] = React.useState([]) const [selectedChart, setSelectedChart] = React.useState('history') + const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false }) const [historyPieChartData, setHistoryPieChartData] = React.useState([]) const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([]) @@ -1085,7 +1105,17 @@ const UserActivites = () => { > {/* Left Side - Timeline (Mobile: Full width, Desktop: Flexible) */} - + { + setNoteViewerConfig({ + isOpen: true, + title: 'Note', + content: notes, + onClose: () => setNoteViewerConfig({ isOpen: false }), + }) + }} + /> {/* Right Sidebar - Charts (Mobile: Full width, Desktop: Fixed width + sticky) */} @@ -1235,6 +1265,7 @@ const UserActivites = () => { )} + ) }