## 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
377 lines
22 KiB
HTML
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>
|