mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:12:09 +01:00
feat: implement comprehensive enum formatting system
- Create centralized enum formatting utility for database enums - Transform raw enums to human-readable text (SALARY_COMPENSATION → Salary & Compensation) - Apply formatting across sessions list, individual session pages, and charts - Improve color contrast ratios for better WCAG compliance - Add semantic list structure with proper article elements - Enhance accessibility with proper ARIA labels and screen reader support - Fix all instances where users saw ugly database enums in UI
This commit is contained in:
@ -4,6 +4,7 @@ import { useEffect, useState, useCallback, useRef } from "react";
|
|||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Company, MetricsResult, WordCloudWord } from "../../../lib/types";
|
import { Company, MetricsResult, WordCloudWord } from "../../../lib/types";
|
||||||
|
import { formatEnumValue } from "@/lib/format-enums";
|
||||||
import MetricCard from "../../../components/ui/metric-card";
|
import MetricCard from "../../../components/ui/metric-card";
|
||||||
import ModernLineChart from "../../../components/charts/line-chart";
|
import ModernLineChart from "../../../components/charts/line-chart";
|
||||||
import ModernBarChart from "../../../components/charts/bar-chart";
|
import ModernBarChart from "../../../components/charts/bar-chart";
|
||||||
@ -259,10 +260,13 @@ function DashboardContent() {
|
|||||||
const getCategoriesData = () => {
|
const getCategoriesData = () => {
|
||||||
if (!metrics?.categories) return [];
|
if (!metrics?.categories) return [];
|
||||||
|
|
||||||
return Object.entries(metrics.categories).map(([name, value]) => ({
|
return Object.entries(metrics.categories).map(([name, value]) => {
|
||||||
name: name.length > 15 ? name.substring(0, 15) + "..." : name,
|
const formattedName = formatEnumValue(name) || name;
|
||||||
|
return {
|
||||||
|
name: formattedName.length > 15 ? formattedName.substring(0, 15) + "..." : formattedName,
|
||||||
value: value as number,
|
value: value as number,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLanguagesData = () => {
|
const getLanguagesData = () => {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import SessionDetails from "../../../../components/SessionDetails";
|
|||||||
import TranscriptViewer from "../../../../components/TranscriptViewer";
|
import TranscriptViewer from "../../../../components/TranscriptViewer";
|
||||||
import MessageViewer from "../../../../components/MessageViewer";
|
import MessageViewer from "../../../../components/MessageViewer";
|
||||||
import { ChatSession } from "../../../../lib/types";
|
import { ChatSession } from "../../../../lib/types";
|
||||||
|
import { formatCategory } from "@/lib/format-enums";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -181,10 +182,10 @@ export default function SessionViewPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{session.category && session.category !== 'UNRECOGNIZED_OTHER' && session.category !== 'ACCESS_LOGIN' && (
|
{session.category && (
|
||||||
<Badge variant="secondary" className="gap-1">
|
<Badge variant="secondary" className="gap-1">
|
||||||
<Activity className="h-3 w-3" />
|
<Activity className="h-3 w-3" />
|
||||||
{session.category.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase())}
|
{formatCategory(session.category)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{session.language && (
|
{session.language && (
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { formatCategory } from "@/lib/format-enums";
|
||||||
import {
|
import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Search,
|
Search,
|
||||||
@ -223,7 +224,7 @@ export default function SessionsPage() {
|
|||||||
<option value="">All Categories</option>
|
<option value="">All Categories</option>
|
||||||
{filterOptions.categories.map((cat) => (
|
{filterOptions.categories.map((cat) => (
|
||||||
<option key={cat} value={cat}>
|
<option key={cat} value={cat}>
|
||||||
{cat}
|
{formatCategory(cat)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@ -379,12 +380,17 @@ export default function SessionsPage() {
|
|||||||
|
|
||||||
{/* Sessions List */}
|
{/* Sessions List */}
|
||||||
{!loading && !error && sessions.length > 0 && (
|
{!loading && !error && sessions.length > 0 && (
|
||||||
<div className="grid gap-4">
|
<ul role="list" aria-label="Chat sessions" className="grid gap-4">
|
||||||
{sessions.map((session) => (
|
{sessions.map((session) => (
|
||||||
<Card key={session.id} className="hover:shadow-md transition-shadow">
|
<li key={session.id}>
|
||||||
|
<Card className="hover:shadow-md transition-shadow">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<article aria-labelledby={`session-${session.id}-title`}>
|
||||||
|
<header className="flex justify-between items-start mb-4">
|
||||||
<div className="space-y-2 flex-1">
|
<div className="space-y-2 flex-1">
|
||||||
|
<h3 id={`session-${session.id}-title`} className="sr-only">
|
||||||
|
Session {session.sessionId || session.id} from {new Date(session.startTime).toLocaleDateString()}
|
||||||
|
</h3>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Badge variant="outline" className="font-mono text-xs">
|
<Badge variant="outline" className="font-mono text-xs">
|
||||||
ID
|
ID
|
||||||
@ -395,7 +401,7 @@ export default function SessionsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
<Clock className="h-3 w-3 mr-1" />
|
<Clock className="h-3 w-3 mr-1" aria-hidden="true" />
|
||||||
{new Date(session.startTime).toLocaleDateString()}
|
{new Date(session.startTime).toLocaleDateString()}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
@ -404,23 +410,28 @@ export default function SessionsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/dashboard/sessions/${session.id}`}>
|
<Link href={`/dashboard/sessions/${session.id}`}>
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
<Button
|
||||||
<Eye className="h-4 w-4" />
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
aria-label={`View details for session ${session.sessionId || session.id}`}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" aria-hidden="true" />
|
||||||
<span className="hidden sm:inline">View Details</span>
|
<span className="hidden sm:inline">View Details</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
{session.category && session.category !== 'UNRECOGNIZED_OTHER' && session.category !== 'ACCESS_LOGIN' && (
|
{session.category && (
|
||||||
<Badge variant="secondary" className="gap-1">
|
<Badge variant="secondary" className="gap-1">
|
||||||
<Filter className="h-3 w-3" />
|
<Filter className="h-3 w-3" aria-hidden="true" />
|
||||||
{session.category.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase())}
|
{formatCategory(session.category)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{session.language && (
|
{session.language && (
|
||||||
<Badge variant="outline" className="gap-1">
|
<Badge variant="outline" className="gap-1">
|
||||||
<Globe className="h-3 w-3" />
|
<Globe className="h-3 w-3" aria-hidden="true" />
|
||||||
{session.language.toUpperCase()}
|
{session.language.toUpperCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@ -435,10 +446,12 @@ export default function SessionsPage() {
|
|||||||
{session.initialMsg}
|
{session.initialMsg}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
</article>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
|
|||||||
@ -105,13 +105,13 @@
|
|||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.97 0 0);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
--muted: oklch(0.97 0 0);
|
--muted: oklch(0.97 0 0);
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted-foreground: oklch(0.45 0 0);
|
||||||
--accent: oklch(0.97 0 0);
|
--accent: oklch(0.97 0 0);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.15 0 0);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.55 0.245 27.325);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.85 0 0);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.85 0 0);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.6 0 0);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
@ -139,13 +139,13 @@
|
|||||||
--secondary: oklch(0.269 0 0);
|
--secondary: oklch(0.269 0 0);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.269 0 0);
|
||||||
--muted-foreground: oklch(0.65 0 0);
|
--muted-foreground: oklch(0.75 0 0);
|
||||||
--accent: oklch(0.269 0 0);
|
--accent: oklch(0.269 0 0);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.75 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 15%);
|
--border: oklch(1 0 0 / 25%);
|
||||||
--input: oklch(1 0 0 / 20%);
|
--input: oklch(1 0 0 / 30%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.75 0 0);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { ChatSession } from "../lib/types";
|
import { ChatSession } from "../lib/types";
|
||||||
import LanguageDisplay from "./LanguageDisplay";
|
import LanguageDisplay from "./LanguageDisplay";
|
||||||
import CountryDisplay from "./CountryDisplay";
|
import CountryDisplay from "./CountryDisplay";
|
||||||
|
import { formatCategory } from "@/lib/format-enums";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@ -16,13 +17,7 @@ interface SessionDetailsProps {
|
|||||||
* Component to display session details with formatted country and language names
|
* Component to display session details with formatted country and language names
|
||||||
*/
|
*/
|
||||||
export default function SessionDetails({ session }: SessionDetailsProps) {
|
export default function SessionDetails({ session }: SessionDetailsProps) {
|
||||||
// Helper function to format category names
|
// Using centralized formatCategory utility
|
||||||
const formatCategory = (category: string) => {
|
|
||||||
if (category === 'UNRECOGNIZED_OTHER' || category === 'ACCESS_LOGIN') {
|
|
||||||
return null; // Don't show these internal enum values
|
|
||||||
}
|
|
||||||
return category.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase());
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@ -55,7 +50,7 @@ export default function SessionDetails({ session }: SessionDetailsProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{session.category && formatCategory(session.category) && (
|
{session.category && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Category</p>
|
<p className="text-sm text-muted-foreground">Category</p>
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
|
|||||||
69
lib/format-enums.ts
Normal file
69
lib/format-enums.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for formatting database enums into user-friendly text
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Custom mappings for specific enum values that need special formatting
|
||||||
|
const ENUM_MAPPINGS: Record<string, string> = {
|
||||||
|
// HR/Employment related
|
||||||
|
'SALARY_COMPENSATION': 'Salary & Compensation',
|
||||||
|
'CONTRACT_HOURS': 'Contract & Hours',
|
||||||
|
'SCHEDULE_HOURS': 'Schedule & Hours',
|
||||||
|
'LEAVE_VACATION': 'Leave & Vacation',
|
||||||
|
'SICK_LEAVE_RECOVERY': 'Sick Leave & Recovery',
|
||||||
|
'WORKWEAR_STAFF_PASS': 'Workwear & Staff Pass',
|
||||||
|
'TEAM_CONTACTS': 'Team & Contacts',
|
||||||
|
'PERSONAL_QUESTIONS': 'Personal Questions',
|
||||||
|
'PERSONALQUESTIONS': 'Personal Questions',
|
||||||
|
|
||||||
|
// Process related
|
||||||
|
'ONBOARDING': 'Onboarding',
|
||||||
|
'OFFBOARDING': 'Offboarding',
|
||||||
|
|
||||||
|
// Access related
|
||||||
|
'ACCESS_LOGIN': 'Access & Login',
|
||||||
|
|
||||||
|
// Technical/Other
|
||||||
|
'UNRECOGNIZED_OTHER': 'General Inquiry',
|
||||||
|
|
||||||
|
// Add more mappings as needed
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a database enum value into user-friendly text
|
||||||
|
* @param enumValue - The raw enum value from the database
|
||||||
|
* @returns Formatted string or null if input is empty
|
||||||
|
*/
|
||||||
|
export function formatEnumValue(enumValue: string | null | undefined): string | null {
|
||||||
|
if (!enumValue) return null;
|
||||||
|
|
||||||
|
// Check for custom mapping first
|
||||||
|
if (ENUM_MAPPINGS[enumValue]) {
|
||||||
|
return ENUM_MAPPINGS[enumValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: convert snake_case to Title Case
|
||||||
|
return enumValue
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a category enum specifically for display
|
||||||
|
* @param category - The category enum value
|
||||||
|
* @returns Formatted category name or null if empty
|
||||||
|
*/
|
||||||
|
export function formatCategory(category: string | null | undefined): string | null {
|
||||||
|
return formatEnumValue(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats an array of enum values into user-friendly text
|
||||||
|
* @param enumValues - Array of enum values
|
||||||
|
* @returns Array of formatted values (filters out null/undefined)
|
||||||
|
*/
|
||||||
|
export function formatEnumArray(enumValues: (string | null | undefined)[]): string[] {
|
||||||
|
return enumValues
|
||||||
|
.map(value => formatEnumValue(value))
|
||||||
|
.filter((value): value is string => Boolean(value));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user