105 lines
5.0 KiB
TypeScript
105 lines
5.0 KiB
TypeScript
import { useState } from 'react';
|
|
import type { Task, User, Status } from './data';
|
|
import { PRIORITY_COLORS, STATUS_COLORS, STATUS_LABELS } from './data';
|
|
import { Avatar, PriorityBadge, StatusBadge, ProgressBar } from './Shared';
|
|
|
|
function TaskCard({ task, onClick, users }: { task: Task; onClick: () => void; users: User[] }) {
|
|
const p = PRIORITY_COLORS[task.priority];
|
|
const due = new Date(task.dueDate + 'T00:00:00');
|
|
const overdue = due < new Date() && task.status !== 'done';
|
|
const commCount = task.comments.length;
|
|
return (
|
|
<div className="task-card" style={{ borderLeftColor: p.color }}
|
|
draggable
|
|
onDragStart={e => {
|
|
e.dataTransfer.setData('text/plain', task.id);
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
(e.currentTarget as HTMLElement).classList.add('dragging');
|
|
}}
|
|
onDragEnd={e => (e.currentTarget as HTMLElement).classList.remove('dragging')}
|
|
onClick={onClick}
|
|
>
|
|
<div className="task-card-row">
|
|
<span className="task-card-title">{task.title}</span>
|
|
<Avatar userId={task.assignee} size={24} users={users} />
|
|
</div>
|
|
<div className="task-card-badges">
|
|
<PriorityBadge level={task.priority} />
|
|
<StatusBadge status={task.status} />
|
|
{task.tags.slice(0, 2).map(t => <span key={t} className="tag-pill" style={{ fontSize: 9 }}>{t}</span>)}
|
|
</div>
|
|
{task.subtasks.length > 0 && <ProgressBar subtasks={task.subtasks} />}
|
|
<div className="task-card-meta">
|
|
<span className={overdue ? 'task-card-overdue' : ''}>📅 {due.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</span>
|
|
{commCount > 0 && <span>💬 {commCount}</span>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTask, onMoveTask, users }: {
|
|
status: Status; statusLabel: string; tasks: Task[]; color: string;
|
|
onTaskClick: (t: Task) => void; onAddTask: (s: Status) => void;
|
|
onMoveTask: (taskId: string, newStatus: Status) => void; users: User[];
|
|
}) {
|
|
const [dragOver, setDragOver] = useState(false);
|
|
|
|
return (
|
|
<div
|
|
className={`kanban-column ${dragOver ? 'kanban-column-dragover' : ''}`}
|
|
onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOver(true); }}
|
|
onDragEnter={e => { e.preventDefault(); setDragOver(true); }}
|
|
onDragLeave={e => {
|
|
// Only set false if leaving the column (not entering a child)
|
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOver(false);
|
|
}}
|
|
onDrop={e => {
|
|
e.preventDefault();
|
|
setDragOver(false);
|
|
const taskId = e.dataTransfer.getData('text/plain');
|
|
if (taskId) onMoveTask(taskId, status);
|
|
}}
|
|
>
|
|
<div className="kanban-col-header">
|
|
<div className="kanban-col-dot" style={{ background: color }} />
|
|
<span className="kanban-col-label">{statusLabel}</span>
|
|
<span className="kanban-col-count">{tasks.length}</span>
|
|
<button className="kanban-col-add" onClick={() => onAddTask(status)}>+</button>
|
|
</div>
|
|
<div className="kanban-col-body">
|
|
{tasks.length === 0 ? (
|
|
<div className="kanban-empty">
|
|
{dragOver ? '⬇ Drop here' : 'No tasks here · Click + to add one'}
|
|
</div>
|
|
) : (
|
|
tasks.map(t => <TaskCard key={t.id} task={t} onClick={() => onTaskClick(t)} users={users} />)
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface KanbanProps {
|
|
tasks: Task[]; currentUser: User; onTaskClick: (t: Task) => void;
|
|
onAddTask: (s: Status) => void; onMoveTask: (taskId: string, newStatus: Status) => void;
|
|
filterUser: string | null; searchQuery: string; users: User[];
|
|
}
|
|
|
|
export function KanbanBoard({ tasks, currentUser, onTaskClick, onAddTask, onMoveTask, filterUser, searchQuery, users }: KanbanProps) {
|
|
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 statuses: Status[] = ['todo', 'inprogress', 'review', 'done'];
|
|
return (
|
|
<div className="kanban-board">
|
|
{statuses.map(s => (
|
|
<KanbanColumn key={s} status={s} statusLabel={STATUS_LABELS[s]} color={STATUS_COLORS[s]}
|
|
tasks={filtered.filter(t => t.status === s)} onTaskClick={onTaskClick}
|
|
onAddTask={onAddTask} onMoveTask={onMoveTask} users={users} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|