Improve user experience for financial records and task management pages

Refactors FinancesPage to use controlled form, adds form validation; TasksPage refactors imports, introduces dialogs.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: d7e7c4e8-20cb-41c4-9d0e-79f48938fede
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/f798e6c9-89b6-4ae7-b440-7fbba0f6e6c3.jpg
This commit is contained in:
ghaddaditw
2025-06-08 15:29:03 +00:00
parent 02032aa941
commit f98f7cf1fc
2 changed files with 315 additions and 385 deletions
+51 -6
View File
@@ -123,18 +123,63 @@ export default function FinancesPage() {
Add Record Add Record
</Button> </Button>
</DialogTrigger> </DialogTrigger>
</Dialog>
</PageHeader>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Add Financial Record</DialogTitle> <DialogTitle>Add Financial Record</DialogTitle>
<DialogDescription> <DialogDescription>
Add a new income or expense record. Record a new income or expense transaction.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleCreateRecord} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="amount">Amount</Label>
<Input
id="amount"
type="number"
step="0.01"
value={formData.amount}
onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="type">Type</Label>
<Select value={formData.type} onValueChange={(value) => setFormData({ ...formData, type: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="income">Income</SelectItem>
<SelectItem value="expense">Expense</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<Input
id="category"
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "Adding..." : "Add Record"}
</Button>
</form>
</DialogContent>
</Dialog>
</PageHeader>
<div className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="type">Type</Label> <Label htmlFor="type">Type</Label>
<Select value={newRecord.type} onValueChange={(value) => setNewRecord({ ...newRecord, type: value, category: "" })}> <Select value={newRecord.type} onValueChange={(value) => setNewRecord({ ...newRecord, type: value, category: "" })}>
+262 -377
View File
@@ -1,28 +1,34 @@
import { useState, useMemo, useEffect } from "react"; import { useState, useEffect, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from "react-i18next"; import { useTranslation } from 'react-i18next';
import { useVoiceIntegration } from "@/hooks/useVoiceIntegration"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { VoiceShortcuts } from "@/components/voice/VoiceShortcuts"; import { Button } from '@/components/ui/button';
import { ProjectManager } from "@/components/tasks/ProjectManager"; import { ProjectManager } from '@/components/tasks/ProjectManager';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from '@/components/ui/input';
import { Button } from "@/components/ui/button"; import { Label } from '@/components/ui/label';
import { Input } from "@/components/ui/input"; import { Textarea } from '@/components/ui/textarea';
import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Badge } from '@/components/ui/badge';
import { Badge } from "@/components/ui/badge"; import { PageHeader } from '@/components/ui/page-header';
import { Checkbox } from "@/components/ui/checkbox"; import { useToast } from '@/hooks/use-toast';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import VoiceShortcuts from '@/components/voice/VoiceShortcuts';
import { PageHeader } from "@/components/ui/page-header"; import Sidebar from '@/components/layout/Sidebar';
import { useToast } from "@/hooks/use-toast";
import Sidebar from "@/components/layout/Sidebar";
import { import {
Plus, Calendar, Clock, Search, Filter, Trash2, CheckCircle2, Plus,
PlayCircle, MoreHorizontal, Edit, ArrowUpDown, AlertTriangle, FolderOpen Search,
} from "lucide-react"; Filter,
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; FolderOpen,
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; Calendar,
import { apiRequest } from "@/lib/queryClient"; Clock,
CheckCircle2,
Circle,
ArrowUpDown,
MoreHorizontal,
Edit,
Trash2
} from 'lucide-react';
import { apiRequest } from '@/lib/queryClient';
interface Task { interface Task {
id: number; id: number;
@@ -38,10 +44,8 @@ interface Task {
export default function TasksPage() { export default function TasksPage() {
const { toast } = useToast(); const { toast } = useToast();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { speak } = useVoiceIntegration();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editingTask, setEditingTask] = useState<Task | null>(null); const [editingTask, setEditingTask] = useState<Task | null>(null);
const [selectedTasks, setSelectedTasks] = useState<number[]>([]);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");
const [priorityFilter, setPriorityFilter] = useState("all"); const [priorityFilter, setPriorityFilter] = useState("all");
@@ -51,54 +55,51 @@ export default function TasksPage() {
title: "", title: "",
description: "", description: "",
priority: "medium" as const, priority: "medium" as const,
dueDate: "", dueDate: ""
}); });
const { data: tasksResponse, isLoading } = useQuery({ // Debounced search
queryKey: ["/api/tasks"], const [debouncedSearch, setDebouncedSearch] = useState("");
});
const allTasks: Task[] = (tasksResponse as any)?.tasks || [];
// Voice announcement when page loads
useEffect(() => { useEffect(() => {
if (allTasks.length > 0) { const timer = setTimeout(() => {
const timer = setTimeout(() => { setDebouncedSearch(searchQuery);
speak(`Tasks page loaded. You have ${allTasks.length} tasks. Say "create task" followed by a task name to add a new task.`); }, 300);
}, 1000); return () => clearTimeout(timer);
return () => clearTimeout(timer); }, [searchQuery]);
}
}, [allTasks.length, speak]);
// Enhanced filtering and sorting // Filter and sort tasks
const filteredAndSortedTasks = useMemo(() => { const filteredAndSortedTasks = useMemo(() => {
let filtered = allTasks.filter((task) => { if (!tasks) return [];
const matchesSearch = task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
(task.description?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false); const filtered = tasks.filter((task: Task) => {
const matchesSearch = task.title.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
task.description?.toLowerCase().includes(debouncedSearch.toLowerCase());
const matchesStatus = statusFilter === "all" || task.status === statusFilter; const matchesStatus = statusFilter === "all" || task.status === statusFilter;
const matchesPriority = priorityFilter === "all" || task.priority === priorityFilter; const matchesPriority = priorityFilter === "all" || task.priority === priorityFilter;
return matchesSearch && matchesStatus && matchesPriority; return matchesSearch && matchesStatus && matchesPriority;
}); });
filtered.sort((a, b) => { return filtered.sort((a: Task, b: Task) => {
let aValue, bValue; let aValue: any, bValue: any;
switch (sortBy) { switch (sortBy) {
case "title": case "title":
aValue = a.title.toLowerCase(); aValue = a.title.toLowerCase();
bValue = b.title.toLowerCase(); bValue = b.title.toLowerCase();
break; break;
case "priority": case "priority":
const priorityOrder = { low: 1, medium: 2, high: 3 }; const priorityOrder = { high: 3, medium: 2, low: 1 };
aValue = priorityOrder[a.priority]; aValue = priorityOrder[a.priority];
bValue = priorityOrder[b.priority]; bValue = priorityOrder[b.priority];
break; break;
case "dueDate": case "dueDate":
aValue = a.dueDate ? new Date(a.dueDate).getTime() : Infinity; aValue = a.dueDate ? new Date(a.dueDate) : new Date('9999-12-31');
bValue = b.dueDate ? new Date(b.dueDate).getTime() : Infinity; bValue = b.dueDate ? new Date(b.dueDate) : new Date('9999-12-31');
break; break;
default: default:
aValue = new Date(a.createdAt).getTime(); aValue = new Date(a.createdAt);
bValue = new Date(b.createdAt).getTime(); bValue = new Date(b.createdAt);
} }
if (sortOrder === "asc") { if (sortOrder === "asc") {
@@ -109,133 +110,115 @@ export default function TasksPage() {
}); });
return filtered; return filtered;
}, [allTasks, searchQuery, statusFilter, priorityFilter, sortBy, sortOrder]); }, [tasks, debouncedSearch, statusFilter, priorityFilter, sortBy, sortOrder]);
// Fetch tasks
const { data: tasks, isLoading } = useQuery({
queryKey: ['/api/tasks'],
queryFn: async () => {
const response = await fetch('/api/tasks');
if (!response.ok) throw new Error('Failed to fetch tasks');
return response.json();
}
});
// Create task mutation
const createTaskMutation = useMutation({ const createTaskMutation = useMutation({
mutationFn: async (task: typeof newTask) => { mutationFn: async (taskData: typeof newTask) => {
const response = await fetch("/api/tasks", { const response = await fetch('/api/tasks', {
method: "POST", method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
"Content-Type": "application/json", body: JSON.stringify(taskData)
},
body: JSON.stringify(task),
credentials: "include",
}); });
if (!response.ok) throw new Error("Failed to create task"); if (!response.ok) throw new Error('Failed to create task');
return response.json(); return response.json();
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
setOpen(false); setOpen(false);
setNewTask({ title: "", description: "", priority: "medium", dueDate: "" }); setNewTask({ title: "", description: "", priority: "medium", dueDate: "" });
toast({ toast({ title: "Task created successfully!" });
title: "Task created",
description: "Your task has been successfully created.",
});
}, },
onError: () => { onError: () => {
toast({ toast({ title: "Failed to create task", variant: "destructive" });
title: "Error", }
description: "Failed to create task. Please try again.",
variant: "destructive",
});
},
}); });
// Update task mutation
const updateTaskMutation = useMutation({ const updateTaskMutation = useMutation({
mutationFn: async ({ id, updates }: { id: number; updates: Partial<Task> }) => { mutationFn: async ({ id, ...updates }: Partial<Task> & { id: number }) => {
const response = await fetch(`/api/tasks/${id}`, { const response = await fetch(`/api/tasks/${id}`, {
method: "PATCH", method: 'PATCH',
headers: { headers: { 'Content-Type': 'application/json' },
"Content-Type": "application/json", body: JSON.stringify(updates)
},
body: JSON.stringify(updates),
credentials: "include",
}); });
if (!response.ok) throw new Error("Failed to update task"); if (!response.ok) throw new Error('Failed to update task');
return response.json(); return response.json();
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
setEditingTask(null); setEditingTask(null);
toast({ toast({ title: "Task updated successfully!" });
title: "Task updated",
description: "Task has been successfully updated.",
});
}, },
onError: () => {
toast({ title: "Failed to update task", variant: "destructive" });
}
}); });
// Delete task mutation
const deleteTaskMutation = useMutation({ const deleteTaskMutation = useMutation({
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const response = await fetch(`/api/tasks/${id}`, { const response = await fetch(`/api/tasks/${id}`, {
method: "DELETE", method: 'DELETE'
credentials: "include",
}); });
if (!response.ok) throw new Error("Failed to delete task"); if (!response.ok) throw new Error('Failed to delete task');
return response.json(); return response.json();
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
toast({ toast({ title: "Task deleted successfully!" });
title: "Task deleted",
description: "Task has been successfully deleted.",
});
},
});
const bulkUpdateMutation = useMutation({
mutationFn: async ({ ids, updates }: { ids: number[]; updates: Partial<Task> }) => {
await Promise.all(
ids.map(id =>
fetch(`/api/tasks/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updates),
credentials: "include",
})
)
);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
setSelectedTasks([]);
toast({
title: "Tasks updated",
description: `${selectedTasks.length} tasks have been updated.`,
});
}, },
onError: () => {
toast({ title: "Failed to delete task", variant: "destructive" });
}
}); });
const handleCreateTask = (e: React.FormEvent) => { const handleCreateTask = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
createTaskMutation.mutate(newTask); createTaskMutation.mutate(newTask);
speak(`Creating task "${newTask.title}"`);
}; };
const handleEditTask = (e: React.FormEvent) => { const handleEditTask = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (editingTask) { if (editingTask) {
updateTaskMutation.mutate({ id: editingTask.id, updates: editingTask }); updateTaskMutation.mutate(editingTask);
} }
}; };
const handleSelectTask = (taskId: number, checked: boolean) => { const handleStatusChange = (taskId: number, newStatus: Task['status']) => {
if (checked) { updateTaskMutation.mutate({ id: taskId, status: newStatus });
setSelectedTasks([...selectedTasks, taskId]); };
} else {
setSelectedTasks(selectedTasks.filter(id => id !== taskId)); const handleDeleteTask = (taskId: number) => {
if (confirm('Are you sure you want to delete this task?')) {
deleteTaskMutation.mutate(taskId);
} }
}; };
const handleSelectAll = (checked: boolean) => { // Task statistics
if (checked) { const taskStats = useMemo(() => {
setSelectedTasks(filteredAndSortedTasks.map(task => task.id)); if (!tasks) return { total: 0, pending: 0, in_progress: 0, completed: 0, overdue: 0 };
} else {
setSelectedTasks([]); const stats = {
} total: tasks.length,
}; pending: tasks.filter((t: Task) => t.status === 'pending').length,
in_progress: tasks.filter((t: Task) => t.status === 'in_progress').length,
completed: tasks.filter((t: Task) => t.status === 'completed').length,
overdue: tasks.filter((t: Task) => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== 'completed').length
};
return stats;
}, [tasks]);
const getPriorityColor = (priority: string) => { const getPriorityColor = (priority: string) => {
switch (priority) { switch (priority) {
@@ -255,31 +238,23 @@ export default function TasksPage() {
} }
}; };
const isOverdue = (dueDate: string) => { const isOverdue = (dueDate?: string) => {
if (!dueDate) return false;
return new Date(dueDate) < new Date(); return new Date(dueDate) < new Date();
}; };
// Task statistics // Loading state
const taskStats = useMemo(() => {
return {
total: allTasks.length,
pending: allTasks.filter(t => t.status === "pending").length,
inProgress: allTasks.filter(t => t.status === "in_progress").length,
completed: allTasks.filter(t => t.status === "completed").length,
overdue: allTasks.filter(t => t.dueDate && isOverdue(t.dueDate) && t.status !== "completed").length,
};
}, [allTasks]);
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex h-screen bg-gray-50 dark:bg-gray-900"> <div className="flex h-screen bg-gray-50 dark:bg-gray-900">
<VoiceShortcuts page="tasks" />
<Sidebar className="w-64 border-r" /> <Sidebar className="w-64 border-r" />
<div className="flex-1 overflow-auto p-6"> <div className="flex-1 overflow-auto p-6">
<div className="animate-pulse space-y-4"> <div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4"></div> <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div> <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
<div className="space-y-3"> <div className="space-y-3">
{[1, 2, 3].map((i) => ( {[...Array(5)].map((_, i) => (
<div key={i} className="h-24 bg-gray-200 dark:bg-gray-700 rounded"></div> <div key={i} className="h-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
))} ))}
</div> </div>
@@ -365,259 +340,169 @@ export default function TasksPage() {
{/* Statistics Cards */} {/* Statistics Cards */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6"> <div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold">{taskStats.total}</div> <div className="text-2xl font-bold">{taskStats.total}</div>
<p className="text-xs text-muted-foreground">Total Tasks</p> <p className="text-xs text-muted-foreground">Total Tasks</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-yellow-600">{taskStats.pending}</div> <div className="text-2xl font-bold text-yellow-600">{taskStats.pending}</div>
<p className="text-xs text-muted-foreground">Pending</p> <p className="text-xs text-muted-foreground">Pending</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-blue-600">{taskStats.inProgress}</div> <div className="text-2xl font-bold text-blue-600">{taskStats.in_progress}</div>
<p className="text-xs text-muted-foreground">In Progress</p> <p className="text-xs text-muted-foreground">In Progress</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-green-600">{taskStats.completed}</div> <div className="text-2xl font-bold text-green-600">{taskStats.completed}</div>
<p className="text-xs text-muted-foreground">Completed</p> <p className="text-xs text-muted-foreground">Completed</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-red-600">{taskStats.overdue}</div> <div className="text-2xl font-bold text-red-600">{taskStats.overdue}</div>
<p className="text-xs text-muted-foreground">Overdue</p> <p className="text-xs text-muted-foreground">Overdue</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* Filters and Search */} {/* Filters and Search */}
<div className="flex flex-wrap gap-4 mb-6"> <div className="flex flex-wrap gap-4 mb-6">
<div className="relative flex-1 min-w-64"> <div className="flex-1 min-w-64">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <Input
<Input placeholder="Search tasks..."
placeholder="Search tasks..." value={searchQuery}
value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)}
onChange={(e) => setSearchQuery(e.target.value)} className="w-full"
className="pl-10" />
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
</SelectContent>
</Select>
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Priority</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="low">Low</SelectItem>
</SelectContent>
</Select>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="createdAt">Created Date</SelectItem>
<SelectItem value="dueDate">Due Date</SelectItem>
<SelectItem value="title">Title</SelectItem>
<SelectItem value="priority">Priority</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
>
<ArrowUpDown className="w-4 h-4" />
</Button>
</div> </div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
{/* Bulk Actions */} <SelectTrigger className="w-40">
{selectedTasks.length > 0 && ( <SelectValue placeholder="Status" />
<div className="flex items-center gap-4 mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg"> </SelectTrigger>
<span className="text-sm font-medium"> <SelectContent>
{selectedTasks.length} task{selectedTasks.length > 1 ? 's' : ''} selected <SelectItem value="all">All Status</SelectItem>
</span> <SelectItem value="pending">Pending</SelectItem>
<div className="flex gap-2"> <SelectItem value="in_progress">In Progress</SelectItem>
<Button <SelectItem value="completed">Completed</SelectItem>
size="sm" </SelectContent>
onClick={() => bulkUpdateMutation.mutate({ ids: selectedTasks, updates: { status: "completed" } })} </Select>
> <Select value={priorityFilter} onValueChange={setPriorityFilter}>
<CheckCircle2 className="w-4 h-4 mr-1" /> <SelectTrigger className="w-40">
Mark Complete <SelectValue placeholder="Priority" />
</Button> </SelectTrigger>
<Button <SelectContent>
size="sm" <SelectItem value="all">All Priority</SelectItem>
variant="outline" <SelectItem value="high">High</SelectItem>
onClick={() => bulkUpdateMutation.mutate({ ids: selectedTasks, updates: { status: "in_progress" } })} <SelectItem value="medium">Medium</SelectItem>
> <SelectItem value="low">Low</SelectItem>
<PlayCircle className="w-4 h-4 mr-1" /> </SelectContent>
Start Progress </Select>
</Button> <Select value={sortBy} onValueChange={setSortBy}>
<Button <SelectTrigger className="w-40">
size="sm" <SelectValue placeholder="Sort by" />
variant="destructive" </SelectTrigger>
onClick={() => { <SelectContent>
selectedTasks.forEach(id => deleteTaskMutation.mutate(id)); <SelectItem value="createdAt">Created Date</SelectItem>
setSelectedTasks([]); <SelectItem value="dueDate">Due Date</SelectItem>
}} <SelectItem value="title">Title</SelectItem>
> <SelectItem value="priority">Priority</SelectItem>
<Trash2 className="w-4 h-4 mr-1" /> </SelectContent>
Delete </Select>
</Button> <Button
</div> variant="outline"
</div> size="icon"
)} onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
>
<ArrowUpDown className="w-4 h-4" />
</Button>
</div> </div>
{/* Tasks List */} {/* Tasks List */}
<div className="space-y-4"> <div className="space-y-4">
{filteredAndSortedTasks.length === 0 ? ( {filteredAndSortedTasks.length === 0 ? (
<Card> <Card>
<CardContent className="p-6 text-center"> <CardContent className="p-8 text-center">
<Calendar className="w-12 h-12 mx-auto text-gray-400 mb-4" /> <div className="text-muted-foreground mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2"> {tasks?.length === 0 ? "No tasks yet" : "No tasks match your filters"}
{searchQuery || statusFilter !== "all" || priorityFilter !== "all" </div>
? "No tasks match your filters" <Button onClick={() => setOpen(true)}>
: "No tasks yet" <Plus className="w-4 h-4 mr-2" />
} Create Your First Task
</h3> </Button>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{searchQuery || statusFilter !== "all" || priorityFilter !== "all"
? "Try adjusting your search or filters."
: "Get started by creating your first task."
}
</p>
{!searchQuery && statusFilter === "all" && priorityFilter === "all" && (
<Button onClick={() => setOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Task
</Button>
)}
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<> <>
{/* Select All */} {filteredAndSortedTasks.map((task: Task) => (
<div className="flex items-center gap-3 p-3 border rounded-lg bg-white dark:bg-gray-800"> <Card key={task.id} className={`${isOverdue(task.dueDate) ? 'border-red-200 dark:border-red-800' : ''}`}>
<Checkbox <CardContent className="p-6">
checked={selectedTasks.length === filteredAndSortedTasks.length} <div className="flex items-start justify-between">
onCheckedChange={handleSelectAll} <div className="flex items-start space-x-4 flex-1">
/> <Button
<span className="text-sm font-medium"> variant="ghost"
Select all ({filteredAndSortedTasks.length} tasks) size="sm"
</span> onClick={() => handleStatusChange(task.id, task.status === 'completed' ? 'pending' : 'completed')}
</div> >
{task.status === 'completed' ? (
{/* Task Cards */} <CheckCircle2 className="w-5 h-5 text-green-600" />
{filteredAndSortedTasks.map((task) => ( ) : (
<Card key={task.id} className="hover:shadow-md transition-shadow"> <Circle className="w-5 h-5" />
<CardHeader> )}
<div className="flex items-start gap-3"> </Button>
<Checkbox <div className="flex-1">
checked={selectedTasks.includes(task.id)} <div className="flex items-center gap-2 mb-1">
onCheckedChange={(checked) => handleSelectTask(task.id, checked as boolean)} <h3 className={`font-medium ${task.status === 'completed' ? 'line-through text-muted-foreground' : ''}`}>
/>
<div className="flex-1">
<div className="flex justify-between items-start">
<div className="flex-1">
<CardTitle className="text-lg flex items-center gap-2">
{task.title} {task.title}
{task.dueDate && isOverdue(task.dueDate) && task.status !== "completed" && ( </h3>
<AlertTriangle className="w-4 h-4 text-red-500" />
)}
</CardTitle>
{task.description && (
<CardDescription className="mt-1">
{task.description}
</CardDescription>
)}
</div>
<div className="flex gap-2 ml-4">
<Badge variant={getPriorityColor(task.priority)}> <Badge variant={getPriorityColor(task.priority)}>
{task.priority} {task.priority}
</Badge> </Badge>
<Badge variant={getStatusColor(task.status)}> <Badge variant={getStatusColor(task.status)}>
{task.status.replace('_', ' ')} {task.status.replace('_', ' ')}
</Badge> </Badge>
<DropdownMenu> {isOverdue(task.dueDate) && task.status !== 'completed' && (
<DropdownMenuTrigger asChild> <Badge variant="destructive">Overdue</Badge>
<Button variant="ghost" size="sm"> )}
<MoreHorizontal className="w-4 h-4" /> </div>
</Button> {task.description && (
</DropdownMenuTrigger> <p className="text-sm text-muted-foreground mb-2">{task.description}</p>
<DropdownMenuContent align="end"> )}
<DropdownMenuItem onClick={() => setEditingTask(task)}> <div className="flex items-center gap-4 text-xs text-muted-foreground">
<Edit className="w-4 h-4 mr-2" /> {task.dueDate && (
Edit <div className="flex items-center gap-1">
</DropdownMenuItem> <Calendar className="w-3 h-3" />
<DropdownMenuSeparator /> Due: {new Date(task.dueDate).toLocaleDateString()}
{task.status !== "completed" && ( </div>
<DropdownMenuItem )}
onClick={() => updateTaskMutation.mutate({ id: task.id, updates: { status: "completed" } })} <div className="flex items-center gap-1">
> <Clock className="w-3 h-3" />
<CheckCircle2 className="w-4 h-4 mr-2" /> Created: {new Date(task.createdAt).toLocaleDateString()}
Mark Complete </div>
</DropdownMenuItem>
)}
{task.status === "pending" && (
<DropdownMenuItem
onClick={() => updateTaskMutation.mutate({ id: task.id, updates: { status: "in_progress" } })}
>
<PlayCircle className="w-4 h-4 mr-2" />
Start Progress
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => deleteTaskMutation.mutate(task.id)}
className="text-red-600"
>
<Trash2 className="w-4 h-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
</div> </div>
</div> <div className="flex items-center gap-2">
</CardHeader> <Button
<CardContent> variant="ghost"
<div className="flex justify-between items-center text-sm text-gray-600 dark:text-gray-300"> size="sm"
<div className="flex items-center gap-4"> onClick={() => setEditingTask(task)}
{task.dueDate && ( >
<div className={`flex items-center gap-1 ${ <Edit className="w-4 h-4" />
isOverdue(task.dueDate) && task.status !== "completed" </Button>
? "text-red-600 font-medium" <Button
: "" variant="ghost"
}`}> size="sm"
<Clock className="w-4 h-4" /> onClick={() => handleDeleteTask(task.id)}
Due: {new Date(task.dueDate).toLocaleDateString()} >
</div> <Trash2 className="w-4 h-4" />
)} </Button>
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
Created: {new Date(task.createdAt).toLocaleDateString()}
</div>
</div> </div>
</div> </div>
</CardContent> </CardContent>