Compare commits

..

3 Commits

Author SHA1 Message Date
paul-nothaft b21d13d580 Improve the app's visual appeal and user interaction
continuous-integration/drone/push Build is passing
Refactor UI components and update styling to enhance user experience and maintainability.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/nn5bWKE
2025-10-24 06:13:28 +00:00
paul-nothaft 814163b07f Add functionality to delete tasks with a confirmation dialog
Implement task deletion with optimistic UI updates, confirmation dialogs using Shadcn UI's AlertDialog, and API integration for backend task removal. Includes i18n support for delete confirmation messages.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/nn5bWKE
2025-10-24 06:12:45 +00:00
paul-nothaft 8fe1c66d70 Improve task management by adding translations for all app features
Implement i18n support by adding new translation keys for Calendar, Kanban Board, and TaskCard components, and update existing translations in de.json and en.json.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/nn5bWKE
2025-10-24 06:05:04 +00:00
7 changed files with 209 additions and 47 deletions
+75
View File
@@ -6,6 +6,16 @@ import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
// Components // Components
import BottomNavigation from './components/BottomNavigation'; import BottomNavigation from './components/BottomNavigation';
@@ -98,6 +108,7 @@ function App() {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false); const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false);
const [selectedTask, setSelectedTask] = useState<Task | null>(null); const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null);
// Fetch tasks and labels from server on mount // Fetch tasks and labels from server on mount
useEffect(() => { useEffect(() => {
@@ -229,6 +240,44 @@ function App() {
console.log('Task details saved:', updatedTask.title); console.log('Task details saved:', updatedTask.title);
}; };
const handleTaskDelete = async (taskId: string) => {
setDeleteTaskId(taskId);
};
const confirmTaskDelete = async () => {
if (!deleteTaskId) return;
// Optimistic deletion - remove from UI immediately
const taskToDelete = tasks.find(task => task.id === deleteTaskId);
setTasks(prev => prev.filter(task => task.id !== deleteTaskId));
// Close task details modal if the deleted task was open
if (selectedTask?.id === deleteTaskId) {
handleTaskDetailsClose();
}
setDeleteTaskId(null);
try {
const response = await fetch(`/api/tasks/${deleteTaskId}`, {
method: 'DELETE'
});
// 404 is acceptable - task doesn't exist on backend (seeded task)
if (!response.ok && response.status !== 404) {
throw new Error('Failed to delete task');
}
console.log('Task deleted:', deleteTaskId);
} catch (error) {
console.error('Error deleting task:', error);
// Restore task on error
if (taskToDelete) {
setTasks(prev => [...prev, taskToDelete]);
}
}
};
const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => { const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
console.log('Creating project from template:', { templateId, startDate, projectName }); console.log('Creating project from template:', { templateId, startDate, projectName });
// In a real app, this would create multiple tasks based on the template // In a real app, this would create multiple tasks based on the template
@@ -242,6 +291,7 @@ function App() {
tasks={tasks} tasks={tasks}
onTaskUpdate={handleTaskUpdate} onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick} onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer} onStartTimer={startTimer}
onStopTimer={stopTimer} onStopTimer={stopTimer}
/> />
@@ -264,6 +314,7 @@ function App() {
onTaskStatusChange={handleTaskStatusChange} onTaskStatusChange={handleTaskStatusChange}
onTaskUpdate={handleTaskUpdate} onTaskUpdate={handleTaskUpdate}
onTaskClick={handleTaskClick} onTaskClick={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer} onStartTimer={startTimer}
onStopTimer={stopTimer} onStopTimer={stopTimer}
/> />
@@ -354,6 +405,30 @@ function App() {
onSave={handleTaskDetailsSave} onSave={handleTaskDetailsSave}
labels={labels} labels={labels}
/> />
{/* Delete Confirmation Dialog */}
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>
<AlertDialogContent data-testid="dialog-delete-confirmation">
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteConfirmation.title')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteConfirmation.description')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-cancel-delete">
{t('deleteConfirmation.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmTaskDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
data-testid="button-confirm-delete"
>
{t('deleteConfirmation.confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
<Toaster /> <Toaster />
+13 -7
View File
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -6,6 +7,7 @@ import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react';
import { Task, Label } from '@shared/schema'; import { Task, Label } from '@shared/schema';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { addDays, format, isSameDay, startOfWeek, subWeeks } from 'date-fns'; import { addDays, format, isSameDay, startOfWeek, subWeeks } from 'date-fns';
import { de, enUS } from 'date-fns/locale';
interface CalendarViewProps { interface CalendarViewProps {
tasks: Task[]; tasks: Task[];
@@ -15,9 +17,13 @@ interface CalendarViewProps {
} }
export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSelect }: CalendarViewProps) { export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSelect }: CalendarViewProps) {
const { t, i18n } = useTranslation();
const [currentDate, setCurrentDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(new Date());
const [draggedTask, setDraggedTask] = useState<string | null>(null); const [draggedTask, setDraggedTask] = useState<string | null>(null);
// Get date-fns locale based on current language
const dateLocale = i18n.language === 'de' ? de : enUS;
// Fetch labels to get label colors // Fetch labels to get label colors
const { data: labels = [] } = useQuery<Label[]>({ const { data: labels = [] } = useQuery<Label[]>({
queryKey: ['/api/labels'], queryKey: ['/api/labels'],
@@ -71,7 +77,7 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar className="w-4 h-4 sm:w-5 sm:h-5 text-primary" /> <Calendar className="w-4 h-4 sm:w-5 sm:h-5 text-primary" />
<h2 className="text-base sm:text-lg font-semibold" data-testid="text-calendar-title"> <h2 className="text-base sm:text-lg font-semibold" data-testid="text-calendar-title">
Calendar {t('calendar.title')}
</h2> </h2>
</div> </div>
@@ -129,12 +135,12 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel
<div className={`text-xs font-medium ${ <div className={`text-xs font-medium ${
isPreviousWeek ? 'text-muted-foreground/60' : 'text-muted-foreground' isPreviousWeek ? 'text-muted-foreground/60' : 'text-muted-foreground'
}`}> }`}>
{format(date, 'EEE')} {format(date, 'EEE', { locale: dateLocale })}
</div> </div>
<div className={`text-sm font-semibold ${ <div className={`text-sm font-semibold ${
isToday ? 'text-primary' : isPreviousWeek ? 'text-muted-foreground/60' : '' isToday ? 'text-primary' : isPreviousWeek ? 'text-muted-foreground/60' : ''
}`}> }`}>
{format(date, 'd')} {format(date, 'd', { locale: dateLocale })}
</div> </div>
</div> </div>
@@ -178,7 +184,7 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel
{dayTasks.length > 3 && ( {dayTasks.length > 3 && (
<Badge variant="secondary" className="w-full justify-center text-xs"> <Badge variant="secondary" className="w-full justify-center text-xs">
+{dayTasks.length - 3} more {t('calendar.moreItems', { count: dayTasks.length - 3 })}
</Badge> </Badge>
)} )}
</div> </div>
@@ -191,15 +197,15 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel
<div className="flex items-center gap-4 text-xs text-muted-foreground"> <div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-primary"></div> <div className="w-3 h-3 rounded-full bg-primary"></div>
<span>Today</span> <span>{t('calendar.today')}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-muted"></div> <div className="w-3 h-3 rounded-full bg-muted"></div>
<span>Weekend</span> <span>{t('calendar.weekend')}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-muted opacity-50"></div> <div className="w-3 h-3 rounded-full bg-muted opacity-50"></div>
<span>Previous Week</span> <span>{t('calendar.previousWeek')}</span>
</div> </div>
</div> </div>
</div> </div>
+29 -23
View File
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -22,6 +23,7 @@ import { Task, Label } from '@shared/schema';
import TaskCard from './TaskCard'; import TaskCard from './TaskCard';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addWeeks, addMonths, isWithinInterval } from 'date-fns'; import { format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addWeeks, addMonths, isWithinInterval } from 'date-fns';
import { de, enUS } from 'date-fns/locale';
interface KanbanBoardProps { interface KanbanBoardProps {
tasks: Task[]; tasks: Task[];
@@ -36,6 +38,7 @@ interface KanbanBoardProps {
type ViewMode = 'traditional' | 'weekly' | 'monthly'; type ViewMode = 'traditional' | 'weekly' | 'monthly';
export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, onTaskClick, onTaskDelete, onStartTimer, onStopTimer }: KanbanBoardProps) { export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, onTaskClick, onTaskDelete, onStartTimer, onStopTimer }: KanbanBoardProps) {
const { t, i18n } = useTranslation();
const [viewMode, setViewMode] = useState<ViewMode>('traditional'); const [viewMode, setViewMode] = useState<ViewMode>('traditional');
const [currentDate, setCurrentDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(new Date());
const [draggedTask, setDraggedTask] = useState<string | null>(null); const [draggedTask, setDraggedTask] = useState<string | null>(null);
@@ -44,6 +47,9 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
const [selectedLabels, setSelectedLabels] = useState<string[]>([]); const [selectedLabels, setSelectedLabels] = useState<string[]>([]);
const [selectedPriorities, setSelectedPriorities] = useState<string[]>([]); const [selectedPriorities, setSelectedPriorities] = useState<string[]>([]);
// Get date-fns locale based on current language
const dateLocale = i18n.language === 'de' ? de : enUS;
// Fetch labels to get label colors // Fetch labels to get label colors
const { data: labels = [] } = useQuery<Label[]>({ const { data: labels = [] } = useQuery<Label[]>({
queryKey: ['/api/labels'], queryKey: ['/api/labels'],
@@ -52,9 +58,9 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
}); });
const columns = [ const columns = [
{ id: 'todo', title: 'To Do', status: 'todo' as const }, { id: 'todo', title: t('kanban.todo'), status: 'todo' as const },
{ id: 'inProgress', title: 'In Progress', status: 'inProgress' as const }, { id: 'inProgress', title: t('kanban.inProgress'), status: 'inProgress' as const },
{ id: 'done', title: 'Done', status: 'done' as const } { id: 'done', title: t('kanban.done'), status: 'done' as const }
]; ];
const getTasksForColumn = (status: Task['status']) => { const getTasksForColumn = (status: Task['status']) => {
@@ -124,11 +130,11 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
const getPeriodTitle = () => { const getPeriodTitle = () => {
if (viewMode === 'weekly') { if (viewMode === 'weekly') {
return `Week of ${format(startOfWeek(currentDate, { weekStartsOn: 1 }), 'MMM d')}`; return t('kanban.weekOf', { date: format(startOfWeek(currentDate, { weekStartsOn: 1 }), 'MMM d', { locale: dateLocale }) });
} else if (viewMode === 'monthly') { } else if (viewMode === 'monthly') {
return format(currentDate, 'MMMM yyyy'); return format(currentDate, 'MMMM yyyy', { locale: dateLocale });
} }
return 'All Tasks'; return t('kanban.allTasks');
}; };
// Filter management functions // Filter management functions
@@ -174,7 +180,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LayoutGrid className="w-4 h-4 sm:w-5 sm:h-5 text-primary" /> <LayoutGrid className="w-4 h-4 sm:w-5 sm:h-5 text-primary" />
<h2 className="text-base sm:text-lg font-semibold" data-testid="text-kanban-title"> <h2 className="text-base sm:text-lg font-semibold" data-testid="text-kanban-title">
Board {t('kanban.title')}
</h2> </h2>
</div> </div>
@@ -212,7 +218,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
<div className="flex items-center justify-between flex-wrap gap-3 sm:gap-4"> <div className="flex items-center justify-between flex-wrap gap-3 sm:gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-muted-foreground" /> <Filter className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">Filters</span> <span className="text-sm font-medium">{t('kanban.filters')}</span>
{hasActiveFilters && ( {hasActiveFilters && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
{selectedLabels.length + selectedPriorities.length} {selectedLabels.length + selectedPriorities.length}
@@ -232,7 +238,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
data-testid="button-filter-labels" data-testid="button-filter-labels"
> >
<Tag className="w-4 h-4 mr-2" /> <Tag className="w-4 h-4 mr-2" />
Labels {t('kanban.labels')}
{selectedLabels.length > 0 && ( {selectedLabels.length > 0 && (
<Badge variant="secondary" className="ml-2 text-xs"> <Badge variant="secondary" className="ml-2 text-xs">
{selectedLabels.length} {selectedLabels.length}
@@ -242,7 +248,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-56" align="start"> <PopoverContent className="w-56" align="start">
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-medium">Filter by Labels</h4> <h4 className="text-sm font-medium">{t('kanban.filterByLabels')}</h4>
<Separator /> <Separator />
<div className="space-y-2 max-h-48 overflow-y-auto"> <div className="space-y-2 max-h-48 overflow-y-auto">
{labels.length > 0 ? ( {labels.length > 0 ? (
@@ -270,7 +276,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
</div> </div>
)) ))
) : ( ) : (
<p className="text-sm text-muted-foreground">No labels available</p> <p className="text-sm text-muted-foreground">{t('kanban.noLabelsAvailable')}</p>
)} )}
</div> </div>
</div> </div>
@@ -289,7 +295,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
data-testid="button-filter-priorities" data-testid="button-filter-priorities"
> >
<AlertTriangle className="w-4 h-4 mr-2" /> <AlertTriangle className="w-4 h-4 mr-2" />
Priorities {t('kanban.priorities')}
{selectedPriorities.length > 0 && ( {selectedPriorities.length > 0 && (
<Badge variant="secondary" className="ml-2 text-xs"> <Badge variant="secondary" className="ml-2 text-xs">
{selectedPriorities.length} {selectedPriorities.length}
@@ -299,7 +305,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-48" align="start"> <PopoverContent className="w-48" align="start">
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-medium">Filter by Priority</h4> <h4 className="text-sm font-medium">{t('kanban.filterByPriority')}</h4>
<Separator /> <Separator />
<div className="space-y-2"> <div className="space-y-2">
{priorities.map((priority) => ( {priorities.map((priority) => (
@@ -321,7 +327,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
variant="secondary" variant="secondary"
className={`text-xs ${getPriorityColor(priority)}`} className={`text-xs ${getPriorityColor(priority)}`}
> >
{priority} {t(`priority.${priority}`)}
</Badge> </Badge>
</label> </label>
</div> </div>
@@ -344,7 +350,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
className="text-muted-foreground hover:text-foreground" className="text-muted-foreground hover:text-foreground"
> >
<X className="w-4 h-4 mr-2" /> <X className="w-4 h-4 mr-2" />
Clear All {t('kanban.clearAll')}
</Button> </Button>
</> </>
)} )}
@@ -355,7 +361,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
{hasActiveFilters && ( {hasActiveFilters && (
<div className="mt-3 pt-3 border-t"> <div className="mt-3 pt-3 border-t">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-muted-foreground">Active filters:</span> <span className="text-xs text-muted-foreground">{t('kanban.activeFilters')}</span>
{selectedLabels.map((labelId) => { {selectedLabels.map((labelId) => {
const label = labels.find(l => l.id === labelId); const label = labels.find(l => l.id === labelId);
return label ? ( return label ? (
@@ -384,7 +390,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
className={`text-xs ${getPriorityColor(priority)}`} className={`text-xs ${getPriorityColor(priority)}`}
data-testid={`active-filter-priority-${priority}`} data-testid={`active-filter-priority-${priority}`}
> >
{priority} {t(`priority.${priority}`)}
<X <X
className="w-3 h-3 ml-1 cursor-pointer" className="w-3 h-3 ml-1 cursor-pointer"
onClick={() => togglePriority(priority)} onClick={() => togglePriority(priority)}
@@ -402,15 +408,15 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="traditional" data-testid="tab-traditional"> <TabsTrigger value="traditional" data-testid="tab-traditional">
<LayoutGrid className="w-4 h-4 mr-2" /> <LayoutGrid className="w-4 h-4 mr-2" />
Traditional {t('kanban.traditional')}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="weekly" data-testid="tab-weekly"> <TabsTrigger value="weekly" data-testid="tab-weekly">
<CalendarIcon className="w-4 h-4 mr-2" /> <CalendarIcon className="w-4 h-4 mr-2" />
Weekly {t('kanban.weekly')}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="monthly" data-testid="tab-monthly"> <TabsTrigger value="monthly" data-testid="tab-monthly">
<CalendarIcon className="w-4 h-4 mr-2" /> <CalendarIcon className="w-4 h-4 mr-2" />
Monthly {t('kanban.monthly')}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -469,7 +475,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
{columnTasks.length === 0 && ( {columnTasks.length === 0 && (
<div className="text-center text-muted-foreground text-sm py-8"> <div className="text-center text-muted-foreground text-sm py-8">
No tasks in {column.title.toLowerCase()} {t('kanban.noTasksInColumn', { column: column.title.toLowerCase() })}
</div> </div>
)} )}
</div> </div>
@@ -533,7 +539,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
{columnTasks.length === 0 && ( {columnTasks.length === 0 && (
<div className="text-center text-muted-foreground text-sm py-8"> <div className="text-center text-muted-foreground text-sm py-8">
No tasks in {column.title.toLowerCase()} for this week {t('kanban.noTasksInColumnForWeek', { column: column.title.toLowerCase() })}
</div> </div>
)} )}
</div> </div>
@@ -597,7 +603,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
{columnTasks.length === 0 && ( {columnTasks.length === 0 && (
<div className="text-center text-muted-foreground text-sm py-8"> <div className="text-center text-muted-foreground text-sm py-8">
No tasks in {column.title.toLowerCase()} for this month {t('kanban.noTasksInColumnForMonth', { column: column.title.toLowerCase() })}
</div> </div>
)} )}
</div> </div>
+12 -10
View File
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -23,6 +24,7 @@ interface TaskCardProps {
} }
export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, isDragging }: TaskCardProps) { export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, isDragging }: TaskCardProps) {
const { t } = useTranslation();
// Fetch labels to get the label color // Fetch labels to get the label color
const { data: labels = [] } = useQuery<Label[]>({ const { data: labels = [] } = useQuery<Label[]>({
@@ -105,7 +107,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
className={`text-xs ${getPriorityColor(task.priority)}`} className={`text-xs ${getPriorityColor(task.priority)}`}
data-testid={`badge-priority-${task.id}`} data-testid={`badge-priority-${task.id}`}
> >
{task.priority} {t(`priority.${task.priority}`)}
</Badge> </Badge>
<Badge <Badge
@@ -113,7 +115,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
className={`text-xs ${getStatusColor(task.status)}`} className={`text-xs ${getStatusColor(task.status)}`}
data-testid={`badge-status-${task.id}`} data-testid={`badge-status-${task.id}`}
> >
{task.status.replace(/([A-Z])/g, ' $1').toLowerCase()} {t(`status.${task.status}`)}
</Badge> </Badge>
{task.dueDate && ( {task.dueDate && (
@@ -134,7 +136,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
className={task.isTracking ? 'text-primary font-medium' : 'text-muted-foreground'} className={task.isTracking ? 'text-primary font-medium' : 'text-muted-foreground'}
> >
{task.timeTracked > 0 ? formatTime(task.timeTracked) : '0m'} {task.timeTracked > 0 ? formatTime(task.timeTracked) : '0m'}
{task.isTracking && ' (running)'} {task.isTracking && ` (${t('taskCard.running')})`}
</span> </span>
</div> </div>
)} )}
@@ -163,7 +165,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
data-testid={`menu-edit-${task.id}`} data-testid={`menu-edit-${task.id}`}
> >
<Edit className="w-4 h-4 mr-2" /> <Edit className="w-4 h-4 mr-2" />
Edit Task {t('taskCard.editTask')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
@@ -174,7 +176,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
data-testid={`menu-timer-${task.id}`} data-testid={`menu-timer-${task.id}`}
> >
<Timer className="w-4 h-4 mr-2" /> <Timer className="w-4 h-4 mr-2" />
{task.isTracking ? 'Stop Timer' : 'Start Timer'} {task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@@ -189,7 +191,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
data-testid={`menu-complete-${task.id}`} data-testid={`menu-complete-${task.id}`}
> >
<CheckCircle className="w-4 h-4 mr-2" /> <CheckCircle className="w-4 h-4 mr-2" />
Mark as Done {t('taskCard.markAsDone')}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
@@ -203,7 +205,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
data-testid={`menu-start-${task.id}`} data-testid={`menu-start-${task.id}`}
> >
<Play className="w-4 h-4 mr-2" /> <Play className="w-4 h-4 mr-2" />
Start Working {t('taskCard.startWorking')}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
@@ -217,7 +219,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
data-testid={`menu-todo-${task.id}`} data-testid={`menu-todo-${task.id}`}
> >
<Clock className="w-4 h-4 mr-2" /> <Clock className="w-4 h-4 mr-2" />
Move to Todo {t('taskCard.moveToTodo')}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
@@ -231,7 +233,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
data-testid={`menu-reopen-${task.id}`} data-testid={`menu-reopen-${task.id}`}
> >
<Play className="w-4 h-4 mr-2" /> <Play className="w-4 h-4 mr-2" />
Reopen Task {t('taskCard.reopenTask')}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
@@ -248,7 +250,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
data-testid={`menu-delete-${task.id}`} data-testid={`menu-delete-${task.id}`}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
Delete Task {t('taskCard.deleteTask')}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</DropdownMenuContent> </DropdownMenuContent>
+6 -1
View File
@@ -15,6 +15,7 @@ interface TasksWithCalendarProps {
tasks: Task[]; tasks: Task[];
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void; onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
onTaskEdit?: (task: Task) => void; onTaskEdit?: (task: Task) => void;
onTaskDelete?: (taskId: string) => void;
onStartTimer?: (taskId: string) => void; onStartTimer?: (taskId: string) => void;
onStopTimer?: (taskId: string) => void; onStopTimer?: (taskId: string) => void;
} }
@@ -22,7 +23,7 @@ interface TasksWithCalendarProps {
type SortOption = 'dueDate' | 'priority' | 'title' | 'status'; type SortOption = 'dueDate' | 'priority' | 'title' | 'status';
type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue'; type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onStartTimer, onStopTimer }: TasksWithCalendarProps) { export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onTaskDelete, onStartTimer, onStopTimer }: TasksWithCalendarProps) {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<SortOption>('dueDate'); const [sortBy, setSortBy] = useState<SortOption>('dueDate');
const [filterBy, setFilterBy] = useState<FilterOption>('all'); const [filterBy, setFilterBy] = useState<FilterOption>('all');
@@ -304,6 +305,10 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onS
onTaskEdit?.(task); onTaskEdit?.(task);
console.log(`Edit task ${task.title}`); console.log(`Edit task ${task.title}`);
}} }}
onDelete={() => {
onTaskDelete?.(task.id);
console.log(`Delete task ${task.title}`);
}}
onStatusChange={(newStatus) => { onStatusChange={(newStatus) => {
if (newStatus === 'done') { if (newStatus === 'done') {
handleTaskComplete(task); handleTaskComplete(task);
+37 -3
View File
@@ -62,22 +62,50 @@
"play": "Timer starten", "play": "Timer starten",
"pause": "Timer pausieren", "pause": "Timer pausieren",
"edit": "Aufgabe bearbeiten", "edit": "Aufgabe bearbeiten",
"editTask": "Aufgabe bearbeiten",
"stopTimer": "Timer stoppen",
"startTimer": "Timer starten",
"markAsDone": "Als erledigt markieren",
"startWorking": "Mit Arbeit beginnen",
"moveToTodo": "Zu erledigen verschieben",
"reopenTask": "Aufgabe erneut öffnen",
"deleteTask": "Aufgabe löschen",
"running": "läuft",
"overdue": "Überfällig", "overdue": "Überfällig",
"timeTracked": "{{hours}}Std {{minutes}}Min erfasst" "timeTracked": "{{hours}}Std {{minutes}}Min erfasst"
}, },
"calendar": { "calendar": {
"title": "Kalenderansicht", "title": "Kalender",
"today": "Heute", "today": "Heute",
"weekend": "Wochenende",
"previousWeek": "Vorherige Woche",
"moreItems": "+{{count}} weitere",
"tasksDue": "{{count}} Aufgabe fällig", "tasksDue": "{{count}} Aufgabe fällig",
"tasksDue_plural": "{{count}} Aufgaben fällig" "tasksDue_plural": "{{count}} Aufgaben fällig"
}, },
"kanban": { "kanban": {
"title": "Kanban-Board", "title": "Board",
"todo": "Zu erledigen", "todo": "Zu erledigen",
"inProgress": "In Bearbeitung", "inProgress": "In Bearbeitung",
"done": "Erledigt", "done": "Erledigt",
"taskCount": "{{count}} Aufgabe", "taskCount": "{{count}} Aufgabe",
"taskCount_plural": "{{count}} Aufgaben" "taskCount_plural": "{{count}} Aufgaben",
"filters": "Filter",
"labels": "Labels",
"priorities": "Prioritäten",
"filterByLabels": "Nach Labels filtern",
"filterByPriority": "Nach Priorität filtern",
"clearAll": "Alle löschen",
"activeFilters": "Aktive Filter:",
"noLabelsAvailable": "Keine Labels verfügbar",
"traditional": "Traditionell",
"weekly": "Wöchentlich",
"monthly": "Monatlich",
"allTasks": "Alle Aufgaben",
"weekOf": "Woche vom {{date}}",
"noTasksInColumn": "Keine Aufgaben in {{column}}",
"noTasksInColumnForWeek": "Keine Aufgaben in {{column}} für diese Woche",
"noTasksInColumnForMonth": "Keine Aufgaben in {{column}} für diesen Monat"
}, },
"settings": { "settings": {
"title": "Einstellungen", "title": "Einstellungen",
@@ -145,5 +173,11 @@
"close": "Schließen", "close": "Schließen",
"loading": "Laden...", "loading": "Laden...",
"error": "Ein Fehler ist aufgetreten" "error": "Ein Fehler ist aufgetreten"
},
"deleteConfirmation": {
"title": "Aufgabe löschen",
"description": "Möchten Sie diese Aufgabe wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"cancel": "Abbrechen",
"confirm": "Löschen"
} }
} }
+37 -3
View File
@@ -62,22 +62,50 @@
"play": "Start timer", "play": "Start timer",
"pause": "Pause timer", "pause": "Pause timer",
"edit": "Edit task", "edit": "Edit task",
"editTask": "Edit Task",
"stopTimer": "Stop Timer",
"startTimer": "Start Timer",
"markAsDone": "Mark as Done",
"startWorking": "Start Working",
"moveToTodo": "Move to Todo",
"reopenTask": "Reopen Task",
"deleteTask": "Delete Task",
"running": "running",
"overdue": "Overdue", "overdue": "Overdue",
"timeTracked": "{{hours}}h {{minutes}}m tracked" "timeTracked": "{{hours}}h {{minutes}}m tracked"
}, },
"calendar": { "calendar": {
"title": "Calendar View", "title": "Calendar",
"today": "Today", "today": "Today",
"weekend": "Weekend",
"previousWeek": "Previous Week",
"moreItems": "+{{count}} more",
"tasksDue": "{{count}} task due", "tasksDue": "{{count}} task due",
"tasksDue_plural": "{{count}} tasks due" "tasksDue_plural": "{{count}} tasks due"
}, },
"kanban": { "kanban": {
"title": "Kanban Board", "title": "Board",
"todo": "To Do", "todo": "To Do",
"inProgress": "In Progress", "inProgress": "In Progress",
"done": "Done", "done": "Done",
"taskCount": "{{count}} task", "taskCount": "{{count}} task",
"taskCount_plural": "{{count}} tasks" "taskCount_plural": "{{count}} tasks",
"filters": "Filters",
"labels": "Labels",
"priorities": "Priorities",
"filterByLabels": "Filter by Labels",
"filterByPriority": "Filter by Priority",
"clearAll": "Clear All",
"activeFilters": "Active filters:",
"noLabelsAvailable": "No labels available",
"traditional": "Traditional",
"weekly": "Weekly",
"monthly": "Monthly",
"allTasks": "All Tasks",
"weekOf": "Week of {{date}}",
"noTasksInColumn": "No tasks in {{column}}",
"noTasksInColumnForWeek": "No tasks in {{column}} for this week",
"noTasksInColumnForMonth": "No tasks in {{column}} for this month"
}, },
"settings": { "settings": {
"title": "Settings", "title": "Settings",
@@ -145,5 +173,11 @@
"close": "Close", "close": "Close",
"loading": "Loading...", "loading": "Loading...",
"error": "An error occurred" "error": "An error occurred"
},
"deleteConfirmation": {
"title": "Delete Task",
"description": "Are you sure you want to delete this task? This action cannot be undone.",
"cancel": "Cancel",
"confirm": "Delete"
} }
} }