- 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
165 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
}
|