diff --git a/app/api/kiosk/stats/route.ts b/app/api/kiosk/stats/route.ts index 7d00eb9..50a07e5 100644 --- a/app/api/kiosk/stats/route.ts +++ b/app/api/kiosk/stats/route.ts @@ -12,6 +12,18 @@ export async function GET(request: NextRequest) { AND (source IS NULL OR source != 8)` ); + // Top 3 critical tickets + const topCriticalResult = await postgresClient.query( + `SELECT t.ticket_number, t.title, c.company_name, t.priority + FROM tickets t + LEFT JOIN companies c ON t.company_id = c.id + WHERE t.completed_date IS NULL + AND t.priority <= 3 + AND (t.source IS NULL OR t.source != 8) + ORDER BY t.priority ASC, t.create_date ASC + LIMIT 3` + ); + // Tickets waiting engagement (specific statuses) - exclude RMM alerts const waitingTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count @@ -21,6 +33,18 @@ export async function GET(request: NextRequest) { AND (source IS NULL OR source != 8)` ); + // Top 3 waiting tickets + const topWaitingResult = await postgresClient.query( + `SELECT t.ticket_number, t.title, c.company_name, t.last_activity_date + FROM tickets t + LEFT JOIN companies c ON t.company_id = c.id + WHERE t.completed_date IS NULL + AND t.status IN (21, 9, 19) + AND (t.source IS NULL OR t.source != 8) + ORDER BY t.last_activity_date ASC NULLS FIRST + LIMIT 3` + ); + // Stale tickets (no activity in 7+ days) - exclude RMM alerts const staleTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count @@ -30,6 +54,18 @@ export async function GET(request: NextRequest) { AND (source IS NULL OR source != 8)` ); + // Top 3 stale tickets (oldest activity first) + const topStaleResult = await postgresClient.query( + `SELECT t.ticket_number, t.title, c.company_name, t.last_activity_date + FROM tickets t + LEFT JOIN companies c ON t.company_id = c.id + WHERE t.completed_date IS NULL + AND t.last_activity_date < NOW() - INTERVAL '7 days' + AND (t.source IS NULL OR t.source != 8) + ORDER BY t.last_activity_date ASC NULLS FIRST + LIMIT 3` + ); + // Overdue tickets - exclude RMM alerts const overdueTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count @@ -39,6 +75,18 @@ export async function GET(request: NextRequest) { AND (source IS NULL OR source != 8)` ); + // Top 3 overdue tickets (most overdue first) + const topOverdueResult = await postgresClient.query( + `SELECT t.ticket_number, t.title, c.company_name, t.due_date_time + FROM tickets t + LEFT JOIN companies c ON t.company_id = c.id + WHERE t.completed_date IS NULL + AND t.due_date_time < NOW() + AND (t.source IS NULL OR t.source != 8) + ORDER BY t.due_date_time ASC + LIMIT 3` + ); + // Total open tickets - exclude RMM alerts const openTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count @@ -55,6 +103,17 @@ export async function GET(request: NextRequest) { AND (source IS NULL OR source != 8)` ); + // Top 3 recently closed tickets + const topClosedResult = await postgresClient.query( + `SELECT t.ticket_number, t.title, c.company_name, t.completed_date + FROM tickets t + LEFT JOIN companies c ON t.company_id = c.id + WHERE DATE(t.completed_date) = CURRENT_DATE + AND (t.source IS NULL OR t.source != 8) + ORDER BY t.completed_date DESC + LIMIT 3` + ); + // Average resolution time (last 30 days) - exclude RMM alerts const avgResolutionResult = await postgresClient.query( `SELECT AVG(EXTRACT(EPOCH FROM (completed_date - create_date))/3600) as avg_hours @@ -123,11 +182,36 @@ export async function GET(request: NextRequest) { const stats = { criticalTickets: parseInt(criticalTicketsResult.rows[0]?.count || '0'), + topCritical: topCriticalResult.rows.map((r: any) => ({ + ticketNumber: r.ticket_number, + title: r.title, + companyName: r.company_name, + })), waitingTickets: parseInt(waitingTicketsResult.rows[0]?.count || '0'), + topWaiting: topWaitingResult.rows.map((r: any) => ({ + ticketNumber: r.ticket_number, + title: r.title, + companyName: r.company_name, + })), staleTickets: parseInt(staleTicketsResult.rows[0]?.count || '0'), + topStale: topStaleResult.rows.map((r: any) => ({ + ticketNumber: r.ticket_number, + title: r.title, + companyName: r.company_name, + })), overdueTickets: parseInt(overdueTicketsResult.rows[0]?.count || '0'), + topOverdue: topOverdueResult.rows.map((r: any) => ({ + ticketNumber: r.ticket_number, + title: r.title, + companyName: r.company_name, + })), openTickets: parseInt(openTicketsResult.rows[0]?.count || '0'), closedToday: parseInt(closedTodayResult.rows[0]?.count || '0'), + topClosed: topClosedResult.rows.map((r: any) => ({ + ticketNumber: r.ticket_number, + title: r.title, + companyName: r.company_name, + })), avgResolutionHours: parseFloat(avgResolutionResult.rows[0]?.avg_hours || '0'), hoursThisWeek: parseFloat(timeEntriesResult.rows[0]?.hours || '0'), activeCompanies: parseInt(companiesResult.rows[0]?.count || '0'), diff --git a/app/kiosk/page.tsx b/app/kiosk/page.tsx index dd7096e..89d7199 100644 --- a/app/kiosk/page.tsx +++ b/app/kiosk/page.tsx @@ -5,13 +5,24 @@ import { CyclingDisplay } from '@/components/kiosk/cycling-display'; import { Ticker } from '@/components/kiosk/ticker'; import { SettingsPanel } from '@/components/kiosk/settings-panel'; +interface TicketItem { + ticketNumber: string; + title: string; + companyName: string; +} + interface KpiStats { criticalTickets: number; + topCritical?: TicketItem[]; waitingTickets: number; + topWaiting?: TicketItem[]; staleTickets: number; + topStale?: TicketItem[]; overdueTickets: number; + topOverdue?: TicketItem[]; openTickets: number; closedToday: number; + topClosed?: TicketItem[]; avgResolutionHours: number; hoursThisWeek: number; activeCompanies: number; diff --git a/components/kiosk/cycling-display.tsx b/components/kiosk/cycling-display.tsx index c1a9199..037a123 100644 --- a/components/kiosk/cycling-display.tsx +++ b/components/kiosk/cycling-display.tsx @@ -17,13 +17,24 @@ import { Server } from 'lucide-react'; +interface TicketItem { + ticketNumber: string; + title: string; + companyName: string; +} + interface KpiStats { criticalTickets: number; + topCritical?: TicketItem[]; waitingTickets: number; + topWaiting?: TicketItem[]; staleTickets: number; + topStale?: TicketItem[]; overdueTickets: number; + topOverdue?: TicketItem[]; openTickets: number; closedToday: number; + topClosed?: TicketItem[]; avgResolutionHours: number; hoursThisWeek: number; activeCompanies: number; @@ -48,6 +59,7 @@ export function CyclingDisplay({ stats, cycleInterval }: CyclingDisplayProps) { icon: AlertTriangle, color: stats.criticalTickets > 10 ? 'red' : stats.criticalTickets > 5 ? 'yellow' : 'green', trend: 'Priority 1-3', + tickets: stats.topCritical, }, { title: 'Tickets Waiting Engagement', @@ -55,6 +67,7 @@ export function CyclingDisplay({ stats, cycleInterval }: CyclingDisplayProps) { icon: Clock, color: stats.waitingTickets > 20 ? 'yellow' : 'blue', trend: 'Awaiting Response', + tickets: stats.topWaiting, }, { title: 'Stale Tickets', @@ -62,6 +75,7 @@ export function CyclingDisplay({ stats, cycleInterval }: CyclingDisplayProps) { icon: Timer, color: stats.staleTickets > 15 ? 'yellow' : 'blue', trend: '7+ Days No Activity', + tickets: stats.topStale, }, { title: 'Overdue Tickets', @@ -69,6 +83,7 @@ export function CyclingDisplay({ stats, cycleInterval }: CyclingDisplayProps) { icon: AlertCircle, color: stats.overdueTickets > 10 ? 'red' : stats.overdueTickets > 5 ? 'yellow' : 'green', trend: 'Past Due Date', + tickets: stats.topOverdue, }, { title: 'Total Open Tickets', @@ -83,6 +98,7 @@ export function CyclingDisplay({ stats, cycleInterval }: CyclingDisplayProps) { icon: CheckCircle2, color: 'green', trend: 'Last 24 Hours', + tickets: stats.topClosed, }, { title: 'Avg Resolution Time', @@ -160,6 +176,7 @@ export function CyclingDisplay({ stats, cycleInterval }: CyclingDisplayProps) { color={currentKpi.color as any} trend={currentKpi.trend} suffix={currentKpi.suffix} + tickets={currentKpi.tickets} /> diff --git a/components/kiosk/kpi-card.tsx b/components/kiosk/kpi-card.tsx index a312bc1..e52f972 100644 --- a/components/kiosk/kpi-card.tsx +++ b/components/kiosk/kpi-card.tsx @@ -2,6 +2,12 @@ import { LucideIcon } from 'lucide-react'; +interface TicketItem { + ticketNumber: string; + title: string; + companyName: string; +} + interface KpiCardProps { title: string; value: number | string; @@ -9,9 +15,10 @@ interface KpiCardProps { trend?: string; color?: 'red' | 'yellow' | 'green' | 'blue' | 'purple'; suffix?: string; + tickets?: TicketItem[]; } -export function KpiCard({ title, value, icon: Icon, trend, color = 'blue', suffix = '' }: KpiCardProps) { +export function KpiCard({ title, value, icon: Icon, trend, color = 'blue', suffix = '', tickets = [] }: KpiCardProps) { const colorClasses = { red: 'text-red-500 border-red-500', yellow: 'text-yellow-500 border-yellow-500', @@ -30,18 +37,40 @@ export function KpiCard({ title, value, icon: Icon, trend, color = 'blue', suffi return (
- -
+ +
{value}{suffix}
-
+
{title}
{trend && ( -
+
{trend}
)} + + {tickets && tickets.length > 0 && ( +
+ {tickets.map((ticket, index) => ( +
+
+ + #{ticket.ticketNumber} + +
+
+ {ticket.companyName} +
+
+ {ticket.title} +
+
+
+
+ ))} +
+ )}
); }