// Finance — Gelir & Gider (Gelişmiş) const FNIcons = window.Icons; const INCOME_CATS = ['Firma Ödemesi', 'Avans', 'Kira Geliri', 'Ek Gelir', 'İade', 'Diğer']; const EXPENSE_CATS = ['Yazılım & Araç', 'Reklam', 'Barındırma', 'Seyahat', 'Malzeme', 'Personel', 'Abonelik', 'Vergi', 'Diğer']; // Sabit + kullanıcı tanımlı kategoriler const fnGetCats = (state, type) => { const base = type === 'income' ? INCOME_CATS : EXPENSE_CATS; const custom = state?.customCategories?.[type] || []; return [...base, ...custom.filter(c => !base.includes(c))]; }; // KDV tutarı — tutar KDV dahil kabul edilir const fnVatAmount = (tx) => { if (!tx.vat || tx.vat <= 0) return 0; return tx.amount - tx.amount / (1 + tx.vat / 100); }; // Aynı gün bir sonraki ay (ay sonuna kıstırılır) const fnNextMonth = (dateStr) => { const d = new Date(dateStr + 'T12:00:00'); const day = d.getDate(); d.setDate(1); d.setMonth(d.getMonth() + 1); const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate(); d.setDate(Math.min(day, lastDay)); const pad = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; }; const INC_COLOR = '#22c55e'; const EXP_COLOR = '#ef4444'; // ── İşlem Durumu ────────────────────────────────────────────── const TX_STATUSES = { done: { label: 'Alındı / Ödendi', short: 'Alındı', color: '#22c55e', bg: 'rgba(34,197,94,.12)' }, pending: { label: 'Bekliyor', short: 'Bekliyor', color: '#f59e0b', bg: 'rgba(245,158,11,.12)' }, deferred: { label: 'Vadeli', short: 'Vadeli', color: '#6366f1', bg: 'rgba(99,102,241,.12)' }, cancelled: { label: 'İptal', short: 'İptal', color: '#94a3b8', bg: 'rgba(148,163,184,.12)'}, }; const fnTodayISO = () => new Date().toISOString().slice(0, 10); // Vade geçmiş + henüz tamamlanmamış → gecikmiş const fnIsOverdue = (tx) => { const s = tx.status || 'done'; if (s === 'done' || s === 'cancelled') return false; const due = tx.dueDate || tx.date; return !!due && due < fnTodayISO(); }; // Bakiyeye sayılır mı? (done veya status yok → gerçekleşmiş) const fnIsRealized = (tx) => !tx.status || tx.status === 'done'; // Durum rozeti bileşeni function StatusBadge({ tx }) { const overdue = fnIsOverdue(tx); if (overdue) { return Gecikmiş; } const st = TX_STATUSES[tx.status] || TX_STATUSES.done; if (tx.status === 'done' || !tx.status) return null; // done = varsayılan, rozet yok return ( {st.short} ); } // Önceki döneme göre değişim rozeti (tutar bazlı) function FnDelta({ cur, prev, invert }) { if (prev === null || prev === undefined) return null; const diff = cur - prev; if (Math.abs(diff) < 1) return null; const up = diff > 0; const good = invert ? !up : up; return (
{up ? '▲' : '▼'} {fmtM(Math.abs(diff))} önceki döneme göre
); } const fmtM = (n, compact = false) => { if (compact && Math.abs(n) >= 1000) { return new Intl.NumberFormat('tr-TR', { maximumFractionDigits: 1 }).format(n / 1000) + 'B'; } return new Intl.NumberFormat('tr-TR', { maximumFractionDigits: 0 }).format(n) + ' ₺'; }; const fmtDate = (d) => new Date(d + 'T00:00:00').toLocaleDateString('tr-TR', { day: 'numeric', month: 'short', year: 'numeric' }); // ── Nakit Akışı Grafik (SVG Grouped Bar + Net Line) ─────────── function CashFlowChart({ data }) { const [hov, setHov] = React.useState(null); const W = 700, H = 200, pL = 52, pR = 12, pT = 14, pB = 34; const cW = W - pL - pR, cH = H - pT - pB; const maxVal = Math.max(...data.map(m => Math.max(m.inc, m.exp)), 1); const n = data.length; const groupW = cW / n; const barW = Math.min(18, groupW * 0.35); const gap = 3; const barX = (i, which) => pL + i * groupW + groupW / 2 + (which === 0 ? -(barW + gap / 2) : gap / 2); const barH = (v) => Math.max(2, (v / maxVal) * cH); const barY = (v) => pT + cH - barH(v); const netPoints = data.map((m, i) => { const net = m.inc - m.exp; const cx = pL + i * groupW + groupW / 2; const cy = pT + cH - ((net / maxVal) * cH); return [cx, Math.max(pT + 2, Math.min(pT + cH, cy))]; }); // Köşeli kırık çizgi yerine yumuşak kübik eğri — premium his const netPath = netPoints.reduce((p, [x1, y1], i) => { if (i === 0) return `M${x1},${y1}`; const [x0, y0] = netPoints[i - 1]; const mx = (x0 + x1) / 2; return p + ` C${mx},${y0} ${mx},${y1} ${x1},${y1}`; }, ''); const yTicks = [0, 0.5, 1].map(r => ({ y: pT + cH * (1 - r), val: maxVal * r })); const TIP_W = 124, TIP_H = 66; return (
{/* Grid */} {yTicks.map((t, i) => ( {fmtM(t.val, true)} ))} {/* Bars */} {data.map((m, i) => { const isHov = hov === i; const cx = pL + i * groupW + groupW / 2; const tipX = Math.max(pL, Math.min(cx - TIP_W / 2, W - pR - TIP_W)); const net = m.inc - m.exp; return ( setHov(i)} onMouseLeave={() => setHov(null)} style={{ cursor: 'default' }} > {/* Income bar */} {m.inc > 0 && ( )} {/* Expense bar */} {m.exp > 0 && ( )} {/* Hover crosshair + tooltip */} {isHov && ( {m.lbl} ▲ Gelir: {fmtM(m.inc)} ▼ Gider: {fmtM(m.exp)} = 0 ? INC_COLOR : EXP_COLOR}> {'= Net: ' + (net >= 0 ? '+' : '') + fmtM(net)} )} {/* Month label */} {m.lbl} ); })} {/* Net balance line */} {data.some(m => m.inc > 0 || m.exp > 0) && ( <> {netPoints.map(([cx, cy], i) => ( ))} )} {/* Axes */}
); } // ── Kategori Yatay Bar ──────────────────────────────────────── function CategoryBars({ data, color, max }) { if (!data.length) return
Bu dönemde veri yok
; const maxVal = max || Math.max(...data.map(d => d.value), 1); return (
{data.map((d, i) => (
{d.label}
{fmtM(d.value)}
))}
); } // ── Yaklaşan Ödemeler & Tahsilatlar (14 gün) ───────────────── function YaklasanOdemeler({ transactions, projById }) { const today = fnTodayISO(); const in14 = new Date(); in14.setDate(in14.getDate() + 14); const in14S = in14.toISOString().slice(0, 10); const upcoming = transactions .filter(t => { if (t.status === 'done' || t.status === 'cancelled') return false; const due = t.dueDate || t.date; return due >= today && due <= in14S; }) .sort((a, b) => (a.dueDate||a.date).localeCompare(b.dueDate||b.date)) .slice(0, 6); if (!upcoming.length) return null; const fmtShort = d => new Date(d+'T00:00:00').toLocaleDateString('tr-TR',{day:'numeric',month:'short'}); const diffDays = d => Math.round((new Date(d+'T00:00:00') - new Date().setHours(0,0,0,0)) / 86400000); return (
Yaklaşan Ödemeler & Tahsilatlar {upcoming.length} işlem · 14 gün
{upcoming.map(t => { const proj = projById[t.projectId]; const due = t.dueDate || t.date; const d = diffDays(due); const lbl = d === 0 ? 'Bugün' : d === 1 ? 'Yarın' : `${d}g`; const isUrgent = d <= 1; return (
{lbl} {fmtShort(due)}
{t.description} {proj && {proj.name}}
{t.type === 'income' ? '+' : '-'}{fmtM(t.amount)}
); })}
); } // ── Nakit Akış Tahmini (önümüzdeki 30 gün) ─────────────────── function NakitTahmini({ transactions, projById }) { const today = fnTodayISO(); const in30 = new Date(); in30.setDate(in30.getDate() + 30); const in30S = in30.toISOString().slice(0, 10); const pending = transactions.filter(t => { if (t.status === 'done' || t.status === 'cancelled') return false; const due = t.dueDate || t.date; return due <= in30S; }); const overdueIncluded = pending.filter(t => (t.dueDate || t.date) < today).reduce((s,t) => s+t.amount, 0); const expInc = pending.filter(t => t.type === 'income') .reduce((s,t) => s+t.amount, 0); const expExp = pending.filter(t => t.type === 'expense').reduce((s,t) => s+t.amount, 0); const netP = expInc - expExp; const total = expInc + expExp || 1; const incPct = Math.round(expInc / total * 100); const expPct = Math.round(expExp / total * 100); const sorted = [...pending] .sort((a, b) => (a.dueDate||a.date).localeCompare(b.dueDate||b.date)) .slice(0, 5); const fmtShort = d => new Date(d+'T00:00:00').toLocaleDateString('tr-TR',{day:'numeric',month:'short'}); const diffDays = d => Math.round((new Date(d+'T00:00:00') - new Date().setHours(0,0,0,0)) / 86400000); return (
30 Günlük Projeksiyon {pending.length} bekleyen işlem
{pending.length === 0 ? (
Bekleyen işlem yok
) : ( <>
{fmtM(expInc)} Beklenen Gelir
{fmtM(expExp)} Beklenen Gider
= 0 ? '#16a34a' : '#dc2626' }}> {netP >= 0 ? '+' : ''}{fmtM(netP)} Net Projeksiyon
Gelir {incPct}% Gider {expPct}%
{sorted.length > 0 && (
{sorted.map(t => { const due = t.dueDate || t.date; const d = diffDays(due); const lbl = d < 0 ? Math.abs(d) + 'g gecikmiş' : d === 0 ? 'Bugün' : d === 1 ? 'Yarın' : d + 'g'; const overdue = d < 0; const proj = projById[t.projectId]; return (
{t.description} {proj && {proj.name}}
{t.type === 'income' ? '+' : '-'}{fmtM(t.amount)} {lbl}
); })}
)} {overdueIncluded > 0 && (
⚠ {fmtM(overdueIncluded)} vadesi geçmiş işlem dahil
)} )}
); } // ── Kâr Marjı Trendi ───────────────────────────────────────── function MarjTrend({ monthlyData }) { const [hovIdx, setHovIdx] = React.useState(null); const pts = monthlyData.filter(m => m.inc > 0 || m.exp > 0); if (pts.length < 2) return null; const margins = pts.map(m => m.inc > 0 ? Math.round((m.inc-m.exp)/m.inc*100) : 0); const W = 400, H = 60; const minM = Math.min(...margins, -10), maxM = Math.max(...margins, 30); const range = maxM - minM || 1; const step = W / (pts.length - 1); const path = margins.map((v, i) => `${i===0?'M':'L'}${(i*step).toFixed(1)},${(H - ((v-minM)/range)*H).toFixed(1)}`).join(' '); const lastM = margins[margins.length-1]; const color = lastM >= 0 ? '#16a34a' : '#dc2626'; const zeroY = H - ((0-minM)/range)*H; const TIP_W = 90, TIP_H = 30; return (
12 Aylık Kâr Marjı Trendi {lastM}%
{zeroY >= 0 && zeroY <= H && ( )} {margins.map((v, i) => { const cx = i * step; const cy = H - ((v - minM) / range) * H; return ( = 0 ? '#16a34a' : '#dc2626'} style={{ transition: 'r 0.12s ease' }} /> ); })} {/* Invisible hit zones */} {pts.map((m, i) => { const zoneW = pts.length > 1 ? step : W; const zoneX = Math.max(0, i * step - zoneW / 2); return ( setHovIdx(i)} onMouseLeave={() => setHovIdx(null)} /> ); })} {pts.map((m, i) => ( {m.lbl} ))} {/* Hover tooltip */} {hovIdx !== null && (() => { const v = margins[hovIdx]; const cx = hovIdx * step; const cy = H - ((v - minM) / range) * H; const tipX = Math.max(0, Math.min(cx - TIP_W / 2, W - TIP_W)); const tipY = Math.max(2, cy - TIP_H - 8); const c = v >= 0 ? '#16a34a' : '#dc2626'; return ( {pts[hovIdx].lbl} {v}% kâr marjı ); })()}
); } // ── Firma K&Z Tablosu ───────────────────────────────────────── function ProjectPLTable({ rows, monthlyData, onStatement }) { if (!rows.length) return null; return (
Firma Gelir Gider Net Kâr Marjı
{rows.map(({ project: p, inc, exp, net, margin }) => { const isPos = net >= 0; const barPct = Math.min(100, Math.abs(margin ?? 0)); return (
{p.name}
{inc > 0 ? fmtM(inc) : '—'} {exp > 0 ? fmtM(exp) : '—'} {net !== 0 ? (isPos ? '+' : '') + fmtM(net) : '—'}
{margin !== null ? ( <>
{margin}% ) : }
); })}
); } const NET_POS = '#16a34a'; const NET_NEG = '#dc2626'; // ── Firma Cari Ekstresi ────────────────────────────────────── function StatementModal({ project, transactions, onClose }) { const rows = React.useMemo(() => { const txs = transactions .filter(t => t.projectId === project.id && t.status !== 'cancelled') .sort((a, b) => a.date.localeCompare(b.date) || (a.createdAt||'').localeCompare(b.createdAt||'')); let balance = 0; return txs.map(t => { balance += t.type === 'income' ? t.amount : -t.amount; return { tx: t, balance }; }); }, [project.id, transactions]); const totInc = rows.reduce((s, r) => s + (r.tx.type === 'income' ? r.tx.amount : 0), 0); const totExp = rows.reduce((s, r) => s + (r.tx.type === 'expense' ? r.tx.amount : 0), 0); const finalBalance = rows.length ? rows[rows.length - 1].balance : 0; React.useEffect(() => { const fn = e => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', fn); return () => window.removeEventListener('keydown', fn); }, [onClose]); const printStatement = () => { const root = document.documentElement; const prevTheme = root.dataset.theme; root.dataset.theme = 'light'; document.body.classList.add('fn-printing-statement'); setTimeout(() => { window.print(); root.dataset.theme = prevTheme; document.body.classList.remove('fn-printing-statement'); }, 100); }; return (
e.stopPropagation()}>

{project.name} — Cari Ekstre

Düzenleme tarihi: {fmtDate(fnTodayISO())} · {rows.length} işlem
{rows.length === 0 ? (
Bu firmaya ait işlem yok
) : ( <>
Tarih Açıklama Gelir Gider Bakiye
{rows.map(({ tx: t, balance }) => (
{fmtDate(t.date)} {t.description} {t.invoiceNo && #{t.invoiceNo}} {t.status && t.status !== 'done' && ({TX_STATUSES[t.status]?.short})} {t.type === 'income' ? fmtM(t.amount) : '—'} {t.type === 'expense' ? fmtM(t.amount) : '—'} = 0 ? NET_POS : NET_NEG }}> {balance < 0 ? '-' : ''}{fmtM(Math.abs(balance))}
))}
Toplam {fmtM(totInc)} {fmtM(totExp)} = 0 ? NET_POS : NET_NEG }}> {finalBalance < 0 ? '-' : ''}{fmtM(Math.abs(finalBalance))}
)}
); } // ── Kategori Bütçe Hedefleri Modalı ────────────────────────── function BudgetModal({ state, onSetCategoryBudget, onClose }) { const cats = fnGetCats(state, 'expense'); const budgets = state.categoryBudgets || {}; const [draft, setDraft] = React.useState(() => { const d = {}; cats.forEach(c => { d[c] = budgets[c] ? String(budgets[c]) : ''; }); return d; }); React.useEffect(() => { const fn = e => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', fn); return () => window.removeEventListener('keydown', fn); }, [onClose]); const save = () => { cats.forEach(c => { const v = parseFloat(String(draft[c]).replace(',', '.')); onSetCategoryBudget(c, v > 0 ? v : null); }); onClose(); }; return (
e.stopPropagation()} style={{ maxWidth: 420 }}>

Aylık Bütçe Hedefleri

Kategori bazında aylık gider limiti belirle. Boş bırakılan kategori takip edilmez.

{cats.map(c => ( ))}
); } // ── Ana Ekran ───────────────────────────────────────────────── function FinanceView({ state, onAddTransaction, onUpdateTransaction, onDeleteTransaction, onSetCategoryBudget, onAddCustomCategory }) { const [period, setPeriod] = React.useState('month'); const [customFrom, setCustomFrom] = React.useState(''); const [customTo, setCustomTo] = React.useState(''); const [filterType, setFilterType] = React.useState('all'); const [filterProj, setFilterProj] = React.useState('all'); const [filterCat, setFilterCat] = React.useState('all'); const [filterStatus, setFilterStatus]= React.useState('all'); const [search, setSearch] = React.useState(''); const [catTab, setCatTab] = React.useState('expense'); const [showAdd, setShowAdd] = React.useState(false); const [addType, setAddType] = React.useState('income'); const [editTx, setEditTx] = React.useState(null); const [delConfirm, setDelConfirm] = React.useState(null); const [statementProj,setStatementProj] = React.useState(null); const [showBudgets, setShowBudgets] = React.useState(false); const [editGoal, setEditGoal] = React.useState(false); const [goalDraft, setGoalDraft] = React.useState(''); const transactions = state.transactions || []; const projects = state.projects || []; const projById = Object.fromEntries(projects.map(p => [p.id, p])); // ── Tekrarlayan işlemler — şablonlardan eksik ayları üret ── const recurGenRef = React.useRef(false); React.useEffect(() => { if (recurGenRef.current) return; recurGenRef.current = true; const today = fnTodayISO(); transactions.filter(t => t.recurrence?.type === 'monthly').forEach(tpl => { const children = transactions.filter(t => t.recurrenceSourceId === tpl.id); let lastDate = children.reduce((m, c) => c.date > m ? c.date : m, tpl.date); let next = fnNextMonth(lastDate); let guard = 0; while (next <= today && guard < 12) { onAddTransaction({ type: tpl.type, amount: tpl.amount, description: tpl.description, category: tpl.category, projectId: tpl.projectId || null, invoiceNo: null, notes: tpl.notes || null, vat: tpl.vat || null, date: next, status: 'pending', dueDate: next, recurrenceSourceId: tpl.id, }); lastDate = next; next = fnNextMonth(lastDate); guard++; } }); }, []); // Dönem hesaplama const todayStr = new Date().toISOString().slice(0, 10); const dateRange = React.useMemo(() => { const now = new Date(); const pad = n => String(n).padStart(2, '0'); const fmt = d => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; if (period === 'month') { return { from: fmt(new Date(now.getFullYear(), now.getMonth(), 1)), to: fmt(new Date(now.getFullYear(), now.getMonth() + 1, 0)) }; } if (period === 'last') { return { from: fmt(new Date(now.getFullYear(), now.getMonth() - 1, 1)), to: fmt(new Date(now.getFullYear(), now.getMonth(), 0)) }; } if (period === 'year') { return { from: `${now.getFullYear()}-01-01`, to: `${now.getFullYear()}-12-31` }; } if (period === 'custom') return { from: customFrom, to: customTo }; return { from: '', to: '' }; }, [period, customFrom, customTo]); // Döneme göre filtre const periodTx = React.useMemo(() => transactions.filter(t => { if (dateRange.from && t.date < dateRange.from) return false; if (dateRange.to && t.date > dateRange.to) return false; return true; }), [transactions, dateRange]); // Liste için ek filtre const filtered = React.useMemo(() => [...periodTx].filter(t => { if (filterType !== 'all' && t.type !== filterType) return false; if (filterProj !== 'all' && t.projectId !== filterProj) return false; if (filterCat !== 'all' && t.category !== filterCat) return false; if (filterStatus !== 'all') { if (filterStatus === 'overdue') { if (!fnIsOverdue(t)) return false; } else { if ((t.status || 'done') !== filterStatus) return false; } } if (search) { const q = search.toLowerCase(); const hay = [t.description, t.invoiceNo, t.notes, t.category].filter(Boolean).join(' ').toLowerCase(); if (!hay.includes(q)) return false; } return true; }).sort((a, b) => b.date.localeCompare(a.date) || (b.createdAt||'').localeCompare(a.createdAt||'')), [periodTx, filterType, filterProj, filterCat, filterStatus, search]); // KPI'lar — gerçekleşen vs beklenen ayrımı const realizedInc = periodTx.filter(t => t.type === 'income' && fnIsRealized(t)).reduce((s,t) => s+t.amount, 0); const realizedExp = periodTx.filter(t => t.type === 'expense' && fnIsRealized(t)).reduce((s,t) => s+t.amount, 0); const netRealized = realizedInc - realizedExp; const expectedInc = periodTx.filter(t => t.type === 'income' && !fnIsRealized(t) && t.status !== 'cancelled').reduce((s,t) => s+t.amount, 0); const expectedExp = periodTx.filter(t => t.type === 'expense' && !fnIsRealized(t) && t.status !== 'cancelled').reduce((s,t) => s+t.amount, 0); const overdueList = periodTx.filter(t => fnIsOverdue(t)); const overdueAmt = overdueList.reduce((s,t) => s+t.amount, 0); // Gelir hedefi const incomeGoal = (state.categoryBudgets || {})['__income_goal__'] || 0; const goalPct = incomeGoal > 0 ? Math.round((realizedInc / incomeGoal) * 100) : 0; // Önceki dönem karşılaştırması const prevKpi = React.useMemo(() => { 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 (period === 'month') { from = fmt(new Date(now.getFullYear(), now.getMonth() - 1, 1)); to = fmt(new Date(now.getFullYear(), now.getMonth(), 0)); } else if (period === 'last') { from = fmt(new Date(now.getFullYear(), now.getMonth() - 2, 1)); to = fmt(new Date(now.getFullYear(), now.getMonth() - 1, 0)); } else if (period === 'year') { from = `${now.getFullYear()-1}-01-01`; to = `${now.getFullYear()-1}-12-31`; } else if (period === 'custom' && customFrom && customTo) { const f = new Date(customFrom + 'T00:00:00'), t2 = new Date(customTo + 'T00:00:00'); const span = Math.round((t2 - f) / 86400000) + 1; const pf = new Date(f); pf.setDate(pf.getDate() - span); const pt = new Date(f); pt.setDate(pt.getDate() - 1); from = fmt(pf); to = fmt(pt); } else return null; const ptx = transactions.filter(t => t.date >= from && t.date <= to); return { inc: ptx.filter(t => t.type === 'income' && fnIsRealized(t)).reduce((s,t) => s+t.amount, 0), exp: ptx.filter(t => t.type === 'expense' && fnIsRealized(t)).reduce((s,t) => s+t.amount, 0), }; }, [period, customFrom, customTo, transactions]); // KDV özeti (seçili dönem, iptal hariç) const vatCollected = periodTx.filter(t => t.type === 'income' && t.status !== 'cancelled').reduce((s,t) => s + fnVatAmount(t), 0); const vatPaid = periodTx.filter(t => t.type === 'expense' && t.status !== 'cancelled').reduce((s,t) => s + fnVatAmount(t), 0); const hasVat = vatCollected > 0 || vatPaid > 0; // Eski KPI isimleri (grafik ve diğer hesaplar için) const totalIncome = realizedInc + expectedInc; const totalExpense = realizedExp + expectedExp; const netBalance = netRealized; const txCount = periodTx.filter(t => t.status !== 'cancelled').length; // Son 12 ay verisi (grafik her zaman 12 ayı gösterir) const monthlyData = React.useMemo(() => { const now = new Date(); return Array.from({ length: 12 }, (_, i) => { const d = new Date(now.getFullYear(), now.getMonth() - (11 - i), 1); const key = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`; const lbl = d.toLocaleDateString('tr-TR', { month: 'short' }); const inc = transactions.filter(t => t.type === 'income' && t.date.startsWith(key)).reduce((s,t) => s+t.amount, 0); const exp = transactions.filter(t => t.type === 'expense' && t.date.startsWith(key)).reduce((s,t) => s+t.amount, 0); return { key, lbl, inc, exp }; }); }, [transactions]); // Aylık ortalamalar (veri olan aylardan) const activeMonths = Math.max(1, monthlyData.filter(m => m.inc > 0 || m.exp > 0).length); const allIncome = transactions.filter(t => t.type === 'income') .reduce((s,t) => s+t.amount, 0); const allExpense = transactions.filter(t => t.type === 'expense').reduce((s,t) => s+t.amount, 0); const avgIncome = allIncome / activeMonths; const avgExpense = allExpense / activeMonths; // En iyi ay (net) const bestMonth = monthlyData.reduce((best, m) => (m.inc - m.exp) > (best ? best.inc - best.exp : -Infinity) ? m : best, null); // Kategori dağılımı (seçilen dönem) const catData = React.useMemo(() => { const cats = fnGetCats(state, catTab); return cats.map(cat => ({ label: cat, value: periodTx.filter(t => t.type === catTab && t.category === cat).reduce((s,t) => s+t.amount, 0), })).filter(c => c.value > 0).sort((a,b) => b.value - a.value); }, [periodTx, catTab, state.customCategories]); // Bütçe takibi — her zaman içinde bulunulan aya göre const categoryBudgets = state.categoryBudgets || {}; const budgetRows = React.useMemo(() => { const monthKey = todayStr.slice(0, 7); return Object.entries(categoryBudgets).filter(([cat]) => cat !== '__income_goal__').map(([cat, limit]) => { const spent = transactions .filter(t => t.type === 'expense' && t.category === cat && t.status !== 'cancelled' && t.date.startsWith(monthKey)) .reduce((s, t) => s + t.amount, 0); return { cat, limit, spent, pct: Math.round((spent / limit) * 100) }; }).sort((a, b) => b.pct - a.pct); }, [categoryBudgets, transactions, todayStr]); // Proje K&Z const projectPL = React.useMemo(() => projects.map(p => { const txs = periodTx.filter(t => t.projectId === p.id); const inc = txs.filter(t => t.type === 'income') .reduce((s,t) => s+t.amount, 0); const exp = txs.filter(t => t.type === 'expense').reduce((s,t) => s+t.amount, 0); const net = inc - exp; const margin = inc > 0 ? Math.round((net / inc) * 100) : null; return { project: p, inc, exp, net, margin, count: txs.length }; }).filter(r => r.count > 0).sort((a,b) => Math.abs(b.net) - Math.abs(a.net)), [projects, periodTx]); // İşlemleri aya göre grupla const txGroups = React.useMemo(() => { const groups = {}; filtered.forEach(t => { const key = t.date.slice(0, 7); if (!groups[key]) { const d = new Date(key + '-01'); groups[key] = { key, label: d.toLocaleDateString('tr-TR', { month: 'long', year: 'numeric' }), items: [], inc: 0, exp: 0 }; } groups[key].items.push(t); if (t.type === 'income') groups[key].inc += t.amount; else groups[key].exp += t.amount; }); return Object.values(groups).sort((a,b) => b.key.localeCompare(a.key)); }, [filtered]); // Tüm kategoriler (liste filtresi için) const allCats = React.useMemo(() => { const set = new Set(periodTx.map(t => t.category)); return [...set].sort(); }, [periodTx]); // İşlem kopyalama (rest spread kullanılmıyor — Babel global scope çakışması) const duplicateTx = (t) => { onAddTransaction({ type: t.type, amount: t.amount, description: t.description, category: t.category, projectId: t.projectId || null, invoiceNo: t.invoiceNo || null, notes: t.notes || null, vat: t.vat || null, date: todayStr, status: 'pending', dueDate: null, }); }; // Hızlı durum güncelleme const cycleStatus = (t) => { const ORDER = ['done','pending','deferred','cancelled']; const cur = t.status || 'done'; const next = ORDER[(ORDER.indexOf(cur) + 1) % ORDER.length]; onUpdateTransaction?.(t.id, { status: next }); }; // CSV dışa aktarma const exportCSV = () => { const rows = [ ['Tarih','Tür','Tutar','Açıklama','Kategori','Firma'], ...filtered.map(t => [ t.date, t.type === 'income' ? 'Gelir' : 'Gider', t.amount.toFixed(2), `"${(t.description||'').replace(/"/g,'""')}"`, t.category || '', projById[t.projectId]?.name || '', ].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 = `finans-${todayStr}.csv`; a.click(); URL.revokeObjectURL(url); }; const PERIODS = [ { id: 'month', label: 'Bu Ay' }, { id: 'last', label: 'Geçen Ay' }, { id: 'year', label: 'Bu Yıl' }, { id: 'all', label: 'Tümü' }, { id: 'custom',label: 'Özel' }, ]; return (
{/* ── Header ──────────────────────────────────────────── */}

Gelir & Gider

{PERIODS.map(p => ( ))}
{period === 'custom' && (
setCustomFrom(e.target.value)} /> setCustomTo(e.target.value)} />
)}
{/* Cetvel imzası */}