---
task_id: sprint_4_story_2_task_3
story_id: sprint_4_story_2
sprint_id: sprint_4
status: not_started
type: frontend
assignee: Frontend Developer 2
created_date: 2025-11-05
estimated_hours: 4
---

# Task 3: Implement TaskList Component with Filters and Sorting

## Description

Create the TaskList component that displays all Tasks for a Story, with filtering by status/priority/assignee and sorting capabilities. This component is the container for Task management UI.

## What to Do

1. Create `components/projects/task-list.tsx`
2. Display Task count badge (e.g., "Tasks (8)")
3. Add "Add Task" button to open inline form
4. Implement Task filters (All, Todo, InProgress, Done)
5. Implement Task sorting (Priority, Status, Created date, Assignee)
6. Show empty state when no Tasks exist
7. Render TaskCard components for each Task
8. Add loading skeleton during data fetch
9. Integrate with useTasks hook
10. Test with various filter/sort combinations

## Files to Create

- `components/projects/task-list.tsx` (new, ~300-400 lines)

## Implementation Details

```typescript
// components/projects/task-list.tsx
'use client';

import { useState, useMemo } from 'react';
import { Plus, Filter, ArrowUpDown } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { TaskCard } from './task-card';
import { TaskForm } from './task-form';
import { useTasks } from '@/lib/hooks/use-tasks';
import type { Task, WorkItemStatus, WorkItemPriority } from '@/types/project';

interface TaskListProps {
  storyId: string;
  readonly?: boolean;
}

type FilterStatus = 'All' | WorkItemStatus;
type FilterPriority = 'All' | WorkItemPriority;
type SortOption = 'priority' | 'status' | 'createdAt' | 'assignee';

export function TaskList({ storyId, readonly = false }: TaskListProps) {
  // State
  const [showAddForm, setShowAddForm] = useState(false);
  const [filterStatus, setFilterStatus] = useState<FilterStatus>('All');
  const [filterPriority, setFilterPriority] = useState<FilterPriority>('All');
  const [sortBy, setSortBy] = useState<SortOption>('priority');

  // Fetch Tasks
  const { data: tasks = [], isLoading, error } = useTasks(storyId);

  // Filter and sort Tasks
  const filteredAndSortedTasks = useMemo(() => {
    let result = [...tasks];

    // Apply status filter
    if (filterStatus !== 'All') {
      result = result.filter((task) => task.status === filterStatus);
    }

    // Apply priority filter
    if (filterPriority !== 'All') {
      result = result.filter((task) => task.priority === filterPriority);
    }

    // Apply sorting
    result.sort((a, b) => {
      switch (sortBy) {
        case 'priority':
          return getPriorityValue(b.priority) - getPriorityValue(a.priority);
        case 'status':
          return getStatusValue(a.status) - getStatusValue(b.status);
        case 'createdAt':
          return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
        case 'assignee':
          return (a.assigneeId || '').localeCompare(b.assigneeId || '');
        default:
          return 0;
      }
    });

    return result;
  }, [tasks, filterStatus, filterPriority, sortBy]);

  // Task count
  const taskCount = tasks.length;
  const completedCount = tasks.filter((t) => t.status === 'Done').length;

  // Loading state
  if (isLoading) {
    return <TaskListSkeleton />;
  }

  // Error state
  if (error) {
    return (
      <Card className="p-6">
        <p className="text-sm text-destructive">Failed to load Tasks. Please try again.</p>
      </Card>
    );
  }

  return (
    <div className="space-y-4">
      {/* Header */}
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-2">
          <h2 className="text-lg font-semibold">Tasks</h2>
          <Badge variant="secondary" className="text-xs">
            {taskCount} total
          </Badge>
          {completedCount > 0 && (
            <Badge variant="success" className="text-xs">
              {completedCount} done
            </Badge>
          )}
        </div>

        {!readonly && (
          <Button
            onClick={() => setShowAddForm(true)}
            size="sm"
            data-testid="add-task-button"
          >
            <Plus className="h-4 w-4 mr-2" />
            Add Task
          </Button>
        )}
      </div>

      {/* Filters and Sorting */}
      {taskCount > 0 && (
        <div className="flex items-center gap-3 flex-wrap">
          <div className="flex items-center gap-2">
            <Filter className="h-4 w-4 text-muted-foreground" />
            <span className="text-sm text-muted-foreground">Filter:</span>
          </div>

          <Select value={filterStatus} onValueChange={(v) => setFilterStatus(v as FilterStatus)}>
            <SelectTrigger className="w-[140px] h-8">
              <SelectValue placeholder="Status" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="All">All Status</SelectItem>
              <SelectItem value="Backlog">Backlog</SelectItem>
              <SelectItem value="Todo">Todo</SelectItem>
              <SelectItem value="InProgress">In Progress</SelectItem>
              <SelectItem value="Done">Done</SelectItem>
            </SelectContent>
          </Select>

          <Select value={filterPriority} onValueChange={(v) => setFilterPriority(v as FilterPriority)}>
            <SelectTrigger className="w-[140px] h-8">
              <SelectValue placeholder="Priority" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="All">All Priority</SelectItem>
              <SelectItem value="Low">Low</SelectItem>
              <SelectItem value="Medium">Medium</SelectItem>
              <SelectItem value="High">High</SelectItem>
              <SelectItem value="Critical">Critical</SelectItem>
            </SelectContent>
          </Select>

          <div className="flex items-center gap-2 ml-auto">
            <ArrowUpDown className="h-4 w-4 text-muted-foreground" />
            <span className="text-sm text-muted-foreground">Sort:</span>
            <Select value={sortBy} onValueChange={(v) => setSortBy(v as SortOption)}>
              <SelectTrigger className="w-[140px] h-8">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="priority">Priority</SelectItem>
                <SelectItem value="status">Status</SelectItem>
                <SelectItem value="createdAt">Created Date</SelectItem>
                <SelectItem value="assignee">Assignee</SelectItem>
              </SelectContent>
            </Select>
          </div>
        </div>
      )}

      {/* Add Task Form */}
      {showAddForm && (
        <Card className="p-4 border-primary">
          <TaskForm
            storyId={storyId}
            onSuccess={() => setShowAddForm(false)}
            onCancel={() => setShowAddForm(false)}
          />
        </Card>
      )}

      {/* Task List */}
      {filteredAndSortedTasks.length > 0 ? (
        <div className="space-y-2">
          {filteredAndSortedTasks.map((task) => (
            <TaskCard key={task.id} task={task} readonly={readonly} />
          ))}
        </div>
      ) : (
        <EmptyState
          hasFilters={filterStatus !== 'All' || filterPriority !== 'All'}
          onReset={() => {
            setFilterStatus('All');
            setFilterPriority('All');
          }}
          onAddTask={() => setShowAddForm(true)}
          readonly={readonly}
        />
      )}
    </div>
  );
}

// Helper functions for sorting
function getPriorityValue(priority: WorkItemPriority): number {
  const values = { Low: 1, Medium: 2, High: 3, Critical: 4 };
  return values[priority] || 0;
}

function getStatusValue(status: WorkItemStatus): number {
  const values = { Backlog: 1, Todo: 2, InProgress: 3, Done: 4 };
  return values[status] || 0;
}

// Empty state component
function EmptyState({
  hasFilters,
  onReset,
  onAddTask,
  readonly,
}: {
  hasFilters: boolean;
  onReset: () => void;
  onAddTask: () => void;
  readonly: boolean;
}) {
  if (hasFilters) {
    return (
      <Card className="p-8 text-center">
        <p className="text-sm text-muted-foreground mb-4">
          No Tasks match the selected filters.
        </p>
        <Button onClick={onReset} variant="outline" size="sm">
          Reset Filters
        </Button>
      </Card>
    );
  }

  return (
    <Card className="p-8 text-center">
      <div className="text-4xl mb-4">☑️</div>
      <h3 className="text-lg font-semibold mb-2">No Tasks Yet</h3>
      <p className="text-sm text-muted-foreground mb-4">
        Break down this Story into technical Tasks to track implementation progress.
      </p>
      {!readonly && (
        <Button onClick={onAddTask} size="sm">
          <Plus className="h-4 w-4 mr-2" />
          Add Your First Task
        </Button>
      )}
    </Card>
  );
}

// Loading skeleton
function TaskListSkeleton() {
  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <Skeleton className="h-6 w-32" />
        <Skeleton className="h-9 w-24" />
      </div>
      <Skeleton className="h-8 w-full" />
      <div className="space-y-2">
        <Skeleton className="h-20 w-full" />
        <Skeleton className="h-20 w-full" />
        <Skeleton className="h-20 w-full" />
      </div>
    </div>
  );
}
```

## Acceptance Criteria

- [ ] TaskList component displays all Tasks for Story
- [ ] Task count badge shows total and completed count
- [ ] "Add Task" button opens inline Task form
- [ ] Status filter works (All, Backlog, Todo, InProgress, Done)
- [ ] Priority filter works (All, Low, Medium, High, Critical)
- [ ] Sorting works (Priority, Status, Created date, Assignee)
- [ ] Filters and sorting can be combined
- [ ] Empty state displays when no Tasks exist
- [ ] Empty state with filters shows "Reset Filters" button
- [ ] Loading skeleton displays during data fetch
- [ ] Error state displays on fetch failure
- [ ] Component integrates with useTasks hook
- [ ] Readonly mode hides "Add Task" button
- [ ] Performance: Filters/sorts without lag (<100ms)

## Testing

**Manual Testing**:
1. Navigate to Story detail page
2. Verify Task count badge shows correctly (e.g., "8 total, 3 done")
3. Click "Add Task" → Verify inline form appears
4. Create 5 Tasks with different statuses and priorities
5. Test status filter → Select "Done" → Verify only Done Tasks show
6. Test priority filter → Select "High" → Verify only High priority Tasks show
7. Test combined filters → Status: InProgress + Priority: High
8. Test sorting → Sort by Priority → Verify High/Critical at top
9. Test sorting → Sort by Status → Verify Backlog → Todo → InProgress → Done
10. Reset filters → Verify all Tasks show again
11. Test empty state (Story with no Tasks) → Verify helpful message

**Unit Test**:
```typescript
import { render, screen, fireEvent } from '@testing-library/react';
import { TaskList } from './task-list';

describe('TaskList', () => {
  const mockTasks = [
    { id: '1', title: 'Task 1', status: 'Todo', priority: 'High' },
    { id: '2', title: 'Task 2', status: 'Done', priority: 'Low' },
    { id: '3', title: 'Task 3', status: 'InProgress', priority: 'Critical' },
  ];

  it('renders task count badge', () => {
    render(<TaskList storyId="story-123" />);
    expect(screen.getByText('3 total')).toBeInTheDocument();
    expect(screen.getByText('1 done')).toBeInTheDocument();
  });

  it('filters tasks by status', () => {
    render(<TaskList storyId="story-123" />);
    fireEvent.click(screen.getByText('All Status'));
    fireEvent.click(screen.getByText('Done'));
    expect(screen.getAllByTestId('task-card')).toHaveLength(1);
    expect(screen.getByText('Task 2')).toBeInTheDocument();
  });

  it('sorts tasks by priority', () => {
    render(<TaskList storyId="story-123" />);
    const tasks = screen.getAllByTestId('task-card');
    expect(tasks[0]).toHaveTextContent('Task 3'); // Critical first
    expect(tasks[1]).toHaveTextContent('Task 1'); // High second
  });
});
```

## Dependencies

**Prerequisites**:
- Task 2 (useTasks hook must exist)
- TaskCard component (Task 4 - can use placeholder initially)
- TaskForm component (Task 5 - can use placeholder initially)
- ✅ shadcn/ui Card, Button, Badge, Select components

**Blocks**:
- Task 6 (Story detail page integration)

## Estimated Time

4 hours

## Notes

**Performance**: Use `useMemo` for filtering/sorting to avoid recalculating on every render. With 100+ Tasks, this prevents performance issues.

**Empty State**: Different empty states for "no Tasks" vs "no Tasks matching filters" improve UX.

**Progressive Enhancement**: Component works with TaskCard and TaskForm placeholders initially. Replace with real components in Tasks 4 and 5.
