// Reporting view — tabs, presets, burnup, gecikme dağılımı, verimlilik skoru const RPIcons = window.Icons; const { parseDateKey: rpDate, toDateKey: rpDateKey, COLUMNS: RP_COLS, isOverdue: rpIsOverdue } = window.PMStore; // ── Donut chart — saf SVG ──────────────────────────────────── function rpPolarToCart(cx, cy, r, deg) { const rad = (deg - 90) * Math.PI / 180; return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)]; } function rpArcPath(cx, cy, R, r, s, e) { if (Math.abs(e - s) >= 360) e = s + 359.99; const [ax, ay] = rpPolarToCart(cx, cy, R, s); const [bx, by] = rpPolarToCart(cx, cy, R, e); const [ex, ey] = rpPolarToCart(cx, cy, r, e); const [fx, fy] = rpPolarToCart(cx, cy, r, s); const lg = (e - s) > 180 ? 1 : 0; return `M${ax},${ay}A${R},${R} 0 ${lg},1 ${bx},${by}L${ex},${ey}A${r},${r} 0 ${lg},0 ${fx},${fy}Z`; } function DonutChart({ segments, size = 168, thickness = 36, centerVal, centerLbl, onSlice }) { const [hov, setHov] = React.useState(null); const cx = size / 2, cy = size / 2; const R = size / 2 - 6, r = R - thickness; const total = segments.reduce((s, d) => s + d.value, 0); if (!total) return
Veri yok
; let angle = 0; const slices = segments.map(seg => { const sweep = (seg.value / total) * 360; const path = rpArcPath(cx, cy, R, r, angle, angle + sweep); angle += sweep; return { ...seg, path, pct: Math.round(seg.value / total * 100) }; }); const active = hov !== null ? slices[hov] : null; return (
{slices.map((sl, i) => ( setHov(i)} onMouseLeave={() => setHov(null)} onClick={() => { if (onSlice && sl.id) onSlice(sl.id); }} /> ))} {active ? ( <> {active.value} {active.label.length > 15 ? active.label.slice(0, 14) + '…' : active.label} {active.pct}% ) : ( <> {centerVal} {centerLbl} )}
{slices.map((sl, i) => (
setHov(i)} onMouseLeave={() => setHov(null)}> {sl.label} {sl.value} {sl.pct}%
))}
); } // ── Burnup Chart (8 haftalık) ───────────────────────────────── function BurnupChart({ tasks }) { const now = new Date(); now.setHours(0,0,0,0); let cumAdded = 0, cumDone = 0; const data = Array.from({ length: 8 }, (_, i) => { const wend = new Date(now); wend.setDate(now.getDate() - (7 - i - 1) * 7); const wstart = new Date(wend); wstart.setDate(wend.getDate() - 6); const added = tasks.filter(t => { if (!t.createdAt) return false; const d = new Date(t.createdAt); return d >= wstart && d <= wend; }).length; const done = tasks.filter(t => { if (t.status !== 'done') return false; const ts = t.completedAt || t.updatedAt; if (!ts) return false; const d = new Date(ts); return d >= wstart && d <= wend; }).length; cumAdded += added; cumDone += done; return { cumAdded, cumDone, label: `${wend.getDate()}/${wend.getMonth()+1}` }; }); const maxVal = Math.max(...data.map(d => d.cumAdded), 1); const W = 500, H = 140, pL = 32, pR = 10, pT = 10, pB = 28; const cW = W - pL - pR, cH = H - pT - pB; const step = cW / (data.length - 1); const px = i => pL + i * step; const py = v => pT + cH - (v / maxVal) * cH; const addedPath = data.map((d, i) => `${i===0?'M':'L'}${px(i).toFixed(1)},${py(d.cumAdded).toFixed(1)}`).join(' '); const donePath = data.map((d, i) => `${i===0?'M':'L'}${px(i).toFixed(1)},${py(d.cumDone).toFixed(1)}`).join(' '); return (
{[0, 0.5, 1].map((r, i) => ( ))} {data.map((d, i) => ( {d.label} ))}
— Eklenen (kümülatif) —— Tamamlanan (kümülatif)
); } // ── Gecikme Dağılımı ───────────────────────────────────────── function GecikmeDistribution({ tasks }) { const now = new Date(); now.setHours(0,0,0,0); const buckets = [ { label: '1–3 gün', color: '#f59e0b', count: 0 }, { label: '4–7 gün', color: '#f97316', count: 0 }, { label: '8–14 gün', color: '#ef4444', count: 0 }, { label: '14+ gün', color: '#7f1d1d', count: 0 }, ]; tasks.forEach(t => { if (t.status === 'done' || !t.due) return; const due = rpDate(t.due); due.setHours(0,0,0,0); const diff = Math.round((now - due) / 86400000); if (diff <= 0) return; if (diff <= 3) buckets[0].count++; else if (diff <= 7) buckets[1].count++; else if (diff <= 14) buckets[2].count++; else buckets[3].count++; }); const total = buckets.reduce((s, b) => s + b.count, 0); if (!total) return
Gecikmiş görev yok ✓
; const maxB = Math.max(...buckets.map(b => b.count), 1); return (
{buckets.map((b, i) => (
{b.label}
0 ? b.color : 'var(--text-faint)' }}> {b.count}
))}
{total} gecikmiş görev toplam
); } // ── Gün × Saat Isı Haritası ────────────────────────────────── function HeatmapChart({ tasks }) { const DAYS = ['Pzt','Sal','Çar','Per','Cum','Cmt','Paz']; const BLOCKS = ['00–04','04–08','08–12','12–16','16–20','20–24']; const grid = Array.from({ length: 7 }, () => Array(6).fill(0)); let max = 0; tasks.forEach(t => { if (t.status !== 'done' || !t.completedAt) return; const d = new Date(t.completedAt); const day = (d.getDay() + 6) % 7; const block = Math.floor(d.getHours() / 4); grid[day][block]++; if (grid[day][block] > max) max = grid[day][block]; }); if (!max) return
Tamamlanma verisi yok
; return (
{BLOCKS.map(b => {b})}
{DAYS.map((day, di) => (
{day} {grid[di].map((v, bi) => ( ))}
))}
); } // ── Dönem karşılaştırma rozeti ─────────────────────────────── function MetricDelta({ cur, prev, suffix, invert }) { if (prev === null || prev === undefined || cur === null || cur === undefined) return null; const c = parseFloat(cur), p = parseFloat(prev); if (isNaN(c) || isNaN(p)) return null; const diff = c - p; if (Math.abs(diff) < 0.05) return — önceki dönemle aynı; const up = diff > 0; const good = invert ? !up : up; const txt = Number.isInteger(Math.abs(diff)) ? Math.abs(diff) : Math.abs(diff).toFixed(1); return ( {up ? '▲' : '▼'} {txt}{suffix || ''} önceki döneme göre ); } // ── Ana Rapor Görünümü ──────────────────────────────────────── function ReportView({ state, onSelectTask }) { const [tab, setTab] = React.useState('genel'); const [projectFilter, setProjectFilter] = React.useState('all'); const [dateFrom, setDateFrom] = React.useState(''); const [dateTo, setDateTo] = React.useState(''); const [activePreset, setActivePreset] = React.useState(''); const projects = state.projects || []; const allTasks = state.tasks || []; const tags = state.tags || []; // Hızlı dönem presetleri const applyPreset = (preset) => { const now = new Date(); const pad = n => String(n).padStart(2,'0'); const fmt = d => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; let from = '', to = ''; if (preset === 'week') { const start = new Date(now); start.setDate(now.getDate() - ((now.getDay()+6)%7)); const end = new Date(start); end.setDate(start.getDate() + 6); from = fmt(start); to = fmt(end); } else if (preset === 'month') { from = fmt(new Date(now.getFullYear(), now.getMonth(), 1)); to = fmt(new Date(now.getFullYear(), now.getMonth()+1, 0)); } else if (preset === 'year') { from = `${now.getFullYear()}-01-01`; to = `${now.getFullYear()}-12-31`; } setDateFrom(from); setDateTo(to); setActivePreset(from === '' && to === '' ? 'all' : preset); }; const clearDates = () => { setDateFrom(''); setDateTo(''); setActivePreset('all'); }; const fromDate = dateFrom ? new Date(dateFrom + 'T00:00:00') : null; const toDate = dateTo ? new Date(dateTo + 'T23:59:59') : null; const tasks = allTasks.filter(t => { if (projectFilter !== 'all' && t.projectId !== projectFilter) return false; if (fromDate || toDate) { const ref = t.completedAt ? new Date(t.completedAt) : (t.createdAt ? new Date(t.createdAt) : null); if (!ref) return !fromDate; if (fromDate && ref < fromDate) return false; if (toDate && ref > toDate) return false; } return true; }); const projById = Object.fromEntries(projects.map(p => [p.id, p])); const tagById = Object.fromEntries(tags.map(t => [t.id, t])); const done = tasks.filter(t => t.status === 'done'); // Önceki dönem metrikleri — yalnızca tarih aralığı seçiliyken hesaplanır const prevMetrics = React.useMemo(() => { if (!fromDate || !toDate) return null; const span = toDate - fromDate; const prevTo = new Date(fromDate.getTime() - 1); const prevFrom = new Date(fromDate.getTime() - span - 1); const prevTasks = allTasks.filter(t => { if (projectFilter !== 'all' && t.projectId !== projectFilter) return false; const ref = t.completedAt ? new Date(t.completedAt) : (t.createdAt ? new Date(t.createdAt) : null); return ref && ref >= prevFrom && ref <= prevTo; }); const prevDone = prevTasks.filter(t => t.status === 'done'); const prevTimes = prevDone .filter(t => t.createdAt && (t.completedAt || t.updatedAt)) .map(t => (new Date(t.completedAt || t.updatedAt) - new Date(t.createdAt)) / 86400000); return { total: prevTasks.length, rate: prevTasks.length ? Math.round(prevDone.length / prevTasks.length * 100) : 0, avgDays: prevTimes.length ? (prevTimes.reduce((a, b) => a + b, 0) / prevTimes.length).toFixed(1) : null, }; }, [dateFrom, dateTo, projectFilter, allTasks.length]); const doneTs = (t) => t.completedAt || t.updatedAt; const completionTimes = done .filter(t => t.createdAt && doneTs(t)) .map(t => (new Date(doneTs(t)) - new Date(t.createdAt)) / 86400000); const avgDays = completionTimes.length ? (completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length).toFixed(1) : null; const totalMinutes = tasks.reduce((s, t) => s + (t.timeLog || []).reduce((ss, e) => ss + (e.duration || 0), 0), 0); const fmtTime = (mins) => { if (!mins) return '0s'; const h = Math.floor(mins/60), m = mins%60; return m > 0 ? `${h}s ${m}dk` : `${h}s`; }; const weeklyData = React.useMemo(() => { const weeks = []; const now = new Date(); now.setHours(0,0,0,0); for (let w = 7; w >= 0; w--) { const start = new Date(now); start.setDate(start.getDate() - w*7 - now.getDay() + 1); const end = new Date(start); end.setDate(end.getDate() + 6); end.setHours(23,59,59); const label = `${start.getDate()}/${start.getMonth()+1}`; const count = done.filter(t => { const ts = doneTs(t); if (!ts) return false; const d = new Date(ts); return d >= start && d <= end; }).length; weeks.push({ label, count }); } return weeks; }, [done.length, projectFilter, dateFrom, dateTo]); const maxWeekly = Math.max(...weeklyData.map(w => w.count), 1); const projectBreakdown = projects.map(p => { const ptasks = tasks.filter(t => t.projectId === p.id); const byStatus = {}; RP_COLS.forEach(c => { byStatus[c.id] = ptasks.filter(t => t.status === c.id).length; }); const timeMin = ptasks.reduce((s, t) => s + (t.timeLog||[]).reduce((ss,e) => ss+(e.duration||0), 0), 0); return { project: p, total: ptasks.length, byStatus, timeMin }; }).filter(r => r.total > 0); // Etiket × durum çapraz analizi const tagStats = tags.map(tag => { const tt = tasks.filter(t => t.tags?.includes(tag.id)); const doneC = tt.filter(t => t.status === 'done').length; const overC = tt.filter(t => t.status !== 'done' && rpIsOverdue(t.due, t.status)).length; return { tag, count: tt.length, doneC, overC, actC: tt.length - doneC - overC }; }).filter(r => r.count > 0).sort((a,b) => b.count - a.count).slice(0, 10); const maxTag = Math.max(...tagStats.map(t => t.count), 1); const prioCounts = { high: tasks.filter(t => t.priority === 'high').length, medium: tasks.filter(t => t.priority === 'medium').length, low: tasks.filter(t => t.priority === 'low').length, }; const prioTotal = Object.values(prioCounts).reduce((a,b) => a+b, 0) || 1; const recentDone = [...done] .filter(t => doneTs(t)) .sort((a,b) => new Date(doneTs(b)) - new Date(doneTs(a))) .slice(0, 6); const completionRate = tasks.length ? Math.round(done.length / tasks.length * 100) : 0; const projectDonutData = [...projects] .map(p => ({ id: p.id, label: p.name, value: tasks.filter(t => t.projectId === p.id).length, color: p.color })) .filter(d => d.value > 0).sort((a,b) => b.value - a.value); const statusDonutData = [ { label: 'Yapılacak', value: tasks.filter(t => t.status === 'todo').length, color: '#94a3b8' }, { label: 'Devam Eden', value: tasks.filter(t => t.status === 'doing').length, color: '#3b82f6' }, { label: 'İncelemede', value: tasks.filter(t => t.status === 'review').length, color: '#a855f7' }, { label: 'Tamamlandı', value: tasks.filter(t => t.status === 'done').length, color: '#ff601f' }, ].filter(d => d.value > 0); const prioDonutData = [ { label: 'Yüksek', value: prioCounts.high, color: '#ef4444' }, { label: 'Orta', value: prioCounts.medium, color: '#f59e0b' }, { label: 'Düşük', value: prioCounts.low, color: '#3b82f6' }, ].filter(d => d.value > 0); const timeByProject = projectBreakdown .filter(r => r.timeMin > 0).sort((a,b) => b.timeMin - a.timeMin).slice(0, 5); const maxTime = Math.max(...timeByProject.map(r => r.timeMin), 1); // Zamanında teslim — tamamlanan görevlerin kaçı teslim tarihini aşmadan bitti? const doneWithDue = done.filter(t => t.due && doneTs(t)); const onTimeCount = doneWithDue.filter(t => rpDateKey(new Date(doneTs(t))) <= t.due).length; const lateDoneCount = doneWithDue.length - onTimeCount; const onTimeRate = doneWithDue.length ? Math.round(onTimeCount / doneWithDue.length * 100) : null; // Verimlilik Skoru — gecikme bileşeni hem aktif gecikmeleri hem geç tamamlananları sayar const overdueCount = tasks.filter(t => t.status !== 'done' && rpIsOverdue(t.due, t.status)).length; const lateBlend = tasks.length > 0 ? (overdueCount + lateDoneCount) / tasks.length : 0; const scoreCompletion = Math.round(completionRate * 0.4); const scoreTime = avgDays ? Math.round(Math.max(0, (1 - Math.min(parseFloat(avgDays), 20)/20)) * 30) : 15; const scoreOverdue = Math.round((1 - Math.min(lateBlend, 1)) * 30); const perfScore = scoreCompletion + scoreTime + scoreOverdue; const perfLabel = perfScore >= 80 ? 'Mükemmel 🏆' : perfScore >= 60 ? 'İyi 👍' : perfScore >= 40 ? 'Orta ⚡' : 'Geliştirilmeli ⚠️'; const perfColor = perfScore >= 80 ? '#22c55e' : perfScore >= 60 ? '#3b82f6' : perfScore >= 40 ? '#f59e0b' : '#ef4444'; // Verimlilik skoru geçmişi — haftalık snapshot (yalnızca filtresiz görünümde kaydedilir) const [perfHistory, setPerfHistory] = React.useState([]); React.useEffect(() => { let hist = []; try { hist = JSON.parse(localStorage.getItem('navia_perf_history') || '[]'); } catch (e) { hist = []; } if (projectFilter === 'all' && !dateFrom && !dateTo && allTasks.length > 0) { const monday = new Date(); monday.setHours(0,0,0,0); monday.setDate(monday.getDate() - ((monday.getDay() + 6) % 7)); const wk = rpDateKey(monday); const last = hist[hist.length - 1]; if (last && last.week === wk) last.score = perfScore; else hist.push({ week: wk, score: perfScore }); if (hist.length > 12) hist = hist.slice(hist.length - 12); localStorage.setItem('navia_perf_history', JSON.stringify(hist)); } setPerfHistory(hist); }, [perfScore, projectFilter, dateFrom, dateTo]); // Tahmini bitiş — son 4 haftanın tamamlama hızına göre const activeCount = tasks.filter(t => t.status !== 'done').length; const fourWeeksAgo = new Date(); fourWeeksAgo.setDate(fourWeeksAgo.getDate() - 28); const recentDoneCount = done.filter(t => doneTs(t) && new Date(doneTs(t)) >= fourWeeksAgo).length; const weeklyRate = recentDoneCount / 4; const forecastWeeks = weeklyRate > 0 && activeCount > 0 ? Math.ceil(activeCount / weeklyRate) : null; // Sprint hızı (velocity) — kapanmış sprintler const completedSprints = (state.sprints || []) .filter(sp => sp.status === 'completed' && sp.velocityPoints != null) .sort((a, b) => (a.endDate || '').localeCompare(b.endDate || '')) .slice(-8); const maxVel = Math.max(...completedSprints.map(sp => sp.velocityPoints), 1); const avgVel = completedSprints.length ? Math.round(completedSprints.reduce((s, sp) => s + sp.velocityPoints, 0) / completedSprints.length) : 0; // Gerçek kârlılık — finans işlemleri + işçilik maliyeti (saat ücreti × süre) const txInRange = (state.transactions || []).filter(tx => { if (projectFilter !== 'all' && tx.projectId !== projectFilter) return false; if (!fromDate && !toDate) return true; const d = new Date((tx.date || '1970-01-01') + 'T12:00:00'); if (fromDate && d < fromDate) return false; if (toDate && d > toDate) return false; return true; }); const profitRows = projects.map(p => { const ptx = txInRange.filter(tx => tx.projectId === p.id && (!tx.status || tx.status === 'done')); const income = ptx.filter(tx => tx.type === 'income') .reduce((s, tx) => s + (tx.amount || 0), 0); const expense = ptx.filter(tx => tx.type === 'expense').reduce((s, tx) => s + (tx.amount || 0), 0); const pb = projectBreakdown.find(r => r.project.id === p.id); const laborCost = p.hourlyRate ? Math.round(((pb ? pb.timeMin : 0) / 60) * p.hourlyRate) : 0; return { project: p, income, expense, laborCost, net: income - expense - laborCost }; }).filter(r => r.income > 0 || r.expense > 0 || r.laborCost > 0) .sort((a, b) => b.net - a.net); // PDF / yazdır — yazdırma sırasında geçici olarak aydınlık temaya geç const exportPDF = () => { const root = document.documentElement; const prevTheme = root.dataset.theme; root.dataset.theme = 'light'; setTimeout(() => { window.print(); root.dataset.theme = prevTheme; }, 100); }; const exportCSV = () => { const rows = [ ['Başlık','Firma','Durum','Öncelik','Teslim','Tamamlanma','Story Point','Toplam Süre (dk)'], ...tasks.map(t => [ `"${(t.title||'').replace(/"/g,'""')}"`, `"${(projById[t.projectId]?.name||'').replace(/"/g,'""')}"`, t.status, t.priority, t.due||'', t.completedAt ? new Date(t.completedAt).toLocaleDateString('tr-TR') : '', t.storyPoints||'', (t.timeLog||[]).reduce((s,e) => s+(e.duration||0), 0), ].join(',')) ]; const blob = new Blob([rows.join('\n')], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `navia-rapor-${new Date().toISOString().slice(0,10)}.csv`; a.click(); URL.revokeObjectURL(url); }; const TABS = [ { id: 'genel', label: 'Genel' }, { id: 'gorevler', label: 'Görevler' }, { id: 'firmalar', label: 'Firmalar' }, ]; const PRESETS = [ { id: 'week', label: 'Bu Hafta' }, { id: 'month', label: 'Bu Ay' }, { id: 'year', label: 'Bu Yıl' }, { id: 'all', label: 'Tümü' }, ]; return (
{/* ── Header ─────────────────────────────────────────── */}

Raporlar

{/* Hızlı dönem butonları */}
{PRESETS.map(p => ( ))}
{ setDateFrom(e.target.value); setActivePreset(''); }} title="Başlangıç tarihi" /> { setDateTo(e.target.value); setActivePreset(''); }} title="Bitiş tarihi" /> {(dateFrom || dateTo) && ( )}
{/* Cetvel imzası */}