// Sprint view: Backlog + Sprint board + Burndown + Story points + Review modal + Velocity const SPIcons = window.Icons; const { COLUMNS: SP_COLS, parseDateKey: spDate, relativeDate: spRel, isOverdue: spOver } = window.PMStore; const { Tag: SPTag, Priority: SPPriority } = window.UI; const SP_POINTS = [1, 2, 3, 5, 8, 13]; const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 }; function SprintView({ state, onSelectTask, onUpdateTask, onAddTask, onAddSprint, onUpdateSprint, onDeleteSprint, onCloseSprint, onMoveToSprint }) { const sprints = (state.sprints || []).filter(s => s.projectId === state.activeProjectId); const activeSprint = sprints.find(s => s.status === 'active') || sprints.find(s => s.status === 'planning') || null; const [selectedSprintId, setSelectedSprintId] = React.useState(() => activeSprint?.id || null); const [showCreate, setShowCreate] = React.useState(false); const [showEdit, setShowEdit] = React.useState(false); const [showReview, setShowReview] = React.useState(false); const [dragTaskId, setDragTaskId] = React.useState(null); const [dragTarget, setDragTarget] = React.useState(null); const [backlogSort, setBacklogSort] = React.useState('order'); React.useEffect(() => { const active = sprints.find(s => s.status === 'active') || sprints.find(s => s.status === 'planning'); if (active && !selectedSprintId) setSelectedSprintId(active.id); }, [state.activeProjectId]); const sprint = sprints.find(s => s.id === selectedSprintId) || null; const projectTasks = state.tasks.filter(t => t.projectId === state.activeProjectId && !t.archived); const backlogTasksRaw = projectTasks.filter(t => !t.sprintId); const backlogTasks = [...backlogTasksRaw].sort((a, b) => { if (backlogSort === 'priority') return (PRIORITY_ORDER[a.priority] ?? 1) - (PRIORITY_ORDER[b.priority] ?? 1); if (backlogSort === 'due') { if (!a.due && !b.due) return 0; if (!a.due) return 1; if (!b.due) return -1; return spDate(a.due) - spDate(b.due); } return (a.order ?? 0) - (b.order ?? 0); }); const sprintTasks = sprint ? projectTasks.filter(t => t.sprintId === sprint.id) : []; const usePoints = sprintTasks.some(t => t.storyPoints); const plannedPts = sprintTasks.reduce((s, t) => s + (t.storyPoints || 0), 0); const donePts = sprintTasks.filter(t => t.status === 'done').reduce((s, t) => s + (t.storyPoints || 0), 0); const capacity = sprint?.capacity || 0; const completedSprints = sprints.filter(s => s.status === 'completed' && s.velocityPoints != null).slice(-6); const handleDrop = (target) => { if (!dragTaskId) return; const spId = target === 'backlog' ? null : target; onMoveToSprint ? onMoveToSprint(dragTaskId, spId) : onUpdateTask(dragTaskId, { sprintId: spId }); setDragTaskId(null); setDragTarget(null); }; const handleMoveTask = (taskId, spId) => { onMoveToSprint ? onMoveToSprint(taskId, spId) : onUpdateTask(taskId, { sprintId: spId }); }; const handleStartSprint = () => onUpdateSprint(sprint.id, { status: 'active' }); const handleReviewConfirm = ({ taskDisposition, velocityPoints }) => { onCloseSprint(sprint.id, { velocityPoints, taskDisposition }); setShowReview(false); setSelectedSprintId(null); }; return (
{/* ── Sprint seçici + meta ─────────────────────────────── */}
{sprint && sprint.status !== 'completed' && ( )}
{sprint && (
{sprint.status === 'planning' ? 'Planlama' : sprint.status === 'active' ? 'Aktif' : 'Tamamlandı'} {sprint.startDate && sprint.endDate && ( {sprint.startDate} → {sprint.endDate} )} {sprint.goal && "{sprint.goal}"} {capacity > 0 && ( capacity ? 'var(--p-high)' : undefined }}> {plannedPts}/{capacity} SP )} {!capacity && usePoints && ( {plannedPts} SP · {donePts} tamamlandı )}
{sprint.status === 'planning' && ( )} {sprint.status === 'active' && ( )}
)}
{/* ── Burndown + velocity ──────────────────────────────── */} {sprint && sprint.startDate && sprint.endDate && (
Burndown
{completedSprints.length > 0 && }
)} {/* ── Backlog | Sprint board ───────────────────────────── */}
{/* Backlog */}
{ e.preventDefault(); setDragTarget('backlog'); }} onDragLeave={() => setDragTarget(null)} onDrop={() => handleDrop('backlog')} >
Backlog {backlogTasks.length}
{backlogTasks.length === 0 &&
Backlog boş
} {backlogTasks.map(t => ( onSelectTask(t.id)} onDragStart={() => setDragTaskId(t.id)} onDragEnd={() => { setDragTaskId(null); setDragTarget(null); }} onMoveToSprint={sprint ? () => handleMoveTask(t.id, sprint.id) : null} onUpdatePoints={(pts) => onUpdateTask(t.id, { storyPoints: pts })} /> ))}
{/* Sprint */}
{ e.preventDefault(); setDragTarget(sprint?.id || 'sprint'); }} onDragLeave={() => setDragTarget(null)} onDrop={() => sprint && handleDrop(sprint.id)} >
{sprint?.name || 'Sprint'} {sprintTasks.length} {sprint && ( )}
{!sprint ? (
Sprint seçin veya oluşturun
) : (
{sprintTasks.length === 0 && (
Sprint boş — backlog'dan görev sürükleyin
)} {SP_COLS.map(col => { const colTasks = sprintTasks.filter(t => t.status === col.id); if (!colTasks.length) return null; return (
{col.label} {colTasks.length}
{colTasks.map(t => ( onSelectTask(t.id)} onDragStart={() => setDragTaskId(t.id)} onDragEnd={() => { setDragTaskId(null); setDragTarget(null); }} onMoveToSprint={() => handleMoveTask(t.id, null)} moveLabel="Backlog'a taşı" onUpdatePoints={(pts) => onUpdateTask(t.id, { storyPoints: pts })} /> ))}
); })}
)}
{/* ── Modals ──────────────────────────────────────────── */} {showCreate && ( { onAddSprint(data); setShowCreate(false); }} onClose={() => setShowCreate(false)} /> )} {showEdit && sprint && ( { onUpdateSprint(sprint.id, data); setShowEdit(false); }} onClose={() => setShowEdit(false)} onDelete={() => { onDeleteSprint(sprint.id); setSelectedSprintId(null); setShowEdit(false); }} onCloseSprint={sprint.status === 'active' ? () => { setShowEdit(false); setShowReview(true); } : null} /> )} {showReview && sprint && ( s.id !== sprint.id && s.status !== 'completed')} onConfirm={handleReviewConfirm} onClose={() => setShowReview(false)} /> )}
); } /* ── Sprint istatistik satırı ──────────────────────────────── */ function SprintStats({ tasks, usePoints }) { const done = tasks.filter(t => t.status === 'done').length; const total = tasks.length; const pct = total ? Math.round(done / total * 100) : 0; const donePts = tasks.filter(t => t.status === 'done').reduce((s, t) => s + (t.storyPoints || 0), 0); const totalPts = tasks.reduce((s, t) => s + (t.storyPoints || 0), 0); return (
{done}/{total} görev {usePoints && {donePts}/{totalPts} SP} {pct}%
); } /* ── Burndown grafiği ──────────────────────────────────────── */ function BurndownChart({ sprint, tasks, usePoints }) { const start = new Date(sprint.startDate + 'T00:00:00'); const end = new Date(sprint.endDate + 'T23:59:59'); const totalDays = Math.max(1, Math.round((end - start) / 86400000)); const val = (t) => usePoints ? (t.storyPoints || 1) : 1; const totalValue = tasks.reduce((s, t) => s + val(t), 0); if (!totalValue) return (
Sprint'e görev eklendikçe burndown grafiği görünecek.
); const today = new Date(); const points = []; for (let d = 0; d <= totalDays; d++) { const dayEnd = new Date(start); dayEnd.setDate(dayEnd.getDate() + d); dayEnd.setHours(23, 59, 59); const remaining = tasks.reduce((s, t) => { if (!t.completedAt) return s + val(t); return new Date(t.completedAt) > dayEnd ? s + val(t) : s; }, 0); points.push({ d, remaining }); } const W = 500, H = 150, pL = 32, pR = 12, pT = 8, pB = 24; const cW = W - pL - pR, cH = H - pT - pB; const x = (d) => pL + (d / totalDays) * cW; const y = (v) => pT + cH - (v / totalValue) * cH; const todayDay = Math.min(totalDays, Math.max(0, Math.round((today - start) / 86400000))); const idealPath = `M ${x(0)} ${y(totalValue)} L ${x(totalDays)} ${y(0)}`; const actualPts = points.filter(p => p.d <= todayDay); const actualPath = actualPts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${x(p.d)} ${y(p.remaining)}`).join(' '); return ( {[0, 0.5, 1].map(v => ( ))} {[totalValue, Math.round(totalValue / 2), 0].map(v => ( {v} ))} {actualPath && } {todayDay > 0 && todayDay < totalDays && ( )} {sprint.startDate?.slice(5)} {sprint.endDate?.slice(5)} İdeal Gerçek ); } /* ── Velocity çubuk grafiği ────────────────────────────────── */ function VelocityBar({ sprints }) { const max = Math.max(...sprints.map(s => s.velocityPoints || 0), 1); const avg = Math.round(sprints.reduce((s, sp) => s + sp.velocityPoints, 0) / sprints.length); return (
Sprint hızı Ort. {avg}
{sprints.map(s => (
{s.velocityPoints}
{s.name.length > 7 ? s.name.slice(0, 7) + '…' : s.name}
))}
); } /* ── Sprint görev kartı ────────────────────────────────────── */ function SprintTaskCard({ task, state, onSelect, onDragStart, onDragEnd, onMoveToSprint, moveLabel, onUpdatePoints }) { const tagById = Object.fromEntries(state.tags.map(t => [t.id, t])); const taskTags = (task.tags || []).map(id => tagById[id]).filter(Boolean); const overdue = spOver(task.due, task.status); const [showPts, setShowPts] = React.useState(false); const ptsRef = React.useRef(null); React.useEffect(() => { if (!showPts) return; const close = (e) => { if (ptsRef.current && !ptsRef.current.contains(e.target)) setShowPts(false); }; document.addEventListener('mousedown', close); return () => document.removeEventListener('mousedown', close); }, [showPts]); return (
{task.title}
{ e.stopPropagation(); setShowPts(v => !v); }}> {task.storyPoints ?? '?'} {showPts && (
{SP_POINTS.map(p => ( ))}
)}
{taskTags.length > 0 && (
{taskTags.slice(0, 3).map(tag => )}
)}
{task.due && ( {spRel(task.due)} )} {onMoveToSprint && ( )}
); } /* ── Sprint oluştur / düzenle modal ───────────────────────── */ function SprintModal({ title, initial, projectId, onSave, onClose, onDelete, onCloseSprint }) { const [name, setName] = React.useState(initial?.name || ''); const [goal, setGoal] = React.useState(initial?.goal || ''); const [startDate, setStart] = React.useState(initial?.startDate || ''); const [endDate, setEnd] = React.useState(initial?.endDate || ''); const [capacity, setCap] = React.useState(initial?.capacity || ''); const save = () => { if (!name.trim()) return; onSave({ projectId, name: name.trim(), goal: goal.trim(), startDate: startDate || null, endDate: endDate || null, capacity: capacity ? parseInt(capacity, 10) : null, }); }; return (
e.stopPropagation()}>

{title}

{onDelete && ( )} {onCloseSprint && ( )}
); } /* ── Sprint kapanış review modal ──────────────────────────── */ function SprintReviewModal({ sprint, tasks, nextSprints, onConfirm, onClose }) { const done = tasks.filter(t => t.status === 'done'); const unfinished = tasks.filter(t => t.status !== 'done'); const usePoints = tasks.some(t => t.storyPoints); const donePts = done.reduce((s, t) => s + (t.storyPoints || 0), 0); const totalPts = tasks.reduce((s, t) => s + (t.storyPoints || 0), 0); const velocity = usePoints ? donePts : done.length; const [disposition, setDisposition] = React.useState( () => Object.fromEntries(unfinished.map(t => [t.id, 'backlog'])) ); const allToBacklog = () => setDisposition(Object.fromEntries(unfinished.map(t => [t.id, 'backlog']))); const allRollover = (spId) => setDisposition(Object.fromEntries(unfinished.map(t => [t.id, spId]))); return (
e.stopPropagation()}>

Sprint Kapanışı · {sprint.name}

{/* Özet istatistikler */}
{done.length} Tamamlandı
{unfinished.length} Tamamlanmadı
{usePoints && (
{donePts}/{totalPts} Story point
)}
{velocity} {usePoints ? 'SP hızı' : 'Görev hızı'}
{/* Tamamlanmayan görevler */} {unfinished.length > 0 && (
Tamamlanmayan görevler
{nextSprints.slice(0, 1).map(ns => ( ))}
{unfinished.map(t => (
{t.title} {t.storyPoints && {t.storyPoints}}
{nextSprints.map(ns => ( ))}
))}
)}
); } window.SprintView = SprintView;