- Implement native HTML5 Drag and Drop API on task cards - Cards show grab cursor and reduce opacity while dragging - Drop zones highlight with indigo glow and pulse animation - Moving a task updates its status and logs an activity entry - Added handleMoveTask to App.tsx with STATUS_LABELS import - CSS: drag-over styles, pulse keyframes, grab/grabbing cursors
148 lines
7.3 KiB
TypeScript
148 lines
7.3 KiB
TypeScript
import { useState } from 'react';
|
|
import { SEED_TASKS, STATUS_LABELS } from './data';
|
|
import type { Task, User, Status } from './data';
|
|
import { LoginPage } from './Login';
|
|
import { Sidebar } from './Sidebar';
|
|
import { TopNavbar, BottomToggleBar } from './NavBars';
|
|
import { CalendarView, QuickAddPanel } from './Calendar';
|
|
import { KanbanBoard } from './Kanban';
|
|
import { ListView } from './ListView';
|
|
import { TaskDrawer, AddTaskModal } from './TaskDrawer';
|
|
import { DashboardPage } from './Dashboard';
|
|
import { TeamTasksPage, MembersPage } from './Pages';
|
|
import { ReportsPage } from './Reports';
|
|
import './index.css';
|
|
|
|
const PAGE_TITLES: Record<string, string> = {
|
|
dashboard: 'Dashboard', calendar: 'Calendar', kanban: 'Kanban Board',
|
|
mytasks: 'My Tasks', teamtasks: 'Team Tasks', reports: 'Reports', members: 'Members',
|
|
list: 'List View',
|
|
};
|
|
|
|
const VIEW_PAGES = ['calendar', 'kanban', 'list'];
|
|
|
|
export default function App() {
|
|
const now = new Date();
|
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
|
const [tasks, setTasks] = useState<Task[]>(SEED_TASKS);
|
|
const [activePage, setActivePage] = useState('calendar');
|
|
const [activeView, setActiveView] = useState('calendar');
|
|
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
const [addModalDefaults, setAddModalDefaults] = useState<{ date?: string; status?: Status }>({});
|
|
const [calMonth, setCalMonth] = useState({ year: now.getFullYear(), month: now.getMonth() });
|
|
const [calView, setCalView] = useState('month');
|
|
const [filterUser, setFilterUser] = useState<string | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [quickAddDay, setQuickAddDay] = useState<{ date: string; rect: { top: number; left: number } } | null>(null);
|
|
|
|
if (!currentUser) return <LoginPage onLogin={u => { setCurrentUser(u); setActivePage('calendar'); setActiveView('calendar'); }} />;
|
|
|
|
const handleNavigate = (page: string) => {
|
|
setActivePage(page);
|
|
if (VIEW_PAGES.includes(page)) setActiveView(page);
|
|
};
|
|
|
|
const handleViewChange = (view: string) => {
|
|
setActiveView(view);
|
|
if (VIEW_PAGES.includes(view)) setActivePage(view);
|
|
};
|
|
|
|
const handleTaskClick = (t: Task) => setActiveTask(t);
|
|
|
|
const handleDayClick = (date: string, el: HTMLElement) => {
|
|
const rect = el.getBoundingClientRect();
|
|
setQuickAddDay({ date, rect: { top: rect.bottom, left: rect.left } });
|
|
};
|
|
|
|
const handleQuickAdd = (partial: Partial<Task>) => {
|
|
const task: Task = {
|
|
id: `t${Date.now()}`, title: partial.title || '', description: partial.description || '',
|
|
status: (partial.status || 'todo') as Status, priority: partial.priority || 'medium',
|
|
assignee: partial.assignee || 'u1', reporter: currentUser.id, dueDate: partial.dueDate || '',
|
|
tags: partial.tags || [], subtasks: partial.subtasks || [], comments: partial.comments || [],
|
|
activity: [{ id: `a${Date.now()}`, text: '📝 Task created', timestamp: new Date().toISOString() }],
|
|
};
|
|
setTasks(prev => [...prev, task]);
|
|
setQuickAddDay(null);
|
|
};
|
|
|
|
const handleAddTask = (task: Task) => setTasks(prev => [...prev, { ...task, reporter: currentUser.id }]);
|
|
|
|
const handleUpdateTask = (updated: Task) => {
|
|
setTasks(prev => prev.map(t => t.id === updated.id ? updated : t));
|
|
setActiveTask(updated);
|
|
};
|
|
|
|
const handleNewTask = () => { setAddModalDefaults({}); setShowAddModal(true); };
|
|
const handleKanbanAdd = (status: Status) => { setAddModalDefaults({ status }); setShowAddModal(true); };
|
|
const handleToggleDone = (taskId: string) => {
|
|
setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: t.status === 'done' ? 'todo' : 'done' as Status } : t));
|
|
};
|
|
|
|
const handleMoveTask = (taskId: string, newStatus: Status) => {
|
|
setTasks(prev => prev.map(t => t.id === taskId ? {
|
|
...t, status: newStatus,
|
|
activity: [...t.activity, { id: `a${Date.now()}`, text: `🔄 ${currentUser.name} moved task to ${STATUS_LABELS[newStatus]}`, timestamp: new Date().toISOString() }]
|
|
} : t));
|
|
};
|
|
|
|
const displayPage = VIEW_PAGES.includes(activePage) ? activeView : activePage;
|
|
const filteredMyTasks = tasks.filter(t => t.assignee === currentUser.id);
|
|
|
|
const pageTitle = PAGE_TITLES[displayPage] || 'Calendar';
|
|
|
|
return (
|
|
<div className="app-shell">
|
|
<TopNavbar title={pageTitle} filterUser={filterUser} onFilterChange={setFilterUser}
|
|
searchQuery={searchQuery} onSearch={setSearchQuery} onNewTask={handleNewTask} />
|
|
<div className="app-body">
|
|
<Sidebar currentUser={currentUser} activePage={activePage} onNavigate={handleNavigate}
|
|
onSignOut={() => { setCurrentUser(null); setActivePage('calendar'); setActiveView('calendar'); }} />
|
|
<div className="main-content">
|
|
{displayPage === 'calendar' && (
|
|
<CalendarView tasks={tasks} currentUser={currentUser} calMonth={calMonth} calView={calView}
|
|
onMonthChange={setCalMonth} onViewChange={setCalView} onTaskClick={handleTaskClick}
|
|
onDayClick={handleDayClick} filterUser={filterUser} searchQuery={searchQuery} />
|
|
)}
|
|
{displayPage === 'kanban' && (
|
|
<KanbanBoard tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
|
onAddTask={handleKanbanAdd} filterUser={filterUser} searchQuery={searchQuery}
|
|
onMoveTask={handleMoveTask} />
|
|
)}
|
|
{displayPage === 'list' && (
|
|
<ListView tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
|
filterUser={filterUser} searchQuery={searchQuery} onToggleDone={handleToggleDone} />
|
|
)}
|
|
{displayPage === 'dashboard' && <DashboardPage tasks={tasks} currentUser={currentUser} />}
|
|
{displayPage === 'mytasks' && (
|
|
<ListView tasks={filteredMyTasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
|
filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} />
|
|
)}
|
|
{displayPage === 'teamtasks' && <TeamTasksPage tasks={tasks} currentUser={currentUser} />}
|
|
{displayPage === 'reports' && <ReportsPage tasks={tasks} />}
|
|
{displayPage === 'members' && <MembersPage tasks={tasks} />}
|
|
</div>
|
|
</div>
|
|
|
|
{VIEW_PAGES.includes(activePage) && (
|
|
<BottomToggleBar activeView={activeView} onViewChange={handleViewChange} />
|
|
)}
|
|
|
|
{activeTask && <TaskDrawer task={activeTask} currentUser={currentUser} onClose={() => setActiveTask(null)} onUpdate={handleUpdateTask} />}
|
|
{showAddModal && <AddTaskModal onClose={() => setShowAddModal(false)} onAdd={handleAddTask} defaultDate={addModalDefaults.date} defaultStatus={addModalDefaults.status} />}
|
|
|
|
{quickAddDay && (
|
|
<div style={{ position: 'fixed', inset: 0, zIndex: 199 }} onClick={() => setQuickAddDay(null)}>
|
|
<div style={{ position: 'absolute', top: Math.min(quickAddDay.rect.top, window.innerHeight - 280), left: Math.min(quickAddDay.rect.left, window.innerWidth - 340) }}
|
|
onClick={e => e.stopPropagation()}>
|
|
<QuickAddPanel date={quickAddDay.date} onAdd={handleQuickAdd}
|
|
onOpenFull={() => { setAddModalDefaults({ date: quickAddDay.date }); setShowAddModal(true); setQuickAddDay(null); }}
|
|
onClose={() => setQuickAddDay(null)} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|