Update app to fetch and display tasks and labels from the server
Introduce useEffect to fetch tasks and labels on component mount, normalize date data, and implement task editing functionality in the modal. 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/XZ04DPQ
This commit is contained in:
+99
-22
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { queryClient } from "./lib/queryClient";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
@@ -15,7 +15,7 @@ import CalendarView from './components/CalendarView';
|
||||
import KanbanBoard from './components/KanbanBoard';
|
||||
import ProjectTemplate from './components/ProjectTemplate';
|
||||
import ThemeToggle from './components/ThemeToggle';
|
||||
import { Task } from '@shared/schema';
|
||||
import { Task, Label } from '@shared/schema';
|
||||
import { addDays, subDays } from 'date-fns';
|
||||
import { useTimer } from './hooks/useTimer';
|
||||
|
||||
@@ -91,33 +91,109 @@ const initialTasks: Task[] = [
|
||||
function App() {
|
||||
const [currentTab, setCurrentTab] = useState('tasks');
|
||||
const [tasks, setTasks] = useState<Task[]>(initialTasks);
|
||||
const [labels, setLabels] = useState<Label[]>([]);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||
|
||||
const handleCreateTask = (newTask: Partial<Task>) => {
|
||||
const task: Task = {
|
||||
id: Date.now().toString(),
|
||||
title: newTask.title || '',
|
||||
description: newTask.description || null,
|
||||
status: 'todo',
|
||||
priority: newTask.priority || 'medium',
|
||||
dueDate: newTask.dueDate || null,
|
||||
timeTracked: 0,
|
||||
isTracking: false,
|
||||
projectId: newTask.projectId || null,
|
||||
notes: newTask.notes || null,
|
||||
labelId: newTask.labelId || null
|
||||
// Fetch tasks and labels from server on mount
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch tasks
|
||||
const tasksResponse = await fetch('/api/tasks');
|
||||
if (tasksResponse.ok) {
|
||||
const serverTasks: Task[] = await tasksResponse.json();
|
||||
// Normalize server tasks - convert date strings to Date objects
|
||||
const normalizedTasks = serverTasks.map(task => ({
|
||||
...task,
|
||||
dueDate: task.dueDate ? new Date(task.dueDate) : null
|
||||
}));
|
||||
// Merge server tasks with initial tasks (avoiding duplicates)
|
||||
const existingIds = new Set(normalizedTasks.map(t => t.id));
|
||||
const uniqueInitialTasks = initialTasks.filter(t => !existingIds.has(t.id));
|
||||
setTasks([...normalizedTasks, ...uniqueInitialTasks]);
|
||||
}
|
||||
|
||||
// Fetch labels
|
||||
const labelsResponse = await fetch('/api/labels');
|
||||
if (labelsResponse.ok) {
|
||||
const serverLabels: Label[] = await labelsResponse.json();
|
||||
setLabels(serverLabels);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
// Keep initial data on error
|
||||
}
|
||||
};
|
||||
setTasks(prev => [...prev, task]);
|
||||
console.log('Task created:', task);
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const handleCreateTask = async (newTask: Partial<Task>) => {
|
||||
try {
|
||||
const response = await fetch('/api/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: newTask.title || '',
|
||||
description: newTask.description || null,
|
||||
status: 'todo',
|
||||
priority: newTask.priority || 'medium',
|
||||
dueDate: newTask.dueDate || null,
|
||||
timeTracked: 0,
|
||||
isTracking: false,
|
||||
projectId: newTask.projectId || null,
|
||||
notes: newTask.notes || null,
|
||||
labelId: newTask.labelId || null
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create task');
|
||||
}
|
||||
|
||||
const createdTask: Task = await response.json();
|
||||
// Normalize the created task's date
|
||||
const normalizedTask = {
|
||||
...createdTask,
|
||||
dueDate: createdTask.dueDate ? new Date(createdTask.dueDate) : null
|
||||
};
|
||||
setTasks(prev => [...prev, normalizedTask]);
|
||||
console.log('Task created:', normalizedTask);
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTaskUpdate = (taskId: string, updates: Partial<Task>) => {
|
||||
setTasks(prev => prev.map(task =>
|
||||
task.id === taskId ? { ...task, ...updates } : task
|
||||
));
|
||||
console.log('Task updated:', taskId, updates);
|
||||
const handleTaskUpdate = async (taskId: string, updates: Partial<Task>) => {
|
||||
try {
|
||||
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');
|
||||
}
|
||||
|
||||
const updatedTask: Task = await response.json();
|
||||
// Normalize the updated task's date
|
||||
const normalizedTask = {
|
||||
...updatedTask,
|
||||
dueDate: updatedTask.dueDate ? new Date(updatedTask.dueDate) : null
|
||||
};
|
||||
setTasks(prev => prev.map(task =>
|
||||
task.id === taskId ? normalizedTask : task
|
||||
));
|
||||
console.log('Task updated:', taskId, updates);
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error);
|
||||
// Optimistic update fallback
|
||||
setTasks(prev => prev.map(task =>
|
||||
task.id === taskId ? { ...task, ...updates } : task
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the timer hook after handleTaskUpdate is defined
|
||||
@@ -270,6 +346,7 @@ function App() {
|
||||
onClose={handleTaskDetailsClose}
|
||||
task={selectedTask}
|
||||
onSave={handleTaskDetailsSave}
|
||||
labels={labels}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Clock, Timer, FileText, Plus, Edit2, Save, X, Calendar } from 'lucide-react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Clock, Timer, FileText, Plus, Edit2, Save, X, Calendar, Tag } from 'lucide-react';
|
||||
import { Task, Label } from '@shared/schema';
|
||||
|
||||
interface TimeEntry {
|
||||
@@ -32,6 +33,14 @@ export default function TaskDetailsModal({
|
||||
onSave,
|
||||
labels = []
|
||||
}: TaskDetailsModalProps) {
|
||||
// Edit form state
|
||||
const [editedTitle, setEditedTitle] = useState('');
|
||||
const [editedDescription, setEditedDescription] = useState('');
|
||||
const [editedStatus, setEditedStatus] = useState<'todo' | 'inProgress' | 'done'>('todo');
|
||||
const [editedPriority, setEditedPriority] = useState<'low' | 'medium' | 'high'>('medium');
|
||||
const [editedLabelId, setEditedLabelId] = useState<string | null>(null);
|
||||
const [editedDueDate, setEditedDueDate] = useState('');
|
||||
|
||||
// Notes state
|
||||
const [notes, setNotes] = useState('');
|
||||
const [isEditingNotes, setIsEditingNotes] = useState(false);
|
||||
@@ -49,6 +58,14 @@ export default function TaskDetailsModal({
|
||||
|
||||
useEffect(() => {
|
||||
if (task && isOpen) {
|
||||
// Initialize edit form with task data
|
||||
setEditedTitle(task.title);
|
||||
setEditedDescription(task.description || '');
|
||||
setEditedStatus(task.status as 'todo' | 'inProgress' | 'done');
|
||||
setEditedPriority(task.priority as 'low' | 'medium' | 'high');
|
||||
setEditedLabelId(task.labelId || null);
|
||||
setEditedDueDate(task.dueDate ? new Date(task.dueDate).toISOString().split('T')[0] : '');
|
||||
|
||||
setNotes(task.notes || '');
|
||||
// Create a single time entry from timeTracked for display purposes
|
||||
if (task.timeTracked > 0) {
|
||||
@@ -83,6 +100,21 @@ export default function TaskDetailsModal({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSaveTaskEdit = () => {
|
||||
if (!task) return;
|
||||
|
||||
const updatedTask: Task = {
|
||||
...task,
|
||||
title: editedTitle,
|
||||
description: editedDescription || null,
|
||||
status: editedStatus,
|
||||
priority: editedPriority,
|
||||
labelId: editedLabelId,
|
||||
dueDate: editedDueDate ? new Date(editedDueDate) : null
|
||||
};
|
||||
onSave(updatedTask);
|
||||
};
|
||||
|
||||
const handleSaveNotes = () => {
|
||||
if (!task) return;
|
||||
|
||||
@@ -239,8 +271,12 @@ export default function TaskDetailsModal({
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Tabs defaultValue="notes" className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<Tabs defaultValue="edit" className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="edit" data-testid="tab-edit">
|
||||
<Edit2 className="w-4 h-4 mr-2" />
|
||||
Edit
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="notes" data-testid="tab-notes">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Notes
|
||||
@@ -251,6 +287,115 @@ export default function TaskDetailsModal({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Edit Tab */}
|
||||
<TabsContent value="edit" className="space-y-4">
|
||||
<Card className="p-4">
|
||||
<h4 className="font-medium mb-4">Edit Task</h4>
|
||||
<div className="space-y-4">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Title</label>
|
||||
<Input
|
||||
value={editedTitle}
|
||||
onChange={(e) => setEditedTitle(e.target.value)}
|
||||
placeholder="Task title"
|
||||
data-testid="input-task-title"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Description</label>
|
||||
<Textarea
|
||||
value={editedDescription}
|
||||
onChange={(e) => setEditedDescription(e.target.value)}
|
||||
placeholder="Task description"
|
||||
rows={3}
|
||||
data-testid="input-task-description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status and Priority */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Status</label>
|
||||
<Select value={editedStatus} onValueChange={(value) => setEditedStatus(value as 'todo' | 'inProgress' | 'done')}>
|
||||
<SelectTrigger data-testid="select-status">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="todo">To Do</SelectItem>
|
||||
<SelectItem value="inProgress">In Progress</SelectItem>
|
||||
<SelectItem value="done">Done</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Priority</label>
|
||||
<Select value={editedPriority} onValueChange={(value) => setEditedPriority(value as 'low' | 'medium' | 'high')}>
|
||||
<SelectTrigger data-testid="select-priority">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Label</label>
|
||||
<Select value={editedLabelId || 'none'} onValueChange={(value) => setEditedLabelId(value === 'none' ? null : value)}>
|
||||
<SelectTrigger data-testid="input-label-selector">
|
||||
<SelectValue placeholder="Select a label" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No Label</SelectItem>
|
||||
{labels.map((label) => (
|
||||
<SelectItem key={label.id} value={label.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: label.color }}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Due Date */}
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Due Date</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={editedDueDate}
|
||||
onChange={(e) => setEditedDueDate(e.target.value)}
|
||||
data-testid="input-due-date"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
onClick={handleSaveTaskEdit}
|
||||
disabled={!editedTitle.trim()}
|
||||
data-testid="button-save-task-edit"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Notes Tab */}
|
||||
<TabsContent value="notes" className="space-y-4">
|
||||
<Card className="p-4">
|
||||
|
||||
Reference in New Issue
Block a user