From 8c28183410ecadadadab5511e96d1480a6f86542 Mon Sep 17 00:00:00 2001 From: Emilio Veloci Date: Sat, 21 Feb 2026 17:15:51 +0100 Subject: [PATCH 01/31] README: Point to the full documentation in backend --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b235431..65b9eae 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,9 @@ As an avid for open-source, I was eager to create a solution that could benefit - Recurring Tasks: Schedule tasks to repeat daily, weekly, monthly, or yearly, with flexible customization options. - Progress Tracking: Track the completion status of tasks and view historical data. -## Installation +## Development Environment -1. Clone the repository: -2. Navigate to the project directory: `cd frontend` -3. Download dependency `npm install` -4. Run locally `npm start` +Follow the full instructions here: https://github.com/donetick/donetick?tab=readme-ov-file#development-environment ## Contributing @@ -41,7 +38,7 @@ Contributions are welcome! If you would like to contribute to Donetick, please f Donetick is a work in progress and has been a fantastic learning experience for me as I've honed my React skills,I'm looking for collaborators to help improve and refine the Donetick. Feel free to open PR or suggest changes. -## Plans : +## Plans: My goal is to expand Donetick by offering a hosted infrastructure option. This will make it even easier for users to access and utilize Donetick's features without the need for self-hosting. From 842fb976cda7a11d3817cf530f8540c027297f61 Mon Sep 17 00:00:00 2001 From: Emilio Veloci Date: Sat, 21 Feb 2026 17:58:21 +0100 Subject: [PATCH 02/31] Chores filter: Add filter for "Assigned to me or not assigned" Use case: An user might want to work on the chores assigned to them or still not assigned, whereas they might not be interested in the chores already assigned to other users. --- src/utils/Chores.jsx | 3 + src/views/Chores/SortAndGrouping.jsx | 241 ++++++++++----------------- 2 files changed, 91 insertions(+), 153 deletions(-) diff --git a/src/utils/Chores.jsx b/src/utils/Chores.jsx index 74e5226..257e031 100644 --- a/src/utils/Chores.jsx +++ b/src/utils/Chores.jsx @@ -332,6 +332,9 @@ export const ChoreFilters = userId => ({ assigned_to_me: chore => { return chore.assignedTo && chore.assignedTo === userId }, + available_for_me: chore => { + return chore.assignedTo === null || chore.assignedTo === userId + }, assigned_to_others: chore => { return chore.assignedTo && chore.assignedTo !== userId }, diff --git a/src/views/Chores/SortAndGrouping.jsx b/src/views/Chores/SortAndGrouping.jsx index f11f8af..bf653ca 100644 --- a/src/views/Chores/SortAndGrouping.jsx +++ b/src/views/Chores/SortAndGrouping.jsx @@ -85,7 +85,12 @@ const SortAndGrouping = ({ { name: 'Labels', value: 'labels' }, ] - const filterItems = ['anyone', 'assigned_to_me', 'assigned_to_others'] + const filterItems = [ + 'anyone', + 'assigned_to_me', + 'available_for_me', + 'assigned_to_others', + ] // Total selectable items: 4 (group by) + 3 (filters) + 1 (create custom filter) = 8 const totalItems = groupByItems.length + filterItems.length + 1 @@ -169,6 +174,65 @@ const SortAndGrouping = ({ } }, []) + const MenuItem_QuickFilter = props => { + return ( + { + setFilter(props.filterKey) + handleMenuClose() + }} + onMouseEnter={() => setIsKeyboardNavigating(false)} + sx={{ + borderRadius: 'var(--joy-radius-sm)', + backgroundColor: + selectedFilter === props.filterKey + ? 'var(--joy-palette-primary-softBg)' + : selectedIndex === props.index && + anchorEl && + isKeyboardNavigating + ? 'var(--joy-palette-neutral-softHoverBg)' + : 'transparent', + '&:hover': { + backgroundColor: + selectedFilter === props.filterKey + ? 'var(--joy-palette-primary-softBg)' + : 'var(--joy-palette-neutral-softHoverBg)', + }, + }} + > + + + + + + + {props.label} + + + + + ) + } + return ( <> {!label && ( @@ -359,162 +423,33 @@ const SortAndGrouping = ({ - { - setFilter('anyone') - handleMenuClose() - }} - onMouseEnter={() => setIsKeyboardNavigating(false)} - sx={{ - borderRadius: 'var(--joy-radius-sm)', - backgroundColor: - selectedFilter === 'anyone' - ? 'var(--joy-palette-primary-softBg)' - : selectedIndex === 4 && anchorEl && isKeyboardNavigating - ? 'var(--joy-palette-neutral-softHoverBg)' - : 'transparent', - '&:hover': { - backgroundColor: - selectedFilter === 'anyone' - ? 'var(--joy-palette-primary-softBg)' - : 'var(--joy-palette-neutral-softHoverBg)', - }, - }} - > - - - - - - - Anyone - - - - + index={4} + filterKey='anyone' + label='Anyone' + /> - { - setFilter('assigned_to_me') - handleMenuClose() - }} - onMouseEnter={() => setIsKeyboardNavigating(false)} - sx={{ - borderRadius: 'var(--joy-radius-sm)', - backgroundColor: - selectedFilter === 'assigned_to_me' - ? 'var(--joy-palette-primary-softBg)' - : selectedIndex === 5 && anchorEl && isKeyboardNavigating - ? 'var(--joy-palette-neutral-softHoverBg)' - : 'transparent', - '&:hover': { - backgroundColor: - selectedFilter === 'assigned_to_me' - ? 'var(--joy-palette-primary-softBg)' - : 'var(--joy-palette-neutral-softHoverBg)', - }, - }} - > - - - - - - - Assigned to me - - - - + index={5} + filterKey='assigned_to_me' + label='Assigned to me' + /> - + + { - setFilter('assigned_to_others') - handleMenuClose() - }} - onMouseEnter={() => setIsKeyboardNavigating(false)} - sx={{ - borderRadius: 'var(--joy-radius-sm)', - backgroundColor: - selectedFilter === 'assigned_to_others' - ? 'var(--joy-palette-primary-softBg)' - : selectedIndex === 6 && anchorEl && isKeyboardNavigating - ? 'var(--joy-palette-neutral-softHoverBg)' - : 'transparent', - '&:hover': { - backgroundColor: - selectedFilter === 'assigned_to_others' - ? 'var(--joy-palette-primary-softBg)' - : 'var(--joy-palette-neutral-softHoverBg)', - }, - }} - > - - - - - - - Assigned to others - - - - + index={7} + filterKey='assigned_to_others' + label='Assigned to others' + /> @@ -528,7 +463,7 @@ const SortAndGrouping = ({ sx={{ borderRadius: 'var(--joy-radius-sm)', backgroundColor: - selectedIndex === 7 && anchorEl && isKeyboardNavigating + selectedIndex === 8 && anchorEl && isKeyboardNavigating ? 'var(--joy-palette-success-softHoverBg)' : 'transparent', '&:hover': { From 651a6d6b3db9d6ecd12e8bc90bd3de8897f486d2 Mon Sep 17 00:00:00 2001 From: Mihaly Hobor Date: Fri, 27 Feb 2026 23:51:24 +0100 Subject: [PATCH 03/31] Fix chore list view doesn't update when chore is deleted. --- src/views/Chores/hooks/useChoreActions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/Chores/hooks/useChoreActions.js b/src/views/Chores/hooks/useChoreActions.js index abf781f..437d8de 100644 --- a/src/views/Chores/hooks/useChoreActions.js +++ b/src/views/Chores/hooks/useChoreActions.js @@ -259,6 +259,7 @@ export const useChoreActions = ({ c => c.id !== chore.id, ) setChores(newChores) + updateChoreInState(chore.id, 'deleted') setFilteredChores(newFilteredChores) showSuccess({ title: 'Task Deleted', From 343245f29047e64b9fd7807c35d2d2a9fee492db Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Mon, 9 Mar 2026 01:10:00 -0400 Subject: [PATCH 04/31] Add Smart Insights feature to Sidepanel and refactor SidepanelSettings --- src/utils/SidepanelConfig.js | 36 ++- src/views/Chores/Sidepanel.jsx | 19 +- src/views/Chores/SmartInsightsCard.jsx | 379 +++++++++++++++++++++++ src/views/Settings/SidepanelSettings.jsx | 64 +--- 4 files changed, 444 insertions(+), 54 deletions(-) create mode 100644 src/views/Chores/SmartInsightsCard.jsx diff --git a/src/utils/SidepanelConfig.js b/src/utils/SidepanelConfig.js index ed065d3..26ffb8e 100644 --- a/src/utils/SidepanelConfig.js +++ b/src/utils/SidepanelConfig.js @@ -7,13 +7,21 @@ export const DEFAULT_SIDEPANEL_CONFIG = [ enabled: true, order: 0, }, + { + id: 'smartInsights', + name: 'Smart Insights', + description: 'Quick actions based on your tasks', + iconName: 'TrendingUp', + enabled: false, + order: 1, + }, { id: 'assignees', name: 'Tasks by Assignee', description: 'Groups tasks by who they are assigned to', iconName: 'Person', enabled: true, - order: 1, + order: 2, }, { id: 'calendar', @@ -21,7 +29,7 @@ export const DEFAULT_SIDEPANEL_CONFIG = [ description: 'Shows tasks in a calendar format', iconName: 'CalendarMonth', enabled: true, - order: 2, + order: 3, }, { id: 'activities', @@ -29,7 +37,7 @@ export const DEFAULT_SIDEPANEL_CONFIG = [ description: 'Shows recent task completions and activities', iconName: 'History', enabled: true, - order: 3, + order: 4, }, { id: 'weeklyGoals', @@ -37,21 +45,37 @@ export const DEFAULT_SIDEPANEL_CONFIG = [ description: 'Shows weekly progress and family completion stats', iconName: 'EmojiEvents', enabled: true, - order: 4, + order: 5, }, ] export const getSidepanelConfig = () => { const saved = localStorage.getItem('sidepanelConfig') + let savedConfig = [] + if (saved) { try { - return JSON.parse(saved) + savedConfig = JSON.parse(saved) } catch (error) { console.error('Error parsing sidepanel config:', error) return DEFAULT_SIDEPANEL_CONFIG } } - return DEFAULT_SIDEPANEL_CONFIG + + // Merge saved config with default config + // This ensures new items in DEFAULT_SIDEPANEL_CONFIG are added to existing configs + const mergedConfig = DEFAULT_SIDEPANEL_CONFIG.map(defaultItem => { + const savedItem = savedConfig.find(item => item.id === defaultItem.id) + return savedItem || defaultItem + }) + + // Add any saved items that are no longer in default (for backwards compatibility) + const newSavedItems = savedConfig.filter( + savedItem => + !DEFAULT_SIDEPANEL_CONFIG.find(item => item.id === savedItem.id), + ) + + return [...mergedConfig, ...newSavedItems] } export const saveSidepanelConfig = config => { diff --git a/src/views/Chores/Sidepanel.jsx b/src/views/Chores/Sidepanel.jsx index b551f1b..a045249 100644 --- a/src/views/Chores/Sidepanel.jsx +++ b/src/views/Chores/Sidepanel.jsx @@ -6,10 +6,17 @@ import { ChoresGrouper } from '../../utils/Chores' import { getSidepanelConfig } from '../../utils/SidepanelConfig' import CalendarCard from '../components/CalendarCard' import ActivitiesCard from './ActivitesCard' +import SmartInsightsCard from './SmartInsightsCard' import TasksByAssigneeCard from './TasksByAssigneeCard' import UserSwitcher from './UserSwitcher' -const Sidepanel = ({ chores }) => { +const Sidepanel = ({ + chores, + allChores, + applyTempFilter, + clearTempFilter, + tempFilter, +}) => { const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('lg')) const [dueDatePieChartData, setDueDatePieChartData] = useState([]) const [sidepanelConfig, setSidepanelConfig] = useState([]) @@ -55,6 +62,16 @@ const Sidepanel = ({ chores }) => { switch (cardConfig.id) { case 'welcome': return + case 'smartInsights': + return ( + + ) case 'assignees': return case 'calendar': diff --git a/src/views/Chores/SmartInsightsCard.jsx b/src/views/Chores/SmartInsightsCard.jsx new file mode 100644 index 0000000..0e1b666 --- /dev/null +++ b/src/views/Chores/SmartInsightsCard.jsx @@ -0,0 +1,379 @@ +import { + EventBusy, + EventNote, + HourglassEmpty, + PriorityHigh, + TrendingUp, + WatchLater, +} from '@mui/icons-material' +import { Box, Button, Chip, Sheet, Typography } from '@mui/joy' +import { useMemo } from 'react' +import { TASK_COLOR } from '../../utils/Colors' + +// Static insight filter definitions – used for URL restoration +export const INSIGHT_FILTER_DEFS = { + overdue: { + name: 'Overdue', + filter: { + conditions: [{ type: 'dueDate', operator: 'isOverdue', value: null }], + operator: 'AND', + }, + }, + 'due-today': { + name: 'Due Today', + filter: { + conditions: [{ type: 'dueDate', operator: 'isDueToday', value: null }], + operator: 'AND', + }, + }, + 'pending-approval': { + name: 'Pending Approval', + filter: { + conditions: [{ type: 'status', operator: 'is', value: 3 }], + operator: 'AND', + }, + }, + 'due-this-week': { + name: 'Due This Week', + filter: { + conditions: [{ type: 'dueDate', operator: 'isDueThisWeek', value: null }], + operator: 'AND', + }, + }, + 'high-priority': { + name: 'High Priority', + filter: { + conditions: [{ type: 'priority', operator: 'is', value: [1, 2] }], + operator: 'AND', + }, + }, + 'no-due-date': { + name: 'No Due Date', + filter: { + conditions: [{ type: 'dueDate', operator: 'hasNoDueDate', value: null }], + operator: 'AND', + }, + }, +} + +const SmartInsightsCard = ({ + chores, + applyTempFilter, + clearTempFilter, + tempFilter, +}) => { + // Detect all possible insights from chores + const insights = useMemo(() => { + if (!chores || chores.length === 0) return [] + + const now = new Date() + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const tomorrow = new Date(today) + tomorrow.setDate(tomorrow.getDate() + 1) + const nextWeek = new Date(today) + nextWeek.setDate(nextWeek.getDate() + 7) + + const detectedInsights = [] + + // 1. Overdue tasks (Highest Priority) + const overdueTasks = chores.filter( + chore => chore.nextDueDate && new Date(chore.nextDueDate) < now, + ) + if (overdueTasks.length > 0) { + detectedInsights.push({ + id: 'overdue', + priority: 1, + count: overdueTasks.length, + title: 'Overdue', + description: `${overdueTasks.length} ${overdueTasks.length === 1 ? 'task is' : 'tasks are'} overdue`, + color: 'danger', + bgColor: TASK_COLOR.OVERDUE, + icon: , + filter: { + conditions: [ + { + type: 'dueDate', + operator: 'isOverdue', + value: null, + }, + ], + operator: 'AND', + }, + }) + } + + // 2. Due today (High Priority) + const dueTodayTasks = chores.filter( + chore => + chore.nextDueDate && + new Date(chore.nextDueDate).toDateString() === today.toDateString(), + ) + if (dueTodayTasks.length > 0) { + detectedInsights.push({ + id: 'due-today', + priority: 2, + count: dueTodayTasks.length, + title: 'Due Today', + description: `${dueTodayTasks.length} ${dueTodayTasks.length === 1 ? 'task' : 'tasks'} due by end of day`, + color: 'warning', + bgColor: '#FFA500', + icon: , + filter: { + conditions: [ + { + type: 'dueDate', + operator: 'isDueToday', + value: null, + }, + ], + operator: 'AND', + }, + }) + } + + // 3. Pending approval (High Priority) + const pendingApprovalTasks = chores.filter(chore => chore.status === 3) + if (pendingApprovalTasks.length > 0) { + detectedInsights.push({ + id: 'pending-approval', + priority: 3, + count: pendingApprovalTasks.length, + title: 'Pending Approval', + description: `${pendingApprovalTasks.length} ${pendingApprovalTasks.length === 1 ? 'task awaits' : 'tasks await'} approval`, + color: 'neutral', + bgColor: TASK_COLOR.PENDING_REVIEW, + icon: , + filter: { + conditions: [ + { + type: 'status', + operator: 'is', + value: 3, + }, + ], + operator: 'AND', + }, + }) + } + + // 4. Due this week (excluding today) (Medium Priority) + const dueThisWeekTasks = chores.filter(chore => { + if (!chore.nextDueDate) return false + const dueDate = new Date(chore.nextDueDate) + return dueDate >= tomorrow && dueDate < nextWeek + }) + if (dueThisWeekTasks.length > 0) { + detectedInsights.push({ + id: 'due-this-week', + priority: 4, + count: dueThisWeekTasks.length, + title: 'Due This Week', + description: `${dueThisWeekTasks.length} ${dueThisWeekTasks.length === 1 ? 'task' : 'tasks'} due in the next 7 days`, + color: 'primary', + bgColor: TASK_COLOR.IN_PROGRESS, + icon: , + filter: { + conditions: [ + { + type: 'dueDate', + operator: 'isDueThisWeek', + value: null, + }, + ], + operator: 'AND', + }, + }) + } + + // 5. High priority tasks (Medium Priority) + const highPriorityTasks = chores.filter( + chore => chore.priority === 1 || chore.priority === 2, + ) + if (highPriorityTasks.length > 0) { + detectedInsights.push({ + id: 'high-priority', + priority: 5, + count: highPriorityTasks.length, + title: 'High Priority', + description: `${highPriorityTasks.length} ${highPriorityTasks.length === 1 ? 'task requires' : 'tasks require'} immediate attention`, + color: 'warning', + bgColor: '#FF6B6B', + icon: , + filter: { + conditions: [ + { + type: 'priority', + operator: 'is', + value: [1, 2], + }, + ], + operator: 'AND', + }, + }) + } + + // 6. No due date (Lower Priority) + const noDueDateTasks = chores.filter( + chore => !chore.nextDueDate || chore.nextDueDate === null, + ) + if (noDueDateTasks.length > 0) { + detectedInsights.push({ + id: 'no-due-date', + priority: 6, + count: noDueDateTasks.length, + title: 'No Due Date', + description: `${noDueDateTasks.length} ${noDueDateTasks.length === 1 ? 'task needs' : 'tasks need'} a deadline`, + color: 'neutral', + bgColor: '#9E9E9E', + icon: , + filter: { + conditions: [ + { + type: 'dueDate', + operator: 'hasNoDueDate', + value: null, + }, + ], + operator: 'AND', + }, + }) + } + + // Sort by priority and return top 3 + return detectedInsights.sort((a, b) => a.priority - b.priority).slice(0, 3) + }, [chores]) + + const handleInsightClick = insight => { + // Toggle: if already active, clear it; otherwise apply it + if (isInsightActive(insight)) { + clearTempFilter() + } else { + applyTempFilter(insight.filter, { + id: insight.id, + name: insight.title, + description: insight.description, + icon: insight.icon, + color: insight.color, + }) + } + } + + const isInsightActive = insight => { + if (!tempFilter || !tempFilter.conditions) return false + return ( + JSON.stringify(tempFilter.conditions) === + JSON.stringify(insight.filter.conditions) + ) + } + + if (insights.length === 0) { + return null + } + + return ( + + {/* Header */} + + + + + Smart Insights + + {tempFilter && ( + + Active + + )} + + + {tempFilter + ? 'Click active filter to clear' + : 'Quick actions based on your tasks'} + + + + {/* Insight Cards */} + + {insights.map(insight => { + const isActive = isInsightActive(insight) + return ( + + ) + })} + + + ) +} + +export default SmartInsightsCard diff --git a/src/views/Settings/SidepanelSettings.jsx b/src/views/Settings/SidepanelSettings.jsx index 0734da7..6467858 100644 --- a/src/views/Settings/SidepanelSettings.jsx +++ b/src/views/Settings/SidepanelSettings.jsx @@ -2,8 +2,11 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd' import { CalendarMonth, DragIndicator, + EmojiEvents, History, Person, + SupervisorAccount, + TrendingUp, Visibility, VisibilityOff, WavingHand, @@ -23,48 +26,22 @@ import { Typography, } from '@mui/joy' import { useEffect, useState } from 'react' +import { + DEFAULT_SIDEPANEL_CONFIG, + getSidepanelConfig, + saveSidepanelConfig, +} from '../../utils/SidepanelConfig' import SettingsLayout from './SettingsLayout' -const DEFAULT_SIDEPANEL_CONFIG = [ - { - id: 'welcome', - name: 'Welcome Card', - description: 'Shows greeting and quick stats', - iconName: 'WavingHand', - enabled: true, - order: 0, - }, - { - id: 'assignees', - name: 'Tasks by Assignee', - description: 'Groups tasks by who they are assigned to', - iconName: 'Person', - enabled: true, - order: 1, - }, - { - id: 'calendar', - name: 'Calendar View', - description: 'Shows tasks in a calendar format', - iconName: 'CalendarMonth', - enabled: true, - order: 2, - }, - { - id: 'activities', - name: 'Recent Activities', - description: 'Shows recent task completions and activities', - iconName: 'History', - enabled: true, - order: 3, - }, -] - const SidepanelSettings = () => { - const [config, setConfig] = useState(DEFAULT_SIDEPANEL_CONFIG) + const [config, setConfig] = useState(getSidepanelConfig()) const getIcon = iconName => { switch (iconName) { + case 'SupervisorAccount': + return + case 'TrendingUp': + return case 'WavingHand': return case 'Person': @@ -73,27 +50,20 @@ const SidepanelSettings = () => { return case 'History': return + case 'EmojiEvents': + return default: return } } useEffect(() => { - const saved = localStorage.getItem('sidepanelConfig') - if (saved) { - try { - const parsed = JSON.parse(saved) - setConfig(parsed) - } catch (error) { - console.error('Error parsing sidepanel config:', error) - } - } + setConfig(getSidepanelConfig()) }, []) const saveConfig = newConfig => { setConfig(newConfig) - localStorage.setItem('sidepanelConfig', JSON.stringify(newConfig)) - window.dispatchEvent(new Event('sidepanelConfigChanged')) + saveSidepanelConfig(newConfig) } const handleToggleEnabled = (id, enabled) => { From 980a539ad2606af97753a6b05b179d4bca859db3 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Wed, 11 Mar 2026 22:26:42 -0400 Subject: [PATCH 05/31] Fix https://github.com/donetick/frontend/issues/72 Add styles to prevent iOS Safari auto-zoom on input focus in index.css and RichTextEditor.css --- src/index.css | 9 +++++++++ src/views/components/RichTextEditor.css | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/src/index.css b/src/index.css index 466e1e8..f2fa607 100644 --- a/src/index.css +++ b/src/index.css @@ -32,6 +32,15 @@ html { +/* Prevent iOS Safari from auto-zooming on input focus (triggered when font-size < 16px) */ +@supports (-webkit-touch-callout: none) { + input, + textarea, + select { + font-size: max(16px, 1em); + } +} + /* Ensure smooth transitions for dynamic content */ * { box-sizing: border-box; diff --git a/src/views/components/RichTextEditor.css b/src/views/components/RichTextEditor.css index 5c5cf95..20147ea 100644 --- a/src/views/components/RichTextEditor.css +++ b/src/views/components/RichTextEditor.css @@ -167,6 +167,13 @@ line-height: 1.5; } +/* Prevent iOS Safari from auto-zooming on focus (triggered when font-size < 16px) */ +@supports (-webkit-touch-callout: none) { + .quill-root .ql-editor { + font-size: 16px; + } +} + /* Custom focus styles */ .quill-root:focus-within .ql-toolbar.ql-snow { border-color: var(--joy-palette-primary-outlinedBorder, #1976d2); From 4b1bd48487088c269f9e123c080346bc3d3e6d72 Mon Sep 17 00:00:00 2001 From: Emilio Veloci Date: Thu, 12 Mar 2026 16:32:41 +0100 Subject: [PATCH 06/31] ChoreCard: Fix rendering of chores names starting with a digit --- src/views/Chores/ChoreCard.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index 010b796..8f5bcec 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -78,7 +78,7 @@ const ChoreCard = ({ const getName = name => { const split = Array.from(chore.name) // if the first character is emoji then remove it from the name - if (/\p{Emoji}/u.test(split[0])) { + if (isNaN(Number(split[0])) && /\p{Emoji}/u.test(split[0])) { return split.slice(1).join('').trim() } return name From e5edaeb14764ef80a7f6981878a0d265a253733f Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 15 Mar 2026 23:07:16 -0400 Subject: [PATCH 07/31] Remove Deadline for now as i want to spend more time thinking about it. as of now it's not tied to anything anyway --- src/views/ChoreEdit/ChoreEdit.jsx | 14 ++++++++++---- src/views/components/AddTaskModal.jsx | 9 ++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index c1b4af3..44425fa 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -1223,9 +1223,10 @@ const ChoreEdit = () => { )} {/* Expires After (Deadline) */} - + {/* { if (e.target.checked) { setDeadlineOffset(86400) // default 1 day in seconds @@ -1237,9 +1238,11 @@ const ChoreEdit = () => { label='Set a deadline' /> - Task will be considered expired after the due date + {isRolling && !['once', 'no_repeat'].includes(frequencyType) + ? 'Deadline is not available when scheduling from completion date' + : 'Task will be considered expired after the due date'} - + */} {deadlineOffset !== -1 && ( { setIsRolling(true)} + onClick={() => { + setIsRolling(true) + setDeadlineOffset(-1) + }} label='Reschedule from completion date' /> diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index 4e83499..f70abb6 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -20,7 +20,6 @@ import { } from './CustomParsers' import SmartTaskTitleInput from './SmartTaskTitleInput' -import DurationInput from '../../components/common/DurationInput' import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint' import NotificationTemplate from '../../components/NotificationTemplate' import LearnMoreButton from './LearnMore' @@ -804,7 +803,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { Edit Notifications )} - {!hasDeadline && dueDate && ( + {/* {!hasDeadline && dueDate && ( - )} + )} */} {hasDescription && ( @@ -978,7 +977,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { )} */} - {hasDeadline && dueDate && ( + {/* {hasDeadline && dueDate && ( { after due date - )} + )} */} {hasNotifications && dueDate && ( Date: Sun, 15 Mar 2026 23:08:09 -0400 Subject: [PATCH 08/31] Change button variant to 'outlined' for active insights in SmartInsightsCard --- src/views/Chores/SmartInsightsCard.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Chores/SmartInsightsCard.jsx b/src/views/Chores/SmartInsightsCard.jsx index 0e1b666..be316d0 100644 --- a/src/views/Chores/SmartInsightsCard.jsx +++ b/src/views/Chores/SmartInsightsCard.jsx @@ -317,7 +317,7 @@ const SmartInsightsCard = ({ return ( )} - {!hasDeadline && dueDate && ( + {/* {!hasDeadline && dueDate && ( - )} + )} */} {hasDescription && ( @@ -978,7 +977,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { )} */} - {hasDeadline && dueDate && ( + {/* {hasDeadline && dueDate && ( { after due date - )} + )} */} {hasNotifications && dueDate && ( Date: Sun, 15 Mar 2026 23:08:09 -0400 Subject: [PATCH 11/31] Change button variant to 'outlined' for active insights in SmartInsightsCard --- src/views/Chores/SmartInsightsCard.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Chores/SmartInsightsCard.jsx b/src/views/Chores/SmartInsightsCard.jsx index 0e1b666..be316d0 100644 --- a/src/views/Chores/SmartInsightsCard.jsx +++ b/src/views/Chores/SmartInsightsCard.jsx @@ -317,7 +317,7 @@ const SmartInsightsCard = ({ return ( + + + + + + { - archiveChore.mutate(choreId) - }} + onClick={handleDelete} > - Archive - - ) : ( - - )} - - + Delete + + + )} )} - + {!resource?.is_user_creation_disabled && ( + + )} Date: Sat, 28 Mar 2026 02:41:13 +0100 Subject: [PATCH 21/31] Rework notification input fields, fix verification happens too early. Adjust ranges. --- src/components/NotificationTemplate.jsx | 199 ++++++++++++++++-------- 1 file changed, 135 insertions(+), 64 deletions(-) diff --git a/src/components/NotificationTemplate.jsx b/src/components/NotificationTemplate.jsx index 0556e39..98c39de 100644 --- a/src/components/NotificationTemplate.jsx +++ b/src/components/NotificationTemplate.jsx @@ -12,7 +12,7 @@ import Input from '@mui/joy/Input' import Option from '@mui/joy/Option' import Select from '@mui/joy/Select' import Typography from '@mui/joy/Typography' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState, useRef } from 'react' import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors' import { TIME_UNITS } from '../utils/DurationUtils' @@ -73,6 +73,9 @@ const NotificationTemplate = ({ [], ) + const notificationsRef = useRef(notifications) + const [draftValues, setDraftValues] = useState({}) + const [error, setError] = useState(null) const [showSaveDefault, setShowSaveDefault] = useState(false) // Create a map of notification indices for timeline display @@ -114,7 +117,6 @@ const NotificationTemplate = ({ // Sort notifications and update the index mapping useEffect(() => { updateNotificationIndices() - setError(null) }, [updateNotificationIndices]) // Notify parent component of changes including the template name @@ -125,8 +127,8 @@ const NotificationTemplate = ({ }, [notifications, onChange]) // Validates if a notification configuration already exists - const isDuplicate = (notification, currentIdx = -1) => { - return notifications.some((n, idx) => { + const isDuplicate = (notification, currentIdx = -1, list = notifications) => { + return list.some((n, idx) => { if (idx === currentIdx) return false return ( @@ -136,6 +138,26 @@ const NotificationTemplate = ({ }) } + const getSmartSuggestion = type => { + let suggestions = [] + if (type === 'reminder' || type === 'before') { + suggestions = [ + { value: -1, unit: 'd' }, + { value: -3, unit: 'h' }, + { value: -30, unit: 'm' }, + ] + } else if (type === 'followup' || type === 'after') { + suggestions = [ + { value: 1, unit: 'd' }, + { value: 3, unit: 'd' }, + { value: 7, unit: 'd' }, + ] + } + return suggestions.find( + suggestion => !isDuplicate(suggestion, -1, notificationsRef.current), + ) + } + const handleChange = (idx, field, value) => { const currentNotification = notifications[idx] const uiRep = getUIRepresentation(currentNotification) @@ -149,6 +171,15 @@ const NotificationTemplate = ({ // Reset display value when switching to "On Due" if (value === 'ondue') { updatedUIRep.displayValue = 0 + } else if (Number(currentNotification.value) === 0) { + const suggestion = getSmartSuggestion(value) + if (suggestion) { + updatedUIRep.displayValue = Math.abs(suggestion.value) + updatedUIRep.unit = suggestion.unit + } else { + updatedUIRep.displayValue = 1 + updatedUIRep.unit = 'h' + } } } else if (field === 'displayValue') { updatedUIRep.displayValue = Math.max(0, Number(value)) @@ -168,71 +199,41 @@ const NotificationTemplate = ({ unit: updatedUIRep.unit, } - // Check if another notification is already "On Due" (value = 0) - if (newInternalValue === 0) { - const existingOnDue = notifications.findIndex( - (n, i) => i !== idx && Number(n.value) === 0, - ) + const updated = notifications.map((n, i) => + i === idx ? updatedNotification : n, + ) + setNotifications(updated) + notificationsRef.current = updated + setError(null) + } - if (existingOnDue !== -1) { - setError( - 'Only one notification can be set to "On Due". Please choose a different timing.', - ) - return - } - } + const handleBlur = idx => { + const currentList = notificationsRef.current + const currentNotification = currentList[idx] - if (isDuplicate(updatedNotification, idx)) { + if (!currentNotification) return + + if (isDuplicate(currentNotification, idx, currentList)) { setError( 'This notification setting already exists. Please use a different timing.', ) return } - - const updated = notifications.map((n, i) => - i === idx ? updatedNotification : n, - ) - setNotifications(updated) - setError(null) } const addSmartNotification = type => { if (notifications.length >= maxNotifications) return setShowSaveDefault(true) let newNotification - let suggestions = [] - - switch (type) { - case 'reminder': - // Suggest common reminder times that don't exist - suggestions = [ - { value: -1, unit: 'd' }, // 1 day before - { value: -3, unit: 'h' }, // 3 hours before - { value: -30, unit: 'm' }, // 3 days before - ] - break - - case 'due': - if (notifications.some(n => Number(n.value) === 0)) { - setError('Only one "Due Alert" notification is allowed.') - return - } - newNotification = { value: 0, unit: 'm' } - break - - case 'followup': - suggestions = [ - { value: 1, unit: 'd' }, // 1 day after - { value: 3, unit: 'd' }, // 3 days after - { value: 7, unit: 'd' }, // 1 week after - ] - break - } - - // For reminder/followup, find first non-duplicate suggestion - if (suggestions.length > 0) { - newNotification = suggestions.find(suggestion => !isDuplicate(suggestion)) + if (type === 'due') { + if (notificationsRef.current.some(n => Number(n.value) === 0)) { + setError('Only one "Due Alert" notification is allowed.') + return + } + newNotification = { value: 0, unit: 'm' } + } else { + newNotification = getSmartSuggestion(type) if (!newNotification) { setError(`All common ${type} times are already configured.`) return @@ -243,15 +244,25 @@ const NotificationTemplate = ({ const updatedNotifications = [...notifications, newNotification] setNotifications(updatedNotifications) + notificationsRef.current = updatedNotifications setError(null) } const removeNotification = idx => { const updated = notifications.filter((_, i) => i !== idx) setNotifications(updated) + notificationsRef.current = updated + + setDraftValues(prev => { + const next = { ...prev } + delete next[idx] + return next + }) + onChange && onChange(updated) setShowSaveDefault(true) } + const renderTimeline = () => { // Convert notifications to minutes for proper chronological sorting const convertToMinutes = (value, unit) => { @@ -459,6 +470,11 @@ const NotificationTemplate = ({ const badgeNumber = notificationIndexMap[idx] const uiRep = getUIRepresentation(n) + // Check if an "On Due" notification exists anywhere else in the list + const hasOnDueElsewhere = notificationsRef.current.some( + (notif, i) => i !== idx && Number(notif.value) === 0, + ) + const getNotificationColors = value => { if (Number(value) < 0) { return { @@ -487,7 +503,7 @@ const NotificationTemplate = ({ const colors = getNotificationColors(n.value) return ( - <> + handleChange(idx, 'timing', value)} + onBlur={() => handleBlur(idx)} sx={{ minWidth: 80 }} size={'sm'} > {timingOptions.map(opt => ( - ))} @@ -560,11 +579,62 @@ const NotificationTemplate = ({ - handleChange(idx, 'displayValue', e.target.value) + value={ + draftValues[idx] !== undefined + ? draftValues[idx] + : uiRep.displayValue } + disabled={uiRep.timing === 'ondue'} + onChange={e => { + const val = e.target.value + if (val.includes('-')) return + + // Prevent setting to '0' if an 'On Due' already exists elsewhere + if (val === '0' || val === '') { + if (hasOnDueElsewhere) return + } + + setDraftValues(prev => ({ ...prev, [idx]: val })) + }} + onKeyDown={e => { + if (['-', 'e', '+', '.'].includes(e.key)) { + e.preventDefault() + return + } + + // Physically block typing '0' if 'On Due' exists + if ( + e.key === '0' || + e.key === 'Backspace' || + e.key === 'Delete' + ) { + const currentVal = + draftValues[idx] !== undefined + ? draftValues[idx] + : uiRep.displayValue.toString() + + // If typing 0 into an empty input, or deleting the last character + const willBeZero = + (e.key === '0' && currentVal === '') || + ((e.key === 'Backspace' || e.key === 'Delete') && + currentVal.length <= 1) + + if (willBeZero && hasOnDueElsewhere) { + e.preventDefault() + } + } + }} + onBlur={e => { + let val = e.target.value + if (val === '' || Number(val) < 0) val = 0 + handleChange(idx, 'displayValue', val) + setDraftValues(prev => { + const next = { ...prev } + delete next[idx] + return next + }) + handleBlur(idx) + }} sx={{ width: 60, opacity: uiRep.timing === 'ondue' ? 0.6 : 1, @@ -576,6 +646,7 @@ const NotificationTemplate = ({ value={n.unit} disabled={uiRep.timing === 'ondue'} onChange={(_, value) => handleChange(idx, 'unit', value)} + onBlur={() => handleBlur(idx)} sx={{ minWidth: 70, opacity: uiRep.timing === 'ondue' ? 0.6 : 1, @@ -605,7 +676,7 @@ const NotificationTemplate = ({ - + ) })} Date: Tue, 31 Mar 2026 00:45:05 +0200 Subject: [PATCH 22/31] Update label for quick filter to 'Available for me' --- src/views/Chores/SortAndGrouping.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Chores/SortAndGrouping.jsx b/src/views/Chores/SortAndGrouping.jsx index bf653ca..eeee5a1 100644 --- a/src/views/Chores/SortAndGrouping.jsx +++ b/src/views/Chores/SortAndGrouping.jsx @@ -441,7 +441,7 @@ const SortAndGrouping = ({ key={`${k}-assignee-available-for-me`} index={6} filterKey='available_for_me' - label='Assigned to me or not assigned' + label='Available for me' /> Date: Thu, 2 Apr 2026 16:08:59 +0200 Subject: [PATCH 23/31] Fix issue where cannot delete input field before input --- src/components/NotificationTemplate.jsx | 38 ++++++------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/src/components/NotificationTemplate.jsx b/src/components/NotificationTemplate.jsx index 98c39de..734fd0b 100644 --- a/src/components/NotificationTemplate.jsx +++ b/src/components/NotificationTemplate.jsx @@ -589,44 +589,24 @@ const NotificationTemplate = ({ const val = e.target.value if (val.includes('-')) return - // Prevent setting to '0' if an 'On Due' already exists elsewhere - if (val === '0' || val === '') { - if (hasOnDueElsewhere) return - } - setDraftValues(prev => ({ ...prev, [idx]: val })) }} onKeyDown={e => { if (['-', 'e', '+', '.'].includes(e.key)) { e.preventDefault() - return - } - - // Physically block typing '0' if 'On Due' exists - if ( - e.key === '0' || - e.key === 'Backspace' || - e.key === 'Delete' - ) { - const currentVal = - draftValues[idx] !== undefined - ? draftValues[idx] - : uiRep.displayValue.toString() - - // If typing 0 into an empty input, or deleting the last character - const willBeZero = - (e.key === '0' && currentVal === '') || - ((e.key === 'Backspace' || e.key === 'Delete') && - currentVal.length <= 1) - - if (willBeZero && hasOnDueElsewhere) { - e.preventDefault() - } } }} onBlur={e => { let val = e.target.value - if (val === '' || Number(val) < 0) val = 0 + + const numericVal = Number(val) + val = + numericVal <= 0 + ? hasOnDueElsewhere + ? 1 + : 0 + : numericVal + handleChange(idx, 'displayValue', val) setDraftValues(prev => { const next = { ...prev } From 1d11f8316cadf6e02b5a23b9aea3c3885a2ed464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20H=C3=B3bor?= Date: Sat, 4 Apr 2026 01:06:18 +0200 Subject: [PATCH 24/31] Fix the issue where the chore reappears after completion then becomes hidden again. --- src/views/Chores/hooks/useChoreActions.js | 42 +++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/views/Chores/hooks/useChoreActions.js b/src/views/Chores/hooks/useChoreActions.js index 437d8de..a8e0088 100644 --- a/src/views/Chores/hooks/useChoreActions.js +++ b/src/views/Chores/hooks/useChoreActions.js @@ -154,6 +154,18 @@ export const useChoreActions = ({ async (action, chore, extraData = {}) => { switch (action) { case 'complete': + // 1. Instantly hide the chore from the UI and Cache + setChores(prev => prev.filter(c => c.id !== chore.id)) + setFilteredChores(prev => prev.filter(c => c.id !== chore.id)) + + queryClient.setQueriesData({ queryKey: ['chores'] }, oldData => { + if (!oldData || !oldData.res) return oldData; + return { + ...oldData, + res: oldData.res.filter(c => c.id !== chore.id), + } + }); + try { const response = await MarkChoreComplete( chore.id, @@ -162,10 +174,36 @@ export const useChoreActions = ({ null, ) if (response.ok) { - const data = await response.json() - updateChoreInState(data.res, 'completed') + // 2. Show the success notification with Undo + showSuccess({ + message: 'Task completed', + undoAction: async () => { + try { + const undoResponse = await UndoChoreAction(chore.id) + if (undoResponse.ok) { + refetchChores() + showUndo({ + title: 'Undo Successful', + message: 'Task completion has been undone.', + }) + } else throw new Error('Failed to undo') + } catch (error) { + showError({ + title: 'Undo Failed', + message: 'Unable to undo the action. Please try again.', + }) + } + }, + }) + + // 3. Fetch the fresh active list from the server silently + // (This brings in the next occurrence if recurring, without showing the completed one) + queryClient.invalidateQueries({ queryKey: ['chores'] }) + } else { + refetchChores() // Network failed, revert to truth } } catch (error) { + refetchChores() // Network failed, revert to truth if (error?.queued) { showError({ title: 'Update Failed', From 175ef04c7014723f360b754d4b695fa9f0761ff6 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 5 Apr 2026 18:25:55 -0400 Subject: [PATCH 25/31] Fix SmartTaskTitleInput to set text color to transparent for better visibility in dark mode --- src/views/components/SmartTaskTitleInput.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/components/SmartTaskTitleInput.jsx b/src/views/components/SmartTaskTitleInput.jsx index 4d87016..8432c44 100644 --- a/src/views/components/SmartTaskTitleInput.jsx +++ b/src/views/components/SmartTaskTitleInput.jsx @@ -213,7 +213,7 @@ const SmartTaskTitleInput = ({ fontSize: 'inherit', lineHeight: 'inherit', backgroundColor: 'transparent', - color: mode === 'dark' ? '#cbd5e1' : '#1a202c', + color: 'transparent', caretColor: mode === 'dark' ? '#fff' : '#000', }} /> From 9a637c484bfb5b37302ca9628e87fe21a3795b20 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 5 Apr 2026 18:27:07 -0400 Subject: [PATCH 26/31] fix issue if we use `Reset Filter` showing task across project instead of default only --- src/views/Chores/MyChores.jsx | 15 ++++++--------- src/views/Chores/hooks/useChoreFilters.js | 4 +--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index d57c099..b3c4e4d 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -221,15 +221,12 @@ const MyChores = () => { let choresToGroup = chores if (tempFilter || activeFilterId) { choresToGroup = customFilteredChores - } else if (selectedProject) { - // Otherwise, use project-filtered chores for section grouping - if (selectedProject.id === 'default') { - // Default project: only show tasks without a projectId - choresToGroup = chores.filter(chore => !chore.projectId) - } else { - // Other projects: use the existing filter function - choresToGroup = filterByProject(chores, selectedProject.id) - } + } else if (!selectedProject || selectedProject.id === 'default') { + // No project selected or default project: only show tasks without a projectId + choresToGroup = chores.filter(chore => !chore.projectId) + } else { + // Specific project: use the existing filter function + choresToGroup = filterByProject(chores, selectedProject.id) } const sections = ChoresGrouper( diff --git a/src/views/Chores/hooks/useChoreFilters.js b/src/views/Chores/hooks/useChoreFilters.js index 8153f1d..e30bb32 100644 --- a/src/views/Chores/hooks/useChoreFilters.js +++ b/src/views/Chores/hooks/useChoreFilters.js @@ -15,9 +15,7 @@ export const useChoreFilters = ({ ) const projectFilteredChores = useMemo(() => { - if (!selectedProject) return chores - - if (selectedProject.id === 'default') { + if (!selectedProject || selectedProject.id === 'default') { return chores.filter(chore => !chore.projectId) } From 083680b5639c44d6b2f6f56466009b967dfb3236 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 5 Apr 2026 18:27:37 -0400 Subject: [PATCH 27/31] ChoreHistoryItem to improve status icon handling and update date parsing logic to match choreHistory card --- src/views/User/UserActivities.jsx | 37 +++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/views/User/UserActivities.jsx b/src/views/User/UserActivities.jsx index 7a76eef..fa12fcf 100644 --- a/src/views/User/UserActivities.jsx +++ b/src/views/User/UserActivities.jsx @@ -1,14 +1,15 @@ -import CheckCircleIcon from '@mui/icons-material/CheckCircle' -import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty' -import ThumbDownIcon from '@mui/icons-material/ThumbDown' -import TimelapseIcon from '@mui/icons-material/Timelapse' import { Cell, Pie, PieChart, Tooltip } from 'recharts' import { - Block, + AccessTime, Check, EventBusy, Group, + HourglassEmpty, + Redo, + RunningWithErrors, + Schedule, + ThumbDown, Timeline, Toll, } from '@mui/icons-material' @@ -43,7 +44,9 @@ const groupByDate = history => { const aggregated = {} for (let i = 0; i < history.length; i++) { const item = history[i] - const date = new Date(item.performedAt).toLocaleDateString() + const date = new Date( + item.performedAt || item.updatedAt, + ).toLocaleDateString() if (!aggregated[date]) { aggregated[date] = [] } @@ -56,17 +59,21 @@ const ChoreHistoryItem = ({ time, name, points, status, performer }) => { const getStatusIcon = status => { switch (status) { case 0: - return + return case 1: return case 2: - return + return case 3: - return + return case 4: - return + return + case 5: + return + case 6: + return default: - return + return } } @@ -121,6 +128,10 @@ const ChoreHistoryItem = ({ time, name, points, status, performer }) => { const ChoreHistoryTimeline = ({ history }) => { const groupedHistory = groupByDate(history) + const sortedEntries = Object.entries(groupedHistory).sort( + ([a], [b]) => new Date(b) - new Date(a), + ) + return ( @@ -146,7 +157,9 @@ const ChoreHistoryTimeline = ({ history }) => { <> Date: Sun, 5 Apr 2026 18:44:41 -0400 Subject: [PATCH 28/31] Enhance AddTaskModal to support custom due times and update due date handling --- src/views/ChoreEdit/ChoreEdit.jsx | 25 +++--- src/views/components/AddTaskModal.jsx | 118 +++++++++++++++++++++++--- 2 files changed, 122 insertions(+), 21 deletions(-) diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index f7303b8..8dcf06b 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -296,7 +296,7 @@ const ChoreEdit = () => { if (dueDateOnly) { const combinedDateTime = moment(`${dueDateOnly}T${defaultTime}`).format( - 'YYYY-MM-DDTHH:mm:00', + 'YYYY-MM-DDTHH:mm:59', ) setDueDate(combinedDateTime) @@ -314,7 +314,7 @@ const ChoreEdit = () => { if (dueDateOnly) { const endOfDay = moment(dueDateOnly) .endOf('day') - .format('YYYY-MM-DDTHH:mm:00') + .format('YYYY-MM-DDTHH:mm:ss') setDueDate(endOfDay) } } @@ -563,7 +563,7 @@ const ChoreEdit = () => { const today = moment(new Date()).format('YYYY-MM-DD') setDueDateOnly(today) // Default to end of day - setDueDate(moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:00')) + setDueDate(moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:59')) setUseCustomTime(false) setDueTime(null) } @@ -1112,7 +1112,7 @@ const ChoreEdit = () => { const today = moment(new Date()).format('YYYY-MM-DD') setDueDateOnly(today) setDueDate( - moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:00'), + moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:59'), ) setUseCustomTime(false) setDueTime(null) @@ -1644,7 +1644,10 @@ const ChoreEdit = () => { > {choreId > 0 && ( - + - + 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 29/31] 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 30/31] 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 31/31] 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 = () => { )} + ) }