Skip to content

Chương 13: Sprint Planning & Analytics

"Lan: 'Tôi cần nhìn thấy velocity, burndown chart, ai đang overloaded.' Đức code dashboard trong 2 giờ với AI. Nhưng Hà phát hiện: 'Dashboard load mất 8 giây — N+1 query!'"


🎯 Mục tiêu

  • Sprint model & planning
  • Analytics dashboard (burndown, velocity)
  • 🐛 Bug #7: N+1 query
  • 🐛 Bug #12: Magic numbers

Phần 1: Sprint Management (25 phút)

API

bash
antigravity "@cm-execution Build sprint management:
File: src/routes/sprints.ts

Endpoints:
- POST /api/sprints — create sprint (name, start_date, end_date, project_id)
- GET /api/sprints?project_id=xxx — list sprints
- PUT /api/sprints/:id — update (activate, complete)
- GET /api/sprints/:id/stats — sprint statistics

Stats include:
- total tasks, completed, in progress
- completion percentage
- velocity (tasks/day)
- burndown data points"

Phần 2: Analytics Dashboard (30 phút)

Stats API

typescript
// src/services/analytics.ts

// 🐛 BUG #7: N+1 Query
export async function getProjectStats(projectId: string) {
  const { data: tasks } = await supabase
    .from('tasks')
    .select('*')
    .eq('project_id', projectId)

  // N+1: Fetching assignee for EACH task separately!
  const enriched = await Promise.all(
    tasks!.map(async (task) => {
      const { data: user } = await supabase
        .from('profiles')
        .select('name, avatar_url')
        .eq('id', task.assignee_id)
        .single()
      return { ...task, assignee: user }
    })
  )

  // 🐛 BUG #12: Magic numbers
  const velocity = enriched.filter(t => t.status === 'done').length / 14 // What is 14?!
  const health = velocity > 0.5 ? 'good' : velocity > 0.3 ? 'warning' : 'danger'

  return {
    total: enriched.length,
    done: enriched.filter(t => t.status === 'done').length,
    in_progress: enriched.filter(t => t.status === 'in_progress').length,
    velocity,
    health
  }
}

Fix Bug #7: Join Instead of N+1

typescript
// ✅ Fixed: Single query with join
export async function getProjectStats(projectId: string) {
  const { data: tasks } = await supabase
    .from('tasks')
    .select('*, assignee:profiles!assignee_id(name, avatar_url)')
    .eq('project_id', projectId)

  // One query instead of N+1!
  return computeStats(tasks!)
}

Fix Bug #12: Named Constants

typescript
// ✅ Fixed: Named constant
const SPRINT_DURATION_DAYS = 14
const VELOCITY_GOOD_THRESHOLD = 0.5
const VELOCITY_WARNING_THRESHOLD = 0.3

const velocity = doneCount / SPRINT_DURATION_DAYS
const health = velocity > VELOCITY_GOOD_THRESHOLD ? 'good'
  : velocity > VELOCITY_WARNING_THRESHOLD ? 'warning' : 'danger'

Phần 3: Burndown Chart (25 phút)

javascript
// Frontend: Chart.js burndown
async function renderBurndown(sprintId) {
  const res = await fetch(`/api/sprints/${sprintId}/burndown`)
  const data = await res.json()

  new Chart(document.getElementById('burndown'), {
    type: 'line',
    data: {
      labels: data.dates,
      datasets: [
        {
          label: 'Ideal',
          data: data.ideal,
          borderColor: '#6366f1',
          borderDash: [5, 5]
        },
        {
          label: 'Actual',
          data: data.actual,
          borderColor: '#22c55e',
          fill: true,
          backgroundColor: 'rgba(34, 197, 94, 0.1)'
        }
      ]
    }
  })
}

Phần 4: Lab — Team Dashboard (15 phút)

bash
antigravity "Build analytics dashboard page:
- Sprint selector dropdown
- Burndown chart (Chart.js)
- Task distribution pie chart (by status)
- Team workload bar chart (tasks per person)
- Velocity trend line
- Dark theme matching TeamFlow design"

Homework

  • [ ] Sprint CRUD working
  • [ ] Dashboard with charts
  • [ ] Bug #7 and #12 fixed
  • [ ] Performance < 500ms load time

Chương tiếp: PRD, User Stories & Product Thinking →

Powered by CodyMaster × VitePress