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:
Binary file not shown.
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user