// 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 ─────────────────────────────────────────── */}
{/* Cetvel imzası */}
{/* ── Metric cards ───────────────────────────────────── */}
{tasks.length}
Toplam Görev
{prevMetrics && }
{completionRate}%
Tamamlanma Oranı
{prevMetrics && }
{avgDays ?? '—'}
Ort. Tamamlanma (gün)
{prevMetrics && avgDays !== null && prevMetrics.avgDays !== null &&
}
{onTimeRate !== null ? `${onTimeRate}%` : '—'}
Zamanında Teslim
{onTimeRate !== null && {onTimeCount}/{doneWithDue.length} görev }
{activeCount}
Aktif Görev
{fmtTime(totalMinutes)}
Toplam Süre
{perfScore}
Verimlilik Skoru
{perfLabel}
{/* ── Sekmeler ───────────────────────────────────────── */}
{TABS.map(t => (
setTab(t.id)}>
{t.label}
))}
{/* Boş dönem uyarısı */}
{tasks.length === 0 && (
Seçili filtrelerde görev bulunamadı
{ clearDates(); setProjectFilter('all'); }}>
Filtreleri temizle
)}
{/* ══════════════════════════════════════════════════════
SEKME 1: GENEL
══════════════════════════════════════════════════════ */}
{tasks.length > 0 && tab === 'genel' && (
<>
{/* Donut üçlüsü */}
Firma Bazlı Dağılım · dilime tıkla, filtrele
setProjectFilter(id)} />
{/* Verimlilik detay + son tamamlananlar */}
Verimlilik Analizi
Tamamlanma
{scoreCompletion}/40
Gecikme
0 ? '#f59e0b' : '#22c55e' }} />
{scoreOverdue}/30
{perfLabel}
{perfHistory.length >= 2 && (
`${(i / (perfHistory.length - 1)) * 116 + 2},${26 - (h.score / 100) * 24}`).join(' ')} />
Skor trendi · son {perfHistory.length} hafta
)}
Son Tamamlananlar
{recentDone.length === 0 ? (
Henüz tamamlanan görev yok
) : (
{recentDone.map(t => {
const proj = projById[t.projectId];
return (
onSelectTask(t.id)}>
{t.title}
{t.completedAt ? new Date(t.completedAt).toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' }) : ''}
);
})}
)}
>
)}
{/* ══════════════════════════════════════════════════════
SEKME 2: GÖREVLER
══════════════════════════════════════════════════════ */}
{tasks.length > 0 && tab === 'gorevler' && (
{/* Haftalık tamamlanma */}
Son 8 Hafta — Tamamlanan Görevler
{weeklyData.map((w, i) => (
))}
{/* Burnup */}
Burnup — Eklenen vs Tamamlanan (8 hafta)
{forecastWeeks !== null
? <>Mevcut hızla (~{weeklyRate.toFixed(1)} görev/hafta) aktif {activeCount} görev ~{forecastWeeks} hafta içinde tamamlanır.>
: activeCount === 0
? 'Aktif görev yok — her şey tamamlandı 🎉'
: 'Tahmin için son 4 haftada yeterli tamamlama verisi yok.'}
{/* Gün × saat ısı haritası */}
Üretkenlik Isı Haritası — Gün × Saat
{/* Sprint velocity */}
{completedSprints.length > 0 && (
Sprint Hızı (Velocity)
{completedSprints.map(sp => (
{sp.velocityPoints}
{(sp.name || 'Sprint').slice(0, 8)}
))}
Ortalama {avgVel} puan / sprint
)}
{/* Gecikme dağılımı */}
{/* Öncelik */}
Öncelik Dağılımı
{[
{ key: 'high', label: 'Yüksek', color: 'var(--p-high)' },
{ key: 'medium', label: 'Orta', color: 'var(--p-med)' },
{ key: 'low', label: 'Düşük', color: 'var(--p-low)' },
].map(({ key, label, color }) => (
{label}
{prioCounts[key]}
))}
{/* Etiket × durum */}
Etiket Analizi — Durum Dağılımı
{tagStats.length === 0 ? (
Henüz etiket yok
) : (
{tagStats.map(r => (
{r.tag.name}
{r.doneC > 0 &&
}
{r.actC > 0 &&
}
{r.overC > 0 &&
}
{r.doneC}/{r.count}
))}
Tamamlanan
Aktif
Gecikmiş
)}
)}
{/* ══════════════════════════════════════════════════════
SEKME 3: FİRMALAR
══════════════════════════════════════════════════════ */}
{tasks.length > 0 && tab === 'firmalar' && (
{/* Kârlılık — gerçek gelir ÷ harcanan süre = efektif saatlik kazanç */}
{(() => {
const inRange = (dstr) => {
if (!dstr) return false;
const d = new Date(dstr + 'T12:00:00');
if (fromDate && d < fromDate) return false;
if (toDate && d > toDate) return false;
return true;
};
const rows = projects
.filter(p => !p.archived && (projectFilter === 'all' || p.id === projectFilter))
.map(p => {
const revenue = (state.transactions || [])
.filter(tx => tx.projectId === p.id && tx.type === 'income' && (tx.status || 'done') === 'done')
.filter(tx => (!fromDate && !toDate) || inRange(tx.date))
.reduce((s, tx) => s + (tx.amount || 0), 0);
const mins = allTasks.filter(t => t.projectId === p.id)
.reduce((s, t) => s + (t.timeLog || [])
.filter(e => (!fromDate && !toDate) || inRange(e.date))
.reduce((a, e) => a + (e.duration || 0), 0), 0);
const effective = mins > 0 ? revenue / (mins / 60) : null;
return { project: p, revenue, mins, effective, target: p.hourlyRate || null };
})
.filter(r => r.revenue > 0 || r.mins > 0)
.sort((a, b) => (b.effective ?? -1) - (a.effective ?? -1));
if (rows.length === 0) return null;
const money = v => Math.round(v).toLocaleString('tr-TR') + ' ₺';
const best = rows.find(r => r.effective !== null);
return (
Kârlılık — Efektif Saatlik Kazanç
{best && (
En kârlı: {best.project.name} · saatte {money(best.effective)}
)}
Firma
Gelir
Süre
₺ / saat
Hedef
{rows.map(r => {
const underTarget = r.effective !== null && r.target && r.effective < r.target;
return (
{r.project.name}
{r.revenue > 0 ? money(r.revenue) : '—'}
{r.mins > 0 ? fmtTime(r.mins) : süre kaydı yok }
{r.effective !== null ? money(r.effective) : '—'}
{r.target
? {money(r.target)}{r.effective !== null && (underTarget ? ' ▼' : ' ▲')}
: '—'}
);
})}
Gelir: tahsil edilmiş işlemler · Süre: görevlere işlenen zaman kayıtları{(fromDate || toDate) ? ' · seçili dönem' : ''}
);
})()}
{/* Firma durum dağılımı */}
Firma Bazlı Durum Dağılımı
{projectBreakdown.length === 0 ? (
Görev yok
) : (
{projectBreakdown.map(({ project, total, byStatus, timeMin }) => (
{project.name}
{total}
{timeMin > 0 && {fmtTime(timeMin)} }
{RP_COLS.map(c => {
const w = byStatus[c.id] / total * 100;
if (!w) return null;
return
;
})}
{RP_COLS.map(c => byStatus[c.id] > 0 && (
{c.label} {byStatus[c.id]}
))}
))}
)}
{/* Firma bazlı süre */}
{timeByProject.length > 0 && (
Firma Bazlı Süre
{timeByProject.map(({ project, timeMin }) => (
{project.name}
{fmtTime(timeMin)}
))}
)}
{/* Bütçe & Karlılık */}
{projectBreakdown.some(r => r.project.budget) && (
Bütçe & Karlılık
{projectBreakdown.filter(r => r.project.budget).map(({ project, timeMin }) => {
const budget = project.budget || 0;
const hourlyRate = project.hourlyRate || 0;
const spent = hourlyRate ? Math.round((timeMin/60) * hourlyRate) : 0;
const pct = budget ? Math.round((spent/budget)*100) : 0;
const over = pct > 100;
return (
{project.name}
{hourlyRate ? `${spent.toLocaleString('tr-TR')} / ${budget.toLocaleString('tr-TR')} ₺` : `${budget.toLocaleString('tr-TR')} ₺ bütçe`}
{over &&
Aşıldı! }
{!over && pct > 0 &&
{pct}% }
);
})}
)}
{/* Gerçek kârlılık — finans verisiyle */}
{profitRows.length > 0 && (
Gerçek Kârlılık — Finans Verisi
Firma
Gelir
Gider
İşçilik
Net
{profitRows.map(r => (
{r.project.name}
{r.income ? `+${r.income.toLocaleString('tr-TR')} ₺` : '—'}
{r.expense ? `−${r.expense.toLocaleString('tr-TR')} ₺` : '—'}
{r.laborCost ? `−${r.laborCost.toLocaleString('tr-TR')} ₺` : '—'}
= 0 ? '#22c55e' : '#ef4444' }}>
{r.net >= 0 ? '+' : '−'}{Math.abs(r.net).toLocaleString('tr-TR')} ₺
))}
İşçilik = kaydedilen süre × firma saat ücreti · yalnızca gerçekleşen işlemler
)}
)}
);
}
window.ReportView = ReportView;