- 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
139 lines
4.5 KiB
TypeScript
139 lines
4.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
|
|
interface TickerActivity {
|
|
ticketNumber: string;
|
|
title: string;
|
|
priority: number;
|
|
statusLabel: string;
|
|
companyName: string;
|
|
}
|
|
|
|
interface TickerProps {
|
|
activities: TickerActivity[];
|
|
speed: number;
|
|
}
|
|
|
|
export function Ticker({ activities, speed }: TickerProps) {
|
|
const [isPaused, setIsPaused] = useState(false);
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
const animationRef = useRef<number | null>(null);
|
|
const positionRef = useRef(0);
|
|
const lastTimeRef = useRef<number>(0);
|
|
|
|
// Simple requestAnimationFrame animation that respects speed
|
|
useEffect(() => {
|
|
if (!contentRef.current) return;
|
|
|
|
const animate = (timestamp: number) => {
|
|
if (lastTimeRef.current === 0) {
|
|
lastTimeRef.current = timestamp;
|
|
}
|
|
|
|
const deltaTime = timestamp - lastTimeRef.current;
|
|
lastTimeRef.current = timestamp;
|
|
|
|
if (!isPaused && contentRef.current) {
|
|
// Move based on speed - higher speed = faster movement
|
|
// Speed 10 = 20px/s, Speed 60 = 120px/s, Speed 100 = 200px/s
|
|
const pixelsPerSecond = speed * 2;
|
|
positionRef.current -= (pixelsPerSecond * deltaTime) / 1000;
|
|
|
|
// Get the width of the first set
|
|
const contentWidth = contentRef.current.scrollWidth / 2;
|
|
|
|
// Reset when we've scrolled past the first set
|
|
if (Math.abs(positionRef.current) >= contentWidth) {
|
|
positionRef.current = 0;
|
|
}
|
|
|
|
contentRef.current.style.transform = `translateX(${positionRef.current}px)`;
|
|
}
|
|
|
|
animationRef.current = requestAnimationFrame(animate);
|
|
};
|
|
|
|
animationRef.current = requestAnimationFrame(animate);
|
|
|
|
return () => {
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current);
|
|
}
|
|
};
|
|
}, [speed, isPaused]);
|
|
|
|
const getPriorityColor = (priority: number) => {
|
|
if (priority <= 3) return 'text-red-500';
|
|
if (priority <= 5) return 'text-orange-500';
|
|
return 'text-blue-500';
|
|
};
|
|
|
|
const getPriorityIcon = (priority: number) => {
|
|
if (priority <= 3) return '🔴';
|
|
if (priority <= 5) return '🟠';
|
|
return '🔵';
|
|
};
|
|
|
|
const getStatusColor = (statusLabel: string) => {
|
|
const status = statusLabel?.toLowerCase() || '';
|
|
if (status.includes('new') || status.includes('resource requested') || status.includes('end user note added')) {
|
|
return 'text-red-500 font-bold';
|
|
}
|
|
if (status.includes('in progress')) {
|
|
return 'text-green-500 font-bold';
|
|
}
|
|
return 'text-gray-500';
|
|
};
|
|
|
|
if (activities.length === 0) {
|
|
return (
|
|
<div className="fixed bottom-0 left-0 right-0 bg-black border-t-4 border-blue-600 h-24 flex items-center justify-center">
|
|
<span className="text-gray-500">No recent ticket activity</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Limit to 20 items for better performance on low-end hardware
|
|
const limitedActivities = activities.slice(0, 20);
|
|
|
|
return (
|
|
<div
|
|
className="fixed bottom-0 left-0 right-0 bg-black border-t-4 border-blue-600 h-24 overflow-hidden"
|
|
onMouseEnter={() => setIsPaused(true)}
|
|
onMouseLeave={() => setIsPaused(false)}
|
|
>
|
|
<div
|
|
ref={contentRef}
|
|
className="flex items-center h-full whitespace-nowrap"
|
|
>
|
|
{/* Duplicate content for seamless loop */}
|
|
{[...limitedActivities, ...limitedActivities].map((activity, index) => (
|
|
<div
|
|
key={`${activity.ticketNumber}-${index}`}
|
|
className="inline-flex items-center mx-8 flex-shrink-0"
|
|
>
|
|
<div className="flex flex-col gap-1">
|
|
{/* Line 1: Priority - Ticket Number - Company Name */}
|
|
<div className="flex items-center text-xl">
|
|
<span className="mr-2 text-2xl">{getPriorityIcon(activity.priority)}</span>
|
|
<span className={`font-bold mr-2 ${getPriorityColor(activity.priority)}`}>
|
|
{activity.ticketNumber}
|
|
</span>
|
|
<span className="text-white mr-2">-</span>
|
|
<span className="text-white font-semibold">
|
|
{activity.companyName}
|
|
</span>
|
|
</div>
|
|
{/* Line 2: Title - Status */}
|
|
<div className="flex items-center text-base">
|
|
<span className="text-gray-400">{activity.title?.substring(0, 60) || ''}</span>
|
|
<span className={`ml-2 ${getStatusColor(activity.statusLabel)}`}>- {activity.statusLabel || 'Unknown'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|