Improve Team-indeling dashboard UI and cache invalidation

- Replace 'TEAM' label with Type attribute (Business/Enabling/Staf) in team blocks
- Make Type labels larger (text-sm) and brighter colors
- Make SUBTEAM label less bright (indigo-300) and smaller (text-[10px])
- Add 'FTE' suffix to bandbreedte values in header and application blocks
- Add Platform and Connected Device labels to application blocks
- Show Platform FTE and Workloads FTE separately in Platform blocks
- Add spacing between Regiemodel letter and count value
- Add cache invalidation for Team Dashboard when applications are updated
- Enrich team references with Type attribute in getSubteamToTeamMapping
This commit is contained in:
2026-01-10 02:16:55 +01:00
parent ea1c84262c
commit ca21b9538d
54 changed files with 13444 additions and 1789 deletions

View File

@@ -1,14 +1,112 @@
import { useEffect, useState } from 'react';
import { Routes, Route, Link, useLocation } from 'react-router-dom';
import { useEffect, useState, useRef } from 'react';
import { Routes, Route, Link, useLocation, Navigate, useParams } from 'react-router-dom';
import { clsx } from 'clsx';
import SearchDashboard from './components/SearchDashboard';
import Dashboard from './components/Dashboard';
import ApplicationList from './components/ApplicationList';
import ApplicationDetail from './components/ApplicationDetail';
import ApplicationInfo from './components/ApplicationInfo';
import GovernanceModelHelper from './components/GovernanceModelHelper';
import TeamDashboard from './components/TeamDashboard';
import ConfigurationV25 from './components/ConfigurationV25';
import ReportsDashboard from './components/ReportsDashboard';
import GovernanceAnalysis from './components/GovernanceAnalysis';
import DataModelDashboard from './components/DataModelDashboard';
import FTECalculator from './components/FTECalculator';
import Login from './components/Login';
import { useAuthStore } from './stores/authStore';
// Redirect component for old app-components/overview/:id paths
function RedirectToApplicationEdit() {
const { id } = useParams<{ id: string }>();
return <Navigate to={`/application/${id}/edit`} replace />;
}
// Dropdown menu item type
interface NavItem {
path: string;
label: string;
exact?: boolean;
}
interface NavDropdown {
label: string;
icon?: React.ReactNode;
items: NavItem[];
basePath: string;
}
// Dropdown component for navigation
function NavDropdown({ dropdown, isActive }: { dropdown: NavDropdown; isActive: boolean }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const location = useLocation();
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Close dropdown on route change
useEffect(() => {
setIsOpen(false);
}, [location.pathname]);
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className={clsx(
'flex items-center gap-1 px-3 py-2 rounded-md text-sm font-medium transition-colors',
isActive
? 'bg-blue-50 text-blue-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
)}
>
{dropdown.label}
<svg
className={clsx('w-4 h-4 transition-transform', isOpen && 'rotate-180')}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div className="absolute left-0 mt-1 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
{dropdown.items.map((item) => {
const itemActive = item.exact
? location.pathname === item.path
: location.pathname.startsWith(item.path);
return (
<Link
key={item.path}
to={item.path}
className={clsx(
'block px-4 py-2 text-sm transition-colors',
itemActive
? 'bg-blue-50 text-blue-700'
: 'text-gray-700 hover:bg-gray-50'
)}
>
{item.label}
</Link>
);
})}
</div>
)}
</div>
);
}
function UserMenu() {
const { user, authMethod, logout } = useAuthStore();
const [isOpen, setIsOpen] = useState(false);
@@ -84,12 +182,32 @@ function UserMenu() {
function AppContent() {
const location = useLocation();
const navItems = [
{ path: '/', label: 'Dashboard', exact: true },
{ path: '/applications', label: 'Applicaties', exact: false },
{ path: '/teams', label: 'Team-indeling', exact: true },
{ path: '/configuration', label: 'FTE Config v25', exact: true },
];
// Navigation structure
const appComponentsDropdown: NavDropdown = {
label: 'Application Component',
basePath: '/application',
items: [
{ path: '/app-components', label: 'Dashboard', exact: true },
{ path: '/application/overview', label: 'Overzicht', exact: false },
{ path: '/application/fte-calculator', label: 'FTE Calculator', exact: true },
{ path: '/app-components/fte-config', label: 'FTE Config', exact: true },
],
};
const reportsDropdown: NavDropdown = {
label: 'Rapporten',
basePath: '/reports',
items: [
{ path: '/reports', label: 'Overzicht', exact: true },
{ path: '/reports/team-dashboard', label: 'Team-indeling', exact: true },
{ path: '/reports/governance-analysis', label: 'Analyse Regiemodel', exact: true },
{ path: '/reports/data-model', label: 'Datamodel', exact: true },
],
};
const isAppComponentsActive = location.pathname.startsWith('/app-components') || location.pathname.startsWith('/application');
const isReportsActive = location.pathname.startsWith('/reports');
const isDashboardActive = location.pathname === '/';
return (
<div className="min-h-screen bg-white">
@@ -98,39 +216,35 @@ function AppContent() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-8">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">ZiRA</span>
</div>
<Link to="/" className="flex items-center space-x-3">
<img src="/logo-zuyderland.svg" alt="Zuyderland" className="w-9 h-9" />
<div>
<h1 className="text-lg font-semibold text-gray-900">
Classificatie Tool
Analyse Tool
</h1>
<p className="text-xs text-gray-500">Zuyderland CMDB</p>
</div>
</div>
</Link>
<nav className="hidden md:flex space-x-1">
{navItems.map((item) => {
const isActive = item.exact
? location.pathname === item.path
: location.pathname.startsWith(item.path);
<nav className="hidden md:flex items-center space-x-1">
{/* Dashboard (Search) */}
<Link
to="/"
className={clsx(
'px-3 py-2 rounded-md text-sm font-medium transition-colors',
isDashboardActive
? 'bg-blue-50 text-blue-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
)}
>
Dashboard
</Link>
return (
<Link
key={item.path}
to={item.path}
className={clsx(
'px-3 py-2 rounded-md text-sm font-medium transition-colors',
isActive
? 'bg-blue-50 text-blue-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
)}
>
{item.label}
</Link>
);
})}
{/* Application Component Dropdown */}
<NavDropdown dropdown={appComponentsDropdown} isActive={isAppComponentsActive} />
{/* Reports Dropdown */}
<NavDropdown dropdown={reportsDropdown} isActive={isReportsActive} />
</nav>
</div>
@@ -142,9 +256,30 @@ function AppContent() {
{/* Main content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/applications" element={<ApplicationList />} />
<Route path="/applications/:id" element={<ApplicationDetail />} />
{/* Main Dashboard (Search) */}
<Route path="/" element={<SearchDashboard />} />
{/* Application routes (new structure) */}
<Route path="/application/overview" element={<ApplicationList />} />
<Route path="/application/fte-calculator" element={<FTECalculator />} />
<Route path="/application/:id" element={<ApplicationInfo />} />
<Route path="/application/:id/edit" element={<GovernanceModelHelper />} />
{/* Application Component routes */}
<Route path="/app-components" element={<Dashboard />} />
<Route path="/app-components/fte-config" element={<ConfigurationV25 />} />
{/* Reports routes */}
<Route path="/reports" element={<ReportsDashboard />} />
<Route path="/reports/team-dashboard" element={<TeamDashboard />} />
<Route path="/reports/governance-analysis" element={<GovernanceAnalysis />} />
<Route path="/reports/data-model" element={<DataModelDashboard />} />
{/* Legacy redirects for bookmarks - redirect old paths to new ones */}
<Route path="/app-components/overview" element={<Navigate to="/application/overview" replace />} />
<Route path="/app-components/overview/:id" element={<RedirectToApplicationEdit />} />
<Route path="/applications" element={<Navigate to="/application/overview" replace />} />
<Route path="/applications/:id" element={<RedirectToApplicationEdit />} />
<Route path="/teams" element={<TeamDashboard />} />
<Route path="/configuration" element={<ConfigurationV25 />} />
</Routes>
@@ -178,12 +313,12 @@ function App() {
}
// Show login if OAuth is enabled and not authenticated
if (config?.oauthEnabled && !isAuthenticated) {
if (config?.authMethod === 'oauth' && !isAuthenticated) {
return <Login />;
}
// Show login if nothing is configured
if (!config?.oauthEnabled && !config?.serviceAccountEnabled) {
if (config?.authMethod === 'none') {
return <Login />;
}