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:
parent
4d531b2f2d
commit
20a0785ec3
9 changed files with 692 additions and 0 deletions
140
app/api/kiosk/stats/route.ts
Normal file
140
app/api/kiosk/stats/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue