@@ -60,9 +60,23 @@ interface RecentPoll {
6060 responseBody : string | null ;
6161}
6262
63+ interface StillPollingMeeting {
64+ cityId : string ;
65+ meetingId : string ;
66+ meetingDate : string ;
67+ unlinkedSubjects : Array < { id : string ; name : string } > ;
68+ totalEligibleSubjects : number ;
69+ totalPolls : number ;
70+ firstPollAt : string | null ;
71+ lastPollAt : string | null ;
72+ currentTierLabel : string | null ;
73+ nextPollEligible : string | null ;
74+ }
75+
6376interface PollingStatsData {
6477 backoffSchedule : BackoffTier [ ] ;
6578 maxPollingDays : number ;
79+ meetingsStillPolling : StillPollingMeeting [ ] ;
6680 summary : {
6781 totalDiscoveries : number ;
6882 meetingsStillPolling : number ;
@@ -206,6 +220,7 @@ export function PollingStats({ stats, pollCities, cityFilter, pollMeetings, meet
206220 const [ sortField , setSortField ] = useState < SortField > ( 'discoveredAt' ) ;
207221 const [ sortDirection , setSortDirection ] = useState < SortDirection > ( 'desc' ) ;
208222 const [ selectedPoll , setSelectedPoll ] = useState < RecentPoll | null > ( null ) ;
223+ const [ selectedMeeting , setSelectedMeeting ] = useState < StillPollingMeeting | null > ( null ) ;
209224 const { updateParam, updateParams, isPending } = useUrlParams ( ) ;
210225
211226 const handleSort = ( field : SortField ) => {
@@ -243,7 +258,7 @@ export function PollingStats({ stats, pollCities, cityFilter, pollMeetings, meet
243258 } ,
244259 {
245260 title : 'Meetings Still Polling' ,
246- value : stats . summary . meetingsStillPolling ,
261+ value : stats . meetingsStillPolling . length ,
247262 icon : < Activity className = "h-4 w-4" /> ,
248263 description : 'With unlinked subjects' ,
249264 } ,
@@ -276,11 +291,90 @@ export function PollingStats({ stats, pollCities, cityFilter, pollMeetings, meet
276291 ) ;
277292
278293 return (
294+ < >
279295 < Sheet open = { ! ! selectedPoll } onOpenChange = { ( open ) => ! open && setSelectedPoll ( null ) } >
280296 < div className = "space-y-6" >
281297 { /* Summary Cards */ }
282298 < StatsCard items = { summaryItems } columns = { 4 } />
283299
300+ { /* Meetings Still Polling */ }
301+ < CollapsibleSection
302+ title = "Meetings Still Polling"
303+ badge = { `${ stats . meetingsStillPolling . length } meetings` }
304+ defaultOpen = { stats . meetingsStillPolling . length > 0 }
305+ >
306+ { stats . meetingsStillPolling . length === 0 ? (
307+ < div className = "p-8 text-center text-muted-foreground" >
308+ No meetings are currently being polled.
309+ </ div >
310+ ) : (
311+ < div className = "overflow-x-auto" >
312+ < table className = "w-full text-sm" >
313+ < thead >
314+ < tr className = "bg-muted/50 border-b text-muted-foreground" >
315+ < th className = "text-left px-4 py-2 font-medium" > City</ th >
316+ < th className = "text-left px-4 py-2 font-medium" > Meeting ID</ th >
317+ < th className = "text-left px-4 py-2 font-medium" > Meeting Date</ th >
318+ < th className = "text-left px-4 py-2 font-medium" > Unlinked</ th >
319+ < th className = "text-left px-4 py-2 font-medium" > Polls</ th >
320+ < th className = "text-left px-4 py-2 font-medium" > First Poll</ th >
321+ < th className = "text-left px-4 py-2 font-medium" > Last Poll</ th >
322+ < th className = "text-left px-4 py-2 font-medium" > Backoff</ th >
323+ < th className = "text-right px-4 py-2 font-medium" > Details</ th >
324+ </ tr >
325+ </ thead >
326+ < TooltipProvider >
327+ < tbody >
328+ { stats . meetingsStillPolling . map ( m => (
329+ < tr key = { `${ m . cityId } :${ m . meetingId } ` } className = "border-b last:border-b-0 hover:bg-muted/30" >
330+ < td className = "px-4 py-2 whitespace-nowrap font-mono text-xs" > { m . cityId } </ td >
331+ < td className = "px-4 py-2 whitespace-nowrap font-mono text-xs" > { m . meetingId } </ td >
332+ < td className = "px-4 py-2 whitespace-nowrap" > { m . meetingDate } </ td >
333+ < td className = "px-4 py-2 whitespace-nowrap" >
334+ { m . unlinkedSubjects . length } / { m . totalEligibleSubjects }
335+ </ td >
336+ < td className = "px-4 py-2 whitespace-nowrap text-center" > { m . totalPolls } </ td >
337+ < td className = "px-4 py-2 whitespace-nowrap" >
338+ { m . firstPollAt ? (
339+ < Tooltip >
340+ < TooltipTrigger asChild >
341+ < span className = "block cursor-default" > { new Date ( m . firstPollAt ) . toLocaleDateString ( ) } </ span >
342+ </ TooltipTrigger >
343+ < TooltipContent side = "bottom" > { new Date ( m . firstPollAt ) . toLocaleString ( ) } </ TooltipContent >
344+ </ Tooltip >
345+ ) : 'Never' }
346+ </ td >
347+ < td className = "px-4 py-2 whitespace-nowrap" >
348+ { m . lastPollAt ? (
349+ < Tooltip >
350+ < TooltipTrigger asChild >
351+ < span className = "block cursor-default" > { new Date ( m . lastPollAt ) . toLocaleDateString ( ) } </ span >
352+ </ TooltipTrigger >
353+ < TooltipContent side = "bottom" > { new Date ( m . lastPollAt ) . toLocaleString ( ) } </ TooltipContent >
354+ </ Tooltip >
355+ ) : 'Never' }
356+ </ td >
357+ < td className = "px-4 py-2 whitespace-nowrap text-xs" >
358+ { m . currentTierLabel ?? '\u2014' }
359+ </ td >
360+ < td className = "px-4 py-2 text-right" >
361+ < Button
362+ variant = "ghost"
363+ size = "sm"
364+ onClick = { ( ) => setSelectedMeeting ( m ) }
365+ >
366+ < Eye className = "h-4 w-4" />
367+ </ Button >
368+ </ td >
369+ </ tr >
370+ ) ) }
371+ </ tbody >
372+ </ TooltipProvider >
373+ </ table >
374+ </ div >
375+ ) }
376+ </ CollapsibleSection >
377+
284378 { /* Recent Polls */ }
285379 < CollapsibleSection
286380 title = "Recent Polls"
@@ -579,5 +673,82 @@ export function PollingStats({ stats, pollCities, cityFilter, pollMeetings, meet
579673 </ SheetContent >
580674 ) }
581675 </ Sheet >
676+
677+ { /* Still Polling Meeting Details Sidebar */ }
678+ < Sheet open = { ! ! selectedMeeting } onOpenChange = { ( open ) => ! open && setSelectedMeeting ( null ) } >
679+ { selectedMeeting && (
680+ < SheetContent className = "sm:max-w-lg overflow-y-auto" >
681+ < SheetHeader >
682+ < SheetTitle > Meeting Polling Details</ SheetTitle >
683+ < SheetDescription >
684+ { selectedMeeting . cityId } / { selectedMeeting . meetingId }
685+ </ SheetDescription >
686+ </ SheetHeader >
687+
688+ < div className = "mt-6 space-y-6" >
689+ { /* Metadata Grid */ }
690+ < div className = "grid grid-cols-2 gap-4" >
691+ < div >
692+ < div className = "text-sm text-muted-foreground mb-1" > City</ div >
693+ < div className = "text-sm font-mono" > { selectedMeeting . cityId } </ div >
694+ </ div >
695+ < div >
696+ < div className = "text-sm text-muted-foreground mb-1" > Meeting Date</ div >
697+ < div className = "text-sm font-medium" > { selectedMeeting . meetingDate } </ div >
698+ </ div >
699+ < div >
700+ < div className = "text-sm text-muted-foreground mb-1" > Backoff Tier</ div >
701+ < div className = "text-sm font-medium" > { selectedMeeting . currentTierLabel ?? 'Not started' } </ div >
702+ </ div >
703+ < div >
704+ < div className = "text-sm text-muted-foreground mb-1" > Next Poll Eligible</ div >
705+ < div className = "text-sm font-medium" >
706+ { selectedMeeting . nextPollEligible
707+ ? new Date ( selectedMeeting . nextPollEligible ) . toLocaleString ( )
708+ : 'Next cron run' }
709+ </ div >
710+ </ div >
711+ < div >
712+ < div className = "text-sm text-muted-foreground mb-1" > Total Polls</ div >
713+ < div className = "text-sm font-medium" > { selectedMeeting . totalPolls } </ div >
714+ </ div >
715+ < div >
716+ < div className = "text-sm text-muted-foreground mb-1" > Last Poll</ div >
717+ < div className = "text-sm font-medium" >
718+ { selectedMeeting . lastPollAt ? new Date ( selectedMeeting . lastPollAt ) . toLocaleString ( ) : 'Never' }
719+ </ div >
720+ </ div >
721+ </ div >
722+
723+ { /* Meeting Admin Link */ }
724+ < Link href = { `/${ selectedMeeting . cityId } /${ selectedMeeting . meetingId } /admin` } >
725+ < Button variant = "outline" className = "w-full" >
726+ < ExternalLink className = "h-4 w-4 mr-2" />
727+ Go to Meeting Admin
728+ </ Button >
729+ </ Link >
730+
731+ { /* Unlinked Subjects */ }
732+ < div >
733+ < div className = "text-sm font-medium mb-2" >
734+ Unlinked Subjects ({ selectedMeeting . unlinkedSubjects . length } / { selectedMeeting . totalEligibleSubjects } )
735+ </ div >
736+ { selectedMeeting . unlinkedSubjects . length === 0 ? (
737+ < p className = "text-sm text-muted-foreground" > All subjects have linked decisions.</ p >
738+ ) : (
739+ < ul className = "space-y-2" >
740+ { selectedMeeting . unlinkedSubjects . map ( s => (
741+ < li key = { s . id } className = "text-sm border rounded-md px-3 py-2 bg-muted/20" >
742+ { s . name }
743+ </ li >
744+ ) ) }
745+ </ ul >
746+ ) }
747+ </ div >
748+ </ div >
749+ </ SheetContent >
750+ ) }
751+ </ Sheet >
752+ </ >
582753 ) ;
583754}
0 commit comments