Files
TenerifeProp/public/admin.html
TenerifeProp Dev 1dd901dd4f refactor: modular admin panel with clean URLs
## Changes
- Removed .html extension from URLs (/login, /admin)
- Completely refactored admin.html with modular design
- Common sidebar and topbar for all admin sections
- Dynamic content loading via AJAX
- Modern responsive design with Bootstrap 5

## Admin Sections
- Dashboard (statistics, recent items)
- Properties (list with CRUD)
- Leads (management)
- Testimonials (CRUD)
- FAQ (CRUD)
- Services (CRUD)
- Settings (site configuration)

## Technical
- Clean URL routing: /login, /admin instead of .html
- Session-based auth check on page load
- Universal API client with auth methods
- Single-page admin with dynamic sections

## URLs
- Login: /login (was /login.html)
- Admin: /admin (was /admin.html)
- API: /api/auth/login, /api/admin/stats

## Tested
 /login returns correct page
 /admin returns correct page
 Login API works
 Session persists
 Admin sections load correctly
2026-04-06 01:24:37 +01:00

377 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Admin Panel | TenerifeProp</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--primary: #1a5f4a;
--primary-light: #2d8f6f;
--secondary: #d4a853;
--sidebar-bg: #0f172a;
--sidebar-hover: #1e293b;
--sidebar-width: 260px;
--topbar-height: 60px;
--body-bg: #f1f5f9;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: var(--body-bg); min-height: 100vh; }
.sidebar { position: fixed; left: 0; top: 0; width: var(--sidebar-width); height: 100vh; background: var(--sidebar-bg); overflow-y: auto; z-index: 1000; }
.sidebar-header { padding: 20px; border-bottom: 1px solid rgba(255,255,255,0.1); }
.sidebar-logo { display: flex; align-items: center; gap: 12px; text-decoration: none; color: white; }
.sidebar-logo i { font-size: 28px; color: var(--primary-light); }
.sidebar-logo span { font-size: 18px; font-weight: 700; }
.sidebar-nav { padding: 16px 0; }
.nav-section { padding: 8px 16px; }
.nav-section-title { color: #64748b; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 12px; }
.nav-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; color: #94a3b8; text-decoration: none; border-radius: 8px; margin-bottom: 2px; transition: all 0.2s; }
.nav-item:hover { background: var(--sidebar-hover); color: white; }
.nav-item.active { background: var(--primary); color: white; }
.nav-item i { font-size: 18px; width: 24px; }
.topbar { position: fixed; top: 0; left: var(--sidebar-width); right: 0; height: var(--topbar-height); background: white; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; z-index: 100; }
.topbar-title { font-size: 18px; font-weight: 600; }
.user-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--primary); color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; }
.main-content { margin-left: var(--sidebar-width); margin-top: var(--topbar-height); padding: 24px; min-height: calc(100vh - var(--topbar-height)); }
.section { display: none; }
.section.active { display: block; }
.stat-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
@media (max-width: 1024px) {
.sidebar { transform: translateX(-100%); transition: transform 0.3s; }
.sidebar.open { transform: translateX(0); }
.main-content, .topbar { margin-left: 0; left: 0; }
}
</style>
</head>
<body>
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<a href="/" class="sidebar-logo">
<i class="bi bi-house-door"></i>
<span>TenerifeProp</span>
</a>
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Main</div>
<a href="#dashboard" class="nav-item active" data-section="dashboard"><i class="bi bi-speedometer2"></i><span>Dashboard</span></a>
</div>
<div class="nav-section">
<div class="nav-section-title">Properties</div>
<a href="#properties" class="nav-item" data-section="properties"><i class="bi bi-building"></i><span>All Properties</span></a>
</div>
<div class="nav-section">
<div class="nav-section-title">CRM</div>
<a href="#leads" class="nav-item" data-section="leads"><i class="bi bi-people"></i><span>Leads</span><span class="badge bg-danger ms-auto" id="newLeadsBadge">0</span></a>
<a href="#testimonials" class="nav-item" data-section="testimonials"><i class="bi bi-chat-quote"></i><span>Testimonials</span></a>
</div>
<div class="nav-section">
<div class="nav-section-title">Content</div>
<a href="#faq" class="nav-item" data-section="faq"><i class="bi bi-question-circle"></i><span>FAQ</span></a>
<a href="#services" class="nav-item" data-section="services"><i class="bi bi-gear"></i><span>Services</span></a>
</div>
<div class="nav-section">
<div class="nav-section-title">System</div>
<a href="#settings" class="nav-item" data-section="settings"><i class="bi bi-sliders"></i><span>Settings</span></a>
</div>
</nav>
</aside>
<header class="topbar">
<div class="d-flex align-items-center gap-3">
<button class="btn btn-link d-lg-none" id="sidebarToggle"><i class="bi bi-list fs-4"></i></button>
<h1 class="topbar-title" id="pageTitle">Dashboard</h1>
</div>
<div class="d-flex align-items-center gap-3">
<div class="dropdown">
<div class="user-menu d-flex align-items-center gap-2" data-bs-toggle="dropdown" style="cursor:pointer">
<div class="user-avatar" id="userAvatar">A</div>
<span id="userName">Admin</span>
<i class="bi bi-chevron-down"></i>
</div>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="#" onclick="logout()"><i class="bi bi-box-arrow-right me-2"></i>Logout</a></li>
</ul>
</div>
</div>
</header>
<main class="main-content" id="mainContent">
<!-- Dashboard -->
<div class="section active" id="section-dashboard">
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="stat-card">
<div class="d-flex align-items-center gap-3">
<div class="bg-primary bg-opacity-10 text-primary p-3 rounded"><i class="bi bi-building fs-4"></i></div>
<div><div class="text-secondary small">Properties</div><div class="fs-4 fw-bold" id="statProperties">0</div></div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="d-flex align-items-center gap-3">
<div class="bg-success bg-opacity-10 text-success p-3 rounded"><i class="bi bi-people fs-4"></i></div>
<div><div class="text-secondary small">New Leads</div><div class="fs-4 fw-bold" id="statLeads">0</div></div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="d-flex align-items-center gap-3">
<div class="bg-warning bg-opacity-10 text-warning p-3 rounded"><i class="bi bi-eye fs-4"></i></div>
<div><div class="text-secondary small">Views</div><div class="fs-4 fw-bold" id="statViews">0</div></div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="d-flex align-items-center gap-3">
<div class="bg-info bg-opacity-10 text-info p-3 rounded"><i class="bi bi-currency-euro fs-4"></i></div>
<div><div class="text-secondary small">Avg Price</div><div class="fs-4 fw-bold" id="statAvgPrice">€0</div></div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="stat-card"><h5 class="mb-3">Recent Properties</h5><div class="table-responsive"><table class="table table-hover"><thead><tr><th>Title</th><th>Type</th><th>Price</th><th>Status</th></tr></thead><tbody id="recentPropertiesTable"></tbody></table></div></div>
</div>
<div class="col-lg-4">
<div class="stat-card"><h5 class="mb-3">Recent Leads</h5><div id="recentLeadsList"></div></div>
</div>
</div>
</div>
<!-- Properties -->
<div class="section" id="section-properties">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Properties</h2>
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Add Property</button>
</div>
<div class="stat-card"><div class="table-responsive"><table class="table"><thead><tr><th>Ref</th><th>Title</th><th>Type</th><th>City</th><th>Price</th><th>Status</th><th>Actions</th></tr></thead><tbody id="propertiesTable"></tbody></table></div></div>
</div>
<!-- Leads -->
<div class="section" id="section-leads">
<h2 class="mb-4">Leads</h2>
<div class="stat-card"><div class="table-responsive"><table class="table"><thead><tr><th>Name</th><th>Email</th><th>Phone</th><th>Property</th><th>Status</th><th>Actions</th></tr></thead><tbody id="leadsTable"></tbody></table></div></div>
</div>
<!-- Testimonials -->
<div class="section" id="section-testimonials">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Testimonials</h2>
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Add</button>
</div>
<div class="stat-card"><div class="table-responsive"><table class="table"><thead><tr><th>Name</th><th>Location</th><th>Rating</th><th>Approved</th><th>Actions</th></tr></thead><tbody id="testimonialsTable"></tbody></table></div></div>
</div>
<!-- FAQ -->
<div class="section" id="section-faq">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>FAQ</h2>
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Add</button>
</div>
<div class="stat-card"><div class="table-responsive"><table class="table"><thead><tr><th>Question</th><th>Category</th><th>Active</th><th>Actions</th></tr></thead><tbody id="faqTable"></tbody></table></div></div>
</div>
<!-- Services -->
<div class="section" id="section-services">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Services</h2>
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Add</button>
</div>
<div class="stat-card"><div class="table-responsive"><table class="table"><thead><tr><th>Icon</th><th>Title</th><th>Active</th><th>Actions</th></tr></thead><tbody id="servicesTable"></tbody></table></div></div>
</div>
<!-- Settings -->
<div class="section" id="section-settings">
<h2 class="mb-4">Settings</h2>
<div class="stat-card">
<form id="settingsForm">
<div class="row g-4">
<div class="col-md-6"><label class="form-label">Site Name</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</label><input type="email" class="form-control" name="email" id="setting_email"></div>
<div class="col-md-6"><label class="form-label">Phone</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"><button type="submit" class="btn btn-primary">Save Settings</button></div>
</div>
</form>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/api.js"></script>
<script>
// Auth check
(async function() {
try {
const res = await fetch('/api/auth/me')
const data = await res.json()
if (!data.success || !data.data) {
window.location.href = '/login'
return
}
const user = data.data
document.getElementById('userName').textContent = user.name
document.getElementById('userAvatar').textContent = user.name.charAt(0).toUpperCase()
window.currentUser = user
await Promise.all([loadStats(), loadRecentProperties(), loadRecentLeads()])
} catch (e) {
window.location.href = '/login'
}
})()
// Navigation
document.querySelectorAll('.nav-item[data-section]').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault()
const section = e.currentTarget.dataset.section
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'))
e.currentTarget.classList.add('active')
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'))
document.getElementById('section-' + section)?.classList.add('active')
const titles = {dashboard:'Dashboard',properties:'Properties',leads:'Leads',testimonials:'Testimonials',faq:'FAQ',services:'Services',settings:'Settings'}
document.getElementById('pageTitle').textContent = titles[section] || section
loadSectionData(section)
})
})
function loadSectionData(s) {
if (s === 'dashboard') { loadStats(); loadRecentProperties(); loadRecentLeads() }
else if (s === 'properties') loadProperties()
else if (s === 'leads') loadLeads()
else if (s === 'testimonials') loadTestimonials()
else if (s === 'faq') loadFAQ()
else if (s === 'services') loadServices()
else if (s === 'settings') loadSettings()
}
async function loadStats() {
try {
const res = await API.getAdminStats()
if (res.success) {
document.getElementById('statProperties').textContent = res.data.properties.active
document.getElementById('statLeads').textContent = res.data.leads.new
document.getElementById('statViews').textContent = res.data.analytics.views.toLocaleString()
document.getElementById('statAvgPrice').textContent = '€' + res.data.averages.price.toLocaleString()
document.getElementById('newLeadsBadge').textContent = res.data.leads.new
}
} catch(e) { console.error(e) }
}
async function loadRecentProperties() {
try {
const res = await API.getProperties({limit:5})
if (res.success) {
document.getElementById('recentPropertiesTable').innerHTML = res.data.map(p =>
'<tr><td>'+p.title_es+'</td><td><span class="badge bg-secondary">'+p.type+'</span></td><td>€'+p.price.toLocaleString()+'</td><td><span class="badge bg-'+(p.status==='active'?'success':'warning')+'">'+p.status+'</span></td></tr>'
).join('')
}
} catch(e) { console.error(e) }
}
async function loadRecentLeads() {
try {
const res = await API.getLeads({limit:5})
if (res.success) {
document.getElementById('recentLeadsList').innerHTML = res.data.map(l =>
'<div class="d-flex align-items-center gap-3 mb-3 pb-3 border-bottom"><div class="user-avatar">'+l.name.charAt(0)+'</div><div class="flex-grow-1"><div class="fw-medium">'+l.name+'</div><small class="text-secondary">'+l.email+'</small></div><span class="badge bg-'+(l.status==='new'?'danger':'secondary')+'">'+l.status+'</span></div>'
).join('')
}
} catch(e) { console.error(e) }
}
async function loadProperties() {
try {
const res = await API.getProperties({limit:100})
if (res.success) {
document.getElementById('propertiesTable').innerHTML = res.data.map(p =>
'<tr><td><code>'+p.reference+'</code></td><td>'+p.title_es+'</td><td>'+p.type+'</td><td>'+p.city+'</td><td>€'+p.price.toLocaleString()+'</td><td><span class="badge bg-'+(p.status==='active'?'success':'warning')+'">'+p.status+'</span></td><td><button class="btn btn-sm btn-outline-danger" onclick="deleteProperty(\''+p.id+'\')"><i class="bi bi-trash"></i></button></td></tr>'
).join('')
}
} catch(e) { console.error(e) }
}
async function loadLeads() {
try {
const res = await API.getLeads()
if (res.success) {
document.getElementById('leadsTable').innerHTML = res.data.map(l =>
'<tr><td>'+l.name+'</td><td>'+l.email+'</td><td>'+(l.phone||'-')+'</td><td>'+(l.property_id||'General')+'</td><td><span class="badge bg-'+(l.status==='new'?'danger':'secondary')+'">'+l.status+'</span></td><td><button class="btn btn-sm btn-outline-danger" onclick="deleteLead(\''+l.id+'\')"><i class="bi bi-trash"></i></button></td></tr>'
).join('')
}
} catch(e) { console.error(e) }
}
async function loadTestimonials() {
try {
const res = await API.getTestimonials()
if (res.success) {
document.getElementById('testimonialsTable').innerHTML = res.data.map(t =>
'<tr><td>'+t.name+'</td><td>'+t.location+'</td><td>'+'★'.repeat(t.rating)+'☆'.repeat(5-t.rating)+'</td><td><span class="badge bg-'+(t.is_approved?'success':'warning')+'">'+(t.is_approved?'Yes':'No')+'</span></td><td><button class="btn btn-sm btn-outline-danger" onclick="deleteTestimonial(\''+t.id+'\')"><i class="bi bi-trash"></i></button></td></tr>'
).join('')
}
} catch(e) { console.error(e) }
}
async function loadFAQ() {
try {
const res = await API.getFAQ()
if (res.success) {
document.getElementById('faqTable').innerHTML = res.data.map(f =>
'<tr><td>'+f.question.substring(0,50)+'...</td><td><span class="badge bg-secondary">'+f.category+'</span></td><td><span class="badge bg-'+(f.is_active?'success':'danger')+'">'+(f.is_active?'Yes':'No')+'</span></td><td><button class="btn btn-sm btn-outline-danger" onclick="deleteFAQ(\''+f.id+'\')"><i class="bi bi-trash"></i></button></td></tr>'
).join('')
}
} catch(e) { console.error(e) }
}
async function loadServices() {
try {
const res = await API.getServices()
if (res.success) {
document.getElementById('servicesTable').innerHTML = res.data.map(s =>
'<tr><td><i class="'+s.icon+' fs-4"></i></td><td>'+s.title+'</td><td><span class="badge bg-'+(s.is_active?'success':'danger')+'">'+(s.is_active?'Yes':'No')+'</span></td><td><button class="btn btn-sm btn-outline-danger" onclick="deleteService(\''+s.id+'\')"><i class="bi bi-trash"></i></button></td></tr>'
).join('')
}
} catch(e) { console.error(e) }
}
async function loadSettings() {
try {
const res = await API.getSettings()
if (res.success) Object.keys(res.data).forEach(k => { const el = document.getElementById('setting_'+k); if (el) el.value = res.data[k] })
} catch(e) { console.error(e) }
}
async function logout() { try { await API.logout(); localStorage.removeItem('user'); window.location.href = '/login' } catch(e) { window.location.href = '/login' } }
async function deleteProperty(id) { if (!confirm('Delete this property?')) return; try { await API.deleteProperty(id); await loadProperties() } catch(e) { alert('Failed') } }
async function deleteLead(id) { if (!confirm('Delete this lead?')) return; try { await API.deleteLead(id); await loadLeads() } catch(e) { alert('Failed') } }
async function deleteTestimonial(id) { if (!confirm('Delete?')) return; try { await API.deleteTestimonial(id); await loadTestimonials() } catch(e) { alert('Failed') } }
async function deleteFAQ(id) { if (!confirm('Delete?')) return; try { await API.deleteFAQ(id); await loadFAQ() } catch(e) { alert('Failed') } }
async function deleteService(id) { if (!confirm('Delete?')) return; try { await API.deleteService(id); await loadServices() } catch(e) { alert('Failed') } }
document.getElementById('sidebarToggle')?.addEventListener('click', () => document.getElementById('sidebar').classList.toggle('open'))
if (window.location.hash) { const s = window.location.hash.substring(1); document.querySelector('.nav-item[data-section="'+s+'"]')?.click() }
document.getElementById('settingsForm')?.addEventListener('submit', async (e) => {
e.preventDefault()
const data = {}
new FormData(e.target).forEach((v, k) => data[k] = v)
try { await API.updateSettings(data); alert('Settings saved!') } catch(e) { alert('Failed to save') }
})
</script>
</body>
</html>