From fe0aa91ca9ef1585c1616abfe711d814f07713a4 Mon Sep 17 00:00:00 2001 From: ghaddaditw <40211818-ghaddaditw@users.noreply.replit.com> Date: Sun, 8 Jun 2025 00:05:48 +0000 Subject: [PATCH] Improve tasks management with enhanced filtering, sorting, and editing Fixes data corruption and enhances task management in FinancesPage and TasksPage components with query updates and filtering/sorting. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ff0be73b-afdd-4747-978b-bb8301fb0a82 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/b26ef047-843d-4455-a9b2-1b44a2987682.jpg --- client/src/pages/FinancesPage.tsx | 7 +- client/src/pages/TasksPage.tsx | 691 ++++++++++++++++++++++++------ 2 files changed, 560 insertions(+), 138 deletions(-) diff --git a/client/src/pages/FinancesPage.tsx b/client/src/pages/FinancesPage.tsx index 978401a..994016e 100644 --- a/client/src/pages/FinancesPage.tsx +++ b/client/src/pages/FinancesPage.tsx @@ -23,14 +23,17 @@ export default function FinancesPage() { category: "", }); - const { data: records = [], isLoading: recordsLoading } = useQuery({ + const { data: recordsResponse, isLoading: recordsLoading } = useQuery({ queryKey: ["/api/financial/records"], }); - const { data: summary = { income: 0, expenses: 0, net: 0 }, isLoading: summaryLoading } = useQuery({ + const { data: summaryResponse, isLoading: summaryLoading } = useQuery({ queryKey: ["/api/financial/summary"], }); + const records = (recordsResponse as any)?.records || []; + const summary = (summaryResponse as any)?.summary || { income: 0, expenses: 0, net: 0 }; + const createRecordMutation = useMutation({ mutationFn: (record: typeof newRecord) => apiRequest("/api/financial/records", { diff --git a/client/src/pages/TasksPage.tsx b/client/src/pages/TasksPage.tsx index d8f4bd6..20f85c8 100644 --- a/client/src/pages/TasksPage.tsx +++ b/client/src/pages/TasksPage.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useMemo } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -7,33 +7,106 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; import { useToast } from "@/hooks/use-toast"; import Sidebar from "@/components/layout/Sidebar"; -import { Plus, Calendar, Clock, AlertCircle } from "lucide-react"; +import { + Plus, Calendar, Clock, Search, Filter, Trash2, CheckCircle2, + PlayCircle, MoreHorizontal, Edit, ArrowUpDown, AlertTriangle +} from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; import { apiRequest } from "@/lib/queryClient"; +interface Task { + id: number; + title: string; + description?: string; + priority: "low" | "medium" | "high"; + status: "pending" | "in_progress" | "completed"; + dueDate?: string; + createdAt: string; + updatedAt: string; +} + export default function TasksPage() { const { toast } = useToast(); const queryClient = useQueryClient(); const [open, setOpen] = useState(false); + const [editingTask, setEditingTask] = useState(null); + const [selectedTasks, setSelectedTasks] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [priorityFilter, setPriorityFilter] = useState("all"); + const [sortBy, setSortBy] = useState("createdAt"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); const [newTask, setNewTask] = useState({ title: "", description: "", - priority: "medium", + priority: "medium" as const, dueDate: "", }); - const { data: tasks = [], isLoading } = useQuery({ + const { data: tasksResponse, isLoading } = useQuery({ queryKey: ["/api/tasks"], }); + const allTasks: Task[] = (tasksResponse as any)?.tasks || []; + + // Enhanced filtering and sorting + const filteredAndSortedTasks = useMemo(() => { + let filtered = allTasks.filter((task) => { + const matchesSearch = task.title.toLowerCase().includes(searchQuery.toLowerCase()) || + (task.description?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false); + const matchesStatus = statusFilter === "all" || task.status === statusFilter; + const matchesPriority = priorityFilter === "all" || task.priority === priorityFilter; + return matchesSearch && matchesStatus && matchesPriority; + }); + + filtered.sort((a, b) => { + let aValue, bValue; + switch (sortBy) { + case "title": + aValue = a.title.toLowerCase(); + bValue = b.title.toLowerCase(); + break; + case "priority": + const priorityOrder = { low: 1, medium: 2, high: 3 }; + aValue = priorityOrder[a.priority]; + bValue = priorityOrder[b.priority]; + break; + case "dueDate": + aValue = a.dueDate ? new Date(a.dueDate).getTime() : Infinity; + bValue = b.dueDate ? new Date(b.dueDate).getTime() : Infinity; + break; + default: + aValue = new Date(a.createdAt).getTime(); + bValue = new Date(b.createdAt).getTime(); + } + + if (sortOrder === "asc") { + return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + } else { + return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; + } + }); + + return filtered; + }, [allTasks, searchQuery, statusFilter, priorityFilter, sortBy, sortOrder]); + const createTaskMutation = useMutation({ - mutationFn: (task: typeof newTask) => - apiRequest("/api/tasks", { + mutationFn: async (task: typeof newTask) => { + const response = await fetch("/api/tasks", { method: "POST", + headers: { + "Content-Type": "application/json", + }, body: JSON.stringify(task), - }), + credentials: "include", + }); + if (!response.ok) throw new Error("Failed to create task"); + return response.json(); + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); setOpen(false); @@ -53,16 +126,67 @@ export default function TasksPage() { }); const updateTaskMutation = useMutation({ - mutationFn: ({ id, status }: { id: number; status: string }) => - apiRequest(`/api/tasks/${id}`, { + mutationFn: async ({ id, updates }: { id: number; updates: Partial }) => { + const response = await fetch(`/api/tasks/${id}`, { method: "PATCH", - body: JSON.stringify({ status }), - }), + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updates), + credentials: "include", + }); + if (!response.ok) throw new Error("Failed to update task"); + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); + setEditingTask(null); + toast({ + title: "Task updated", + description: "Task has been successfully updated.", + }); + }, + }); + + const deleteTaskMutation = useMutation({ + mutationFn: async (id: number) => { + const response = await fetch(`/api/tasks/${id}`, { + method: "DELETE", + credentials: "include", + }); + if (!response.ok) throw new Error("Failed to delete task"); + return response.json(); + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/tasks"] }); toast({ - title: "Task updated", - description: "Task status has been updated.", + title: "Task deleted", + description: "Task has been successfully deleted.", + }); + }, + }); + + const bulkUpdateMutation = useMutation({ + mutationFn: async ({ ids, updates }: { ids: number[]; updates: Partial }) => { + 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.`, }); }, }); @@ -72,6 +196,29 @@ export default function TasksPage() { createTaskMutation.mutate(newTask); }; + const handleEditTask = (e: React.FormEvent) => { + e.preventDefault(); + if (editingTask) { + updateTaskMutation.mutate({ id: editingTask.id, updates: editingTask }); + } + }; + + const handleSelectTask = (taskId: number, checked: boolean) => { + if (checked) { + setSelectedTasks([...selectedTasks, taskId]); + } else { + setSelectedTasks(selectedTasks.filter(id => id !== taskId)); + } + }; + + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedTasks(filteredAndSortedTasks.map(task => task.id)); + } else { + setSelectedTasks([]); + } + }; + const getPriorityColor = (priority: string) => { switch (priority) { case "high": return "destructive"; @@ -90,6 +237,21 @@ export default function TasksPage() { } }; + const isOverdue = (dueDate: string) => { + return new Date(dueDate) < new Date(); + }; + + // Task statistics + 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) { return (
@@ -114,49 +276,375 @@ export default function TasksPage() {
-
-
-

Tasks

-

- Manage your tasks and track your progress -

+ {/* Header with Stats */} +
+
+
+

Tasks

+

+ Manage your tasks and track your progress +

+
+ + + + + + + Create New Task + + Add a new task to your todo list. + + +
+
+ + setNewTask({ ...newTask, title: e.target.value })} + required + /> +
+
+ +