import { Router } from "express"; import { db } from "../db.ts"; import type { AuthedRequest } from "../lib/auth.ts"; import { ownerOnly } from "../lib/auth.ts"; export const dashboardRouter = Router(); dashboardRouter.use(ownerOnly); dashboardRouter.get("/overview", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const scalar = (sql: string, ...args: any[]) => (db.prepare(sql).get(...args) as any)?.v ?? 0; const revenue_today = scalar( `SELECT COALESCE(SUM(amount+tip),0) v FROM tickets WHERE business_id = ? AND date(created_at) = date('now')`, bid ); const revenue_week = scalar( `SELECT COALESCE(SUM(amount+tip),0) v FROM tickets WHERE business_id = ? AND created_at >= datetime('now','-7 days')`, bid ); const revenue_month = scalar( `SELECT COALESCE(SUM(amount+tip),0) v FROM tickets WHERE business_id = ? AND created_at >= datetime('now','-30 days')`, bid ); const revenue_prev = scalar( `SELECT COALESCE(SUM(amount+tip),0) v FROM tickets WHERE business_id = ? AND created_at >= datetime('now','-60 days') AND created_at < datetime('now','-30 days')`, bid ); const revenue_change_pct = revenue_prev > 0 ? Math.round(((revenue_month - revenue_prev) / revenue_prev) * 1000) / 10 : 0; const appts_today = scalar( `SELECT COUNT(*) v FROM appointments WHERE business_id = ? AND date(start_at) = date('now')`, bid ); const appts_week = scalar( `SELECT COUNT(*) v FROM appointments WHERE business_id = ? AND start_at >= datetime('now','-7 days')`, bid ); const appts_upcoming = scalar( `SELECT COUNT(*) v FROM appointments WHERE business_id = ? AND status = 'scheduled' AND start_at >= datetime('now')`, bid ); const active_clients = scalar( `SELECT COUNT(DISTINCT client_id) v FROM tickets WHERE business_id = ? AND created_at >= datetime('now','-30 days')`, bid ); const avg_ticket = scalar( `SELECT COALESCE(AVG(amount+tip),0) v FROM tickets WHERE business_id = ? AND created_at >= datetime('now','-30 days')`, bid ); const total_range = scalar( `SELECT COUNT(*) v FROM appointments WHERE business_id = ? AND created_at >= datetime('now','-30 days')`, bid ); const cancels = scalar( `SELECT COUNT(*) v FROM appointments WHERE business_id = ? AND status IN ('cancelled','no_show') AND created_at >= datetime('now','-30 days')`, bid ); const cancel_rate = total_range > 0 ? Math.round((cancels / total_range) * 1000) / 10 : 0; const completed = scalar( `SELECT COUNT(*) v FROM appointments WHERE business_id = ? AND status = 'completed' AND created_at >= datetime('now','-30 days')`, bid ); const occupancy_pct = total_range > 0 ? Math.round((completed / total_range) * 100) : 0; res.json({ revenue_today: Math.round(revenue_today * 100) / 100, revenue_week: Math.round(revenue_week * 100) / 100, revenue_month: Math.round(revenue_month * 100) / 100, revenue_change_pct, appts_today, appts_week, appts_upcoming, active_clients, avg_ticket: Math.round(avg_ticket * 100) / 100, cancel_rate, occupancy_pct, }); }); dashboardRouter.get("/best-employees", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const range = (req.query.range as string) || "30"; const rows = db .prepare( `SELECT e.id, e.name, e.color, e.role, COUNT(t.id) appts, COALESCE(SUM(t.amount+t.tip),0) revenue, COALESCE((SELECT AVG(rating) FROM reviews WHERE employee_id = e.id), e.rating) avg_rating FROM employees e LEFT JOIN tickets t ON t.employee_id = e.id AND t.created_at >= datetime('now','-${Number(range)} days') WHERE e.business_id = ? GROUP BY e.id ORDER BY revenue DESC` ) .all(bid) as any[]; const totalAppts = rows.reduce((a, r) => a + r.appts, 0); const out = rows.map((r) => ({ employee: { id: r.id, name: r.name, color: r.color, role: r.role }, appointments: r.appts, revenue: Math.round(r.revenue * 100) / 100, avg_rating: Math.round(r.avg_rating * 10) / 10, utilization_pct: totalAppts > 0 ? Math.round((r.appts / totalAppts) * 100) : 0, })); res.json({ employees: out }); }); dashboardRouter.get("/best-services", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const range = (req.query.range as string) || "30"; const rows = db .prepare( `SELECT s.id, s.name, s.category, s.color, COUNT(t.id) count, COALESCE(SUM(t.amount+t.tip),0) revenue FROM services s LEFT JOIN tickets t ON t.service_id = s.id AND t.created_at >= datetime('now','-${Number(range)} days') WHERE s.business_id = ? GROUP BY s.id ORDER BY revenue DESC` ) .all(bid) as any[]; const out = rows.map((r) => ({ service: { id: r.id, name: r.name, category: r.category, color: r.color }, count: r.count, revenue: Math.round(r.revenue * 100) / 100, })); res.json({ services: out }); }); dashboardRouter.get("/top-tickets", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const limit = Math.min(50, Number(req.query.limit) || 10); const range = (req.query.range as string) || "30"; const rows = db .prepare( `SELECT t.*, s.name service_name, s.category service_category, s.color service_color, e.name employee_name, e.color employee_color, c.name client_name FROM tickets t JOIN services s ON s.id = t.service_id JOIN employees e ON e.id = t.employee_id JOIN clients c ON c.id = t.client_id WHERE t.business_id = ? AND t.created_at >= datetime('now','-${Number(range)} days') ORDER BY (t.amount + t.tip) DESC LIMIT ?` ) .all(bid, limit) as any[]; const out = rows.map((t) => ({ ticket: { id: t.id, business_id: t.business_id, appointment_id: t.appointment_id, client_id: t.client_id, employee_id: t.employee_id, service_id: t.service_id, amount: t.amount, tip: t.tip, payment_method: t.payment_method, created_at: t.created_at, service: { id: t.service_id, name: t.service_name, category: t.service_category, color: t.service_color }, employee: { id: t.employee_id, name: t.employee_name, color: t.employee_color }, client: { id: t.client_id, name: t.client_name }, }, })); res.json({ tickets: out }); }); dashboardRouter.get("/top-clients", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const limit = Math.min(50, Number(req.query.limit) || 10); const range = (req.query.range as string) || "30"; const rows = db .prepare( `SELECT c.id, c.name, c.tags, c.phone, COUNT(t.id) visits, COALESCE(SUM(t.amount+t.tip),0) total, MAX(t.created_at) last_visit FROM clients c LEFT JOIN tickets t ON t.client_id = c.id AND t.created_at >= datetime('now','-${Number(range)} days') WHERE c.business_id = ? GROUP BY c.id HAVING visits > 0 ORDER BY total DESC LIMIT ?` ) .all(bid, limit) as any[]; const out = rows.map((r) => ({ client: { id: r.id, name: r.name, tags: r.tags, phone: r.phone }, visits: r.visits, total_spent: Math.round(r.total * 100) / 100, last_visit: r.last_visit, })); res.json({ clients: out }); }); dashboardRouter.get("/frequent-clients", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const limit = Math.min(50, Number(req.query.limit) || 10); const range = (req.query.range as string) || "30"; const rows = db .prepare( `SELECT c.id, c.name, c.tags, c.phone, COUNT(t.id) visits, COALESCE(SUM(t.amount+t.tip),0) total, MAX(t.created_at) last_visit FROM clients c JOIN tickets t ON t.client_id = c.id AND t.created_at >= datetime('now','-${Number(range)} days') WHERE c.business_id = ? GROUP BY c.id ORDER BY visits DESC, total DESC LIMIT ?` ) .all(bid, limit) as any[]; const out = rows.map((r) => ({ client: { id: r.id, name: r.name, tags: r.tags, phone: r.phone }, visits: r.visits, total_spent: Math.round(r.total * 100) / 100, last_visit: r.last_visit, })); res.json({ clients: out }); }); dashboardRouter.get("/revenue-trend", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const days = Math.min(120, Number(req.query.days) || 30); const rows = db .prepare( `SELECT date(created_at) date, COALESCE(SUM(amount+tip),0) revenue, COUNT(*) appts FROM tickets WHERE business_id = ? AND created_at >= datetime('now','-${days} days') GROUP BY date(created_at) ORDER BY date ASC` ) .all(bid) as any[]; // Fill missing days with 0 const map = new Map(rows.map((r) => [r.date, r])); const out: { date: string; revenue: number; appts: number }[] = []; const today = new Date(); for (let i = days - 1; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); const key = d.toISOString().slice(0, 10); const row = map.get(key); out.push({ date: key, revenue: row ? Math.round(row.revenue * 100) / 100 : 0, appts: row ? row.appts : 0, }); } res.json({ points: out }); }); dashboardRouter.get("/revenue-by-category", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const range = (req.query.range as string) || "30"; const rows = db .prepare( `SELECT s.category, COUNT(t.id) count, COALESCE(SUM(t.amount+t.tip),0) revenue FROM tickets t JOIN services s ON s.id = t.service_id WHERE t.business_id = ? AND t.created_at >= datetime('now','-${Number(range)} days') GROUP BY s.category ORDER BY revenue DESC` ) .all(bid) as any[]; const out = rows.map((r) => ({ category: r.category, count: r.count, revenue: Math.round(r.revenue * 100) / 100, })); res.json({ slices: out }); }); dashboardRouter.get("/tickets", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const limit = Math.min(200, Number(req.query.limit) || 50); const rows = db .prepare( `SELECT t.*, s.name service_name, s.category service_category, s.color service_color, e.name employee_name, e.color employee_color, c.name client_name FROM tickets t JOIN services s ON s.id = t.service_id JOIN employees e ON e.id = t.employee_id JOIN clients c ON c.id = t.client_id WHERE t.business_id = ? ORDER BY t.created_at DESC LIMIT ?` ) .all(bid, limit) as any[]; const out = rows.map((t) => ({ id: t.id, business_id: t.business_id, appointment_id: t.appointment_id, client_id: t.client_id, employee_id: t.employee_id, service_id: t.service_id, amount: t.amount, tip: t.tip, commission: t.commission, payment_method: t.payment_method, created_at: t.created_at, service: { id: t.service_id, name: t.service_name, category: t.service_category, color: t.service_color }, employee: { id: t.employee_id, name: t.employee_name, color: t.employee_color }, client: { id: t.client_id, name: t.client_name }, })); res.json({ tickets: out }); }); dashboardRouter.get("/commissions", (req: AuthedRequest, res) => { const bid = req.user!.business_id; const range = (req.query.range as string) || "30"; const rows = db .prepare( `SELECT e.id, e.name, e.color, e.role, COUNT(t.id) sales, COALESCE(SUM(t.amount + t.tip), 0) revenue, COALESCE(SUM(t.commission), 0) commission FROM employees e LEFT JOIN tickets t ON t.employee_id = e.id AND t.created_at >= datetime('now','-${Number(range)} days') WHERE e.business_id = ? GROUP BY e.id ORDER BY commission DESC` ) .all(bid) as any[]; const out = rows.map((r) => ({ employee: { id: r.id, name: r.name, color: r.color, role: r.role }, sales: r.sales, revenue: Math.round(r.revenue * 100) / 100, commission: Math.round(r.commission * 100) / 100, })); const total = out.reduce((a, r) => a + r.commission, 0); res.json({ rows: out, total: Math.round(total * 100) / 100 }); });