wulf-pulse/components/kiosk/kpi-card.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

165 lines
5.6 KiB
TypeScript

'use client';
import { LucideIcon } from 'lucide-react';
interface TicketItem {
ticketNumber: string;
title: string;
companyName: string;
status?: string;
}
interface KpiCardProps {
title: string;
value: number | string;
icon: LucideIcon;
trend?: string;
color?: 'red' | 'yellow' | 'green' | 'blue' | 'purple';
suffix?: string;
tickets?: TicketItem[];
lastWeek?: number;
lastMonth?: number;
weeklyTrend?: 'up' | 'down' | 'same';
monthlyTrend?: 'up' | 'down' | 'same';
}
export function KpiCard({
title,
value,
icon: Icon,
trend,
color = 'blue',
suffix = '',
tickets = [],
lastWeek,
lastMonth,
weeklyTrend,
monthlyTrend
}: KpiCardProps) {
const colorClasses = {
red: 'text-red-500 border-red-500',
yellow: 'text-yellow-500 border-yellow-500',
green: 'text-green-500 border-green-500',
blue: 'text-blue-500 border-blue-500',
purple: 'text-purple-500 border-purple-500',
};
const bgClasses = {
red: 'bg-red-500/10',
yellow: 'bg-yellow-500/10',
green: 'bg-green-500/10',
blue: 'bg-blue-500/10',
purple: 'bg-purple-500/10',
};
const getTrendIcon = (trend?: 'up' | 'down' | 'same') => {
switch (trend) {
case 'up': return '↑';
case 'down': return '↓';
default: return '→';
}
};
const getTrendColor = (trend?: 'up' | 'down' | 'same') => {
switch (trend) {
case 'up': return 'text-green-500';
case 'down': return 'text-red-500';
default: return 'text-gray-500';
}
};
return (
<div className={`flex h-full w-full ${bgClasses[color]} border-4 ${colorClasses[color]} rounded-3xl overflow-hidden`}>
{/* Left 1/3 - KPI and Graphic */}
<div className="w-1/3 flex flex-col items-center justify-center p-8 border-r border-gray-700">
<Icon className={`w-20 h-20 mb-4 ${colorClasses[color]}`} />
<div className={`text-6xl font-bold mb-2 ${colorClasses[color]}`}>
{value}{suffix}
</div>
<div className="text-2xl text-gray-300 text-center font-semibold">
{title}
</div>
{trend && (
<div className="text-lg text-gray-500 mt-2">
{trend}
</div>
)}
</div>
{/* Right 2/3 - Supporting Info */}
<div className="w-2/3 p-8 flex flex-col">
{/* Trend Comparisons - Always show if trend data exists */}
{(lastWeek !== undefined || lastMonth !== undefined) && (
<div className="mb-6">
<h3 className="text-xl font-semibold text-gray-300 mb-4">Trend Analysis</h3>
<div className="grid grid-cols-2 gap-4">
{lastWeek !== undefined && (
<div className="bg-black/30 rounded-lg p-4 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">vs Last Week</div>
<div className="flex items-center gap-2">
<span className={`text-2xl font-bold ${getTrendColor(weeklyTrend)}`}>
{getTrendIcon(weeklyTrend)}
</span>
<span className="text-xl text-gray-300">{lastWeek}</span>
</div>
</div>
)}
{lastMonth !== undefined && (
<div className="bg-black/30 rounded-lg p-4 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">vs Last Month</div>
<div className="flex items-center gap-2">
<span className={`text-2xl font-bold ${getTrendColor(monthlyTrend)}`}>
{getTrendIcon(monthlyTrend)}
</span>
<span className="text-xl text-gray-300">{lastMonth}</span>
</div>
</div>
)}
</div>
</div>
)}
{/* Top Tickets - Only show if tickets exist */}
{tickets && tickets.length > 0 && (
<div className="flex-1">
<h3 className="text-xl font-semibold text-gray-300 mb-4">Top Tickets</h3>
<div className="space-y-2 flex-1 overflow-y-auto">
{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-base font-semibold text-gray-300 truncate">
{ticket.companyName}
</div>
<div className="text-sm text-gray-400 truncate">
{ticket.title}
</div>
{ticket.status && (
<div className="text-xs text-blue-400 mt-1">
{ticket.status}
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Show message if no trend data and no tickets */}
{(!lastWeek && !lastMonth) && (!tickets || tickets.length === 0) && (
<div className="flex-1 flex items-center justify-center">
<div className="text-gray-500 text-center">
<div className="text-lg mb-2">No additional data available</div>
<div className="text-sm">Check back later for updates</div>
</div>
</div>
)}
</div>
</div>
);
}