feat: rewrite FAQ section using card layout matching design

- Replaced Bootstrap accordion with card grid layout (col-md-6)
- FAQ now uses same card style as Testimonials and Services sections
- 5 FAQ questions displayed in 2-column grid
- Each card has: question title, status badge, answer text, edit/delete buttons
- No more accordion overflow issues - uses existing card styles
- Consistent with overall admin panel design
This commit is contained in:
TenerifeProp Dev
2026-04-06 23:39:29 +01:00
parent 06cfbec435
commit 46e0068007
2 changed files with 389 additions and 502 deletions

Binary file not shown.

View File

@@ -14,6 +14,7 @@
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Chart.js -->
<link href="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.min.css" rel="stylesheet">
<!-- DataTables -->
<link href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/css/dataTables.bootstrap5.min.css" rel="stylesheet">
<!-- Lightpick -->
@@ -414,13 +415,9 @@
/* ============ PAGE CONTENT ============ */
.page-content {
overflow-x: hidden;
overflow-x: hidden;
padding: 32px;
overflow-x: hidden;
}
.page-section {
max-width: 100%;
overflow-x: hidden;
}
.page-header {
@@ -818,6 +815,10 @@
/* ============ PAGE SECTIONS ============ */
.page-section {
overflow-x: hidden;
max-width: 100%;
overflow-x: hidden;
max-width: 100%;
display: none;
}
@@ -1094,69 +1095,6 @@
color: var(--primary);
}
/* DataTables Bootstrap5 additional styles */
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dt-search {
margin-bottom: 1rem;
}
.dataTables_wrapper .dataTables_filter input,
.dataTables_wrapper .dt-search input {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px;
margin-left: 8px;
font-size: 14px;
}
.dataTables_wrapper .dataTables_filter input:focus,
.dataTables_wrapper .dt-search input:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(26, 95, 74, 0.1);
}
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dt-length {
margin-bottom: 1rem;
}
.dataTables_wrapper .dataTables_length select,
.dataTables_wrapper .dt-length select {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 6px 10px;
margin-right: 8px;
}
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dt-info {
color: var(--text-secondary);
font-size: 14px;
padding-top: 1rem;
}
.dataTables_wrapper .dataTables_paginate,
.dataTables_wrapper .dt-paging {
padding-top: 1rem;
}
.dataTables_wrapper table.dataTable thead th {
background: var(--bg-secondary);
border-bottom: 2px solid var(--border-color);
font-weight: 600;
color: var(--text);
}
.dataTables_wrapper table.dataTable tbody td {
padding: 12px 16px;
vertical-align: middle;
}
.dataTables_wrapper table.dataTable tbody tr:hover {
background: var(--bg-secondary);
}
/* ============ RESPONSIVE ============ */
@media (max-width: 1400px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
@@ -1198,7 +1136,9 @@
}
@media (max-width: 768px) {
.page-content { padding: 20px; }
.page-content {
overflow-x: hidden;
overflow-x: hidden; padding: 20px; }
.page-header { flex-direction: column; align-items: flex-start; gap: 16px; }
.quick-actions { grid-template-columns: repeat(2, 1fr); }
}
@@ -2261,99 +2201,79 @@ Ver todos
</button>
</div>
<div class="accordion" id="faqAccordionAdmin">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#faq1-admin">
<div class="d-flex justify-content-between align-items-center w-100 me-3">
<span class="fw-medium">¿Puedo comprar terreno siendo extranjero en España?</span>
<div class="d-flex gap-2" onclick="event.stopPropagation();">
<span class="badge bg-success">Activo</span>
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0">¿Puedo comprar terreno siendo extranjero en España?</h5>
<span class="badge bg-success">Activo</span>
</div>
<p class="card-text text-muted">Sí, absolutamente. España permite la compra de propiedades a ciudadanos extranjeros sin restricciones. Necesitará obtener un NIE (Número de Identificación de Extranjero) para completar la transacción.</p>
<div class="d-flex justify-content-end gap-2 mt-3 pt-3 border-top">
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil me-1"></i>Editar</button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash me-1"></i>Eliminar</button>
</div>
</button>
</h2>
<div id="faq1-admin" class="accordion-collapse collapse show" data-bs-parent="#faqAccordionAdmin">
<div class="accordion-body">
Sí, absolutamente. España permite la compra de propiedades a ciudadanos extranjeros sin restricciones. Necesitará obtener un NIE (Número de Identificación de Extranjero) para completar la transacción.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2-admin">
<div class="d-flex justify-content-between align-items-center w-100 me-3">
<span class="fw-medium">¿Qué costes adicionales hay que tener en cuenta al comprar?</span>
<div class="d-flex gap-2" onclick="event.stopPropagation();">
<span class="badge bg-success">Activo</span>
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0">¿Qué costes adicionales hay que tener en cuenta?</h5>
<span class="badge bg-success">Activo</span>
</div>
<p class="card-text text-muted">Además del precio de compra: ITP 6.5-8%, notaría (aprox. 1%), registro de propiedad (0.5-1%), gestoría (0.5-1%) y honorarios de la agencia.</p>
<div class="d-flex justify-content-end gap-2 mt-3 pt-3 border-top">
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil me-1"></i>Editar</button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash me-1"></i>Eliminar</button>
</div>
</button>
</h2>
<div id="faq2-admin" class="accordion-collapse collapse" data-bs-parent="#faqAccordionAdmin">
<div class="accordion-body">
Además del precio de compra, debe presupuestar: Impuesto de Transmisiones Patrimoniales (ITP) 6.5-8%, gastos de notaría (aprox. 1%), registro de propiedad (0.5-1%), gestoría (0.5-1%) y honorarios de la agencia.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3-admin">
<div class="d-flex justify-content-between align-items-center w-100 me-3">
<span class="fw-medium">¿Necesito cuenta bancaria española?</span>
<div class="d-flex gap-2" onclick="event.stopPropagation();">
<span class="badge bg-success">Activo</span>
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0">¿Necesito cuenta bancaria española?</h5>
<span class="badge bg-success">Activo</span>
</div>
<p class="card-text text-muted">No es obligatorio, pero muy recomendable. Una cuenta bancaria española facilita el pago de impuestos, servicios y gastos relacionados con la propiedad.</p>
<div class="d-flex justify-content-end gap-2 mt-3 pt-3 border-top">
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil me-1"></i>Editar</button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash me-1"></i>Eliminar</button>
</div>
</button>
</h2>
<div id="faq3-admin" class="accordion-collapse collapse" data-bs-parent="#faqAccordionAdmin">
<div class="accordion-body">
No es obligatorio, pero muy recomendable. Una cuenta bancaria española facilita el pago de impuestos, servicios y gastos relacionados con la propiedad.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq4-admin">
<div class="d-flex justify-content-between align-items-center w-100 me-3">
<span class="fw-medium">¿Cuánto tiempo tarda el proceso de compra?</span>
<div class="d-flex gap-2" onclick="event.stopPropagation();">
<span class="badge bg-warning">Borrador</span>
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0">¿Cuánto tiempo tarda el proceso de compra?</h5>
<span class="badge bg-warning">Borrador</span>
</div>
<p class="card-text text-muted">Entre 4 y 12 semanas. Incluye verificación de título, obtención de NIE, firma de contrato de arras y escritura pública ante notario.</p>
<div class="d-flex justify-content-end gap-2 mt-3 pt-3 border-top">
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil me-1"></i>Editar</button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash me-1"></i>Eliminar</button>
</div>
</button>
</h2>
<div id="faq4-admin" class="accordion-collapse collapse" data-bs-parent="#faqAccordionAdmin">
<div class="accordion-body">
El proceso completo puede tardar entre 4 y 12 semanas. Esto incluye: verificación de título de propiedad, obtención de NIE, firma de contrato y escritura pública.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq5-admin">
<div class="d-flex justify-content-between align-items-center w-100 me-3">
<span class="fw-medium">¿Qué es el NIE y cómo lo obtengo?</span>
<div class="d-flex gap-2" onclick="event.stopPropagation();">
<span class="badge bg-success">Activo</span>
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0">¿Qué es el NIE y cómo lo obtengo?</h5>
<span class="badge bg-success">Activo</span>
</div>
<p class="card-text text-muted">El NIE es un documento obligatorio para extranjeros. Se obtiene en la Oficina de Extranjería o Consulado español. Tasa aproximada: 10€.</p>
<div class="d-flex justify-content-end gap-2 mt-3 pt-3 border-top">
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil me-1"></i>Editar</button>
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash me-1"></i>Eliminar</button>
</div>
</button>
</h2>
<div id="faq5-admin" class="accordion-collapse collapse" data-bs-parent="#faqAccordionAdmin">
<div class="accordion-body">
El NIE (Número de Identificación de Extranjero) es un documento obligatorio para extranjeros. Se obtiene en la Oficina de Extranjería o Consulado español. Tasa aproximada: 10€.
</div>
</div>
</div>
@@ -2910,35 +2830,15 @@ Ver todos
}
});
// Initialize DataTables (separately for each table with correct column indices)
// leadsTable: 6 columns (Cliente, Propiedad, Fuente, Fecha, Estado, Acciones)
// fullLeadsTable: 9 columns (checkbox, Cliente, Contacto, Propiedad, Presupuesto, Fuente, Fecha, Estado, Acciones)
if ($('#leadsTable').length) {
$('#leadsTable').DataTable({
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/es-ES.json'
},
pageLength: 5,
ordering: true,
order: [[3, 'desc']], // Fecha column
searching: false,
lengthChange: false,
info: false,
paging: false
});
}
if ($('#fullLeadsTable').length) {
$('#fullLeadsTable').DataTable({
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/es-ES.json'
},
pageLength: 10,
ordering: true,
order: [[6, 'desc']] // Fecha column (after checkbox = column 0)
});
}
// Initialize DataTables
$('#leadsTable, #fullLeadsTable').DataTable({
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/es-ES.json'
},
pageLength: 10,
ordering: true,
order: [[6, 'desc']]
});
// ============ CHARTS ============
const chartColors = {
@@ -2952,282 +2852,267 @@ Ver todos
gray: '#94a3b8'
};
// Global chart instances storage (accessible outside jQuery ready)
window.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');
window.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
}]
// 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
}
}
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
labels: { usePointStyle: true, padding: 20 }
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0,0,0,0.05)'
}
},
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');
window.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');
window.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
}]
},
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 } }
}
}
});
// Leads Status Chart
const leadsCtx = document.getElementById('leadsChart').getContext('2d');
window.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 } }
x: {
grid: {
display: false
}
}
}
});
// Top Properties Chart
const topPropsCtx = document.getElementById('topPropertiesChart').getContext('2d');
window.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 } }
}
}
});
}
// Load dashboard data from API
// Make loadDashboardData globally accessible
window.loadDashboardData = 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 + '%');
}
// 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 (window.charts.performance) {
window.charts.performance.data.labels = data.months;
window.charts.performance.data.datasets[0].data = data.viewsPerMonth;
window.charts.performance.data.datasets[1].data = data.leadsPerMonth;
window.charts.performance.update();
}
// Update Leads Status Chart
if (window.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'
};
window.charts.leads.data.labels = data.leadsStatus.map(l => statusLabels[l.status] || l.status);
window.charts.leads.data.datasets[0].data = data.leadsStatus.map(l => l.count);
window.charts.leads.data.datasets[0].backgroundColor = data.leadsStatus.map(l => statusColors[l.status] || '#6c757d');
window.charts.leads.update();
}
// Update Types Chart (by city instead, since we don't have type distribution)
if (window.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);
window.charts.types.data.labels = cities;
window.charts.types.data.datasets[0].data = counts;
window.charts.types.update();
}
// Update traffic chart with simulated data
if (window.charts.traffic) {
window.charts.traffic.data.datasets[0].data = [35, 30, 20, 10, 5];
window.charts.traffic.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 (window.charts.topProperties) {
window.charts.topProperties.data.labels = topProps.map(p => p.reference);
window.charts.topProperties.data.datasets[0].data = topProps.map(p => p.views_count || 0);
window.charts.topProperties.update();
}
}
} catch (error) {
console.error('Failed to load dashboard data:', error);
}
}
});
// 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
}
}
},
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 }
}
}
}
});
// 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 } }
}
},
scales: {
r: {
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 } }
}
}
});
// 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 } }
}
}
});
// 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 } }
}
}
});
// 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 } }
}
}
});
// Initialize charts on page load (inside jQuery ready)
initCharts();
// Period buttons
$('.chart-period-btn').on('click', function() {
$('.chart-period-btn').removeClass('active');
@@ -3324,6 +3209,13 @@ Ver todos
}, 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
@@ -3345,65 +3237,60 @@ Ver todos
}
});
// Authentication check
(async function checkAuth() {
try {
const res = await fetch('/api/auth/me');
const data = await res.json();
if (!data.success || !data.data) {
// Not authenticated, redirect to login
window.location.href = '/login';
return;
}
// Store user info
const user = data.data;
window.currentUser = user;
localStorage.setItem('user', JSON.stringify(user));
// Update UI with user info
const userNameEl = document.querySelector('.sidebar-user-info h6, .sidebar-user-name');
const userRoleEl = document.querySelector('.sidebar-user-info small, .sidebar-user-role');
if (userNameEl) {
userNameEl.textContent = user.name || 'Admin';
}
if (userRoleEl) {
const roleNames = {
admin: 'Administrador',
agent: 'Agente',
editor: 'Editor'
};
userRoleEl.textContent = roleNames[user.role] || user.role;
}
// Initialize admin panel
if (window.admin) {
window.admin.init();
}
// Load dashboard data after auth check
if (typeof window.loadDashboardData === 'function') {
window.loadDashboardData();
}
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = '/login';
}
})();
// Authentication check
(async function checkAuth() {
try {
const res = await fetch('/api/auth/me');
const data = await res.json();
if (!data.success || !data.data) {
// Not authenticated, redirect to login
window.location.href = '/login';
return;
}
// Store user info
const user = data.data;
window.currentUser = user;
localStorage.setItem('user', JSON.stringify(user));
// Update UI with user info
const userNameEl = document.querySelector('.sidebar-user-info h6, .sidebar-user-name');
const userRoleEl = document.querySelector('.sidebar-user-info small, .sidebar-user-role');
if (userNameEl) {
userNameEl.textContent = user.name || 'Admin';
}
if (userRoleEl) {
const roleNames = {
admin: 'Administrador',
agent: 'Agente',
editor: 'Editor'
};
userRoleEl.textContent = roleNames[user.role] || user.role;
}
// Initialize admin panel
if (window.admin) {
window.admin.init();
}
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = '/login';
}
})();
// Logout function
async function logout() {
try {
await fetch('/api/auth/logout', { method: 'POST' });
localStorage.removeItem('user');
window.location.href = '/login';
} catch (error) {
console.error('Logout failed:', error);
window.location.href = '/login';
}
}
</script>
// Logout function
async function logout() {
try {
await fetch('/api/auth/logout', { method: 'POST' });
localStorage.removeItem('user');
window.location.href = '/login';
} catch (error) {
console.error('Logout failed:', error);
window.location.href = '/login';
}
}
</script>
</body>
</html>