建立檔案

This commit is contained in:
2025-08-05 08:22:44 +08:00
commit 042d03aff7
122 changed files with 34763 additions and 0 deletions

View File

@@ -0,0 +1,744 @@
"use client"
import { useState } from "react"
import { useCompetition } from "@/contexts/competition-context"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
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 { 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Alert, AlertDescription } from "@/components/ui/alert"
import {
Lightbulb,
Plus,
MoreHorizontal,
Edit,
Trash2,
Eye,
FileText,
Users,
Calendar,
CheckCircle,
AlertTriangle,
Loader2,
Target,
AlertCircle,
} from "lucide-react"
import type { Proposal } from "@/types/competition"
export function ProposalManagement() {
const { proposals, addProposal, updateProposal, getProposalById, teams, getTeamById } = useCompetition()
const [searchTerm, setSearchTerm] = useState("")
const [selectedTeam, setSelectedTeam] = useState("all")
const [selectedProposal, setSelectedProposal] = useState<Proposal | null>(null)
const [showProposalDetail, setShowProposalDetail] = useState(false)
const [showAddProposal, setShowAddProposal] = useState(false)
const [showEditProposal, setShowEditProposal] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [success, setSuccess] = useState("")
const [error, setError] = useState("")
const [newProposal, setNewProposal] = useState({
title: "",
description: "",
problemStatement: "",
solution: "",
expectedImpact: "",
teamId: "",
attachments: [] as string[],
})
const filteredProposals = proposals.filter((proposal) => {
const team = getTeamById(proposal.teamId)
const matchesSearch =
proposal.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
proposal.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
team?.name.toLowerCase().includes(searchTerm.toLowerCase())
const matchesTeam = selectedTeam === "all" || proposal.teamId === selectedTeam
return matchesSearch && matchesTeam
})
const handleViewProposal = (proposal: Proposal) => {
setSelectedProposal(proposal)
setShowProposalDetail(true)
}
const handleEditProposal = (proposal: Proposal) => {
setSelectedProposal(proposal)
setNewProposal({
title: proposal.title,
description: proposal.description,
problemStatement: proposal.problemStatement,
solution: proposal.solution,
expectedImpact: proposal.expectedImpact,
teamId: proposal.teamId,
attachments: proposal.attachments || [],
})
setShowEditProposal(true)
}
const handleDeleteProposal = (proposal: Proposal) => {
setSelectedProposal(proposal)
setShowDeleteConfirm(true)
}
const confirmDeleteProposal = () => {
if (selectedProposal) {
// In a real app, you would call a delete function here
setShowDeleteConfirm(false)
setSelectedProposal(null)
setSuccess("提案刪除成功!")
setTimeout(() => setSuccess(""), 3000)
}
}
const handleAddProposal = async () => {
setError("")
if (
!newProposal.title ||
!newProposal.description ||
!newProposal.problemStatement ||
!newProposal.solution ||
!newProposal.expectedImpact ||
!newProposal.teamId
) {
setError("請填寫所有必填欄位")
return
}
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 1000))
addProposal({
...newProposal,
submittedAt: new Date().toISOString(),
})
setShowAddProposal(false)
setNewProposal({
title: "",
description: "",
problemStatement: "",
solution: "",
expectedImpact: "",
teamId: "",
attachments: [],
})
setSuccess("提案創建成功!")
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
const handleUpdateProposal = async () => {
if (!selectedProposal) return
setError("")
if (
!newProposal.title ||
!newProposal.description ||
!newProposal.problemStatement ||
!newProposal.solution ||
!newProposal.expectedImpact ||
!newProposal.teamId
) {
setError("請填寫所有必填欄位")
return
}
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 1000))
updateProposal(selectedProposal.id, newProposal)
setShowEditProposal(false)
setSelectedProposal(null)
setSuccess("提案更新成功!")
setIsLoading(false)
setTimeout(() => setSuccess(""), 3000)
}
return (
<div className="space-y-6">
{/* Success/Error Messages */}
{success && (
<Alert className="border-green-200 bg-green-50">
<CheckCircle className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-800">{success}</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900"></h1>
<p className="text-gray-600"></p>
</div>
<Button
onClick={() => setShowAddProposal(true)}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-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">{proposals.length}</p>
</div>
<Lightbulb className="w-8 h-8 text-purple-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">{new Set(proposals.map((p) => p.teamId)).size}</p>
</div>
<Users 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">
{
proposals.filter((p) => {
const submittedDate = new Date(p.submittedAt)
const now = new Date()
return (
submittedDate.getMonth() === now.getMonth() && submittedDate.getFullYear() === now.getFullYear()
)
}).length
}
</p>
</div>
<Calendar className="w-8 h-8 text-green-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">
<Input
placeholder="搜尋提案標題、描述或團隊名稱..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex gap-3">
<Select value={selectedTeam} onValueChange={setSelectedTeam}>
<SelectTrigger className="w-40">
<SelectValue placeholder="團隊" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{teams.map((team) => (
<SelectItem key={team.id} value={team.id}>
{team.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* Proposals Table */}
<Card>
<CardHeader>
<CardTitle> ({filteredProposals.length})</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredProposals.map((proposal) => {
const team = getTeamById(proposal.teamId)
const submittedDate = new Date(proposal.submittedAt)
return (
<TableRow key={proposal.id}>
<TableCell>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-purple-500 to-pink-500 rounded-lg flex items-center justify-center">
<Lightbulb className="w-4 h-4 text-white" />
</div>
<div>
<p className="font-medium">{proposal.title}</p>
<p className="text-sm text-gray-500 line-clamp-1">{proposal.description}</p>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<div className="w-6 h-6 bg-gradient-to-r from-green-500 to-blue-500 rounded flex items-center justify-center">
<Users className="w-3 h-3 text-white" />
</div>
<div>
<p className="font-medium text-sm">{team?.name || "未知團隊"}</p>
<p className="text-xs text-gray-500">{team?.department}</p>
</div>
</div>
</TableCell>
<TableCell>
<div className="max-w-xs">
<p className="text-sm line-clamp-2">{proposal.problemStatement}</p>
</div>
</TableCell>
<TableCell>
<div className="text-sm">
<p>{submittedDate.toLocaleDateString()}</p>
<p className="text-gray-500">{submittedDate.toLocaleTimeString()}</p>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
<FileText className="w-4 h-4 text-gray-500" />
<span className="text-sm">{proposal.attachments?.length || 0}</span>
</div>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleViewProposal(proposal)}>
<Eye className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEditProposal(proposal)}>
<Edit className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteProposal(proposal)}>
<Trash2 className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Add Proposal Dialog */}
<Dialog open={showAddProposal} onOpenChange={setShowAddProposal}>
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-6">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="proposalTitle"> *</Label>
<Input
id="proposalTitle"
value={newProposal.title}
onChange={(e) => setNewProposal({ ...newProposal, title: e.target.value })}
placeholder="輸入提案標題"
/>
</div>
<div className="space-y-2">
<Label htmlFor="proposalTeam"> *</Label>
<Select
value={newProposal.teamId}
onValueChange={(value) => setNewProposal({ ...newProposal, teamId: value })}
>
<SelectTrigger>
<SelectValue placeholder="選擇團隊" />
</SelectTrigger>
<SelectContent>
{teams.map((team) => (
<SelectItem key={team.id} value={team.id}>
{team.name} ({team.department})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="proposalDescription"> *</Label>
<Textarea
id="proposalDescription"
value={newProposal.description}
onChange={(e) => setNewProposal({ ...newProposal, description: e.target.value })}
placeholder="簡要描述提案的核心內容"
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="problemStatement"> *</Label>
<Textarea
id="problemStatement"
value={newProposal.problemStatement}
onChange={(e) => setNewProposal({ ...newProposal, problemStatement: e.target.value })}
placeholder="詳細描述要解決的問題或痛點"
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="solution"> *</Label>
<Textarea
id="solution"
value={newProposal.solution}
onChange={(e) => setNewProposal({ ...newProposal, solution: e.target.value })}
placeholder="描述您提出的解決方案"
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="expectedImpact"> *</Label>
<Textarea
id="expectedImpact"
value={newProposal.expectedImpact}
onChange={(e) => setNewProposal({ ...newProposal, expectedImpact: e.target.value })}
placeholder="描述預期產生的商業和社會影響"
rows={3}
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowAddProposal(false)}>
</Button>
<Button onClick={handleAddProposal} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"創建提案"
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Edit Proposal Dialog */}
<Dialog open={showEditProposal} onOpenChange={setShowEditProposal}>
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-6">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="editProposalTitle"> *</Label>
<Input
id="editProposalTitle"
value={newProposal.title}
onChange={(e) => setNewProposal({ ...newProposal, title: e.target.value })}
placeholder="輸入提案標題"
/>
</div>
<div className="space-y-2">
<Label htmlFor="editProposalTeam"> *</Label>
<Select
value={newProposal.teamId}
onValueChange={(value) => setNewProposal({ ...newProposal, teamId: value })}
>
<SelectTrigger>
<SelectValue placeholder="選擇團隊" />
</SelectTrigger>
<SelectContent>
{teams.map((team) => (
<SelectItem key={team.id} value={team.id}>
{team.name} ({team.department})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="editProposalDescription"> *</Label>
<Textarea
id="editProposalDescription"
value={newProposal.description}
onChange={(e) => setNewProposal({ ...newProposal, description: e.target.value })}
placeholder="簡要描述提案的核心內容"
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="editProblemStatement"> *</Label>
<Textarea
id="editProblemStatement"
value={newProposal.problemStatement}
onChange={(e) => setNewProposal({ ...newProposal, problemStatement: e.target.value })}
placeholder="詳細描述要解決的問題或痛點"
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="editSolution"> *</Label>
<Textarea
id="editSolution"
value={newProposal.solution}
onChange={(e) => setNewProposal({ ...newProposal, solution: e.target.value })}
placeholder="描述您提出的解決方案"
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="editExpectedImpact"> *</Label>
<Textarea
id="editExpectedImpact"
value={newProposal.expectedImpact}
onChange={(e) => setNewProposal({ ...newProposal, expectedImpact: e.target.value })}
placeholder="描述預期產生的商業和社會影響"
rows={3}
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowEditProposal(false)}>
</Button>
<Button onClick={handleUpdateProposal} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"更新提案"
)}
</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>{selectedProposal?.title}</DialogDescription>
</DialogHeader>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowDeleteConfirm(false)}>
</Button>
<Button variant="destructive" onClick={confirmDeleteProposal}>
</Button>
</div>
</DialogContent>
</Dialog>
{/* Proposal Detail Dialog */}
<Dialog open={showProposalDetail} onOpenChange={setShowProposalDetail}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedProposal && (
<Tabs defaultValue="overview" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger>
<TabsTrigger value="team"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="flex items-start space-x-4">
<div className="w-16 h-16 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl flex items-center justify-center">
<Lightbulb className="w-8 h-8 text-white" />
</div>
<div className="flex-1">
<h3 className="text-xl font-semibold">{selectedProposal.title}</h3>
<p className="text-gray-600 mb-2">{selectedProposal.description}</p>
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="bg-purple-100 text-purple-700">
</Badge>
<Badge variant="outline" className="bg-gray-100 text-gray-700">
{getTeamById(selectedProposal.teamId)?.name || "未知團隊"}
</Badge>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500">ID</p>
<p className="font-medium">{selectedProposal.id}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{new Date(selectedProposal.submittedAt).toLocaleString()}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{getTeamById(selectedProposal.teamId)?.name || "未知團隊"}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{selectedProposal.attachments?.length || 0} </p>
</div>
</div>
</TabsContent>
<TabsContent value="details" className="space-y-6">
<div className="space-y-6">
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<h5 className="font-semibold text-red-900 mb-2 flex items-center">
<AlertCircle className="w-5 h-5 mr-2" />
</h5>
<p className="text-red-800">{selectedProposal.problemStatement}</p>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h5 className="font-semibold text-green-900 mb-2 flex items-center">
<CheckCircle className="w-5 h-5 mr-2" />
</h5>
<p className="text-green-800">{selectedProposal.solution}</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h5 className="font-semibold text-blue-900 mb-2 flex items-center">
<Target className="w-5 h-5 mr-2" />
</h5>
<p className="text-blue-800">{selectedProposal.expectedImpact}</p>
</div>
</div>
</TabsContent>
<TabsContent value="team" className="space-y-4">
{(() => {
const team = getTeamById(selectedProposal.teamId)
return team ? (
<div className="space-y-4">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gradient-to-r from-green-500 to-blue-500 rounded-lg flex items-center justify-center">
<Users className="w-6 h-6 text-white" />
</div>
<div>
<h4 className="font-semibold text-lg">{team.name}</h4>
<p className="text-gray-600">{team.department}</p>
<p className="text-sm text-gray-500">{team.contactEmail}</p>
</div>
</div>
<div>
<h5 className="font-medium mb-3"></h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{team.members.map((member) => (
<div key={member.id} className="flex items-center space-x-3 p-3 border rounded-lg">
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<span className="text-green-700 font-medium text-sm">{member.name[0]}</span>
</div>
<div className="flex-1">
<div className="flex items-center space-x-2">
<span className="font-medium">{member.name}</span>
{member.id === team.leader && (
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
</Badge>
)}
</div>
<div className="text-sm text-gray-600">
{member.department} {member.role}
</div>
</div>
</div>
))}
</div>
</div>
</div>
) : (
<div className="text-center py-8">
<Users 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>
)
})()}
</TabsContent>
</Tabs>
)}
</DialogContent>
</Dialog>
</div>
)
}