Compare commits
1 Commits
87d5bfe23a
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
769a64f612 |
12
src/App.tsx
12
src/App.tsx
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { SEED_TASKS } from './data';
|
||||
import { SEED_TASKS, STATUS_LABELS } from './data';
|
||||
import type { Task, User, Status } from './data';
|
||||
import { LoginPage } from './Login';
|
||||
import { Sidebar } from './Sidebar';
|
||||
@@ -80,6 +80,13 @@ export default function App() {
|
||||
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);
|
||||
|
||||
@@ -100,7 +107,8 @@ export default function App() {
|
||||
)}
|
||||
{displayPage === 'kanban' && (
|
||||
<KanbanBoard tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
||||
onAddTask={handleKanbanAdd} filterUser={filterUser} searchQuery={searchQuery} />
|
||||
onAddTask={handleKanbanAdd} filterUser={filterUser} searchQuery={searchQuery}
|
||||
onMoveTask={handleMoveTask} />
|
||||
)}
|
||||
{displayPage === 'list' && (
|
||||
<ListView tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
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 }: { task: Task; onClick: () => void }) {
|
||||
function TaskCard({ task, onClick, onDragStart }: { task: Task; onClick: () => void; onDragStart: (e: React.DragEvent, task: Task) => void }) {
|
||||
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 }} onClick={onClick}>
|
||||
<div className="task-card" style={{ borderLeftColor: p.color, cursor: 'grab' }}
|
||||
draggable
|
||||
onDragStart={e => onDragStart(e, task)}
|
||||
onDragEnd={e => (e.currentTarget as HTMLElement).style.opacity = '1'}
|
||||
onClick={onClick}>
|
||||
<div className="task-card-row">
|
||||
<span className="task-card-title">{task.title}</span>
|
||||
<Avatar userId={task.assignee} size={24} />
|
||||
@@ -27,12 +32,20 @@ function TaskCard({ task, onClick }: { task: Task; onClick: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTask }: {
|
||||
function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTask, onDragStart, onDrop, isDragOver, onDragOver, onDragLeave }: {
|
||||
status: Status; statusLabel: string; tasks: Task[]; color: string;
|
||||
onTaskClick: (t: Task) => void; onAddTask: (s: Status) => void;
|
||||
onDragStart: (e: React.DragEvent, task: Task) => void;
|
||||
onDrop: (e: React.DragEvent, status: Status) => void;
|
||||
isDragOver: boolean;
|
||||
onDragOver: (e: React.DragEvent, status: Status) => void;
|
||||
onDragLeave: (e: React.DragEvent) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="kanban-column">
|
||||
<div className={`kanban-column ${isDragOver ? 'kanban-column-drag-over' : ''}`}
|
||||
onDragOver={e => onDragOver(e, status)}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={e => onDrop(e, status)}>
|
||||
<div className="kanban-col-header">
|
||||
<div className="kanban-col-dot" style={{ background: color }} />
|
||||
<span className="kanban-col-label">{statusLabel}</span>
|
||||
@@ -41,9 +54,11 @@ function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTas
|
||||
</div>
|
||||
<div className="kanban-col-body">
|
||||
{tasks.length === 0 ? (
|
||||
<div className="kanban-empty">No tasks here · Click + to add one</div>
|
||||
<div className={`kanban-empty ${isDragOver ? 'kanban-empty-active' : ''}`}>
|
||||
{isDragOver ? '⬇ Drop task here' : 'No tasks here · Click + to add one'}
|
||||
</div>
|
||||
) : (
|
||||
tasks.map(t => <TaskCard key={t.id} task={t} onClick={() => onTaskClick(t)} />)
|
||||
tasks.map(t => <TaskCard key={t.id} task={t} onClick={() => onTaskClick(t)} onDragStart={onDragStart} />)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,20 +68,58 @@ function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTas
|
||||
interface KanbanProps {
|
||||
tasks: Task[]; currentUser: User; onTaskClick: (t: Task) => void;
|
||||
onAddTask: (s: Status) => void; filterUser: string | null; searchQuery: string;
|
||||
onMoveTask: (taskId: string, newStatus: Status) => void;
|
||||
}
|
||||
|
||||
export function KanbanBoard({ tasks, currentUser, onTaskClick, onAddTask, filterUser, searchQuery }: KanbanProps) {
|
||||
export function KanbanBoard({ tasks, currentUser, onTaskClick, onAddTask, filterUser, searchQuery, onMoveTask }: KanbanProps) {
|
||||
const [dragOverColumn, setDragOverColumn] = useState<Status | 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 handleDragStart = (e: React.DragEvent, task: Task) => {
|
||||
e.dataTransfer.setData('text/plain', task.id);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
(e.currentTarget as HTMLElement).style.opacity = '0.4';
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, status: Status) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
setDragOverColumn(status);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
// Only clear if leaving the column entirely (not entering a child)
|
||||
const related = e.relatedTarget as HTMLElement | null;
|
||||
if (!related || !(e.currentTarget as HTMLElement).contains(related)) {
|
||||
setDragOverColumn(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, newStatus: Status) => {
|
||||
e.preventDefault();
|
||||
const taskId = e.dataTransfer.getData('text/plain');
|
||||
const task = tasks.find(t => t.id === taskId);
|
||||
if (task && task.status !== newStatus) {
|
||||
onMoveTask(taskId, newStatus);
|
||||
}
|
||||
setDragOverColumn(null);
|
||||
};
|
||||
|
||||
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} />
|
||||
tasks={filtered.filter(t => t.status === s)} onTaskClick={onTaskClick} onAddTask={onAddTask}
|
||||
onDragStart={handleDragStart}
|
||||
onDrop={handleDrop}
|
||||
isDragOver={dragOverColumn === s}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
2107
src/index.css
2107
src/index.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user