Files
ai-showcase-platform/components/admin/app-management.tsx
2025-09-19 08:29:51 +08:00

1587 lines
61 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"
// 輔助函數:安全地格式化數字
const formatNumber = (value: any, decimals: number = 0): string => {
if (value == null) {
return decimals > 0 ? '0.' + '0'.repeat(decimals) : '0'
}
// 處理字符串和數字類型
const numValue = typeof value === 'string' ? parseFloat(value) : value
if (isNaN(numValue)) {
return decimals > 0 ? '0.' + '0'.repeat(decimals) : '0'
}
return decimals > 0 ? numValue.toFixed(decimals) : numValue.toString()
}
const formatLocaleNumber = (value: any): string => {
if (value == null) {
return '0'
}
// 處理字符串和數字類型
const numValue = typeof value === 'string' ? parseFloat(value) : value
if (isNaN(numValue)) {
return '0'
}
return numValue.toLocaleString()
}
import { useAuth } from "@/contexts/auth-context"
import { useToast } from "@/hooks/use-toast"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import {
Search,
Plus,
MoreHorizontal,
Edit,
Trash2,
Eye,
Star,
Heart,
TrendingUp,
Bot,
CheckCircle,
Clock,
MessageSquare,
ExternalLink,
AlertTriangle,
X,
XCircle,
Check,
TrendingDown,
Link,
Zap,
Brain,
Mic,
ImageIcon,
FileText,
BarChart3,
Camera,
Music,
Video,
Code,
Database,
Globe,
Smartphone,
Monitor,
Headphones,
Palette,
Calculator,
Shield,
Settings,
Lightbulb,
Users
} from "lucide-react"
// Add available icons array after imports
const availableIcons = [
{ name: "Bot", icon: Bot, color: "from-blue-500 to-purple-500" },
{ name: "Brain", icon: Brain, color: "from-purple-500 to-pink-500" },
{ name: "Zap", icon: Zap, color: "from-yellow-500 to-orange-500" },
{ name: "Mic", icon: Mic, color: "from-green-500 to-teal-500" },
{ name: "ImageIcon", icon: ImageIcon, color: "from-pink-500 to-rose-500" },
{ name: "FileText", icon: FileText, color: "from-blue-500 to-cyan-500" },
{ name: "BarChart3", icon: BarChart3, color: "from-emerald-500 to-green-500" },
{ name: "Camera", icon: Camera, color: "from-indigo-500 to-purple-500" },
{ name: "Music", icon: Music, color: "from-violet-500 to-purple-500" },
{ name: "Video", icon: Video, color: "from-red-500 to-pink-500" },
{ name: "Code", icon: Code, color: "from-gray-500 to-slate-500" },
{ name: "Database", icon: Database, color: "from-cyan-500 to-blue-500" },
{ name: "Globe", icon: Globe, color: "from-blue-500 to-indigo-500" },
{ name: "Smartphone", icon: Smartphone, color: "from-slate-500 to-gray-500" },
{ name: "Monitor", icon: Monitor, color: "from-gray-600 to-slate-600" },
{ name: "Headphones", icon: Headphones, color: "from-purple-500 to-violet-500" },
{ name: "Palette", icon: Palette, color: "from-pink-500 to-purple-500" },
{ name: "Calculator", icon: Calculator, color: "from-orange-500 to-red-500" },
{ name: "Shield", icon: Shield, color: "from-green-500 to-emerald-500" },
{ name: "Settings", icon: Settings, color: "from-gray-500 to-zinc-500" },
{ name: "Lightbulb", icon: Lightbulb, color: "from-yellow-500 to-amber-500" },
]
// App data - empty for production
const mockApps: any[] = []
export function AppManagement() {
const { user } = useAuth()
const { toast } = useToast()
const [apps, setApps] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("")
const [selectedType, setSelectedType] = useState("all")
const [selectedStatus, setSelectedStatus] = useState("all")
const [selectedApp, setSelectedApp] = useState<any>(null)
const [showAppDetail, setShowAppDetail] = useState(false)
const [appStats, setAppStats] = useState({
basic: {
views: 0,
likes: 0,
favorites: 0,
rating: 0,
reviewCount: 0
},
usage: {
dailyUsers: 0,
weeklyUsers: 0,
monthlyUsers: 0,
totalSessions: 0,
topDepartments: [],
trendData: []
}
})
const [appReviews, setAppReviews] = useState<any[]>([])
const [isLoadingStats, setIsLoadingStats] = useState(false)
const [isLoadingReviews, setIsLoadingReviews] = useState(false)
const [showAddApp, setShowAddApp] = useState(false)
const [showEditApp, setShowEditApp] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [showApprovalDialog, setShowApprovalDialog] = useState(false)
const [approvalAction, setApprovalAction] = useState<"approve" | "reject">("approve")
const [approvalReason, setApprovalReason] = useState("")
const [newApp, setNewApp] = useState({
name: "",
type: "文字處理",
department: "HQBU",
creator_id: "",
description: "",
appUrl: "",
icon: "Bot",
iconColor: "from-blue-500 to-purple-500",
})
const [pagination, setPagination] = useState({
page: 1,
limit: 5,
total: 0,
totalPages: 0
})
const [stats, setStats] = useState({
totalApps: 0,
activeApps: 0,
inactiveApps: 0,
pendingApps: 0,
totalViews: 0,
totalLikes: 0,
newThisMonth: 0
})
const [users, setUsers] = useState<any[]>([])
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
// 載入應用數據
const loadApps = async () => {
try {
setIsLoading(true)
const params = new URLSearchParams({
page: pagination.page.toString(),
limit: pagination.limit.toString(),
search: searchTerm,
type: selectedType,
status: selectedStatus
})
const response = await fetch(`/api/admin/apps?${params}`)
const data = await response.json()
if (data.success) {
setApps(data.data.apps)
setPagination(data.data.pagination)
setStats(data.data.stats)
}
} catch (error) {
console.error('載入應用數據錯誤:', error)
} finally {
setIsLoading(false)
}
}
// 載入用戶列表
const loadUsers = async () => {
try {
setIsLoadingUsers(true)
const response = await fetch('/api/admin/users/list')
const data = await response.json()
if (data.success) {
setUsers(data.data.users)
}
} catch (error) {
console.error('載入用戶列表錯誤:', error)
} finally {
setIsLoadingUsers(false)
}
}
// 初始載入
useEffect(() => {
loadApps()
loadUsers()
}, [pagination.page, searchTerm, selectedType, selectedStatus])
const filteredApps = apps
const handleViewApp = (app: any) => {
setSelectedApp(app)
setShowAppDetail(true)
loadAppStats(app.id)
loadAppReviews(app.id)
}
// 載入應用統計數據
const loadAppStats = async (appId: string) => {
setIsLoadingStats(true)
try {
const response = await fetch(`/api/admin/apps/${appId}/stats`)
const data = await response.json()
if (data.success) {
setAppStats(data.data)
}
} catch (error) {
console.error('載入應用統計數據錯誤:', error)
} finally {
setIsLoadingStats(false)
}
}
// 載入應用評價
const loadAppReviews = async (appId: string) => {
setIsLoadingReviews(true)
try {
const response = await fetch(`/api/admin/apps/${appId}/reviews`)
const data = await response.json()
if (data.success) {
setAppReviews(data.data.reviews)
}
} catch (error) {
console.error('載入應用評價錯誤:', error)
} finally {
setIsLoadingReviews(false)
}
}
// 刪除評價
const handleDeleteReview = async (reviewId: string) => {
if (!selectedApp) return
try {
const response = await fetch(`/api/admin/apps/${selectedApp.id}/reviews`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ reviewId })
})
const data = await response.json()
if (data.success) {
toast({
title: "刪除成功",
description: "評價已成功刪除",
variant: "success",
})
// 重新載入評價列表
loadAppReviews(selectedApp.id)
// 重新載入統計數據
loadAppStats(selectedApp.id)
} else {
toast({
title: "刪除失敗",
description: data.error || '刪除評價失敗',
variant: "destructive",
})
}
} catch (error) {
console.error('刪除評價錯誤:', error)
toast({
title: "錯誤",
description: '刪除評價時發生錯誤',
variant: "destructive",
})
}
}
const handleEditApp = (app: any) => {
setSelectedApp(app)
setNewApp({
name: app.name,
type: app.type,
department: app.department,
creator_id: app.creator_id || "",
description: app.description,
appUrl: app.appUrl,
icon: app.icon || "Bot",
iconColor: app.iconColor || "from-blue-500 to-purple-500",
})
setShowEditApp(true)
}
const handleDeleteApp = (app: any) => {
setSelectedApp(app)
setShowDeleteConfirm(true)
}
const confirmDeleteApp = async () => {
try {
if (!selectedApp) return
const response = await fetch(`/api/admin/apps/${selectedApp.id}`, {
method: 'DELETE'
})
const data = await response.json()
if (data.success) {
await loadApps()
setShowDeleteConfirm(false)
setSelectedApp(null)
toast({
title: "刪除成功",
description: `應用「${selectedApp.name}」已永久刪除`,
variant: "success",
})
} else {
toast({
title: "刪除失敗",
description: data.error || '刪除應用失敗',
variant: "destructive",
})
}
} catch (error) {
console.error('刪除應用錯誤:', error)
toast({
title: "錯誤",
description: '刪除應用時發生錯誤',
variant: "destructive",
})
}
}
const handleToggleAppStatus = async (appId: string) => {
try {
const response = await fetch(`/api/admin/apps/${appId}/toggle-status`, {
method: 'POST'
})
const data = await response.json()
if (data.success) {
await loadApps()
// 根據新狀態顯示不同的成功訊息
const newStatus = data.data?.app?.is_active ? '發布' : '下架'
toast({
title: "狀態更新成功",
description: `應用已成功${newStatus}`,
variant: "success",
})
} else {
toast({
title: "狀態更新失敗",
description: data.error || '切換應用狀態失敗',
variant: "destructive",
})
}
} catch (error) {
console.error('切換應用狀態錯誤:', error)
toast({
title: "錯誤",
description: '切換應用狀態時發生錯誤',
variant: "destructive",
})
}
}
const handleApprovalAction = (app: any, action: "approve" | "reject") => {
setSelectedApp(app)
setApprovalAction(action)
setApprovalReason("")
setShowApprovalDialog(true)
}
const confirmApproval = () => {
if (selectedApp) {
setApps(
apps.map((app) =>
app.id === selectedApp.id
? {
...app,
status: approvalAction === "approve" ? "published" : "rejected",
}
: app,
),
)
setShowApprovalDialog(false)
setSelectedApp(null)
setApprovalReason("")
}
}
const handleAddApp = async () => {
try {
if (!newApp.creator_id) {
toast({
title: "驗證失敗",
description: '請選擇創建者',
variant: "destructive",
})
return
}
const response = await fetch('/api/admin/apps', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newApp.name,
description: newApp.description,
creator_id: newApp.creator_id,
category: newApp.type, // 使用 type 作為 category
type: newApp.type,
app_url: newApp.appUrl,
icon: newApp.icon,
icon_color: newApp.iconColor
})
})
const data = await response.json()
if (data.success) {
// 重新載入應用列表
await loadApps()
setNewApp({
name: "",
type: "文字處理",
department: "HQBU",
creator_id: "",
description: "",
appUrl: "",
icon: "Bot",
iconColor: "from-blue-500 to-purple-500",
})
setShowAddApp(false)
} else {
toast({
title: "創建失敗",
description: data.error || '創建應用失敗',
variant: "destructive",
})
}
} catch (error) {
console.error('創建應用錯誤:', error)
toast({
title: "錯誤",
description: '創建應用時發生錯誤',
variant: "destructive",
})
}
}
const handleUpdateApp = async () => {
try {
if (!selectedApp) return
if (!newApp.creator_id) {
toast({
title: "驗證失敗",
description: '請選擇創建者',
variant: "destructive",
})
return
}
const response = await fetch(`/api/admin/apps/${selectedApp.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newApp.name,
description: newApp.description,
category: newApp.type,
type: newApp.type,
app_url: newApp.appUrl,
icon: newApp.icon,
icon_color: newApp.iconColor
})
})
const data = await response.json()
if (data.success) {
await loadApps()
setShowEditApp(false)
setSelectedApp(null)
} else {
toast({
title: "更新失敗",
description: data.error || '更新應用失敗',
variant: "destructive",
})
}
} catch (error) {
console.error('更新應用錯誤:', error)
toast({
title: "錯誤",
description: '更新應用時發生錯誤',
variant: "destructive",
})
}
}
const getStatusColor = (status: string) => {
switch (status) {
case "published":
return "bg-green-100 text-green-800 border-green-200"
case "pending":
return "bg-yellow-100 text-yellow-800 border-yellow-200"
case "draft":
return "bg-gray-100 text-gray-800 border-gray-200"
case "rejected":
return "bg-red-100 text-red-800 border-red-200"
default:
return "bg-gray-100 text-gray-800 border-gray-200"
}
}
const getTypeColor = (type: string) => {
const colors = {
: "bg-blue-100 text-blue-800 border-blue-200",
: "bg-purple-100 text-purple-800 border-purple-200",
: "bg-green-100 text-green-800 border-green-200",
: "bg-orange-100 text-orange-800 border-orange-200",
}
return colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800 border-gray-200"
}
const getStatusText = (status: string) => {
switch (status) {
case "published":
return "已發布"
case "pending":
return "待審核"
case "draft":
return "草稿"
case "rejected":
return "已拒絕"
default:
return status
}
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900"></h1>
<p className="text-gray-600"> AI </p>
</div>
<Button
onClick={() => setShowAddApp(true)}
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.totalApps}</p>
</div>
<Bot className="w-8 h-8 text-blue-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.activeApps}</p>
</div>
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.inactiveApps}</p>
</div>
<XCircle className="w-8 h-8 text-red-600" />
</div>
</CardContent>
</Card>
</div>
{/* Filters */}
<Card>
<CardContent className="p-4">
<div className="flex flex-col lg:flex-row gap-4 items-center">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜尋應用名稱或創建者..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex gap-3">
<Select value={selectedType} onValueChange={setSelectedType}>
<SelectTrigger className="w-32">
<SelectValue placeholder="類型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="文字處理"></SelectItem>
<SelectItem value="圖像生成"></SelectItem>
<SelectItem value="語音辨識"></SelectItem>
<SelectItem value="推薦系統"></SelectItem>
<SelectItem value="音樂生成"></SelectItem>
<SelectItem value="程式開發"></SelectItem>
<SelectItem value="影像處理"></SelectItem>
<SelectItem value="對話系統"></SelectItem>
<SelectItem value="數據分析"></SelectItem>
<SelectItem value="設計工具"></SelectItem>
<SelectItem value="語音技術"></SelectItem>
<SelectItem value="教育工具"></SelectItem>
</SelectContent>
</Select>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-32">
<SelectValue placeholder="狀態" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="published"></SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="draft">稿</SelectItem>
<SelectItem value="rejected"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* Apps Table */}
<Card>
<CardHeader>
<CardTitle> ({pagination.total})</CardTitle>
<CardDescription> AI </CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredApps.map((app) => (
<TableRow key={app.id}>
<TableCell>
<div className="flex items-center space-x-3">
<div
className={`w-8 h-8 bg-gradient-to-r ${app.iconColor} rounded-lg flex items-center justify-center`}
>
{(() => {
const IconComponent = availableIcons.find((icon) => icon.name === app.icon)?.icon || Bot
return <IconComponent className="w-4 h-4 text-white" />
})()}
</div>
<div>
<div className="flex items-center space-x-2">
<p className="font-medium">{app.name}</p>
{app.appUrl && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => window.open(app.appUrl, "_blank")}
title="開啟應用"
>
<ExternalLink className="w-3 h-3" />
</Button>
)}
</div>
</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className={getTypeColor(app.type)}>
{app.type}
</Badge>
</TableCell>
<TableCell>
<div>
<p className="font-medium">{app.creator}</p>
<p className="text-sm text-gray-500">{app.department}</p>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className={getStatusColor(app.status)}>
{getStatusText(app.status)}
</Badge>
</TableCell>
<TableCell>
<div className="text-sm">
<div className="flex items-center space-x-1">
<Eye className="w-3 h-3" />
<span>{app.views}</span>
</div>
<div className="flex items-center space-x-1">
<Heart className="w-3 h-3" />
<span>{app.likes}</span>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
<Star className="w-4 h-4 text-yellow-500" />
<span className="font-medium">{formatNumber(app.rating, 1)}</span>
<span className="text-sm text-gray-500">({app.reviewCount || 0})</span>
</div>
</TableCell>
<TableCell className="text-sm text-gray-600">{app.createdAt}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleViewApp(app)}>
<Eye className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEditApp(app)}>
<Edit className="w-4 h-4 mr-2" />
</DropdownMenuItem>
{app.appUrl && (
<DropdownMenuItem onClick={() => window.open(app.appUrl, "_blank")}>
<ExternalLink className="w-4 h-4 mr-2" />
</DropdownMenuItem>
)}
{app.status === "pending" && (
<>
<DropdownMenuItem onClick={() => handleApprovalAction(app, "approve")}>
<Check className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleApprovalAction(app, "reject")}>
<X className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</>
)}
{app.status !== "pending" && (
<DropdownMenuItem onClick={() => handleToggleAppStatus(app.id)}>
{app.status === "published" ? (
<>
<TrendingDown className="w-4 h-4 mr-2" />
</>
) : (
<>
<TrendingUp className="w-4 h-4 mr-2" />
</>
)}
</DropdownMenuItem>
)}
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteApp(app)}>
<Trash2 className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* 分頁控制元件 */}
{pagination.totalPages > 1 && (
<div className="flex flex-col items-center space-y-4 mt-6">
<div className="text-sm text-gray-600">
{pagination.page} {pagination.totalPages} ( {pagination.total} )
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setPagination(prev => ({ ...prev, page: Math.max(1, prev.page - 1) }))}
disabled={pagination.page === 1}
className="flex items-center space-x-1"
>
<span></span>
<span></span>
</Button>
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(pagination.totalPages, 5) }, (_, i) => {
let page;
if (pagination.totalPages <= 5) {
page = i + 1;
} else if (pagination.page <= 3) {
page = i + 1;
} else if (pagination.page >= pagination.totalPages - 2) {
page = pagination.totalPages - 4 + i;
} else {
page = pagination.page - 2 + i;
}
return (
<Button
key={page}
variant={pagination.page === page ? "default" : "outline"}
size="sm"
onClick={() => setPagination(prev => ({ ...prev, page }))}
className="w-10 h-10 p-0"
>
{page}
</Button>
)
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setPagination(prev => ({ ...prev, page: Math.min(prev.totalPages, prev.page + 1) }))}
disabled={pagination.page === pagination.totalPages}
className="flex items-center space-x-1"
>
<span></span>
<span></span>
</Button>
</div>
</div>
)}
{/* Add App Dialog */}
<Dialog open={showAddApp} onOpenChange={setShowAddApp}>
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> AI </DialogTitle>
<DialogDescription> AI </DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={newApp.name}
onChange={(e) => setNewApp({ ...newApp, name: e.target.value })}
placeholder="輸入應用名稱"
/>
</div>
<div className="space-y-2">
<Label htmlFor="creator"> *</Label>
<Select
value={newApp.creator_id}
onValueChange={(value) => setNewApp({ ...newApp, creator_id: value })}
>
<SelectTrigger>
<SelectValue placeholder="選擇創建者" />
</SelectTrigger>
<SelectContent>
{isLoadingUsers ? (
<SelectItem value="" disabled>...</SelectItem>
) : (
users.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.name} ({user.email})
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="type"></Label>
<Select value={newApp.type} onValueChange={(value) => setNewApp({ ...newApp, type: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="文字處理"></SelectItem>
<SelectItem value="圖像生成"></SelectItem>
<SelectItem value="語音辨識"></SelectItem>
<SelectItem value="推薦系統"></SelectItem>
<SelectItem value="音樂生成"></SelectItem>
<SelectItem value="程式開發"></SelectItem>
<SelectItem value="影像處理"></SelectItem>
<SelectItem value="對話系統"></SelectItem>
<SelectItem value="數據分析"></SelectItem>
<SelectItem value="設計工具"></SelectItem>
<SelectItem value="語音技術"></SelectItem>
<SelectItem value="教育工具"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="department"></Label>
<Select
value={newApp.department}
onValueChange={(value) => setNewApp({ ...newApp, department: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ACBU">ACBU</SelectItem>
<SelectItem value="AUBU">AUBU</SelectItem>
<SelectItem value="FAB3">FAB3</SelectItem>
<SelectItem value="FNBU">FNBU</SelectItem>
<SelectItem value="HQBU">HQBU</SelectItem>
<SelectItem value="HRBU">HRBU</SelectItem>
<SelectItem value="IBU">IBU</SelectItem>
<SelectItem value="ICDU">ICDU</SelectItem>
<SelectItem value="ICBU">ICBU</SelectItem>
<SelectItem value="ITBU">ITBU</SelectItem>
<SelectItem value="MBU1">MBU1</SelectItem>
<SelectItem value="MBU5">MBU5</SelectItem>
<SelectItem value="PJA">PJA</SelectItem>
<SelectItem value="PBU">PBU</SelectItem>
<SelectItem value="SBG">SBG</SelectItem>
<SelectItem value="SBU">SBU</SelectItem>
<SelectItem value="董事會"></SelectItem>
<SelectItem value="法務室"></SelectItem>
<SelectItem value="關係企業發展"></SelectItem>
<SelectItem value="稽核室"></SelectItem>
<SelectItem value="總經理室"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="icon"></Label>
<div className="grid grid-cols-7 gap-2 p-4 border rounded-lg max-h-60 overflow-y-auto bg-gray-50">
{availableIcons.map((iconOption) => {
const IconComponent = iconOption.icon
return (
<button
key={iconOption.name}
type="button"
onClick={() => {
setNewApp({
...newApp,
icon: iconOption.name,
iconColor: iconOption.color,
})
}}
className={`w-12 h-12 rounded-lg flex items-center justify-center transition-all hover:scale-105 ${
newApp.icon === iconOption.name
? `bg-gradient-to-r ${iconOption.color} shadow-lg ring-2 ring-blue-500`
: `bg-gradient-to-r ${iconOption.color} opacity-70 hover:opacity-100`
}`}
title={iconOption.name}
>
<IconComponent className="w-6 h-6 text-white" />
</button>
)
})}
</div>
<p className="text-xs text-gray-500"></p>
</div>
<div className="space-y-2">
<Label htmlFor="appUrl"></Label>
<div className="relative">
<Link className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="appUrl"
value={newApp.appUrl}
onChange={(e) => setNewApp({ ...newApp, appUrl: e.target.value })}
placeholder="https://your-app.example.com"
className="pl-10"
/>
</div>
<p className="text-xs text-gray-500"></p>
</div>
<div className="space-y-2">
<Label htmlFor="description"> *</Label>
<Textarea
id="description"
value={newApp.description}
onChange={(e) => setNewApp({ ...newApp, description: e.target.value })}
placeholder="描述應用的功能和特色"
rows={3}
/>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowAddApp(false)}>
</Button>
<Button onClick={handleAddApp} disabled={!newApp.name || !newApp.creator_id || !newApp.description}>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Edit App Dialog */}
<Dialog open={showEditApp} onOpenChange={setShowEditApp}>
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="edit-name"> *</Label>
<Input
id="edit-name"
value={newApp.name}
onChange={(e) => setNewApp({ ...newApp, name: e.target.value })}
placeholder="輸入應用名稱"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-creator"> *</Label>
<Select
value={newApp.creator_id}
onValueChange={(value) => setNewApp({ ...newApp, creator_id: value })}
>
<SelectTrigger>
<SelectValue placeholder="選擇創建者" />
</SelectTrigger>
<SelectContent>
{isLoadingUsers ? (
<SelectItem value="" disabled>...</SelectItem>
) : (
users.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.name} ({user.email})
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="edit-type"></Label>
<Select value={newApp.type} onValueChange={(value) => setNewApp({ ...newApp, type: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="文字處理"></SelectItem>
<SelectItem value="圖像生成"></SelectItem>
<SelectItem value="語音辨識"></SelectItem>
<SelectItem value="推薦系統"></SelectItem>
<SelectItem value="音樂生成"></SelectItem>
<SelectItem value="程式開發"></SelectItem>
<SelectItem value="影像處理"></SelectItem>
<SelectItem value="對話系統"></SelectItem>
<SelectItem value="數據分析"></SelectItem>
<SelectItem value="設計工具"></SelectItem>
<SelectItem value="語音技術"></SelectItem>
<SelectItem value="教育工具"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="edit-department"></Label>
<Select
value={newApp.department}
onValueChange={(value) => setNewApp({ ...newApp, department: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ACBU">ACBU</SelectItem>
<SelectItem value="AUBU">AUBU</SelectItem>
<SelectItem value="FAB3">FAB3</SelectItem>
<SelectItem value="FNBU">FNBU</SelectItem>
<SelectItem value="HQBU">HQBU</SelectItem>
<SelectItem value="HRBU">HRBU</SelectItem>
<SelectItem value="IBU">IBU</SelectItem>
<SelectItem value="ICDU">ICDU</SelectItem>
<SelectItem value="ICBU">ICBU</SelectItem>
<SelectItem value="ITBU">ITBU</SelectItem>
<SelectItem value="MBU1">MBU1</SelectItem>
<SelectItem value="MBU5">MBU5</SelectItem>
<SelectItem value="PJA">PJA</SelectItem>
<SelectItem value="PBU">PBU</SelectItem>
<SelectItem value="SBG">SBG</SelectItem>
<SelectItem value="SBU">SBU</SelectItem>
<SelectItem value="董事會"></SelectItem>
<SelectItem value="法務室"></SelectItem>
<SelectItem value="關係企業發展"></SelectItem>
<SelectItem value="稽核室"></SelectItem>
<SelectItem value="總經理室"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="icon"></Label>
<div className="grid grid-cols-7 gap-2 p-4 border rounded-lg max-h-60 overflow-y-auto bg-gray-50">
{availableIcons.map((iconOption) => {
const IconComponent = iconOption.icon
return (
<button
key={iconOption.name}
type="button"
onClick={() => {
setNewApp({
...newApp,
icon: iconOption.name,
iconColor: iconOption.color,
})
}}
className={`w-12 h-12 rounded-lg flex items-center justify-center transition-all hover:scale-105 ${
newApp.icon === iconOption.name
? `bg-gradient-to-r ${iconOption.color} shadow-lg ring-2 ring-blue-500`
: `bg-gradient-to-r ${iconOption.color} opacity-70 hover:opacity-100`
}`}
title={iconOption.name}
>
<IconComponent className="w-6 h-6 text-white" />
</button>
)
})}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="edit-appUrl"></Label>
<div className="relative">
<Link className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="edit-appUrl"
value={newApp.appUrl}
onChange={(e) => setNewApp({ ...newApp, appUrl: e.target.value })}
placeholder="https://your-app.example.com"
className="pl-10"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="edit-description"> *</Label>
<Textarea
id="edit-description"
value={newApp.description}
onChange={(e) => setNewApp({ ...newApp, description: e.target.value })}
placeholder="描述應用的功能和特色"
rows={3}
/>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowEditApp(false)}>
</Button>
<Button onClick={handleUpdateApp} disabled={!newApp.name || !newApp.creator_id || !newApp.description}>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<AlertTriangle className="w-5 h-5 text-red-500" />
<span></span>
</DialogTitle>
<DialogDescription>{selectedApp?.name}</DialogDescription>
</DialogHeader>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowDeleteConfirm(false)}>
</Button>
<Button variant="destructive" onClick={confirmDeleteApp}>
</Button>
</div>
</DialogContent>
</Dialog>
{/* Approval Dialog */}
<Dialog open={showApprovalDialog} onOpenChange={setShowApprovalDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
{approvalAction === "approve" ? (
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<X className="w-5 h-5 text-red-500" />
)}
<span>{approvalAction === "approve" ? "批准應用" : "拒絕應用"}</span>
</DialogTitle>
<DialogDescription>
{approvalAction === "approve"
? `確認批准應用「${selectedApp?.name}」並發布到平台?`
: `確認拒絕應用「${selectedApp?.name}」的發布申請?`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="approval-reason">
{approvalAction === "approve" ? "批准備註(可選)" : "拒絕原因 *"}
</Label>
<Textarea
id="approval-reason"
value={approvalReason}
onChange={(e) => setApprovalReason(e.target.value)}
placeholder={approvalAction === "approve" ? "輸入批准備註..." : "請說明拒絕原因,以便開發者了解並改進"}
rows={3}
/>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowApprovalDialog(false)}>
</Button>
<Button
onClick={confirmApproval}
variant={approvalAction === "approve" ? "default" : "destructive"}
disabled={approvalAction === "reject" && !approvalReason.trim()}
>
{approvalAction === "approve" ? "確認批准" : "確認拒絕"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* App Detail Dialog */}
<Dialog open={showAppDetail} onOpenChange={setShowAppDetail}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedApp && (
<Tabs defaultValue="info" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="info"></TabsTrigger>
<TabsTrigger value="stats"></TabsTrigger>
<TabsTrigger value="reviews"></TabsTrigger>
</TabsList>
<TabsContent value="info" className="space-y-4">
<div className="flex items-start space-x-4">
<div
className={`w-16 h-16 bg-gradient-to-r ${selectedApp.iconColor} rounded-xl flex items-center justify-center`}
>
{(() => {
const IconComponent = availableIcons.find((icon) => icon.name === selectedApp.icon)?.icon || Bot
return <IconComponent className="w-8 h-8 text-white" />
})()}
</div>
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2 justify-between">
<h3 className="text-xl font-semibold">{selectedApp.name}</h3>
{selectedApp.appUrl && (
<Button variant="outline" size="sm" onClick={() => window.open(selectedApp.appUrl, "_blank")}>
<ExternalLink className="w-4 h-4 mr-2" />
</Button>
)}
</div>
<p className="text-gray-600 mb-2">{selectedApp.description}</p>
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className={getTypeColor(selectedApp.type)}>
{selectedApp.type}
</Badge>
<Badge variant="outline" className="bg-gray-100 text-gray-700">
{selectedApp.department}
</Badge>
<Badge variant="outline" className={getStatusColor(selectedApp.status)}>
{getStatusText(selectedApp.status)}
</Badge>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{selectedApp.creator}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{selectedApp.createdAt}</p>
</div>
<div>
<p className="text-sm text-gray-500">ID</p>
<p className="font-medium">{selectedApp.id}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{selectedApp.department}</p>
</div>
</div>
{selectedApp.appUrl && (
<div>
<p className="text-sm text-gray-500"></p>
<div className="flex items-center space-x-2">
<p className="font-medium text-blue-600">{selectedApp.appUrl}</p>
<Button
variant="ghost"
size="sm"
onClick={() => navigator.clipboard.writeText(selectedApp.appUrl)}
>
</Button>
</div>
</div>
)}
</TabsContent>
<TabsContent value="stats" className="space-y-4">
{/* 基本統計數據 */}
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">
{isLoadingStats ? '...' : (appStats?.basic?.views || 0)}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-red-600">
{isLoadingStats ? '...' : (appStats?.basic?.likes || 0)}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-purple-600">
{isLoadingStats ? '...' : (appStats?.basic?.favorites || 0)}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-yellow-600">
{isLoadingStats ? '...' : formatNumber(appStats?.basic?.rating, 1)}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-green-600">
{isLoadingStats ? '...' : (appStats?.basic?.reviewCount || 0)}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
</div>
{/* 使用趨勢 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">使</CardTitle>
<Users className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{isLoadingStats ? '...' : (appStats?.usage?.dailyUsers || 0)}
</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">使</CardTitle>
<TrendingUp className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{isLoadingStats ? '...' : (appStats?.usage?.weeklyUsers || 0)}
</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">使</CardTitle>
<BarChart3 className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{isLoadingStats ? '...' : formatLocaleNumber(appStats?.usage?.totalSessions)}
</div>
<p className="text-xs text-muted-foreground">使</p>
</CardContent>
</Card>
</div>
{/* 部門使用統計 */}
{appStats?.usage?.topDepartments && appStats.usage.topDepartments.length > 0 && (
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
<CardDescription>使</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{appStats.usage.topDepartments.map((dept: any, index: number) => (
<div key={index} className="flex items-center justify-between">
<span className="text-sm font-medium">{dept.department}</span>
<span className="text-sm text-gray-500">{dept.count} </span>
</div>
))}
</div>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="reviews" className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold"></h3>
<div className="text-sm text-gray-500">
{isLoadingReviews ? '載入中...' : `${appReviews.length} 條評價`}
</div>
</div>
{isLoadingReviews ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="text-gray-500 mt-2">...</p>
</div>
) : appReviews.length === 0 ? (
<div className="text-center py-8">
<MessageSquare className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-600 mb-2"></h3>
<p className="text-gray-500"></p>
</div>
) : (
<div className="space-y-4">
{appReviews.map((review) => (
<Card key={review.id}>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2">
<div className="flex items-center space-x-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${
i < review.rating
? 'text-yellow-400 fill-current'
: 'text-gray-300'
}`}
/>
))}
</div>
<span className="text-sm font-medium">{review.userName}</span>
<span className="text-xs text-gray-500">({review.userDepartment})</span>
</div>
<p className="text-gray-700 mb-2">{review.review}</p>
<p className="text-xs text-gray-500">
{review.ratedAt ? new Date(review.ratedAt).toLocaleString('zh-TW') : '未知時間'}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteReview(review.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
</Tabs>
)}
</DialogContent>
</Dialog>
</div>
)
}