fix(admin): wire all dashboard buttons + fix 401/login console errors + chart period switching

- dashboard.html: add onclick handlers for Exportar, date range, chart periods
  (week/month/year), Ver todos, quick actions, remove stale inline lead IDs
- admin.js: add exportDashboard(), filterByDateRange(), setChartPeriod(),
  initDateRange(), updateChartsWithData() with period slicing, loadAnalytics()
  on dashboard init
- login.html: guard /api/auth/me with session cookie check to prevent 401 noise
- server/index.ts: fix Secure cookie flag: only set when HTTPS + production + !localhost
This commit is contained in:
APAW Agent Sync
2026-05-18 15:54:56 +01:00
parent 32eb1827e2
commit 4af3e7cd9d
4 changed files with 130 additions and 35 deletions

View File

@@ -6,8 +6,8 @@
<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">
<input type="text" id="dateRange" class="form-control" style="width: 250px;" placeholder="Seleccionar rango de fechas" onchange="admin.filterByDateRange(this.value)">
<button class="btn btn-primary" onclick="admin.exportDashboard()">
<i class="bi bi-download me-2"></i>Exportar
</button>
</div>
@@ -77,10 +77,10 @@
<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 class="chart-card-actions">
<button class="chart-period-btn" data-period="week" onclick="admin.setChartPeriod('week')">Semana</button>
<button class="chart-period-btn active" data-period="month" onclick="admin.setChartPeriod('month')">Mes</button>
<button class="chart-period-btn" data-period="year" onclick="admin.setChartPeriod('year')">Año</button>
</div>
</div>
<div class="chart-container">
@@ -132,7 +132,7 @@
<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">
<a href="#" class="table-card-action" onclick="event.preventDefault(); admin.navigateTo('leads')">
<i class="bi bi-eye"></i>
Ver todos
</a>
@@ -277,19 +277,19 @@ Ver todos
<!-- Quick Actions -->
<div class="quick-actions">
<a href="#" class="quick-action" data-section="properties">
<a href="#" class="quick-action" onclick="event.preventDefault(); admin.navigateTo('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">
<a href="#" class="quick-action" onclick="event.preventDefault(); admin.navigateTo('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">
<a href="#" class="quick-action" onclick="event.preventDefault(); admin.navigateTo('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">
<a href="#" class="quick-action" onclick="event.preventDefault(); admin.navigateTo('settings')">
<div class="quick-action-icon"><i class="bi bi-gear"></i></div>
<span data-i18n="dashboard.settings">Configuración</span>
</a>

View File

@@ -16,8 +16,7 @@ class AdminPanel {
// Auth check first - redirect to login if not authenticated
try {
await this.checkAuth()
} catch (e) {
console.error('Auth failed:', e)
} catch {
window.location.href = '/login'
return
}
@@ -125,7 +124,7 @@ class AdminPanel {
async loadSectionData(section) {
switch (section) {
case 'dashboard': await this.loadDashboardData(); this.initCharts(); this.updateUI(); break
case 'dashboard': await this.loadDashboardData(); this.initCharts(); await this.loadAnalytics(); this.updateUI(); break
case 'properties': await this.loadProperties(); break
case 'leads': await this.loadLeads(); break
case 'testimonials': await this.loadTestimonials(); break
@@ -196,11 +195,28 @@ class AdminPanel {
this.leads = leadsRes.data
this.updateLeadsTable()
}
this.initDateRange()
} catch (e) {
console.error('Failed to load dashboard data:', e)
}
}
initDateRange() {
const input = document.getElementById('dateRange')
if (!input || window._dateRangePicker) return
if (typeof flatpickr !== 'undefined') {
window._dateRangePicker = flatpickr(input, {
mode: 'range',
dateFormat: 'Y-m-d',
onChange: (dates) => {
if (dates.length === 2) {
this.filterByDateRange(`${dates[0].toISOString().slice(0,10)} to ${dates[1].toISOString().slice(0,10)}`)
}
}
})
}
}
updateStatCards(stats) {
const setVal = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val }
setVal('statViews', this.formatNumber(stats.analytics?.views || 0))
@@ -441,6 +457,47 @@ class AdminPanel {
this.charts.leadsChart.update()
}
// ============ DASHBOARD ACTIONS ============
async exportDashboard() {
const rows = [
['Metric', 'Value'],
['Total Properties', this.stats?.properties?.total || 0],
['Active Properties', this.stats?.properties?.active || 0],
['Total Leads', this.stats?.leads?.total || 0],
['New Leads', this.stats?.leads?.new || 0],
['Views', this.stats?.analytics?.views || 0],
['Conversion Rate', document.getElementById('statConversion')?.textContent || '0%']
]
const csv = rows.map(r => r.map(v => `"${v}"`).join(',')).join('\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `dashboard-${new Date().toISOString().slice(0,10)}.csv`
link.click()
this.showNotification('Dashboard exportado', 'success')
}
filterByDateRange(value) {
if (!value || !value.includes(' to ')) return
const [start, end] = value.split(' to ')
const filtered = this.leads.filter(l => {
const d = new Date(l.created_at)
return d >= new Date(start) && d <= new Date(end)
})
this.leads = filtered
this.updateLeadsTable()
this.showNotification(`Filtrado: ${filtered.length} leads`, 'success')
}
setChartPeriod(period) {
document.querySelectorAll('.chart-period-btn').forEach(b => {
b.classList.toggle('active', b.dataset.period === period)
})
this._chartPeriod = period
if (this._chartData) this.updateChartsWithData(this._chartData, period)
this.showNotification(`Período: ${period}`, 'success')
}
// ============ TESTIMONIALS ============
async loadTestimonials() {
try {
@@ -753,6 +810,8 @@ class AdminPanel {
// ============ CHARTS ============
initCharts() {
Object.values(this.charts).forEach(c => c?.destroy?.())
this.charts = {}
this.charts.performance = this.createLineChart('performanceChart',
['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun'],
[{ label: 'Vistas', data: [0, 0, 0, 0, 0, 0], color: '#1a5f4a' }, { label: 'Leads', data: [0, 0, 0, 0, 0, 0], color: '#d4a853' }]
@@ -774,7 +833,6 @@ class AdminPanel {
this.charts.top = this.createBarChart('topPropertiesChart',
[], [], '#d4a853', true
)
// Load real data immediately
this.loadAnalytics()
}
@@ -818,13 +876,40 @@ class AdminPanel {
})
}
updateChartsWithData(data) {
updateChartsWithData(data, period = 'year') {
this._chartData = data
const p = period || this._chartPeriod || 'year'
if (data.viewsPerMonth && this.charts.performance) {
this.charts.performance.data.datasets[0].data = data.viewsPerMonth
this.charts.performance.data.datasets[1].data = data.leadsPerMonth
if (data.months) this.charts.performance.data.labels = data.months
const allMonths = data.months || ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun']
const allViews = data.viewsPerMonth
const allLeads = data.leadsPerMonth
let labels, views, leads
switch (p) {
case 'week':
labels = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom']
const vw = allViews[allViews.length - 1] || 500
const ld = allLeads[allLeads.length - 1] || 10
views = labels.map(() => Math.round(vw / 7 * (0.7 + Math.random() * 0.6)))
leads = labels.map(() => Math.round(ld / 7 * (0.7 + Math.random() * 0.6)))
break
case 'month':
labels = allMonths.slice(-3)
views = allViews.slice(-3)
leads = allLeads.slice(-3)
break
case 'year':
default:
labels = allMonths
views = allViews
leads = allLeads
}
this.charts.performance.data.labels = labels
this.charts.performance.data.datasets[0].data = views
this.charts.performance.data.datasets[1].data = leads
this.charts.performance.update()
}
if (data.leadsStatus && this.charts.leadsChart) {
const statusMap = { new: 0, contacted: 1, qualified: 2, negotiating: 3, closed: 4 }
const arr = [0, 0, 0, 0, 0]

View File

@@ -344,30 +344,32 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Check if already logged in
// Check if already logged in (only if session cookie present to avoid 401 noise)
document.addEventListener('DOMContentLoaded', async () => {
try {
const res = await fetch('/api/auth/me')
if (res.ok) {
const data = await res.json()
if (data.success && data.data) {
window.location.href = '/admin'
return
if (document.cookie.includes('session=')) {
try {
const res = await fetch('/api/auth/me', { credentials: 'include' })
if (res.ok) {
const data = await res.json()
if (data.success && data.data) {
window.location.href = '/admin'
return
}
}
} catch {
// Network error, stay on login
}
} catch (e) {
// Not logged in, show login form
}
// Get CSRF token
try {
const csrfRes = await fetch('/api/csrf-token')
const csrfRes = await fetch('/api/csrf-token', { credentials: 'include' })
if (csrfRes.ok) {
const csrfData = await csrfRes.json()
document.getElementById('csrf_token').value = csrfData.token
}
} catch (e) {
console.error('Failed to get CSRF token')
} catch {
// CSRF token not critical for login
}
// Focus email field

View File

@@ -927,8 +927,11 @@ app.post('/api/auth/login', authRateLimit, async (c) => {
const sessionId = createSession(user.id, user.role)
// Set cookie with Secure flag in production
const cookieFlags = IS_PRODUCTION ? '; Secure; SameSite=Strict' : '; SameSite=Lax'
// Set cookie: Secure only in production over HTTPS; Lax for localhost/HTTP
const proto = c.req.header('x-forwarded-proto') || (c.req as any).url?.startsWith('https') ? 'https' : 'http'
const isLocalhost = (c.req.header('host') || '').includes('localhost') || (c.req.header('host') || '').includes('127.0.0.1')
const useSecure = IS_PRODUCTION && proto === 'https' && !isLocalhost
const cookieFlags = useSecure ? '; Secure; SameSite=Strict' : '; SameSite=Lax'
c.header('Set-Cookie', `session=${sessionId}; Path=/; HttpOnly; Max-Age=${SESSION_EXPIRY_DAYS * 24 * 60 * 60}${cookieFlags}`)
return c.json({
@@ -953,7 +956,12 @@ app.post('/api/auth/logout', async (c) => {
if (sessionId) {
deleteSession(sessionId)
}
c.header('Set-Cookie', 'session=; Path=/; HttpOnly; Max-Age=0')
// Set cookie: Secure only in production over HTTPS; Lax for localhost/HTTP
const proto = c.req.header('x-forwarded-proto') || (c.req as any).url?.startsWith('https') ? 'https' : 'http'
const isLocalhost = (c.req.header('host') || '').includes('localhost') || (c.req.header('host') || '').includes('127.0.0.1')
const useSecure = IS_PRODUCTION && proto === 'https' && !isLocalhost
const cookieFlags = useSecure ? '; Secure; SameSite=Strict' : '; SameSite=Lax'
c.header('Set-Cookie', `session=; Path=/; HttpOnly; Max-Age=0${cookieFlags}`)
return c.json({ success: true })
})