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:
paul-nothaft
2025-10-23 11:29:42 +00:00
parent bcb43a0ae7
commit 8986785d9a
3 changed files with 255 additions and 25 deletions
+8
View File
@@ -18,10 +18,18 @@ externalPort = 80
localPort = 35345 localPort = 35345
externalPort = 3002 externalPort = 3002
[[ports]]
localPort = 39063
externalPort = 3001
[[ports]] [[ports]]
localPort = 41353 localPort = 41353
externalPort = 3000 externalPort = 3000
[[ports]]
localPort = 43181
externalPort = 4200
[[ports]] [[ports]]
localPort = 44261 localPort = 44261
externalPort = 3003 externalPort = 3003
+99 -22
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { queryClient } from "./lib/queryClient"; import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
@@ -15,7 +15,7 @@ import CalendarView from './components/CalendarView';
import KanbanBoard from './components/KanbanBoard'; import KanbanBoard from './components/KanbanBoard';
import ProjectTemplate from './components/ProjectTemplate'; import ProjectTemplate from './components/ProjectTemplate';
import ThemeToggle from './components/ThemeToggle'; import ThemeToggle from './components/ThemeToggle';
import { Task } from '@shared/schema'; import { Task, Label } from '@shared/schema';
import { addDays, subDays } from 'date-fns'; import { addDays, subDays } from 'date-fns';
import { useTimer } from './hooks/useTimer'; import { useTimer } from './hooks/useTimer';
@@ -91,33 +91,109 @@ const initialTasks: Task[] = [
function App() { function App() {
const [currentTab, setCurrentTab] = useState('tasks'); const [currentTab, setCurrentTab] = useState('tasks');
const [tasks, setTasks] = useState<Task[]>(initialTasks); const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [labels, setLabels] = useState<Label[]>([]);
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 handleCreateTask = (newTask: Partial<Task>) => { // Fetch tasks and labels from server on mount
const task: Task = { useEffect(() => {
id: Date.now().toString(), const fetchData = async () => {
title: newTask.title || '', try {
description: newTask.description || null, // Fetch tasks
status: 'todo', const tasksResponse = await fetch('/api/tasks');
priority: newTask.priority || 'medium', if (tasksResponse.ok) {
dueDate: newTask.dueDate || null, const serverTasks: Task[] = await tasksResponse.json();
timeTracked: 0, // Normalize server tasks - convert date strings to Date objects
isTracking: false, const normalizedTasks = serverTasks.map(task => ({
projectId: newTask.projectId || null, ...task,
notes: newTask.notes || null, dueDate: task.dueDate ? new Date(task.dueDate) : null
labelId: newTask.labelId || 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]); fetchData();
console.log('Task created:', task); }, []);
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>) => { const handleTaskUpdate = async (taskId: string, updates: Partial<Task>) => {
setTasks(prev => prev.map(task => try {
task.id === taskId ? { ...task, ...updates } : task const response = await fetch(`/api/tasks/${taskId}`, {
)); method: 'PATCH',
console.log('Task updated:', taskId, updates); 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 // Initialize the timer hook after handleTaskUpdate is defined
@@ -270,6 +346,7 @@ function App() {
onClose={handleTaskDetailsClose} onClose={handleTaskDetailsClose}
task={selectedTask} task={selectedTask}
onSave={handleTaskDetailsSave} onSave={handleTaskDetailsSave}
labels={labels}
/> />
</div> </div>
+148 -3
View File
@@ -7,7 +7,8 @@ import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; 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'; import { Task, Label } from '@shared/schema';
interface TimeEntry { interface TimeEntry {
@@ -32,6 +33,14 @@ export default function TaskDetailsModal({
onSave, onSave,
labels = [] labels = []
}: TaskDetailsModalProps) { }: 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 // Notes state
const [notes, setNotes] = useState(''); const [notes, setNotes] = useState('');
const [isEditingNotes, setIsEditingNotes] = useState(false); const [isEditingNotes, setIsEditingNotes] = useState(false);
@@ -49,6 +58,14 @@ export default function TaskDetailsModal({
useEffect(() => { useEffect(() => {
if (task && isOpen) { 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 || ''); setNotes(task.notes || '');
// Create a single time entry from timeTracked for display purposes // Create a single time entry from timeTracked for display purposes
if (task.timeTracked > 0) { if (task.timeTracked > 0) {
@@ -83,6 +100,21 @@ export default function TaskDetailsModal({
onClose(); 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 = () => { const handleSaveNotes = () => {
if (!task) return; if (!task) return;
@@ -239,8 +271,12 @@ export default function TaskDetailsModal({
</div> </div>
</Card> </Card>
<Tabs defaultValue="notes" className="space-y-4"> <Tabs defaultValue="edit" className="space-y-4">
<TabsList className="grid w-full grid-cols-2"> <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"> <TabsTrigger value="notes" data-testid="tab-notes">
<FileText className="w-4 h-4 mr-2" /> <FileText className="w-4 h-4 mr-2" />
Notes Notes
@@ -251,6 +287,115 @@ export default function TaskDetailsModal({
</TabsTrigger> </TabsTrigger>
</TabsList> </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 */} {/* Notes Tab */}
<TabsContent value="notes" className="space-y-4"> <TabsContent value="notes" className="space-y-4">
<Card className="p-4"> <Card className="p-4">