import { useEffect, useState, useCallback } from 'react'; import { useNavigate, useSearchParams, Link } from 'react-router-dom'; import { clsx } from 'clsx'; import { searchApplications, getReferenceData } from '../services/api'; import { useSearchStore } from '../stores/searchStore'; import { useNavigationStore } from '../stores/navigationStore'; import type { ApplicationListItem, SearchResult, ReferenceValue, ApplicationStatus } from '../types'; const ALL_STATUSES: ApplicationStatus[] = [ 'In Production', 'Implementation', 'Proof of Concept', 'End of support', 'End of life', 'Deprecated', 'Shadow IT', 'Closed', 'Undefined', ]; export default function ApplicationList() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const { filters, currentPage, pageSize, setSearchText, setStatuses, setApplicationFunction, setGovernanceModel, setApplicationSubteam, setApplicationType, setOrganisation, setHostingType, setBusinessImportance, setCurrentPage, resetFilters, } = useSearchStore(); const { setNavigationContext } = useNavigationStore(); const [result, setResult] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [organisations, setOrganisations] = useState([]); const [hostingTypes, setHostingTypes] = useState([]); const [businessImportanceOptions, setBusinessImportanceOptions] = useState([]); const [applicationSubteams, setApplicationSubteams] = useState([]); const [showFilters, setShowFilters] = useState(true); // Sync URL params with store on mount useEffect(() => { const pageParam = searchParams.get('page'); if (pageParam) { const page = parseInt(pageParam, 10); if (!isNaN(page) && page > 0 && page !== currentPage) { setCurrentPage(page); } } }, []); // Only run on mount // Update URL when page changes useEffect(() => { const currentUrlPage = searchParams.get('page'); const currentUrlPageNum = currentUrlPage ? parseInt(currentUrlPage, 10) : 1; if (currentPage !== currentUrlPageNum) { if (currentPage === 1) { // Remove page param when on page 1 searchParams.delete('page'); } else { searchParams.set('page', currentPage.toString()); } setSearchParams(searchParams, { replace: true }); } }, [currentPage, searchParams, setSearchParams]); const fetchApplications = useCallback(async () => { setLoading(true); setError(null); try { const data = await searchApplications(filters, currentPage, pageSize); setResult(data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load applications'); } finally { setLoading(false); } }, [filters, currentPage, pageSize]); useEffect(() => { fetchApplications(); }, [fetchApplications]); useEffect(() => { async function loadReferenceData() { try { const data = await getReferenceData(); setOrganisations(data.organisations); setHostingTypes(data.hostingTypes); setBusinessImportanceOptions(data.businessImportance || []); setApplicationSubteams(data.applicationSubteams || []); } catch (err) { console.error('Failed to load reference data', err); } } loadReferenceData(); }, []); // Update navigation context whenever results change, so "Opslaan & Volgende" works // even when user opens an application in a new tab useEffect(() => { if (result && result.applications.length > 0) { const allIds = result.applications.map((a) => a.id); // Preserve current index if it's still valid, otherwise reset to 0 setNavigationContext(allIds, filters, 0); } }, [result, filters, setNavigationContext]); const handleRowClick = (app: ApplicationListItem, index: number, event: React.MouseEvent) => { // Update current index in navigation context if (result) { const allIds = result.applications.map((a) => a.id); setNavigationContext(allIds, filters, index); } // Let the browser handle CTRL+click / CMD+click / middle-click natively for new tab // Only navigate programmatically for regular clicks if (!event.ctrlKey && !event.metaKey && !event.shiftKey && event.button === 0) { event.preventDefault(); navigate(`/application/${app.id}`); } }; const toggleStatus = (status: ApplicationStatus) => { const current = filters.statuses || []; if (current.includes(status)) { setStatuses(current.filter((s) => s !== status)); } else { setStatuses([...current, status]); } }; return (
{/* Page header */}

Applicaties

Zoek en classificeer applicatiecomponenten

{/* Search and filters */}
{/* Search bar */}
setSearchText(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
{/* Filters */} {showFilters && (

Filters

{/* Status filter */}
{ALL_STATUSES.map((status) => ( ))}
{/* Classification filters */}
{(['all', 'filled', 'empty'] as const).map((value) => ( ))}
{(['all', 'filled', 'empty'] as const).map((value) => ( ))}
{(['all', 'filled', 'empty'] as const).map((value) => ( ))}
{/* Dropdown filters */}
)} {/* Results count */}
{result ? ( <> Resultaten: {result.totalCount} applicaties ) : ( 'Laden...' )}
{/* Results table */} {loading ? (
) : error ? (
{error}
) : (
{result?.applications.map((app, index) => ( ))}
# Naam Status AppFunctie Governance Benodigde inspanning
handleRowClick(app, index, e)} className="block px-4 py-3 text-sm text-gray-500" > {(currentPage - 1) * pageSize + index + 1} handleRowClick(app, index, e)} className="block px-4 py-3" >
{app.name}
{app.key}
handleRowClick(app, index, e)} className="block px-4 py-3" > handleRowClick(app, index, e)} className="block px-4 py-3" > {app.applicationFunctions && app.applicationFunctions.length > 0 ? (
{app.applicationFunctions.map((func) => ( {func.name}{func.applicationFunctionCategory ? ` (${func.applicationFunctionCategory.name})` : ''} ))}
) : ( Leeg )}
handleRowClick(app, index, e)} className="block px-4 py-3" > {app.governanceModel ? ( {app.governanceModel.name} ) : ( Leeg )} handleRowClick(app, index, e)} className="block px-4 py-3 text-sm text-gray-900" > {app.requiredEffortApplicationManagement !== null ? ( {app.requiredEffortApplicationManagement.toFixed(2)} FTE ) : ( - )}
)} {/* Pagination */} {result && result.totalPages > 1 && (
{currentPage > 1 ? ( setCurrentPage(currentPage - 1)} className="btn btn-secondary" > Vorige ) : ( )} Pagina {currentPage} van {result.totalPages} {currentPage < result.totalPages ? ( setCurrentPage(currentPage + 1)} className="btn btn-secondary" > Volgende ) : ( )}
)}
); } export function StatusBadge({ status }: { status: string | null }) { const statusColors: Record = { 'Closed': 'badge-dark-red', 'Deprecated': 'badge-yellow', 'End of life': 'badge-light-red', 'End of support': 'badge-light-red', 'Implementation': 'badge-blue', 'In Production': 'badge-dark-green', 'Proof of Concept': 'badge-light-green', 'Shadow IT': 'badge-black', 'Undefined': 'badge-gray', }; if (!status) return -; return ( {status} ); } export function BusinessImportanceBadge({ importance }: { importance: string | null }) { // Helper function to get the number prefix from the importance string const getImportanceNumber = (value: string | null): string | null => { if (!value) return null; // Match patterns like "0 - Critical Infrastructure" or just "0" const match = value.match(/^(\d+)/); return match ? match[1] : null; }; const importanceNumber = getImportanceNumber(importance); // Map importance number to icon type and color const getImportanceConfig = (num: string | null) => { switch (num) { case '0': return { icon: 'warning', color: 'badge-darker-red', label: importance || '0 - Critical Infrastructure', }; case '1': return { icon: 'exclamation', color: 'badge-dark-red', label: importance || '1 - Critical', }; case '2': return { icon: 'exclamation', color: 'badge-red', label: importance || '2 - Highest', }; case '3': return { icon: 'circle', color: 'badge-yellow-orange', label: importance || '3 - High', }; case '4': return { icon: 'circle', color: 'badge-dark-blue', label: importance || '4 - Medium', }; case '5': return { icon: 'circle', color: 'badge-light-blue', label: importance || '5 - Low', }; case '6': return { icon: 'circle', color: 'badge-lighter-blue', label: importance || '6 - Lowest', }; case '9': return { icon: 'question', color: 'badge-black', label: importance || '9 - Unknown', }; default: return { icon: null, color: 'badge-gray', label: importance || '-', }; } }; if (!importance) return -; const config = getImportanceConfig(importanceNumber); // Icon components const WarningIcon = () => ( ); const ExclamationIcon = () => ( ); const CircleIcon = () => ( ); const QuestionIcon = () => ( ); const renderIcon = () => { switch (config.icon) { case 'warning': return ; case 'exclamation': return ; case 'circle': return ; case 'question': return ; default: return null; } }; return ( {renderIcon()} {config.label} ); }