740 lines
38 KiB
TypeScript
740 lines
38 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog"
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|
import { Progress } from "@/components/ui/progress"
|
|
import {
|
|
Target,
|
|
Plus,
|
|
Edit,
|
|
Trash2,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
DollarSign,
|
|
Users,
|
|
Activity,
|
|
Zap,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
BarChart3,
|
|
Download,
|
|
} from "lucide-react"
|
|
|
|
// 使用真實資料庫數據
|
|
import { KPI } from '@/lib/database'
|
|
|
|
// 預設 KPI 數據(當資料庫無數據時使用)
|
|
const defaultKpiData = [
|
|
{
|
|
id: 1,
|
|
title: "營收成長率",
|
|
description: "年度營收成長百分比目標",
|
|
category: "financial",
|
|
weight: 30,
|
|
targetValue: 100,
|
|
currentValue: 85,
|
|
unit: "%",
|
|
startDate: "2024-01-01",
|
|
endDate: "2024-12-31",
|
|
status: "active",
|
|
owner: "陳雅雯",
|
|
lastUpdated: "2024-02-10",
|
|
color: "emerald",
|
|
trend: "up",
|
|
},
|
|
{
|
|
id: 2,
|
|
title: "團隊滿意度",
|
|
description: "員工滿意度調查分數",
|
|
category: "team",
|
|
weight: 25,
|
|
targetValue: 90,
|
|
currentValue: 92,
|
|
unit: "分",
|
|
startDate: "2024-01-01",
|
|
endDate: "2024-12-31",
|
|
status: "active",
|
|
owner: "王志明",
|
|
lastUpdated: "2024-02-08",
|
|
color: "blue",
|
|
trend: "up",
|
|
},
|
|
{
|
|
id: 3,
|
|
title: "市場佔有率",
|
|
description: "公司在目標市場的佔有率",
|
|
category: "operational",
|
|
weight: 20,
|
|
targetValue: 75,
|
|
currentValue: 68,
|
|
unit: "%",
|
|
startDate: "2024-01-01",
|
|
endDate: "2024-12-31",
|
|
status: "active",
|
|
owner: "李美玲",
|
|
lastUpdated: "2024-02-05",
|
|
color: "purple",
|
|
trend: "down",
|
|
},
|
|
{
|
|
id: 4,
|
|
title: "創新指數",
|
|
description: "新產品開發和創新項目數量",
|
|
category: "innovation",
|
|
weight: 25,
|
|
targetValue: 80,
|
|
currentValue: 45,
|
|
unit: "項",
|
|
startDate: "2024-01-01",
|
|
endDate: "2024-12-31",
|
|
status: "at_risk",
|
|
owner: "張建國",
|
|
lastUpdated: "2024-02-03",
|
|
color: "orange",
|
|
trend: "down",
|
|
},
|
|
{
|
|
id: 5,
|
|
title: "客戶滿意度",
|
|
description: "客戶服務滿意度評分",
|
|
category: "operational",
|
|
weight: 20,
|
|
targetValue: 95,
|
|
currentValue: 88,
|
|
unit: "分",
|
|
startDate: "2024-01-01",
|
|
endDate: "2024-12-31",
|
|
status: "active",
|
|
owner: "林小華",
|
|
lastUpdated: "2024-02-12",
|
|
color: "cyan",
|
|
trend: "up",
|
|
},
|
|
{
|
|
id: 6,
|
|
title: "成本控制率",
|
|
description: "營運成本控制效率",
|
|
category: "financial",
|
|
weight: 15,
|
|
targetValue: 85,
|
|
currentValue: 78,
|
|
unit: "%",
|
|
startDate: "2024-01-01",
|
|
endDate: "2024-12-31",
|
|
status: "pending",
|
|
owner: "黃大明",
|
|
lastUpdated: "2024-02-01",
|
|
color: "red",
|
|
trend: "stable",
|
|
},
|
|
]
|
|
|
|
const categoryColors = {
|
|
financial: { bg: "from-emerald-50 to-green-100", border: "border-emerald-200", text: "text-emerald-700" },
|
|
team: { bg: "from-blue-50 to-cyan-100", border: "border-blue-200", text: "text-blue-700" },
|
|
operational: { bg: "from-purple-50 to-violet-100", border: "border-purple-200", text: "text-purple-700" },
|
|
innovation: { bg: "from-orange-50 to-amber-100", border: "border-orange-200", text: "text-orange-700" },
|
|
}
|
|
|
|
const statusColors = {
|
|
active: { bg: "bg-green-100", text: "text-green-800", border: "border-green-300" },
|
|
at_risk: { bg: "bg-red-100", text: "text-red-800", border: "border-red-300" },
|
|
pending: { bg: "bg-yellow-100", text: "text-yellow-800", border: "border-yellow-300" },
|
|
completed: { bg: "bg-blue-100", text: "text-blue-800", border: "border-blue-300" },
|
|
}
|
|
|
|
function CircularProgress({ value, size = 60, color = "blue" }: { value: number; size?: number; color?: string }) {
|
|
const radius = (size - 8) / 2
|
|
const circumference = 2 * Math.PI * radius
|
|
const strokeDasharray = circumference
|
|
const strokeDashoffset = circumference - (value / 100) * circumference
|
|
|
|
const colorMap = {
|
|
emerald: value >= 70 ? "#10b981" : "#ef4444",
|
|
blue: value >= 70 ? "#3b82f6" : "#ef4444",
|
|
purple: value >= 70 ? "#8b5cf6" : "#ef4444",
|
|
orange: value >= 70 ? "#f59e0b" : "#ef4444",
|
|
cyan: value >= 70 ? "#06b6d4" : "#ef4444",
|
|
red: value >= 70 ? "#ef4444" : "#dc2626",
|
|
}
|
|
|
|
return (
|
|
<div className="relative" style={{ width: size, height: size }}>
|
|
<svg width={size} height={size} className="transform -rotate-90">
|
|
<circle cx={size / 2} cy={size / 2} r={radius} stroke="#e5e7eb" strokeWidth="4" fill="none" />
|
|
<circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={radius}
|
|
stroke={colorMap[color as keyof typeof colorMap]}
|
|
strokeWidth="4"
|
|
fill="none"
|
|
strokeDasharray={strokeDasharray}
|
|
strokeDashoffset={strokeDashoffset}
|
|
strokeLinecap="round"
|
|
className="transition-all duration-1000 ease-out"
|
|
/>
|
|
</svg>
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<span className="text-xs font-bold text-gray-800">{value}%</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function KPIPage() {
|
|
const [activeTab, setActiveTab] = useState("overview")
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
|
const [selectedCategory, setSelectedCategory] = useState("all")
|
|
const [selectedStatus, setSelectedStatus] = useState("all")
|
|
const [kpiData, setKpiData] = useState<KPI[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
fetchKPIData()
|
|
}, [])
|
|
|
|
const fetchKPIData = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const response = await fetch('/api/kpi?userId=user_admin')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setKpiData(data)
|
|
} else {
|
|
setKpiData(defaultKpiData as any)
|
|
}
|
|
} catch (error) {
|
|
console.error('獲取 KPI 數據失敗:', error)
|
|
setError('無法載入 KPI 數據')
|
|
setKpiData(defaultKpiData as any)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
// 轉換數據格式以匹配 UI 需求
|
|
const displayKpiData = kpiData.map(kpi => ({
|
|
...kpi,
|
|
currentValue: kpi.current_value,
|
|
targetValue: kpi.target_value,
|
|
lastUpdated: kpi.updated_at,
|
|
color: kpi.category === 'financial' ? 'emerald' :
|
|
kpi.category === 'team' ? 'blue' :
|
|
kpi.category === 'operational' ? 'purple' : 'orange',
|
|
trend: kpi.current_value >= kpi.target_value ? 'up' : 'down'
|
|
}))
|
|
|
|
const filteredKPIs = displayKpiData.filter((kpi) => {
|
|
const categoryMatch = selectedCategory === "all" || kpi.category === selectedCategory
|
|
const statusMatch = selectedStatus === "all" || kpi.status === selectedStatus
|
|
return categoryMatch && statusMatch
|
|
})
|
|
|
|
const kpiStats = {
|
|
total: displayKpiData.length,
|
|
active: displayKpiData.filter((k) => k.status === "active").length,
|
|
atRisk: displayKpiData.filter((k) => k.status === "at_risk").length,
|
|
completed: displayKpiData.filter((k) => k.status === "completed").length,
|
|
avgProgress: Math.round(
|
|
displayKpiData.reduce((acc, k) => acc + (k.currentValue / k.targetValue) * 100, 0) / displayKpiData.length,
|
|
),
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 p-3 sm:p-6">
|
|
<div className="max-w-7xl mx-auto space-y-4 sm:space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
|
<div className="flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0 sm:space-x-4">
|
|
<img
|
|
src="data:image/png;base64,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"
|
|
alt="公司 Logo"
|
|
className="h-10 w-auto sm:h-12 md:hidden"
|
|
/>
|
|
<div>
|
|
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
|
KPI 管理中心
|
|
</h1>
|
|
<p className="text-sm sm:text-base text-gray-600 mt-1">關鍵績效指標設定與追蹤</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center space-y-2 sm:space-y-0 sm:space-x-4">
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
新增 KPI
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
|
建立新的 KPI
|
|
</DialogTitle>
|
|
<DialogDescription>設定關鍵績效指標的詳細資訊</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid grid-cols-4 items-center gap-4">
|
|
<Label htmlFor="title" className="text-right font-medium">
|
|
KPI 名稱
|
|
</Label>
|
|
<Input id="title" className="col-span-3" placeholder="輸入 KPI 名稱" />
|
|
</div>
|
|
<div className="grid grid-cols-4 items-center gap-4">
|
|
<Label htmlFor="description" className="text-right font-medium">
|
|
描述
|
|
</Label>
|
|
<Textarea id="description" className="col-span-3" placeholder="詳細描述此 KPI" />
|
|
</div>
|
|
<div className="grid grid-cols-4 items-center gap-4">
|
|
<Label htmlFor="category" className="text-right font-medium">
|
|
類別
|
|
</Label>
|
|
<Select>
|
|
<SelectTrigger className="col-span-3">
|
|
<SelectValue placeholder="選擇類別" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="financial">財務</SelectItem>
|
|
<SelectItem value="team">團隊</SelectItem>
|
|
<SelectItem value="operational">營運</SelectItem>
|
|
<SelectItem value="innovation">創新</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid grid-cols-4 items-center gap-4">
|
|
<Label htmlFor="weight" className="text-right font-medium">
|
|
權重 (%)
|
|
</Label>
|
|
<Input id="weight" type="number" className="col-span-3" placeholder="0-100" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="grid grid-cols-2 items-center gap-2">
|
|
<Label htmlFor="target" className="text-right font-medium">
|
|
目標值
|
|
</Label>
|
|
<Input id="target" type="number" placeholder="目標" />
|
|
</div>
|
|
<div className="grid grid-cols-2 items-center gap-2">
|
|
<Label htmlFor="unit" className="text-right font-medium">
|
|
單位
|
|
</Label>
|
|
<Input id="unit" placeholder="%, 分, 項" />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="grid grid-cols-2 items-center gap-2">
|
|
<Label htmlFor="startDate" className="text-right font-medium">
|
|
開始日期
|
|
</Label>
|
|
<Input id="startDate" type="date" />
|
|
</div>
|
|
<div className="grid grid-cols-2 items-center gap-2">
|
|
<Label htmlFor="endDate" className="text-right font-medium">
|
|
結束日期
|
|
</Label>
|
|
<Input id="endDate" type="date" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
type="submit"
|
|
className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
|
|
>
|
|
建立 KPI
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
<Button variant="outline" className="bg-white/80 backdrop-blur-sm">
|
|
<Download className="h-4 w-4 mr-2" />
|
|
匯出報表
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-6">
|
|
<Card className="shadow-lg bg-gradient-to-br from-blue-50 to-cyan-100 border-0">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium text-blue-700">總 KPI 數</CardTitle>
|
|
<div className="p-2 bg-blue-100 rounded-full">
|
|
<Target className="h-4 w-4 text-blue-600" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-blue-800">{kpiStats.total}</div>
|
|
<p className="text-xs text-blue-600">+2 較上月</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="shadow-lg bg-gradient-to-br from-green-50 to-emerald-100 border-0">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium text-green-700">進行中</CardTitle>
|
|
<div className="p-2 bg-green-100 rounded-full">
|
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-green-800">{kpiStats.active}</div>
|
|
<p className="text-xs text-green-600">正常執行</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="shadow-lg bg-gradient-to-br from-red-50 to-pink-100 border-0">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium text-red-700">風險項目</CardTitle>
|
|
<div className="p-2 bg-red-100 rounded-full">
|
|
<AlertTriangle className="h-4 w-4 text-red-600" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-red-800">{kpiStats.atRisk}</div>
|
|
<p className="text-xs text-red-600">需要關注</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="shadow-lg bg-gradient-to-br from-purple-50 to-violet-100 border-0">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium text-purple-700">平均進度</CardTitle>
|
|
<div className="p-2 bg-purple-100 rounded-full">
|
|
<BarChart3 className="h-4 w-4 text-purple-600" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-purple-800">{kpiStats.avgProgress}%</div>
|
|
<p className="text-xs text-purple-600">整體表現</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<Card className="shadow-lg bg-white/80 backdrop-blur-sm">
|
|
<CardHeader>
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0">
|
|
<div>
|
|
<CardTitle className="text-lg font-bold bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent">
|
|
KPI 管理
|
|
</CardTitle>
|
|
<CardDescription>管理和追蹤所有關鍵績效指標</CardDescription>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
|
|
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
|
<SelectTrigger className="w-full sm:w-32">
|
|
<SelectValue placeholder="類別" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">所有類別</SelectItem>
|
|
<SelectItem value="financial">財務</SelectItem>
|
|
<SelectItem value="team">團隊</SelectItem>
|
|
<SelectItem value="operational">營運</SelectItem>
|
|
<SelectItem value="innovation">創新</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
|
|
<SelectTrigger className="w-full sm:w-32">
|
|
<SelectValue placeholder="狀態" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">所有狀態</SelectItem>
|
|
<SelectItem value="active">進行中</SelectItem>
|
|
<SelectItem value="at_risk">風險</SelectItem>
|
|
<SelectItem value="pending">待審核</SelectItem>
|
|
<SelectItem value="completed">已完成</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3">
|
|
<TabsTrigger value="overview" className="flex items-center space-x-2">
|
|
<BarChart3 className="h-4 w-4" />
|
|
<span>總覽</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="list" className="flex items-center space-x-2">
|
|
<Target className="h-4 w-4" />
|
|
<span>列表</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="progress" className="flex items-center space-x-2">
|
|
<TrendingUp className="h-4 w-4" />
|
|
<span>進度</span>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* Overview Tab */}
|
|
<TabsContent value="overview" className="space-y-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
{filteredKPIs.map((kpi) => (
|
|
<Card
|
|
key={kpi.id}
|
|
className={`shadow-lg hover:shadow-xl transition-all duration-300 border-0 bg-gradient-to-br ${
|
|
categoryColors[kpi.category as keyof typeof categoryColors]?.bg
|
|
}`}
|
|
>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<CardTitle className="text-sm font-semibold text-gray-800 mb-1">{kpi.title}</CardTitle>
|
|
<CardDescription className="text-xs text-gray-600 line-clamp-2">
|
|
{kpi.description}
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex flex-col items-end space-y-2">
|
|
<div
|
|
className={`p-1.5 rounded-full ${
|
|
categoryColors[kpi.category as keyof typeof categoryColors]?.bg
|
|
}`}
|
|
>
|
|
{kpi.category === "financial" && <DollarSign className="h-3 w-3 text-emerald-600" />}
|
|
{kpi.category === "team" && <Users className="h-3 w-3 text-blue-600" />}
|
|
{kpi.category === "operational" && <Activity className="h-3 w-3 text-purple-600" />}
|
|
{kpi.category === "innovation" && <Zap className="h-3 w-3 text-orange-600" />}
|
|
</div>
|
|
<Badge
|
|
className={`text-xs px-2 py-1 ${
|
|
statusColors[kpi.status as keyof typeof statusColors]?.bg
|
|
} ${statusColors[kpi.status as keyof typeof statusColors]?.text} border-0`}
|
|
>
|
|
{kpi.status === "active"
|
|
? "進行中"
|
|
: kpi.status === "at_risk"
|
|
? "風險"
|
|
: kpi.status === "pending"
|
|
? "待審核"
|
|
: "已完成"}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center justify-center">
|
|
<CircularProgress
|
|
value={Math.round((kpi.currentValue / kpi.targetValue) * 100)}
|
|
size={80}
|
|
color={kpi.color}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-xs">
|
|
<span className="text-gray-600">目前值</span>
|
|
<span className="font-semibold text-gray-800">
|
|
{kpi.currentValue} {kpi.unit}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-xs">
|
|
<span className="text-gray-600">目標值</span>
|
|
<span className="font-semibold text-gray-800">
|
|
{kpi.targetValue} {kpi.unit}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-xs">
|
|
<span className="text-gray-600">權重</span>
|
|
<span className="font-semibold text-gray-800">{kpi.weight}%</span>
|
|
</div>
|
|
<div className="flex justify-between text-xs">
|
|
<span className="text-gray-600">負責人</span>
|
|
<span className="font-semibold text-gray-800">{kpi.owner}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pt-2 border-t border-gray-200">
|
|
<div className="flex items-center space-x-1">
|
|
{kpi.trend === "up" ? (
|
|
<TrendingUp className="h-3 w-3 text-green-500" />
|
|
) : kpi.trend === "down" ? (
|
|
<TrendingDown className="h-3 w-3 text-red-500" />
|
|
) : (
|
|
<div className="h-3 w-3 bg-gray-400 rounded-full" />
|
|
)}
|
|
<span className="text-xs text-gray-600">趨勢</span>
|
|
</div>
|
|
<div className="flex space-x-1">
|
|
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
|
|
<Edit className="h-3 w-3" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 text-red-500">
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* List Tab */}
|
|
<TabsContent value="list" className="space-y-4">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>KPI 名稱</TableHead>
|
|
<TableHead>類別</TableHead>
|
|
<TableHead>進度</TableHead>
|
|
<TableHead>狀態</TableHead>
|
|
<TableHead>負責人</TableHead>
|
|
<TableHead>最後更新</TableHead>
|
|
<TableHead>操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredKPIs.map((kpi) => (
|
|
<TableRow key={kpi.id} className="hover:bg-gray-50">
|
|
<TableCell>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{kpi.title}</div>
|
|
<div className="text-sm text-gray-500 truncate max-w-xs">{kpi.description}</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
variant="outline"
|
|
className={`${categoryColors[kpi.category as keyof typeof categoryColors]?.border} ${
|
|
categoryColors[kpi.category as keyof typeof categoryColors]?.text
|
|
} bg-white`}
|
|
>
|
|
{kpi.category === "financial"
|
|
? "財務"
|
|
: kpi.category === "team"
|
|
? "團隊"
|
|
: kpi.category === "operational"
|
|
? "營運"
|
|
: "創新"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center space-x-2">
|
|
<Progress value={(kpi.currentValue / kpi.targetValue) * 100} className="w-16 h-2" />
|
|
<span className="text-sm font-medium">
|
|
{Math.round((kpi.currentValue / kpi.targetValue) * 100)}%
|
|
</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
className={`${statusColors[kpi.status as keyof typeof statusColors]?.bg} ${
|
|
statusColors[kpi.status as keyof typeof statusColors]?.text
|
|
} border-0`}
|
|
>
|
|
{kpi.status === "active"
|
|
? "進行中"
|
|
: kpi.status === "at_risk"
|
|
? "風險"
|
|
: kpi.status === "pending"
|
|
? "待審核"
|
|
: "已完成"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center space-x-2">
|
|
<Avatar className="h-6 w-6">
|
|
<AvatarFallback className="text-xs bg-gradient-to-br from-blue-400 to-purple-500 text-white">
|
|
{kpi.owner.charAt(0)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span className="text-sm">{kpi.owner}</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-sm text-gray-500">{kpi.lastUpdated}</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center space-x-1">
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-red-500">
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TabsContent>
|
|
|
|
{/* Progress Tab */}
|
|
<TabsContent value="progress" className="space-y-6">
|
|
<div className="grid gap-4">
|
|
{filteredKPIs.map((kpi) => (
|
|
<Card key={kpi.id} className="shadow-md hover:shadow-lg transition-shadow">
|
|
<CardContent className="p-6">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0">
|
|
<div className="flex-1">
|
|
<div className="flex items-center space-x-3 mb-2">
|
|
<h3 className="font-semibold text-gray-900">{kpi.title}</h3>
|
|
<Badge
|
|
className={`${statusColors[kpi.status as keyof typeof statusColors]?.bg} ${
|
|
statusColors[kpi.status as keyof typeof statusColors]?.text
|
|
} border-0`}
|
|
>
|
|
{kpi.status === "active"
|
|
? "進行中"
|
|
: kpi.status === "at_risk"
|
|
? "風險"
|
|
: kpi.status === "pending"
|
|
? "待審核"
|
|
: "已完成"}
|
|
</Badge>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-gray-600">
|
|
進度: {kpi.currentValue} / {kpi.targetValue} {kpi.unit}
|
|
</span>
|
|
<span className="font-semibold">
|
|
{Math.round((kpi.currentValue / kpi.targetValue) * 100)}%
|
|
</span>
|
|
</div>
|
|
<Progress value={(kpi.currentValue / kpi.targetValue) * 100} className="h-3" />
|
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
|
<span>權重: {kpi.weight}%</span>
|
|
<span>負責人: {kpi.owner}</span>
|
|
<span>更新: {kpi.lastUpdated}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
{kpi.trend === "up" ? (
|
|
<TrendingUp className="h-5 w-5 text-green-500" />
|
|
) : kpi.trend === "down" ? (
|
|
<TrendingDown className="h-5 w-5 text-red-500" />
|
|
) : (
|
|
<div className="h-5 w-5 bg-gray-400 rounded-full" />
|
|
)}
|
|
<Button variant="outline" size="sm">
|
|
更新進度
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|