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:
1985
public/admin.html
1985
public/admin.html
File diff suppressed because it is too large
Load Diff
64
public/admin/analytics.html
Normal file
64
public/admin/analytics.html
Normal 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>
|
||||
|
||||
@@ -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">Añ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');
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"> </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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
69
public/admin/traffic.html
Normal 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
73
public/admin/users.html
Normal 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>
|
||||
|
||||
@@ -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' }))
|
||||
|
||||
Reference in New Issue
Block a user