feat: add top 3 tickets display on kiosk KPI cards

- Show company name and ticket title for top 3 tickets per category
- Critical tickets sorted by priority then create date
- Waiting tickets sorted by oldest activity
- Stale tickets sorted by oldest activity
- Overdue tickets sorted by most overdue
- Closed tickets sorted by most recent
- Adjusted card layout to accommodate ticket list
This commit is contained in:
root 2026-02-03 08:25:27 -05:00
parent 1d1b4d2a71
commit 724791121a
4 changed files with 146 additions and 5 deletions

View file

@ -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'),

View file

@ -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;