feat: add executive kiosk dashboard for TV display

- Created /kiosk route with full-screen display
- Added API endpoints for stats and activity feed
- Built cycling KPI display with 12 metrics
- Added scrolling ticker for ticket activity
- Implemented UI-configurable cycle interval (3-15s, default 7s)
- Focus on critical tickets and business metrics
- Added navigation link from dashboard
- Dark theme optimized for TV viewing
This commit is contained in:
root 2026-02-02 23:36:35 -05:00
parent 4d531b2f2d
commit 20a0785ec3
9 changed files with 692 additions and 0 deletions

120
app/kiosk/page.tsx Normal file
View file

@ -0,0 +1,120 @@
'use client';
import { useEffect, useState } from 'react';
import { CyclingDisplay } from '@/components/kiosk/cycling-display';
import { Ticker } from '@/components/kiosk/ticker';
import { SettingsPanel } from '@/components/kiosk/settings-panel';
interface KpiStats {
criticalTickets: number;
waitingTickets: number;
staleTickets: number;
overdueTickets: number;
openTickets: number;
closedToday: number;
avgResolutionHours: number;
hoursThisWeek: number;
activeCompanies: number;
openQuotes: number;
nmsCoverage: number;
rmmCoverage: number;
}
interface TickerActivity {
ticketNumber: string;
title: string;
priority: number;
statusLabel: string;
companyName: string;
}
export default function KioskPage() {
const [stats, setStats] = useState<KpiStats>({
criticalTickets: 0,
waitingTickets: 0,
staleTickets: 0,
overdueTickets: 0,
openTickets: 0,
closedToday: 0,
avgResolutionHours: 0,
hoursThisWeek: 0,
activeCompanies: 0,
openQuotes: 0,
nmsCoverage: 0,
rmmCoverage: 0,
});
const [activities, setActivities] = useState<TickerActivity[]>([]);
const [cycleInterval, setCycleInterval] = useState(7);
useEffect(() => {
// Load saved cycle interval from localStorage
const savedInterval = localStorage.getItem('kiosk-cycle-interval');
if (savedInterval) {
setCycleInterval(parseInt(savedInterval));
}
}, []);
useEffect(() => {
// Fetch initial data
fetchData();
// Set up auto-refresh every 60 seconds
const statsInterval = setInterval(fetchData, 60000);
return () => {
clearInterval(statsInterval);
};
}, []);
const fetchData = async () => {
try {
// Fetch stats
const statsRes = await fetch('/api/kiosk/stats');
if (statsRes.ok) {
const statsData = await statsRes.json();
setStats(statsData);
}
// Fetch activity feed
const activityRes = await fetch('/api/kiosk/activity');
if (activityRes.ok) {
const activityData = await activityRes.json();
setActivities(activityData.activities || []);
}
} catch (error) {
console.error('Error fetching kiosk data:', error);
}
};
return (
<div className="h-screen w-screen bg-black text-white overflow-hidden flex flex-col">
{/* Settings Panel */}
<SettingsPanel
cycleInterval={cycleInterval}
onIntervalChange={setCycleInterval}
/>
{/* Main Content Area */}
<CyclingDisplay stats={stats} cycleInterval={cycleInterval} />
{/* Ticker */}
<Ticker activities={activities} />
{/* Custom CSS for ticker animation */}
<style jsx global>{`
@keyframes ticker {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.animate-ticker {
animation: ticker 60s linear infinite;
}
`}</style>
</div>
);
}