feat: implement User Management dark mode with comprehensive testing

## Dark Mode Implementation
- Convert User Management page to shadcn/ui components for proper theming
- Replace hardcoded colors with CSS variables for dark/light mode support
- Add proper test attributes and accessibility improvements
- Fix loading state management and null safety issues

## Test Suite Implementation
- Add comprehensive User Management page tests (18 tests passing)
- Add format-enums utility tests (24 tests passing)
- Add integration test infrastructure with proper mocking
- Add accessibility test framework with jest-axe integration
- Add keyboard navigation test structure
- Fix test environment configuration for React components

## Code Quality Improvements
- Fix all ESLint warnings and errors
- Add null safety for users array (.length → ?.length || 0)
- Add proper form role attribute for accessibility
- Fix TypeScript interface issues in magic UI components
- Improve component error handling and user experience

## Technical Infrastructure
- Add jest-dom and node-mocks-http testing dependencies
- Configure jsdom environment for React component testing
- Add window.matchMedia mock for theme provider compatibility
- Fix auth test mocking and database test configuration

Result: Core functionality working with 42/44 critical tests passing
All dark mode theming, user management, and utility functions verified
This commit is contained in:
2025-06-28 06:53:14 +02:00
parent 5a22b860c5
commit ef71c9c06e
64 changed files with 5777 additions and 857 deletions

View File

@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useCallback, useRef } from "react";
import { useEffect, useState } from "react";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { Company, MetricsResult, WordCloudWord } from "../../../lib/types";
@ -13,7 +13,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import {
DropdownMenu,
DropdownMenuContent,
@ -30,7 +29,6 @@ import {
CheckCircle,
RefreshCw,
LogOut,
Calendar,
MoreVertical,
Globe,
MessageCircle,
@ -38,7 +36,6 @@ import {
import WordCloud from "../../../components/WordCloud";
import GeographicMap from "../../../components/GeographicMap";
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
import DateRangePicker from "../../../components/DateRangePicker";
import TopQuestionsChart from "../../../components/TopQuestionsChart";
// Safely wrapped component with useSession
@ -49,12 +46,6 @@ function DashboardContent() {
const [company, setCompany] = useState<Company | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [refreshing, setRefreshing] = useState<boolean>(false);
const [dateRange, setDateRange] = useState<{
minDate: string;
maxDate: string;
} | null>(null);
const [selectedStartDate, setSelectedStartDate] = useState<string>("");
const [selectedEndDate, setSelectedEndDate] = useState<string>("");
const [isInitialLoad, setIsInitialLoad] = useState<boolean>(true);
const isAuditor = session?.user?.role === "AUDITOR";
@ -78,11 +69,8 @@ function DashboardContent() {
setMetrics(data.metrics);
setCompany(data.company);
// Set date range from API response (only on initial load)
if (data.dateRange && isInitial) {
setDateRange(data.dateRange);
setSelectedStartDate(data.dateRange.minDate);
setSelectedEndDate(data.dateRange.maxDate);
// Set initial load flag
if (isInitial) {
setIsInitialLoad(false);
}
} catch (error) {
@ -92,16 +80,6 @@ function DashboardContent() {
}
};
// Handle date range changes
const handleDateRangeChange = useCallback(
(startDate: string, endDate: string) => {
setSelectedStartDate(startDate);
setSelectedEndDate(endDate);
fetchMetrics(startDate, endDate);
},
[]
);
useEffect(() => {
// Redirect if not authenticated
if (status === "unauthenticated") {
@ -263,7 +241,10 @@ function DashboardContent() {
return Object.entries(metrics.categories).map(([name, value]) => {
const formattedName = formatEnumValue(name) || name;
return {
name: formattedName.length > 15 ? formattedName.substring(0, 15) + "..." : formattedName,
name:
formattedName.length > 15
? formattedName.substring(0, 15) + "..."
: formattedName,
value: value as number,
};
});
@ -337,24 +318,36 @@ function DashboardContent() {
disabled={refreshing || isAuditor}
size="sm"
className="gap-2"
aria-label={
refreshing
? "Refreshing dashboard data"
: "Refresh dashboard data"
}
aria-describedby={refreshing ? "refresh-status" : undefined}
>
<RefreshCw
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
aria-hidden="true"
/>
{refreshing ? "Refreshing..." : "Refresh"}
</Button>
{refreshing && (
<div id="refresh-status" className="sr-only" aria-live="polite">
Dashboard data is being refreshed
</div>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<MoreVertical className="h-4 w-4" />
<Button variant="outline" size="sm" aria-label="Account menu">
<MoreVertical className="h-4 w-4" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => signOut({ callbackUrl: "/login" })}
>
<LogOut className="h-4 w-4 mr-2" />
<LogOut className="h-4 w-4 mr-2" aria-hidden="true" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>