/* global React, DATA */ const Forecast = () => { const today = new Date('2026-05-05'); // Cover the entire season Apr-Oct → ~180 days from today through Oct 31. const days = useMemo(() => Array.from({ length: 180 }, (_, i) => { const d = new Date(today); d.setDate(d.getDate() + i); return d; }), []); // Build monthly budget (€M/day) from the budget_vs_actual_2026 data so // Tahmin / Gerçekleşen track Bütçe — same source of truth as the Bütçe page. const budgetByMonth = useMemo(() => { const m = {}; (DATA.budget_vs_actual_2026 || []).forEach(r => { const daysIn = new Date(2026, r.month, 0).getDate(); m[r.month] = { budget: r.budget / daysIn / 1e6, ly: r.last_year / daysIn / 1e6, actual: r.actual_otb / daysIn / 1e6, }; }); return m; }, []); // Smooth seasonal basis. The monthly budget is a per-day constant within each // month, so using it directly draws flat plateaus joined by steps — the line // "rises a lot then sits on a plateau". Instead we anchor each month's €M/day // at its mid-point in the 180-day window and cosine-interpolate between // anchors, so Tahmin / Bütçe / 2025 trace one continuous seasonal arc. const anchors = useMemo(() => { const span = {}; days.forEach((d, i) => { const m = d.getMonth() + 1; if (!span[m]) span[m] = { first: i, last: i }; else span[m].last = i; }); return Object.keys(span).map(mk => { const m = Number(mk); const ref = budgetByMonth[m] || { budget: 0, ly: 0, actual: 0 }; return { i: Math.round((span[m].first + span[m].last) / 2), budget: ref.budget, ly: ref.ly }; }).sort((a, b) => a.i - b.i); }, [days, budgetByMonth]); const interp = useMemo(() => (idx, key) => { if (!anchors.length) return 0; if (idx <= anchors[0].i) return anchors[0][key]; const last = anchors[anchors.length - 1]; if (idx >= last.i) return last[key]; for (let k = 0; k < anchors.length - 1; k++) { const a = anchors[k], b = anchors[k + 1]; if (idx >= a.i && idx <= b.i) { const t = (idx - a.i) / (b.i - a.i); const e = 0.5 - 0.5 * Math.cos(Math.PI * t); // cosine ease-in-out return a[key] + (b[key] - a[key]) * e; } } return last[key]; }, [anchors]); const data = useMemo(() => days.map((d, i) => { const m = d.getMonth() + 1; const budgetDay = interp(i, 'budget'); const lyDay = interp(i, 'ly'); // Gentle daily texture so the smooth arc still reads as real data, not a // mathematical curve — much smaller than the old ±7% which fought the trend. const noise = Math.sin(i * 0.35) * 0.018 + Math.cos(i * 0.11) * 0.012; // Tahmin tracks the interpolated budget closely (within ±4%) const dailyForecast = budgetDay * (0.965 + noise); // OTB curve: starts ~95% of forecast on day 0, decays toward ~30% by day 90+ const otbPct = 0.30 + 0.65 * Math.exp(-i / 38); return { label: `${d.getDate()}/${m}`, day: d.getDate(), month: m, forecast: Number(dailyForecast || 0), otb: Number((dailyForecast * otbPct) || 0), ly: Number(lyDay || 0), budget: Number(budgetDay || 0), idx: i, }; }), [days, interp]); // Month-name ticks: anchor at i=0 plus each first-of-month boundary. const monthTicks = useMemo(() => { const out = [0]; data.forEach((d, i) => { if (i > 0 && d.day === 1) out.push(i); }); return out; }, [data]); const monthByIdx = useMemo(() => { const m = {}; monthTicks.forEach(i => { m[data[i].label] = MONTHS[data[i].month]; }); return m; }, [data, monthTicks]); const next30 = data.slice(0, 30); const totals30 = next30.reduce((acc, d) => ({ forecast: acc.forecast + d.forecast, ly: acc.ly + d.ly, budget: acc.budget + d.budget }), { forecast: 0, ly: 0, budget: 0 }); // Year-end forecast: past months use Gerçekleşen, current month blends actual + remaining // forecast, future months use budget × 0.95 (matches the daily-forecast tracking). const todayMonth = today.getMonth() + 1; const yearEnd = useMemo(() => { let total = 0, ly = 0, budget = 0; (DATA.budget_vs_actual_2026 || []).forEach(r => { const act = r.actual_otb || 0; const bud = r.budget || 0; if (r.month < todayMonth) total += act; else if (r.month === todayMonth) total += Math.max(act, bud * 0.92); else total += bud * 0.95; ly += r.last_year || 0; budget += bud; }); return { total, ly, budget }; }, []); const riskWindows = [ { period: '12–19 Tem', region: 'Belek', risk: 'Tükenme riski', occ: 91, action: 'Fiyatı +%6 yükselt' }, { period: '02–09 Ağu', region: 'Bodrum', risk: 'Tükenme riski', occ: 87, action: 'Fiyatı +%4 yükselt' }, { period: '24–31 May', region: 'Antalya', risk: 'Tempo gerisi', occ: 42, action: 'Fiyatı %3 düşür, DE öne çıkar' }, { period: '14–21 Haz', region: 'Marmaris', risk: 'Yolda', occ: 68, action: 'Tut' }, { period: '06–13 Eyl', region: 'Fethiye', risk: 'Tempo gerisi', occ: 38, action: 'UK için EB uzat' }, ]; const forecastFacts = { ufuk: 'Mayıs → Ekim (sezon sonu)', yil_sonu_tahmin_M: +(yearEnd.total / 1e6).toFixed(1), yil_sonu_butce_M: +(yearEnd.budget / 1e6).toFixed(1), yil_sonu_2025_gerceklesen_M: +(yearEnd.ly / 1e6).toFixed(1), yil_sonu_yoy_pct: yearEnd.ly ? +(((yearEnd.total / yearEnd.ly) - 1) * 100).toFixed(1) : null, yil_sonu_butce_gerceklesme_pct: yearEnd.budget ? +((yearEnd.total / yearEnd.budget) * 100).toFixed(1) : null, sonraki_30g_tahmin_M: +totals30.forecast.toFixed(1), sonraki_30g_butce_M: +totals30.budget.toFixed(1), sonraki_30g_butceye_pct: totals30.budget ? +((totals30.forecast / totals30.budget) * 100).toFixed(1) : null, tahmin_guveni_pct: 83, son_30g_mape_pct: 4.2, risk_pencereleri: riskWindows.map(r => ({ donem: r.period, bolge: r.region, durum: r.risk, doluluk_pct: r.occ })), }; return React.createElement('div', { style: { padding: 16, display: 'flex', flexDirection: 'column', gap: 12 } }, React.createElement('div', { className: 'grid grid-4 gap-3' }, React.createElement(KPI, { label: 'Yıl Sonu Tahmin', value: fmtEurM(yearEnd.total), sub: `bütçe ${fmtEurM(yearEnd.budget)}`, delta: yearEnd.ly ? ((yearEnd.total/yearEnd.ly - 1) * 100) : 0 }), React.createElement(KPI, { label: 'Bütçeye Karşı', value: (totals30.budget ? (totals30.forecast/totals30.budget) * 100 : 0).toFixed(1) + '%', sub: 'bütçe €' + (totals30.budget || 0).toFixed(0) + 'M' }), React.createElement(KPI, { label: 'Yakalanan Gerçekleşen', value: (totals30.forecast ? (next30.reduce((s,d)=>s+(d.otb||0),0) / totals30.forecast) * 100 : 0).toFixed(0) + '%', sub: 'sonraki 30g penceresi' }), React.createElement(KPI, { label: 'Tahmin Güveni', value: '%83', sub: 'son 30g MAPE %4,2' }) ), React.createElement(Panel, { title: 'Günlük Gelir Tahmini · Sezon Sonuna Kadar (Mayıs → Ekim)', sub: 'Tahmin (düz) · Gerçekleşen (yeşil) · Geçen Yıl (soluk) · Bütçe (kesikli)', actions: React.createElement('div', { className: 'row gap-2' }, React.createElement('button', { className: 'btn btn-sm' }, 'Günlük'), React.createElement('button', { className: 'btn btn-sm btn-ghost' }, 'Haftalık'), React.createElement('button', { className: 'btn btn-sm' }, 'Dışa Aktar') ) }, React.createElement(LineChart, { data, height: 320, format: v => '€' + v.toFixed(1) + 'M', xFormat: v => monthByIdx[v] || v, xTickIndices: monthTicks, smooth: true, series: [ { key: 'ly', label: '2025 Gerçekleşen', color: 'color-mix(in srgb, var(--text-dim) 50%, transparent)', width: 1.2 }, { key: 'budget', label: 'Bütçe', color: 'var(--info)', dashed: true, width: 1.2 }, { key: 'forecast', label: 'Tahmin', color: 'var(--brand)', width: 2, fill: true }, { key: 'otb', label: 'Gerçekleşen', color: 'var(--pos)', width: 2 }, ] }) ), React.createElement(InsightBox, { label: 'Günlük Gelir Tahmini · Sezon Sonuna Kadar', hint: 'Sezon sonuna kadar günlük gelir tahmini (Tahmin vs Bütçe vs 2025 vs OTB). Yıl sonu tahminini bütçe ve YoY\'a göre konumlandır, sonraki 30 günü ve en kritik risk pencerelerini vurgula.', facts: forecastFacts, }), React.createElement('div', { className: 'grid gap-3', style: { gridTemplateColumns: '3fr 2fr' } }, React.createElement(Panel, { title: 'Risk ve Fırsat Pencereleri', sub: 'Sonraki 90 gün · otomatik tespit', flush: true }, React.createElement('table', { className: 'tbl' }, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', null, 'Pencere'), React.createElement('th', null, 'Bölge'), React.createElement('th', null, 'Durum'), React.createElement('th', { className: 'num' }, 'Doluluk'), React.createElement('th', null, 'Önerilen Aksiyon'), React.createElement('th', null, '') ) ), React.createElement('tbody', null, riskWindows.map((r, i) => { const cls = r.risk.includes('Tükenme') ? 'pill-warn' : r.risk.includes('gerisi') ? 'pill-neg' : 'pill-pos'; return React.createElement('tr', { key: i }, React.createElement('td', { className: 'mono', style: { fontWeight: 500 } }, r.period), React.createElement('td', null, r.region), React.createElement('td', null, React.createElement('span', { className: 'pill ' + cls }, r.risk)), React.createElement('td', { className: 'num' }, r.occ + '%'), React.createElement('td', null, r.action), React.createElement('td', null, React.createElement('button', { className: 'btn btn-sm' }, 'Aç')) ); }) ) ) ), React.createElement(Panel, { title: 'Tahmin Etkenleri', sub: 'Sonraki 30g\'e en çok katkı yapanlar' }, React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } }, [ { f: 'Belek 5★ Haziran tempo', impact: '+€8,4M', dir: 'pos', pct: 6.2 }, { f: 'Rusya Gerçekleşen YoY -%12', impact: '−€3,1M', dir: 'neg', pct: -3.7 }, { f: 'DE erken rezervasyon artışı', impact: '+€2,6M', dir: 'pos', pct: 2.4 }, { f: 'AYT uçuş LF %96', impact: '+€1,9M', dir: 'pos', pct: 1.8 }, { f: 'Bodrum AI 4★ yavaşlama', impact: '−€1,4M', dir: 'neg', pct: -1.5 }, { f: 'PL paket karması artış', impact: '+€0,9M', dir: 'pos', pct: 0.8 }, ].map((d, i) => React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 10 } }, React.createElement('div', { style: { flex: 1, fontSize: 11.5, fontWeight: 500 } }, d.f), React.createElement('div', { style: { width: 120, height: 6, background: 'var(--bg-3)', borderRadius: 3, position: 'relative' } }, React.createElement('div', { style: { position: 'absolute', left: '50%', top: 0, height: '100%', width: Math.abs(d.pct) * 8 + 'px', background: d.dir === 'pos' ? 'var(--pos)' : 'var(--neg)', borderRadius: 2, transform: d.dir === 'neg' ? 'translateX(-100%)' : '' } }) ), React.createElement('div', { className: 'num mono', style: { minWidth: 56, textAlign: 'right', color: d.dir === 'pos' ? 'var(--pos)' : 'var(--neg)', fontWeight: 600 } }, d.impact) )) ) ) ) ); }; window.Forecast = Forecast;