refactor: split admin.html into modular section files with dynamic loading

- Extracted 10 sections from admin.html (3315 lines) into separate files:
  - dashboard.html (298 lines)
  - properties.html (242 lines)
  - leads.html (280 lines)
  - testimonials.html (78 lines)
  - faq.html (91 lines)
  - services.html (61 lines)
  - settings.html (93 lines)
  - users.html (73 lines)
  - analytics.html (64 lines)
  - traffic.html (69 lines)
- admin.html reduced from 3315 to 1582 lines
- Added dynamic section loader via fetch()
- Sections load on-demand when clicking sidebar links
- Previously loaded sections cached in memory
- Updated server routes to serve all section files
- DataTables initialized per-section on load
This commit is contained in:
TenerifeProp Dev
2026-04-07 00:25:36 +01:00
parent a649ff502f
commit 9cffbb3bf3
12 changed files with 1428 additions and 3107 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
<!-- ============ ANALYTICS SECTION ============ -->
<section class="page-section" id="section-analytics">
<div class="page-header">
<div>
<h1 class="page-title">Estadísticas detalladas</h1>
<p class="page-subtitle">Análisis profundo del rendimiento</p>
</div>
<div class="d-flex gap-3">
<select class="form-select" style="width: 200px;">
<option>Últimos 30 días</option>
<option>Últimos 90 días</option>
<option>Este año</option>
</select>
<button class="btn btn-primary">
<i class="bi bi-download me-2"></i>Exportar informe
</button>
</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card-header">
<div class="stat-card-icon green"><i class="bi bi-people"></i></div>
<div class="stat-card-trend up"><i class="bi bi-arrow-up"></i>18%</div>
</div>
<div class="stat-card-value">8,452</div>
<div class="stat-card-label">Visitantes únicos</div>
</div>
<div class="stat-card">
<div class="stat-card-header">
<div class="stat-card-icon blue"><i class="bi bi-clock-history"></i></div>
<div class="stat-card-trend up"><i class="bi bi-arrow-up"></i>12%</div>
</div>
<div class="stat-card-value">3:24</div>
<div class="stat-card-label">Tiempo en sitio (min)</div>
</div>
<div class="stat-card">
<div class="stat-card-header">
<div class="stat-card-icon orange"><i class="bi bi-bounce"></i></div>
<div class="stat-card-trend down"><i class="bi bi-arrow-down"></i>3%</div>
</div>
<div class="stat-card-value">42%</div>
<div class="stat-card-label">Rebote</div>
</div>
<div class="stat-card">
<div class="stat-card-header">
<div class="stat-card-icon red"><i class="bi bi-calendar-check"></i></div>
<div class="stat-card-trend up"><i class="bi bi-arrow-up"></i>25%</div>
</div>
<div class="stat-card-value">156</div>
<div class="stat-card-label">Conversiones</div>
</div>
</div>
<div class="chart-card mt-4">
<div class="chart-card-header">
<h4 class="chart-card-title">Rendimiento por día</h4>
</div>
<div class="chart-container" style="height: 350px;">
<canvas id="dailyPerformanceChart"></canvas>
</div>
</div>
</section>

View File

@@ -1,548 +1,298 @@
<!-- Dashboard Section -->
<div class="section active" id="section-dashboard">
<div class="page-header">
<div>
<h1 class="page-title">Dashboard</h1>
<p class="page-subtitle">Resumen general de la plataforma</p>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary" onclick="admin.exportStats()">
<i class="bi bi-download me-2"></i>Exportar
</button>
<button class="btn btn-primary" onclick="admin.showPropertyModal()">
<i class="bi bi-plus-lg me-2"></i>Nueva Propiedad
</button>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card-header">
<div class="stat-card-icon green">
<i class="bi bi-building"></i>
</div>
<div class="stat-card-trend up" id="propertiesTrend">
<i class="bi bi-arrow-up"></i>
<span>12%</span>
</div>
</div>
<div class="stat-card-value" id="statProperties">0</div>
<div class="stat-card-label">Propiedades Activas</div>
<div class="stat-card-sub">+<span id="propertiesMonth">3</span> este mes</div>
</div>
<div class="stat-card">
<div class="stat-card-header">
<div class="stat-card-icon blue">
<i class="bi bi-people"></i>
</div>
<div class="stat-card-trend up" id="leadsTrend">
<i class="bi bi-arrow-up"></i>
<span>24%</span>
</div>
</div>
<div class="stat-card-value" id="statLeads">0</div>
<div class="stat-card-label">Nuevos Leads</div>
<div class="stat-card-sub"><span id="totalLeads">0</span> totales</div>
</div>
<div class="stat-card">
<div class="stat-card-header">
<div class="stat-card-icon orange">
<i class="bi bi-eye"></i>
</div>
<div class="stat-card-trend" id="viewsTrend">
<i class="bi bi-arrow-up"></i>
<span>8%</span>
</div>
</div>
<div class="stat-card-value" id="statViews">0</div>
<div class="stat-card-label">Vistas Totales</div>
<div class="stat-card-sub"><span id="viewsMonth">0</span> este mes</div>
</div>
<div class="stat-card">
<div class="stat-card-header">
<div class="stat-card-icon red">
<i class="bi bi-currency-euro"></i>
</div>
<div class="stat-card-trend up" id="priceTrend">
<i class="bi bi-arrow-up"></i>
<span>5%</span>
</div>
</div>
<div class="stat-card-value" id="statAvgPrice">€0</div>
<div class="stat-card-label">Precio Promedio</div>
<div class="stat-card-sub"><span id="pricePerM2">0</span>/m²</div>
</div>
</div>
<!-- Charts Row -->
<div class="row mt-4">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Rendimiento</h5>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary active" data-period="week">Semana</button>
<button class="btn btn-outline-primary" data-period="month">Mes</button>
<button class="btn btn-outline-primary" data-period="year">Año</button>
<!-- ============ DASHBOARD SECTION ============ -->
<section class="page-section active" id="section-dashboard">
<div class="page-header">
<div>
<h1 class="page-title" data-i18n="dashboard.title">Dashboard</h1>
<p class="page-subtitle" data-i18n="dashboard.subtitle">Resumen del rendimiento de tu negocio</p>
</div>
<div class="d-flex gap-3">
<input type="text" id="dateRange" class="form-control" style="width: 250px;" placeholder="Seleccionar rango de fechas">
<button class="btn btn-primary">
<i class="bi bi-download me-2"></i>Exportar
</button>
</div>
</div>
<div class="card-body">
<canvas id="performanceChart" height="300"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Estado de Leads</h5>
</div>
<div class="card-body">
<canvas id="leadsStatusChart" height="300"></canvas>
</div>
</div>
</div>
</div>
<!-- Secondary Charts Row -->
<div class="row mt-4">
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Tipos de Propiedades</h5>
</div>
<div class="card-body">
<canvas id="typesChart" height="250"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Fuente de Tráfico</h5>
</div>
<div class="card-body">
<canvas id="trafficChart" height="250"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Propiedades Top</h5>
</div>
<div class="card-body">
<canvas id="topPropertiesChart" height="250"></canvas>
</div>
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card animate-fade-in">
<div class="stat-card-header">
<div class="stat-card-icon green">
<i class="bi bi-eye"></i>
</div>
<div class="stat-card-trend up">
<i class="bi bi-arrow-up"></i>
12.5%
</div>
</div>
<div class="stat-card-value" id="statViews">24,892</div>
<div class="stat-card-label" data-i18n="dashboard.views">Vistas de propiedades</div>
</div>
<!-- Recent Section -->
<div class="row mt-4">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Propiedades Recientes</h5>
<a href="#properties" class="btn btn-sm btn-outline-primary" onclick="admin.navigateTo('properties')">Ver todas</a>
<div class="stat-card animate-fade-in">
<div class="stat-card-header">
<div class="stat-card-icon blue">
<i class="bi bi-cursor-click"></i>
</div>
<div class="stat-card-trend up">
<i class="bi bi-arrow-up"></i>
8.3%
</div>
</div>
<div class="stat-card-value" id="statClicks">3,421</div>
<div class="stat-card-label" data-i18n="dashboard.clicks">Clics en WhatsApp</div>
</div>
<div class="stat-card animate-fade-in">
<div class="stat-card-header">
<div class="stat-card-icon orange">
<i class="bi bi-envelope"></i>
</div>
<div class="stat-card-trend up">
<i class="bi bi-arrow-up"></i>
15.7%
</div>
</div>
<div class="stat-card-value" id="statLeads">156</div>
<div class="stat-card-label" data-i18n="dashboard.leads">Nuevos leads</div>
</div>
<div class="stat-card animate-fade-in">
<div class="stat-card-header">
<div class="stat-card-icon red">
<i class="bi bi-percent"></i>
</div>
<div class="stat-card-trend up">
<i class="bi bi-arrow-up"></i>
2.1%
</div>
</div>
<div class="stat-card-value" id="statConversion">13.7%</div>
<div class="stat-card-label" data-i18n="dashboard.conversion">Tasa de conversión</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Propiedad</th>
<th>Tipo</th>
<th>Ubicación</th>
<th>Precio</th>
<th>Estado</th>
<th>Vistas</th>
</tr>
</thead>
<tbody id="recentPropertiesTable">
<tr>
<td colspan="6" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
</td>
</tr>
</tbody>
<!-- Charts -->
<div class="charts-grid">
<div class="chart-card">
<div class="chart-card-header">
<h4 class="chart-card-title" data-i18n="dashboard.analytics">Rendimiento mensual</h4>
<div class="chart-card-actions">
<button class="chart-period-btn" data-period="week">Semana</button>
<button class="chart-period-btn active" data-period="month">Mes</button>
<button class="chart-period-btn" data-period="year">o</button>
</div>
</div>
<div class="chart-container">
<canvas id="performanceChart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-card-header">
<h4 class="chart-card-title" data-i18n="dashboard.sources">Fuentes de tráfico</h4>
</div>
<div class="chart-container">
<canvas id="trafficChart"></canvas>
</div>
</div>
</div>
<!-- Additional Charts Row -->
<div class="rows-grid">
<div class="chart-card">
<div class="chart-card-header">
<h4 class="chart-card-title" data-i18n="dashboard.propertyTypes">Propiedades por tipo</h4>
</div>
<div class="chart-container" style="height: 220px;">
<canvas id="typesChart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-card-header">
<h4 class="chart-card-title" data-i18n="dashboard.leadsStatus">Estado de leads</h4>
</div>
<div class="chart-container" style="height: 220px;">
<canvas id="leadsChart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-card-header">
<h4 class="chart-card-title" data-i18n="dashboard.topProperties">Top 5 propiedades</h4>
</div>
<div class="chart-container" style="height: 220px;">
<canvas id="topPropertiesChart"></canvas>
</div>
</div>
</div>
<!-- Recent Leads Table -->
<div class="table-card mt-4">
<div class="table-card-header">
<h4 class="table-card-title" data-i18n="dashboard.recentLeads">Leads recientes</h4>
<a href="#" class="table-card-action" data-section="leads">
<i class="bi bi-eye"></i>
Ver todos
</a>
</div>
<div class="table-wrapper">
<table class="table table-hover mb-0" id="leadsTable">
<thead>
<tr>
<th data-i18n="table.client">Cliente</th>
<th data-i18n="table.property">Propiedad</th>
<th data-i18n="table.source">Fuente</th>
<th data-i18n="table.date">Fecha</th>
<th data-i18n="table.status">Estado</th>
<th data-i18n="table.actions">Acciones</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Michael Schmidt
<small>+34 600 123 456</small>
</div>
</div>
</td>
<td>
<div class="table-property">
<img src="https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=100&q=80" class="table-property-img">
<div class="table-property-info">
<h6>Terreno Urbano Adeje</h6>
<span>385.000 €</span>
</div>
</div>
</td>
<td><span class="badge bg-info">WhatsApp</span></td>
<td>15/01/2024 14:32</td>
<td><span class="table-badge pending">Pendiente</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn view" title="Ver"><i class="bi bi-eye"></i></button>
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
<tr>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Anna Petrova
<small>anna@mail.com</small>
</div>
</div>
</td>
<td>
<div class="table-property">
<img src="https://images.unsplash.com/photo-1500382017468-9049fed747ef?w=100&q=80" class="table-property-img">
<div class="table-property-info">
<h6>Terreno Agrícola Güímar</h6>
<span>125.000 €</span>
</div>
</div>
</td>
<td><span class="badge bg-secondary">Formulario</span></td>
<td>15/01/2024 12:18</td>
<td><span class="table-badge new">Nuevo</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn view" title="Ver"><i class="bi bi-eye"></i></button>
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
<tr>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Pierre Dubois
<small>+33 600 789 012</small>
</div>
</div>
</td>
<td>
<div class="table-property">
<img src="https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=100&q=80" class="table-property-img">
<div class="table-property-info">
<h6>Villa Los Cristianos</h6>
<span>595.000 €</span>
</div>
</div>
</td>
<td><span class="badge bg-primary">Directo</span></td>
<td>14/01/2024 18:45</td>
<td><span class="table-badge completed">Contactado</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn view" title="Ver"><i class="bi bi-eye"></i></button>
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
<tr>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Carlos García
<small>carlos.g@mail.com</small>
</div>
</div>
</td>
<td>
<div class="table-property">
<img src="https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=100&q=80" class="table-property-img">
<div class="table-property-info">
<h6>Apartamento Puerto Cruz</h6>
<span>245.000 €</span>
</div>
</div>
</td>
<td><span class="badge bg-success">Referido</span></td>
<td>14/01/2024 10:22</td>
<td><span class="table-badge completed">En proceso</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn view" title="Ver"><i class="bi bi-eye"></i></button>
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Leads Recientes</h5>
<a href="#leads" class="btn btn-sm btn-outline-primary" onclick="admin.navigateTo('leads')">Ver todos</a>
<!-- Quick Actions -->
<div class="quick-actions">
<a href="#" class="quick-action" data-section="properties">
<div class="quick-action-icon"><i class="bi bi-plus-lg"></i></div>
<span data-i18n="dashboard.addProperty">Añadir propiedad</span>
</a>
<a href="#" class="quick-action" data-section="leads">
<div class="quick-action-icon"><i class="bi bi-envelope-plus"></i></div>
<span data-i18n="dashboard.viewLeads">Ver leads</span>
</a>
<a href="#" class="quick-action" data-section="analytics">
<div class="quick-action-icon"><i class="bi bi-bar-chart"></i></div>
<span data-i18n="dashboard.fullReport">Informe completo</span>
</a>
<a href="#" class="quick-action" data-section="settings">
<div class="quick-action-icon"><i class="bi bi-gear"></i></div>
<span data-i18n="dashboard.settings">Configuración</span>
</a>
</div>
<div class="card-body" id="recentLeadsList">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<script>
// Dashboard Charts
let dashboardCharts = {};
async function loadDashboard() {
try {
// Load stats
const statsRes = await API.getAdminStats();
if (statsRes.success) {
updateStatCards(statsRes.data);
}
// Load analytics for charts
const [chartsRes, leadsRes, propertiesRes] = await Promise.all([
fetch('/api/admin/analytics/charts').then(r => r.json()),
API.getLeads({ limit: 5 }),
API.getProperties({ limit: 5 })
]);
if (chartsRes.success) {
updateCharts(chartsRes.data);
}
if (leadsRes.success) {
updateLeadsList(leadsRes.data);
}
if (propertiesRes.success) {
updatePropertiesTable(propertiesRes.data);
}
} catch (e) {
console.error('Failed to load dashboard:', e);
}
}
function updateStatCards(stats) {
// Animate counters
animateCounter('statProperties', stats.properties.active);
animateCounter('statLeads', stats.leads.new);
animateCounter('statViews', stats.analytics.views);
const priceFormatter = new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
});
document.getElementById('statAvgPrice').textContent = priceFormatter.format(stats.averages.price);
document.getElementById('totalLeads').textContent = stats.leads.total;
document.getElementById('pricePerM2').textContent = stats.averages.pricePerM2.toLocaleString();
// Update lead count badge
if (document.getElementById('newLeadsBadge')) {
document.getElementById('newLeadsBadge').textContent = stats.leads.new;
}
}
function animateCounter(id, target) {
const el = document.getElementById(id);
if (!el) return;
const duration = 1000;
const start = 0;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = Math.floor(start + (target - start) * eased);
el.textContent = current.toLocaleString();
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
function updateCharts(data) {
// Performance Chart (Views & Leads)
const performanceCtx = document.getElementById('performanceChart')?.getContext('2d');
if (performanceCtx) {
if (dashboardCharts.performance) dashboardCharts.performance.destroy();
dashboardCharts.performance = new Chart(performanceCtx, {
type: 'line',
data: {
labels: data.months,
datasets: [
{
label: 'Vistas',
data: data.viewsPerMonth,
borderColor: '#1a5f4a',
backgroundColor: 'rgba(26, 95, 74, 0.1)',
tension: 0.4,
fill: true
},
{
label: 'Leads',
data: data.leadsPerMonth,
borderColor: '#d4a853',
backgroundColor: 'rgba(212, 168, 83, 0.1)',
tension: 0.4,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// Leads Status Chart
const leadsCtx = document.getElementById('leadsStatusChart')?.getContext('2d');
if (leadsCtx && data.leadsStatus) {
if (dashboardCharts.leadsStatus) dashboardCharts.leadsStatus.destroy();
const statusColors = {
'new': '#3b82f6',
'contacted': '#f59e0b',
'qualified': '#10b981',
'negotiating': '#8b5cf6',
'closed': '#1a5f4a'
};
const statusLabels = {
'new': 'Nuevo',
'contacted': 'Contactado',
'qualified': 'Calificado',
'negotiating': 'Negociando',
'closed': 'Cerrado'
};
dashboardCharts.leadsStatus = new Chart(leadsCtx, {
type: 'doughnut',
data: {
labels: data.leadsStatus.map(l => statusLabels[l.status] || l.status),
datasets: [{
data: data.leadsStatus.map(l => l.count),
backgroundColor: data.leadsStatus.map(l => statusColors[l.status] || '#6c757d')
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// Types Chart
const typesCtx = document.getElementById('typesChart')?.getContext('2d');
if (typesCtx && data.propertiesByCity) {
if (dashboardCharts.types) dashboardCharts.types.destroy();
dashboardCharts.types = new Chart(typesCtx, {
type: 'bar',
data: {
labels: data.propertiesByCity.slice(0, 5).map(c => c.city),
datasets: [{
label: 'Propiedades',
data: data.propertiesByCity.slice(0, 5).map(c => c.count),
backgroundColor: ['#1a5f4a', '#d4a853', '#e85d04', '#3b82f6', '#10b981']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
}
}
});
}
// Traffic Chart
const trafficCtx = document.getElementById('trafficChart')?.getContext('2d');
if (trafficCtx) {
if (dashboardCharts.traffic) dashboardCharts.traffic.destroy();
const trafficSources = [
{ source: 'Directo', count: 35 },
{ source: 'Búsqueda', count: 30 },
{ source: 'Social', count: 20 },
{ source: 'Referido', count: 10 },
{ source: 'Email', count: 5 }
];
dashboardCharts.traffic = new Chart(trafficCtx, {
type: 'doughnut',
data: {
labels: trafficSources.map(s => s.source),
datasets: [{
data: trafficSources.map(s => s.count),
backgroundColor: ['#1a5f4a', '#d4a853', '#e85d04', '#3b82f6', '#6c757d']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// Top Properties Chart
const topCtx = document.getElementById('topPropertiesChart')?.getContext('2d');
if (topCtx && data.topProperties) {
if (dashboardCharts.top) dashboardCharts.top.destroy();
dashboardCharts.top = new Chart(topCtx, {
type: 'bar',
data: {
labels: data.topProperties.map(p => p.reference),
datasets: [{
label: 'Vistas',
data: data.topProperties.map(p => p.views_count || 0),
backgroundColor: '#d4a853'
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
}
}
});
}
}
function updatePropertiesTable(properties) {
const tbody = document.getElementById('recentPropertiesTable');
if (!tbody) return;
const typeLabels = {
'urban': 'Urbano',
'agricultural': 'Agrícola',
'house': 'Casa',
'apartment': 'Apartamento'
};
const statusColors = {
'active': 'success',
'reserved': 'warning',
'sold': 'secondary'
};
tbody.innerHTML = properties.map(p => {
const images = p.images ? JSON.parse(p.images) : [];
const image = images[0] || 'https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=100&q=80';
return `
<tr>
<td>
<div class="d-flex align-items-center">
<img src="${image}" class="rounded me-2" style="width:40px;height:40px;object-fit:cover" alt="">
<div>
<div class="fw-medium">${p.title_es?.substring(0, 30) || 'Sin título'}...</div>
<small class="text-muted">${p.reference}</small>
</div>
</div>
</td>
<td><span class="badge bg-light text-dark">${typeLabels[p.type] || p.type}</span></td>
<td>${p.city}</td>
<td>€${p.price?.toLocaleString()}</td>
<td><span class="badge bg-${statusColors[p.status] || 'secondary'}">${p.status}</span></td>
<td>
<div class="d-flex gap-2 align-items-center">
<i class="bi bi-eye text-muted"></i>
<span>${p.views_count || 0}</span>
</div>
</td>
</tr>
`;
}).join('');
}
function updateLeadsList(leads) {
const container = document.getElementById('recentLeadsList');
if (!container) return;
const statusColors = {
'new': 'danger',
'contacted': 'warning',
'qualified': 'info',
'negotiating': 'primary',
'closed': 'success'
};
const statusLabels = {
'new': 'Nuevo',
'contacted': 'Contactado',
'qualified': 'Calificado',
'negotiating': 'Negociando',
'closed': 'Cerrado'
};
container.innerHTML = leads.map(l => `
<div class="d-flex align-items-center gap-3 mb-3 pb-3 border-bottom">
<div class="avatar bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width:40px;height:40px">
${l.name?.charAt(0) || '?'}
</div>
<div class="flex-grow-1">
<div class="fw-medium">${l.name}</div>
<small class="text-muted">${l.email}</small>
</div>
<span class="badge bg-${statusColors[l.status] || 'secondary'}">${statusLabels[l.status] || l.status}</span>
</div>
`).join('');
}
// Initialize dashboard when loaded
document.addEventListener('DOMContentLoaded', () => {
// Check if Chart.js is loaded
if (typeof Chart === 'undefined') {
console.warn('Chart.js not loaded, charts will not render');
}
});

View File

@@ -1,75 +1,91 @@
<!-- FAQ Section -->
<div class="section" id="section-faq">
<div class="page-header">
<div>
<h1 class="page-title">Preguntas Frecuentes</h1>
<p class="page-subtitle">Gestión de FAQ</p>
</div>
<button class="btn btn-primary" onclick="showFaqModal()">
<i class="bi bi-plus-lg me-2"></i>Nueva Pregunta
</button>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Pregunta (ES)</th>
<th>Categoría</th>
<th>Orden</th>
<th>Activo</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="faqTable"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
async function loadFAQ() {
try {
const res = await API.getFAQ();
const faqs = res.data || [];
document.getElementById('faqTable').innerHTML = faqs.map(f => `
<tr>
<td>${f.question.substring(0, 60)}...</td>
<td><span class="badge bg-secondary">${f.category}</span></td>
<td>${f.order_num}</td>
<td><span class="badge bg-${f.is_active ? 'success' : 'danger'}">${f.is_active ? 'Sí' : 'No'}</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="editFAQ('${f.id}')"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteFAQ('${f.id}')"><i class="bi bi-trash"></i></button>
<!-- ============ FAQ SECTION ============ -->
<section class="page-section" id="section-faq">
<div class="page-header">
<div>
<h1 class="page-title">FAQ</h1>
<p class="page-subtitle">Gestiona las preguntas frecuentes</p>
</div>
</td>
</tr>
`).join('');
} catch (e) {
console.error('Failed to load FAQ:', e);
}
}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#faqModal">
<i class="bi bi-plus-lg me-2"></i>Añadir pregunta
</button>
</div>
function showFaqModal() {
console.log('Show FAQ modal');
}
<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>
</div>
</div>
</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>
</div>
</div>
</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>
</div>
</div>
</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>
</div>
</div>
</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>
</div>
</div>
</div>
</div>
</section>
function editFAQ(id) {
console.log('Edit FAQ:', id);
}
async function deleteFAQ(id) {
if (!confirm('¿Eliminar esta pregunta?')) return;
try {
await API.deleteFAQ(id);
await loadFAQ();
} catch (e) {
alert('Error al eliminar');
}
}
</script>

View File

@@ -1,185 +1,280 @@
<!-- Leads Section -->
<div class="section" id="section-leads">
<div class="page-header">
<div>
<h1 class="page-title">Leads</h1>
<p class="page-subtitle">Gestión de contactos y solicitudes</p>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary" onclick="exportLeads()">
<i class="bi bi-download me-2"></i>Exportar
</button>
</div>
</div>
<!-- ============ LEADS SECTION ============ -->
<section class="page-section" id="section-leads">
<div class="page-header">
<div>
<h1 class="page-title" data-i18n="leads.title">Leads</h1>
<p class="page-subtitle" data-i18n="leads.subtitle">Gestiona las solicitudes de tus clientes</p>
</div>
<div class="d-flex gap-3">
<button class="btn btn-outline-primary">
<i class="bi bi-download me-2"></i>Exportar
</button>
<button class="btn btn-primary">
<i class="bi bi-plus-lg me-2"></i>Añadir lead manualmente
</button>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label">Buscar</label>
<input type="text" class="form-control" id="searchLead" placeholder="Nombre o email...">
</div>
<div class="col-md-2">
<label class="form-label">Estado</label>
<select class="form-select" id="leadStatus">
<option value="">Todos</option>
<option value="new">Nuevo</option>
<option value="contacted">Contactado</option>
<option value="qualified">Calificado</option>
<option value="closed">Cerrado</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Propiedad</label>
<select class="form-select" id="leadProperty">
<option value="">Todas</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Desde</label>
<input type="date" class="form-control" id="leadFromDate">
</div>
<div class="col-md-3">
<button class="btn btn-primary w-100" onclick="loadLeads()">
<i class="bi bi-funnel me-2"></i>Filtrar
</button>
</div>
</div>
</div>
</div>
<!-- Stats -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body text-center">
<h3 class="text-primary" id="leadsNew">0</h3>
<small class="text-muted">Nuevos</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body text-center">
<h3 class="text-info" id="leadsContacted">0</h3>
<small class="text-muted">Contactados</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body text-center">
<h3 class="text-warning" id="leadsQualified">0</h3>
<small class="text-muted">Calificados</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body text-center">
<h3 class="text-success" id="leadsClosed">0</h3>
<small class="text-muted">Cerrados</small>
</div>
</div>
</div>
</div>
<!-- Table -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="leadsTable">
<thead>
<tr>
<th>Nombre</th>
<th>Email</th>
<th>Teléfono</th>
<th>Propiedad</th>
<th>Mensaje</th>
<th>Estado</th>
<th>Fecha</th>
<th>Acciones</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-2">
<select class="form-select" id="leadStatus">
<option value="">Todos los estados</option>
<option value="new">Nuevo</option>
<option value="pending">Pendiente</option>
<option value="contacted">Contactado</option>
<option value="qualified">Cualificado</option>
<option value="converted">Convertido</option>
<option value="lost">Perdido</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" id="leadSource">
<option value="">Todas las fuentes</option>
<option value="whatsapp">WhatsApp</option>
<option value="form">Formulario web</option>
<option value="phone">Llamada</option>
<option value="direct">Tráfico directo</option>
<option value="referral">Referido</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" id="leadProperty">
<option value="">Todas las propiedades</option>
<option>Terreno Urbano Adeje</option>
<option>Terreno Agrícola Güímar</option>
<option>Villa Los Cristianos</option>
</select>
</div>
<div class="col-md-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchLead" placeholder="Buscar lead...">
</div>
</div>
<script>
async function loadLeads() {
try {
const res = await API.getLeads();
const leads = res.data || [];
// Update stats
document.getElementById('leadsNew').textContent = leads.filter(l => l.status === 'new').length;
document.getElementById('leadsContacted').textContent = leads.filter(l => l.status === 'contacted').length;
document.getElementById('leadsQualified').textContent = leads.filter(l => l.status === 'qualified').length;
document.getElementById('leadsClosed').textContent = leads.filter(l => l.status === 'closed').length;
renderLeads(leads);
} catch (e) {
console.error('Failed to load leads:', e);
}
}
function renderLeads(leads) {
const tbody = document.querySelector('#leadsTable tbody');
if (leads.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-4">No hay leads</td></tr>';
return;
}
tbody.innerHTML = leads.map(l => `
<tr>
<td><strong>${l.name}</strong></td>
<td><a href="mailto:${l.email}">${l.email}</a></td>
<td>${l.phone || '-'}</td>
<td>${l.property_id || 'General'}</td>
<td>${l.message ? l.message.substring(0, 50) + '...' : '-'}</td>
<td><span class="badge bg-${l.status === 'new' ? 'danger' : 'secondary'}">${l.status}</span></td>
<td>${new Date(l.created_at).toLocaleDateString()}</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="updateLeadStatus('${l.id}', 'contacted')">
<i class="bi bi-telephone"></i>
</button>
<button class="btn btn-sm btn-outline-success" onclick="updateLeadStatus('${l.id}', 'qualified')">
<i class="bi bi-check-circle"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteLead('${l.id}')">
<i class="bi bi-trash"></i>
</button>
<div class="col-md-3">
<input type="text" id="leadDateRange" class="form-control" placeholder="Rango de fechas">
</div>
</div>
</div>
</div>
</td>
</tr>
`).join('');
}
async function updateLeadStatus(id, status) {
try {
await API.updateLead(id, { status });
await loadLeads();
} catch (e) {
alert('Error al actualizar lead');
}
}
<!-- Leads Table -->
<div class="table-card">
<div class="table-card-header">
<h4 class="table-card-title">Lista de leads</h4>
<div class="d-flex gap-2">
<span class="badge bg-success">12 Nuevos</span>
<span class="badge bg-warning">8 Pendientes</span>
<span class="badge bg-info">5 En proceso</span>
</div>
</div>
<div class="table-wrapper">
<table class="table table-hover mb-0" id="fullLeadsTable">
<thead>
<tr>
<th><input type="checkbox" class="form-check-input"></th>
<th data-i18n="table.client">Cliente</th>
<th data-i18n="table.contact">Contacto</th>
<th data-i18n="table.property">Propiedad</th>
<th data-i18n="table.budget">Presupuesto</th>
<th data-i18n="table.source">Fuente</th>
<th data-i18n="table.date">Fecha</th>
<th data-i18n="table.status">Estado</th>
<th data-i18n="table.actions">Acciones</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" class="form-check-input"></td>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Michael Schmidt
<small>🇩🇪 Alemania</small>
</div>
</div>
</td>
<td>
<div>+34 600 123 456<br>
<small class="text-muted">michael@mail.de</small></div>
</td>
<td>
<div class="table-property">
<img src="https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=100&q=80" class="table-property-img">
<div class="table-property-info">
<h6>Terreno Urbano Adeje</h6>
<span>385.000 €</span>
</div>
</div>
</td>
<td>400.000 €</td>
<td><span class="badge bg-success">WhatsApp</span></td>
<td>15/01/2024 14:32</td>
<td><span class="table-badge pending">Pendiente</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn view" title="Ver"><i class="bi bi-eye"></i></button>
<button class="table-action-btn" title="WhatsApp" style="background: #25D366; color: white;"><i class="bi bi-whatsapp"></i></button>
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
<tr>
<td><input type="checkbox" class="form-check-input"></td>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Anna Petrova
<small>🇷🇺 Rusia</small>
</div>
</div>
</td>
<td>
<div>anna@mail.com<br>
<small class="text-muted">Sin teléfono</small></div>
</td>
<td>
<div class="table-property">
<img src="https://images.unsplash.com/photo-1500382017468-9049fed747ef?w=100&q=80" class="table-property-img">
<div class="table-property-info">
<h6>Terreno Agrícola Güímar</h6>
<span>125.000 €</span>
</div>
</div>
</td>
<td>150.000 €</td>
<td><span class="badge bg-secondary">Formulario</span></td>
<td>15/01/2024 12:18</td>
<td><span class="table-badge new">Nuevo</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn view" title="Ver"><i class="bi bi-eye"></i></button>
<button class="table-action-btn" title="Enviar email"><i class="bi bi-envelope"></i></button>
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
<tr>
<td><input type="checkbox" class="form-check-input"></td>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Pierre Dubois
<small>🇫🇷 Francia</small>
</div>
</div>
</td>
<td>
<div>+33 600 789 012<br>
<small class="text-muted">pierre.d@mail.fr</small></div>
</td>
<td>
<div class="table-property">
<img src="https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=100&q=80" class="table-property-img">
<div class="table-property-info">
<h6>Villa Los Cristianos</h6>
<span>595.000 €</span>
</div>
</div>
</td>
<td>650.000 €</td>
<td><span class="badge bg-primary">Directo</span></td>
<td>14/01/2024 18:45</td>
<td><span class="table-badge completed">Contactado</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn view" title="Ver"><i class="bi bi-eye"></i></button>
<button class="table-action-btn" title="WhatsApp" style="background: #25D366; color: white;"><i class="bi bi-whatsapp"></i></button>
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
<tr>
<td><input type="checkbox" class="form-check-input"></td>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Carlos García
<small>🇪🇸 España</small>
</div>
</div>
</td>
<td>
<div>+34 600 456 789<br>
<small class="text-muted">carlos.g@mail.com</small></div>
</td>
<td>
<div class="table-property">
<img src="https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=100&q=80" class="table-property-img">
<div class="table-property-info">
<h6>Apartamento Puerto Cruz</h6>
<span>245.000 €</span>
</div>
</div>
</td>
<td>280.000 €</td>
<td><span class="badge bg-info">Referido</span></td>
<td>14/01/2024 10:22</td>
<td><span class="table-badge" style="background: rgba(59,130,246,0.1); color: var(--info);">En proceso</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn view" title="Ver"><i class="bi bi-eye"></i></button>
<button class="table-action-btn" title="WhatsApp" style="background: #25D366; color: white;"><i class="bi bi-whatsapp"></i></button>
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
<tr>
<td><input type="checkbox" class="form-check-input"></td>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Emma Johnson
<small>🇬🇧 Reino Unido</small>
</div>
</div>
</td>
<td>
<div>+44 7700 123 456<br>
<small class="text-muted">emma.j@mail.co.uk</small></div>
</td>
<td>
<div class="table-property">
<img src="https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=100&q=80" class="table-property-img">
<div class="table-property-info">
<h6>Chalet La Caleta</h6>
<span>750.000 €</span>
</div>
</div>
</td>
<td>800.000 €</td>
<td><span class="badge bg-warning">Instagram</span></td>
<td>13/01/2024 16:30</td>
<td><span class="table-badge completed">Cualificado</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn view" title="Ver"><i class="bi bi-eye"></i></button>
<button class="table-action-btn" title="WhatsApp" style="background: #25D366; color: white;"><i class="bi bi-whatsapp"></i></button>
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
async function deleteLead(id) {
if (!confirm('¿Eliminar este lead?')) return;
try {
await API.deleteLead(id);
await loadLeads();
} catch (e) {
alert('Error al eliminar lead');
}
}
function exportLeads() {
console.log('Export leads');
}
</script>

View File

@@ -1,194 +1,242 @@
<!-- Properties Section -->
<div class="section" id="section-properties">
<div class="page-header">
<div>
<h1 class="page-title">Propiedades</h1>
<p class="page-subtitle">Gestión de terrenos y propiedades</p>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary" onclick="exportProperties()">
<i class="bi bi-download me-2"></i>Exportar
</button>
<button class="btn btn-primary" onclick="showPropertyModal()">
<i class="bi bi-plus-lg me-2"></i>Nueva Propiedad
</button>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Buscar</label>
<input type="text" class="form-control" id="searchProperty" placeholder="Buscar por título...">
</div>
<div class="col-md-2">
<label class="form-label">Tipo</label>
<select class="form-select" id="filterType">
<option value="">Todos</option>
<option value="urban">Urbano</option>
<option value="agricultural">Agrícola</option>
<option value="house">Casa</option>
<option value="apartment">Apartamento</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Estado</label>
<select class="form-select" id="filterStatus">
<option value="">Todos</option>
<option value="active">Activo</option>
<option value="reserved">Reservado</option>
<option value="sold">Vendido</option>
<option value="inactive">Inactivo</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Ciudad</label>
<select class="form-select" id="filterCity">
<option value="">Todas</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">&nbsp;</label>
<button class="btn btn-primary w-100" onclick="loadProperties()">
<i class="bi bi-funnel me-2"></i>Filtrar
<!-- ============ PROPERTIES SECTION ============ -->
<section class="page-section" id="section-properties">
<div class="page-header">
<div>
<h1 class="page-title" data-i18n="properties.title">Propiedades</h1>
<p class="page-subtitle" data-i18n="properties.subtitle">Gestiona tu catálogo de propiedades</p>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#propertyModal">
<i class="bi bi-plus-lg me-2"></i>Añadir propiedad
</button>
</div>
</div>
</div>
</div>
<!-- Results -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="propertiesTable">
<thead>
<tr>
<th>Referencia</th>
<th>Título</th>
<th>Tipo</th>
<th>Ubicación</th>
<th>Precio</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="7" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<select class="form-select" id="filterType">
<option value="">Todos los tipos</option>
<option value="agricultural">Terrenos agrícolas</option>
<option value="urban">Terrenos urbanos</option>
<option value="house">Casas</option>
<option value="apartment">Apartamentos</option>
<option value="ruins">Ruinas</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="filterStatus">
<option value="">Todos los estados</option>
<option value="active">Activo</option>
<option value="inactive">Inactivo</option>
<option value="sold">Vendido</option>
</select>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchProperty" placeholder="Buscar propiedad...">
</div>
</div>
<script>
let allProperties = [];
async function loadProperties() {
try {
const filters = {
type: document.getElementById('filterType').value,
status: document.getElementById('filterStatus').value,
city: document.getElementById('filterCity').value,
search: document.getElementById('searchProperty').value
};
const res = await API.getProperties({ limit: 100, ...filters });
allProperties = res.data || [];
// Populate cities filter
if (!document.getElementById('filterCity').options.length) {
const cities = [...new Set(allProperties.map(p => p.city))];
cities.forEach(city => {
const opt = document.createElement('option');
opt.value = city;
opt.textContent = city;
document.getElementById('filterCity').appendChild(opt);
});
}
renderProperties(allProperties);
} catch (e) {
console.error('Failed to load properties:', e);
}
}
function renderProperties(properties) {
const tbody = document.querySelector('#propertiesTable tbody');
if (properties.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4">No se encontraron propiedades</td></tr>';
return;
}
tbody.innerHTML = properties.map(p => `
<tr>
<td><code>${p.reference}</code></td>
<td>
<div class="d-flex align-items-center">
<img src="${JSON.parse(p.images)[0]}" class="rounded me-2" style="width:50px;height:50px;object-fit:cover">
<div>
<div class="fw-medium">${p.title_es.substring(0, 30)}...</div>
<small class="text-muted">${p.area.toLocaleString()} m²</small>
<div class="col-md-2">
<button class="btn btn-outline-primary w-100">
<i class="bi bi-funnel me-2"></i>Filtrar
</button>
</div>
</div>
</div>
</div>
</td>
<td><span class="badge bg-secondary">${p.type}</span></td>
<td>${p.city}</td>
<td><strong>€${p.price.toLocaleString()}</strong></td>
<td><span class="badge bg-${p.status === 'active' ? 'success' : 'warning'}">${p.status}</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="editProperty('${p.id}')" title="Editar">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteProperty('${p.id}')" title="Eliminar">
<i class="bi bi-trash"></i>
</button>
<!-- Properties Grid -->
<div class="properties-grid" id="propertiesGrid">
<!-- Property Card 1 -->
<div class="property-admin-card">
<div class="property-admin-card-image">
<img src="https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=800&q=80" alt="Terreno urbano">
<div class="property-admin-card-badges">
<span class="property-admin-card-badge active">Activo</span>
<span class="property-admin-card-badge urban">Urbano</span>
</div>
<div class="property-admin-card-actions">
<a href="#" class="property-admin-card-action" title="Ver"><i class="bi bi-eye"></i></a>
<a href="#" class="property-admin-card-action" title="Editar"><i class="bi bi-pencil"></i></a>
<a href="#" class="property-admin-card-action delete" title="Eliminar"><i class="bi bi-trash"></i></a>
</div>
</div>
<div class="property-admin-card-content">
<h5 class="property-admin-card-title">Terreno Urbano en Adeje</h5>
<p class="property-admin-card-location"><i class="bi bi-geo-alt"></i>Adeje, Tenerife Sur</p>
<div class="property-admin-card-stats">
<span class="property-admin-card-stat"><i class="bi bi-eye"></i>1,245</span>
<span class="property-admin-card-stat"><i class="bi bi-cursor-click"></i>89</span>
<span class="property-admin-card-stat"><i class="bi bi-envelope"></i>12</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="property-admin-card-price">385.000 €</span>
<small class="text-muted">2.500 m²</small>
</div>
</div>
</div>
<!-- Property Card 2 -->
<div class="property-admin-card">
<div class="property-admin-card-image">
<img src="https://images.unsplash.com/photo-1500382017468-9049fed747ef?w=800&q=80" alt="Terreno agrícola">
<div class="property-admin-card-badges">
<span class="property-admin-card-badge active">Activo</span>
<span class="property-admin-card-badge agricultural">Agrícola</span>
</div>
<div class="property-admin-card-actions">
<a href="#" class="property-admin-card-action" title="Ver"><i class="bi bi-eye"></i></a>
<a href="#" class="property-admin-card-action" title="Editar"><i class="bi bi-pencil"></i></a>
<a href="#" class="property-admin-card-action delete" title="Eliminar"><i class="bi bi-trash"></i></a>
</div>
</div>
<div class="property-admin-card-content">
<h5 class="property-admin-card-title">Terreno Agrícola en Güímar</h5>
<p class="property-admin-card-location"><i class="bi bi-geo-alt"></i>Güímar, Tenerife Sur</p>
<div class="property-admin-card-stats">
<span class="property-admin-card-stat"><i class="bi bi-eye"></i>892</span>
<span class="property-admin-card-stat"><i class="bi bi-cursor-click"></i>56</span>
<span class="property-admin-card-stat"><i class="bi bi-envelope"></i>8</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="property-admin-card-price">125.000 €</span>
<small class="text-muted">8.500 m²</small>
</div>
</div>
</div>
<!-- Property Card 3 -->
<div class="property-admin-card">
<div class="property-admin-card-image">
<img src="https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80" alt="Villa">
<div class="property-admin-card-badges">
<span class="property-admin-card-badge active">Activo</span>
<span class="property-admin-card-badge house">Casa</span>
</div>
<div class="property-admin-card-actions">
<a href="#" class="property-admin-card-action" title="Ver"><i class="bi bi-eye"></i></a>
<a href="#" class="property-admin-card-action" title="Editar"><i class="bi bi-pencil"></i></a>
<a href="#" class="property-admin-card-action delete" title="Eliminar"><i class="bi bi-trash"></i></a>
</div>
</div>
<div class="property-admin-card-content">
<h5 class="property-admin-card-title">Villa con Vistas al Mar</h5>
<p class="property-admin-card-location"><i class="bi bi-geo-alt"></i>Los Cristianos, Arona</p>
<div class="property-admin-card-stats">
<span class="property-admin-card-stat"><i class="bi bi-eye"></i>2,156</span>
<span class="property-admin-card-stat"><i class="bi bi-cursor-click"></i>167</span>
<span class="property-admin-card-stat"><i class="bi bi-envelope"></i>24</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="property-admin-card-price">595.000 €</span>
<small class="text-muted">350 m²</small>
</div>
</div>
</div>
<!-- Property Card 4 -->
<div class="property-admin-card">
<div class="property-admin-card-image">
<img src="https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&q=80" alt="Apartamento">
<div class="property-admin-card-badges">
<span class="property-admin-card-badge active">Activo</span>
<span class="property-admin-card-badge apartment">Apartamento</span>
</div>
<div class="property-admin-card-actions">
<a href="#" class="property-admin-card-action" title="Ver"><i class="bi bi-eye"></i></a>
<a href="#" class="property-admin-card-action" title="Editar"><i class="bi bi-pencil"></i></a>
<a href="#" class="property-admin-card-action delete" title="Eliminar"><i class="bi bi-trash"></i></a>
</div>
</div>
<div class="property-admin-card-content">
<h5 class="property-admin-card-title">Apartamento Puerto de la Cruz</h5>
<p class="property-admin-card-location"><i class="bi bi-geo-alt"></i>Puerto de la Cruz</p>
<div class="property-admin-card-stats">
<span class="property-admin-card-stat"><i class="bi bi-eye"></i>1,089</span>
<span class="property-admin-card-stat"><i class="bi bi-cursor-click"></i>78</span>
<span class="property-admin-card-stat"><i class="bi bi-envelope"></i>15</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="property-admin-card-price">245.000 €</span>
<small class="text-muted">85 m²</small>
</div>
</div>
</div>
<!-- Property Card 5 -->
<div class="property-admin-card">
<div class="property-admin-card-image">
<img src="https://images.unsplash.com/photo-1518780664697-55e3ad937233?w=800&q=80" alt="Ruinas">
<div class="property-admin-card-badges">
<span class="property-admin-card-badge active">Activo</span>
<span class="property-admin-card-badge ruins">Ruinas</span>
</div>
<div class="property-admin-card-actions">
<a href="#" class="property-admin-card-action" title="Ver"><i class="bi bi-eye"></i></a>
<a href="#" class="property-admin-card-action" title="Editar"><i class="bi bi-pencil"></i></a>
<a href="#" class="property-admin-card-action delete" title="Eliminar"><i class="bi bi-trash"></i></a>
</div>
</div>
<div class="property-admin-card-content">
<h5 class="property-admin-card-title">Casa Ruina San Miguel</h5>
<p class="property-admin-card-location"><i class="bi bi-geo-alt"></i>San Miguel de Abona</p>
<div class="property-admin-card-stats">
<span class="property-admin-card-stat"><i class="bi bi-eye"></i>567</span>
<span class="property-admin-card-stat"><i class="bi bi-cursor-click"></i>34</span>
<span class="property-admin-card-stat"><i class="bi bi-envelope"></i>6</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="property-admin-card-price">175.000 €</span>
<small class="text-muted">4.200 m²</small>
</div>
</div>
</div>
<!-- Property Card 6 -->
<div class="property-admin-card">
<div class="property-admin-card-image">
<img src="https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800&q=80" alt="Parcela">
<div class="property-admin-card-badges">
<span class="property-admin-card-badge active">Activo</span>
<span class="property-admin-card-badge urban">Urbano</span>
</div>
<div class="property-admin-card-actions">
<a href="#" class="property-admin-card-action" title="Ver"><i class="bi bi-eye"></i></a>
<a href="#" class="property-admin-card-action" title="Editar"><i class="bi bi-pencil"></i></a>
<a href="#" class="property-admin-card-action delete" title="Eliminar"><i class="bi bi-trash"></i></a>
</div>
</div>
<div class="property-admin-card-content">
<h5 class="property-admin-card-title">Parcela Urbana Granadilla</h5>
<p class="property-admin-card-location"><i class="bi bi-geo-alt"></i>Granadilla de Abona</p>
<div class="property-admin-card-stats">
<span class="property-admin-card-stat"><i class="bi bi-eye"></i>734</span>
<span class="property-admin-card-stat"><i class="bi bi-cursor-click"></i>45</span>
<span class="property-admin-card-stat"><i class="bi bi-envelope"></i>9</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="property-admin-card-price">210.000 €</span>
<small class="text-muted">1.800 m²</small>
</div>
</div>
</div>
</div>
</td>
</tr>
`).join('');
}
async function deleteProperty(id) {
if (!confirm('¿Está seguro de eliminar esta propiedad?')) return;
try {
await API.deleteProperty(id);
await loadProperties();
} catch (e) {
alert('Error al eliminar la propiedad');
}
}
<!-- Pagination -->
<nav class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#"><i class="bi bi-chevron-left"></i></a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#"><i class="bi bi-chevron-right"></i></a>
</li>
</ul>
</nav>
</section>
function editProperty(id) {
// TODO: Open edit modal
console.log('Edit property:', id);
}
function showPropertyModal() {
// TODO: Open create modal
console.log('Show property modal');
}
function exportProperties() {
const csv = allProperties.map(p =>
`${p.reference},${p.title_es},${p.type},${p.city},${p.price},${p.status}`
).join('\n');
const blob = new Blob(['Referencia,Título,Tipo,Ciudad,Precio,Estado\n' + csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'propiedades.csv';
a.click();
}
</script>

View File

@@ -1,75 +1,61 @@
<!-- Services Section -->
<div class="section" id="section-services">
<div class="page-header">
<div>
<h1 class="page-title">Servicios</h1>
<p class="page-subtitle">Gestión de servicios ofrecidos</p>
</div>
<button class="btn btn-primary" onclick="showServiceModal()">
<i class="bi bi-plus-lg me-2"></i>Nuevo Servicio
</button>
</div>
<!-- ============ SERVICES SECTION ============ -->
<section class="page-section" id="section-services">
<div class="page-header">
<div>
<h1 class="page-title">Servicios</h1>
<p class="page-subtitle">Gestiona los servicios que ofreces</p>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#serviceModal">
<i class="bi bi-plus-lg me-2"></i>Añadir servicio
</button>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Icono</th>
<th>Título (ES)</th>
<th>Descripción</th>
<th>Activo</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="servicesTable"></tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<div class="service-icon mx-auto mb-3" style="width: 80px; height: 80px; background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); border-radius: 20px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-file-text text-white" style="font-size: 2rem;"></i>
</div>
<h5>Asesoría Legal</h5>
<p class="text-muted small">Verificación completa de la documentación</p>
<div class="d-flex justify-content-center gap-2 mt-3">
<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>
<script>
async function loadServices() {
try {
const res = await API.getServices();
const services = res.data || [];
document.getElementById('servicesTable').innerHTML = services.map(s => `
<tr>
<td><i class="${s.icon} fs-4"></i></td>
<td><strong>${s.title}</strong></td>
<td>${s.description.substring(0, 50)}...</td>
<td><span class="badge bg-${s.is_active ? 'success' : 'danger'}">${s.is_active ? 'Sí' : 'No'}</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="editService('${s.id}')"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteService('${s.id}')"><i class="bi bi-trash"></i></button>
</div>
</div>
</td>
</tr>
`).join('');
} catch (e) {
console.error('Failed to load services:', e);
}
}
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<div class="service-icon mx-auto mb-3" style="width: 80px; height: 80px; background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); border-radius: 20px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-key text-white" style="font-size: 2rem;"></i>
</div>
<h5>Financiación Hipotecaria</h5>
<p class="text-muted small">Apoyo en obtención de préstamos hipotecarios</p>
<div class="d-flex justify-content-center gap-2 mt-3">
<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>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<div class="service-icon mx-auto mb-3" style="width: 80px; height: 80px; background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); border-radius: 20px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-tools text-white" style="font-size: 2rem;"></i>
</div>
<h5>Reformas y Construcción</h5>
<p class="text-muted small">Red de arquitectos y constructores locales</p>
<div class="d-flex justify-content-center gap-2 mt-3">
<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>
</div>
</div>
</div>
</section>
function showServiceModal() {
console.log('Show service modal');
}
function editService(id) {
console.log('Edit service:', id);
}
async function deleteService(id) {
if (!confirm('¿Eliminar este servicio?')) return;
try {
await API.deleteService(id);
await loadServices();
} catch (e) {
alert('Error al eliminar');
}
}
</script>

View File

@@ -1,142 +1,93 @@
<!-- Settings Section -->
<div class="section" id="section-settings">
<div class="page-header">
<h1 class="page-title">Configuración</h1>
<p class="page-subtitle">Ajustes del sitio</p>
</div>
<div class="row">
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Información General</h5>
</div>
<div class="card-body">
<form id="settingsForm">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Nombre del Sitio</label>
<input type="text" class="form-control" name="site_name" id="setting_site_name">
</div>
<div class="col-md-6">
<label class="form-label">Email de Contacto</label>
<input type="email" class="form-control" name="email" id="setting_email">
</div>
<div class="col-md-6">
<label class="form-label">Teléfono</label>
<input type="text" class="form-control" name="phone" id="setting_phone">
</div>
<div class="col-md-6">
<label class="form-label">WhatsApp</label>
<input type="text" class="form-control" name="whatsapp" id="setting_whatsapp">
</div>
<div class="col-12">
<label class="form-label">Dirección</label>
<textarea class="form-control" name="address" id="setting_address" rows="2"></textarea>
</div>
</div>
</form>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Redes Sociales</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Facebook</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-facebook"></i></span>
<input type="url" class="form-control" name="social_facebook" id="setting_facebook">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Instagram</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-instagram"></i></span>
<input type="url" class="form-control" name="social_instagram" id="setting_instagram">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Twitter</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-twitter"></i></span>
<input type="url" class="form-control" name="social_twitter" id="setting_twitter">
</div>
</div>
<div class="col-md-6">
<label class="form-label">LinkedIn</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-linkedin"></i></span>
<input type="url" class="form-control" name="social_linkedin" id="setting_linkedin">
</div>
</div>
<!-- ============ SETTINGS SECTION ============ -->
<section class="page-section" id="section-settings">
<div class="page-header">
<div>
<h1 class="page-title">Configuración</h1>
<p class="page-subtitle">Ajustes generales del sistema</p>
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" form="settingsForm" class="btn btn-primary">
<i class="bi bi-check-lg me-2"></i>Guardar Cambios
</button>
<button type="button" class="btn btn-outline-secondary" onclick="loadSettings()">
<i class="bi bi-arrow-clockwise me-2"></i>Restablecer
</button>
</div>
<div class="row">
<div class="col-md-3">
<div class="list-group">
<a href="#settings-general" class="list-group-item list-group-item-action active" data-bs-toggle="list">
<i class="bi bi-gear me-2"></i>General
</a>
<a href="#settings-contact" class="list-group-item list-group-item-action" data-bs-toggle="list">
<i class="bi bi-telephone me-2"></i>Contacto
</a>
<a href="#settings-notifications" class="list-group-item list-group-item-action" data-bs-toggle="list">
<i class="bi bi-bell me-2"></i>Notificaciones
</a>
<a href="#settings-integrations" class="list-group-item list-group-item-action" data-bs-toggle="list">
<i class="bi bi-plug me-2"></i>Integraciones
</a>
<a href="#settings-seo" class="list-group-item list-group-item-action" data-bs-toggle="list">
<i class="bi bi-search me-2"></i>SEO
</a>
</div>
</div>
<div class="col-md-9">
<div class="tab-content">
<div class="tab-pane fade show active" id="settings-general">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Configuración General</h5>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Mapa</h5>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Nombre de la empresa</label>
<input type="text" class="form-control" value="TenerifeProp">
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Centro del Mapa (Lat)</label>
<input type="number" step="0.0001" class="form-control" name="map_lat" id="setting_map_lat">
</div>
<div class="mb-3">
<label class="form-label">Centro del Mapa (Lng)</label>
<input type="number" step="0.0001" class="form-control" name="map_lng" id="setting_map_lng">
</div>
<div class="mb-3">
<label class="form-label">Zoom</label>
<input type="number" class="form-control" name="map_zoom" id="setting_map_zoom">
</div>
<div class="col-md-6">
<label class="form-label">Idioma principal</label>
<select class="form-select">
<option value="es" selected>Español</option>
<option value="ru">Русский</option>
<option value="en">English</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Descripción</label>
<textarea class="form-control" rows="3">Agencia inmobiliaria especializada en la venta de terrenos y propiedades en Tenerife.</textarea>
</div>
<button class="btn btn-primary">Guardar cambios</button>
</div>
</div>
</div>
<div class="tab-pane fade" id="settings-contact">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Información de Contacto</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Teléfono</label>
<input type="text" class="form-control" value="+34 922 123 456">
</div>
<div class="col-md-6">
<label class="form-label">WhatsApp</label>
<input type="text" class="form-control" value="+34 600 123 456">
</div>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" value="info@tenerifeprop.com">
</div>
<div class="mb-3">
<label class="form-label">Dirección</label>
<textarea class="form-control" rows="2">Avda. de la Constitución, 25
38640 Adeje, Tenerife, España</textarea>
</div>
<button class="btn btn-primary">Guardar cambios</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<script>
async function loadSettings() {
try {
const res = await API.getSettings();
if (res.success && res.data) {
Object.keys(res.data).forEach(key => {
const el = document.getElementById('setting_' + key);
if (el) el.value = res.data[key];
});
}
} catch (e) {
console.error('Failed to load settings:', e);
}
}
document.getElementById('settingsForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {};
formData.forEach((v, k) => data[k] = v);
try {
await API.updateSettings(data);
alert('Configuración guardada');
} catch (e) {
alert('Error al guardar');
}
});
</script>

View File

@@ -1,82 +1,78 @@
<!-- Testimonials Section -->
<div class="section" id="section-testimonials">
<div class="page-header">
<div>
<h1 class="page-title">Testimonios</h1>
<p class="page-subtitle">Gestión de testimonios de clientes</p>
</div>
<button class="btn btn-primary" onclick="showTestimonialModal()">
<i class="bi bi-plus-lg me-2"></i>Nuevo Testimonio
</button>
</div>
<!-- ============ TESTIMONIALS SECTION ============ -->
<section class="page-section" id="section-testimonials">
<div class="page-header">
<div>
<h1 class="page-title">Testimonios</h1>
<p class="page-subtitle">Gestiona los testimonios de clientes</p>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#testimonialModal">
<i class="bi bi-plus-lg me-2"></i>Añadir testimonio
</button>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Cliente</th>
<th>Ubicación</th>
<th>Valoración</th>
<th>Testimonio</th>
<th>Aprobado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="testimonialsTable"></tbody>
</table>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<div class="d-flex gap-3">
<img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop" class="rounded-circle" style="width: 60px; height: 60px; object-fit: cover;">
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<div>
<h5 class="mb-1">Michael Schmidt</h5>
<p class="text-muted mb-2">🇩🇪 Alemania</p>
</div>
<div class="text-warning">
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
</div>
</div>
<p class="mb-2">"Encuentré mi terreno perfecto en solo 3 semanas. El equipo de TenerifeProp me ayudó con todo, desde la documentación hasta la conexión de servicios."</p>
<small class="text-muted">Hace 2 semanas</small>
</div>
</div>
<div class="mt-3 pt-3 border-top d-flex gap-2">
<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>
<span class="badge bg-success ms-auto">Activo</span>
</div>
</div>
<script>
async function loadTestimonials() {
try {
const res = await API.getTestimonials();
const testimonials = res.data || [];
document.getElementById('testimonialsTable').innerHTML = testimonials.map(t => `
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar bg-primary text-white me-2">${t.name.charAt(0)}</div>
<strong>${t.name}</strong>
</div>
</div>
</td>
<td>${t.location}</td>
<td>${'★'.repeat(t.rating)}${'☆'.repeat(5-t.rating)}</td>
<td>${t.text.substring(0, 60)}...</td>
<td><span class="badge bg-${t.is_approved ? 'success' : 'warning'}">${t.is_approved ? 'Sí' : 'No'}</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="editTestimonial('${t.id}')"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteTestimonial('${t.id}')"><i class="bi bi-trash"></i></button>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<div class="d-flex gap-3">
<img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop" class="rounded-circle" style="width: 60px; height: 60px; object-fit: cover;">
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<div>
<h5 class="mb-1">Anna Petrova</h5>
<p class="text-muted mb-2">🇷🇺 Rusia</p>
</div>
<div class="text-warning">
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
</div>
</div>
<p class="mb-2">"Las ruinas que compramos fueron restauradas y ahora tenemos la casa de nuestros sueños. El equipo懂 todo el proceso legal."</p>
<small class="text-muted">Hace 1 mes</small>
</div>
</div>
<div class="mt-3 pt-3 border-top d-flex gap-2">
<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>
<span class="badge bg-success ms-auto">Activo</span>
</div>
</div>
</div>
</div>
</td>
</tr>
`).join('');
} catch (e) {
console.error('Failed to load testimonials:', e);
}
}
</div>
</section>
function showTestimonialModal() {
console.log('Show testimonial modal');
}
function editTestimonial(id) {
console.log('Edit testimonial:', id);
}
async function deleteTestimonial(id) {
if (!confirm('¿Eliminar testimonio?')) return;
try {
await API.deleteTestimonial(id);
await loadTestimonials();
} catch (e) {
alert('Error al eliminar');
}
}
</script>

69
public/admin/traffic.html Normal file
View File

@@ -0,0 +1,69 @@
<!-- ============ TRAFFIC SECTION ============ -->
<section class="page-section" id="section-traffic">
<div class="page-header">
<div>
<h1 class="page-title">Tráfico</h1>
<p class="page-subtitle">Fuentes de tráfico y comportamiento</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="chart-card">
<div class="chart-card-header">
<h4 class="chart-card-title">Dispositivos</h4>
</div>
<div class="chart-container" style="height: 250px;">
<canvas id="devicesChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="chart-card">
<div class="chart-card-header">
<h4 class="chart-card-title">Geolocalización (Top 5 países)</h4>
</div>
<div class="chart-container" style="height: 250px;">
<canvas id="geoChart"></canvas>
</div>
</div>
</div>
</div>
<div class="table-card mt-4">
<div class="table-card-header">
<h4 class="table-card-title">Páginas más visitadas</h4>
</div>
<div class="table-wrapper">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Página</th>
<th>Vistas</th>
<th>Únicos</th>
<th>Tiempo avg</th>
<th>Tasa de rebote</th>
</tr>
</thead>
<tbody>
<tr>
<td>/terreno-urbano-adeje</td>
<td>2,345</td>
<td>1,890</td>
<td>4:32</td>
<td>28%</td>
</tr>
<tr>
<td>/catalogo</td>
<td>1,892</td>
<td>1,456</td>
<td>3:45</td>
<td>35%</td>
</tr>
<tr>
<td>/villa-los-cristianos</td>
<td>1,567</td>
<td>1,234</td>
<td>5:12</td>
<td>22%</td>
</tr>

73
public/admin/users.html Normal file
View File

@@ -0,0 +1,73 @@
<!-- ============ USERS SECTION ============ -->
<section class="page-section" id="section-users">
<div class="page-header">
<div>
<h1 class="page-title">Usuarios</h1>
<p class="page-subtitle">Gestiona los usuarios del sistema</p>
</div>
<button class="btn btn-primary">
<i class="bi bi-plus-lg me-2"></i>Añadir usuario
</button>
</div>
<div class="table-card">
<div class="table-wrapper">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Usuario</th>
<th>Email</th>
<th>Rol</th>
<th>Último acceso</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
Carlos Martínez
<small>Admin principal</small>
</div>
</div>
</td>
<td>carlos@tenerifeprop.com</td>
<td><span class="badge bg-danger">Administrador</span></td>
<td>Hace 5 minutos</td>
<td><span class="table-badge completed">Activo</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
</div>
</td>
</tr>
<tr>
<td>
<div class="table-user">
<img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop" class="table-user-avatar">
<div class="table-user-info">
María García
<small>Agente inmobiliario</small>
</div>
</div>
</td>
<td>maria@tenerifeprop.com</td>
<td><span class="badge bg-primary">Agente</span></td>
<td>Hace 1 hora</td>
<td><span class="table-badge completed">Activo</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn edit" title="Editar"><i class="bi bi-pencil"></i></button>
<button class="table-action-btn delete" title="Eliminar"><i class="bi bi-trash"></i></button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>

View File

@@ -1432,6 +1432,12 @@ app.get('/admin/testimonials.html', serveStatic({ path: './public/admin/testimon
app.get('/admin/faq.html', serveStatic({ path: './public/admin/faq.html' }))
app.get('/admin/services.html', serveStatic({ path: './public/admin/services.html' }))
app.get('/admin/settings.html', serveStatic({ path: './public/admin/settings.html' }))
app.get('/admin/users.html', serveStatic({ path: './public/admin/users.html' }))
app.get('/admin/analytics.html', serveStatic({ path: './public/admin/analytics.html' }))
app.get('/admin/traffic.html', serveStatic({ path: './public/admin/traffic.html' }))
app.get('/admin/users.html', serveStatic({ path: './public/admin/users.html' }))
app.get('/admin/analytics.html', serveStatic({ path: './public/admin/analytics.html' }))
app.get('/admin/traffic.html', serveStatic({ path: './public/admin/traffic.html' }))
// SPA routes
app.get('/property/*', serveStatic({ path: './public/property.html' }))