- Add session-based authentication system - Implement admin CRUD endpoints for properties, leads, testimonials, FAQ, services - Fix security issue: remove public GET /api/leads endpoint - Add basic input validation for leads endpoint - Add global error handler - Fix Docker healthcheck using bun's fetch - Add @types/bcrypt dependency - Add .dockerignore - Add host reboot prohibition to global rules
774 lines
23 KiB
JavaScript
774 lines
23 KiB
JavaScript
// TenerifeProp - Admin Panel JavaScript
|
|
class AdminPanel {
|
|
constructor() {
|
|
this.currentSection = 'dashboard'
|
|
this.properties = []
|
|
this.leads = []
|
|
this.charts = {}
|
|
this.lang = localStorage.getItem('lang') || 'es'
|
|
}
|
|
|
|
async init() {
|
|
await this.loadTranslations()
|
|
this.initSidebar()
|
|
this.initTopbar()
|
|
this.initLanguageSwitcher()
|
|
await this.loadDashboardData()
|
|
this.initCharts()
|
|
this.updateUI()
|
|
}
|
|
|
|
async loadTranslations() {
|
|
try {
|
|
const [esRes, ruRes] = await Promise.all([
|
|
fetch('/src/i18n/es.json'),
|
|
fetch('/src/i18n/ru.json')
|
|
])
|
|
|
|
window.translations = {
|
|
es: await esRes.json(),
|
|
ru: await ruRes.json()
|
|
}
|
|
|
|
if (window.i18n) {
|
|
window.i18n.translations = window.translations
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load translations:', e)
|
|
}
|
|
}
|
|
|
|
initSidebar() {
|
|
// Section navigation
|
|
document.querySelectorAll('.sidebar-link').forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault()
|
|
const section = e.currentTarget.dataset.section
|
|
this.navigateTo(section)
|
|
})
|
|
})
|
|
}
|
|
|
|
navigateTo(section) {
|
|
this.currentSection = section
|
|
|
|
// Update sidebar
|
|
document.querySelectorAll('.sidebar-link').forEach(link => {
|
|
link.classList.toggle('active', link.dataset.section === section)
|
|
})
|
|
|
|
// Update sections
|
|
document.querySelectorAll('.page-section').forEach(sec => {
|
|
sec.classList.toggle('active', sec.id === `section-${section}`)
|
|
})
|
|
|
|
// Update page title
|
|
const titles = {
|
|
dashboard: { title: 'Dashboard', subtitle: 'Resumen del rendimiento de tu negocio' },
|
|
properties: { title: 'Propiedades', subtitle: 'Gestiona tu inventario de propiedades' },
|
|
leads: { title: 'Leads', subtitle: 'Administra las solicitudes de clientes' },
|
|
testimonials: { title: 'Testimonios', subtitle: 'Gestiona las opiniones de clientes' },
|
|
faq: { title: 'FAQ', subtitle: 'Preguntas frecuentes' },
|
|
services: { title: 'Servicios', subtitle: 'Lista de servicios ofrecidos' },
|
|
settings: { title: 'Configuración', subtitle: 'Ajustes del sistema' },
|
|
analytics: { title: 'Estadísticas', subtitle: 'Análisis del rendimiento' }
|
|
}
|
|
|
|
const t = titles[section] || { title: section, subtitle: '' }
|
|
const pageTitle = document.querySelector('.page-title')
|
|
const pageSubtitle = document.querySelector('.page-subtitle')
|
|
|
|
if (pageTitle) pageTitle.textContent = this.t(`admin.${section}`, t.title)
|
|
if (pageSubtitle) pageSubtitle.textContent = this.t(`admin.${section}Subtitle`, t.subtitle)
|
|
|
|
// Load section data
|
|
this.loadSectionData(section)
|
|
}
|
|
|
|
async loadSectionData(section) {
|
|
switch (section) {
|
|
case 'dashboard':
|
|
await this.loadDashboardData()
|
|
break
|
|
case 'properties':
|
|
await this.loadProperties()
|
|
break
|
|
case 'leads':
|
|
await this.loadLeads()
|
|
break
|
|
case 'testimonials':
|
|
await this.loadTestimonials()
|
|
break
|
|
case 'faq':
|
|
await this.loadFAQ()
|
|
break
|
|
case 'services':
|
|
await this.loadServices()
|
|
break
|
|
}
|
|
}
|
|
|
|
initTopbar() {
|
|
// Sidebar toggle
|
|
const sidebarToggle = document.getElementById('sidebarToggle')
|
|
const sidebar = document.getElementById('sidebar')
|
|
|
|
if (sidebarToggle && sidebar) {
|
|
sidebarToggle.addEventListener('click', () => {
|
|
sidebar.classList.toggle('collapsed')
|
|
})
|
|
}
|
|
|
|
// Global search
|
|
const globalSearch = document.getElementById('globalSearch')
|
|
if (globalSearch) {
|
|
globalSearch.addEventListener('input', debounce((e) => {
|
|
this.handleGlobalSearch(e.target.value)
|
|
}, 300))
|
|
}
|
|
}
|
|
|
|
handleGlobalSearch(query) {
|
|
if (query.length < 2) return
|
|
|
|
// Search across properties and leads
|
|
console.log('Searching for:', query)
|
|
// Implementation would show search results dropdown
|
|
}
|
|
|
|
initLanguageSwitcher() {
|
|
document.querySelectorAll('.topbar-lang button').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const lang = e.target.dataset.lang
|
|
this.setLanguage(lang)
|
|
})
|
|
})
|
|
}
|
|
|
|
setLanguage(lang) {
|
|
this.lang = lang
|
|
localStorage.setItem('lang', lang)
|
|
|
|
if (window.i18n) {
|
|
window.i18n.setLanguage(lang)
|
|
}
|
|
|
|
// Update language buttons
|
|
document.querySelectorAll('.topbar-lang button').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.lang === lang)
|
|
})
|
|
}
|
|
|
|
t(key, fallback) {
|
|
if (window.i18n) {
|
|
return window.i18n.t(key, fallback)
|
|
}
|
|
return fallback || key
|
|
}
|
|
|
|
async loadDashboardData() {
|
|
try {
|
|
const [statsRes, leadsRes, propertiesRes] = await Promise.all([
|
|
API.getStats(),
|
|
API.getLeads({ limit: 5 }),
|
|
API.getProperties({ limit: 5, lang: this.lang })
|
|
])
|
|
|
|
if (statsRes.success) {
|
|
this.updateStatCards(statsRes.data)
|
|
}
|
|
|
|
if (leadsRes.success) {
|
|
this.leads = leadsRes.data
|
|
this.updateLeadsTable()
|
|
}
|
|
|
|
if (propertiesRes.success) {
|
|
this.properties = propertiesRes.data
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load dashboard data:', e)
|
|
}
|
|
}
|
|
|
|
updateStatCards(stats) {
|
|
const statViews = document.getElementById('statViews')
|
|
const statLeads = document.getElementById('statLeads')
|
|
const statProperties = document.getElementById('statProperties')
|
|
|
|
if (statViews) statViews.textContent = this.formatNumber(stats.totalViews || 0)
|
|
if (statLeads) statLeads.textContent = stats.totalLeads || 0
|
|
if (statProperties) statProperties.textContent = stats.activeProperties || 0
|
|
}
|
|
|
|
formatNumber(num) {
|
|
if (num >= 1000000) {
|
|
return (num / 1000000).toFixed(1) + 'M'
|
|
}
|
|
if (num >= 1000) {
|
|
return (num / 1000).toFixed(1) + 'K'
|
|
}
|
|
return num.toString()
|
|
}
|
|
|
|
updateLeadsTable() {
|
|
const tbody = document.querySelector('#leadsTable tbody')
|
|
if (!tbody) return
|
|
|
|
tbody.innerHTML = this.leads.map(lead => `
|
|
<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">
|
|
${lead.name}
|
|
<small>${lead.phone || lead.email}</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>${lead.property_id || 'General inquiry'}</h6>
|
|
<span>${new Date(lead.created_at).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td><span class="badge bg-${this.getSourceColor(lead.source)}">${lead.source || 'webform'}</span></td>
|
|
<td>${new Date(lead.created_at).toLocaleDateString()}</td>
|
|
<td><span class="table-badge ${lead.status}">${this.t(`status.${lead.status}`, lead.status)}</span></td>
|
|
<td>
|
|
<div class="table-actions">
|
|
<button class="table-action-btn view" onclick="admin.viewLead('${lead.id}')" title="Ver">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<button class="table-action-btn edit" onclick="admin.editLead('${lead.id}')" title="Editar">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button class="table-action-btn delete" onclick="admin.deleteLead('${lead.id}')" title="Eliminar">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')
|
|
}
|
|
|
|
getSourceColor(source) {
|
|
const colors = {
|
|
whatsapp: 'success',
|
|
webform: 'secondary',
|
|
email: 'info',
|
|
phone: 'primary'
|
|
}
|
|
return colors[source] || 'secondary'
|
|
}
|
|
|
|
async loadProperties() {
|
|
try {
|
|
const res = await API.getProperties({ lang: this.lang, limit: 100 })
|
|
if (res.success) {
|
|
this.properties = res.data
|
|
this.renderPropertiesGrid()
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load properties:', e)
|
|
}
|
|
}
|
|
|
|
renderPropertiesGrid() {
|
|
const container = document.getElementById('propertiesGrid')
|
|
if (!container) return
|
|
|
|
container.innerHTML = this.properties.map(prop => `
|
|
<div class="col-lg-4 col-md-6">
|
|
<div class="property-admin-card">
|
|
<div class="property-admin-card-image">
|
|
<img src="${prop.images ? JSON.parse(prop.images)[0] : 'https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=400'}" alt="${prop.title}">
|
|
<div class="property-admin-card-badges">
|
|
<span class="property-admin-card-badge ${prop.status}">${this.t(`status.${prop.status}`, prop.status)}</span>
|
|
<span class="property-admin-card-badge ${prop.type}">${this.t(`property.${prop.type}Land`, prop.type)}</span>
|
|
</div>
|
|
<div class="property-admin-card-actions">
|
|
<a href="/property/${prop.slug}" class="property-admin-card-action" target="_blank" title="Ver">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
<button class="property-admin-card-action" onclick="admin.editProperty('${prop.id}')" title="Editar">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button class="property-admin-card-action delete" onclick="admin.deleteProperty('${prop.id}')" title="Eliminar">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="property-admin-card-content">
|
|
<h5 class="property-admin-card-title">${prop.title}</h5>
|
|
<p class="property-admin-card-location">
|
|
<i class="bi bi-geo-alt"></i>
|
|
${prop.city}, ${prop.zone || ''}
|
|
</p>
|
|
<div class="property-admin-card-stats">
|
|
<div class="property-admin-card-stat">
|
|
<i class="bi bi-eye"></i>
|
|
<span>${prop.views_count || 0}</span>
|
|
</div>
|
|
<div class="property-admin-card-stat">
|
|
<i class="bi bi-heart"></i>
|
|
<span>${prop.favorite_count || 0}</span>
|
|
</div>
|
|
<div class="property-admin-card-stat">
|
|
<i class="bi bi-chat"></i>
|
|
<span>${prop.inquiry_count || 0}</span>
|
|
</div>
|
|
</div>
|
|
<div class="property-admin-card-price">
|
|
${this.formatPrice(prop.price)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')
|
|
}
|
|
|
|
formatPrice(price) {
|
|
return new Intl.NumberFormat('es-ES', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
}).format(price)
|
|
}
|
|
|
|
async loadLeads() {
|
|
try {
|
|
const res = await API.getLeads()
|
|
if (res.success) {
|
|
this.leads = res.data
|
|
this.updateLeadsTable()
|
|
this.updateLeadsChart()
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load leads:', e)
|
|
}
|
|
}
|
|
|
|
updateLeadsChart() {
|
|
// Implementation for updating leads chart
|
|
if (this.charts.leadsChart) {
|
|
const statusCounts = this.leads.reduce((acc, lead) => {
|
|
acc[lead.status] = (acc[lead.status] || 0) + 1
|
|
return acc
|
|
}, {})
|
|
|
|
this.charts.leadsChart.data.datasets[0].data = [
|
|
statusCounts.new || 0,
|
|
statusCounts.contacted || 0,
|
|
statusCounts.qualified || 0,
|
|
statusCounts.negotiating || 0,
|
|
statusCounts.closed || 0
|
|
]
|
|
this.charts.leadsChart.update()
|
|
}
|
|
}
|
|
|
|
async loadTestimonials() {
|
|
try {
|
|
const res = await API.getTestimonials(this.lang)
|
|
if (res.success) {
|
|
this.renderTestimonialsTable(res.data)
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load testimonials:', e)
|
|
}
|
|
}
|
|
|
|
renderTestimonialsTable(testimonials) {
|
|
const tbody = document.querySelector('#testimonialsTable tbody')
|
|
if (!tbody) return
|
|
|
|
tbody.innerHTML = testimonials.map(t => `
|
|
<tr>
|
|
<td>
|
|
<div class="table-user">
|
|
<img src="${t.avatar || 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100'}" class="table-user-avatar">
|
|
<div class="table-user-info">
|
|
${t.name}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${t.location}</td>
|
|
<td>${'★'.repeat(t.rating)}${'☆'.repeat(5 - t.rating)}</td>
|
|
<td>${t.text.substring(0, 50)}...</td>
|
|
<td><span class="table-badge ${t.is_approved ? 'completed' : 'pending'}">${t.is_approved ? 'Aprobado' : 'Pendiente'}</span></td>
|
|
<td>
|
|
<div class="table-actions">
|
|
<button class="table-action-btn edit" onclick="admin.editTestimonial('${t.id}')" title="Editar">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button class="table-action-btn delete" onclick="admin.deleteTestimonial('${t.id}')" title="Eliminar">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')
|
|
}
|
|
|
|
async loadFAQ() {
|
|
try {
|
|
const res = await API.getFAQ(this.lang)
|
|
if (res.success) {
|
|
this.renderFAQTable(res.data)
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load FAQ:', e)
|
|
}
|
|
}
|
|
|
|
renderFAQTable(faqs) {
|
|
const tbody = document.querySelector('#faqTable tbody')
|
|
if (!tbody) return
|
|
|
|
tbody.innerHTML = faqs.map(f => `
|
|
<tr>
|
|
<td>${f.question}</td>
|
|
<td>${f.answer.substring(0, 100)}...</td>
|
|
<td><span class="badge bg-${f.is_active ? 'success' : 'secondary'}">${f.is_active ? 'Activo' : 'Inactivo'}</span></td>
|
|
<td>
|
|
<div class="table-actions">
|
|
<button class="table-action-btn edit" onclick="admin.editFAQ('${f.id}')" title="Editar">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button class="table-action-btn delete" onclick="admin.deleteFAQ('${f.id}')" title="Eliminar">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')
|
|
}
|
|
|
|
async loadServices() {
|
|
try {
|
|
const res = await API.getServices(this.lang)
|
|
if (res.success) {
|
|
this.renderServicesTable(res.data)
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load services:', e)
|
|
}
|
|
}
|
|
|
|
renderServicesTable(services) {
|
|
const tbody = document.querySelector('#servicesTable tbody')
|
|
if (!tbody) return
|
|
|
|
tbody.innerHTML = services.map(s => `
|
|
<tr>
|
|
<td><i class="${s.icon}"></i></td>
|
|
<td>${s.title}</td>
|
|
<td>${s.description.substring(0, 100)}...</td>
|
|
<td><span class="badge bg-${s.is_active ? 'success' : 'secondary'}">${s.is_active ? 'Activo' : 'Inactivo'}</span></td>
|
|
<td>
|
|
<div class="table-actions">
|
|
<button class="table-action-btn edit" onclick="admin.editService('${s.id}')" title="Editar">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button class="table-action-btn delete" onclick="admin.deleteService('${s.id}')" title="Eliminar">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')
|
|
}
|
|
|
|
initCharts() {
|
|
// Performance Chart
|
|
const performanceCtx = document.getElementById('performanceChart')?.getContext('2d')
|
|
if (performanceCtx && typeof Chart !== 'undefined') {
|
|
this.charts.performance = new Chart(performanceCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun'],
|
|
datasets: [{
|
|
label: 'Vistas',
|
|
data: [1200, 1900, 3000, 2500, 2800, 3200],
|
|
borderColor: '#1a5f4a',
|
|
backgroundColor: 'rgba(26, 95, 74, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
}, {
|
|
label: 'Leads',
|
|
data: [20, 35, 50, 45, 60, 75],
|
|
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
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Traffic Chart
|
|
const trafficCtx = document.getElementById('trafficChart')?.getContext('2d')
|
|
if (trafficCtx && typeof Chart !== 'undefined') {
|
|
this.charts.traffic = new Chart(trafficCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Directo', 'Búsqueda', 'Social', 'Referido', 'Email'],
|
|
datasets: [{
|
|
data: [35, 30, 20, 10, 5],
|
|
backgroundColor: [
|
|
'#1a5f4a',
|
|
'#d4a853',
|
|
'#e85d04',
|
|
'#3b82f6',
|
|
'#6c757d'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Types Chart
|
|
const typesCtx = document.getElementById('typesChart')?.getContext('2d')
|
|
if (typesCtx && typeof Chart !== 'undefined') {
|
|
this.charts.types = new Chart(typesCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['Urbano', 'Agrícola', 'Casa', 'Apartamento', 'Ruinas'],
|
|
datasets: [{
|
|
label: 'Propiedades',
|
|
data: [15, 12, 8, 5, 3],
|
|
backgroundColor: '#1a5f4a'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Leads Status Chart
|
|
const leadsCtx = document.getElementById('leadsChart')?.getContext('2d')
|
|
if (leadsCtx && typeof Chart !== 'undefined') {
|
|
this.charts.leadsChart = new Chart(leadsCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Nuevo', 'Contactado', 'Calificado', 'Negociando', 'Cerrado'],
|
|
datasets: [{
|
|
data: [5, 3, 2, 1, 1],
|
|
backgroundColor: [
|
|
'#3b82f6',
|
|
'#f59e0b',
|
|
'#10b981',
|
|
'#8b5cf6',
|
|
'#1a5f4a'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Top Properties Chart
|
|
const topCtx = document.getElementById('topPropertiesChart')?.getContext('2d')
|
|
if (topCtx && typeof Chart !== 'undefined') {
|
|
this.charts.top = new Chart(topCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['TP-001', 'TP-003', 'TP-002', 'TP-005', 'TP-004'],
|
|
datasets: [{
|
|
label: 'Vistas',
|
|
data: [2345, 1876, 892, 654, 432],
|
|
backgroundColor: '#d4a853'
|
|
}]
|
|
},
|
|
options: {
|
|
indexAxis: 'y',
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// CRUD Operations
|
|
viewLead(id) {
|
|
const lead = this.leads.find(l => l.id === id)
|
|
if (!lead) return
|
|
|
|
// Show lead details modal
|
|
console.log('View lead:', lead)
|
|
}
|
|
|
|
editLead(id) {
|
|
const lead = this.leads.find(l => l.id === id)
|
|
if (!lead) return
|
|
|
|
// Show edit lead modal
|
|
console.log('Edit lead:', lead)
|
|
}
|
|
|
|
async deleteLead(id) {
|
|
if (!confirm('¿Está seguro de eliminar este lead?')) return
|
|
|
|
try {
|
|
// API call to delete
|
|
console.log('Deleting lead:', id)
|
|
this.leads = this.leads.filter(l => l.id !== id)
|
|
this.updateLeadsTable()
|
|
} catch (e) {
|
|
console.error('Failed to delete lead:', e)
|
|
}
|
|
}
|
|
|
|
editProperty(id) {
|
|
const property = this.properties.find(p => p.id === id)
|
|
if (!property) return
|
|
|
|
// Show edit property modal
|
|
console.log('Edit property:', property)
|
|
}
|
|
|
|
async deleteProperty(id) {
|
|
if (!confirm('¿Está seguro de eliminar esta propiedad?')) return
|
|
|
|
try {
|
|
// API call to delete
|
|
console.log('Deleting property:', id)
|
|
this.properties = this.properties.filter(p => p.id !== id)
|
|
this.renderPropertiesGrid()
|
|
} catch (e) {
|
|
console.error('Failed to delete property:', e)
|
|
}
|
|
}
|
|
|
|
editTestimonial(id) {
|
|
console.log('Edit testimonial:', id)
|
|
}
|
|
|
|
deleteTestimonial(id) {
|
|
if (!confirm('¿Está seguro de eliminar este testimonio?')) return
|
|
console.log('Deleting testimonial:', id)
|
|
}
|
|
|
|
editFAQ(id) {
|
|
console.log('Edit FAQ:', id)
|
|
}
|
|
|
|
deleteFAQ(id) {
|
|
if (!confirm('¿Está seguro de eliminar esta pregunta?')) return
|
|
console.log('Deleting FAQ:', id)
|
|
}
|
|
|
|
editService(id) {
|
|
console.log('Edit service:', id)
|
|
}
|
|
|
|
deleteService(id) {
|
|
if (!confirm('¿Está seguro de eliminar este servicio?')) return
|
|
console.log('Deleting service:', id)
|
|
}
|
|
|
|
// Modal handlers
|
|
showPropertyModal(property = null) {
|
|
// Implementation
|
|
}
|
|
|
|
showLeadModal(lead = null) {
|
|
// Implementation
|
|
}
|
|
|
|
showNotification(message, type = 'info') {
|
|
const notification = document.createElement('div')
|
|
notification.className = `alert alert-${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'info'} alert-dismissible fade show position-fixed`
|
|
notification.style.cssText = 'top: 100px; right: 30px; z-index: 9999; max-width: 400px;'
|
|
notification.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`
|
|
document.body.appendChild(notification)
|
|
|
|
setTimeout(() => {
|
|
notification.remove()
|
|
}, 5000)
|
|
}
|
|
|
|
updateUI() {
|
|
// Update language buttons
|
|
document.querySelectorAll('.topbar-lang button').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.lang === this.lang)
|
|
})
|
|
|
|
// Update lead count badge
|
|
const leadsCountBadge = document.getElementById('leadsCount')
|
|
if (leadsCountBadge && this.leads.length > 0) {
|
|
const newLeads = this.leads.filter(l => l.status === 'new').length
|
|
leadsCountBadge.textContent = newLeads
|
|
}
|
|
}
|
|
}
|
|
|
|
// Utility function
|
|
function debounce(func, wait) {
|
|
let timeout
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout)
|
|
func(...args)
|
|
}
|
|
clearTimeout(timeout)
|
|
timeout = setTimeout(later, wait)
|
|
}
|
|
}
|
|
|
|
// Initialize admin panel when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.admin = new AdminPanel()
|
|
admin.init()
|
|
}) |