/* global React, DATA */ const SEVERITY_META = { critical: { label: 'Kritik', pill: 'pill-neg', accent: 'var(--neg)' }, warning: { label: 'Uyarı', pill: 'pill-warn', accent: 'var(--warn)' }, info: { label: 'Bilgi', pill: 'pill-info', accent: 'var(--info)' }, // legacy compatibility — older alerts used high/medium/low high: { label: 'Kritik', pill: 'pill-neg', accent: 'var(--neg)' }, medium: { label: 'Uyarı', pill: 'pill-warn', accent: 'var(--warn)' }, low: { label: 'Bilgi', pill: 'pill-info', accent: 'var(--info)' }, }; const _normSev = (s) => ({ high: 'critical', medium: 'warning', low: 'info' }[s] || s); const TYPE_BADGE_TR = { budget_gap: 'Bütçe Açığı', revpar_vs_ly: 'RevPAR YoY', occupancy_drop: 'Doluluk Düşüşü', adr_erosion: 'ADR Erozyonu', pickup_anomaly: 'Pickup Anomalisi', load_factor_risk: 'LF Riski', demand_shift: 'Talep Dalgalanması', class_imbalance: 'Sınıf Dengesizliği', pricing_outlier: 'Fiyat Sapması', price_increase_opp: 'Fiyat Artış Fırsatı', markdown_needed: 'İndirim Gerekli', underpriced_vs_comp: 'Rakibe Göre Ucuz', }; // Types pooled into the cross-scope "Fiyat" tab (in addition to any alert // explicitly tagged category="pricing" by the backend). const PRICING_TYPES = new Set([ 'adr_erosion', 'pricing_outlier', 'price_increase_opp', 'markdown_needed', 'underpriced_vs_comp', ]); const SEVERITY_RANK = { critical: 0, warning: 1, info: 2 }; const ACTION_LABEL = { open_hotel: 'İncele', open_flight: 'Rotayı aç', price_suggestion: 'Fiyat öner', }; const Alerts = () => { const all = DATA.alerts || []; const [scope, setScope] = useState('hotel'); // 'hotel' | 'flight' const [sev, setSev] = useState('all'); // 'all' | 'critical' | 'warning' | 'info' const [dismissed, setDismissed] = useState(() => new Set()); // Normalize: ensure every alert has scope + severity. Legacy alerts without scope // are treated as hotel-scope. const norm = useMemo(() => all.map((a, i) => ({ ...a, _idx: i, scope: a.scope || (a.hotel && a.hotel.includes('→') ? 'flight' : 'hotel'), severity: _normSev(a.severity), })), [all]); const visible = norm.filter(a => !dismissed.has(a._idx)); const isPricing = (a) => a.category === 'pricing' || PRICING_TYPES.has(a.type); const scopeCounts = { hotel: visible.filter(a => a.scope === 'hotel').length, flight: visible.filter(a => a.scope === 'flight').length, pricing: visible.filter(isPricing).length, }; const inScope = scope === 'pricing' ? visible.filter(isPricing) : visible.filter(a => a.scope === scope); const sevCounts = { all: inScope.length, critical: inScope.filter(a => a.severity === 'critical').length, warning: inScope.filter(a => a.severity === 'warning').length, info: inScope.filter(a => a.severity === 'info').length, }; const filtered = (sev === 'all' ? inScope : inScope.filter(a => a.severity === sev)) .slice() .sort((a, b) => (SEVERITY_RANK[a.severity] ?? 3) - (SEVERITY_RANK[b.severity] ?? 3)); const onAction = (a) => { const k = a.action && a.action.kind; const p = (a.action && a.action.payload) || {}; if (p.hotel_name || a.entity?.hotel_name) { const hotelName = p.hotel_name || a.entity.hotel_name; const hotel = (DATA.hotel_index || []).find(h => h.HotelName === hotelName) || { HotelName: hotelName }; window.dispatchEvent(new CustomEvent('open-hotel', { detail: hotel })); return; } if (p.page) { window.dispatchEvent(new CustomEvent('navigate', { detail: { page: p.page } })); return; } if (k === 'open_flight') { window.dispatchEvent(new CustomEvent('navigate', { detail: { page: 'flights' } })); } }; const dismiss = (idx) => setDismissed(d => new Set([...d, idx])); const scopeLabel = scope === 'hotel' ? 'Otel' : scope === 'flight' ? 'Uçuş' : 'Fiyat'; const alertFacts = { kapsam: scopeLabel, secili_siddet: sev, toplam_acik_uyari: visible.length, kapsam_dagilimi: { otel: scopeCounts.hotel, ucus: scopeCounts.flight, fiyat: scopeCounts.pricing }, bu_kapsamda_siddet: { kritik: sevCounts.critical, uyari: sevCounts.warning, bilgi: sevCounts.info }, uyarilar: filtered.slice(0, 12).map(a => ({ siddet: (SEVERITY_META[a.severity] || {}).label || a.severity, tip: TYPE_BADGE_TR[a.type] || a.type || '', varlik: a.entity?.hotel_name || a.entity?.route || a.hotel || '', baslik: a.title_tr || a.title || '', })), }; return React.createElement('div', { style: { padding: 16, display: 'flex', flexDirection: 'column', gap: 12 } }, // scope tabs React.createElement('div', { className: 'panel' }, React.createElement('div', { className: 'tabs' }, [ { id: 'hotel', label: 'Otel', count: scopeCounts.hotel }, { id: 'flight', label: 'Uçuş', count: scopeCounts.flight }, { id: 'pricing', label: 'Fiyat', count: scopeCounts.pricing }, ].map(t => React.createElement('div', { key: t.id, className: 'tab' + (scope === t.id ? ' active' : ''), onClick: () => { setScope(t.id); setSev('all'); } }, React.createElement('span', null, t.label), React.createElement('span', { className: 'mono', style: { marginLeft: 8, color: 'var(--text-faint)' } }, t.count) )) ), // severity chips React.createElement('div', { style: { padding: '10px 16px', display: 'flex', gap: 6, alignItems: 'center', borderTop: '1px solid var(--line)' } }, React.createElement('span', { style: { fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase', fontWeight: 700, color: 'var(--text-muted)', marginRight: 4 } }, 'Şiddet'), [ { id: 'all', label: 'Tümü', color: '' }, { id: 'critical', label: 'Kritik', color: 'var(--neg)' }, { id: 'warning', label: 'Uyarı', color: 'var(--warn)' }, { id: 'info', label: 'Bilgi', color: 'var(--info)' }, ].map(c => React.createElement('div', { key: c.id, className: 'filter-chip' + (sev === c.id ? ' active' : ''), onClick: () => setSev(c.id), style: c.color ? { borderLeft: `3px solid ${c.color}` } : undefined }, React.createElement('span', null, c.label), React.createElement('span', { className: 'mono', style: { marginLeft: 6, color: 'var(--text-faint)' } }, sevCounts[c.id]) )) ) ), React.createElement(InsightBox, { label: `Uyarı Triyajı · ${scopeLabel}`, hint: 'Açık RM uyarılarının triyaj özeti. Kaç uyarı, hangi şiddet dağılımı, en kritik temalar/varlıklar ve masanın bugün önceliklendirmesi gereken 2-3 uyarıyı vurgula.', facts: alertFacts, }), // empty state or cards filtered.length === 0 ? React.createElement('div', { className: 'panel', style: { padding: 48, textAlign: 'center', color: 'var(--text-muted)', fontSize: 12 } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 4, color: 'var(--ink)' } }, `Aktif ${scopeLabel.toLowerCase()} uyarısı yok`), React.createElement('div', null, 'Tüm sinyaller eşiğin içinde.') ) : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } }, filtered.map((a) => { const meta = SEVERITY_META[a.severity] || SEVERITY_META.info; const entityTag = a.entity?.hotel_name || a.entity?.route || a.hotel || ''; const subTag = a.entity?.region || a.entity?.airline || a.entity?.date || ''; // Build a one-line metric row from the metrics dict const m = a.metrics || {}; const metricBits = []; if (m.current_pct != null && m.target_pct != null) metricBits.push(`Gerç. %${Math.round(m.current_pct)} / Hedef %${Math.round(m.target_pct)}`); if (m.ratio_pct != null) metricBits.push(`Oran %${Math.round(m.ratio_pct)}`); if (m.revpar_yoy_pct != null) metricBits.push(`RevPAR YoY %${m.revpar_yoy_pct.toFixed(1)}`); if (m.delta_pp != null) metricBits.push(`Δ ${m.delta_pp.toFixed(1)}pp`); if (m.adr_wow_pct != null) metricBits.push(`ADR WoW %${m.adr_wow_pct.toFixed(1)}`); if (m.sigma != null) metricBits.push(`pickup ${m.sigma > 0 ? 'yukarı' : 'aşağı'}`); if (m.load_factor_pct != null) metricBits.push(`LF %${m.load_factor_pct.toFixed(0)}`); if (m.capacity != null && m.sold != null) metricBits.push(`${m.sold}/${m.capacity} koltuk`); if (m.shift_pct != null) metricBits.push(`Talep %${m.shift_pct >= 0 ? '+' : ''}${m.shift_pct.toFixed(0)}`); if (m.y_sold_pct != null && m.mh_sold_pct != null) metricBits.push(`Y %${Math.round(m.y_sold_pct)} · M/H %${Math.round(m.mh_sold_pct)}`); if (m.price_delta_pct != null) metricBits.push(`Δ %${m.price_delta_pct.toFixed(1)}`); if (m.current != null && m.suggested != null) metricBits.push(`€${m.current} → €${m.suggested}`); if (m.occupancy_pct != null) metricBits.push(`Doluluk %${Math.round(m.occupancy_pct)}`); if (m.comp_avg != null) metricBits.push(`Rakip ort €${m.comp_avg}`); const actionKind = a.action?.kind || 'open_hotel'; const actionLabel = ACTION_LABEL[actionKind] || a.action_label || 'Aç'; return React.createElement('div', { key: a.id || a._idx, className: 'panel', style: { padding: 14, borderLeft: `3px solid ${meta.accent}`, display: 'flex', alignItems: 'flex-start', gap: 14 } }, React.createElement('div', { style: { flex: 1, minWidth: 0 } }, React.createElement('div', { className: 'row gap-2', style: { marginBottom: 6, flexWrap: 'wrap' } }, React.createElement('span', { className: `pill ${meta.pill}` }, meta.label.toUpperCase()), React.createElement('span', { className: 'pill pill-outline mono', style: { fontSize: 9.5 } }, TYPE_BADGE_TR[a.type] || a.type || ''), React.createElement('span', { style: { fontWeight: 700, fontSize: 12.5, flex: 1 } }, a.title_tr || a.title), entityTag && React.createElement('span', { className: 'pill pill-outline' }, entityTag) ), React.createElement('div', { style: { fontSize: 11.5, color: 'var(--text-dim)', lineHeight: 1.55, marginBottom: 8, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' } }, a.body_tr || a.body), metricBits.length > 0 && React.createElement('div', { className: 'row gap-2', style: { flexWrap: 'wrap', fontFamily: 'var(--mono)', fontSize: 10.5, color: 'var(--text-muted)' } }, metricBits.map((b, k) => React.createElement('span', { key: k }, b))), subTag && React.createElement('div', { style: { fontSize: 10, color: 'var(--text-faint)', marginTop: 6, fontStyle: 'italic' } }, subTag) ), React.createElement('div', { className: 'row gap-2', style: { flexShrink: 0 } }, React.createElement('button', { className: 'btn btn-sm btn-ghost', onClick: () => dismiss(a._idx), title: 'Kapat' }, '×'), React.createElement('button', { className: 'btn btn-sm btn-primary', onClick: () => onAction(a) }, actionLabel) ) ); }) ) ); }; window.Alerts = Alerts;