Files
ColaFlow/docs/plans/sprint_4_story_1_task_3.md
Yaojia Wang 88d6413f81 feat(frontend): Create Sprint 4 Stories and Tasks for Story Management
Created comprehensive Story and Task files for Sprint 4 frontend implementation:

Story 1: Story Detail Page Foundation (P0 Critical - 3 days)
- 6 tasks: route creation, header, sidebar, data loading, Edit/Delete, responsive design
- Fixes critical 404 error when clicking Story cards
- Two-column layout consistent with Epic detail page

Story 2: Task Management in Story Detail (P0 Critical - 2 days)
- 6 tasks: API verification, hooks, TaskList, TaskCard, TaskForm, integration
- Complete Task CRUD with checkbox status toggle
- Filters, sorting, and optimistic UI updates

Story 3: Enhanced Story Form (P1 High - 2 days)
- 6 tasks: acceptance criteria, assignee selector, tags, story points, integration
- Aligns with UX design specification
- Backward compatible with existing Stories

Story 4: Quick Add Story Workflow (P1 High - 2 days)
- 5 tasks: inline form, keyboard shortcuts, batch creation, navigation
- Rapid Story creation with minimal fields
- Keyboard shortcut (Cmd/Ctrl + N)

Story 5: Story Card Component (P2 Medium - 1 day)
- 4 tasks: component variants, visual states, Task count, optimization
- Reusable component with list/kanban/compact variants
- React.memo optimization

Story 6: Kanban Story Creation Enhancement (P2 Optional - 2 days)
- 4 tasks: Epic card enhancement, inline form, animation, real-time updates
- Contextual Story creation from Kanban
- Stretch goal - implement only if ahead of schedule

Total: 6 Stories, 31 Tasks, 12 days estimated
Priority breakdown: P0 (2), P1 (2), P2 (2 optional)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 21:49:57 +01:00

10 KiB

task_id, story_id, sprint_id, status, type, assignee, created_date, estimated_hours
task_id story_id sprint_id status type assignee created_date estimated_hours
sprint_4_story_1_task_3 sprint_4_story_1 sprint_4 not_started frontend Frontend Developer 1 2025-11-05 4

Task 3: Implement Story Metadata Sidebar Component

Description

Create the metadata sidebar component that displays Story status, priority, assignee, time tracking, dates, and a parent Epic card. This sidebar provides quick access to Story metadata and context.

What to Do

  1. Create components/projects/story-metadata-sidebar.tsx
  2. Display Status dropdown (quick status change)
  3. Display Priority dropdown (quick priority change)
  4. Display Assignee with avatar and change button
  5. Display Time Tracking (Estimated vs Actual hours, progress bar)
  6. Display Dates (Created, Updated with relative time)
  7. Display Parent Epic card (title, status, progress, link)
  8. Add responsive design (sidebar moves to top on mobile)
  9. Integrate with Story update hooks

Files to Create/Modify

  • components/projects/story-metadata-sidebar.tsx (new, ~150-200 lines)
  • components/projects/epic-card-compact.tsx (optional, for parent Epic display)
  • app/(dashboard)/stories/[id]/page.tsx (modify, integrate sidebar)

Implementation Details

// components/projects/story-metadata-sidebar.tsx
'use client';

import { useState } from 'react';
import Link from 'next/link';
import { Calendar, Clock, User } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Progress } from '@/components/ui/progress';
import type { Story, Epic } from '@/types/project';
import { useChangeStoryStatus, useAssignStory } from '@/lib/hooks/use-stories';
import { formatDistanceToNow } from 'date-fns';

interface StoryMetadataSidebarProps {
  story: Story;
  parentEpic?: Epic;
}

export function StoryMetadataSidebar({ story, parentEpic }: StoryMetadataSidebarProps) {
  const changeStatus = useChangeStoryStatus();
  const assignStory = useAssignStory();

  const handleStatusChange = async (newStatus: string) => {
    await changeStatus.mutateAsync({ storyId: story.id, status: newStatus });
  };

  const completionPercentage = story.actualHours && story.estimatedHours
    ? Math.min(100, (story.actualHours / story.estimatedHours) * 100)
    : 0;

  return (
    <div className="space-y-6">
      {/* Status Section */}
      <Card className="p-4">
        <h3 className="text-sm font-medium mb-3">Status</h3>
        <Select
          value={story.status}
          onValueChange={handleStatusChange}
          disabled={changeStatus.isPending}
        >
          <SelectTrigger>
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="Backlog">Backlog</SelectItem>
            <SelectItem value="Todo">Todo</SelectItem>
            <SelectItem value="InProgress">In Progress</SelectItem>
            <SelectItem value="Done">Done</SelectItem>
          </SelectContent>
        </Select>
      </Card>

      {/* Priority Section */}
      <Card className="p-4">
        <h3 className="text-sm font-medium mb-3">Priority</h3>
        <Badge variant={getPriorityVariant(story.priority)} className="w-full justify-center">
          {story.priority}
        </Badge>
      </Card>

      {/* Assignee Section */}
      <Card className="p-4">
        <h3 className="text-sm font-medium mb-3">Assignee</h3>
        {story.assigneeId ? (
          <div className="flex items-center gap-3">
            <Avatar className="h-8 w-8">
              <AvatarImage src={`/avatars/${story.assigneeId}.png`} />
              <AvatarFallback>
                <User className="h-4 w-4" />
              </AvatarFallback>
            </Avatar>
            <div className="flex-1">
              <p className="text-sm font-medium">Assignee Name</p>
              <Button variant="link" size="sm" className="h-auto p-0 text-xs">
                Change
              </Button>
            </div>
          </div>
        ) : (
          <Button variant="outline" size="sm" className="w-full">
            <User className="h-4 w-4 mr-2" />
            Assign
          </Button>
        )}
      </Card>

      {/* Time Tracking Section */}
      <Card className="p-4">
        <h3 className="text-sm font-medium mb-3">Time Tracking</h3>
        <div className="space-y-3">
          <div className="flex justify-between text-sm">
            <span className="text-muted-foreground">Estimated</span>
            <span className="font-medium">{story.estimatedHours || 0}h</span>
          </div>
          <div className="flex justify-between text-sm">
            <span className="text-muted-foreground">Actual</span>
            <span className="font-medium">{story.actualHours || 0}h</span>
          </div>
          <Progress value={completionPercentage} className="h-2" />
          <p className="text-xs text-muted-foreground text-center">
            {Math.round(completionPercentage)}% complete
          </p>
        </div>
      </Card>

      {/* Dates Section */}
      <Card className="p-4">
        <h3 className="text-sm font-medium mb-3">Dates</h3>
        <div className="space-y-2 text-sm">
          <div className="flex items-center gap-2 text-muted-foreground">
            <Calendar className="h-4 w-4" />
            <span>Created {formatDistanceToNow(new Date(story.createdAt), { addSuffix: true })}</span>
          </div>
          <div className="flex items-center gap-2 text-muted-foreground">
            <Clock className="h-4 w-4" />
            <span>Updated {formatDistanceToNow(new Date(story.updatedAt), { addSuffix: true })}</span>
          </div>
        </div>
      </Card>

      {/* Parent Epic Section */}
      {parentEpic && (
        <Card className="p-4">
          <h3 className="text-sm font-medium mb-3">Parent Epic</h3>
          <Link
            href={`/epics/${parentEpic.id}`}
            className="block p-3 rounded-lg border hover:bg-accent transition-colors"
          >
            <div className="flex items-start justify-between mb-2">
              <h4 className="font-medium line-clamp-1">{parentEpic.title}</h4>
              <Badge variant="outline" className="text-xs">
                {parentEpic.status}
              </Badge>
            </div>
            <p className="text-xs text-muted-foreground">
              View Epic Details 
            </p>
          </Link>
        </Card>
      )}
    </div>
  );
}

function getPriorityVariant(priority: string) {
  const variants = {
    Low: 'default',
    Medium: 'warning',
    High: 'destructive',
    Critical: 'destructive',
  };
  return variants[priority] || 'default';
}

Integration in page.tsx:

// app/(dashboard)/stories/[id]/page.tsx
import { StoryMetadataSidebar } from '@/components/projects/story-metadata-sidebar';
import { useEpic } from '@/lib/hooks/use-epics';

// Inside component
const { data: story } = useStory(params.id);
const { data: parentEpic } = useEpic(story?.epicId);

<StoryMetadataSidebar story={story} parentEpic={parentEpic} />

Acceptance Criteria

  • Sidebar displays all metadata sections (Status, Priority, Assignee, Time, Dates, Parent Epic)
  • Status dropdown allows quick status changes
  • Status changes update immediately (optimistic UI)
  • Priority badge shows correct color
  • Assignee section shows avatar and name (if assigned)
  • Time tracking shows estimated/actual hours and progress bar
  • Progress bar correctly calculates percentage
  • Dates display in relative format ("2 hours ago", "3 days ago")
  • Parent Epic card is clickable and navigates to Epic detail
  • Parent Epic card shows Epic status and title
  • Responsive: Sidebar moves above content on mobile
  • Loading states show skeleton loaders

Testing

Manual Testing:

  1. View Story detail page
  2. Verify all metadata sections display correctly
  3. Change status via dropdown → Verify updates immediately
  4. Verify priority badge color matches priority
  5. Verify assignee displays correctly (if assigned)
  6. Verify time tracking shows correct hours and percentage
  7. Verify dates are in relative format
  8. Click parent Epic card → Navigates to Epic detail page
  9. Test on mobile → Sidebar moves above main content

Unit Test:

import { render, screen } from '@testing-library/react';
import { StoryMetadataSidebar } from './story-metadata-sidebar';

describe('StoryMetadataSidebar', () => {
  const mockStory = {
    id: '123',
    status: 'InProgress',
    priority: 'High',
    assigneeId: 'user-1',
    estimatedHours: 16,
    actualHours: 8,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  };

  const mockEpic = {
    id: 'epic-1',
    title: 'Parent Epic',
    status: 'InProgress',
  };

  it('renders all metadata sections', () => {
    render(<StoryMetadataSidebar story={mockStory} parentEpic={mockEpic} />);
    expect(screen.getByText('Status')).toBeInTheDocument();
    expect(screen.getByText('Priority')).toBeInTheDocument();
    expect(screen.getByText('Assignee')).toBeInTheDocument();
    expect(screen.getByText('Time Tracking')).toBeInTheDocument();
    expect(screen.getByText('Dates')).toBeInTheDocument();
    expect(screen.getByText('Parent Epic')).toBeInTheDocument();
  });

  it('calculates time tracking percentage correctly', () => {
    render(<StoryMetadataSidebar story={mockStory} parentEpic={mockEpic} />);
    expect(screen.getByText('50% complete')).toBeInTheDocument();
  });
});

Dependencies

Prerequisites:

  • Task 1 (page structure must exist)
  • useChangeStoryStatus() hook (already exists)
  • useAssignStory() hook (already exists)
  • shadcn/ui Card, Badge, Select, Avatar, Progress components
  • date-fns for relative time formatting

Blocks:

  • None (independent component)

Estimated Time

4 hours

Notes

Code Reuse: Copy sidebar structure from Epic detail page if similar metadata sidebar exists. The parent Epic card is similar to showing a parent Project card in Epic sidebar - reuse that pattern.

Future Enhancement: Assignee selector will be enhanced in Story 3 (Enhanced Story Form).