From c1f36ce375fa8b08cd297e8bd383ee2049a9e151 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sun, 15 Mar 2026 15:13:56 -0400 Subject: [PATCH] feat: add sidebar API client, stats bar, activity feed components New components: SidebarStatsBar, SidebarActivityFeed, ActivityItem. New API client for sidebar stats endpoint. Pulse-dot CSS animation. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/api/index.ts | 1 + frontend/src/api/sidebar.ts | 45 ++++++++ .../src/components/sidebar/ActivityItem.tsx | 105 ++++++++++++++++++ .../sidebar/SidebarActivityFeed.tsx | 80 +++++++++++++ .../components/sidebar/SidebarStatsBar.tsx | 58 ++++++++++ frontend/src/index.css | 5 + 6 files changed, 294 insertions(+) create mode 100644 frontend/src/api/sidebar.ts create mode 100644 frontend/src/components/sidebar/ActivityItem.tsx create mode 100644 frontend/src/components/sidebar/SidebarActivityFeed.tsx create mode 100644 frontend/src/components/sidebar/SidebarStatsBar.tsx diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index dbdc5c16..87d45040 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -23,3 +23,4 @@ export { flowTransferApi } from './flowTransfer' export { kbAcceleratorApi } from './kbAccelerator' export { scriptsApi } from './scripts' export { integrationsApi, sessionPsaApi } from './integrations' +export { sidebarApi } from './sidebar' diff --git a/frontend/src/api/sidebar.ts b/frontend/src/api/sidebar.ts new file mode 100644 index 00000000..8bbf9498 --- /dev/null +++ b/frontend/src/api/sidebar.ts @@ -0,0 +1,45 @@ +import apiClient from './client' + +export interface SidebarActiveSession { + session_id: string + tree_name: string + tree_id: string + tree_type: 'troubleshooting' | 'procedural' | 'maintenance' + started_at: string + ticket_number: string | null + psa_ticket_id: string | null +} + +export interface SidebarRecentSession { + session_id: string + tree_name: string + tree_id: string + tree_type: 'troubleshooting' | 'procedural' | 'maintenance' + completed_at: string +} + +export interface SidebarTreeCounts { + total: number + troubleshooting: number + procedural: number + maintenance: number +} + +export interface SidebarStatsResponse { + resolved_today: number + active_count: number + total_session_minutes_today: number + tree_counts: SidebarTreeCounts + active_sessions: SidebarActiveSession[] + recent_completions: SidebarRecentSession[] +} + +export const sidebarApi = { + getStats: async (): Promise => { + const tzOffset = new Date().getTimezoneOffset() + const response = await apiClient.get( + `/sessions/sidebar-stats?tz_offset=${tzOffset}` + ) + return response.data + }, +} diff --git a/frontend/src/components/sidebar/ActivityItem.tsx b/frontend/src/components/sidebar/ActivityItem.tsx new file mode 100644 index 00000000..d5f2d071 --- /dev/null +++ b/frontend/src/components/sidebar/ActivityItem.tsx @@ -0,0 +1,105 @@ +import { useNavigate } from 'react-router-dom' +import { getTreeNavigatePath } from '@/lib/routing' +import { cn } from '@/lib/utils' + +interface ActivityItemProps { + sessionId: string + treeName: string + treeId: string + treeType: 'troubleshooting' | 'procedural' | 'maintenance' + status: 'active' | 'paused' | 'recent' + ticketNumber?: string | null + timestamp?: string | null +} + +function formatRelativeTime(dateString: string): string { + const now = Date.now() + const then = new Date(dateString).getTime() + const diffMinutes = Math.floor((now - then) / 60000) + + if (diffMinutes < 1) return 'just now' + if (diffMinutes < 60) return `${diffMinutes}m ago` + const diffHours = Math.floor(diffMinutes / 60) + if (diffHours < 24) return `${diffHours}h ago` + return 'yesterday' +} + +export function ActivityItem({ + sessionId, + treeName, + treeId, + treeType, + status, + ticketNumber, + timestamp, +}: ActivityItemProps) { + const navigate = useNavigate() + + const handleClick = () => { + navigate(getTreeNavigatePath(treeId, treeType), { + state: { sessionId }, + }) + } + + const isRecent = status === 'recent' + + return ( + + ) +} diff --git a/frontend/src/components/sidebar/SidebarActivityFeed.tsx b/frontend/src/components/sidebar/SidebarActivityFeed.tsx new file mode 100644 index 00000000..ddfe6999 --- /dev/null +++ b/frontend/src/components/sidebar/SidebarActivityFeed.tsx @@ -0,0 +1,80 @@ +import { Clock } from 'lucide-react' +import { useNavigate } from 'react-router-dom' +import { ActivityItem } from './ActivityItem' +import type { SidebarActiveSession, SidebarRecentSession } from '@/api/sidebar' + +interface SidebarActivityFeedProps { + activeSessions: SidebarActiveSession[] + recentCompletions: SidebarRecentSession[] + totalActive: number +} + +export function SidebarActivityFeed({ + activeSessions, + recentCompletions, + totalActive, +}: SidebarActivityFeedProps) { + const navigate = useNavigate() + const hasActivity = activeSessions.length > 0 || recentCompletions.length > 0 + + return ( +
+ {/* Header */} +
+ + + Activity + +
+ + {!hasActivity ? ( +

+ No activity today +

+ ) : ( +
+ {/* Active sessions */} + {activeSessions.map((session) => ( + + ))} + + {/* Overflow link */} + {totalActive > 5 && ( + + )} + + {/* Divider between active and recent */} + {activeSessions.length > 0 && recentCompletions.length > 0 && ( +
+ )} + + {/* Recent completions */} + {recentCompletions.map((session) => ( + + ))} +
+ )} +
+ ) +} diff --git a/frontend/src/components/sidebar/SidebarStatsBar.tsx b/frontend/src/components/sidebar/SidebarStatsBar.tsx new file mode 100644 index 00000000..411e2703 --- /dev/null +++ b/frontend/src/components/sidebar/SidebarStatsBar.tsx @@ -0,0 +1,58 @@ +interface SidebarStatsBarProps { + resolved: number + active: number + sessionMinutes: number +} + +function formatDuration(minutes: number): string { + if (minutes < 60) return `${minutes}m` + const h = Math.floor(minutes / 60) + const m = minutes % 60 + return m > 0 ? `${h}h ${m}m` : `${h}h` +} + +export function SidebarStatsBar({ resolved, active, sessionMinutes }: SidebarStatsBarProps) { + return ( +
+
+
+ {resolved} +
+
+ Resolved +
+
+
+
+ {active} +
+
+ Active +
+
+
+
+ {formatDuration(sessionMinutes)} +
+
+ In Session +
+
+
+ ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css index f5028a79..b40e3f7c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -153,6 +153,11 @@ 100% { transform: rotate(0deg); } } + @keyframes pulse-dot { + 0%, 100% { box-shadow: 0 0 4px rgba(52,211,153,0.4); } + 50% { box-shadow: 0 0 8px rgba(52,211,153,0.7); } + } + @keyframes stagger-fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); }