Files
SIB/frontend/src/views/StrategicAnalytics.vue

722 lines
34 KiB
Vue

<template>
<div class="strategic-analytics">
<div class="header-section">
<div class="top-row">
<button class="download-btn" @click="generateReport">
<span class="material-icons notranslate" translate="no">description</span>
Descargar Informe
</button>
<div class="badge">INTELIGENCIA ESTRATÉGICA</div>
</div>
<h1>Centro de Operaciones</h1>
<p class="subtitle">Análisis segmentado de rendimiento SIB</p>
</div>
<!-- TACTICAL TAB SELECTOR -->
<div class="tabs-control">
<button
class="tab-btn"
:class="{ active: activeTab === 'overview' }"
@click="activeTab = 'overview'"
>
<span class="material-icons notranslate" translate="no">dashboard</span>
Visión General
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'transport' }"
@click="activeTab = 'transport'"
>
<span class="material-icons notranslate" translate="no">directions_bus</span>
Logística de Transporte
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'commerce' }"
@click="activeTab = 'commerce'"
>
<span class="material-icons notranslate" translate="no">storefront</span>
Inteligencia Comercial
</button>
</div>
<div v-if="loading" class="loading-state">
<span class="material-icons spin notranslate" translate="no">sync</span>
<p>Sincronizando con la red...</p>
</div>
<template v-else>
<!-- SECTION 1: OVERVIEW -->
<div v-if="activeTab === 'overview'" class="tab-content animate-fade">
<div class="dashboard-layout">
<div class="main-content">
<div class="kpi-grid">
<div class="kpi-card user-active">
<div class="kpi-icon"><span class="material-icons notranslate" translate="no">person</span></div>
<div class="kpi-data">
<span class="kpi-value">{{ stats.users?.registered_active || 0 }}</span>
<span class="kpi-label">Usuarios Registrados Activos</span>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon"><span class="material-icons notranslate" translate="no">analytics</span></div>
<div class="kpi-data">
<span class="kpi-value">{{ totalInteractionCount }}</span>
<span class="kpi-label">Interacciones Totales Hoy</span>
</div>
</div>
</div>
<section class="analysis-section mini">
<div class="section-header">
<span class="material-icons notranslate" translate="no">schedule</span>
<h2>Mapa de Calor Horario</h2>
</div>
<div class="chart-container large">
<Line :data="usageChartData" :options="usageChartOptions" />
</div>
</section>
</div>
<aside class="side-info">
<div class="info-box">
<span class="material-icons notranslate" translate="no">groups</span>
<h4>Control de Tráfico</h4>
<p>Esta sección muestra la salud general de la app. Si la línea de invitados supera por mucho a la de registrados, es momento de lanzar una campaña de fidelización.</p>
</div>
</aside>
</div>
</div>
<!-- SECTION 2: TRANSPORT -->
<div v-if="activeTab === 'transport'" class="tab-content animate-fade">
<div class="dashboard-layout">
<div class="main-content">
<!-- RUTAS -->
<section class="analysis-section">
<div class="section-header">
<span class="material-icons notranslate" translate="no">bar_chart</span>
<h2>Rutas Turísticas más Consultadas</h2>
</div>
<div class="chart-container">
<Bar :data="routesChartData" :options="routesChartOptions" />
</div>
</section>
<!-- CASETAS -->
<section class="analysis-section">
<div class="section-header">
<span class="material-icons notranslate" translate="no">location_on</span>
<h2>Puntos de Interés: Casetas (Paradas)</h2>
</div>
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>ID Caseta</th>
<th>Peticiones</th>
<th>Popularidad</th>
</tr>
</thead>
<tbody>
<tr v-for="stop in stats.top_stops" :key="stop.id">
<td class="id-cell"># {{ stop.id }}</td>
<td>{{ stop.count }}</td>
<td>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: (stop.count / maxStopCount * 100) + '%' }"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- RENDIMIENTO SHUTTLES -->
<section class="analysis-section">
<div class="section-header">
<span class="material-icons notranslate" translate="no">trending_up</span>
<h2>Tasa de Reservación (Shuttles)</h2>
</div>
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Shuttle</th>
<th>Interés (Vistas)</th>
<th>Reservas & Llamadas</th>
<th>Ratio de Conversión</th>
</tr>
</thead>
<tbody>
<tr v-for="(data, name) in stats.shuttles" :key="name">
<td class="id-cell">{{ name }}</td>
<td>{{ data.views }}</td>
<td>
<div style="display:flex; gap: 12px; align-items:center;">
<span title="WhatsApp" style="display:flex; align-items:center; gap:2px;"><span class="material-icons notranslate" style="font-size:14px; color:#25D366;" translate="no">chat</span> {{ data.whatsapp }}</span>
<span title="Llamadas" style="display:flex; align-items:center; gap:2px;"><span class="material-icons notranslate" style="font-size:14px; color:#cbd5e1;" translate="no">phone</span> {{ data.calls }}</span>
</div>
</td>
<td>
<div style="display:flex; flex-direction:column; gap:4px;">
<div class="mini-bar"><div class="fill" :style="{ width: calculateConversion(data.views, data.contacts) + '%' }"></div></div>
<span style="font-size: 0.65rem; color: #64748b; font-weight:700;">{{ calculateConversion(data.views, data.contacts) }}%</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
<aside class="side-info">
<div class="info-box accent">
<span class="material-icons notranslate" translate="no">local_shipping</span>
<h4>Optimización de Logística</h4>
<p>Identifique paradas saturadas para coordinar con los conductores. Las rutas con conversión mayor al 15% son candidatas para ser rutas 'Express'.</p>
</div>
</aside>
</div>
</div>
<!-- SECTION 3: COMMERCE -->
<div v-if="activeTab === 'commerce'" class="tab-content animate-fade">
<div class="dashboard-layout">
<div class="main-content">
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-icon promo"><span class="material-icons notranslate" translate="no">confirmation_number</span></div>
<div class="kpi-data">
<span class="kpi-value">{{ stats.summary?.total_promo_clicks || 0 }}</span>
<span class="kpi-label">Cupones Activados</span>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon biz"><span class="material-icons notranslate" translate="no">storefront</span></div>
<div class="kpi-data">
<span class="kpi-value">{{ stats.summary?.total_biz_views || 0 }}</span>
<span class="kpi-label">Visitas a Negocios</span>
</div>
</div>
</div>
<section class="analysis-section">
<div class="section-header">
<span class="material-icons notranslate" translate="no">ads_click</span>
<h2>Impacto de Aliados Comerciales</h2>
</div>
<div class="business-list">
<div v-for="(data, name) in stats.businesses" :key="name" class="business-card-analytics">
<!-- Business Header -->
<div class="business-header">
<div class="business-title-info">
<div class="biz-icon-box"><span class="material-icons notranslate" translate="no">storefront</span></div>
<h3>{{ name }}</h3>
</div>
<div class="business-total-badge">
<span class="material-icons notranslate" translate="no">data_exploration</span>
<span><b>{{ data.views + data.social + data.calls + data.location + data.promos + data.favs }}</b> Interacciones</span>
</div>
</div>
<!-- Details Grid -->
<div class="business-details-grid">
<div class="detail-item">
<span class="material-icons notranslate" style="color:#cbd5e1" translate="no">visibility</span>
<div class="detail-info">
<span class="detail-value">{{ data.views }}</span>
<span class="detail-label">Vistas del Local</span>
</div>
</div>
<div class="detail-item">
<span class="material-icons notranslate" style="color:#25D366" translate="no">chat</span>
<div class="detail-info">
<span class="detail-value">{{ data.social }}</span>
<span class="detail-label">Redes / WP</span>
</div>
</div>
<div class="detail-item">
<span class="material-icons notranslate" style="color:#f87171" translate="no">phone</span>
<div class="detail-info">
<span class="detail-value">{{ data.calls }}</span>
<span class="detail-label">Llamadas Directas</span>
</div>
</div>
<div class="detail-item">
<span class="material-icons notranslate" style="color:#60a5fa" translate="no">place</span>
<div class="detail-info">
<span class="detail-value">{{ data.location }}</span>
<span class="detail-label">Usos del Mapa</span>
</div>
</div>
<div class="detail-item">
<span class="material-icons notranslate" style="color:#e91e63" translate="no">favorite</span>
<div class="detail-info">
<span class="detail-value">{{ data.favs }}</span>
<span class="detail-label">Veces Favorito</span>
</div>
</div>
</div>
<!-- Coupons / Promos Summary -->
<div class="business-coupons-section" :class="{ 'has-coupons': Object.keys(data.coupons || {}).length > 0 }">
<div class="coupons-header">
<div style="display:flex; align-items:center; gap: 8px;">
<span class="material-icons notranslate" style="color:#fee715" translate="no">confirmation_number</span>
<h4>Tráfico por Promociones</h4>
</div>
<div class="status-pill-wrap">
<span class="status-pill" :class="getHealthClass(calculateConversion(data.views, data.promos))">
Salud: {{ getHealthLabel(calculateConversion(data.views, data.promos)) }}
</span>
</div>
</div>
<div v-if="Object.keys(data.coupons || {}).length > 0" class="coupon-list">
<div v-for="(couponData, couponName) in data.coupons" :key="couponName" class="coupon-item">
<div class="coupon-name-box">
<span class="material-icons notranslate" style="font-size:14px; color:var(--text-secondary)" translate="no">local_offer</span>
<span class="coupon-name">{{ couponName }}</span>
</div>
<div class="coupon-stats">
<span class="stat" title="Clicks a la promo"><span class="material-icons notranslate" translate="no">visibility</span> {{ couponData.views }}</span>
<span class="stat" title="Clicks al mapa desde promo"><span class="material-icons notranslate" translate="no">place</span> {{ couponData.location }}</span>
</div>
</div>
</div>
<div v-else class="no-coupons">
<span class="material-icons notranslate" translate="no">info</span> No hay promociones generadas.
</div>
</div>
</div>
</div>
</section>
</div>
<aside class="side-info">
<div class="info-box">
<span class="material-icons notranslate" translate="no">shopping_bag</span>
<h4>Retorno Comercial</h4>
<p>Analice qué negocios están monetizando mejor el tráfico de SIB. Use estos datos para ofrecer espacios publicitarios premium a los negocios con salud 'Baja'.</p>
</div>
</aside>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { supabase } from '@/supabase';
import { Bar, Line } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, PointElement, LineElement } from 'chart.js';
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, PointElement, LineElement);
const loading = ref(true);
const activeTab = ref('overview');
const stats = ref<any>({
shuttles: {},
businesses: {},
top_stops: [],
users: { registered_active: 0, patterns: { registered: {}, guests: {} } },
summary: { total_shuttle_contacts: 0, total_promo_clicks: 0, total_biz_views: 0 }
});
const totalInteractionCount = computed(() => {
const s = stats.value.summary;
return (s.total_shuttle_contacts || 0) + (s.total_promo_clicks || 0) + (s.total_biz_views || 0);
});
const maxStopCount = computed(() => {
if (!stats.value.top_stops.length) return 1;
return Math.max(...stats.value.top_stops.map((s: any) => s.count));
});
const generateReport = async () => {
// OPTIMIZACIÓN: Carga dinámica de librerías pesadas para no afectar el rendimiento inicial
const [{ jsPDF }, html2canvas] = await Promise.all([
import('jspdf'),
import('html2canvas').then(m => m.default)
]);
const date = new Date().toLocaleDateString('es-ES', { month: 'long', year: 'numeric' });
const doc = new jsPDF('p', 'mm', 'a4');
const pageWidth = doc.internal.pageSize.getWidth();
// 1. ENCABEZADO STARK STYLE
doc.setFillColor(30, 41, 59); // Color oscuro SIBU
doc.rect(0, 0, pageWidth, 40, 'F');
doc.setTextColor(254, 231, 21); // Amarillo Activo
doc.setFontSize(22);
doc.setFont('helvetica', 'bold');
doc.text('SIB COMMAND CENTER', 15, 20);
doc.setTextColor(255, 255, 255);
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.text(`INFORME DE INTELIGENCIA ESTRATÉGICA - ${date.toUpperCase()}`, 15, 30);
doc.text(`Generado el: ${new Date().toLocaleString()}`, pageWidth - 15, 30, { align: 'right' });
let cursorY = 55;
// 2. RESUMEN EJECUTIVO
doc.setTextColor(30, 41, 59);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text('1. RESUMEN DEL ECOSISTEMA', 15, cursorY);
cursorY += 10;
doc.setFontSize(11);
doc.setFont('helvetica', 'normal');
const summaryText = `Durante el periodo actual, se han detectado ${stats.value.users?.registered_active || 0} usuarios registrados activos. Las interacciones totales en la red ascienden a ${totalInteractionCount.value}, demostrando un flujo de actividad estable.`;
const splitSummary = doc.splitTextToSize(summaryText, pageWidth - 30);
doc.text(splitSummary, 15, cursorY);
cursorY += splitSummary.length * 7;
// 3. CAPTURA DE GRÁFICOS (Solo si están visibles o los forzamos)
// Nota: html2canvas captura el DOM. Intentaremos capturar los contenedores de las gráficas
const charts = document.querySelectorAll('.chart-container');
if (charts.length > 0) {
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text('2. ANÁLISIS VISUAL DE TENDENCIAS', 15, cursorY);
cursorY += 10;
for (const chart of Array.from(charts).slice(0, 2)) {
if (cursorY > 220) { doc.addPage(); cursorY = 20; }
const canvas = await html2canvas(chart as HTMLElement, { backgroundColor: '#1e293b' });
const imgData = canvas.toDataURL('image/png');
doc.addImage(imgData, 'PNG', 15, cursorY, pageWidth - 30, 60);
cursorY += 70;
}
}
// 4. TABLAS DE DATOS (TRANSPORTE & CASETAS)
if (cursorY > 200) { doc.addPage(); cursorY = 20; }
doc.setTextColor(30, 41, 59);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text('3. LOGÍSTICA Y MOVILIDAD TÁCTICA', 15, cursorY);
cursorY += 10;
doc.setFontSize(10);
doc.text('Top 5 Casetas con más concurrencia:', 15, cursorY);
cursorY += 7;
stats.value.top_stops.slice(0, 5).forEach((stop: any) => {
doc.text(`- Caseta #${stop.id}: ${stop.count} peticiones directas detactadas.`, 20, cursorY);
cursorY += 6;
});
// 5. INTELIGENCIA COMERCIAL
cursorY += 10;
if (cursorY > 240) { doc.addPage(); cursorY = 20; }
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text('4. IMPACTO COMERCIAL (ALIADOS)', 15, cursorY);
cursorY += 10;
doc.setFontSize(11);
doc.setFont('helvetica', 'normal');
doc.text(`Promociones activadas: ${stats.value.summary?.total_promo_clicks || 0} veces.`, 15, cursorY);
cursorY += 6;
doc.text(`Interés en perfiles de negocio: ${stats.value.summary?.total_biz_views || 0} visitas registradas.`, 15, cursorY);
// FOOTER
const totalPages = doc.internal.pages.length - 1;
for (let i = 1; i <= totalPages; i++) {
doc.setPage(i);
doc.setFontSize(8);
doc.setTextColor(150);
doc.text(`SIB Command Center - Página ${i} de ${totalPages} - Confidencial Admin`, pageWidth / 2, 285, { align: 'center' });
}
doc.save(`Informe_Estrategico_SIB_${date.replace(/ /g, '_')}.pdf`);
};
// CHARTS CONFIGURATION (MISMOS DATOS QUE ANTES)
const usageChartData = computed(() => {
const hours = Array.from({ length: 24 }, (_, i) => i);
return {
labels: hours.map(h => `${h}:00`),
datasets: [
{ label: 'Registrados', data: hours.map(h => stats.value.users.patterns.registered[h] || 0), borderColor: '#fee715', backgroundColor: 'rgba(254, 231, 21, 0.2)', tension: 0.4, fill: true },
{ label: 'Invitados', data: hours.map(h => stats.value.users.patterns.guests[h] || 0), borderColor: '#64748b', backgroundColor: 'rgba(100, 116, 139, 0.1)', tension: 0.4, fill: true }
]
};
});
const routesChartData = computed(() => {
const routes = stats.value.shuttles || {};
const labels = Object.keys(routes);
return {
labels: labels.slice(0, 8),
datasets: [{ label: 'Consultas', data: labels.slice(0, 8).map(l => routes[l].views), backgroundColor: '#fee715', borderRadius: 10 }]
};
});
const usageChartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#cbd5e1' } } }, scales: { y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#64748b' } }, x: { ticks: { color: '#64748b' } } } };
const routesChartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#64748b' } }, x: { ticks: { color: '#64748b' } } } };
const calculateConversion = (views: number, actions: number) => (views > 0 ? ((actions / views) * 100).toFixed(1) : 0);
const getHealthClass = (rate: any) => (parseFloat(rate) > 20 ? 'excellent' : parseFloat(rate) > 10 ? 'good' : 'low');
const getHealthLabel = (rate: any) => (parseFloat(rate) > 20 ? 'Alta' : parseFloat(rate) > 10 ? 'Media' : 'Baja');
onMounted(async () => {
try {
const [
{ count: userCount },
{ data: events },
{ data: allBusinesses },
{ data: allShuttles },
{ data: allCoupons }
] = await Promise.all([
supabase.from('users').select('*', { count: 'exact', head: true }).eq('is_active', true),
// In a production app with >1M rows we might use a group-by RPC
supabase.from('analytics_events').select('*'),
supabase.from('businesses').select('name'),
supabase.from('shuttles').select('company_name, vehicle_type'),
supabase.from('coupons').select('title, businesses(name)')
])
const shuttleStats: any = {}
const bizStats: any = {}
let total_promo_clicks = 0
let total_shuttle_contacts = 0
let total_biz_views = 0
// PRE-FILL all businesses so they always show even with 0 clicks
if (allBusinesses) {
for (const b of allBusinesses) {
if (b.name) {
bizStats[b.name] = { views: 0, promos: 0, favs: 0, social: 0, location: 0, calls: 0, coupons: {} }
}
}
}
// PRE-FILL all coupons so they always show even with 0 clicks
if (allCoupons) {
for (const c of allCoupons) {
// Handle Supabase relation mapping (it might be array or object depending on generated types)
const bizData: any = c.businesses
const bizName = (Array.isArray(bizData) ? bizData[0]?.name : bizData?.name) || 'Desconocido'
if (c.title) {
if (!bizStats[bizName]) {
bizStats[bizName] = { views: 0, promos: 0, favs: 0, social: 0, location: 0, calls: 0, coupons: {} }
}
if (!bizStats[bizName].coupons[c.title]) {
bizStats[bizName].coupons[c.title] = { views: 0, location: 0 }
}
}
}
}
// PRE-FILL all shuttles so they always show even with 0 clicks
if (allShuttles) {
for (const s of allShuttles) {
const name = s.company_name || s.vehicle_type || 'shuttle'
if (!shuttleStats[name]) {
shuttleStats[name] = { views: 0, contacts: 0, calls: 0, whatsapp: 0 }
}
}
}
const safeRows = events || []
for (const ev of safeRows) {
const nameKey = ev.entity_name || ev.entity_id
if (ev.entity_type === 'shuttle') {
if (!shuttleStats[nameKey]) {
shuttleStats[nameKey] = { views: 0, contacts: 0, calls: 0, whatsapp: 0 }
}
if (ev.event_name === 'view_details') {
shuttleStats[nameKey].views++
} else if (ev.event_name === 'shuttle_contact') {
shuttleStats[nameKey].contacts++
total_shuttle_contacts++
if (ev.properties?.action === 'whatsapp') shuttleStats[nameKey].whatsapp++
if (ev.properties?.action === 'call') shuttleStats[nameKey].calls++
}
} else if (ev.entity_type === 'business') {
if (!bizStats[nameKey]) {
bizStats[nameKey] = { views: 0, promos: 0, favs: 0, social: 0, location: 0, calls: 0, coupons: {} }
}
if (ev.event_name === 'view_details') {
bizStats[nameKey].views++
total_biz_views++
} else if (ev.event_name === 'favorite_add') {
bizStats[nameKey].favs++
} else if (ev.event_name === 'social_click') {
bizStats[nameKey].social++
} else if (ev.event_name === 'location_click') {
bizStats[nameKey].location++
} else if (ev.event_name === 'contact_click') {
bizStats[nameKey].calls++
} else if (ev.event_name === 'promo_click') {
bizStats[nameKey].promos++
total_promo_clicks++
}
} else if (ev.entity_type === 'coupon') {
// Los cupones se suman a la parte del negocio correspondiente
const bizName = ev.properties?.business || nameKey
if (!bizStats[bizName]) {
bizStats[bizName] = { views: 0, promos: 0, favs: 0, social: 0, location: 0, calls: 0, coupons: {} }
}
if (!bizStats[bizName].coupons[nameKey]) {
bizStats[bizName].coupons[nameKey] = { views: 0, location: 0 }
}
if (ev.event_name === 'promo_view') {
bizStats[bizName].promos++
bizStats[bizName].coupons[nameKey].views++
total_promo_clicks++
} else if (ev.event_name === 'location_click') {
bizStats[bizName].location++
bizStats[bizName].coupons[nameKey].location++
}
}
}
stats.value = {
shuttles: shuttleStats,
businesses: bizStats,
top_stops: [], // Legacy logic can still go here if stops tracking is added
users: {
registered_active: userCount || 0,
patterns: { registered: {}, guests: {} }
},
summary: {
total_shuttle_contacts,
total_promo_clicks,
total_biz_views
}
}
} catch (error) { console.error('Error fetching analytics:', error); } finally { loading.value = false; }
});
</script>
<style scoped>
.strategic-analytics { padding: 40px 24px 120px; max-width: 1350px; margin: 0 auto; color: var(--text-primary); }
.header-section { margin-bottom: 30px; }
.top-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
.download-btn {
background: rgba(254, 231, 21, 0.1);
border: 1px solid var(--active-color);
color: var(--active-color);
padding: 8px 16px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
font-weight: 800;
cursor: pointer;
transition: all 0.3s;
}
.download-btn:hover {
background: var(--active-color);
color: #101820;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(254, 231, 21, 0.3);
}
h1 { font-size: 2.2rem; font-weight: 900; margin: 0; }
.subtitle { color: var(--text-secondary); margin-top: 6px; }
/* TABS */
.tabs-control { display: flex; gap: 12px; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 1px solid var(--border-color); }
.tab-btn { background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-secondary); padding: 12px 24px; border-radius: 16px; display: flex; align-items: center; gap: 10px; cursor: pointer; transition: all 0.3s; font-weight: 700; }
.tab-btn.active { background: var(--active-color); color: #101820; border-color: var(--active-color); }
.tab-btn:hover:not(.active) { border-color: var(--active-color); color: var(--active-color); }
/* CONTENT */
.dashboard-layout { display: grid; grid-template-columns: 1fr 340px; gap: 40px; }
.analysis-section { margin-bottom: 60px; }
.section-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
.section-header h2 { font-size: 1rem; font-weight: 800; text-transform: uppercase; color: var(--text-secondary); }
.side-info { display: flex; flex-direction: column; gap: 16px; }
.info-box { background: var(--bg-secondary); padding: 24px; border-radius: 24px; border: 1px solid var(--border-color); }
.info-box .material-icons { color: #fee715; margin-bottom: 12px; }
.info-box h4 { margin: 0 0 8px; font-weight: 800; color: #fee715; }
.info-box p { font-size: 0.85rem; line-height: 1.6; color: var(--text-secondary); margin: 0; }
/* KPI */
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 24px; margin-bottom: 40px; }
.kpi-card { background: var(--card-bg); padding: 24px; border-radius: 24px; border: 1px solid var(--border-color); display: flex; align-items: center; gap: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
.kpi-icon { width: 50px; height: 50px; border-radius: 14px; display: flex; align-items: center; justify-content: center; background: rgba(254, 231, 21, 0.1); color: #fee715; }
.kpi-value { display: block; font-size: 2rem; font-weight: 900; }
.kpi-label { font-size: 0.75rem; color: var(--text-secondary); font-weight: 600; text-transform: uppercase; }
/* TABLES & CHARTS */
.chart-container { height: 320px; background: rgba(0,0,0,0.2); border-radius: 24px; padding: 24px; border: 1px solid var(--border-color); }
.chart-container.large { height: 400px; }
.data-table-wrapper { background: var(--card-bg); border-radius: 24px; border: 1px solid var(--border-color); overflow: hidden; }
.data-table { width: 100%; border-collapse: collapse; }
.data-table th { padding: 16px; font-size: 0.7rem; color: var(--text-secondary); text-transform: uppercase; text-align: left; background: rgba(255,255,255,0.02); }
.data-table td { padding: 18px 16px; border-bottom: 1px solid var(--border-color); }
.id-cell { font-family: monospace; color: #fee715; font-weight: 700; }
.progress-bar { width: 100%; height: 6px; background: rgba(255,255,255,0.05); border-radius: 10px; overflow: hidden; }
.progress-fill { height: 100%; background: #fee715; }
.mini-bar { width: 80px; height: 5px; background: rgba(255,255,255,0.05); border-radius: 10px; overflow: hidden; }
.mini-bar .fill { height: 100%; background: #fee715; }
.status-pill { padding: 4px 12px; border-radius: 100px; font-size: 0.7rem; font-weight: 800; }
.status-pill.excellent { background: rgba(16, 185, 129, 0.1); color: #10b981; }
.status-pill.good { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.status-pill.low { background: rgba(244, 63, 94, 0.1); color: #f43f5e; }
/* BUSINESS LIST CARDS */
.business-list { display: flex; flex-direction: column; gap: 20px; }
.business-card-analytics { background: var(--card-bg); border-radius: 20px; border: 1px solid var(--border-color); padding: 24px; box-shadow: 0 10px 30px rgba(0,0,0,0.05); }
.business-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid var(--border-color); }
.business-title-info { display: flex; align-items: center; gap: 12px; }
.biz-icon-box { background: rgba(254,231,21,0.1); color: #fee715; width: 44px; height: 44px; border-radius: 12px; display: flex; justify-content: center; align-items: center; }
.business-title-info h3 { margin: 0; font-size: 1.3rem; font-weight: 800; color: var(--text-primary); }
.business-total-badge { background: #1e293b; color: white; padding: 6px 14px; border-radius: 20px; display: flex; align-items: center; gap: 6px; font-size: 0.85rem; }
.business-total-badge .material-icons { font-size: 16px; color: #fee715; }
.business-details-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 16px; margin-bottom: 24px; }
.detail-item { display: flex; align-items: center; gap: 12px; background: var(--bg-secondary); padding: 12px 14px; border-radius: 14px; border: 1px solid var(--border-color); }
.detail-item .material-icons { font-size: 20px; }
.detail-info { display: flex; flex-direction: column; }
.detail-value { font-size: 1.2rem; font-weight: 900; color: var(--text-primary); }
.detail-label { font-size: 0.65rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 700; margin-top:2px; }
.business-coupons-section { background: var(--bg-secondary); border-radius: 16px; padding: 20px; border: 1px solid var(--border-color); }
.business-coupons-section.has-coupons { border-color: rgba(254,231,21,0.3); background: rgba(254,231,21,0.02); }
.coupons-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.business-coupons-section h4 { margin: 0; font-size: 1rem; font-weight: 800; color: var(--text-primary); }
.coupon-list { display: flex; flex-direction: column; gap: 10px; }
.coupon-item { display: flex; justify-content: space-between; align-items: center; background: var(--card-bg); padding: 12px 16px; border-radius: 12px; border: 1px solid var(--border-color); box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
.coupon-name-box { display: flex; align-items: center; gap: 8px; }
.coupon-name { font-weight: 700; font-size: 0.9rem; color: var(--text-primary); }
.coupon-stats { display: flex; gap: 16px; font-size: 0.85rem; font-weight: 700; color: var(--text-primary); }
.coupon-stats .stat { display: flex; align-items: center; gap: 4px; background: var(--bg-secondary); padding: 4px 10px; border-radius: 8px;}
.coupon-stats .stat .material-icons { font-size: 14px; color: var(--text-secondary); }
.no-coupons { font-size: 0.85rem; color: var(--text-secondary); display:flex; align-items:center; gap:8px; font-weight:600;}
/* ANIMATIONS */
.animate-fade { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.spin { animation: spin 2s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@media (max-width: 1100px) { .dashboard-layout { grid-template-columns: 1fr; } .side-info { order: 2; } }
</style>