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 →