// ─────────────────────────────────────────────────────────── // Huio Control Panel — Analytics View (cp-analytics-view.jsx) // ─────────────────────────────────────────────────────────── if (!window.CP_STYLES) throw new Error('[cp-analytics-view] window.CP_STYLES not loaded'); function renderAnalytics({ lang = 'vi', t: rawT, appStyles: rawAppStyles, analyticsEvents: rawAnalyticsEvents = [], eventFilter = 'all', setEventFilter = () => {}, platformFilter = 'all', setPlatformFilter = () => {}, searchQuery = '', setSearchQuery = () => {}, hoveredStep: rawHoveredStep = null, setHoveredStep: rawSetHoveredStep = () => {}, }) { const appStyles = rawAppStyles || window.CP_STYLES || {}; const t = typeof rawT === 'function' ? rawT : (k) => k; const analyticsEvents = Array.isArray(rawAnalyticsEvents) ? rawAnalyticsEvents : []; const hoveredStep = rawHoveredStep; const setHoveredStep = typeof rawSetHoveredStep === 'function' ? rawSetHoveredStep : () => {}; const formatPhone = window.formatPhone || ((p) => p || '—'); // Compute funnel data const funnelData = (() => { const userSteps = {}; analyticsEvents.forEach(e => { const id = e.anonymous_id; if (!userSteps[id]) userSteps[id] = new Set(); userSteps[id].add(e.event_type); }); const uniqueUsers = Object.keys(userSteps).length; const steps = [ { id: 'step1', name: lang === 'vi' ? 'Bắt đầu Onboarding' : 'Onboarding Started', check: (events) => events.has('onboarding_started'), color: '#B85C38' }, // terra { id: 'step2', name: lang === 'vi' ? 'Xem Slide / Bỏ qua' : 'Slide Viewed / Skipped', check: (events) => events.has('onboarding_slide_viewed') || events.has('onboarding_completed') || events.has('onboarding_skipped'), color: '#C19433' }, // ochre { id: 'step3', name: lang === 'vi' ? 'Yêu cầu mã OTP' : 'OTP Requested', check: (events) => events.has('auth_otp_requested') || events.has('auth_otp_verified'), color: '#4F6B3D' }, // forest { id: 'step4', name: lang === 'vi' ? 'Mã OTP khớp' : 'OTP Verified', check: (events) => events.has('auth_otp_verified'), color: '#3A8A8A' }, // teal { id: 'step5', name: lang === 'vi' ? 'Chấp nhận điều khoản' : 'Terms Accepted', check: (events) => events.has('terms_accepted'), color: '#A8412A' }, // rust { id: 'step6', name: lang === 'vi' ? 'Lựa chọn vai trò' : 'Role Selected', check: (events) => events.has('role_selected'), color: '#4A7A96' }, // sky { id: 'step7', name: lang === 'vi' ? 'Nộp hồ sơ CCCD / Hoàn tất' : 'CCCD Submitted / Completed', check: (events) => events.has('identity_verification_submitted') || events.has('identity_verification_completed'), color: '#2C2A29' } // charcoal ]; return steps.map(step => { let count = 0; Object.values(userSteps).forEach(events => { if (step.check(events)) count++; }); const percentOfTotal = uniqueUsers > 0 ? Math.round((count / uniqueUsers) * 100) : 0; return { ...step, count, percent: percentOfTotal }; }); })(); // Filter events const filteredEvents = analyticsEvents.filter(e => { // Event filter if (eventFilter === 'onboarding' && !e.event_type.startsWith('onboarding_')) return false; if (eventFilter === 'auth' && !e.event_type.startsWith('auth_') && e.event_type !== 'terms_accepted') return false; if (eventFilter === 'verification' && !e.event_type.startsWith('identity_verification_')) return false; // Platform filter const plat = e.metadata?.platform || ''; if (platformFilter !== 'all' && plat.toLowerCase() !== platformFilter.toLowerCase()) return false; // Search query if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); const eventName = e.event_type.toLowerCase(); const anonId = e.anonymous_id.toLowerCase(); const userName = e.profiles?.name?.toLowerCase() || ''; const userPhone = e.profiles?.phone || ''; if (!eventName.includes(query) && !anonId.includes(query) && !userName.includes(query) && !userPhone.includes(query)) return false; } return true; }); // Compute unique visitors const totalUniqueUsers = Object.keys(analyticsEvents.reduce((acc, e) => { acc[e.anonymous_id] = true; return acc; }, {})).length; // Compute overall conversion (step 7 / step 1) const step1Count = funnelData[0]?.count || 0; const step7Count = funnelData[6]?.count || 0; const overallConversion = step1Count > 0 ? Math.round((step7Count / step1Count) * 100) : 0; // Device breakdown const iosCount = analyticsEvents.filter(e => { const plat = e.metadata?.platform || e.platform || ''; return plat.toLowerCase() === 'ios'; }).length; const androidCount = analyticsEvents.filter(e => { const plat = e.metadata?.platform || e.platform || ''; return plat.toLowerCase() === 'android'; }).length; const totalPlatform = iosCount + androidCount; const iosPercent = totalPlatform > 0 ? Math.round((iosCount / totalPlatform) * 100) : 50; const androidPercent = totalPlatform > 0 ? Math.round((androidCount / totalPlatform) * 100) : 50; return (

{t('analytics.title')}

{t('analytics.sub')}

{/* Quick Metrics Grid */}
{/* Funnel chart card */}

{t('analytics.funnel.title')}

{t('analytics.funnel.desc')}

{/* SVG Interactive Funnel */}
{funnelData.map(step => ( ))} {funnelData.map((step, idx) => { const Y_top = 15 + idx * 62; const Y_bottom = Y_top + 40; const p = step.percent; const p_next = funnelData[idx + 1]?.percent ?? p; const W_top = 20 + (p / 100) * 110; const X_tl = 200 - W_top / 2; const X_tr = 200 + W_top / 2; const W_bottom = 20 + (p_next / 100) * 110; const X_bl = 200 - W_bottom / 2; const X_br = 200 + W_bottom / 2; // Drop-off calculation const prevCount = funnelData[idx - 1]?.count || 0; const currentCount = step.count; const dropOffPercent = prevCount > 0 ? Math.round(((prevCount - currentCount) / prevCount) * 100) : 0; const isHovered = hoveredStep === idx; return ( {/* Dotted helper lines for blueprint aesthetic */} {/* Left Label */} {idx + 1}. {step.name.length > 20 ? step.name.slice(0, 18) + '..' : step.name} {/* Right Value */} {step.count} ({step.percent}%) {/* Funnel trapezoid polygon */} setHoveredStep(idx)} onMouseLeave={() => setHoveredStep(null)} /> {/* Drop-off floating indicator pill */} {idx > 0 && dropOffPercent > 0 && ( -{dropOffPercent}% )} ); })}
{/* Tooltip Overlay */} {hoveredStep !== null && (
{funnelData[hoveredStep].name} Step {hoveredStep + 1}
{lang === 'vi' ? 'Số người dùng hoạt động:' : 'Active users:'} {funnelData[hoveredStep].count}
{lang === 'vi' ? 'Tỷ lệ chuyển đổi tổng thể:' : 'Overall conversion:'} {funnelData[hoveredStep].percent}%
{hoveredStep > 0 && (() => { const prev = funnelData[hoveredStep - 1].count; const curr = funnelData[hoveredStep].count; const rate = prev > 0 ? Math.round((curr / prev) * 100) : 0; const drop = 100 - rate; return (
{lang === 'vi' ? 'Giữ chân từ bước trước:' : 'Retention from previous:'} {rate}%
{lang === 'vi' ? 'Tỷ lệ rớt phễу:' : 'Dropped off:'} -{drop}%
); })()}
)} {/* Platform Breakdown */}
{lang === 'vi' ? 'Phân loại thiết bị (OS)' : 'Device OS Breakdown'} iOS {iosPercent}% | Android {androidPercent}%
🍏 iOS ({iosCount}) 🤖 Android ({androidCount})
{/* Filtering & Live Log Stream */}

{t('analytics.events.title')}

{lang === 'vi' ? 'Dòng dữ liệu sự kiện thời gian thực được đẩy trực tiếp từ ứng dụng di động.' : 'Real-time telemetry event stream pushed directly from user mobile client.' }

{/* Filters Row */}
{/* Event filters */}
{/* Platform filters */} {lang === 'vi' ? 'Thiết bị:' : 'OS:'}
{/* Search input */}
setSearchQuery(e.target.value)} className="hu-input" style={{ width: '100%', paddingLeft: '12px', minHeight: '38px', fontSize: '13px' }} /> {searchQuery && ( )}
{/* Table of raw events */}
{filteredEvents.length === 0 ? ( ) : ( filteredEvents.map(e => { const date = new Date(e.created_at); const timeStr = date.toTimeString().split(' ')[0] + ' ' + date.toLocaleDateString(lang === 'vi' ? 'vi-VN' : 'en-US', {month: 'short', day: 'numeric'}); // Label coloring and styling let eventBadgeClass = "hu-chip hu-chip-gray"; if (e.event_type.includes("failed") || e.event_type.includes("error")) { eventBadgeClass = "hu-chip hu-chip-coral"; } else if (e.event_type.includes("verified") || e.event_type.includes("completed") || e.event_type === "terms_accepted") { eventBadgeClass = "hu-chip hu-chip-teal"; } else if (e.event_type.includes("submit")) { eventBadgeClass = "hu-chip hu-chip-ochre"; } const displayUser = e.profiles?.name ? `${e.profiles.name} (${formatPhone(e.profiles.phone)})` : `Guest (${e.anonymous_id.substring(0, 8)})`; const platform = e.metadata?.platform || 'N/A'; return ( ); }) )}
{t('analytics.col.event')} {t('analytics.col.user')} {t('analytics.col.platform')} {t('analytics.col.time')}
{lang === 'vi' ? 'Không tìm thấy sự kiện nào.' : 'No analytics events found.'}
{e.event_type} {displayUser} {platform.toUpperCase()} {timeStr}
); } window.CpAnalyticsView = { renderAnalytics, };