/* global React, DATA */ const Budget = () => { const monthChart = DATA.budget_vs_actual_2026.map(r => ({ label: MONTHS[r.month], ly: r.last_year/1e6, bud: r.budget/1e6, otb: r.actual_otb/1e6, })); const ytdAgg = monthChart.reduce((acc,r) => ({ bud: acc.bud + r.bud, otb: acc.otb + r.otb, ly: acc.ly + r.ly }), { bud: 0, otb: 0, ly: 0 }); // Build hotel pacing list from real YTD data — full portfolio, not just 10 const h25 = DATA.by_hotel_2025_ytd || DATA.hotel_index || []; const h26 = DATA.by_hotel_2026_ytd || []; const map26 = useMemo(() => { const m = {}; h26.forEach(h => m[h.HotelName] = h); return m; }, [h26]); const portfolio = useMemo(() => h25.map(h => { const c = map26[h.HotelName] || {}; const ly = h.revenue || 0; const target = ly * 1.10; // simple +10% YoY target const otb = c.revenue || 0; const pacing = target ? Math.round(otb / target * 100) : 0; return { hotel: h.HotelName, region: h.SubRegion, stars: h.Stars, ly, target, otb, pacing, pax25: h.pax || 0, pax26: c.pax || 0, }; }), [h25, map26]); // Filters const [search, setSearch] = useState(''); const [region, setRegion] = useState('all'); const [month, setMonth] = useState(0); // 0 = all months const [selectedHotel, setSelectedHotel] = useState(null); const regions = useMemo(() => ['all', ...new Set(portfolio.map(h => h.region))], [portfolio]); const filtered = useMemo(() => portfolio .filter(h => !search || (h.hotel || '').toLowerCase().includes(search.toLowerCase())) .filter(h => region === 'all' || h.region === region) .sort((a, b) => b.otb - a.otb), [portfolio, search, region]); // Booking curve for selected hotel & month const curveKey = selectedHotel ? (month ? `${selectedHotel}|${month}` : selectedHotel) : null; const curve = useMemo(() => { if (!selectedHotel) return null; if (month) return (DATA.booking_curves_by_month || {})[`${selectedHotel}|${month}`]; return (DATA.booking_curves || {})[selectedHotel]; }, [selectedHotel, month]); // When a stay month is picked, label each booking-curve bucket by the actual // calendar booking date (anchor = first day of stay month minus days_before). // Without a month picked, fall back to the days-before label. const dateLabel = (db) => { if (!month) return db + 'g'; const anchor = new Date(2026, month - 1, 1); // first of stay month anchor.setDate(anchor.getDate() - db); return `${anchor.getDate()} ${MONTHS[anchor.getMonth() + 1]}`; }; const revCurve = curve && curve.map(c => ({ label: dateLabel(c.days_before), target: c.target_eur, actual: c.actual_eur, projection: c.projection_eur, ly: c.ly_eur, target_pct: c.target_pct_rev, actual_pct: c.actual_pct_rev, })); const paxCurve = curve && curve.map(c => ({ label: dateLabel(c.days_before), target: c.target_pax, actual: c.actual_pax, projection: c.projection_pax, ly: c.ly_pax, target_pct: c.target_pct_pax, actual_pct: c.actual_pct_pax, })); // Pacing delta = actual vs target at the latest observable bucket let pacingDelta = null; if (curve && curve.length) { const lastObserved = [...curve].reverse().find(c => c.actual_pct_rev != null); if (lastObserved) pacingDelta = lastObserved.actual_pct_rev - lastObserved.target_pct_rev; } const _pace = portfolio.reduce((a, h) => { if (h.pacing >= 70) a.yolda++; else if (h.pacing >= 50) a.izle++; else a.risk++; return a; }, { yolda: 0, izle: 0, risk: 0 }); const budgetFacts = { tam_yil_hedef_M: +ytdAgg.bud.toFixed(0), gerceklesen_M: +ytdAgg.otb.toFixed(0), hedefin_yuzdesi_pct: ytdAgg.bud ? +((ytdAgg.otb / ytdAgg.bud) * 100).toFixed(1) : null, gecen_yil_M: +ytdAgg.ly.toFixed(0), yoy_tempo_pct: ytdAgg.ly ? +(((ytdAgg.otb / ytdAgg.ly) - 1) * 100).toFixed(1) : null, aylik: monthChart.map(r => ({ ay: r.label, hedef_M: +r.bud.toFixed(1), gerceklesen_M: +r.otb.toFixed(1), gecen_yil_M: +r.ly.toFixed(1) })), otel_tempo_dagilimi: { yolda: _pace.yolda, izle: _pace.izle, risk_altinda: _pace.risk, toplam: portfolio.length }, en_riskli_oteller: [...portfolio].sort((a, b) => a.pacing - b.pacing).slice(0, 5) .map(h => ({ otel: h.hotel, bolge: h.region, tempo_pct: h.pacing, hedef_M: +(h.target / 1e6).toFixed(1), gerceklesen_M: +(h.otb / 1e6).toFixed(1) })), }; 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: '2026 Hedef', value: '€' + ytdAgg.bud.toFixed(0) + 'M', sub: 'tam yıl' }), React.createElement(KPI, { label: 'Gerçekleşen', value: '€' + ytdAgg.otb.toFixed(0) + 'M', sub: ((ytdAgg.otb/ytdAgg.bud)*100).toFixed(1) + '% hedefin' }), React.createElement(KPI, { label: '2025 Gerçekleşen', value: '€' + ytdAgg.ly.toFixed(0) + 'M' }), React.createElement(KPI, { label: 'YoY Tempo', value: '+' + ((ytdAgg.otb/(ytdAgg.ly||1)-1)*100).toFixed(1) + '%', sub: 'Gerçekleşen vs aynı dönem GY', delta: 12.4 }) ), React.createElement(Panel, { title: 'Aylık Bütçe Takibi · 2026', sub: 'Geçen yıl (soluk) · Bütçe hedefi (bilgi) · Gerçekleşen (marka)' }, React.createElement(LineChart, { data: monthChart, height: 280, format: v => '€' + v.toFixed(0) + 'M', series: [ { key: 'ly', label: '2025 Gerçekleşen', color: 'color-mix(in srgb, var(--text-dim) 60%, transparent)', width: 1.4 }, { key: 'bud', label: 'Bütçe Hedefi', color: 'var(--info)', width: 1.6, dashed: true }, { key: 'otb', label: 'Gerçekleşen', color: 'var(--brand)', width: 2.2, fill: true }, ] }) ), React.createElement(InsightBox, { label: 'Aylık Bütçe Takibi · 2026', hint: '2026 Gerçekleşen vs Bütçe hedefi vs 2025. Tam yıl hedefe ulaşma oranını, YoY tempoyu, en güçlü/zayıf ayları ve otel portföyünün tempo dağılımını (yolda/izle/risk) vurgula.', facts: budgetFacts, }), // FILTERS + HOTEL TABLE React.createElement(Panel, { title: 'Otel Hedef Takibi', sub: `${filtered.length} otel · 2026 Gerçekleşen vs hedef · satıra tıkla → rezervasyon eğrisi`, flush: true }, React.createElement('div', { className: 'filterbar', style: { borderTop: 'none', flexWrap: 'wrap' } }, React.createElement('input', { className: 'input', style: { width: 220 }, placeholder: 'Otel ara…', value: search, onChange: e => setSearch(e.target.value) }), React.createElement('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap' } }, regions.slice(0, 12).map(r => React.createElement('div', { key: r, className: 'filter-chip' + (region === r ? ' active' : ''), onClick: () => setRegion(r) }, r === 'all' ? 'Tüm bölgeler' : r)) ), React.createElement('div', { style: { display: 'flex', gap: 4, marginLeft: 'auto', alignItems: 'center' } }, React.createElement('span', { style: { fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase', fontWeight: 700, color: 'var(--text-muted)', marginRight: 4 } }, 'Ay'), React.createElement('div', { className: 'filter-chip' + (month === 0 ? ' active' : ''), onClick: () => setMonth(0) }, 'Tümü'), [1,2,3,4,5,6,7,8,9,10,11,12].map(m => React.createElement('div', { key: m, className: 'filter-chip' + (month === m ? ' active' : ''), onClick: () => setMonth(m) }, MONTHS[m])) ) ), React.createElement('div', { style: { maxHeight: 420, overflowY: 'auto' } }, React.createElement('table', { className: 'tbl' }, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', null, '#'), React.createElement('th', null, 'Otel'), React.createElement('th', null, 'Bölge'), React.createElement('th', null, '★'), React.createElement('th', { className: 'num' }, '2025 YTD'), React.createElement('th', { className: 'num' }, '2026 Hedef'), React.createElement('th', { className: 'num' }, '2026 Gerçekleşen'), React.createElement('th', { className: 'num' }, 'Tempo'), React.createElement('th', { style: { width: '20%' } }, 'İlerleme'), React.createElement('th', null, 'Durum') ) ), React.createElement('tbody', null, filtered.slice(0, 200).map((h, i) => { const stat = h.pacing >= 70 ? 'Yolda' : h.pacing >= 50 ? 'İzle' : 'Risk Altında'; const cls = h.pacing >= 70 ? 'pill-pos' : h.pacing >= 50 ? 'pill-warn' : 'pill-neg'; const barCol = h.pacing >= 70 ? 'var(--pos)' : h.pacing >= 50 ? 'var(--warn)' : 'var(--neg)'; const isSel = selectedHotel === h.hotel; return React.createElement('tr', { key: h.hotel, className: 'clickable' + (isSel ? ' selected' : ''), onClick: () => setSelectedHotel(h.hotel) }, React.createElement('td', { className: 'text-faint mono' }, String(i+1).padStart(2, '0')), React.createElement('td', { style: { fontWeight: 500 } }, h.hotel), React.createElement('td', { className: 'text-muted' }, h.region), React.createElement('td', null, React.createElement(Stars, { n: h.stars })), React.createElement('td', { className: 'num text-muted' }, fmtEurM(h.ly)), React.createElement('td', { className: 'num' }, fmtEurM(h.target)), React.createElement('td', { className: 'num', style: { fontWeight: 600 } }, fmtEurM(h.otb)), React.createElement('td', { className: 'num mono', style: { color: barCol, fontWeight: 600 } }, h.pacing + '%'), React.createElement('td', null, React.createElement('div', { style: { width: '100%', height: 6, background: 'var(--bg-3)', borderRadius: 3 } }, React.createElement('div', { style: { width: Math.min(100, h.pacing) + '%', height: '100%', background: barCol, borderRadius: 3, transition: 'width 0.4s' } }) ) ), React.createElement('td', null, React.createElement('span', { className: 'pill ' + cls }, stat)) ); }) ) ) ) ), // BOOKING CURVES — show when hotel selected. Y-axis = absolute revenue/pax, hover shows % too. selectedHotel && React.createElement('div', { className: 'grid grid-2 gap-3' }, React.createElement(Panel, { title: `${selectedHotel} · Gelir Eğrisi`, sub: month ? `${MONTHS_FULL[month]} · 2026 Hedef vs 2026 Gerçekleşen` : 'Tüm aylar · 2026 Hedef vs 2026 Gerçekleşen', actions: React.createElement('button', { className: 'btn btn-sm btn-ghost', onClick: () => setSelectedHotel(null) }, 'Kapat ✕') }, revCurve ? React.createElement(LineChart, { data: revCurve, height: 240, format: v => fmtEurM(v), xFormat: v => v, series: [ { key: 'ly', label: '2025 Gerçekleşen', color: 'color-mix(in srgb, var(--text-dim) 70%, transparent)', width: 1.5, tooltipFormat: d => `2025 Gerçekleşen: ${fmtEurM(d.ly)}` }, { key: 'target', label: '2026 Hedef', color: 'color-mix(in srgb, var(--text-dim) 60%, transparent)', width: 1.6, dashed: true, tooltipFormat: d => `2026 Hedef: ${fmtEurM(d.target)} (${d.target_pct.toFixed(1)}%)` }, { key: 'actual', label: '2026 Gerçekleşen', color: 'var(--brand)', width: 2.4, tooltipFormat: d => d.actual != null ? `2026 Gerçekleşen: ${fmtEurM(d.actual)} (${d.actual_pct.toFixed(1)}%)` : '2026 Gerçekleşen: henüz yok' }, { key: 'projection', label: 'Tahmin (mevcut tempoda)', color: 'var(--brand)', width: 2.4, dashed: true, tooltipFormat: d => d.projection != null && d.actual == null ? `Tahmin: ${fmtEurM(d.projection)} (${(d.projection/d.target*100).toFixed(1)}%)` : `Tahmin · gerçek devam ederse` }, ] }) : React.createElement('div', { style: { padding: '24px 16px', color: 'var(--text-muted)', fontStyle: 'italic', fontSize: 12.5, textAlign: 'center' } }, month ? `${MONTHS_FULL[month]} ayı için bu otelde yeterli veri yok.` : `${selectedHotel} için rezervasyon eğrisi verisi yetersiz.`) ), React.createElement(Panel, { title: `${selectedHotel} · Yolcu Eğrisi`, sub: month ? `${MONTHS_FULL[month]} · 2026 Hedef vs 2026 Gerçekleşen` : 'Tüm aylar · 2026 Hedef vs 2026 Gerçekleşen' }, paxCurve ? React.createElement(LineChart, { data: paxCurve, height: 240, format: v => fmtK(v), xFormat: v => v, series: [ { key: 'ly', label: '2025 Gerçekleşen', color: 'color-mix(in srgb, var(--text-dim) 70%, transparent)', width: 1.5, tooltipFormat: d => `2025 Gerçekleşen: ${fmtN(d.ly)} yolcu` }, { key: 'target', label: '2026 Hedef', color: 'color-mix(in srgb, var(--text-dim) 60%, transparent)', width: 1.6, dashed: true, tooltipFormat: d => `2026 Hedef: ${fmtN(d.target)} yolcu (${d.target_pct.toFixed(1)}%)` }, { key: 'actual', label: '2026 Gerçekleşen', color: 'var(--info)', width: 2.4, tooltipFormat: d => d.actual != null ? `2026 Gerçekleşen: ${fmtN(d.actual)} yolcu (${d.actual_pct.toFixed(1)}%)` : '2026 Gerçekleşen: henüz yok' }, { key: 'projection', label: 'Tahmin (mevcut tempoda)', color: 'var(--info)', width: 2.4, dashed: true, tooltipFormat: d => d.projection != null && d.actual == null ? `Tahmin: ${fmtN(d.projection)} yolcu (${(d.projection/Math.max(d.target,1)*100).toFixed(1)}%)` : 'Tahmin · gerçek devam ederse' }, ] }) : React.createElement('div', { style: { padding: '24px 16px', color: 'var(--text-muted)', fontStyle: 'italic', fontSize: 12.5, textAlign: 'center' } }, 'Yolcu eğrisi için yeterli veri yok.') ) ), selectedHotel && pacingDelta != null && React.createElement('div', { style: { padding: '12px 16px', background: 'var(--paper-soft)', border: '1px solid var(--rule-soft)', fontSize: 12.5, fontStyle: 'italic', borderLeft: `3px solid ${pacingDelta >= 0 ? 'var(--green)' : 'var(--red)'}` } }, React.createElement('span', { style: { fontWeight: 700, fontStyle: 'normal', color: pacingDelta >= 0 ? 'var(--green)' : 'var(--red)' } }, (pacingDelta >= 0 ? '+' : '') + pacingDelta.toFixed(1) + 'pt · '), pacingDelta >= 0 ? 'Hedefin önünde ilerliyor.' : 'Hedefin gerisinde — pace yakalama gerekli.' ) ); }; window.Budget = Budget;