- Login with role-based auth (CTO/Manager/Employee) - Calendar view (month/week) with task chips and quick-add - Kanban board with status columns - Sortable list view with action menus - Task detail drawer with subtasks, comments, activity - Add task modal with validation - Dashboard with stats, workload, priority breakdown - Team tasks grouped by assignee - Reports page with recharts (bar, pie, line, horizontal bar) - Members page with invite modal - Search and assignee filter across views - ErrorBoundary for production error handling - Full dark design system via index.css
89 lines
5.0 KiB
TypeScript
89 lines
5.0 KiB
TypeScript
import { useState } from 'react';
|
|
import type { Task, User } from './data';
|
|
import { getUserById } from './data';
|
|
import { Avatar, PriorityBadge, StatusBadge } from './Shared';
|
|
|
|
interface ListProps {
|
|
tasks: Task[]; currentUser: User; onTaskClick: (t: Task) => void;
|
|
filterUser: string | null; searchQuery: string;
|
|
onToggleDone: (taskId: string) => void;
|
|
}
|
|
|
|
type SortKey = 'dueDate' | 'priority' | 'status' | 'assignee';
|
|
const PRIO_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
|
|
export function ListView({ tasks, currentUser, onTaskClick, filterUser, searchQuery, onToggleDone }: ListProps) {
|
|
const [sortBy, setSortBy] = useState<SortKey>('dueDate');
|
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
|
const [menuOpen, setMenuOpen] = useState<string | null>(null);
|
|
|
|
let filtered = tasks;
|
|
if (currentUser.role === 'employee') filtered = filtered.filter(t => t.assignee === currentUser.id);
|
|
if (filterUser) filtered = filtered.filter(t => t.assignee === filterUser);
|
|
if (searchQuery) filtered = filtered.filter(t => t.title.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
|
|
const sorted = [...filtered].sort((a, b) => {
|
|
let cmp = 0;
|
|
if (sortBy === 'dueDate') cmp = a.dueDate.localeCompare(b.dueDate);
|
|
else if (sortBy === 'priority') cmp = PRIO_ORDER[a.priority] - PRIO_ORDER[b.priority];
|
|
else if (sortBy === 'status') cmp = a.status.localeCompare(b.status);
|
|
else if (sortBy === 'assignee') cmp = a.assignee.localeCompare(b.assignee);
|
|
return sortDir === 'asc' ? cmp : -cmp;
|
|
});
|
|
|
|
const toggleSort = (key: SortKey) => {
|
|
if (sortBy === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
|
|
else { setSortBy(key); setSortDir('asc'); }
|
|
};
|
|
|
|
return (
|
|
<div className="list-view">
|
|
<div className="list-sort-row">
|
|
Sort by:
|
|
{(['dueDate', 'priority', 'status', 'assignee'] as SortKey[]).map(k => (
|
|
<button key={k} className={`list-sort-btn ${sortBy === k ? 'active' : ''}`} onClick={() => toggleSort(k)}>
|
|
{k === 'dueDate' ? 'Due Date' : k.charAt(0).toUpperCase() + k.slice(1)}
|
|
{sortBy === k && (sortDir === 'asc' ? ' ↑' : ' ↓')}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<table className="list-table">
|
|
<thead>
|
|
<tr><th>☐</th><th>Title</th><th>Assignee</th><th>Priority</th><th>Status</th><th>Due Date</th><th>Tags</th><th>Actions</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{sorted.map(t => {
|
|
const u = getUserById(t.assignee);
|
|
const due = new Date(t.dueDate + 'T00:00:00');
|
|
const overdue = due < new Date() && t.status !== 'done';
|
|
return (
|
|
<tr key={t.id}>
|
|
<td><input type="checkbox" checked={t.status === 'done'} onChange={() => onToggleDone(t.id)} /></td>
|
|
<td onClick={() => onTaskClick(t)} style={{ cursor: 'pointer' }}>{t.title}</td>
|
|
<td><div style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Avatar userId={t.assignee} size={20} />{u?.name}</div></td>
|
|
<td><PriorityBadge level={t.priority} /></td>
|
|
<td><StatusBadge status={t.status} /></td>
|
|
<td style={{ color: overdue ? '#ef4444' : undefined }}>{due.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</td>
|
|
<td>
|
|
{t.tags.slice(0, 2).map(tag => <span key={tag} className="tag-pill" style={{ marginRight: 4 }}>{tag}</span>)}
|
|
{t.tags.length > 2 && <span className="tag-pill">+{t.tags.length - 2}</span>}
|
|
</td>
|
|
<td style={{ position: 'relative' }}>
|
|
<button className="list-actions-btn" onClick={e => { e.stopPropagation(); setMenuOpen(menuOpen === t.id ? null : t.id); }}>⋯</button>
|
|
{menuOpen === t.id && (
|
|
<div className="list-dropdown">
|
|
<button className="list-dropdown-item" onClick={() => { onTaskClick(t); setMenuOpen(null); }}>Edit</button>
|
|
<button className="list-dropdown-item" onClick={() => setMenuOpen(null)}>Delete</button>
|
|
<button className="list-dropdown-item" onClick={() => setMenuOpen(null)}>Copy Link</button>
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|