feat: add executive kiosk dashboard for TV display

- Created /kiosk route with full-screen display
- Added API endpoints for stats and activity feed
- Built cycling KPI display with 12 metrics
- Added scrolling ticker for ticket activity
- Implemented UI-configurable cycle interval (3-15s, default 7s)
- Focus on critical tickets and business metrics
- Added navigation link from dashboard
- Dark theme optimized for TV viewing
This commit is contained in:
root 2026-02-02 23:36:35 -05:00
parent 4d531b2f2d
commit 20a0785ec3
9 changed files with 692 additions and 0 deletions

View file

@ -0,0 +1,140 @@
import { NextRequest, NextResponse } from 'next/server';
import { postgresClient } from '@/lib/services/postgres-client';
export async function GET(request: NextRequest) {
try {
// Critical tickets (Priority 1-3)
const criticalTicketsResult = await postgresClient.query(
`SELECT COUNT(*) as count
FROM tickets
WHERE completed_date IS NULL
AND priority <= 3`
);
// Tickets waiting engagement (specific statuses)
const waitingTicketsResult = await postgresClient.query(
`SELECT COUNT(*) as count
FROM tickets
WHERE completed_date IS NULL
AND status IN (21, 9, 19)`
);
// Stale tickets (no activity in 7+ days)
const staleTicketsResult = await postgresClient.query(
`SELECT COUNT(*) as count
FROM tickets
WHERE completed_date IS NULL
AND last_activity_date < NOW() - INTERVAL '7 days'`
);
// Overdue tickets
const overdueTicketsResult = await postgresClient.query(
`SELECT COUNT(*) as count
FROM tickets
WHERE completed_date IS NULL
AND due_date_time < NOW()`
);
// Total open tickets
const openTicketsResult = await postgresClient.query(
`SELECT COUNT(*) as count
FROM tickets
WHERE completed_date IS NULL`
);
// Tickets closed today
const closedTodayResult = await postgresClient.query(
`SELECT COUNT(*) as count
FROM tickets
WHERE DATE(completed_date) = CURRENT_DATE`
);
// Average resolution time (last 30 days)
const avgResolutionResult = await postgresClient.query(
`SELECT AVG(EXTRACT(EPOCH FROM (completed_date - create_date))/3600) as avg_hours
FROM tickets
WHERE completed_date >= NOW() - INTERVAL '30 days'
AND completed_date IS NOT NULL`
);
// Time entries this week
const timeEntriesResult = await postgresClient.query(
`SELECT COALESCE(SUM(hours_worked), 0) as hours
FROM time_entries
WHERE date_worked >= DATE_TRUNC('week', CURRENT_DATE)`
);
// Active companies
const companiesResult = await postgresClient.query(
`SELECT COUNT(*) as count
FROM companies
WHERE is_active = true AND company_type = 1`
);
// Quotes (if table exists)
let quotesOpen = 0;
try {
const quotesResult = await postgresClient.query(
`SELECT COUNT(*) as count
FROM quotes
WHERE status = 'open'`
);
quotesOpen = parseInt(quotesResult.rows[0]?.count || '0');
} catch {
// Quotes table may not exist
}
// NMS Coverage
let nmsPercentage = 0;
try {
const nmsResult = await postgresClient.query(
`SELECT
(SELECT COUNT(*) FROM auvik_tenant_mappings WHERE autotask_company_id IS NOT NULL) as mapped,
(SELECT COUNT(*) FROM auvik_tenants) as total`
);
const mapped = parseInt(nmsResult.rows[0]?.mapped || '0');
const total = parseInt(nmsResult.rows[0]?.total || '0');
nmsPercentage = total > 0 ? Math.round((mapped / total) * 100) : 0;
} catch {
// Tables may not exist
}
// RMM Coverage
let rmmPercentage = 0;
try {
const rmmResult = await postgresClient.query(
`SELECT
(SELECT COUNT(*) FROM rmm_site_mappings WHERE company_id IS NOT NULL) as mapped,
(SELECT COUNT(*) FROM rmm_sites) as total`
);
const mapped = parseInt(rmmResult.rows[0]?.mapped || '0');
const total = parseInt(rmmResult.rows[0]?.total || '0');
rmmPercentage = total > 0 ? Math.round((mapped / total) * 100) : 0;
} catch {
// Tables may not exist
}
const stats = {
criticalTickets: parseInt(criticalTicketsResult.rows[0]?.count || '0'),
waitingTickets: parseInt(waitingTicketsResult.rows[0]?.count || '0'),
staleTickets: parseInt(staleTicketsResult.rows[0]?.count || '0'),
overdueTickets: parseInt(overdueTicketsResult.rows[0]?.count || '0'),
openTickets: parseInt(openTicketsResult.rows[0]?.count || '0'),
closedToday: parseInt(closedTodayResult.rows[0]?.count || '0'),
avgResolutionHours: parseFloat(avgResolutionResult.rows[0]?.avg_hours || '0'),
hoursThisWeek: parseFloat(timeEntriesResult.rows[0]?.hours || '0'),
activeCompanies: parseInt(companiesResult.rows[0]?.count || '0'),
openQuotes: quotesOpen,
nmsCoverage: nmsPercentage,
rmmCoverage: rmmPercentage,
};
return NextResponse.json(stats);
} catch (error) {
console.error('Error fetching kiosk stats:', error);
return NextResponse.json(
{ error: 'Failed to fetch kiosk stats' },
{ status: 500 }
);
}
}