Rename "Templates" tab to "Settings" and enhance calendar functionality

Refactors the application's navigation by renaming the 'templates' tab to 'settings'. It also introduces several enhancements to the calendar view, including fetching and displaying labels with custom colors, enabling drag-and-drop functionality for tasks onto specific dates, and improving the rendering of weekly calendar views with better date and task handling. The `ProjectTemplate` component is updated to `Project` with new interfaces and default data.

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/oa49Utu
This commit is contained in:
paul-nothaft
2025-09-11 14:15:58 +00:00
parent 36453140f3
commit d5688f90b0
9 changed files with 606 additions and 267 deletions
+4
View File
@@ -18,6 +18,10 @@ externalPort = 80
localPort = 35345
externalPort = 3002
[[ports]]
localPort = 40293
externalPort = 3001
[[ports]]
localPort = 41353
externalPort = 3000
+4 -4
View File
@@ -148,7 +148,7 @@ function App() {
/>
);
case 'templates':
case 'settings':
return (
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
@@ -212,7 +212,7 @@ function App() {
</div>
<div className="flex items-center gap-2">
{currentTab === 'templates' && (
{currentTab === 'settings' && (
<Button
variant="outline"
size="sm"
@@ -236,8 +236,8 @@ function App() {
<BottomNavigation
activeTab={currentTab}
onTabChange={(tab) => {
if (tab === 'templates') {
setCurrentTab('templates');
if (tab === 'settings') {
setCurrentTab('settings');
} else {
setCurrentTab(tab);
}
+1 -1
View File
@@ -17,7 +17,7 @@ export default function BottomNavigation({ onTabChange, onCreateTask, activeTab
{ id: 'calendar', label: 'Calendar', icon: Calendar },
{ id: 'create', label: 'Create', icon: Plus, isCreate: true },
{ id: 'kanban', label: 'Board', icon: LayoutGrid },
{ id: 'templates', label: 'Templates', icon: Settings }
{ id: 'settings', label: 'Settings', icon: Settings }
];
const handleTabClick = (tabId: string, isCreate?: boolean) => {
+209 -3
View File
@@ -4,9 +4,10 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { LayoutGrid, Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
import { Task } from './TaskCard';
import { Task, Label } from '@shared/schema';
import TaskCard from './TaskCard';
import { format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addWeeks, addMonths, isWithinInterval } from 'date-fns';
import { useQuery } from '@tanstack/react-query';
import { format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addWeeks, addMonths, isWithinInterval, addDays, isSameDay, getDaysInMonth } from 'date-fns';
interface KanbanBoardProps {
tasks: Task[];
@@ -21,6 +22,13 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
const [currentDate, setCurrentDate] = useState(new Date());
const [draggedTask, setDraggedTask] = useState<string | null>(null);
// Fetch labels to get label colors
const { data: labels = [] } = useQuery<Label[]>({
queryKey: ['/api/labels'],
staleTime: 5 * 60 * 1000, // 5 minutes cache
refetchOnWindowFocus: false,
});
const columns = [
{ id: 'todo', title: 'To Do', status: 'todo' as const },
{ id: 'inProgress', title: 'In Progress', status: 'inProgress' as const },
@@ -84,6 +92,196 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
return 'All Tasks';
};
const getTasksForDate = (date: Date) => {
return tasks.filter(task =>
task.dueDate && isSameDay(task.dueDate, date)
);
};
const handleDateDrop = (date: Date) => {
if (draggedTask) {
const task = tasks.find(t => t.id === draggedTask);
if (task) {
onTaskUpdate?.(draggedTask, { dueDate: date });
console.log(`Task ${draggedTask} moved to ${format(date, 'yyyy-MM-dd')}`);
}
setDraggedTask(null);
}
};
const renderWeeklyCalendar = () => {
const weekStart = startOfWeek(currentDate);
const dates = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
return (
<div className="grid grid-cols-7 gap-2">
{dates.map((date, index) => {
const dayTasks = getTasksForDate(date);
const isToday = isSameDay(date, new Date());
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
return (
<Card
key={index}
className={`p-3 min-h-[200px] transition-all ${
isToday ? 'ring-2 ring-primary' : ''
} ${
isWeekend ? 'bg-muted/30' : ''
}`}
onDragOver={handleDragOver}
onDrop={() => handleDateDrop(date)}
data-testid={`calendar-date-${format(date, 'yyyy-MM-dd')}`}
>
<div className="text-center mb-2">
<div className="text-xs text-muted-foreground font-medium">
{format(date, 'EEE')}
</div>
<div className={`text-sm font-semibold ${
isToday ? 'text-primary' : ''
}`}>
{format(date, 'd')}
</div>
</div>
<div className="space-y-1">
{dayTasks.slice(0, 3).map((task) => {
const taskLabel = task.labelId && labels.length > 0 ? labels.find(label => label.id === task.labelId) : null;
return (
<div
key={task.id}
draggable
onDragStart={() => handleDragStart(task.id)}
onDragEnd={handleDragEnd}
className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`}
data-testid={`calendar-task-${task.id}`}
>
<Badge
variant="outline"
className="w-full justify-start text-xs p-1 h-auto"
style={taskLabel ? {
borderColor: taskLabel.color,
borderWidth: '2px',
borderStyle: 'solid'
} : {}}
>
<div className="truncate flex-1 text-left">
{task.title}
</div>
</Badge>
</div>
);
})}
{dayTasks.length > 3 && (
<Badge variant="secondary" className="w-full justify-center text-xs">
+{dayTasks.length - 3} more
</Badge>
)}
</div>
</Card>
);
})}
</div>
);
};
const renderMonthlyCalendar = () => {
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
const calendarStart = startOfWeek(monthStart);
const calendarEnd = endOfWeek(monthEnd);
const dates = [];
let currentDateIterator = calendarStart;
while (currentDateIterator <= calendarEnd) {
dates.push(currentDateIterator);
currentDateIterator = addDays(currentDateIterator, 1);
}
return (
<div className="space-y-2">
{/* Month grid header */}
<div className="grid grid-cols-7 gap-2 mb-2">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
<div key={day} className="text-center text-xs font-medium text-muted-foreground p-2">
{day}
</div>
))}
</div>
{/* Month grid */}
<div className="grid grid-cols-7 gap-2">
{dates.map((date, index) => {
const dayTasks = getTasksForDate(date);
const isToday = isSameDay(date, new Date());
const isCurrentMonth = date >= monthStart && date <= monthEnd;
return (
<Card
key={index}
className={`p-2 min-h-[100px] transition-all ${
isToday ? 'ring-2 ring-primary' : ''
} ${
!isCurrentMonth ? 'bg-muted/50 text-muted-foreground' : ''
}`}
onDragOver={handleDragOver}
onDrop={() => handleDateDrop(date)}
data-testid={`calendar-date-${format(date, 'yyyy-MM-dd')}`}
>
<div className="text-center mb-1">
<div className={`text-xs font-semibold ${
isToday ? 'text-primary' : ''
}`}>
{format(date, 'd')}
</div>
</div>
<div className="space-y-1">
{dayTasks.slice(0, 2).map((task) => {
const taskLabel = task.labelId && labels.length > 0 ? labels.find(label => label.id === task.labelId) : null;
return (
<div
key={task.id}
draggable
onDragStart={() => handleDragStart(task.id)}
onDragEnd={handleDragEnd}
className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`}
data-testid={`calendar-task-${task.id}`}
>
<Badge
variant="outline"
className="w-full justify-start text-xs p-1 h-auto"
style={taskLabel ? {
borderColor: taskLabel.color,
borderWidth: '2px',
borderStyle: 'solid'
} : {}}
>
<div className="truncate flex-1 text-left">
{task.title}
</div>
</Badge>
</div>
);
})}
{dayTasks.length > 2 && (
<Badge variant="secondary" className="w-full justify-center text-xs">
+{dayTasks.length - 2}
</Badge>
)}
</div>
</Card>
);
})}
</div>
</div>
);
};
return (
<div className="space-y-4">
{/* Header */}
@@ -141,7 +339,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
</TabsTrigger>
</TabsList>
<TabsContent value={viewMode} className="mt-4">
<TabsContent value="traditional" className="mt-4">
{/* Kanban Columns */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{columns.map((column) => {
@@ -202,6 +400,14 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
})}
</div>
</TabsContent>
<TabsContent value="weekly" className="mt-4">
{renderWeeklyCalendar()}
</TabsContent>
<TabsContent value="monthly" className="mt-4">
{renderMonthlyCalendar()}
</TabsContent>
</Tabs>
</div>
);
+350 -239
View File
@@ -14,70 +14,87 @@ import { Label } from '@shared/schema';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@/lib/queryClient';
interface TemplateTask {
interface ProjectTask {
id: string;
title: string;
description?: string;
priority: 'low' | 'medium' | 'high';
dayOffset: number; // Days relative to project start date (negative for before, positive for after)
status: 'todo' | 'inProgress' | 'done';
estimatedHours?: number;
labelId?: string;
}
interface ProjectTemplate {
interface Project {
id: string;
name: string;
description?: string;
tasks: TemplateTask[];
category: string;
status: 'planning' | 'active' | 'finished';
tasks: ProjectTask[];
startDate?: Date;
endDate?: Date;
createdAt: Date;
}
interface ProjectTemplateProps {
templates?: ProjectTemplate[];
onCreateFromTemplate?: (templateId: string, startDate: Date, projectName: string) => void;
onCreateTemplate?: (template: Omit<ProjectTemplate, 'id'>) => void;
onCreateTemplate?: (template: any) => void;
}
const defaultTemplates: ProjectTemplate[] = [
const defaultProjects: Project[] = [
{
id: 'web-project',
name: 'Website Launch',
description: 'Complete website development and launch process',
category: 'Development',
id: '1',
name: 'Website Redesign',
description: 'Complete redesign of company website',
status: 'active',
startDate: new Date(),
createdAt: new Date(),
tasks: [
{ id: '1', title: 'Project planning', priority: 'high', dayOffset: -14, estimatedHours: 4 },
{ id: '2', title: 'Design mockups', priority: 'high', dayOffset: -10, estimatedHours: 16 },
{ id: '3', title: 'Frontend development', priority: 'high', dayOffset: -7, estimatedHours: 40 },
{ id: '4', title: 'Backend API', priority: 'medium', dayOffset: -5, estimatedHours: 24 },
{ id: '5', title: 'Testing and QA', priority: 'high', dayOffset: -2, estimatedHours: 8 },
{ id: '6', title: 'Deploy to production', priority: 'high', dayOffset: 0, estimatedHours: 2 }
{ id: '1', title: 'Research competitors', priority: 'high', status: 'done', estimatedHours: 4 },
{ id: '2', title: 'Create wireframes', priority: 'high', status: 'inProgress', estimatedHours: 8 },
{ id: '3', title: 'Design mockups', priority: 'medium', status: 'todo', estimatedHours: 12 },
{ id: '4', title: 'Frontend development', priority: 'high', status: 'todo', estimatedHours: 40 }
]
},
{
id: 'product-launch',
name: 'Product Launch',
description: 'Marketing and launch campaign for new product',
category: 'Marketing',
id: '2',
name: 'Mobile App Launch',
description: 'Launch new mobile application',
status: 'planning',
createdAt: new Date(),
tasks: [
{ id: '1', title: 'Market research', priority: 'high', dayOffset: -21, estimatedHours: 12 },
{ id: '2', title: 'Create marketing materials', priority: 'medium', dayOffset: -14, estimatedHours: 20 },
{ id: '3', title: 'Press release', priority: 'medium', dayOffset: -7, estimatedHours: 4 },
{ id: '4', title: 'Social media campaign', priority: 'high', dayOffset: -3, estimatedHours: 8 },
{ id: '5', title: 'Launch event', priority: 'high', dayOffset: 0, estimatedHours: 6 }
{ id: '1', title: 'Define requirements', priority: 'high', status: 'todo', estimatedHours: 6 },
{ id: '2', title: 'Create user stories', priority: 'medium', status: 'todo', estimatedHours: 8 },
{ id: '3', title: 'Design UI/UX', priority: 'high', status: 'todo', estimatedHours: 20 }
]
},
{
id: '3',
name: 'Blog Platform',
description: 'Personal blog with CMS',
status: 'finished',
startDate: new Date('2024-01-01'),
endDate: new Date('2024-03-01'),
createdAt: new Date('2024-01-01'),
tasks: [
{ id: '1', title: 'Setup infrastructure', priority: 'high', status: 'done', estimatedHours: 8 },
{ id: '2', title: 'Develop backend API', priority: 'high', status: 'done', estimatedHours: 32 },
{ id: '3', title: 'Create admin panel', priority: 'medium', status: 'done', estimatedHours: 16 },
{ id: '4', title: 'Deploy and test', priority: 'high', status: 'done', estimatedHours: 4 }
]
}
];
export default function ProjectTemplate({
templates = defaultTemplates,
onCreateFromTemplate,
onCreateTemplate
}: ProjectTemplateProps) {
const [selectedTemplate, setSelectedTemplate] = useState<ProjectTemplate | null>(null);
const [projectName, setProjectName] = useState('');
const [startDate, setStartDate] = useState<Date | undefined>();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isLaunchOpen, setIsLaunchOpen] = useState(false);
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
const [projects, setProjects] = useState<Project[]>(defaultProjects);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [draggedProject, setDraggedProject] = useState<string | null>(null);
const [isCreateProjectOpen, setIsCreateProjectOpen] = useState(false);
const [isEditProjectOpen, setIsEditProjectOpen] = useState(false);
const [newProjectName, setNewProjectName] = useState('');
const [newProjectDescription, setNewProjectDescription] = useState('');
// Label management state
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
@@ -126,22 +143,61 @@ export default function ProjectTemplate({
}
});
const handleLaunchProject = () => {
if (selectedTemplate && startDate && projectName.trim()) {
onCreateFromTemplate?.(selectedTemplate.id, startDate, projectName.trim());
console.log('Launching project:', {
template: selectedTemplate.name,
name: projectName,
startDate: startDate.toLocaleDateString()
});
const handleCreateProject = () => {
if (newProjectName.trim()) {
const newProject: Project = {
id: Date.now().toString(),
name: newProjectName.trim(),
description: newProjectDescription.trim() || undefined,
status: 'planning',
tasks: [],
createdAt: new Date()
};
setProjectName('');
setStartDate(undefined);
setSelectedTemplate(null);
setIsLaunchOpen(false);
setProjects([...projects, newProject]);
setNewProjectName('');
setNewProjectDescription('');
setIsCreateProjectOpen(false);
console.log('Created new project:', newProject.name);
}
};
const handleEditProject = (project: Project) => {
setSelectedProject(project);
setIsEditProjectOpen(true);
};
const handleDeleteProject = (projectId: string) => {
if (confirm('Are you sure you want to delete this project?')) {
setProjects(projects.filter(p => p.id !== projectId));
}
};
const handleProjectStatusChange = (projectId: string, newStatus: Project['status']) => {
setProjects(projects.map(project =>
project.id === projectId ? { ...project, status: newStatus } : project
));
};
const handleDragStart = (projectId: string) => {
setDraggedProject(projectId);
};
const handleDragEnd = () => {
setDraggedProject(null);
};
const handleDrop = (status: Project['status']) => {
if (draggedProject) {
handleProjectStatusChange(draggedProject, status);
setDraggedProject(null);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
// Label management functions
const handleSaveLabel = () => {
if (!labelName.trim()) return;
@@ -166,234 +222,289 @@ export default function ProjectTemplate({
}
};
const formatDayOffset = (offset: number) => {
if (offset === 0) return 'Launch day';
if (offset < 0) return `${Math.abs(offset)} days before`;
return `${offset} days after`;
const getTotalEstimatedHours = (project: Project) => {
return project.tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
};
const getTotalEstimatedHours = (template: ProjectTemplate) => {
return template.tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
const getTaskCompletionRate = (project: Project) => {
if (project.tasks.length === 0) return 0;
const completedTasks = project.tasks.filter(task => task.status === 'done').length;
return Math.round((completedTasks / project.tasks.length) * 100);
};
const getTemplatesByCategory = () => {
const categories = Array.from(new Set(templates.map(t => t.category)));
return categories.map(category => ({
category,
templates: templates.filter(t => t.category === category)
}));
const getProjectsByStatus = (status: Project['status']) => {
return projects.filter(project => project.status === status);
};
const projectColumns = [
{ id: 'planning', title: 'Planning', status: 'planning' as const, color: 'bg-slate-100' },
{ id: 'active', title: 'Active', status: 'active' as const, color: 'bg-blue-100' },
{ id: 'finished', title: 'Finished', status: 'finished' as const, color: 'bg-green-100' }
];
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold" data-testid="text-templates-title">
Settings & Templates
<h2 className="text-lg font-semibold" data-testid="text-settings-title">
Settings & Project Management
</h2>
<Button
onClick={() => setIsCreateProjectOpen(true)}
data-testid="button-create-project"
>
<Plus className="w-4 h-4 mr-2" />
New Project
</Button>
</div>
<Tabs defaultValue="templates" className="space-y-4">
<Tabs defaultValue="projects" className="space-y-4">
<TabsList>
<TabsTrigger value="templates" data-testid="tab-templates">Templates</TabsTrigger>
<TabsTrigger value="projects" data-testid="tab-projects">Projects</TabsTrigger>
<TabsTrigger value="labels" data-testid="tab-labels">Labels</TabsTrigger>
</TabsList>
<TabsContent value="templates" className="space-y-6">
{/* Templates Section Header */}
<div className="flex items-center justify-between">
<h3 className="text-md font-medium">Project Templates</h3>
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" data-testid="button-create-template">
<Plus className="w-4 h-4 mr-2" />
Create Template
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Template</DialogTitle>
</DialogHeader>
<div className="p-4 text-center text-muted-foreground">
Template creation form would go here
</div>
</DialogContent>
</Dialog>
</div>
{/* Templates by Category */}
{getTemplatesByCategory().map(({ category, templates: categoryTemplates }) => (
<div key={category} className="space-y-3">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
{category}
</h3>
<div className="grid gap-4 md:grid-cols-2">
{categoryTemplates.map((template) => (
<Card key={template.id} className="p-4 hover-elevate active-elevate-2" data-testid={`template-${template.id}`}>
<div className="space-y-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-semibold text-sm" data-testid={`text-template-name-${template.id}`}>
{template.name}
</h4>
{template.description && (
<p className="text-xs text-muted-foreground mt-1">
{template.description}
</p>
)}
</div>
<TabsContent value="projects" className="space-y-6">
{/* Project Kanban Board */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{projectColumns.map((column) => {
const columnProjects = getProjectsByStatus(column.status);
return (
<Card
key={column.id}
className="p-4"
onDragOver={handleDragOver}
onDrop={() => handleDrop(column.status)}
data-testid={`column-${column.id}`}
>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-sm" data-testid={`text-column-title-${column.id}`}>
{column.title}
</h3>
<Badge variant="secondary" className="text-xs">
{template.tasks.length} tasks
{columnProjects.length}
</Badge>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{getTotalEstimatedHours(template)}h estimated</span>
</div>
<div className="flex items-center gap-1">
<CalendarIcon className="w-3 h-3" />
<span>{Math.abs(Math.min(...template.tasks.map(t => t.dayOffset)))} day timeline</span>
</div>
</div>
<div className="space-y-3 min-h-[400px]">
{columnProjects.map((project) => (
<Card
key={project.id}
draggable
onDragStart={() => handleDragStart(project.id)}
onDragEnd={handleDragEnd}
className={`p-4 cursor-move hover-elevate transition-all ${
draggedProject === project.id ? 'opacity-50 scale-95' : ''
}`}
data-testid={`project-card-${project.id}`}
>
<div className="space-y-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-semibold text-sm" data-testid={`text-project-name-${project.id}`}>
{project.name}
</h4>
{project.description && (
<p className="text-xs text-muted-foreground mt-1">
{project.description}
</p>
)}
</div>
<div className="space-y-2">
<div className="text-xs font-medium text-muted-foreground">Sample tasks:</div>
<div className="space-y-1">
{template.tasks.slice(0, 3).map((task) => (
<div key={task.id} className="flex items-center justify-between text-xs">
<span className="truncate flex-1">{task.title}</span>
<Badge variant="outline" className="text-xs ml-2 flex-shrink-0">
{formatDayOffset(task.dayOffset)}
</Badge>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditProject(project)}
data-testid={`button-edit-${project.id}`}
>
<Edit className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteProject(project.id)}
data-testid={`button-delete-${project.id}`}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{getTotalEstimatedHours(project)}h</span>
</div>
<div className="flex items-center gap-1">
<Badge variant="outline" className="text-xs">
{project.tasks.length} tasks
</Badge>
</div>
</div>
{project.tasks.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">Progress</span>
<span className="font-medium">{getTaskCompletionRate(project)}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${getTaskCompletionRate(project)}%` }}
/>
</div>
</div>
)}
{project.startDate && (
<div className="text-xs text-muted-foreground">
Started: {project.startDate.toLocaleDateString()}
</div>
)}
</div>
))}
{template.tasks.length > 3 && (
<div className="text-xs text-muted-foreground text-center py-1">
+{template.tasks.length - 3} more tasks
</div>
)}
</div>
</div>
</Card>
))}
<div className="flex gap-2 pt-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
console.log('Copying template:', template.name);
}}
data-testid={`button-copy-${template.id}`}
>
<Copy className="w-3 h-3 mr-1" />
Copy
</Button>
<Button
size="sm"
className="flex-1"
onClick={() => {
setSelectedTemplate(template);
setProjectName(template.name);
setIsLaunchOpen(true);
}}
data-testid={`button-use-${template.id}`}
>
<Play className="w-3 h-3 mr-1" />
Use Template
</Button>
{columnProjects.length === 0 && (
<div className="text-center text-muted-foreground text-sm py-8 border-2 border-dashed border-muted rounded-lg">
No projects in {column.title.toLowerCase()}
</div>
)}
</div>
</div>
</Card>
))}
</Card>
);
})}
</div>
</div>
))}
</TabsContent>
{/* Launch Project Dialog */}
<Dialog open={isLaunchOpen} onOpenChange={setIsLaunchOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Play className="w-4 h-4" />
Launch Project from Template
</DialogTitle>
</DialogHeader>
{selectedTemplate && (
<div className="space-y-4">
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm font-medium">{selectedTemplate.name}</div>
<div className="text-xs text-muted-foreground">
{selectedTemplate.tasks.length} tasks {getTotalEstimatedHours(selectedTemplate)}h estimated
</div>
</div>
<div>
{/* Create Project Dialog */}
<Dialog open={isCreateProjectOpen} onOpenChange={setIsCreateProjectOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Project Name</label>
<Input
placeholder="Project name"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
placeholder="Enter project name"
data-testid="input-project-name"
/>
</div>
<div>
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full justify-start"
data-testid="button-project-start-date"
>
<CalendarIcon className="w-4 h-4 mr-2" />
{startDate ? startDate.toLocaleDateString() : 'Select start date'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={startDate}
onSelect={(date) => {
setStartDate(date);
setIsCalendarOpen(false);
}}
disabled={(date) => date < new Date()}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div className="flex gap-3 pt-2">
<Button
variant="outline"
onClick={() => setIsLaunchOpen(false)}
className="flex-1"
data-testid="button-cancel-launch"
>
Cancel
</Button>
<Button
onClick={handleLaunchProject}
disabled={!projectName.trim() || !startDate}
className="flex-1"
data-testid="button-launch-project"
>
Launch Project
</Button>
<div className="space-y-2">
<label className="text-sm font-medium">Description (Optional)</label>
<Input
value={newProjectDescription}
onChange={(e) => setNewProjectDescription(e.target.value)}
placeholder="Enter project description"
data-testid="input-project-description"
/>
</div>
</div>
)}
</DialogContent>
</Dialog>
</TabsContent>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setIsCreateProjectOpen(false)}
className="flex-1"
>
Cancel
</Button>
<Button
onClick={handleCreateProject}
disabled={!newProjectName.trim()}
className="flex-1"
data-testid="button-save-project"
>
Create Project
</Button>
</div>
</DialogContent>
</Dialog>
<TabsContent value="labels" className="space-y-6">
{/* Edit Project Dialog */}
<Dialog open={isEditProjectOpen} onOpenChange={setIsEditProjectOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Edit Project</DialogTitle>
</DialogHeader>
{selectedProject && (
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Project Name</label>
<Input
value={selectedProject.name}
onChange={(e) => setSelectedProject({ ...selectedProject, name: e.target.value })}
placeholder="Enter project name"
data-testid="input-edit-project-name"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description (Optional)</label>
<Input
value={selectedProject.description || ''}
onChange={(e) => setSelectedProject({ ...selectedProject, description: e.target.value })}
placeholder="Enter project description"
data-testid="input-edit-project-description"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Status</label>
<Select
value={selectedProject.status}
onValueChange={(value: Project['status']) =>
setSelectedProject({ ...selectedProject, status: value })
}
>
<SelectTrigger data-testid="select-project-status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="planning">Planning</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="finished">Finished</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => {
setIsEditProjectOpen(false);
setSelectedProject(null);
}}
className="flex-1"
>
Cancel
</Button>
<Button
onClick={() => {
if (selectedProject) {
setProjects(projects.map(p =>
p.id === selectedProject.id ? selectedProject : p
));
setIsEditProjectOpen(false);
setSelectedProject(null);
console.log('Updated project:', selectedProject.name);
}
}}
disabled={!selectedProject?.name.trim()}
className="flex-1"
data-testid="button-update-project"
>
Update Project
</Button>
</div>
</DialogContent>
</Dialog>
<TabsContent value="labels" className="space-y-6">
{/* Labels Section Header */}
<div className="flex items-center justify-between">
<h3 className="text-md font-medium">Task Labels</h3>
+37 -19
View File
@@ -5,8 +5,9 @@ import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Search, Filter, SortAsc, Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
import { Task } from '@shared/schema';
import { Task, Label } from '@shared/schema';
import TaskCard from './TaskCard';
import { useQuery } from '@tanstack/react-query';
import TimeCompletionModal from './TimeCompletionModal';
import { addDays, format, isSameDay, startOfToday } from 'date-fns';
@@ -31,6 +32,13 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit }: T
task: null
});
// Fetch labels to get label colors
const { data: labels = [] } = useQuery<Label[]>({
queryKey: ['/api/labels'],
staleTime: 5 * 60 * 1000, // 5 minutes cache
refetchOnWindowFocus: false,
});
// Get 7 days starting from today for calendar
const dates = Array.from({ length: 7 }, (_, i) => addDays(calendarStartDate, i));
@@ -371,25 +379,35 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit }: T
</div>
<div className="space-y-1">
{dayTasks.slice(0, 2).map((task) => (
<div
key={task.id}
draggable
onDragStart={(e) => handleDragStart(task.id, e)}
onDragEnd={handleDragEnd}
className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`}
data-testid={`calendar-task-${task.id}`}
>
<Badge
variant="outline"
className="w-full justify-start text-xs p-1 h-auto text-left"
{dayTasks.slice(0, 2).map((task) => {
// Find the label for this task
const taskLabel = task.labelId && labels.length > 0 ? labels.find(label => label.id === task.labelId) : null;
return (
<div
key={task.id}
draggable
onDragStart={(e) => handleDragStart(task.id, e)}
onDragEnd={handleDragEnd}
className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`}
data-testid={`calendar-task-${task.id}`}
>
<div className="truncate text-xs">
{task.title}
</div>
</Badge>
</div>
))}
<Badge
variant="outline"
className="w-full justify-start text-xs p-1 h-auto text-left"
style={taskLabel ? {
borderColor: taskLabel.color,
borderWidth: '2px',
borderStyle: 'solid'
} : {}}
>
<div className="truncate text-xs">
{task.title}
</div>
</Badge>
</div>
);
})}
{dayTasks.length > 2 && (
<Badge variant="secondary" className="w-full justify-center text-xs py-0">
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB