- 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
140 lines
4.6 KiB
TypeScript
140 lines
4.6 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|