diff --git a/data/tenerifeprop.db b/data/tenerifeprop.db index 6c5d897..ea0eb50 100644 Binary files a/data/tenerifeprop.db and b/data/tenerifeprop.db differ diff --git a/public/admin-full.html b/public/admin-full.html index 8da98d3..b661fa9 100644 --- a/public/admin-full.html +++ b/public/admin-full.html @@ -3200,7 +3200,7 @@ padding: 15 if (!data.success || !data.data) { // Not authenticated, redirect to login - window.location.href = '/login.html'; + window.location.href = '/login'; return; } @@ -3231,7 +3231,7 @@ padding: 15 } } catch (error) { console.error('Auth check failed:', error); - window.location.href = '/login.html'; + window.location.href = '/login'; } })(); @@ -3240,10 +3240,10 @@ padding: 15 try { await fetch('/api/auth/logout', { method: 'POST' }); localStorage.removeItem('user'); - window.location.href = '/login.html'; + window.location.href = '/login'; } catch (error) { console.error('Logout failed:', error); - window.location.href = '/login.html'; + window.location.href = '/login'; } } diff --git a/public/admin-old.html b/public/admin-old.html index 8da98d3..b661fa9 100644 --- a/public/admin-old.html +++ b/public/admin-old.html @@ -3200,7 +3200,7 @@ padding: 15 if (!data.success || !data.data) { // Not authenticated, redirect to login - window.location.href = '/login.html'; + window.location.href = '/login'; return; } @@ -3231,7 +3231,7 @@ padding: 15 } } catch (error) { console.error('Auth check failed:', error); - window.location.href = '/login.html'; + window.location.href = '/login'; } })(); @@ -3240,10 +3240,10 @@ padding: 15 try { await fetch('/api/auth/logout', { method: 'POST' }); localStorage.removeItem('user'); - window.location.href = '/login.html'; + window.location.href = '/login'; } catch (error) { console.error('Logout failed:', error); - window.location.href = '/login.html'; + window.location.href = '/login'; } } diff --git a/public/admin.html b/public/admin.html index 425f03e..d00d9c5 100644 --- a/public/admin.html +++ b/public/admin.html @@ -2807,266 +2807,275 @@ Ver todos gray: '#94a3b8' }; - // Performance Chart - const performanceCtx = document.getElementById('performanceChart').getContext('2d'); - new Chart(performanceCtx, { - type: 'line', - data: { - labels: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'], - datasets: [{ - label: 'Vistas', - data: [1200, 1900, 2400, 2100, 2800, 3200, 2900, 3500, 3100, 3800, 3400, 4200], - borderColor: chartColors.primary, - backgroundColor: 'rgba(26, 95, 74, 0.1)', - fill: true, - tension: 0.4, - pointRadius: 4, - pointHoverRadius: 6 - }, { - label: 'Leads', - data: [80, 120, 150, 130, 180, 210, 190, 230, 200, 260, 240, 280], - borderColor: chartColors.secondary, - backgroundColor: 'rgba(212, 168, 83, 0.1)', - fill: true, - tension: 0.4, - pointRadius: 4, - pointHoverRadius: 6 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top', - labels: { -usePointStyle: true, -padding: 20 - } - } + // Chart instances storage + let charts = {}; + + // Initialize charts with empty data + function initCharts() { + const chartColors = { + primary: '#1a5f4a', + primaryLight: '#2d8f6f', + secondary: '#d4a853', + success: '#10b981', + warning: '#f59e0b', + danger: '#ef4444', + info: '#3b82f6', + gray: '#94a3b8' + }; + + // Performance Chart + const performanceCtx = document.getElementById('performanceChart').getContext('2d'); + charts.performance = new Chart(performanceCtx, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Vistas', + data: [], + borderColor: chartColors.primary, + backgroundColor: 'rgba(26, 95, 74, 0.1)', + fill: true, + tension: 0.4, + pointRadius: 4, + pointHoverRadius: 6 + }, { + label: 'Leads', + data: [], + borderColor: chartColors.secondary, + backgroundColor: 'rgba(212, 168, 83, 0.1)', + fill: true, + tension: 0.4, + pointRadius: 4, + pointHoverRadius: 6 + }] }, - scales: { - y: { - beginAtZero: true, - grid: { -color: 'rgba(0,0,0,0.05)' + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + labels: { usePointStyle: true, padding: 20 } } }, - x: { - grid: { -display: false - } + scales: { + y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' } }, + x: { grid: { display: false } } } } - } - }); + }); - // Traffic Sources Chart - const trafficCtx = document.getElementById('trafficChart').getContext('2d'); - new Chart(trafficCtx, { - type: 'doughnut', - data: { - labels: ['Google', 'Directo', 'Instagram', 'Referidos', 'Facebook'], - datasets: [{ - data: [35, 25, 18, 12, 10], - backgroundColor: [ - chartColors.primary, - chartColors.secondary, - chartColors.info, - chartColors.success, - chartColors.warning - ], - borderWidth: 0 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'bottom', - labels: { -usePointStyle: true, -padding: 15 + // Traffic Sources Chart + const trafficCtx = document.getElementById('trafficChart').getContext('2d'); + charts.traffic = new Chart(trafficCtx, { + type: 'doughnut', + data: { + labels: ['Directo', 'Búsqueda', 'Social', 'Referido', 'Email'], + datasets: [{ + data: [35, 30, 20, 10, 5], + backgroundColor: [ + chartColors.primary, + chartColors.secondary, + chartColors.info, + chartColors.success, + chartColors.warning + ], + borderWidth: 0 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { usePointStyle: true, padding: 15 } } - } - }, - cutout: '70%' - } - }); - - // Property Types Chart - const typesCtx = document.getElementById('typesChart').getContext('2d'); - new Chart(typesCtx, { - type: 'bar', - data: { - labels: ['Agrícola', 'Urbano', 'Casas', 'Apartamentos', 'Ruinas'], - datasets: [{ - label: 'Propiedades', - data: [25, 30, 20, 15, 10], - backgroundColor: [ - chartColors.primary, - '#4a90d9', - '#9b59b6', - chartColors.secondary, - chartColors.danger - ], - borderRadius: 8 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { display: false } - }, - scales: { - y: { - beginAtZero: true, - grid: { color: 'rgba(0,0,0,0.05)' } }, - x: { - grid: { display: false } - } + cutout: '70%' } - } - }); + }); - // Leads Status Chart - const leadsCtx = document.getElementById('leadsChart').getContext('2d'); - new Chart(leadsCtx, { - type: 'polarArea', - data: { - labels: ['Nuevos', 'Pendientes', 'Contactados', 'Cualificados', 'Convertidos'], - datasets: [{ - data: [12, 8, 15, 6, 4], - backgroundColor: [ - 'rgba(59, 130, 246, 0.8)', - 'rgba(245, 158, 11, 0.8)', - 'rgba(16, 185, 129, 0.8)', - 'rgba(139, 92, 246, 0.8)', - 'rgba(34, 197, 94, 0.8)' - ], - borderWidth: 0 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'bottom', - labels: { usePointStyle: true, padding: 10, font: { size: 10 } } - } + // Property Types Chart + const typesCtx = document.getElementById('typesChart').getContext('2d'); + charts.types = new Chart(typesCtx, { + type: 'bar', + data: { + labels: ['Urbano', 'Agrícola', 'Casa', 'Apartamento', 'Ruinas'], + datasets: [{ + label: 'Propiedades', + data: [0, 0, 0, 0, 0], + backgroundColor: [ + chartColors.primary, + '#4a90d9', + '#9b59b6', + chartColors.secondary, + chartColors.danger + ], + borderRadius: 8 + }] }, - scales: { - r: { - grid: { display: false } + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false } }, + scales: { + y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' } }, + x: { grid: { display: false } } } } - } - }); + }); - // Top Properties Chart - const topPropsCtx = document.getElementById('topPropertiesChart').getContext('2d'); - new Chart(topPropsCtx, { - type: 'bar', - data: { - labels: ['Terreno Adeje', 'Villa Marina', 'Ático Luz', 'Casa Palmera', 'Solar Norte'], - datasets: [{ - label: 'Vistas', - data: [1245, 986, 876, 754, 623], - backgroundColor: chartColors.primary, - borderRadius: 6 - }] - }, - options: { - indexAxis: 'y', - responsive: true, - maintainAspectRatio: false, - plugins: { legend: { display: false } }, - scales: { - x: { grid: { color: 'rgba(0,0,0,0.05)' } }, - y: { grid: { display: false } } + // Leads Status Chart + const leadsCtx = document.getElementById('leadsChart').getContext('2d'); + charts.leads = new Chart(leadsCtx, { + type: 'doughnut', + data: { + labels: ['Nuevo', 'Contactado', 'Calificado', 'Negociando', 'Cerrado'], + datasets: [{ + data: [0, 0, 0, 0, 0], + backgroundColor: [ + '#3b82f6', + '#f59e0b', + '#10b981', + '#8b5cf6', + '#1a5f4a' + ], + borderWidth: 0 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { usePointStyle: true, padding: 10, font: { size: 10 } } + } + } } - } - }); + }); - // Daily Performance Chart (Analytics) - const dailyCtx = document.getElementById('dailyPerformanceChart').getContext('2d'); - new Chart(dailyCtx, { - type: 'bar', - data: { - labels: Array.from({length: 30}, (_, i) => `Día ${i + 1}`), - datasets: [{ - label: 'Visitantes', - data: Array.from({length: 30}, () => Math.floor(Math.random() * 400) + 100), - backgroundColor: 'rgba(26, 95, 74, 0.7)', - borderRadius: 4 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { legend: { display: false } }, - scales: { - y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' } }, - x: { grid: { display: false } } + // Top Properties Chart + const topPropsCtx = document.getElementById('topPropertiesChart').getContext('2d'); + charts.topProperties = new Chart(topPropsCtx, { + type: 'bar', + data: { + labels: [], + datasets: [{ + label: 'Vistas', + data: [], + backgroundColor: chartColors.primary, + borderRadius: 6 + }] + }, + options: { + indexAxis: 'y', + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false } }, + scales: { + x: { grid: { color: 'rgba(0,0,0,0.05)' } }, + y: { grid: { display: false } } + } } - } - }); + }); + } - // Devices Chart - const devicesCtx = document.getElementById('devicesChart').getContext('2d'); - new Chart(devicesCtx, { - type: 'pie', - data: { - labels: ['Móvil', 'Desktop', 'Tablet'], - datasets: [{ - data: [58, 35, 7], - backgroundColor: [chartColors.primary, chartColors.secondary, chartColors.info], - borderWidth: 0 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { position: 'bottom', labels: { usePointStyle: true, padding: 15 } } + // Load dashboard data from API + async function loadDashboardData() { + try { + // Load stats + const statsRes = await fetch('/api/admin/stats', { credentials: 'include' }); + const statsData = await statsRes.json(); + + if (statsData.success) { + const stats = statsData.data; + + // Animate stats + animateCounter($('#statViews'), stats.analytics.views || 0, 1500); + animateCounter($('#statLeads'), stats.leads.new || 0, 1500); + animateCounter($('#statClicks'), stats.analytics.inquiries || 0, 1500); + + // Update conversion rate + const conversion = stats.leads.total > 0 + ? ((stats.leads.closed / stats.leads.total) * 100).toFixed(1) + : '0'; + $('#statConversion').text(conversion + '%'); } - } - }); - // Geo Chart - const geoCtx = document.getElementById('geoChart').getContext('2d'); - new Chart(geoCtx, { - type: 'bar', - data: { - labels: ['España', 'Rusia', 'Alemania', 'Reino Unido', 'Francia'], - datasets: [{ - label: 'Visitantes', - data: [2456, 1234, 987, 876, 654], - backgroundColor: [ - '#e74c3c', - '#3498db', - '#f1c40f', - '#9b59b6', - '#1abc9c' - ], - borderRadius: 8 - }] - }, - options: { - indexAxis: 'y', - responsive: true, - maintainAspectRatio: false, - plugins: { legend: { display: false } }, - scales: { - x: { grid: { color: 'rgba(0,0,0,0.05)' } }, - y: { grid: { display: false } } + // Load charts data + const chartsRes = await fetch('/api/admin/analytics/charts', { credentials: 'include' }); + const chartsData = await chartsRes.json(); + + if (chartsData.success) { + const data = chartsData.data; + + // Update Performance Chart + if (charts.performance) { + charts.performance.data.labels = data.months; + charts.performance.data.datasets[0].data = data.viewsPerMonth; + charts.performance.data.datasets[1].data = data.leadsPerMonth; + charts.performance.update(); + } + + // Update Leads Status Chart + if (charts.leads && data.leadsStatus) { + const statusLabels = { + 'new': 'Nuevo', + 'contacted': 'Contactado', + 'qualified': 'Calificado', + 'negotiating': 'Negociando', + 'closed': 'Cerrado' + }; + const statusColors = { + 'new': '#3b82f6', + 'contacted': '#f59e0b', + 'qualified': '#10b981', + 'negotiating': '#8b5cf6', + 'closed': '#1a5f4a' + }; + + charts.leads.data.labels = data.leadsStatus.map(l => statusLabels[l.status] || l.status); + charts.leads.data.datasets[0].data = data.leadsStatus.map(l => l.count); + charts.leads.data.datasets[0].backgroundColor = data.leadsStatus.map(l => statusColors[l.status] || '#6c757d'); + charts.leads.update(); + } + + // Update Types Chart (by city instead, since we don't have type distribution) + if (charts.types && data.propertiesByCity) { + const cities = data.propertiesByCity.slice(0, 5).map(c => c.city); + const counts = data.propertiesByCity.slice(0, 5).map(c => c.count); + charts.types.data.labels = cities; + charts.types.data.datasets[0].data = counts; + charts.types.update(); + } } + + // Load top properties + const propsRes = await fetch('/api/properties?limit=5&lang=es', { credentials: 'include' }); + const propsData = await propsRes.json(); + + if (propsData.success && propsData.data) { + const topProps = propsData.data + .sort((a, b) => (b.views_count || 0) - (a.views_count || 0)) + .slice(0, 5); + + if (charts.topProperties) { + charts.topProperties.data.labels = topProps.map(p => p.reference); + charts.topProperties.data.datasets[0].data = topProps.map(p => p.views_count || 0); + charts.topProperties.update(); + } + } + + } catch (error) { + console.error('Failed to load dashboard data:', error); } - }); + } + + // Initialize on page load + initCharts(); + loadDashboardData(); // Period buttons $('.chart-period-btn').on('click', function() { @@ -3164,13 +3173,6 @@ padding: 15 }, 16); } - // Animate stats on load - setTimeout(() => { - animateCounter($('#statViews'), 24892, 1500); - animateCounter($('#statClicks'), 3421, 1500); - animateCounter($('#statLeads'), 156, 1500); - }, 500); - // Notifications dropdown $('.topbar-btn').first().on('click', function() { // Show notifications diff --git a/src/server/index.ts b/src/server/index.ts index 75eadba..af64a0e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1434,7 +1434,6 @@ app.get('/admin/settings.html', serveStatic({ path: './public/admin/settings.htm app.get('/property/*', serveStatic({ path: './public/property.html' })) app.get('/admin', serveStatic({ path: './public/admin.html' })) app.get('/login', serveStatic({ path: './public/login.html' })) -app.get('/login.html', serveStatic({ path: './public/login.html' })) // Fallback to index.html for all other routes app.get('*', serveStatic({ path: './public/index.html' }))