wulf-pulse/components/kiosk/cycling-display.tsx
lorentz 3c13defacb feat: kiosk UI updates - new cards, gauge chart, performance improvements
- Updated kiosk stats API with expanded metrics
- New components: company-tickets-card, gauge-chart, service-desk-card, ticket-leaders-card
- Updated cycling-display, kpi-card, and ticker components
- Added performance.css for kiosk optimizations
- Added Wulf logo asset
2026-02-19 15:31:23 -05:00

270 lines
9 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { KpiCard } from './kpi-card';
import { ServiceDeskCard } from './service-desk-card';
import { CompanyTicketsCard } from './company-tickets-card';
import { TicketLeadersCard } from './ticket-leaders-card';
import {
AlertTriangle,
Clock,
Timer,
AlertCircle,
TicketCheck,
CheckCircle2,
TrendingUp,
Briefcase,
FileText,
Building2,
Network,
Server
} from 'lucide-react';
interface TicketItem {
ticketNumber: string;
title: string;
companyName: string;
}
interface ResourceLeader {
resourceName: string;
ticketsClosed: number;
}
interface IssueType {
issueType: string;
subIssueType: string;
count: number;
}
interface KpiStats {
criticalTickets: number;
topCritical?: TicketItem[];
waitingTickets: number;
topWaiting?: TicketItem[];
staleTickets: number;
topStale?: TicketItem[];
overdueTickets: number;
topOverdue?: TicketItem[];
openTickets: number;
closedToday: number;
avgClosedLastWeek: number;
avgClosedLastMonth: number;
topClosed?: TicketItem[];
avgResolutionHours: number;
hoursThisWeek: number;
activeCompanies: number;
openQuotes: number;
topTicketClosers?: ResourceLeader[];
topManagedIssueTypes?: IssueType[];
nmsCoverage: number;
rmmCoverage: number;
serviceDeskManaged: number;
serviceDeskTM: number;
serviceDeskVendor: number;
hynesTickets: number;
seubertTickets: number;
universalTickets: number;
}
interface CyclingDisplayProps {
stats: KpiStats;
cycleInterval: number;
}
export function CyclingDisplay({ stats, cycleInterval }: CyclingDisplayProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isTransitioning, setIsTransitioning] = useState(false);
// Define display items - mix of KPI cards and special cards
const displayItems: Array<{ type: 'serviceDesk' } | { type: 'companyTickets' } | { type: 'ticketLeaders' } | { type: 'kpi'; kpiIndex: number }> = [
{ type: 'serviceDesk' },
{ type: 'companyTickets' },
{ type: 'ticketLeaders' },
];
const kpis = [
{
title: 'Critical Tickets',
value: stats.criticalTickets,
icon: AlertTriangle,
color: stats.criticalTickets > 10 ? 'red' : stats.criticalTickets > 5 ? 'yellow' : 'green',
trend: 'Priority 1-3',
tickets: stats.topCritical,
// Mock trend data for now
lastWeek: Math.max(0, stats.criticalTickets - Math.floor(Math.random() * 5)),
lastMonth: Math.max(0, stats.criticalTickets - Math.floor(Math.random() * 8)),
weeklyTrend: stats.criticalTickets > 5 ? 'up' : 'down',
monthlyTrend: stats.criticalTickets > 8 ? 'up' : 'down',
},
{
title: 'Tickets Waiting Engagement',
value: stats.waitingTickets,
icon: Clock,
color: stats.waitingTickets > 20 ? 'yellow' : 'blue',
trend: 'Awaiting Response',
tickets: stats.topWaiting,
lastWeek: Math.max(0, stats.waitingTickets - Math.floor(Math.random() * 10)),
lastMonth: Math.max(0, stats.waitingTickets - Math.floor(Math.random() * 15)),
weeklyTrend: stats.waitingTickets > 15 ? 'up' : 'down',
monthlyTrend: stats.waitingTickets > 25 ? 'up' : 'down',
},
{
title: 'Stale Tickets',
value: stats.staleTickets,
icon: Timer,
color: stats.staleTickets > 15 ? 'yellow' : 'blue',
trend: '7+ Days No Activity',
tickets: stats.topStale,
lastWeek: Math.max(0, stats.staleTickets - Math.floor(Math.random() * 5)),
lastMonth: Math.max(0, stats.staleTickets - Math.floor(Math.random() * 10)),
weeklyTrend: stats.staleTickets > 10 ? 'up' : 'down',
monthlyTrend: stats.staleTickets > 12 ? 'up' : 'down',
},
{
title: 'Overdue Tickets',
value: stats.overdueTickets,
icon: AlertCircle,
color: stats.overdueTickets > 10 ? 'red' : stats.overdueTickets > 5 ? 'yellow' : 'green',
trend: 'Past Due Date',
tickets: stats.topOverdue,
lastWeek: Math.max(0, stats.overdueTickets - Math.floor(Math.random() * 5)),
lastMonth: Math.max(0, stats.overdueTickets - Math.floor(Math.random() * 8)),
weeklyTrend: stats.overdueTickets > 5 ? 'up' : 'down',
monthlyTrend: stats.overdueTickets > 8 ? 'up' : 'down',
},
{
title: 'Managed Service Tickets',
value: stats.serviceDeskManaged || 0,
icon: TicketCheck,
color: 'blue',
trend: 'Service Desk Managed',
lastWeek: Math.max(0, (stats.serviceDeskManaged || 0) - Math.floor(Math.random() * 10)),
lastMonth: Math.max(0, (stats.serviceDeskManaged || 0) - Math.floor(Math.random() * 15)),
weeklyTrend: (stats.serviceDeskManaged || 0) > 30 ? 'up' : 'down',
monthlyTrend: (stats.serviceDeskManaged || 0) > 40 ? 'up' : 'down',
},
{
title: 'Tickets Closed Today',
value: stats.closedToday,
icon: CheckCircle2,
color: 'green',
trend: 'Last 24 Hours',
tickets: stats.topClosed,
lastWeek: stats.avgClosedLastWeek,
lastMonth: stats.avgClosedLastMonth,
weeklyTrend: stats.closedToday > stats.avgClosedLastWeek ? 'up' : stats.closedToday < stats.avgClosedLastWeek ? 'down' : 'same',
monthlyTrend: stats.closedToday > stats.avgClosedLastMonth ? 'up' : stats.closedToday < stats.avgClosedLastMonth ? 'down' : 'same',
},
{
title: 'Avg Resolution Time',
value: Math.round(stats.avgResolutionHours),
icon: TrendingUp,
color: 'purple',
suffix: 'h',
trend: 'Last 30 Days',
lastWeek: Math.max(0, Math.round(stats.avgResolutionHours - Math.random() * 4)),
lastMonth: Math.max(0, Math.round(stats.avgResolutionHours - Math.random() * 6)),
weeklyTrend: stats.avgResolutionHours > 24 ? 'up' : 'down',
monthlyTrend: stats.avgResolutionHours > 30 ? 'up' : 'down',
},
{
title: 'Hours Logged This Week',
value: Math.round(stats.hoursThisWeek),
icon: Briefcase,
color: 'blue',
suffix: 'h',
trend: 'Billable Hours',
lastWeek: Math.max(0, Math.round(stats.hoursThisWeek - Math.random() * 20)),
lastMonth: Math.max(0, Math.round(stats.hoursThisWeek - Math.random() * 30)),
weeklyTrend: stats.hoursThisWeek > 100 ? 'up' : 'down',
monthlyTrend: stats.hoursThisWeek > 120 ? 'up' : 'down',
},
{
title: 'Open Quotes',
value: stats.openQuotes,
icon: FileText,
color: 'yellow',
trend: 'Pending Approval',
lastWeek: Math.max(0, stats.openQuotes - Math.floor(Math.random() * 3)),
lastMonth: Math.max(0, stats.openQuotes - Math.floor(Math.random() * 5)),
weeklyTrend: stats.openQuotes > 5 ? 'up' : 'down',
monthlyTrend: stats.openQuotes > 8 ? 'up' : 'down',
},
{
title: 'Active Companies',
value: stats.activeCompanies,
icon: Building2,
color: 'blue',
trend: 'Total Customers',
lastWeek: Math.max(0, stats.activeCompanies - Math.floor(Math.random() * 2)),
lastMonth: Math.max(0, stats.activeCompanies - Math.floor(Math.random() * 3)),
weeklyTrend: 'same',
monthlyTrend: 'up',
},
];
// Add KPI cards to display items
kpis.forEach((kpi, index) => {
displayItems.push({ type: 'kpi', kpiIndex: index });
});
useEffect(() => {
const interval = setInterval(() => {
setIsTransitioning(true);
setTimeout(() => {
setCurrentIndex((prev) => (prev + 1) % displayItems.length);
setIsTransitioning(false);
}, 300);
}, cycleInterval * 1000);
return () => clearInterval(interval);
}, [cycleInterval, displayItems.length]);
const currentItem = displayItems[currentIndex];
return (
<div className="flex-1 flex items-center justify-center px-4 pb-4 pt-0">
<div
className={`w-full max-w-7xl h-[80vh] transition-opacity duration-200 ${
isTransitioning ? 'opacity-0' : 'opacity-100'
}`}
style={{
transform: 'translateZ(0)',
backfaceVisibility: 'hidden',
}}
>
{currentItem.type === 'serviceDesk' ? (
<ServiceDeskCard
stats={{
managedCount: stats.serviceDeskManaged,
tmCount: stats.serviceDeskTM,
vendorCount: stats.serviceDeskVendor,
topManagedIssueTypes: stats.topManagedIssueTypes,
}}
/>
) : currentItem.type === 'companyTickets' ? (
<CompanyTicketsCard
stats={{
hynesCount: stats.hynesTickets,
seubertCount: stats.seubertTickets,
universalCount: stats.universalTickets,
}}
/>
) : currentItem.type === 'ticketLeaders' ? (
<TicketLeadersCard leaders={stats.topTicketClosers || []} />
) : (
<KpiCard
title={kpis[currentItem.kpiIndex!].title}
value={kpis[currentItem.kpiIndex!].value}
icon={kpis[currentItem.kpiIndex!].icon}
color={kpis[currentItem.kpiIndex!].color as any}
trend={kpis[currentItem.kpiIndex!].trend}
suffix={kpis[currentItem.kpiIndex!].suffix}
tickets={kpis[currentItem.kpiIndex!].tickets}
/>
)}
</div>
</div>
);
}