Files
hr-assessment-system/app/admin/analytics/page.tsx
2025-09-29 21:00:48 +08:00

431 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import { ProtectedRoute } from "@/components/protected-route"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress"
import { Badge } from "@/components/ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Brain, Lightbulb, BarChart3, ArrowLeft, TrendingUp, Users, Award, Target } from "lucide-react"
import Link from "next/link"
import { useAuth, type User } from "@/lib/hooks/use-auth"
interface DepartmentStats {
department: string
totalUsers: number
participatedUsers: number
participationRate: number
averageLogicScore: number
averageCreativeScore: number
averageCombinedScore: number
overallAverage: number
topPerformer: string | null
testCounts: {
logic: number
creative: number
combined: number
}
}
interface TestResult {
userId: string
userName: string
userDepartment: string
type: "logic" | "creative" | "combined"
score: number
completedAt: string
}
export default function AnalyticsPage() {
return (
<ProtectedRoute adminOnly>
<AnalyticsContent />
</ProtectedRoute>
)
}
function AnalyticsContent() {
const { user } = useAuth()
const [departmentStats, setDepartmentStats] = useState<DepartmentStats[]>([])
const [selectedDepartment, setSelectedDepartment] = useState("all")
const [overallStats, setOverallStats] = useState({
totalUsers: 0,
totalParticipants: 0,
overallParticipationRate: 0,
averageScore: 0,
totalTests: 0,
})
const departments = ["人力資源部", "資訊技術部", "財務部", "行銷部", "業務部", "研發部", "客服部", "其他"]
useEffect(() => {
loadAnalyticsData()
}, [])
const loadAnalyticsData = () => {
// Load users
const users: User[] = JSON.parse(localStorage.getItem("hr_users") || "[]")
// Load all test results
const allResults: TestResult[] = []
users.forEach((user: User) => {
// Check for logic test results
const logicKey = `logicTestResults_${user.id}`
const logicResults = localStorage.getItem(logicKey)
if (logicResults) {
const data = JSON.parse(logicResults)
allResults.push({
userId: user.id,
userName: user.name,
userDepartment: user.department,
type: "logic",
score: data.score,
completedAt: data.completedAt,
})
}
// Check for creative test results
const creativeKey = `creativeTestResults_${user.id}`
const creativeResults = localStorage.getItem(creativeKey)
if (creativeResults) {
const data = JSON.parse(creativeResults)
allResults.push({
userId: user.id,
userName: user.name,
userDepartment: user.department,
type: "creative",
score: data.score,
completedAt: data.completedAt,
})
}
// Check for combined test results
const combinedKey = `combinedTestResults_${user.id}`
const combinedResults = localStorage.getItem(combinedKey)
if (combinedResults) {
const data = JSON.parse(combinedResults)
allResults.push({
userId: user.id,
userName: user.name,
userDepartment: user.department,
type: "combined",
score: data.overallScore,
completedAt: data.completedAt,
})
}
})
// Calculate department statistics
const deptStats: DepartmentStats[] = departments
.map((dept) => {
const deptUsers = users.filter((u) => u.department === dept)
const deptResults = allResults.filter((r) => r.userDepartment === dept)
const participatedUsers = new Set(deptResults.map((r) => r.userId)).size
// Calculate average scores by test type
const logicResults = deptResults.filter((r) => r.type === "logic")
const creativeResults = deptResults.filter((r) => r.type === "creative")
const combinedResults = deptResults.filter((r) => r.type === "combined")
const averageLogicScore =
logicResults.length > 0
? Math.round(logicResults.reduce((sum, r) => sum + r.score, 0) / logicResults.length)
: 0
const averageCreativeScore =
creativeResults.length > 0
? Math.round(creativeResults.reduce((sum, r) => sum + r.score, 0) / creativeResults.length)
: 0
const averageCombinedScore =
combinedResults.length > 0
? Math.round(combinedResults.reduce((sum, r) => sum + r.score, 0) / combinedResults.length)
: 0
// Calculate overall average
const allScores = [averageLogicScore, averageCreativeScore, averageCombinedScore].filter((s) => s > 0)
const overallAverage =
allScores.length > 0 ? Math.round(allScores.reduce((sum, s) => sum + s, 0) / allScores.length) : 0
// Find top performer
const userScores = new Map<string, number[]>()
deptResults.forEach((result) => {
if (!userScores.has(result.userId)) {
userScores.set(result.userId, [])
}
userScores.get(result.userId)!.push(result.score)
})
let topPerformer: string | null = null
let topScore = 0
userScores.forEach((scores, userId) => {
const avgScore = scores.reduce((sum, s) => sum + s, 0) / scores.length
if (avgScore > topScore) {
topScore = avgScore
const user = users.find((u) => u.id === userId)
topPerformer = user ? user.name : null
}
})
return {
department: dept,
totalUsers: deptUsers.length,
participatedUsers,
participationRate: deptUsers.length > 0 ? Math.round((participatedUsers / deptUsers.length) * 100) : 0,
averageLogicScore,
averageCreativeScore,
averageCombinedScore,
overallAverage,
topPerformer,
testCounts: {
logic: logicResults.length,
creative: creativeResults.length,
combined: combinedResults.length,
},
}
})
.filter((stat) => stat.totalUsers > 0) // Only show departments with users
setDepartmentStats(deptStats)
// Calculate overall statistics
const totalUsers = users.length
const totalParticipants = new Set(allResults.map((r) => r.userId)).size
const overallParticipationRate = totalUsers > 0 ? Math.round((totalParticipants / totalUsers) * 100) : 0
const averageScore =
allResults.length > 0 ? Math.round(allResults.reduce((sum, r) => sum + r.score, 0) / allResults.length) : 0
setOverallStats({
totalUsers,
totalParticipants,
overallParticipationRate,
averageScore,
totalTests: allResults.length,
})
}
const getScoreColor = (score: number) => {
if (score >= 90) return "text-green-600"
if (score >= 80) return "text-blue-600"
if (score >= 70) return "text-yellow-600"
if (score >= 60) return "text-orange-600"
return "text-red-600"
}
const getParticipationColor = (rate: number) => {
if (rate >= 80) return "text-green-600"
if (rate >= 60) return "text-blue-600"
if (rate >= 40) return "text-yellow-600"
return "text-red-600"
}
const filteredStats =
selectedDepartment === "all"
? departmentStats
: departmentStats.filter((stat) => stat.department === selectedDepartment)
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card/50 backdrop-blur-sm">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild>
<Link href="/dashboard">
<ArrowLeft className="w-4 h-4 mr-2" />
<span className="hidden sm:inline"></span>
</Link>
</Button>
<div>
<h1 className="text-xl font-bold text-foreground"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
</div>
</header>
<div className="container mx-auto px-4 py-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Overall Statistics */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 md:gap-6">
<Card>
<CardContent className="p-3 md:p-6 text-center">
<div className="w-8 h-8 md:w-12 md:h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-2 md:mb-3">
<Users className="w-4 h-4 md:w-6 md:h-6 text-primary" />
</div>
<div className="text-lg md:text-2xl font-bold text-foreground mb-1">{overallStats.totalUsers}</div>
<div className="text-xs md:text-sm text-muted-foreground"></div>
</CardContent>
</Card>
<Card>
<CardContent className="p-3 md:p-6 text-center">
<div className="w-8 h-8 md:w-12 md:h-12 bg-blue-500/10 rounded-full flex items-center justify-center mx-auto mb-2 md:mb-3">
<Target className="w-4 h-4 md:w-6 md:h-6 text-blue-500" />
</div>
<div className="text-lg md:text-2xl font-bold text-foreground mb-1">{overallStats.totalParticipants}</div>
<div className="text-xs md:text-sm text-muted-foreground"></div>
</CardContent>
</Card>
<Card>
<CardContent className="p-3 md:p-6 text-center">
<div className="w-8 h-8 md:w-12 md:h-12 bg-green-500/10 rounded-full flex items-center justify-center mx-auto mb-2 md:mb-3">
<TrendingUp className="w-4 h-4 md:w-6 md:h-6 text-green-500" />
</div>
<div className="text-lg md:text-2xl font-bold text-foreground mb-1">{overallStats.overallParticipationRate}%</div>
<div className="text-xs md:text-sm text-muted-foreground"></div>
</CardContent>
</Card>
<Card>
<CardContent className="p-3 md:p-6 text-center">
<div className="w-8 h-8 md:w-12 md:h-12 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-2 md:mb-3">
<Award className="w-4 h-4 md:w-6 md:h-6 text-accent" />
</div>
<div className="text-lg md:text-2xl font-bold text-foreground mb-1">{overallStats.averageScore}</div>
<div className="text-xs md:text-sm text-muted-foreground"></div>
</CardContent>
</Card>
<Card>
<CardContent className="p-3 md:p-6 text-center">
<div className="w-8 h-8 md:w-12 md:h-12 bg-purple-500/10 rounded-full flex items-center justify-center mx-auto mb-2 md:mb-3">
<BarChart3 className="w-4 h-4 md:w-6 md:h-6 text-purple-500" />
</div>
<div className="text-lg md:text-2xl font-bold text-foreground mb-1">{overallStats.totalTests}</div>
<div className="text-xs md:text-sm text-muted-foreground"></div>
</CardContent>
</Card>
</div>
{/* Department Filter */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="max-w-xs">
<Select value={selectedDepartment} onValueChange={setSelectedDepartment}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{departments.map((dept) => (
<SelectItem key={dept} value={dept}>
{dept}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Department Statistics */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filteredStats.map((stat) => (
<Card key={stat.department} className="border-2">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>{stat.department}</span>
<Badge variant="outline">
{stat.participatedUsers}/{stat.totalUsers}
</Badge>
</CardTitle>
<CardDescription>
<span className={getParticipationColor(stat.participationRate)}>{stat.participationRate}%</span>
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Participation Rate */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium"></span>
<span className="text-sm font-medium">{stat.participationRate}%</span>
</div>
<Progress value={stat.participationRate} className="h-2" />
</div>
{/* Test Scores */}
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-2">
<Brain className="w-5 h-5 text-primary" />
</div>
<div className={`text-lg font-bold ${getScoreColor(stat.averageLogicScore)}`}>
{stat.averageLogicScore || "-"}
</div>
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground">({stat.testCounts.logic} )</div>
</div>
<div className="text-center">
<div className="w-10 h-10 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-2">
<Lightbulb className="w-5 h-5 text-accent" />
</div>
<div className={`text-lg font-bold ${getScoreColor(stat.averageCreativeScore)}`}>
{stat.averageCreativeScore || "-"}
</div>
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground">({stat.testCounts.creative} )</div>
</div>
<div className="text-center">
<div className="w-10 h-10 bg-gradient-to-r from-primary to-accent rounded-full flex items-center justify-center mx-auto mb-2">
<BarChart3 className="w-5 h-5 text-white" />
</div>
<div className={`text-lg font-bold ${getScoreColor(stat.averageCombinedScore)}`}>
{stat.averageCombinedScore || "-"}
</div>
<div className="text-xs text-muted-foreground"></div>
<div className="text-xs text-muted-foreground">({stat.testCounts.combined} )</div>
</div>
</div>
{/* Overall Average */}
<div className="border-t pt-4">
<div className="flex justify-between items-center">
<span className="font-medium"></span>
<span className={`text-xl font-bold ${getScoreColor(stat.overallAverage)}`}>
{stat.overallAverage || "-"}
</span>
</div>
</div>
{/* Top Performer */}
{stat.topPerformer && (
<div className="border-t pt-4">
<div className="flex justify-between items-center">
<span className="font-medium"></span>
<Badge className="bg-yellow-500 text-white">
<Award className="w-3 h-3 mr-1" />
{stat.topPerformer}
</Badge>
</div>
</div>
)}
</CardContent>
</Card>
))}
</div>
{filteredStats.length === 0 && (
<Card>
<CardContent className="p-8 text-center">
<div className="text-muted-foreground">
{selectedDepartment === "all" ? "暫無部門數據" : "該部門暫無數據"}
</div>
</CardContent>
</Card>
)}
</div>
</div>
</div>
)
}