實作部門分析數據
This commit is contained in:
@@ -57,153 +57,25 @@ function AnalyticsContent() {
|
|||||||
totalTests: 0,
|
totalTests: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const departments = ["人力資源部", "資訊技術部", "財務部", "行銷部", "業務部", "研發部", "客服部", "其他"]
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAnalyticsData()
|
loadAnalyticsData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadAnalyticsData = () => {
|
const loadAnalyticsData = async () => {
|
||||||
// Load users
|
try {
|
||||||
const users: User[] = JSON.parse(localStorage.getItem("hr_users") || "[]")
|
const response = await fetch('/api/admin/analytics/departments')
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
// Load all test results
|
if (data.success) {
|
||||||
const allResults: TestResult[] = []
|
setDepartmentStats(data.data.departmentStats)
|
||||||
|
setOverallStats(data.data.overallStats)
|
||||||
users.forEach((user: User) => {
|
} else {
|
||||||
// Check for logic test results
|
console.error('獲取部門分析數據失敗:', data.message)
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
// Check for creative test results
|
console.error('獲取部門分析數據錯誤:', error)
|
||||||
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) => {
|
const getScoreColor = (score: number) => {
|
||||||
@@ -314,9 +186,9 @@ function AnalyticsContent() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">所有部門</SelectItem>
|
<SelectItem value="all">所有部門</SelectItem>
|
||||||
{departments.map((dept) => (
|
{departmentStats.map((stat) => (
|
||||||
<SelectItem key={dept} value={dept}>
|
<SelectItem key={stat.department} value={stat.department}>
|
||||||
{dept}
|
{stat.department}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
180
app/api/admin/analytics/departments/route.ts
Normal file
180
app/api/admin/analytics/departments/route.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import { executeQuery } from "@/lib/database/connection"
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// 獲取所有用戶
|
||||||
|
const users = await executeQuery(`
|
||||||
|
SELECT id, name, department, role
|
||||||
|
FROM users
|
||||||
|
ORDER BY department, name
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 獲取所有測試結果
|
||||||
|
const testResults = await executeQuery(`
|
||||||
|
SELECT
|
||||||
|
tr.user_id,
|
||||||
|
tr.test_type,
|
||||||
|
tr.score,
|
||||||
|
tr.completed_at,
|
||||||
|
u.name as user_name,
|
||||||
|
u.department
|
||||||
|
FROM test_results tr
|
||||||
|
LEFT JOIN users u ON tr.user_id = u.id
|
||||||
|
ORDER BY u.department, tr.completed_at DESC
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 獲取綜合測試結果
|
||||||
|
const combinedResults = await executeQuery(`
|
||||||
|
SELECT
|
||||||
|
ctr.user_id,
|
||||||
|
ctr.logic_score,
|
||||||
|
ctr.creativity_score,
|
||||||
|
ctr.overall_score,
|
||||||
|
ctr.completed_at,
|
||||||
|
u.name as user_name,
|
||||||
|
u.department
|
||||||
|
FROM combined_test_results ctr
|
||||||
|
LEFT JOIN users u ON ctr.user_id = u.id
|
||||||
|
ORDER BY u.department, ctr.completed_at DESC
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 按部門分組統計
|
||||||
|
const departmentMap = new Map()
|
||||||
|
|
||||||
|
// 初始化所有部門
|
||||||
|
const allDepartments = Array.from(new Set(users.map((u: any) => u.department)))
|
||||||
|
|
||||||
|
allDepartments.forEach(dept => {
|
||||||
|
departmentMap.set(dept, {
|
||||||
|
department: dept,
|
||||||
|
totalUsers: 0,
|
||||||
|
participatedUsers: new Set(),
|
||||||
|
logicScores: [],
|
||||||
|
creativeScores: [],
|
||||||
|
combinedScores: [],
|
||||||
|
testCounts: {
|
||||||
|
logic: 0,
|
||||||
|
creative: 0,
|
||||||
|
combined: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 統計用戶數據
|
||||||
|
users.forEach((user: any) => {
|
||||||
|
const dept = user.department
|
||||||
|
if (departmentMap.has(dept)) {
|
||||||
|
departmentMap.get(dept).totalUsers++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 統計測試結果
|
||||||
|
testResults.forEach((result: any) => {
|
||||||
|
const dept = result.department
|
||||||
|
if (departmentMap.has(dept)) {
|
||||||
|
const deptData = departmentMap.get(dept)
|
||||||
|
deptData.participatedUsers.add(result.user_id)
|
||||||
|
deptData.testCounts[result.test_type]++
|
||||||
|
|
||||||
|
if (result.test_type === 'logic') {
|
||||||
|
deptData.logicScores.push(result.score)
|
||||||
|
} else if (result.test_type === 'creative') {
|
||||||
|
deptData.creativeScores.push(result.score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 統計綜合測試結果
|
||||||
|
combinedResults.forEach((result: any) => {
|
||||||
|
const dept = result.department
|
||||||
|
if (departmentMap.has(dept)) {
|
||||||
|
const deptData = departmentMap.get(dept)
|
||||||
|
deptData.participatedUsers.add(result.user_id)
|
||||||
|
deptData.testCounts.combined++
|
||||||
|
deptData.combinedScores.push(result.overall_score)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 計算部門統計數據
|
||||||
|
const departmentStats = Array.from(departmentMap.values()).map(deptData => {
|
||||||
|
const participatedCount = deptData.participatedUsers.size
|
||||||
|
const participationRate = deptData.totalUsers > 0
|
||||||
|
? Math.round((participatedCount / deptData.totalUsers) * 100)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
// 計算平均分數
|
||||||
|
const averageLogicScore = deptData.logicScores.length > 0
|
||||||
|
? Math.round(deptData.logicScores.reduce((sum: number, score: number) => sum + score, 0) / deptData.logicScores.length)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const averageCreativeScore = deptData.creativeScores.length > 0
|
||||||
|
? Math.round(deptData.creativeScores.reduce((sum: number, score: number) => sum + score, 0) / deptData.creativeScores.length)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const averageCombinedScore = deptData.combinedScores.length > 0
|
||||||
|
? Math.round(deptData.combinedScores.reduce((sum: number, score: number) => sum + score, 0) / deptData.combinedScores.length)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
// 計算整體平均分數
|
||||||
|
const allScores = [averageLogicScore, averageCreativeScore, averageCombinedScore].filter(s => s > 0)
|
||||||
|
const overallAverage = allScores.length > 0
|
||||||
|
? Math.round(allScores.reduce((sum, score) => sum + score, 0) / allScores.length)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
department: deptData.department,
|
||||||
|
totalUsers: deptData.totalUsers,
|
||||||
|
participatedUsers: participatedCount,
|
||||||
|
participationRate,
|
||||||
|
averageLogicScore,
|
||||||
|
averageCreativeScore,
|
||||||
|
averageCombinedScore,
|
||||||
|
overallAverage,
|
||||||
|
testCounts: deptData.testCounts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 計算整體統計
|
||||||
|
const totalUsers = users.length
|
||||||
|
const totalParticipants = new Set([
|
||||||
|
...testResults.map((r: any) => r.user_id),
|
||||||
|
...combinedResults.map((r: any) => r.user_id)
|
||||||
|
]).size
|
||||||
|
const overallParticipationRate = totalUsers > 0
|
||||||
|
? Math.round((totalParticipants / totalUsers) * 100)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
// 計算整體平均分數
|
||||||
|
const allScores = [
|
||||||
|
...testResults.map((r: any) => r.score),
|
||||||
|
...combinedResults.map((r: any) => r.overall_score)
|
||||||
|
]
|
||||||
|
const averageScore = allScores.length > 0
|
||||||
|
? Math.round(allScores.reduce((sum, score) => sum + score, 0) / allScores.length)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const totalTests = testResults.length + combinedResults.length
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
departmentStats,
|
||||||
|
overallStats: {
|
||||||
|
totalUsers,
|
||||||
|
totalParticipants,
|
||||||
|
overallParticipationRate,
|
||||||
|
averageScore,
|
||||||
|
totalTests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("獲取部門分析數據失敗:", error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "獲取部門分析數據失敗", error: error instanceof Error ? error.message : String(error) },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user