// 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 => `
${lead.name} ${lead.phone || lead.email}
${lead.property_id || 'General inquiry'}
${new Date(lead.created_at).toLocaleDateString()}
${lead.source || 'webform'} ${new Date(lead.created_at).toLocaleDateString()} ${this.t(`status.${lead.status}`, lead.status)}
`).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 => `
${prop.title}
${this.t(`status.${prop.status}`, prop.status)} ${this.t(`property.${prop.type}Land`, prop.type)}
${prop.title}

${prop.city}, ${prop.zone || ''}

${prop.views_count || 0}
${prop.favorite_count || 0}
${prop.inquiry_count || 0}
${this.formatPrice(prop.price)}
`).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 => `
${t.name}
${t.location} ${'★'.repeat(t.rating)}${'☆'.repeat(5 - t.rating)} ${t.text.substring(0, 50)}... ${t.is_approved ? 'Aprobado' : 'Pendiente'}
`).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 => ` ${f.question} ${f.answer.substring(0, 100)}... ${f.is_active ? 'Activo' : 'Inactivo'}
`).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 => ` ${s.title} ${s.description.substring(0, 100)}... ${s.is_active ? 'Activo' : 'Inactivo'}
`).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} ` 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() })