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:
parent
1d1b4d2a71
commit
724791121a
4 changed files with 146 additions and 5 deletions
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={`flex flex-col items-center justify-center h-full w-full p-12 ${bgClasses[color]} border-4 ${colorClasses[color]} rounded-3xl`}>
|
||||
<Icon className={`w-32 h-32 mb-8 ${colorClasses[color]}`} />
|
||||
<div className={`text-9xl font-bold mb-4 ${colorClasses[color]}`}>
|
||||
<Icon className={`w-24 h-24 mb-6 ${colorClasses[color]}`} />
|
||||
<div className={`text-8xl font-bold mb-3 ${colorClasses[color]}`}>
|
||||
{value}{suffix}
|
||||
</div>
|
||||
<div className="text-4xl text-gray-300 text-center font-semibold mb-2">
|
||||
<div className="text-3xl text-gray-300 text-center font-semibold mb-4">
|
||||
{title}
|
||||
</div>
|
||||
{trend && (
|
||||
<div className="text-2xl text-gray-500 mt-4">
|
||||
<div className="text-xl text-gray-500 mb-6">
|
||||
{trend}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tickets && tickets.length > 0 && (
|
||||
<div className="w-full mt-4 space-y-3">
|
||||
{tickets.map((ticket, index) => (
|
||||
<div key={ticket.ticketNumber} className="bg-black/30 rounded-lg p-3 border border-gray-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className={`text-lg font-bold ${colorClasses[color]} flex-shrink-0`}>
|
||||
#{ticket.ticketNumber}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-gray-300 truncate">
|
||||
{ticket.companyName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 line-clamp-2">
|
||||
{ticket.title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue