diff --git a/client/src/components/Todo/TodoListPanel.test.tsx b/client/src/components/Todo/TodoListPanel.test.tsx index 06bf2aa7b..7b21dd020 100644 --- a/client/src/components/Todo/TodoListPanel.test.tsx +++ b/client/src/components/Todo/TodoListPanel.test.tsx @@ -256,6 +256,24 @@ describe('TodoListPanel', () => { expect(html.indexOf('High Prio')).toBeLessThan(html.indexOf('Low Prio')); }); + it('FE-COMP-TODO-073: Sort by due date puts the nearest deadline first, undated tasks last (#2205)', async () => { + const user = userEvent.setup(); + const items = [ + buildTodoItem({ name: 'Due Later', due_date: '2099-07-17', checked: 0 }), + buildTodoItem({ name: 'Due Tomorrow', due_date: '2099-07-16', checked: 0 }), + buildTodoItem({ name: 'No Due Date', checked: 0 }), + ]; + render(); + const sortBtn = screen.getAllByRole('button').find( + b => b.textContent?.includes('Due date') || b.getAttribute('title') === 'Due date' + ); + expect(sortBtn).toBeTruthy(); + await user.click(sortBtn!); + const html = document.body.innerHTML; + expect(html.indexOf('Due Tomorrow')).toBeLessThan(html.indexOf('Due Later')); + expect(html.indexOf('Due Later')).toBeLessThan(html.indexOf('No Due Date')); + }); + it('FE-COMP-TODO-019: Detail pane shows task name and allows editing', async () => { const user = userEvent.setup(); const items = [buildTodoItem({ id: 11, name: 'Edit Me', checked: 0 })]; diff --git a/client/src/components/Todo/TodoListPanel.tsx b/client/src/components/Todo/TodoListPanel.tsx index 5a4e04301..4eb57bd71 100644 --- a/client/src/components/Todo/TodoListPanel.tsx +++ b/client/src/components/Todo/TodoListPanel.tsx @@ -74,7 +74,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr const { canEdit, t, formatDate, toggleTodoItem, reorderTodoItems, isMobile, filter, setFilter, selectedId, setSelectedId, - isAddingNew, setIsAddingNew, sortByPrio, setSortByPrio, + isAddingNew, setIsAddingNew, sortByPrio, setSortByPrio, sortByDue, setSortByDue, addingCategory, setAddingCategory, newCategoryName, setNewCategoryName, members, categories, today, filtered, selectedItem, totalCount, doneCount, overdueCount, myCount, @@ -89,7 +89,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr // full item order so unfiltered tasks keep their place. const [dragId, setDragId] = useState(null) const [overId, setOverId] = useState(null) - const canReorder = canEdit && !sortByPrio + const canReorder = canEdit && !sortByPrio && !sortByDue const handleReorderDrop = (targetId: number) => { const from = dragId @@ -162,7 +162,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr {!isMobile &&
{t('todo.sidebar.sortBy')}
} - + {/* Categories */} {!isMobile &&
diff --git a/client/src/components/Todo/useTodoList.ts b/client/src/components/Todo/useTodoList.ts index fea2b5392..86b581dd1 100644 --- a/client/src/components/Todo/useTodoList.ts +++ b/client/src/components/Todo/useTodoList.ts @@ -45,6 +45,7 @@ export function useTodoList(tripId: number, items: TodoItem[], addItemSignal: nu lastHandledAddSignal.current = addItemSignal }, [addItemSignal]) const [sortByPrio, setSortByPrio] = useState(false) + const [sortByDue, setSortByDue] = useState(false) const [addingCategory, setAddingCategory] = useState(false) const [newCategoryName, setNewCategoryName] = useState('') const [members, setMembers] = useState([]) @@ -86,8 +87,15 @@ export function useTodoList(tripId: number, items: TodoItem[], addItemSignal: nu const bp = b.priority || 99 return ap - bp }) + // Nearest deadline first, undated tasks after all dated ones; ties keep the + // manual order, the stable sort leaves them untouched (#2205). + else if (sortByDue) result = [...result].sort((a, b) => { + if (!a.due_date) return b.due_date ? 1 : 0 + if (!b.due_date) return -1 + return a.due_date < b.due_date ? -1 : a.due_date > b.due_date ? 1 : 0 + }) return result - }, [items, filter, currentUserId, today, sortByPrio]) + }, [items, filter, currentUserId, today, sortByPrio, sortByDue]) const selectedItem = items.find(i => i.id === selectedId) || null const totalCount = items.length @@ -109,7 +117,7 @@ export function useTodoList(tripId: number, items: TodoItem[], addItemSignal: nu return { canEdit, t, formatDate, toggleTodoItem, reorderTodoItems, isMobile, filter, setFilter, selectedId, setSelectedId, - isAddingNew, setIsAddingNew, sortByPrio, setSortByPrio, + isAddingNew, setIsAddingNew, sortByPrio, setSortByPrio, sortByDue, setSortByDue, addingCategory, setAddingCategory, newCategoryName, setNewCategoryName, members, categories, today, filtered, selectedItem, totalCount, doneCount, overdueCount, myCount, diff --git a/client/src/mobile/screens/trip/tabs/MTodoListTab.tsx b/client/src/mobile/screens/trip/tabs/MTodoListTab.tsx index 3e6c750eb..727734642 100644 --- a/client/src/mobile/screens/trip/tabs/MTodoListTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MTodoListTab.tsx @@ -36,7 +36,7 @@ export default function MTodoListTab({ planner }: { planner: TripPlanner }) { const tripMembers = planner.tripMembers const [active, setActive] = useState({ kind: 'smart', id: 'all' }) - const [sortByPriority, setSortByPriority] = useState(false) + const [sortBy, setSortBy] = useState<'priority' | 'due' | null>(null) const [editingItemId, setEditingItemId] = useState(null) const [creatingTask, setCreatingTask] = useState(false) @@ -50,10 +50,10 @@ export default function MTodoListTab({ planner }: { planner: TripPlanner }) { active.kind === 'category' ? filterTodoItemsByCategory(items, active.name) : filterTodoItems(items, active.id, currentUserId, today), - sortByPriority, + sortBy, today, ), - [items, active, currentUserId, today, sortByPriority], + [items, active, currentUserId, today, sortBy], ) const pct = items.length > 0 ? Math.round((counts.done / items.length) * 100) : 0 @@ -113,13 +113,22 @@ export default function MTodoListTab({ planner }: { planner: TripPlanner }) { ))} + {categories.length > 0 && } {categories.map(cat => (