/**
* Dashboard routes — provider management pages + API.
*/
import { json, html, readJson, notFound } from '../response.js'
import { requireAuth } from './auth-helpers.js'
export function registerDashboardRoutes (router, server) {
// ---- API Endpoints ----
// Get provider for current user (or claim unclaimed)
router.get('/api/dashboard/provider', async (req, res) => {
const auth = await requireAuth(req, server, 'read')
let providerId = await getProviderIdForUser(server, auth.userId)
// Auto-claim removed — use admin panel to assign providers
if (!providerId) {
return json(res, { provider: null })
}
const providerEntry = await server.db.get(`provider:${providerId}`)
json(res, { provider: providerEntry ? providerEntry.value : null })
})
// Today's schedule + summary
router.get('/api/dashboard/today', async (req, res) => {
const auth = await requireAuth(req, server, 'write')
const providerId = await getProviderIdForUser(server, auth.userId)
if (!providerId) return json(res, { error: 'No provider profile' }, 403)
const today = new Date().toISOString().split('T')[0]
const bookings = await getBookingsForDate(server.db, providerId, today)
// Enrich with client + package info
for (const b of bookings) {
const clientEntry = await server.db.get(`client:${b.clientId}`)
b.clientName = clientEntry ? clientEntry.value.displayName : 'Unknown'
const pkgEntry = await server.db.get(`package-detail:${b.packageId}`)
b.packageName = pkgEntry ? pkgEntry.value.name : 'Unknown'
b.sessionDuration = pkgEntry ? pkgEntry.value.sessionDuration : 60
}
// Sort by start time
bookings.sort((a, b) => (a.startTimeLocal || '').localeCompare(b.startTimeLocal || ''))
// Summary
const active = bookings.filter(b => b.status !== 'cancelled')
const revenue = 0 // Placeholder until payments wired
json(res, {
date: today,
bookings,
summary: {
sessions: active.length,
clients: new Set(active.map(b => b.clientId)).size,
revenue
}
})
})
// Week overview (session counts per day)
router.get('/api/dashboard/week', async (req, res) => {
const auth = await requireAuth(req, server, 'write')
const providerId = await getProviderIdForUser(server, auth.userId)
if (!providerId) return json(res, { error: 'No provider profile' }, 403)
const url = new URL('http://x' + req.url)
const weekOf = url.searchParams.get('weekOf') || getMonday(new Date())
const days = {}
for (let i = 0; i < 7; i++) {
const date = addDays(weekOf, i)
const bookings = await getBookingsForDate(server.db, providerId, date)
const active = bookings.filter(b => b.status !== 'cancelled')
days[date] = { count: active.length, bookings: active }
}
json(res, { weekOf, days })
})
// Client list
router.get('/api/dashboard/clients', async (req, res) => {
const auth = await requireAuth(req, server, 'write')
const providerId = await getProviderIdForUser(server, auth.userId)
if (!providerId) return json(res, { error: 'No provider profile' }, 403)
const url = new URL('http://x' + req.url)
const search = (url.searchParams.get('q') || '').toLowerCase()
const clients = []
for await (const entry of server.db.createReadStream({
gte: `clients-by-provider:${providerId}:`,
lte: `clients-by-provider:${providerId}:~`
})) {
const clientEntry = await server.db.get(`client:${entry.value}`)
if (!clientEntry) continue
const client = clientEntry.value
if (search && !(client.displayName || '').toLowerCase().includes(search) &&
!(client.email || '').toLowerCase().includes(search)) continue
// Get their bookings with this provider
const bookings = []
for await (const bEntry of server.db.createReadStream({
gte: `bookings-by-client:${client.id}:`,
lte: `bookings-by-client:${client.id}:~`
})) {
const booking = await server.db.get(`booking:${bEntry.value}`)
if (booking && booking.value.providerId === providerId) {
bookings.push(booking.value)
}
}
client.bookingCount = bookings.length
client.lastSession = bookings
.filter(b => b.status === 'completed')
.sort((a, b) => (b.date || '').localeCompare(a.date || ''))[0]?.date || null
client.nextSession = bookings
.filter(b => b.status === 'confirmed' && b.date >= new Date().toISOString().split('T')[0])
.sort((a, b) => (a.date || '').localeCompare(b.date || ''))[0]?.date || null
clients.push(client)
}
json(res, clients)
})
// Client detail
router.get('/api/dashboard/clients/:clientId', async (req, res) => {
const auth = await requireAuth(req, server, 'write')
const providerId = await getProviderIdForUser(server, auth.userId)
if (!providerId) return json(res, { error: 'No provider profile' }, 403)
const clientEntry = await server.db.get(`client:${req.params.clientId}`)
if (!clientEntry) return notFound(res, 'Client not found')
const client = clientEntry.value
// All bookings for this client with this provider
const bookings = []
for await (const bEntry of server.db.createReadStream({
gte: `bookings-by-client:${client.id}:`,
lte: `bookings-by-client:${client.id}:~`
})) {
const booking = await server.db.get(`booking:${bEntry.value}`)
if (booking && booking.value.providerId === providerId) {
const b = booking.value
const pkgEntry = await server.db.get(`package-detail:${b.packageId}`)
b.packageName = pkgEntry ? pkgEntry.value.name : 'Unknown'
bookings.push(b)
}
}
bookings.sort((a, b) => (b.date || '').localeCompare(a.date || ''))
json(res, { client, bookings })
})
// Analytics
router.get('/api/dashboard/analytics', async (req, res) => {
const auth = await requireAuth(req, server, 'write')
const providerId = await getProviderIdForUser(server, auth.userId)
if (!providerId) return json(res, { error: 'No provider profile' }, 403)
// Count all bookings by status
let completed = 0
let cancelled = 0
let noShow = 0
let confirmed = 0
const packageCounts = {}
for await (const entry of server.db.createReadStream({
gte: `bookings-by-provider:${providerId}:`,
lte: `bookings-by-provider:${providerId}:~`
})) {
const bEntry = await server.db.get(`booking:${entry.value}`)
if (!bEntry) continue
const b = bEntry.value
if (b.status === 'completed') completed++
else if (b.status === 'cancelled') cancelled++
else if (b.status === 'no-show') noShow++
else if (b.status === 'confirmed') confirmed++
if (!packageCounts[b.packageId]) packageCounts[b.packageId] = { count: 0 }
packageCounts[b.packageId].count++
}
// Enrich package counts with names
for (const [pkgId, data] of Object.entries(packageCounts)) {
const pkgEntry = await server.db.get(`package-detail:${pkgId}`)
data.name = pkgEntry ? pkgEntry.value.name : 'Unknown'
data.price = pkgEntry ? pkgEntry.value.price : 0
}
// Count clients
let clientCount = 0
for await (const _ of server.db.createReadStream({
gte: `clients-by-provider:${providerId}:`,
lte: `clients-by-provider:${providerId}:~`
})) {
clientCount++
}
json(res, {
sessions: { completed, cancelled, noShow, confirmed },
clients: clientCount,
packages: packageCounts
})
})
// Update profile (any user)
router.put('/api/profile', async (req, res) => {
const auth = await requireAuth(req, server, 'read')
const body = await readJson(req)
const { displayName, email, phone } = body
// Update user record in identity registry
const userEntry = await server.db.get(`user:${auth.userId}`)
const user = userEntry ? userEntry.value : { userId: auth.userId, createdAt: Date.now() }
if (displayName !== undefined) user.displayName = displayName
if (email !== undefined) user.email = email
if (phone !== undefined) user.phone = phone
user.updatedAt = Date.now()
await server.db.put(`user:${auth.userId}`, user)
// Also update username in identity registry if available
if (displayName && server.authManager.identityRegistry) {
try {
await server.authManager.identityRegistry.setUsername(auth.userId, displayName)
} catch {}
}
json(res, user)
})
// Get profile
router.get('/api/profile', async (req, res) => {
const auth = await requireAuth(req, server, 'read')
const userEntry = await server.db.get(`user:${auth.userId}`)
json(res, userEntry ? userEntry.value : { userId: auth.userId })
})
// ---- Admin API ----
// List all users with their linked provider
router.get('/api/admin/users', async (req, res) => {
const auth = await requireAuth(req, server, 'admin')
const users = []
for await (const entry of server.db.createReadStream({ gte: 'user:', lte: 'user:~' })) {
const u = entry.value
// Find linked provider
let provider = null
for await (const pe of server.db.createReadStream({ gte: 'provider:', lte: 'provider:~' })) {
if (pe.value.userId === u.userId) { provider = pe.value; break }
}
// Find identities
const identities = []
for await (const ie of server.db.createReadStream({ gte: 'identity:', lte: 'identity:~' })) {
if (ie.value.userId === u.userId) identities.push({ id: ie.value.identityId, publicKey: ie.value.publicKey, method: ie.value.method })
}
users.push({
userId: u.userId,
username: u.username,
name: u.name || u.displayName,
email: u.email,
globalPermissions: u.globalPermissions,
provider: provider ? { id: provider.id, slug: provider.slug, displayName: provider.displayName } : null,
identities,
createdAt: u.createdAt
})
}
json(res, users)
})
// List all providers with owner info
router.get('/api/admin/providers', async (req, res) => {
const auth = await requireAuth(req, server, 'admin')
const providers = []
for await (const entry of server.db.createReadStream({ gte: 'provider:', lte: 'provider:~' })) {
const p = entry.value
// Find owner
let owner = null
if (p.userId) {
const userEntry = await server.db.get(`user:${p.userId}`)
if (userEntry) owner = { userId: userEntry.value.userId, username: userEntry.value.username, name: userEntry.value.name || userEntry.value.displayName }
}
// Count bookings
let bookingCount = 0
for await (const _ of server.db.createReadStream({ gte: `bookings-by-provider:${p.id}:`, lte: `bookings-by-provider:${p.id}:~` })) {
bookingCount++
}
// Gcal status
const gcalEntry = await server.db.get(`gcal:provider:${p.id}`)
const gcalConnected = gcalEntry ? gcalEntry.value.connected : false
providers.push({ ...p, owner, bookingCount, gcalConnected })
}
json(res, providers)
})
// Unlink provider from user (set userId to null)
router.put('/api/admin/providers/:providerId/unlink', async (req, res) => {
const auth = await requireAuth(req, server, 'admin')
const entry = await server.db.get(`provider:${req.params.providerId}`)
if (!entry) return notFound(res, 'Provider not found')
const provider = entry.value
const oldUserId = provider.userId
provider.userId = null
provider.updatedAt = Date.now()
await server.db.put(`provider:${provider.id}`, provider)
// Remove user→provider index
if (oldUserId) await server.db.del(`provider-by-user:${oldUserId}`)
json(res, { ok: true, provider })
})
// Assign provider to user
router.put('/api/admin/providers/:providerId/assign', async (req, res) => {
const auth = await requireAuth(req, server, 'admin')
const body = await readJson(req)
const { userId } = body
if (!userId) return json(res, { error: 'userId required' }, 400)
const entry = await server.db.get(`provider:${req.params.providerId}`)
if (!entry) return notFound(res, 'Provider not found')
const userEntry = await server.db.get(`user:${userId}`)
if (!userEntry) return notFound(res, 'User not found')
const provider = entry.value
// Remove old user index if reassigning
if (provider.userId) await server.db.del(`provider-by-user:${provider.userId}`)
provider.userId = userId
provider.updatedAt = Date.now()
await server.db.put(`provider:${provider.id}`, provider)
// Add new user→provider index
await server.db.put(`provider-by-user:${userId}`, provider.id)
json(res, { ok: true, provider })
})
// Toggle provider active/disabled
router.put('/api/admin/providers/:providerId/toggle', async (req, res) => {
const auth = await requireAuth(req, server, 'admin')
const entry = await server.db.get(`provider:${req.params.providerId}`)
if (!entry) return notFound(res, 'Provider not found')
const provider = entry.value
provider.active = !provider.active
provider.updatedAt = Date.now()
await server.db.put(`provider:${provider.id}`, provider)
json(res, { ok: true, active: provider.active })
})
// Update user permissions
router.put('/api/admin/users/:userId/permissions', async (req, res) => {
const auth = await requireAuth(req, server, 'admin')
const body = await readJson(req)
const { permissions } = body
if (!Array.isArray(permissions)) return json(res, { error: 'permissions array required' }, 400)
const entry = await server.db.get(`user:${req.params.userId}`)
if (!entry) return notFound(res, 'User not found')
const user = entry.value
user.globalPermissions = permissions
user.notebooks = [{ name: '*', permissions }]
user.updatedAt = Date.now()
await server.db.put(`user:${user.userId}`, user)
json(res, { ok: true, user })
})
// System info
router.get('/api/admin/system', async (req, res) => {
const auth = await requireAuth(req, server, 'admin')
let userCount = 0
for await (const _ of server.db.createReadStream({ gte: 'user:', lte: 'user:~' })) userCount++
let providerCount = 0
for await (const _ of server.db.createReadStream({ gte: 'provider:', lte: 'provider:~' })) providerCount++
let bookingCount = 0
for await (const _ of server.db.createReadStream({ gte: 'booking:', lte: 'booking:~' })) bookingCount++
json(res, {
version: '0.1.0',
uptime: process.uptime ? process.uptime() : 0,
users: userCount,
providers: providerCount,
bookings: bookingCount,
gcalConfigured: server.gcalConfigured
})
})
// ---- HTML Pages ----
// Dashboard home
router.get('/dashboard', async (req, res) => {
serveDashboardPage(res, server, 'home')
})
router.get('/dashboard/clients', async (req, res) => {
serveDashboardPage(res, server, 'clients')
})
router.get('/dashboard/clients/:clientId', async (req, res) => {
serveDashboardPage(res, server, 'client-detail', req.params)
})
router.get('/dashboard/packages', async (req, res) => {
serveDashboardPage(res, server, 'packages')
})
router.get('/dashboard/packages/new', async (req, res) => {
serveDashboardPage(res, server, 'package-edit')
})
router.get('/dashboard/packages/:id/edit', async (req, res) => {
serveDashboardPage(res, server, 'package-edit', req.params)
})
router.get('/dashboard/availability', async (req, res) => {
serveDashboardPage(res, server, 'availability')
})
router.get('/dashboard/analytics', async (req, res) => {
serveDashboardPage(res, server, 'analytics')
})
router.get('/dashboard/settings', async (req, res) => {
serveDashboardPage(res, server, 'settings')
})
// Profile page (any authenticated user)
router.get('/profile', async (req, res) => {
serveDashboardPage(res, server, 'profile')
})
// Client portal
router.get('/portal', async (req, res) => {
serveDashboardPage(res, server, 'portal')
})
// Admin pages
router.get('/admin', async (req, res) => {
serveDashboardPage(res, server, 'admin')
})
router.get('/admin/providers', async (req, res) => {
serveDashboardPage(res, server, 'admin-providers')
})
router.get('/admin/users', async (req, res) => {
serveDashboardPage(res, server, 'admin-users')
})
}
// ---- Helpers ----
async function getProviderIdForUser (server, userId) {
// Fast path: use index
const indexed = await server.db.get(`provider-by-user:${userId}`)
if (indexed) return indexed.value
// Fallback: scan (for providers created before index existed)
for await (const entry of server.db.createReadStream({
gte: 'provider:',
lte: 'provider:~'
})) {
if (entry.value.userId === userId) {
// Backfill index
await server.db.put(`provider-by-user:${userId}`, entry.value.id)
return entry.value.id
}
}
return null
}
async function getBookingsForDate (db, providerId, date) {
const bookings = []
for await (const entry of db.createReadStream({
gte: `bookings-by-date:${providerId}:${date}:`,
lte: `bookings-by-date:${providerId}:${date}:~`
})) {
const bookingEntry = await db.get(`booking:${entry.value}`)
if (bookingEntry) bookings.push(bookingEntry.value)
}
return bookings
}
function getMonday (d) {
const date = new Date(d)
const day = date.getDay()
const diff = date.getDate() - day + (day === 0 ? -6 : 1)
date.setDate(diff)
return date.toISOString().split('T')[0]
}
function addDays (dateStr, days) {
const d = new Date(dateStr + 'T12:00:00Z')
d.setUTCDate(d.getUTCDate() + days)
return d.toISOString().split('T')[0]
}
// ---- Page Renderer ----
function serveDashboardPage (res, server, view, params = {}) {
const prefix = server.pathPrefix
const page = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard — hyper-booking</title>
<link rel="stylesheet" href="${prefix}/web/css/app.css">
<link rel="stylesheet" href="${prefix}/web/css/dashboard.css">
</head>
<body>
<div class="dashboard-layout">
<nav class="dashboard-sidebar" id="sidebar">
<div class="sidebar-header">
<a href="${prefix}/dashboard" class="sidebar-logo">hyper-booking</a>
</div>
<div class="sidebar-nav">
<a href="${prefix}/dashboard" class="nav-item ${view === 'home' ? 'active' : ''}">
<span class="nav-icon">📅</span> Calendar
</a>
<a href="${prefix}/dashboard/clients" class="nav-item ${view === 'clients' || view === 'client-detail' ? 'active' : ''}">
<span class="nav-icon">👥</span> Clients
</a>
<a href="${prefix}/dashboard/packages" class="nav-item ${view === 'packages' || view === 'package-edit' ? 'active' : ''}">
<span class="nav-icon">📦</span> Packages
</a>
<a href="${prefix}/dashboard/availability" class="nav-item ${view === 'availability' ? 'active' : ''}">
<span class="nav-icon">🕔</span> Availability
</a>
<a href="${prefix}/dashboard/analytics" class="nav-item ${view === 'analytics' ? 'active' : ''}">
<span class="nav-icon">📈</span> Analytics
</a>
<a href="${prefix}/dashboard/settings" class="nav-item ${view === 'settings' ? 'active' : ''}">
<span class="nav-icon">⚙</span> Settings
</a>
<div class="sidebar-divider"></div>
<a href="${prefix}/portal" class="nav-item ${view === 'portal' ? 'active' : ''}">
<span class="nav-icon">👤</span> Client Portal
</a>
<a href="${prefix}/profile" class="nav-item ${view === 'profile' ? 'active' : ''}">
<span class="nav-icon">🔐</span> Profile
</a>
<a href="${prefix}/admin" class="nav-item ${view === 'admin' || view.startsWith('admin') ? 'active' : ''}" id="admin-link">
<span class="nav-icon">🛡</span> Admin
</a>
</div>
</nav>
<main class="dashboard-main">
<header class="dashboard-header">
<button class="sidebar-toggle" onclick="document.getElementById('sidebar').classList.toggle('open')">☰</button>
<span id="header-title">${viewTitle(view)}</span>
<div class="header-right">
<span id="user-name"></span>
</div>
</header>
<div class="dashboard-content" id="content">
<p class="empty">Loading...</p>
</div>
</main>
</div>
<script src="${prefix}/web/js/identity.js"></script>
<script src="${prefix}/web/js/app.js"></script>
<script src="${prefix}/web/js/dashboard.js"></script>
<script>
initDashboard('${view}', ${JSON.stringify(params)})
</script>
</body>
</html>`
html(res, page)
}
function viewTitle (view) {
const titles = {
home: 'Calendar',
clients: 'Clients',
'client-detail': 'Client Detail',
packages: 'Packages',
'package-edit': 'Edit Package',
availability: 'Availability',
analytics: 'Analytics',
settings: 'Settings',
profile: 'Profile',
portal: 'Client Portal',
admin: 'Admin',
'admin-providers': 'Manage Providers',
'admin-users': 'Manage Users'
}
return titles[view] || 'Dashboard'
}