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:
@@ -18,6 +18,10 @@ externalPort = 80
|
|||||||
localPort = 35345
|
localPort = 35345
|
||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 40293
|
||||||
|
externalPort = 3001
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 41353
|
localPort = 41353
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|||||||
+4
-4
@@ -148,7 +148,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'templates':
|
case 'settings':
|
||||||
return (
|
return (
|
||||||
<ProjectTemplate
|
<ProjectTemplate
|
||||||
onCreateFromTemplate={handleCreateFromTemplate}
|
onCreateFromTemplate={handleCreateFromTemplate}
|
||||||
@@ -212,7 +212,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{currentTab === 'templates' && (
|
{currentTab === 'settings' && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -236,8 +236,8 @@ function App() {
|
|||||||
<BottomNavigation
|
<BottomNavigation
|
||||||
activeTab={currentTab}
|
activeTab={currentTab}
|
||||||
onTabChange={(tab) => {
|
onTabChange={(tab) => {
|
||||||
if (tab === 'templates') {
|
if (tab === 'settings') {
|
||||||
setCurrentTab('templates');
|
setCurrentTab('settings');
|
||||||
} else {
|
} else {
|
||||||
setCurrentTab(tab);
|
setCurrentTab(tab);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function BottomNavigation({ onTabChange, onCreateTask, activeTab
|
|||||||
{ id: 'calendar', label: 'Calendar', icon: Calendar },
|
{ id: 'calendar', label: 'Calendar', icon: Calendar },
|
||||||
{ id: 'create', label: 'Create', icon: Plus, isCreate: true },
|
{ id: 'create', label: 'Create', icon: Plus, isCreate: true },
|
||||||
{ id: 'kanban', label: 'Board', icon: LayoutGrid },
|
{ id: 'kanban', label: 'Board', icon: LayoutGrid },
|
||||||
{ id: 'templates', label: 'Templates', icon: Settings }
|
{ id: 'settings', label: 'Settings', icon: Settings }
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleTabClick = (tabId: string, isCreate?: boolean) => {
|
const handleTabClick = (tabId: string, isCreate?: boolean) => {
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
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 { LayoutGrid, Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
|
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 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 {
|
interface KanbanBoardProps {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
@@ -21,6 +22,13 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
|
|||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
const [draggedTask, setDraggedTask] = useState<string | null>(null);
|
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 = [
|
const columns = [
|
||||||
{ id: 'todo', title: 'To Do', status: 'todo' as const },
|
{ id: 'todo', title: 'To Do', status: 'todo' as const },
|
||||||
{ id: 'inProgress', title: 'In Progress', status: 'inProgress' 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';
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -141,7 +339,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value={viewMode} className="mt-4">
|
<TabsContent value="traditional" className="mt-4">
|
||||||
{/* Kanban Columns */}
|
{/* Kanban Columns */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{columns.map((column) => {
|
{columns.map((column) => {
|
||||||
@@ -202,6 +400,14 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="weekly" className="mt-4">
|
||||||
|
{renderWeeklyCalendar()}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="monthly" className="mt-4">
|
||||||
|
{renderMonthlyCalendar()}
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,70 +14,87 @@ import { Label } from '@shared/schema';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { apiRequest } from '@/lib/queryClient';
|
import { apiRequest } from '@/lib/queryClient';
|
||||||
|
|
||||||
interface TemplateTask {
|
interface ProjectTask {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
priority: 'low' | 'medium' | 'high';
|
priority: 'low' | 'medium' | 'high';
|
||||||
dayOffset: number; // Days relative to project start date (negative for before, positive for after)
|
status: 'todo' | 'inProgress' | 'done';
|
||||||
estimatedHours?: number;
|
estimatedHours?: number;
|
||||||
|
labelId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectTemplate {
|
interface Project {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tasks: TemplateTask[];
|
status: 'planning' | 'active' | 'finished';
|
||||||
category: string;
|
tasks: ProjectTask[];
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectTemplateProps {
|
interface ProjectTemplateProps {
|
||||||
templates?: ProjectTemplate[];
|
|
||||||
onCreateFromTemplate?: (templateId: string, startDate: Date, projectName: string) => void;
|
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',
|
id: '1',
|
||||||
name: 'Website Launch',
|
name: 'Website Redesign',
|
||||||
description: 'Complete website development and launch process',
|
description: 'Complete redesign of company website',
|
||||||
category: 'Development',
|
status: 'active',
|
||||||
|
startDate: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
tasks: [
|
tasks: [
|
||||||
{ id: '1', title: 'Project planning', priority: 'high', dayOffset: -14, estimatedHours: 4 },
|
{ id: '1', title: 'Research competitors', priority: 'high', status: 'done', estimatedHours: 4 },
|
||||||
{ id: '2', title: 'Design mockups', priority: 'high', dayOffset: -10, estimatedHours: 16 },
|
{ id: '2', title: 'Create wireframes', priority: 'high', status: 'inProgress', estimatedHours: 8 },
|
||||||
{ id: '3', title: 'Frontend development', priority: 'high', dayOffset: -7, estimatedHours: 40 },
|
{ id: '3', title: 'Design mockups', priority: 'medium', status: 'todo', estimatedHours: 12 },
|
||||||
{ id: '4', title: 'Backend API', priority: 'medium', dayOffset: -5, estimatedHours: 24 },
|
{ id: '4', title: 'Frontend development', priority: 'high', status: 'todo', estimatedHours: 40 }
|
||||||
{ 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: 'product-launch',
|
id: '2',
|
||||||
name: 'Product Launch',
|
name: 'Mobile App Launch',
|
||||||
description: 'Marketing and launch campaign for new product',
|
description: 'Launch new mobile application',
|
||||||
category: 'Marketing',
|
status: 'planning',
|
||||||
|
createdAt: new Date(),
|
||||||
tasks: [
|
tasks: [
|
||||||
{ id: '1', title: 'Market research', priority: 'high', dayOffset: -21, estimatedHours: 12 },
|
{ id: '1', title: 'Define requirements', priority: 'high', status: 'todo', estimatedHours: 6 },
|
||||||
{ id: '2', title: 'Create marketing materials', priority: 'medium', dayOffset: -14, estimatedHours: 20 },
|
{ id: '2', title: 'Create user stories', priority: 'medium', status: 'todo', estimatedHours: 8 },
|
||||||
{ id: '3', title: 'Press release', priority: 'medium', dayOffset: -7, estimatedHours: 4 },
|
{ id: '3', title: 'Design UI/UX', priority: 'high', status: 'todo', estimatedHours: 20 }
|
||||||
{ id: '4', title: 'Social media campaign', priority: 'high', dayOffset: -3, estimatedHours: 8 },
|
]
|
||||||
{ id: '5', title: 'Launch event', priority: 'high', dayOffset: 0, estimatedHours: 6 }
|
},
|
||||||
|
{
|
||||||
|
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({
|
export default function ProjectTemplate({
|
||||||
templates = defaultTemplates,
|
|
||||||
onCreateFromTemplate,
|
onCreateFromTemplate,
|
||||||
onCreateTemplate
|
onCreateTemplate
|
||||||
}: ProjectTemplateProps) {
|
}: ProjectTemplateProps) {
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<ProjectTemplate | null>(null);
|
const [projects, setProjects] = useState<Project[]>(defaultProjects);
|
||||||
const [projectName, setProjectName] = useState('');
|
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>();
|
const [draggedProject, setDraggedProject] = useState<string | null>(null);
|
||||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
const [isCreateProjectOpen, setIsCreateProjectOpen] = useState(false);
|
||||||
const [isLaunchOpen, setIsLaunchOpen] = useState(false);
|
const [isEditProjectOpen, setIsEditProjectOpen] = useState(false);
|
||||||
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
const [newProjectName, setNewProjectName] = useState('');
|
||||||
|
const [newProjectDescription, setNewProjectDescription] = useState('');
|
||||||
|
|
||||||
// Label management state
|
// Label management state
|
||||||
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
|
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
|
||||||
@@ -126,22 +143,61 @@ export default function ProjectTemplate({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleLaunchProject = () => {
|
const handleCreateProject = () => {
|
||||||
if (selectedTemplate && startDate && projectName.trim()) {
|
if (newProjectName.trim()) {
|
||||||
onCreateFromTemplate?.(selectedTemplate.id, startDate, projectName.trim());
|
const newProject: Project = {
|
||||||
console.log('Launching project:', {
|
id: Date.now().toString(),
|
||||||
template: selectedTemplate.name,
|
name: newProjectName.trim(),
|
||||||
name: projectName,
|
description: newProjectDescription.trim() || undefined,
|
||||||
startDate: startDate.toLocaleDateString()
|
status: 'planning',
|
||||||
});
|
tasks: [],
|
||||||
|
createdAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
setProjectName('');
|
setProjects([...projects, newProject]);
|
||||||
setStartDate(undefined);
|
setNewProjectName('');
|
||||||
setSelectedTemplate(null);
|
setNewProjectDescription('');
|
||||||
setIsLaunchOpen(false);
|
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
|
// Label management functions
|
||||||
const handleSaveLabel = () => {
|
const handleSaveLabel = () => {
|
||||||
if (!labelName.trim()) return;
|
if (!labelName.trim()) return;
|
||||||
@@ -166,234 +222,289 @@ export default function ProjectTemplate({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDayOffset = (offset: number) => {
|
const getTotalEstimatedHours = (project: Project) => {
|
||||||
if (offset === 0) return 'Launch day';
|
return project.tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
|
||||||
if (offset < 0) return `${Math.abs(offset)} days before`;
|
|
||||||
return `${offset} days after`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTotalEstimatedHours = (template: ProjectTemplate) => {
|
const getTaskCompletionRate = (project: Project) => {
|
||||||
return template.tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
|
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 getProjectsByStatus = (status: Project['status']) => {
|
||||||
const categories = Array.from(new Set(templates.map(t => t.category)));
|
return projects.filter(project => project.status === status);
|
||||||
return categories.map(category => ({
|
|
||||||
category,
|
|
||||||
templates: templates.filter(t => t.category === category)
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-lg font-semibold" data-testid="text-templates-title">
|
<h2 className="text-lg font-semibold" data-testid="text-settings-title">
|
||||||
Settings & Templates
|
Settings & Project Management
|
||||||
</h2>
|
</h2>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsCreateProjectOpen(true)}
|
||||||
|
data-testid="button-create-project"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
New Project
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue="templates" className="space-y-4">
|
<Tabs defaultValue="projects" className="space-y-4">
|
||||||
<TabsList>
|
<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>
|
<TabsTrigger value="labels" data-testid="tab-labels">Labels</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="templates" className="space-y-6">
|
<TabsContent value="projects" className="space-y-6">
|
||||||
{/* Templates Section Header */}
|
{/* Project Kanban Board */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<h3 className="text-md font-medium">Project Templates</h3>
|
{projectColumns.map((column) => {
|
||||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
const columnProjects = getProjectsByStatus(column.status);
|
||||||
<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>
|
|
||||||
|
|
||||||
|
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">
|
<Badge variant="secondary" className="text-xs">
|
||||||
{template.tasks.length} tasks
|
{columnProjects.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
<div className="space-y-3 min-h-[400px]">
|
||||||
<div className="flex items-center gap-1">
|
{columnProjects.map((project) => (
|
||||||
<Clock className="w-3 h-3" />
|
<Card
|
||||||
<span>{getTotalEstimatedHours(template)}h estimated</span>
|
key={project.id}
|
||||||
</div>
|
draggable
|
||||||
<div className="flex items-center gap-1">
|
onDragStart={() => handleDragStart(project.id)}
|
||||||
<CalendarIcon className="w-3 h-3" />
|
onDragEnd={handleDragEnd}
|
||||||
<span>{Math.abs(Math.min(...template.tasks.map(t => t.dayOffset)))} day timeline</span>
|
className={`p-4 cursor-move hover-elevate transition-all ${
|
||||||
</div>
|
draggedProject === project.id ? 'opacity-50 scale-95' : ''
|
||||||
</div>
|
}`}
|
||||||
|
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="flex items-center gap-1">
|
||||||
<div className="text-xs font-medium text-muted-foreground">Sample tasks:</div>
|
<Button
|
||||||
<div className="space-y-1">
|
variant="ghost"
|
||||||
{template.tasks.slice(0, 3).map((task) => (
|
size="sm"
|
||||||
<div key={task.id} className="flex items-center justify-between text-xs">
|
onClick={() => handleEditProject(project)}
|
||||||
<span className="truncate flex-1">{task.title}</span>
|
data-testid={`button-edit-${project.id}`}
|
||||||
<Badge variant="outline" className="text-xs ml-2 flex-shrink-0">
|
>
|
||||||
{formatDayOffset(task.dayOffset)}
|
<Edit className="w-3 h-3" />
|
||||||
</Badge>
|
</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>
|
</div>
|
||||||
))}
|
</Card>
|
||||||
{template.tasks.length > 3 && (
|
))}
|
||||||
<div className="text-xs text-muted-foreground text-center py-1">
|
|
||||||
+{template.tasks.length - 3} more tasks
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 pt-2">
|
{columnProjects.length === 0 && (
|
||||||
<Button
|
<div className="text-center text-muted-foreground text-sm py-8 border-2 border-dashed border-muted rounded-lg">
|
||||||
variant="outline"
|
No projects in {column.title.toLowerCase()}
|
||||||
size="sm"
|
</div>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Launch Project Dialog */}
|
{/* Create Project Dialog */}
|
||||||
<Dialog open={isLaunchOpen} onOpenChange={setIsLaunchOpen}>
|
<Dialog open={isCreateProjectOpen} onOpenChange={setIsCreateProjectOpen}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle>Create New Project</DialogTitle>
|
||||||
<Play className="w-4 h-4" />
|
</DialogHeader>
|
||||||
Launch Project from Template
|
<div className="space-y-4 py-4">
|
||||||
</DialogTitle>
|
<div className="space-y-2">
|
||||||
</DialogHeader>
|
<label className="text-sm font-medium">Project Name</label>
|
||||||
|
|
||||||
{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>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Project name"
|
value={newProjectName}
|
||||||
value={projectName}
|
onChange={(e) => setNewProjectName(e.target.value)}
|
||||||
onChange={(e) => setProjectName(e.target.value)}
|
placeholder="Enter project name"
|
||||||
data-testid="input-project-name"
|
data-testid="input-project-name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
<div>
|
<label className="text-sm font-medium">Description (Optional)</label>
|
||||||
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
<Input
|
||||||
<PopoverTrigger asChild>
|
value={newProjectDescription}
|
||||||
<Button
|
onChange={(e) => setNewProjectDescription(e.target.value)}
|
||||||
variant="outline"
|
placeholder="Enter project description"
|
||||||
className="w-full justify-start"
|
data-testid="input-project-description"
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex gap-2">
|
||||||
</DialogContent>
|
<Button
|
||||||
</Dialog>
|
variant="outline"
|
||||||
</TabsContent>
|
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 */}
|
{/* Labels Section Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-md font-medium">Task Labels</h3>
|
<h3 className="text-md font-medium">Task Labels</h3>
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Search, Filter, SortAsc, Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
|
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 TaskCard from './TaskCard';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import TimeCompletionModal from './TimeCompletionModal';
|
import TimeCompletionModal from './TimeCompletionModal';
|
||||||
import { addDays, format, isSameDay, startOfToday } from 'date-fns';
|
import { addDays, format, isSameDay, startOfToday } from 'date-fns';
|
||||||
|
|
||||||
@@ -31,6 +32,13 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit }: T
|
|||||||
task: null
|
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
|
// Get 7 days starting from today for calendar
|
||||||
const dates = Array.from({ length: 7 }, (_, i) => addDays(calendarStartDate, i));
|
const dates = Array.from({ length: 7 }, (_, i) => addDays(calendarStartDate, i));
|
||||||
|
|
||||||
@@ -371,25 +379,35 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit }: T
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{dayTasks.slice(0, 2).map((task) => (
|
{dayTasks.slice(0, 2).map((task) => {
|
||||||
<div
|
// Find the label for this task
|
||||||
key={task.id}
|
const taskLabel = task.labelId && labels.length > 0 ? labels.find(label => label.id === task.labelId) : null;
|
||||||
draggable
|
|
||||||
onDragStart={(e) => handleDragStart(task.id, e)}
|
return (
|
||||||
onDragEnd={handleDragEnd}
|
<div
|
||||||
className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`}
|
key={task.id}
|
||||||
data-testid={`calendar-task-${task.id}`}
|
draggable
|
||||||
>
|
onDragStart={(e) => handleDragStart(task.id, e)}
|
||||||
<Badge
|
onDragEnd={handleDragEnd}
|
||||||
variant="outline"
|
className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`}
|
||||||
className="w-full justify-start text-xs p-1 h-auto text-left"
|
data-testid={`calendar-task-${task.id}`}
|
||||||
>
|
>
|
||||||
<div className="truncate text-xs">
|
<Badge
|
||||||
{task.title}
|
variant="outline"
|
||||||
</div>
|
className="w-full justify-start text-xs p-1 h-auto text-left"
|
||||||
</Badge>
|
style={taskLabel ? {
|
||||||
</div>
|
borderColor: taskLabel.color,
|
||||||
))}
|
borderWidth: '2px',
|
||||||
|
borderStyle: 'solid'
|
||||||
|
} : {}}
|
||||||
|
>
|
||||||
|
<div className="truncate text-xs">
|
||||||
|
{task.title}
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{dayTasks.length > 2 && (
|
{dayTasks.length > 2 && (
|
||||||
<Badge variant="secondary" className="w-full justify-center text-xs py-0">
|
<Badge variant="secondary" className="w-full justify-center text-xs py-0">
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Reference in New Issue
Block a user