diff --git a/client/src/App.tsx b/client/src/App.tsx index ec7ea25..187d62e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -25,6 +25,7 @@ import TasksWithCalendar from './components/TasksWithCalendar'; import CalendarView from './components/CalendarView'; import KanbanBoard from './components/KanbanBoard'; import ProjectTemplate from './components/ProjectTemplate'; +import WeekListView from './components/WeekListView'; import ThemeToggle from './components/ThemeToggle'; import Settings from './pages/settings'; import { Task, Label } from '@shared/schema'; @@ -325,6 +326,18 @@ function App() { /> ); + case 'weeklist': + return ( + + ); + case 'templates': return ( ) => void; + onTaskEdit?: (task: Task) => void; + onTaskDelete?: (taskId: string) => void; + onStartTimer?: (taskId: string) => void; + onStopTimer?: (taskId: string) => void; +} + +type SortOption = 'dueDate' | 'priority' | 'title' | 'status'; +type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue'; + +export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDelete, onStartTimer, onStopTimer }: WeekListViewProps) { + const { t } = useTranslation(); + const [searchQuery, setSearchQuery] = useState(''); + const [sortBy, setSortBy] = useState('dueDate'); + const [filterBy, setFilterBy] = useState('all'); + const [selectedWeekStart, setSelectedWeekStart] = useState(startOfWeek(new Date(), { weekStartsOn: 1 })); + const [completionModal, setCompletionModal] = useState<{ isOpen: boolean; task: Task | null }>({ + isOpen: false, + task: null + }); + + // Fetch labels to get label colors + const { data: labels = [] } = useQuery({ + queryKey: ['/api/labels'], + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + }); + + // Always show 7 days for the selected week (Monday to Sunday) + const weekStart = startOfDay(selectedWeekStart); + const weekEnd = endOfDay(endOfWeek(selectedWeekStart, { weekStartsOn: 1 })); + const dates = Array.from({ length: 7 }, (_, i) => addDays(selectedWeekStart, i)); + + const isOverdue = (task: Task) => { + if (!task.dueDate) return false; + return task.dueDate < new Date() && task.status !== 'done'; + }; + + const isInCurrentWeek = (task: Task) => { + if (!task.dueDate) return false; + return task.dueDate >= weekStart && task.dueDate <= weekEnd; + }; + + const filteredAndSortedTasks = tasks + .filter(task => { + // Only show tasks in the selected week + if (!isInCurrentWeek(task)) return false; + + // Search filter + const matchesSearch = task.title.toLowerCase().includes(searchQuery.toLowerCase()) || + (task.description?.toLowerCase()?.includes(searchQuery.toLowerCase()) ?? false); + + if (!matchesSearch) return false; + + // Status filter + switch (filterBy) { + case 'overdue': + return isOverdue(task); + case 'all': + return true; + default: + return task.status === filterBy; + } + }) + .sort((a, b) => { + switch (sortBy) { + case 'dueDate': + if (!a.dueDate && !b.dueDate) return 0; + if (!a.dueDate) return 1; + if (!b.dueDate) return -1; + return a.dueDate.getTime() - b.dueDate.getTime(); + + case 'priority': + const priorityOrder = { high: 3, medium: 2, low: 1 }; + return priorityOrder[b.priority as keyof typeof priorityOrder] - priorityOrder[a.priority as keyof typeof priorityOrder]; + + case 'title': + return a.title.localeCompare(b.title); + + case 'status': + const statusOrder = { todo: 1, inProgress: 2, done: 3 }; + return statusOrder[a.status as keyof typeof statusOrder] - statusOrder[b.status as keyof typeof statusOrder]; + + default: + return 0; + } + }); + + const getFilterCount = (filter: FilterOption) => { + const weekTasks = tasks.filter(isInCurrentWeek); + switch (filter) { + case 'overdue': + return weekTasks.filter(isOverdue).length; + case 'all': + return weekTasks.length; + default: + return weekTasks.filter(task => task.status === filter).length; + } + }; + + const getTasksForDate = (date: Date) => { + return filteredAndSortedTasks.filter(task => + task.dueDate && isSameDay(task.dueDate, date) + ); + }; + + const navigateCalendar = (direction: 'prev' | 'next') => { + const newWeekStart = addWeeks(selectedWeekStart, direction === 'next' ? 1 : -1); + setSelectedWeekStart(newWeekStart); + console.log(`Navigating to week starting ${newWeekStart.toLocaleDateString()}`); + }; + + const goToCurrentWeek = () => { + setSelectedWeekStart(startOfWeek(new Date(), { weekStartsOn: 1 })); + }; + + const handleTaskComplete = (task: Task) => { + setCompletionModal({ isOpen: true, task }); + }; + + const handleTimeLogged = (timeSpent: number) => { + if (completionModal.task) { + onTaskUpdate?.(completionModal.task.id, { + status: 'done', + timeTracked: (completionModal.task.timeTracked || 0) + timeSpent, + isTracking: false + }); + } + setCompletionModal({ isOpen: false, task: null }); + }; + + return ( +
+ {/* Header and Search Filters */} +
+
+ {/* Title Bar */} +
+

+ {t('weekList.title')} +

+ + {t('weekList.taskCount', { count: filteredAndSortedTasks.length })} + +
+ + {/* Search Bar */} +
+ + { + setSearchQuery(e.target.value); + console.log('Search query:', e.target.value); + }} + className="pl-10" + data-testid="input-search-week-tasks" + /> +
+ +
+ + + +
+
+
+ + {/* Calendar Week Selector */} + +
+
+ +

+ {format(weekStart, 'MMM d')} - {format(weekEnd, 'MMM d, yyyy')} +

+
+ +
+ + + + + +
+
+ + {/* Mini Calendar Bar */} +
+ {dates.map((date, index) => { + const dayTasks = getTasksForDate(date); + const isToday = isSameDay(date, new Date()); + const isWeekend = date.getDay() === 0 || date.getDay() === 6; + + return ( +
+
+ {format(date, 'EEE')} +
+
+ {format(date, 'd')} +
+ {dayTasks.length > 0 && ( + + {dayTasks.length} + + )} +
+ ); + })} +
+
+ + {/* Task List */} +
+ {filteredAndSortedTasks.length === 0 ? ( +
+

+ {searchQuery || filterBy !== 'all' + ? t('weekList.noMatchingTasks') + : t('weekList.noTasks') + } +

+
+ ) : ( + <> +
+ {t('weekList.tasksFor', { start: format(weekStart, 'MMM d'), end: format(weekEnd, 'MMM d') })} +
+ {filteredAndSortedTasks.map((task) => ( +
+ onStartTimer?.(task.id)} + onStopTimer={() => onStopTimer?.(task.id)} + onEdit={() => { + onTaskEdit?.(task); + console.log(`Edit task ${task.title}`); + }} + onDelete={() => { + onTaskDelete?.(task.id); + console.log(`Delete task ${task.title}`); + }} + onStatusChange={(newStatus) => { + if (newStatus === 'done') { + handleTaskComplete(task); + } else { + onTaskUpdate?.(task.id, { status: newStatus }); + } + console.log(`Task ${task.title} status changed to ${newStatus}`); + }} + /> +
+ ))} + + )} +
+ + {/* Time Completion Modal */} + setCompletionModal({ isOpen: false, task: null })} + onSave={handleTimeLogged} + taskTitle={completionModal.task?.title || ''} + /> +
+ ); +} diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json index dfa439a..59f2296 100644 --- a/client/src/i18n/locales/de.json +++ b/client/src/i18n/locales/de.json @@ -6,6 +6,7 @@ "navigation": { "tasks": "Aufgaben", "calendar": "Kalender", + "weekList": "Woche", "create": "Erstellen", "kanban": "Kanban", "settings": "Einstellungen" @@ -30,6 +31,30 @@ "status": "Status" } }, + "weekList": { + "title": "Wochenaufgaben", + "taskCount": "{{count}} Aufgaben", + "searchPlaceholder": "Aufgaben suchen...", + "noTasks": "Keine Aufgaben für diese Woche geplant", + "noMatchingTasks": "Keine Aufgaben entsprechen deinen Filtern für diese Woche", + "filter": { + "all": "Alle", + "todo": "Zu erledigen", + "inProgress": "In Bearbeitung", + "done": "Erledigt", + "overdue": "Überfällig" + }, + "sort": { + "dueDate": "Fälligkeitsdatum", + "priority": "Priorität", + "title": "Titel", + "status": "Status" + }, + "previous": "Zurück", + "today": "Heute", + "next": "Weiter", + "tasksFor": "Aufgaben für {{start}} - {{end}}" + }, "taskCreation": { "title": "Neue Aufgabe", "titlePlaceholder": "Was muss erledigt werden?", diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index 5ca2a61..4ba57bd 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -6,6 +6,7 @@ "navigation": { "tasks": "Tasks", "calendar": "Calendar", + "weekList": "Week", "create": "Create", "kanban": "Kanban", "settings": "Settings" @@ -30,6 +31,30 @@ "status": "Status" } }, + "weekList": { + "title": "Week Tasks", + "taskCount": "{{count}} tasks", + "searchPlaceholder": "Search tasks...", + "noTasks": "No tasks scheduled for this week", + "noMatchingTasks": "No tasks match your filters for this week", + "filter": { + "all": "All", + "todo": "To Do", + "inProgress": "In Progress", + "done": "Done", + "overdue": "Overdue" + }, + "sort": { + "dueDate": "Due Date", + "priority": "Priority", + "title": "Title", + "status": "Status" + }, + "previous": "Previous", + "today": "Today", + "next": "Next", + "tasksFor": "Tasks for {{start}} - {{end}}" + }, "taskCreation": { "title": "New Task", "titlePlaceholder": "What needs to be done?", diff --git a/client/src/pages/week-list.tsx b/client/src/pages/week-list.tsx new file mode 100644 index 0000000..444ca19 --- /dev/null +++ b/client/src/pages/week-list.tsx @@ -0,0 +1,124 @@ +import { useQuery, useMutation } from '@tanstack/react-query'; +import { Task } from '@shared/schema'; +import WeekListView from '@/components/WeekListView'; +import { queryClient } from '@/lib/queryClient'; +import { useToast } from '@/hooks/use-toast'; +import { useState } from 'react'; +import TaskDetailsModal from '@/components/TaskDetailsModal'; + +export default function WeekListPage() { + const { toast } = useToast(); + const [editingTask, setEditingTask] = useState(null); + + // Fetch all tasks + const { data: tasks = [] } = useQuery({ + queryKey: ['/api/tasks'], + }); + + // Update task mutation + const updateTaskMutation = useMutation({ + mutationFn: async ({ taskId, updates }: { taskId: string; updates: Partial }) => { + const response = await fetch(`/api/tasks/${taskId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }); + if (!response.ok) throw new Error('Failed to update task'); + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }); + toast({ + title: 'Task updated', + description: 'Task has been updated successfully', + }); + }, + onError: (error) => { + console.error('Error updating task:', error); + toast({ + title: 'Error', + description: 'Failed to update task', + variant: 'destructive', + }); + }, + }); + + // Delete task mutation + const deleteTaskMutation = useMutation({ + mutationFn: async (taskId: string) => { + const response = await fetch(`/api/tasks/${taskId}`, { + method: 'DELETE', + }); + if (!response.ok) throw new Error('Failed to delete task'); + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }); + toast({ + title: 'Task deleted', + description: 'Task has been deleted successfully', + }); + }, + onError: (error) => { + console.error('Error deleting task:', error); + toast({ + title: 'Error', + description: 'Failed to delete task', + variant: 'destructive', + }); + }, + }); + + const handleTaskUpdate = (taskId: string, updates: Partial) => { + updateTaskMutation.mutate({ taskId, updates }); + }; + + const handleTaskEdit = (task: Task) => { + setEditingTask(task); + }; + + const handleTaskDelete = (taskId: string) => { + deleteTaskMutation.mutate(taskId); + }; + + const handleStartTimer = (taskId: string) => { + // Stop all other timers + const runningTasks = tasks.filter(t => t.isTracking && t.id !== taskId); + runningTasks.forEach(t => { + updateTaskMutation.mutate({ taskId: t.id, updates: { isTracking: false } }); + }); + + // Start this timer + updateTaskMutation.mutate({ taskId, updates: { isTracking: true } }); + }; + + const handleStopTimer = (taskId: string) => { + updateTaskMutation.mutate({ taskId, updates: { isTracking: false } }); + }; + + return ( +
+ + + {/* Task Edit Modal */} + setEditingTask(null)} + onSave={(updatedTask: Task) => { + if (editingTask) { + handleTaskUpdate(editingTask.id, updatedTask); + setEditingTask(null); + } + }} + /> +
+ ); +}