/* global React, DATA */ const Pricing = () => { const [tab, setTab] = useState('queue'); const [filter, setFilter] = useState('all'); const months = [4,5,6,7,8,9,10]; const heat = useMemo(() => { const m = {}; DATA.price_heatmap.forEach(r => m[r.SourceMarket + '|' + r.Month] = r.PriceChangePct); return m; }, []); const recos = useMemo(() => { let arr = DATA.top_recommendations; if (filter === 'raise') arr = arr.filter(r => r.PriceChangePct > 0); if (filter === 'drop') arr = arr.filter(r => r.PriceChangePct < 0); if (filter === 'high') arr = arr.filter(r => r.ConfidencePct >= 80); return arr.slice(0, 50); }, [filter]); // Modeled revenue impact per recommendation. Every rec is an optimisation // move, so its expected impact vs do-nothing is POSITIVE by construction: // raises target strong demand (inelastic, e=-0.45), drops target price- // sensitive cells where the volume gain outweighs the price cut (e=-1.6). // ~500 packages per hotel×market×month cell over the season. const recImpact = (r) => { const p = r.PriceChangePct / 100; const e = p > 0 ? -0.45 : -1.6; return r.CurrentPrice_EUR * (r.Occupancy / 100) * 500 * ((1 + p) * (1 + e * p) - 1); }; const totalImpact = recos.reduce((s, r) => s + recImpact(r), 0); // ── Facts for the live AI summaries (one per tab) ── const allRecos = DATA.top_recommendations || []; const _raise = allRecos.filter(r => r.PriceChangePct > 0); const _drop = allRecos.filter(r => r.PriceChangePct < 0); const _avg = (a, f) => a.length ? a.reduce((s, r) => s + f(r), 0) / a.length : 0; const queueFacts = { toplam_oneri: allRecos.length, yuksek_guven_80_uzeri: allRecos.filter(r => r.ConfidencePct >= 80).length, ort_guven_pct: Math.round(_avg(allRecos, r => r.ConfidencePct)), artis_sayisi: _raise.length, dusus_sayisi: _drop.length, ort_artis_pct: +_avg(_raise, r => r.PriceChangePct).toFixed(1), ort_dusus_pct: +_avg(_drop, r => r.PriceChangePct).toFixed(1), net_modellenen_etki_M: +(totalImpact / 1e6).toFixed(2), not_etki: 'Modellenen etki her öneri için pozitiftir: artışlar güçlü talepte, indirimler hacim kazancının fiyat kaybını aştığı hücrelerde önerilir.', en_buyuk_artislar: [...allRecos].sort((a, b) => b.PriceChangePct - a.PriceChangePct).slice(0, 5) .map(r => ({ otel: r.HotelName, bolge: r.SubRegion, pazar: r.SourceMarket, ay: MONTHS[r.Month], degisim_pct: r.PriceChangePct, guven_pct: r.ConfidencePct })), en_buyuk_dususler: [...allRecos].sort((a, b) => a.PriceChangePct - b.PriceChangePct).slice(0, 5) .map(r => ({ otel: r.HotelName, bolge: r.SubRegion, pazar: r.SourceMarket, ay: MONTHS[r.Month], degisim_pct: r.PriceChangePct, guven_pct: r.ConfidencePct })), }; const _heatByMkt = {}; (DATA.price_heatmap || []).forEach(r => { (_heatByMkt[r.SourceMarket] = _heatByMkt[r.SourceMarket] || []).push(r.PriceChangePct); }); const _heatSorted = [...(DATA.price_heatmap || [])].sort((a, b) => b.PriceChangePct - a.PriceChangePct); const heatmapFacts = { pazar_ortalama_degisim: Object.entries(_heatByMkt).map(([m, a]) => ({ pazar: m, ort_degisim_pct: +(_avg(a.map(x => ({ v: x })), r => r.v)).toFixed(1) })), en_yuksek_artislar: _heatSorted.slice(0, 4).map(r => ({ pazar: r.SourceMarket, ay: MONTHS[r.Month], degisim_pct: +r.PriceChangePct.toFixed(1) })), en_yuksek_dususler: _heatSorted.slice(-4).map(r => ({ pazar: r.SourceMarket, ay: MONTHS[r.Month], degisim_pct: +r.PriceChangePct.toFixed(1) })), }; const _occSorted = [...(DATA.occupancy_grid || [])].sort((a, b) => b.occupancy - a.occupancy); const occFacts = { en_dolu_hucreler: _occSorted.slice(0, 6).map(r => ({ bolge: r.Region, ay: r.StayMonth, doluluk_pct: +r.occupancy.toFixed(0), satilan: r.sold, kontenjan: r.allot })), en_bos_hucreler: _occSorted.slice(-6).map(r => ({ bolge: r.Region, ay: r.StayMonth, doluluk_pct: +r.occupancy.toFixed(0), satilan: r.sold, kontenjan: r.allot })), }; const _fr = DATA.flight_price_recos || []; const flightFacts = { toplam_rota: _fr.length, artis_sayisi: _fr.filter(r => r.PriceChangePct > 0).length, dusus_sayisi: _fr.filter(r => r.PriceChangePct < 0).length, ort_lf_pct: Math.round(_avg(_fr, r => r.demand_pct || 0)), en_buyuk_artislar: [..._fr].sort((a, b) => b.PriceChangePct - a.PriceChangePct).slice(0, 5) .map(r => ({ rota: r.Origin + '→' + r.Destination, havayolu: r.Airline, degisim_pct: r.PriceChangePct, lf_pct: r.demand_pct, rakip: r.comp_index })), en_buyuk_dususler: [..._fr].sort((a, b) => a.PriceChangePct - b.PriceChangePct).slice(0, 5) .map(r => ({ rota: r.Origin + '→' + r.Destination, havayolu: r.Airline, degisim_pct: r.PriceChangePct, lf_pct: r.demand_pct, rakip: r.comp_index })), }; const tabLabels = { queue: 'Hotel-level', flights: 'Flight-level', heatmap: 'Heatmap', occupancy: 'Doluluk' }; const filterLabels = { all: 'Tümü', raise: 'Sadece Artış', drop: 'Sadece Düşüş', high: 'Yüksek Güven' }; 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: 'Açık Öneriler', value: fmtN(DATA.top_recommendations.length), sub: 'oteller ve pazarlar genelinde' }), React.createElement(KPI, { label: 'Yüksek Güven (≥%80)', value: fmtN(DATA.top_recommendations.filter(r=>r.ConfidencePct>=80).length), sub: 'otomatik onaya uygun' }), React.createElement(KPI, { label: 'Net Modellenen Etki', value: '+' + fmtEurM(totalImpact), sub: 'görünen öneriler · sezon geneli', delta: 4.2 }), React.createElement(KPI, { label: 'Ort. Güven', value: '%' + Math.round(DATA.top_recommendations.reduce((s, r) => s + r.ConfidencePct, 0) / Math.max(DATA.top_recommendations.length, 1)), sub: 'tüm öneriler genelinde' }) ), React.createElement('div', { className: 'panel' }, React.createElement('div', { className: 'tabs' }, ['queue','flights','heatmap','occupancy'].map(t => React.createElement('div', { key: t, className: 'tab' + (tab===t?' active':''), onClick: () => setTab(t) }, tabLabels[t]) ) ) ), tab === 'queue' && React.createElement(InsightBox, { label: 'Fiyatlama Önerileri · Otel Bazında', hint: '3 faktörlü (bütçe gerçekleşme · talep · rakip) otel fiyat önerileri. Açık öneri hacmini, artış/düşüş dengesini, ortalama güveni, net modellenen etkiyi ve en kritik artış/düşüş hamlelerini vurgula.', facts: queueFacts, }), tab === 'queue' && React.createElement(Panel, { title: 'Fiyatlama Önerileri · Salt Okunur', sub: `${recos.length} gösteriliyor · her satır: öneri + neden`, flush: true, actions: React.createElement('div', { className: 'row gap-2' }, ['all','raise','drop','high'].map(f => React.createElement('div', { key: f, className: 'filter-chip' + (filter===f?' active':''), onClick: () => setFilter(f) }, filterLabels[f]) ) ) }, React.createElement('div', { style: { maxHeight: 540, overflowY: 'auto' } }, recos.map((r, i) => { const pos = r.PriceChangePct >= 0; // 2-3 plain-language supporting facts (no stats jargon) const why = []; const dem = r.demand_pct != null ? r.demand_pct : r.Occupancy; if (dem != null) why.push(dem >= 80 ? `talep güçlü — doluluk %${Math.round(dem)}` : dem < 60 ? `talep zayıf — doluluk %${Math.round(dem)}` : `doluluk %${Math.round(dem)}`); const ci = r.comp_index; if (ci != null) why.push(ci > 105 ? `rakiplerden %${Math.round(ci - 100)} pahalıyız` : ci < 95 ? `rakiplerden %${Math.round(100 - ci)} ucuzuz` : 'rakiplerle başa baş'); const br = r.budget_realization_pct, bt = r.budget_target_pct_at_anchor; if (br != null && bt != null) { const d = Math.round(br - bt); why.push(d >= 2 ? `satış hedefin ${d} puan önünde` : d <= -2 ? `satış hedefin ${-d} puan gerisinde` : 'satış hedefle uyumlu'); } return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 12, padding: '9px 14px', borderBottom: '1px solid var(--rule-soft)', flexWrap: 'wrap' } }, React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } }, React.createElement('div', { style: { fontSize: 13, fontWeight: 600 } }, r.HotelName.slice(0, 30), React.createElement('span', { className: 'text-muted', style: { fontWeight: 400, fontSize: 11.5, marginLeft: 8 } }, `${r.SubRegion} ${r.Stars}★ · ${r.SourceMarket} · ${MONTHS[r.Month]}`)), React.createElement('div', { className: 'text-muted', style: { fontSize: 11.5, marginTop: 2 } }, 'Neden: ' + why.slice(0, 3).join(' · ')) ), React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, whiteSpace: 'nowrap' } }, React.createElement('span', { className: 'text-muted', style: { fontSize: 12 } }, '€' + fmtN(r.CurrentPrice_EUR)), React.createElement('span', { style: { fontSize: 12, color: 'var(--text-faint)' } }, '→'), React.createElement('span', { style: { fontSize: 13, fontWeight: 700 } }, '€' + fmtN(r.RecommendedPrice_EUR)), React.createElement('span', { className: pos ? 'pill pill-brand' : 'pill pill-neg' }, (pos ? '+' : '') + r.PriceChangePct + '%'), React.createElement('span', { className: 'mono', style: { fontSize: 11, color: 'var(--green)', fontWeight: 600 } }, '+€' + fmtK(recImpact(r))) ) ); }) ) ), tab === 'heatmap' && React.createElement(InsightBox, { label: 'Fiyat Değişim Önerileri · Pazar × Ay', hint: 'Pazar × ay önerilen ortalama fiyat değişim ısı haritası. Hangi pazarların net artış/düşüş eğiliminde olduğunu ve en uç hücreleri (pazar+ay) vurgula.', facts: heatmapFacts, }), tab === 'heatmap' && React.createElement(Panel, { title: 'Fiyat Değişim Önerileri · Pazar × Ay', sub: 'Önerilen ortalama Δ% · kırmızı = düşüş, siyah = artış' }, React.createElement('div', { style: { overflowX: 'auto' } }, React.createElement('table', { style: { width: '100%', borderCollapse: 'separate', borderSpacing: 2 } }, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', { className: 'label', style: { textAlign: 'left', padding: '6px 10px' } }, 'Pazar'), months.map(m => React.createElement('th', { key: m, className: 'label', style: { textAlign: 'center', padding: 6 } }, MONTHS[m])) ) ), React.createElement('tbody', null, DATA.markets_in_heatmap.map(mk => React.createElement('tr', { key: mk }, React.createElement('td', { style: { padding: '6px 10px', fontSize: 12, fontWeight: 500 } }, mk), months.map(m => { const v = heat[mk + '|' + m]; const c = v == null ? { bg: 'var(--bg-3)', fg: 'var(--text-faint)' } : heatColor(v, { max: 10 }); return React.createElement('td', { key: m, className: 'heat-cell', style: { background: c.bg, color: c.fg, minWidth: 64 } }, v == null ? '—' : (v > 0 ? '+' : '') + v.toFixed(1) + '%'); }) )) ) ) ), React.createElement('div', { className: 'row gap-3', style: { justifyContent: 'flex-end', marginTop: 14, fontSize: 10, color: 'var(--text-muted)' } }, React.createElement('span', null, 'Düşüş'), [-8,-4,-1].map(v => { const c = heatColor(v, { max: 10 }); return React.createElement('span', { key: v, style: { width: 22, height: 12, background: c.bg, borderRadius: 2 } }); }), React.createElement('span', { style: { width: 22, height: 12, background: 'var(--bg-3)', borderRadius: 2 } }), [1,4,8].map(v => { const c = heatColor(v, { max: 10 }); return React.createElement('span', { key: v, style: { width: 22, height: 12, background: c.bg, borderRadius: 2 } }); }), React.createElement('span', null, 'Artış') ) ), tab === 'occupancy' && React.createElement(InsightBox, { label: 'Doluluk Tempo · Bölge × Konaklama Ayı', hint: 'Sözleşmedeki kontenjana göre mevcut satış doluluğu %. En sıkı (tükenme riski) bölge-ay hücrelerini ve en boş (tempo gerisi) hücreleri vurgula; gerekiyorsa fiyat aksiyonu öner.', facts: occFacts, }), tab === 'occupancy' && (() => { const occRegions = [...new Set(DATA.occupancy_grid.map(r => r.Region))]; const occMonths = [...new Set(DATA.occupancy_grid.map(r => r.StayMonth))].sort(); const occMap = {}; DATA.occupancy_grid.forEach(r => occMap[r.Region+'|'+r.StayMonth] = r); return React.createElement(Panel, { title: 'Doluluk Tempo · Bölge × Konaklama Ayı', sub: 'Sözleşmedeki kontenjana göre satılan % · ayrıntı için hücreye tıkla' }, React.createElement('div', { style: { overflowX: 'auto' } }, React.createElement('table', { style: { width: '100%', borderCollapse: 'separate', borderSpacing: 2 } }, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', { className: 'label', style: { textAlign: 'left', padding: '6px 10px' } }, 'Bölge'), occMonths.map(m => React.createElement('th', { key: m, className: 'label', style: { textAlign: 'center', padding: 6 } }, m)) ) ), React.createElement('tbody', null, occRegions.map(r => React.createElement('tr', { key: r }, React.createElement('td', { style: { padding: '6px 10px', fontSize: 12, fontWeight: 500 } }, r), occMonths.map(m => { const c = occMap[r+'|'+m]; const o = c ? c.occupancy : null; const col = occColor(o); return React.createElement('td', { key: m, className: 'heat-cell', style: { background: col.bg, color: col.fg, minWidth: 56 }, title: c ? `€${c.avg_price}/hf · ${c.sold}/${c.allot}` : '' }, o != null ? o.toFixed(0) + '%' : '—'); }) )) ) ) ) ); })(), tab === 'flights' && React.createElement(InsightBox, { label: 'Uçuş Rotası Fiyat Önerileri', hint: '3 faktörlü (bütçe gerçekleşme · LF talebi · rakip) uçuş rotası fiyat önerileri. Rota hacmini, artış/düşüş dengesini, ortalama LF\'i ve en kritik rota hamlelerini vurgula.', facts: flightFacts, }), tab === 'flights' && React.createElement(Panel, { title: 'Uçuş Rotası Fiyat Önerileri', sub: `${DATA.flight_price_recos.length} rota · 3 karar faktörü: bütçe gerç. · talep (LF) · rakip`, flush: true }, React.createElement('div', { style: { maxHeight: 540, overflowY: 'auto' } }, React.createElement('table', { className: 'tbl' }, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', null, 'Rota'), React.createElement('th', null, 'Tarih'), React.createElement('th', null, 'Havayolu'), React.createElement('th', { className: 'num' }, 'Mevcut'), React.createElement('th', { className: 'num' }, 'Öneri'), React.createElement('th', { className: 'num' }, 'Δ'), React.createElement('th', { className: 'num', title: 'Booking pace gerçekleşme vs hedef' }, 'Bütçe Gerç.'), React.createElement('th', { className: 'num', title: 'Uçuş doluluk oranı (LF)' }, 'Talep (LF)'), React.createElement('th', { className: 'num', title: 'Rakip seti vs bizim fiyat · 100=eşit' }, 'Rakip') ) ), React.createElement('tbody', null, DATA.flight_price_recos.map((r, i) => { const pos = r.PriceChangePct >= 0; const br = r.budget_realization_pct; const bt = r.budget_target_pct_at_anchor; const brColor = (br != null && bt != null) ? (br >= bt - 2 ? 'var(--green)' : br >= bt - 8 ? 'var(--amber)' : 'var(--red)') : 'var(--text-faint)'; const dem = r.demand_pct; const demColor = dem == null ? 'var(--text-faint)' : (dem >= 80 ? 'var(--green)' : dem >= 60 ? 'var(--amber)' : 'var(--red)'); const ci = r.comp_index; const ciColor = ci == null ? 'var(--text-faint)' : (ci > 105 ? 'var(--red)' : ci < 95 ? 'var(--green)' : 'var(--amber)'); return React.createElement('tr', { key: i }, React.createElement('td', { style: { fontWeight: 600 } }, r.Origin + ' → ' + r.Destination), React.createElement('td', { className: 'mono text-muted' }, r.Date), React.createElement('td', { className: 'text-muted' }, r.Airline), React.createElement('td', { className: 'num text-muted' }, '€' + fmtN(r.CurrentPrice)), React.createElement('td', { className: 'num', style: { fontWeight: 600 } }, '€' + fmtN(r.RecommendedPrice)), React.createElement('td', { className: 'num' }, React.createElement('span', { className: pos?'pill pill-brand':'pill pill-neg' }, (pos?'+':'') + r.PriceChangePct + '%')), React.createElement('td', { className: 'num mono', style: { color: brColor, fontWeight: 600 } }, br != null ? `%${br.toFixed(0)} / %${(bt || 0).toFixed(0)}` : '—'), React.createElement('td', { className: 'num mono', style: { color: demColor, fontWeight: 600 } }, dem != null ? `%${dem.toFixed(0)}` : '—'), React.createElement('td', { className: 'num mono', style: { color: ciColor, fontWeight: 600 } }, ci != null ? ci.toFixed(0) : '—') ); }) ) ) ) ) ); }; window.Pricing = Pricing;