wulf-pulse/components/analytics/ScoreCard.tsx

534 lines
18 KiB
TypeScript
Raw Normal View History

'use client';
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import {
Activity,
FileText,
Clock,
TrendingUp,
TrendingDown,
Minus,
Info,
CheckCircle,
AlertTriangle,
XCircle
} from 'lucide-react';
import { cn } from '@/lib/utils';
import {
ActivityScore,
ContentScore,
TimelinessScore,
TimeEntryAnalysis,
AggregateAnalysis
} from '@/lib/types/analytics';
interface ScoreCardProps {
title: string;
score: number;
description?: string;
trend?: 'up' | 'down' | 'neutral';
trendValue?: number;
icon?: React.ReactNode;
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function ScoreCard({
title,
score,
description,
trend,
trendValue,
icon,
size = 'md',
className
}: ScoreCardProps) {
const percentage = Math.round(score * 100);
const scoreColor = getScoreColor(score);
const scoreLabel = getScoreLabel(score);
const sizeClasses = {
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
};
const titleSizeClasses = {
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg',
};
const scoreSizeClasses = {
sm: 'text-2xl',
md: 'text-3xl',
lg: 'text-4xl',
};
return (
<Card className={cn(sizeClasses[size], className)}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className={cn("flex items-center gap-2", titleSizeClasses[size])}>
{icon}
{title}
</CardTitle>
{trend && (
<div className="flex items-center gap-1">
{trend === 'up' && <TrendingUp className="h-4 w-4 text-green-500" />}
{trend === 'down' && <TrendingDown className="h-4 w-4 text-red-500" />}
{trend === 'neutral' && <Minus className="h-4 w-4 text-gray-500" />}
{(trendValue !== undefined) && (
<span className={cn(
"text-sm font-medium",
trendValue > 0 ? "text-green-600" : trendValue < 0 ? "text-red-600" : "text-gray-600"
)}>
{trendValue > 0 ? '+' : ''}{trendValue}%
</span>
)}
</div>
)}
</div>
{description && (
<p className="text-sm text-gray-600">{description}</p>
)}
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Score Display */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={cn(scoreSizeClasses[size], "font-bold", scoreColor.text)}>
{percentage}%
</span>
<Badge variant="outline" className={scoreColor.badge}>
{scoreLabel}
</Badge>
</div>
<div className="flex items-center gap-1">
{getScoreIcon(score)}
<span className="text-sm text-gray-500">{scoreLabel}</span>
</div>
</div>
{/* Progress Bar */}
<div className="space-y-2">
<Progress
value={percentage}
className="h-2"
/>
<div className="flex justify-between text-xs text-gray-500">
<span>Poor</span>
<span>Average</span>
<span>Excellent</span>
</div>
</div>
</div>
</CardContent>
</Card>
);
}
interface ActivityScoreCardProps {
title: string;
score: ActivityScore;
icon?: React.ReactNode;
className?: string;
}
export function ActivityScoreCard({ title, score, icon, className }: ActivityScoreCardProps) {
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{icon || <Activity className="h-5 w-5 text-blue-500" />}
{title}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Overall Score */}
<div className="text-center">
<div className="text-3xl font-bold text-blue-600">
{Math.round(score.score * 100)}%
</div>
<div className="text-sm text-gray-600">Activity Score</div>
</div>
{/* Breakdown */}
<div className="space-y-3">
<h4 className="font-medium text-sm">Score Breakdown</h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm">Completeness</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.completeness * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.completeness * 100)}%
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Consistency</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.consistency * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.consistency * 100)}%
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Duration</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.duration * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.duration * 100)}%
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Categorization</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.categorization * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.categorization * 100)}%
</span>
</div>
</div>
</div>
</div>
{/* Positive Factors */}
{score.factors.length > 0 && (
<div className="space-y-2">
<h4 className="font-medium text-sm flex items-center gap-1">
<CheckCircle className="h-4 w-4 text-green-500" />
Positive Factors
</h4>
<div className="flex flex-wrap gap-1">
{score.factors.map((factor: string, index: number) => (
<Badge key={index} variant="secondary" className="text-xs">
{factor}
</Badge>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}
interface ContentScoreCardProps {
title: string;
score: ContentScore;
icon?: React.ReactNode;
className?: string;
}
export function ContentScoreCard({ title, score, icon, className }: ContentScoreCardProps) {
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{icon || <FileText className="h-5 w-5 text-green-500" />}
{title}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Overall Score */}
<div className="text-center">
<div className="text-3xl font-bold text-green-600">
{Math.round(score.score * 100)}%
</div>
<div className="text-sm text-gray-600">Content Score</div>
</div>
{/* Breakdown */}
<div className="space-y-3">
<h4 className="font-medium text-sm">Score Breakdown</h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm">Notes Quality</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.notesQuality * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.notesQuality * 100)}%
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Title Clarity</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.titleClarity * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.titleClarity * 100)}%
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Internal Notes</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.internalNotes * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.internalNotes * 100)}%
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Technical Detail</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.technicalDetail * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.technicalDetail * 100)}%
</span>
</div>
</div>
</div>
</div>
{/* Positive Factors */}
{score.factors.length > 0 && (
<div className="space-y-2">
<h4 className="font-medium text-sm flex items-center gap-1">
<CheckCircle className="h-4 w-4 text-green-500" />
Positive Factors
</h4>
<div className="flex flex-wrap gap-1">
{score.factors.map((factor: string, index: number) => (
<Badge key={index} variant="secondary" className="text-xs">
{factor}
</Badge>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}
interface TimelinessScoreCardProps {
title: string;
score: TimelinessScore;
icon?: React.ReactNode;
className?: string;
}
export function TimelinessScoreCard({ title, score, icon, className }: TimelinessScoreCardProps) {
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{icon || <Clock className="h-5 w-5 text-orange-500" />}
{title}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Overall Score */}
<div className="text-center">
<div className="text-3xl font-bold text-orange-600">
{Math.round(score.score * 100)}%
</div>
<div className="text-sm text-gray-600">Timeliness Score</div>
</div>
{/* Breakdown */}
<div className="space-y-3">
<h4 className="font-medium text-sm">Score Breakdown</h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm">Entry Delay</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.entryDelay * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.entryDelay * 100)}%
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Business Hours</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.businessHours * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.businessHours * 100)}%
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Regularity</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.regularity * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.regularity * 100)}%
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Approval Time</span>
<div className="flex items-center gap-2">
<Progress value={score.breakdown.approvalTimeliness * 100} className="w-20 h-2" />
<span className="text-sm font-medium w-10 text-right">
{Math.round(score.breakdown.approvalTimeliness * 100)}%
</span>
</div>
</div>
</div>
</div>
{/* Positive Factors */}
{score.factors.length > 0 && (
<div className="space-y-2">
<h4 className="font-medium text-sm flex items-center gap-1">
<CheckCircle className="h-4 w-4 text-green-500" />
Positive Factors
</h4>
<div className="flex flex-wrap gap-1">
{score.factors.map((factor: string, index: number) => (
<Badge key={index} variant="secondary" className="text-xs">
{factor}
</Badge>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}
interface AggregateScoreCardProps {
analysis: AggregateAnalysis;
className?: string;
}
export function AggregateScoreCard({ analysis, className }: AggregateScoreCardProps) {
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-purple-500" />
Overall Performance
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Overall Score */}
<div className="text-center">
<div className="text-4xl font-bold text-purple-600">
{Math.round(analysis.scores.overall * 100)}%
</div>
<div className="text-sm text-gray-600">Overall Score</div>
</div>
{/* Individual Scores */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center p-3 bg-blue-50 rounded-lg">
<div className="text-2xl font-bold text-blue-600">
{Math.round(analysis.scores.activity * 100)}%
</div>
<div className="text-sm text-gray-600">Activity</div>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<div className="text-2xl font-bold text-green-600">
{Math.round(analysis.scores.content * 100)}%
</div>
<div className="text-sm text-gray-600">Content</div>
</div>
<div className="text-center p-3 bg-orange-50 rounded-lg">
<div className="text-2xl font-bold text-orange-600">
{Math.round(analysis.scores.timeliness * 100)}%
</div>
<div className="text-sm text-gray-600">Timeliness</div>
</div>
</div>
{/* Summary Stats */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Total Entries:</span>
<span className="font-medium">{analysis.totalEntries}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Total Hours:</span>
<span className="font-medium">{Number(analysis.totalHours).toFixed(1)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Avg Hours/Entry:</span>
<span className="font-medium">{Number(analysis.averageHoursPerEntry).toFixed(1)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Date Range:</span>
<span className="font-medium">
{analysis.dateRange.latest.toLocaleDateString()}
</span>
</div>
</div>
</CardContent>
</Card>
);
}
// Helper functions
function getScoreColor(score: number) {
if (score >= 0.8) {
return {
text: 'text-green-600',
badge: 'bg-green-100 text-green-800 border-green-200',
progress: 'bg-green-500',
};
}
if (score >= 0.6) {
return {
text: 'text-yellow-600',
badge: 'bg-yellow-100 text-yellow-800 border-yellow-200',
progress: 'bg-yellow-500',
};
}
return {
text: 'text-red-600',
badge: 'bg-red-100 text-red-800 border-red-200',
progress: 'bg-red-500',
};
}
function getScoreLabel(score: number) {
if (score >= 0.8) return 'Excellent';
if (score >= 0.6) return 'Good';
if (score >= 0.4) return 'Average';
return 'Poor';
}
function getScoreIcon(score: number) {
if (score >= 0.8) {
return <CheckCircle className="h-4 w-4 text-green-500" />;
}
if (score >= 0.6) {
return <AlertTriangle className="h-4 w-4 text-yellow-500" />;
}
return <XCircle className="h-4 w-4 text-red-500" />;
}