import { useState, useMemo } from 'react'; import { useUrlPagination } from '@/hooks/useUrlPagination'; import { useCanvasLoadingPoll } from '@/hooks/useCanvasLoadingPoll'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import useSWR from 'swr'; import { toast } from 'sonner'; import { Plus, ArrowLeft, Edit, MoreVertical, Trash2, BookOpen, Loader2 } from 'lucide-react'; import { Skeleton } from '@/components/ui/skeleton'; import { NoDataDash } from '@/components/shared'; import { AcademicCapIcon, WrenchScrewdriverIcon, HeartIcon, SparklesIcon, LightBulbIcon, HomeModernIcon, HandRaisedIcon } from '@heroicons/react/24/outline'; import API from '@/api/api'; import { Cohort, Facility, ProgramOverview, externalSourceLabel, ProgClassStatus, SelectedClassStatus, classCountsTowardRates, ChangeLogEntry, ServerResponseMany, ServerResponseOne } from '@/types'; import { formatDate, getInstructorName, getStatusColor } from '@/lib/formatters'; import { programTypeColors } from '@/pages/program-detail/constants'; import { cn } from '@/lib/utils'; import { formatHistoryEntry } from '@/components/history/formatHistoryEntry'; import { ClassManagementFormInner } from '@/pages/programs/ClassManagementForm'; import EditProgramDialog from '@/pages/program-detail/EditProgramDialog'; import { canSwitchFacility, useAuth } from '@/auth/useAuth'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { DialogFooter } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Pagination } from '@/components/Pagination'; import Breadcrumbs from '@/components/navigation/Breadcrumbs'; import { FormModal } from '@/components/shared'; import { ArchiveConfirmDialog, CannotArchiveDialog, ReactivateDialog, DeleteProgramDialog } from '@/components/programs/ProgramDialogs'; type HeroIcon = React.ForwardRefExoticComponent< React.PropsWithoutRef> & { title?: string; titleId?: string; } & React.RefAttributes >; const programTypeIcons: Record = { Educational: AcademicCapIcon, Vocational: WrenchScrewdriverIcon, Mental_Health_Behavioral: HeartIcon, Therapeutic: SparklesIcon, Life_Skills: LightBulbIcon, 'Re-Entry': HomeModernIcon, 'Religious_Faith-Based': HandRaisedIcon }; function formatEnum(value: string): string { return value.replace(/_/g, ' '); } export default function ProgramOverviewFacilityAdmin() { const navigate = useNavigate(); const { program_id } = useParams<{ program_id: string }>(); const [searchParams] = useSearchParams(); const { user } = useAuth(); const [activeTab, setActiveTab] = useState('classes'); const { page: historyPage, perPage: historyPerPage, setPage: setHistoryPage, setPerPage: setHistoryPerPage } = useUrlPagination(1, 20, 'history'); const [statusModalOpen, setStatusModalOpen] = useState(false); const [selectedClass, setSelectedClass] = useState(null); const [selectedStatus, setSelectedStatus] = useState(''); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false); const [showArchiveDialog, setShowArchiveDialog] = useState(false); const [showCannotArchiveDialog, setShowCannotArchiveDialog] = useState(false); const [showReactivateDialog, setShowReactivateDialog] = useState(false); const [archiveCheckLoading, setArchiveCheckLoading] = useState(false); const [archiveBlockingFacilities, setArchiveBlockingFacilities] = useState< string[] >([]); const facilityIdParam = searchParams.get('facility_id'); const facilityId = facilityIdParam ? Number(facilityIdParam) : null; const facilityQuery = facilityId ? `&facility_id=${facilityId}` : ''; const { data: facilityResp } = useSWR>( facilityId ? `/api/facilities/${facilityId}` : null ); const { data: programResp, mutate: mutateProgram } = useSWR< ServerResponseOne >( `/api/programs/${program_id}${facilityId ? `?facility_id=${facilityId}` : ''}` ); const program = programResp?.data; const sourceLabel = externalSourceLabel(program?.source); const { exhausted: detailPollExhausted } = useCanvasLoadingPoll( !!program?.loading, mutateProgram ); const { data: deleteCheckResp, mutate: mutateDeleteCheck } = useSWR< ServerResponseOne<{ can_delete: boolean; blockers: { classes?: number; enrollments?: number; completions?: number; attendance_flags?: number; }; }> >(program_id ? `/api/programs/${program_id}/delete-check` : null); const canDelete = deleteCheckResp?.data?.can_delete ?? false; const deleteBlockers = deleteCheckResp?.data?.blockers; const deleteBlockerReason = (() => { if (canDelete || !deleteBlockers) return null; if ((deleteBlockers.classes ?? 0) > 0) return 'Cannot delete program with existing classes'; if ((deleteBlockers.enrollments ?? 0) > 0) return 'Cannot delete program with active enrollments'; if ((deleteBlockers.completions ?? 0) > 0) return 'Cannot delete program with completion records'; if ((deleteBlockers.attendance_flags ?? 0) > 0) return 'Cannot delete program with attendance records'; return 'Cannot delete program'; })(); const { data: classesResp, isLoading: classesLoading, mutate: mutateClasses } = useSWR>( `/api/programs/${program_id}/classes?per_page=100&order_by=ps.start_dt asc${facilityQuery}` ); const classes = useMemo(() => classesResp?.data ?? [], [classesResp?.data]); const { data: historyResp } = useSWR>( activeTab === 'history' ? `/api/programs/${program_id}/history?page=${historyPage}&per_page=${historyPerPage}` : null ); const history = historyResp?.data ?? []; const historyTotal = historyResp?.meta?.total ?? history.length; const nonArchivedClasses = useMemo( () => classes.filter((c) => !c.archived_at), [classes] ); const activeClasses = useMemo( () => nonArchivedClasses.filter( (c) => c.status === SelectedClassStatus.Active ), [nonArchivedClasses] ); const totalCapacity = useMemo( () => activeClasses.reduce((sum, c) => sum + c.capacity, 0), [activeClasses] ); const totalEnrolled = useMemo( () => activeClasses.reduce((sum, c) => sum + c.enrolled, 0), [activeClasses] ); const utilization = totalCapacity > 0 ? Math.round((totalEnrolled / totalCapacity) * 100) : 0; const backendCompletionRate = program?.completion_rate ?? null; async function handleArchiveCheck() { if (!program || archiveCheckLoading) return; setArchiveCheckLoading(true); const resp = await API.get<{ facilities: string[] }>( `programs/${program.id}/archive-check` ); setArchiveCheckLoading(false); if (!resp.success) { toast.error(resp.message || 'Unable to check active class status.'); return; } const blocking = (resp.data as { facilities: string[] }).facilities ?? []; if (blocking.length > 0) { setArchiveBlockingFacilities(blocking); setShowCannotArchiveDialog(true); return; } setShowArchiveDialog(true); } function handleStatusSelectChange(value: string) { if (value === 'Archived') { void handleArchiveCheck(); return; } if (value === 'Reactivate') { setShowReactivateDialog(true); return; } void handleProgramStatusChange(value); } async function handleReactivate(isActive: boolean) { if (!program) return; const resp = await API.patch< { updated: boolean; message: string }, Record >(`programs/${program.id}/status`, { archived_at: null, is_active: isActive }); const statusUpdated = !Array.isArray(resp.data) && resp.data?.updated !== false; if (resp.success && statusUpdated) { toast.success(`${program.name} has been reactivated`); void mutateProgram(); } else { toast.error(resp.message || 'Failed to reactivate program'); } } async function handleProgramStatusChange(newStatus: string) { if (!program) return; const body: Record = {}; if (newStatus === 'Archived') { body.archived_at = new Date().toISOString(); body.is_active = false; } else { body.is_active = newStatus === 'Available'; if (program.archived_at) { body.archived_at = null; } } const resp = await API.patch< { updated: boolean; message: string }, Record >(`programs/${program.id}/status`, body); const statusUpdated = !Array.isArray(resp.data) && resp.data?.updated !== false; if (resp.success && statusUpdated) { const successMessage = newStatus === 'Archived' ? `${program.name} has been archived` : 'Program status updated'; toast.success(successMessage); void mutateProgram(); } else { toast.error(resp.message || 'Failed to update program status'); } } async function handleClassStatusChange(cls: Cohort, newStatus: string) { const resp = await API.patch(`program-classes?id=${cls.id}`, { status: newStatus }); if (resp.success) { toast.success(`Class status updated to ${newStatus}`); void mutateClasses(); void mutateProgram(); } else { toast.error('Failed to update class status'); } } const facilityFromProgram = facilityId ? program?.facilities?.find((facility) => facility.id === facilityId) : null; const facilityName = facilityFromProgram?.name ?? facilityResp?.data?.name ?? user?.facility?.name; const showFacilityContextBanner = Boolean( user && canSwitchFacility(user) && facilityId && facilityName ); // program edits are statewide; sys/dept admins make them from the statewide overview const canEditStatewide = Boolean(user && canSwitchFacility(user)); if (!program) { return (

Loading program...

); } if (program.loading) { return (
{detailPollExhausted ? ( Taking longer than expected —{' '} ) : ( <> Syncing {sourceLabel} data, this may take a moment… )}
{Array.from({ length: 4 }).map((_, i) => ( ))}
); } const isCanvasProgram = (program?.id ?? 0) >= 100_000_000; const programStatus = program.archived_at ? 'Archived' : program.is_active ? 'Available' : 'Inactive'; const fundingDisplay = program.funding_type ? formatEnum(String(program.funding_type)) : ''; const primaryType = program.program_types[0]?.program_type; const Icon = primaryType ? programTypeIcons[primaryType] || LightBulbIcon : LightBulbIcon; const breadcrumbs = showFacilityContextBanner ? [ { label: 'Dashboard', href: '/dashboard' }, { label: 'Programs', href: '/programs' }, { label: program.name, href: `/programs/${program.id}` }, { label: facilityName! } ] : [ { label: 'Dashboard', href: '/dashboard' }, { label: 'Programs', href: '/programs' }, { label: program.name } ]; function handleOpenStatusModal(cls: Cohort) { setSelectedClass(cls); setSelectedStatus(cls.status); setStatusModalOpen(true); } function handleCloseStatusModal(open: boolean) { setStatusModalOpen(open); if (!open) { setSelectedClass(null); } } function handleSaveStatus() { if (!selectedClass) return; void handleClassStatusChange(selectedClass, selectedStatus); setStatusModalOpen(false); setSelectedClass(null); } async function handleDeleteProgram(): Promise { if (!program) return false; const resp = await API.delete(`programs/${program.id}`); if (resp.success) { toast.success(`Program "${program.name}" has been deleted`); navigate('/programs'); return true; } if (resp.status === 409) { void mutateDeleteCheck(); } toast.error(resp.message || 'Failed to delete program'); return false; } function getStatusDescription(status: string) { switch (status) { case 'Active': return 'Class is currently running and accepting attendance.'; case 'Scheduled': return 'Class is scheduled to begin in the future.'; case 'Completed': return 'Class has finished and is now archived.'; case 'Cancelled': return 'Class has been cancelled and will not take place.'; case 'Paused': return 'Class is temporarily paused and not meeting.'; default: return ''; } } return (
{showFacilityContextBanner && (
Viewing: {program.name} at {facilityName}
)} {isCanvasProgram && (
This program is managed externally in {sourceLabel}. Data is read-only.
)}

{program.name}

{program.program_types?.length > 0 && (
{program.program_types.map((pt) => ( {formatEnum( pt.program_type )} ))}
)}

Program Status

{isCanvasProgram && ( Managed in {sourceLabel} )}
{!isCanvasProgram && !canEditStatewide && ( )} {!isCanvasProgram && (
setDeleteModalOpen( true ) } disabled={ !canDelete } > Delete Program
{!canDelete && ( {deleteBlockerReason ?? 'Cannot delete program'} )}
)}
{program.description && (

{program.description}

)}
{!isCanvasProgram && ( )}
Classes ({nonArchivedClasses.length}) Program Details Performance Audit History void mutateClasses()} />

{getStatusDescription(selectedStatus)}

void handleReactivate(isActive)} /> void handleProgramStatusChange('Archived')} /> { if (!open) setArchiveBlockingFacilities([]); setShowCannotArchiveDialog(open); }} programName={program.name} facilities={archiveBlockingFacilities} />
); } function MetricBox({ label, value, subtitle, valueClassName }: { label: string; value: string | number; subtitle?: string; valueClassName?: string; }) { return (

{label}

{value}

{subtitle && (

{subtitle}

)}
); } function ClassesTab({ classes, loading, programId, facilityId, canAddClass, isCanvasProgram, onOpenStatusModal, onCreated }: { classes: Cohort[]; loading: boolean; programId: string; facilityId?: number; canAddClass: boolean; isCanvasProgram?: boolean; onOpenStatusModal: (cls: Cohort) => void; onCreated?: () => void; }) { const navigate = useNavigate(); const [showCreateForm, setShowCreateForm] = useState(false); const activeScheduledClasses = classes.filter( (cls) => cls.status === SelectedClassStatus.Active || cls.status === SelectedClassStatus.Scheduled ); const completedClasses = classes.filter( (cls) => cls.status === SelectedClassStatus.Completed ); const cancelledClasses = classes.filter( (cls) => cls.status === SelectedClassStatus.Cancelled ); const pausedClasses = classes.filter( (cls) => cls.status === SelectedClassStatus.Paused ); return (

Classes

All classes offered under this program

{!isCanvasProgram && ( )}
{showCreateForm && (

Create New Class

setShowCreateForm(false)} onCreated={() => { setShowCreateForm(false); onCreated?.(); }} embedded />
)} {showCreateForm && ( ); } function ClassRow({ cls, onOpenStatusModal, onClick, className, editableStatus, showEnrollment, isCanvas }: { cls: Cohort; onOpenStatusModal: (cls: Cohort) => void; onClick?: () => void; className?: string; editableStatus?: boolean; showEnrollment?: boolean; isCanvas?: boolean; }) { const enrollPct = cls.capacity > 0 ? (cls.enrolled / cls.capacity) * 100 : 0; const scheduleText = String(cls.schedule ?? ''); const roomText = String(cls.room ?? ''); const instructorName = getInstructorName(cls.events); const metaItems = [instructorName, scheduleText, roomText].filter( (item): item is string => Boolean(item) ); const attendanceRate = typeof cls.attendance_rate === 'number' ? Math.round(cls.attendance_rate) : null; const attendanceClass = attendanceRate !== null ? attendanceRate >= 85 ? 'text-brand' : attendanceRate >= 70 ? 'text-brand-gold' : 'text-gray-700' : 'text-gray-500'; const hasStarted = classCountsTowardRates(cls.status); // rate_eligible_* carry the server's enrollment_ended_at predicate, so this // cannot contradict the program-level rate shown in PerformanceTab. const completionBase = cls.rate_eligible_enrollments ?? 0; const completionRate = hasStarted && completionBase > 0 ? Math.round( ((cls.rate_eligible_completions ?? 0) / completionBase) * 100 ) : null; const completionClass = completionRate !== null ? completionRate >= 85 ? 'text-brand' : completionRate >= 70 ? 'text-brand-gold' : 'text-gray-700' : 'text-gray-500'; return (

{cls.class_name}

{editableStatus ? ( { event.stopPropagation(); onOpenStatusModal(cls); }} > {cls.status} ) : ( {cls.status} )}
{cls.description && (

{cls.description}

)} {metaItems.length > 0 && (
{metaItems.map((item, index) => ( {item} ))}
)}
{!isCanvas && (
Attendance: {attendanceRate !== null ? ( {attendanceRate}% ) : ( {cls.status === SelectedClassStatus.Scheduled ? `No attendance yet — starts ${formatDate(cls.start_dt)}` : 'No attendance has been recorded for this class yet'} )}
)} {!isCanvas && (
Completion: {completionRate !== null ? ( {completionRate}% ) : ( {cls.status === SelectedClassStatus.Scheduled ? `This class hasn't started yet — starts ${formatDate(cls.start_dt)}` : cls.status === SelectedClassStatus.Cancelled ? cls.completed > 0 ? `This class was cancelled; ${cls.completed} resident${cls.completed === 1 ? '' : 's'} completed it before cancellation, but a cancelled class doesn't count toward completion rates` : 'This class was cancelled before any residents reached completion eligibility' : `No residents in this class have reached completion eligibility yet (${cls.enrolled} enrolled)`} )}
)}
{showEnrollment && (
Enrollment {isCanvas ? cls.enrolled : `${cls.enrolled} / ${cls.capacity}`}
{!isCanvas && ( )}
)}
); } function ProgramDetailsTab({ program }: { program: ProgramOverview }) { const programStatus = program.archived_at ? 'Archived' : program.is_active ? 'Available' : 'Inactive'; return (

Program Details

Description

{program.description || 'No description provided.'}

Program Types

{program.program_types?.map((pt) => ( {formatEnum(pt.program_type)} ))}

Credit Types

{program.credit_types?.map((ct) => ( {formatEnum(ct.credit_type)} ))}

Funding Types

{program.funding_type ? formatEnum(String(program.funding_type)) : '-'}

Status

{programStatus}
); } function PerformanceTab({ totalEnrolled, totalCapacity, activeClassCount, totalClassCount, completionRate }: { totalEnrolled: number; totalCapacity: number; activeClassCount: number; totalClassCount: number; completionRate: number | null; }) { const capacityPct = totalCapacity > 0 ? Math.round((totalEnrolled / totalCapacity) * 100) : 0; const avgClassSize = activeClassCount > 0 ? Math.round(totalEnrolled / activeClassCount) : 0; return (

Performance Metrics

Total Enrollment

{totalEnrolled}

{capacityPct}% of capacity

Average Class Size

{avgClassSize}

residents per class

Active Classes

{activeClassCount}

out of {totalClassCount} total

Completion Rate

{completionRate !== null ? ( <>

{Math.round(completionRate)}%

 

) : (

No residents have reached completion eligibility yet
)}
); } function AuditHistoryTab({ entries, totalItems, currentPage, itemsPerPage, onPageChange, onItemsPerPageChange }: { entries: ChangeLogEntry[]; totalItems: number; currentPage: number; itemsPerPage: number; onPageChange: (page: number) => void; onItemsPerPageChange: (itemsPerPage: number) => void; }) { const { user } = useAuth(); return (

Audit History

{entries.length === 0 ? (

No history entries found.

) : (
{entries.map((entry) => (
{new Date( entry.created_at ).toLocaleDateString('en-US', { month: 'numeric', day: 'numeric', year: 'numeric' })}

{formatHistoryEntry(entry, user?.timezone)}

))}
)}
{totalItems > 0 && ( )}
); }