修正競賽團隊編輯、個人賽顯示、團體賽顯示 bug
This commit is contained in:
@@ -52,11 +52,15 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
// 獲取所有活躍的應用,編輯團隊時顯示所有應用(包括已綁定的)
|
// 獲取所有活躍的應用,編輯團隊時顯示所有應用(包括已綁定的)
|
||||||
// 使用 is_active = 1 因為數據庫中存儲的是數字 1
|
// 使用 is_active = 1 因為數據庫中存儲的是數字 1
|
||||||
|
// 與 users 表 JOIN 獲取創建者姓名
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT id, name, description, category, type, icon, icon_color, app_url, creator_id, team_id
|
SELECT a.id, a.name, a.description, a.category, a.type, a.icon, a.icon_color, a.app_url,
|
||||||
FROM apps
|
a.creator_id, a.team_id, a.created_at as submissionDate,
|
||||||
WHERE is_active = 1
|
u.name as creator_name
|
||||||
ORDER BY created_at DESC
|
FROM apps a
|
||||||
|
LEFT JOIN users u ON a.creator_id = u.id
|
||||||
|
WHERE a.is_active = 1
|
||||||
|
ORDER BY a.created_at DESC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
@@ -72,17 +76,17 @@ export async function GET(request: NextRequest) {
|
|||||||
console.log('⚠️ 沒有找到 is_active = 1 的應用,嘗試其他查詢條件...');
|
console.log('⚠️ 沒有找到 is_active = 1 的應用,嘗試其他查詢條件...');
|
||||||
|
|
||||||
// 嘗試 is_active = TRUE
|
// 嘗試 is_active = TRUE
|
||||||
const sqlTrue = sql.replace('WHERE is_active = 1', 'WHERE is_active = TRUE');
|
const sqlTrue = sql.replace('WHERE a.is_active = 1', 'WHERE a.is_active = TRUE');
|
||||||
const appsTrue = await db.query(sqlTrue, params);
|
const appsTrue = await db.query(sqlTrue, params);
|
||||||
console.log('📊 is_active = TRUE 查詢結果:', appsTrue.length, '個應用');
|
console.log('📊 is_active = TRUE 查詢結果:', appsTrue.length, '個應用');
|
||||||
|
|
||||||
// 嘗試 is_active = '1'
|
// 嘗試 is_active = '1'
|
||||||
const sqlString = sql.replace('WHERE is_active = 1', 'WHERE is_active = "1"');
|
const sqlString = sql.replace('WHERE a.is_active = 1', 'WHERE a.is_active = "1"');
|
||||||
const appsString = await db.query(sqlString, params);
|
const appsString = await db.query(sqlString, params);
|
||||||
console.log('📊 is_active = "1" 查詢結果:', appsString.length, '個應用');
|
console.log('📊 is_active = "1" 查詢結果:', appsString.length, '個應用');
|
||||||
|
|
||||||
// 嘗試沒有 is_active 條件
|
// 嘗試沒有 is_active 條件
|
||||||
const sqlNoFilter = sql.replace('WHERE is_active = 1', 'WHERE 1=1');
|
const sqlNoFilter = sql.replace('WHERE a.is_active = 1', 'WHERE 1=1');
|
||||||
const appsNoFilter = await db.query(sqlNoFilter, params);
|
const appsNoFilter = await db.query(sqlNoFilter, params);
|
||||||
console.log('📊 無 is_active 過濾查詢結果:', appsNoFilter.length, '個應用');
|
console.log('📊 無 is_active 過濾查詢結果:', appsNoFilter.length, '個應用');
|
||||||
|
|
||||||
|
@@ -10,20 +10,48 @@ export async function GET(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const competitions = await CompetitionService.getAllCompetitions();
|
const competitions = await CompetitionService.getAllCompetitions();
|
||||||
|
|
||||||
|
// 動態計算每個競賽的狀態
|
||||||
|
const now = new Date();
|
||||||
|
const competitionsWithCalculatedStatus = competitions.map(competition => {
|
||||||
|
const startDate = new Date(competition.start_date);
|
||||||
|
const endDate = new Date(competition.end_date);
|
||||||
|
|
||||||
|
let calculatedStatus = competition.status;
|
||||||
|
|
||||||
|
// 確保日期比較的準確性,使用 UTC 時間避免時區問題
|
||||||
|
const nowUTC = new Date(now.getTime() + now.getTimezoneOffset() * 60000);
|
||||||
|
const startDateUTC = new Date(startDate.getTime() + startDate.getTimezoneOffset() * 60000);
|
||||||
|
const endDateUTC = new Date(endDate.getTime() + endDate.getTimezoneOffset() * 60000);
|
||||||
|
|
||||||
|
// 根據實際日期計算狀態
|
||||||
|
if (nowUTC < startDateUTC) {
|
||||||
|
calculatedStatus = 'upcoming'; // 即將開始
|
||||||
|
} else if (nowUTC >= startDateUTC && nowUTC <= endDateUTC) {
|
||||||
|
calculatedStatus = 'active'; // 進行中
|
||||||
|
} else if (nowUTC > endDateUTC) {
|
||||||
|
calculatedStatus = 'completed'; // 已完成
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...competition,
|
||||||
|
status: calculatedStatus
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// 計算統計數據
|
// 計算統計數據
|
||||||
const stats = {
|
const stats = {
|
||||||
total: competitions.length,
|
total: competitionsWithCalculatedStatus.length,
|
||||||
upcoming: competitions.filter(c => c.status === 'upcoming').length,
|
upcoming: competitionsWithCalculatedStatus.filter(c => c.status === 'upcoming').length,
|
||||||
active: competitions.filter(c => c.status === 'active' || c.status === 'ongoing').length,
|
active: competitionsWithCalculatedStatus.filter(c => c.status === 'active' || c.status === 'ongoing').length,
|
||||||
ongoing: competitions.filter(c => c.status === 'ongoing').length,
|
ongoing: competitionsWithCalculatedStatus.filter(c => c.status === 'ongoing').length,
|
||||||
judging: competitions.filter(c => c.status === 'judging').length,
|
judging: competitionsWithCalculatedStatus.filter(c => c.status === 'judging').length,
|
||||||
completed: competitions.filter(c => c.status === 'completed').length,
|
completed: competitionsWithCalculatedStatus.filter(c => c.status === 'completed').length,
|
||||||
individual: competitions.filter(c => c.type === 'individual').length,
|
individual: competitionsWithCalculatedStatus.filter(c => c.type === 'individual').length,
|
||||||
team: competitions.filter(c => c.type === 'team').length,
|
team: competitionsWithCalculatedStatus.filter(c => c.type === 'team').length,
|
||||||
mixed: competitions.filter(c => c.type === 'mixed').length,
|
mixed: competitionsWithCalculatedStatus.filter(c => c.type === 'mixed').length,
|
||||||
proposal: competitions.filter(c => c.type === 'proposal').length,
|
proposal: competitionsWithCalculatedStatus.filter(c => c.type === 'proposal').length,
|
||||||
currentYear: competitions.filter(c => c.year === new Date().getFullYear()).length,
|
currentYear: competitionsWithCalculatedStatus.filter(c => c.year === new Date().getFullYear()).length,
|
||||||
thisMonth: competitions.filter(c =>
|
thisMonth: competitionsWithCalculatedStatus.filter(c =>
|
||||||
c.year === new Date().getFullYear() &&
|
c.year === new Date().getFullYear() &&
|
||||||
c.month === new Date().getMonth() + 1
|
c.month === new Date().getMonth() + 1
|
||||||
).length
|
).length
|
||||||
|
@@ -703,6 +703,7 @@ export function CompetitionManagement() {
|
|||||||
fetchJudgeStats()
|
fetchJudgeStats()
|
||||||
fetchTeams()
|
fetchTeams()
|
||||||
fetchTeamStats()
|
fetchTeamStats()
|
||||||
|
fetchAvailableApps()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 當競賽列表載入完成後,載入每個競賽的評分進度
|
// 當競賽列表載入完成後,載入每個競賽的評分進度
|
||||||
@@ -859,11 +860,33 @@ export function CompetitionManagement() {
|
|||||||
|
|
||||||
// Get participants based on competition type
|
// Get participants based on competition type
|
||||||
const getParticipants = (competitionType: string) => {
|
const getParticipants = (competitionType: string) => {
|
||||||
|
console.log('🔍 getParticipants 調用:', {
|
||||||
|
competitionType,
|
||||||
|
dbTeamsLength: dbTeams.length,
|
||||||
|
teamsLength: teams.length,
|
||||||
|
isLoadingTeams
|
||||||
|
})
|
||||||
switch (competitionType) {
|
switch (competitionType) {
|
||||||
case "individual":
|
case "individual":
|
||||||
return mockIndividualApps
|
console.log('🔍 個人賽APP數據:', {
|
||||||
|
availableAppsLength: availableApps.length,
|
||||||
|
availableApps: availableApps.slice(0, 2)
|
||||||
|
})
|
||||||
|
return availableApps
|
||||||
case "team":
|
case "team":
|
||||||
return dbTeams.length > 0 ? dbTeams : teams
|
// 總是使用 dbTeams,如果為空則返回空數組
|
||||||
|
console.log('🔍 getParticipants 團隊數據:', {
|
||||||
|
dbTeamsLength: dbTeams.length,
|
||||||
|
dbTeams: dbTeams.slice(0, 2), // 只顯示前2個
|
||||||
|
firstTeam: dbTeams[0] ? {
|
||||||
|
id: dbTeams[0].id,
|
||||||
|
name: dbTeams[0].name,
|
||||||
|
leader_name: dbTeams[0].leader_name,
|
||||||
|
member_count: dbTeams[0].member_count,
|
||||||
|
submissionDate: dbTeams[0].submissionDate
|
||||||
|
} : null
|
||||||
|
})
|
||||||
|
return dbTeams
|
||||||
default:
|
default:
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -875,15 +898,22 @@ export function CompetitionManagement() {
|
|||||||
let searchTerm = participantSearchTerm
|
let searchTerm = participantSearchTerm
|
||||||
let departmentFilterValue = departmentFilter
|
let departmentFilterValue = departmentFilter
|
||||||
|
|
||||||
|
console.log('🔍 getFilteredParticipants 調用:', {
|
||||||
|
competitionType,
|
||||||
|
participantsLength: participants.length,
|
||||||
|
searchTerm,
|
||||||
|
departmentFilterValue
|
||||||
|
})
|
||||||
|
|
||||||
// Use separate search terms for mixed competitions
|
// Use separate search terms for mixed competitions
|
||||||
if (newCompetition.type === "mixed") {
|
if (newCompetition.type === "mixed") {
|
||||||
searchTerm = competitionType === "individual" ? individualParticipantSearchTerm : teamParticipantSearchTerm
|
searchTerm = competitionType === "individual" ? individualParticipantSearchTerm : teamParticipantSearchTerm
|
||||||
departmentFilterValue = competitionType === "individual" ? individualDepartmentFilter : teamDepartmentFilter
|
departmentFilterValue = competitionType === "individual" ? individualDepartmentFilter : teamDepartmentFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
return participants.filter((participant) => {
|
const filtered = participants.filter((participant) => {
|
||||||
const searchField = competitionType === "team" ? participant.name : participant.name
|
const searchField = competitionType === "team" ? participant.name : participant.name
|
||||||
const creatorField = competitionType === "team" ? participant.leader : participant.creator
|
const creatorField = competitionType === "team" ? (participant.leader_name || participant.leader) : (participant.creator_name || participant.creator_id || participant.creator)
|
||||||
|
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
searchField.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
searchField.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
@@ -891,6 +921,21 @@ export function CompetitionManagement() {
|
|||||||
const matchesDepartment = departmentFilterValue === "all" || participant.department === departmentFilterValue
|
const matchesDepartment = departmentFilterValue === "all" || participant.department === departmentFilterValue
|
||||||
return matchesSearch && matchesDepartment
|
return matchesSearch && matchesDepartment
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('🔍 getFilteredParticipants 結果:', {
|
||||||
|
competitionType,
|
||||||
|
filteredLength: filtered.length,
|
||||||
|
filtered: filtered.slice(0, 2).map(f => ({
|
||||||
|
id: f.id,
|
||||||
|
name: f.name,
|
||||||
|
leader_name: f.leader_name,
|
||||||
|
leader: f.leader,
|
||||||
|
member_count: f.member_count,
|
||||||
|
submissionDate: f.submissionDate
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
@@ -2522,7 +2567,7 @@ export function CompetitionManagement() {
|
|||||||
<TableRow key={competition.id} className={isCurrentCompetition ? "bg-purple-50" : ""}>
|
<TableRow key={competition.id} className={isCurrentCompetition ? "bg-purple-50" : ""}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{isCurrentCompetition && <Star className="w-4 h-4 text-purple-600 fill-current" />}
|
{isCurrentCompetition && <Star className="w-12 h-12 text-purple-600 fill-current" />}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{competition.name}</p>
|
<p className="font-medium">{competition.name}</p>
|
||||||
<p className="text-sm text-gray-500">{competition.description}</p>
|
<p className="text-sm text-gray-500">{competition.description}</p>
|
||||||
@@ -3092,6 +3137,7 @@ export function CompetitionManagement() {
|
|||||||
const matchesExpertise = judgeExpertiseFilter === "all" ||
|
const matchesExpertise = judgeExpertiseFilter === "all" ||
|
||||||
judge.expertise.some(exp => exp.includes(judgeExpertiseFilter))
|
judge.expertise.some(exp => exp.includes(judgeExpertiseFilter))
|
||||||
|
|
||||||
|
|
||||||
const matchesStatus = judgeStatusFilter === "all" ||
|
const matchesStatus = judgeStatusFilter === "all" ||
|
||||||
(judgeStatusFilter === "active" && judge.is_active === true) ||
|
(judgeStatusFilter === "active" && judge.is_active === true) ||
|
||||||
(judgeStatusFilter === "inactive" && judge.is_active === false)
|
(judgeStatusFilter === "inactive" && judge.is_active === false)
|
||||||
@@ -3124,7 +3170,7 @@ export function CompetitionManagement() {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center space-x-3 mb-3">
|
<div className="flex items-center space-x-3 mb-3">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className={`${!judge.is_active ? 'bg-orange-100 text-orange-700' : 'bg-purple-100 text-purple-700'}`}>
|
<AvatarFallback className={`${!judge.is_active ? 'bg-orange-100 text-orange-700' : 'bg-purple-100 text-purple-700'}`}>
|
||||||
{judge.name[0]}
|
{judge.name[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@@ -3974,7 +4020,7 @@ export function CompetitionManagement() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Avatar className="w-8 h-8">
|
<Avatar className="w-8 h-8">
|
||||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className="bg-blue-100 text-blue-700 text-sm">
|
<AvatarFallback className="bg-blue-100 text-blue-700 text-sm">
|
||||||
{judge.name[0]}
|
{judge.name[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@@ -4386,7 +4432,7 @@ export function CompetitionManagement() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Avatar className="w-8 h-8">
|
<Avatar className="w-8 h-8">
|
||||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className="bg-green-100 text-green-700 text-sm">
|
<AvatarFallback className="bg-green-100 text-green-700 text-sm">
|
||||||
{judge.name[0]}
|
{judge.name[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@@ -4789,7 +4835,7 @@ export function CompetitionManagement() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Avatar className="w-8 h-8">
|
<Avatar className="w-8 h-8">
|
||||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className="bg-purple-100 text-purple-700 text-sm">
|
<AvatarFallback className="bg-purple-100 text-purple-700 text-sm">
|
||||||
{judge.name[0]}
|
{judge.name[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@@ -5205,8 +5251,8 @@ export function CompetitionManagement() {
|
|||||||
{participant.department}
|
{participant.department}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 mt-1">創作者:{participant.creator}</p>
|
<p className="text-xs text-gray-600 mt-1">創作者:{participant.creator_name || participant.creator_id || participant.creator || '未知'}</p>
|
||||||
<p className="text-xs text-gray-500">提交日期:{participant.submissionDate || participant.created_at || '未知'}</p>
|
<p className="text-xs text-gray-500">提交日期:{participant.submissionDate ? new Date(participant.submissionDate).toLocaleDateString('zh-TW', { timeZone: 'Asia/Taipei', year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/-/g, '/') : '未知'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -5263,8 +5309,32 @@ export function CompetitionManagement() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-64 overflow-y-auto border rounded-lg p-4 bg-white">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-64 overflow-y-auto border rounded-lg p-4 bg-white">
|
||||||
{getFilteredParticipants("team").map((participant) => {
|
{isLoadingTeams ? (
|
||||||
|
<div className="col-span-2 flex items-center justify-center py-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
||||||
|
<p className="text-sm text-gray-600">載入團隊數據中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
getFilteredParticipants("team").map((participant) => {
|
||||||
const isSelected = newCompetition.participatingTeams.includes(participant.id)
|
const isSelected = newCompetition.participatingTeams.includes(participant.id)
|
||||||
|
console.log('🔍 團隊數據調試 - 完整對象:', participant)
|
||||||
|
console.log('🔍 團隊數據調試 - 關鍵欄位:', {
|
||||||
|
name: participant.name,
|
||||||
|
leader_name: participant.leader_name,
|
||||||
|
leader: participant.leader,
|
||||||
|
member_count: participant.member_count,
|
||||||
|
submissionDate: participant.submissionDate,
|
||||||
|
hasLeaderName: 'leader_name' in participant,
|
||||||
|
hasMemberCount: 'member_count' in participant,
|
||||||
|
allKeys: Object.keys(participant)
|
||||||
|
})
|
||||||
|
console.log('🔍 渲染測試:', {
|
||||||
|
leaderDisplay: participant.leader_name || participant.leader || '未知',
|
||||||
|
memberDisplay: participant.member_count || participant.memberCount || 0,
|
||||||
|
dateDisplay: participant.submissionDate ? new Date(participant.submissionDate).toLocaleDateString('zh-TW', { timeZone: 'Asia/Taipei', year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/-/g, '/') : '未知'
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={participant.id}
|
key={participant.id}
|
||||||
@@ -5318,11 +5388,12 @@ export function CompetitionManagement() {
|
|||||||
{participant.description && (
|
{participant.description && (
|
||||||
<p className="text-xs text-gray-500 line-clamp-2">{participant.description}</p>
|
<p className="text-xs text-gray-500 line-clamp-2">{participant.description}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-gray-500">提交日期:{participant.submissionDate || participant.created_at || '未知'}</p>
|
<p className="text-xs text-gray-500">提交日期:{participant.submissionDate ? new Date(participant.submissionDate).toLocaleDateString('zh-TW', { timeZone: 'Asia/Taipei', year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/-/g, '/') : '未知'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-3 bg-green-100 rounded-lg">
|
<div className="flex items-center justify-between p-3 bg-green-100 rounded-lg">
|
||||||
@@ -5432,11 +5503,11 @@ export function CompetitionManagement() {
|
|||||||
<div className="flex items-center space-x-4 text-xs text-gray-600">
|
<div className="flex items-center space-x-4 text-xs text-gray-600">
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<User className="w-3 h-3" />
|
<User className="w-3 h-3" />
|
||||||
<span>隊長:{participant.leader}</span>
|
<span>隊長:{participant.leader_name || participant.leader || '未知'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<Users className="w-3 h-3" />
|
<Users className="w-3 h-3" />
|
||||||
<span>{participant.memberCount}人</span>
|
<span>{participant.member_count || participant.memberCount || 0}人</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -5497,10 +5568,16 @@ export function CompetitionManagement() {
|
|||||||
|
|
||||||
{/* Individual information */}
|
{/* Individual information */}
|
||||||
{newCompetition.type === "individual" && (
|
{newCompetition.type === "individual" && (
|
||||||
<p className="text-xs text-gray-600 mt-1">創作者:{participant.creator}</p>
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-gray-600">創作者:{participant.creator_name || participant.creator_id || participant.creator || '未知'}</p>
|
||||||
|
<p className="text-xs text-gray-500">類型:{participant.type || participant.category || '未知'}</p>
|
||||||
|
{participant.description && (
|
||||||
|
<p className="text-xs text-gray-500 line-clamp-2">{participant.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-gray-500">提交日期:{participant.submissionDate}</p>
|
<p className="text-xs text-gray-500">提交日期:{participant.submissionDate ? new Date(participant.submissionDate).toLocaleDateString('zh-TW', { timeZone: 'Asia/Taipei', year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/-/g, '/') : '未知'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@@ -60,10 +60,15 @@ export function TeamManagement() {
|
|||||||
|
|
||||||
const [newMember, setNewMember] = useState({
|
const [newMember, setNewMember] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
|
user_id: "",
|
||||||
department: "HQBU",
|
department: "HQBU",
|
||||||
role: "成員",
|
role: "成員",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 可用用戶狀態
|
||||||
|
const [availableUsers, setAvailableUsers] = useState<any[]>([])
|
||||||
|
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
|
||||||
|
|
||||||
// 獲取團隊數據
|
// 獲取團隊數據
|
||||||
const fetchTeams = async () => {
|
const fetchTeams = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -87,8 +92,30 @@ export function TeamManagement() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTeams()
|
fetchTeams()
|
||||||
|
fetchAvailableUsers()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 獲取可用用戶列表
|
||||||
|
const fetchAvailableUsers = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingUsers(true)
|
||||||
|
const response = await fetch('/api/admin/users/available')
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setAvailableUsers(data.data)
|
||||||
|
} else {
|
||||||
|
console.error('獲取用戶列表失敗:', data.message)
|
||||||
|
setError('獲取用戶列表失敗')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取用戶列表失敗:', error)
|
||||||
|
setError('獲取用戶列表失敗')
|
||||||
|
} finally {
|
||||||
|
setIsLoadingUsers(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filteredTeams = apiTeams.filter((team) => {
|
const filteredTeams = apiTeams.filter((team) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
@@ -102,12 +129,20 @@ export function TeamManagement() {
|
|||||||
setShowTeamDetail(true)
|
setShowTeamDetail(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditTeam = (team: any) => {
|
const handleEditTeam = async (team: any) => {
|
||||||
setSelectedTeam(team)
|
setSelectedTeam(team)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 獲取團隊的詳細信息,包括成員
|
||||||
|
const response = await fetch(`/api/admin/teams/${team.id}`)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const teamDetails = data.data
|
||||||
|
|
||||||
// 確保成員數據結構正確
|
// 確保成員數據結構正確
|
||||||
const members = team.members && Array.isArray(team.members)
|
const members = teamDetails.members && Array.isArray(teamDetails.members)
|
||||||
? team.members.map((member: any) => ({
|
? teamDetails.members.map((member: any) => ({
|
||||||
id: member.user_id || member.id,
|
id: member.user_id || member.id,
|
||||||
user_id: member.user_id || member.id,
|
user_id: member.user_id || member.id,
|
||||||
name: member.name,
|
name: member.name,
|
||||||
@@ -117,16 +152,23 @@ export function TeamManagement() {
|
|||||||
: []
|
: []
|
||||||
|
|
||||||
setNewTeam({
|
setNewTeam({
|
||||||
name: team.name,
|
name: teamDetails.name,
|
||||||
department: team.department,
|
department: teamDetails.department,
|
||||||
contactEmail: team.contact_email || team.contactEmail,
|
contactEmail: teamDetails.contact_email || teamDetails.contactEmail,
|
||||||
description: team.description || '',
|
description: teamDetails.description || '',
|
||||||
members: members,
|
members: members,
|
||||||
leader: team.leader_id || team.leader,
|
leader: teamDetails.leader_id || teamDetails.leader,
|
||||||
apps: team.apps || [],
|
apps: teamDetails.apps || [],
|
||||||
totalLikes: team.total_likes || team.totalLikes || 0,
|
totalLikes: teamDetails.total_likes || teamDetails.totalLikes || 0,
|
||||||
})
|
})
|
||||||
setShowEditTeam(true)
|
setShowEditTeam(true)
|
||||||
|
} else {
|
||||||
|
setError('獲取團隊詳情失敗')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取團隊詳情失敗:', error)
|
||||||
|
setError('獲取團隊詳情失敗')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteTeam = (team: Team) => {
|
const handleDeleteTeam = (team: Team) => {
|
||||||
@@ -145,13 +187,20 @@ export function TeamManagement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddMember = () => {
|
const handleAddMember = () => {
|
||||||
if (!newMember.name.trim()) {
|
if (!newMember.user_id || !newMember.name.trim()) {
|
||||||
setError("請輸入成員姓名")
|
setError("請選擇成員")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查是否已經添加過這個成員
|
||||||
|
if (newTeam.members.some(member => member.user_id === newMember.user_id)) {
|
||||||
|
setError("該成員已經在團隊中")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const member: TeamMember = {
|
const member: TeamMember = {
|
||||||
id: `m${Date.now()}`,
|
id: newMember.user_id, // 使用真實的用戶 ID
|
||||||
|
user_id: newMember.user_id,
|
||||||
name: newMember.name,
|
name: newMember.name,
|
||||||
department: newMember.department,
|
department: newMember.department,
|
||||||
role: newMember.role,
|
role: newMember.role,
|
||||||
@@ -173,6 +222,7 @@ export function TeamManagement() {
|
|||||||
|
|
||||||
setNewMember({
|
setNewMember({
|
||||||
name: "",
|
name: "",
|
||||||
|
user_id: "",
|
||||||
department: "HQBU",
|
department: "HQBU",
|
||||||
role: "成員",
|
role: "成員",
|
||||||
})
|
})
|
||||||
@@ -262,6 +312,9 @@ export function TeamManagement() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('🔍 準備更新的團隊數據:', updateData)
|
||||||
|
console.log('🔍 成員數據:', newTeam.members)
|
||||||
|
|
||||||
// 調用 API 更新團隊
|
// 調用 API 更新團隊
|
||||||
const response = await fetch(`/api/admin/teams/${selectedTeam.id}`, {
|
const response = await fetch(`/api/admin/teams/${selectedTeam.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -731,13 +784,30 @@ export function TeamManagement() {
|
|||||||
<h4 className="font-semibold">新增成員</h4>
|
<h4 className="font-semibold">新增成員</h4>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="editMemberName">成員姓名</Label>
|
<Label htmlFor="editMemberSelect">選擇成員</Label>
|
||||||
<Input
|
<Select
|
||||||
id="editMemberName"
|
value={newMember.user_id}
|
||||||
value={newMember.name}
|
onValueChange={(value) => {
|
||||||
onChange={(e) => setNewMember({ ...newMember, name: e.target.value })}
|
const selectedUser = availableUsers.find(user => user.id === value)
|
||||||
placeholder="輸入成員姓名"
|
setNewMember({
|
||||||
/>
|
...newMember,
|
||||||
|
user_id: value,
|
||||||
|
name: selectedUser?.name || "",
|
||||||
|
department: selectedUser?.department || "HQBU"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="選擇團隊成員" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableUsers.map((user) => (
|
||||||
|
<SelectItem key={user.id} value={user.id}>
|
||||||
|
{user.name} ({user.department})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="editMemberDepartment">部門</Label>
|
<Label htmlFor="editMemberDepartment">部門</Label>
|
||||||
|
@@ -79,7 +79,7 @@ export function ProfileDialog({ open, onOpenChange }: ProfileDialogProps) {
|
|||||||
<div className="flex items-center space-x-6">
|
<div className="flex items-center space-x-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar className="w-24 h-24">
|
<Avatar className="w-24 h-24">
|
||||||
<AvatarImage src={user?.avatar || "/placeholder.svg"} />
|
<AvatarImage src={user?.avatar} />
|
||||||
<AvatarFallback className="text-2xl bg-gradient-to-r from-blue-600 to-purple-600 text-white">
|
<AvatarFallback className="text-2xl bg-gradient-to-r from-blue-600 to-purple-600 text-white">
|
||||||
{user?.name?.charAt(0) || "U"}
|
{user?.name?.charAt(0) || "U"}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
|
@@ -427,7 +427,7 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
|
|||||||
return (
|
return (
|
||||||
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={judge.avatar || "/placeholder.svg?height=40&width=40"} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -495,7 +495,7 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Avatar className="w-12 h-12">
|
<Avatar className="w-12 h-12">
|
||||||
<AvatarImage src={judgeScore.judgeAvatar || "/placeholder.svg"} />
|
<AvatarImage src={judgeScore.judgeAvatar} />
|
||||||
<AvatarFallback className="bg-blue-100 text-blue-700">{judgeScore.judgeName[0]}</AvatarFallback>
|
<AvatarFallback className="bg-blue-100 text-blue-700">{judgeScore.judgeName[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
@@ -192,7 +192,7 @@ export function CompetitionDetailDialog({
|
|||||||
<div key={score.judgeId} className="border rounded-lg p-6">
|
<div key={score.judgeId} className="border rounded-lg p-6">
|
||||||
<div className="flex items-center space-x-4 mb-4">
|
<div className="flex items-center space-x-4 mb-4">
|
||||||
<Avatar className="w-12 h-12">
|
<Avatar className="w-12 h-12">
|
||||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className="bg-purple-100 text-purple-700 text-lg">
|
<AvatarFallback className="bg-purple-100 text-purple-700 text-lg">
|
||||||
{judge.name[0]}
|
{judge.name[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@@ -377,7 +377,7 @@ export function CompetitionDetailDialog({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={`/placeholder-40x40.png?height=40&width=40&text=${member.name[0]}`} />
|
<AvatarImage src={member.avatar} />
|
||||||
<AvatarFallback className="bg-green-100 text-green-700">{member.name[0]}</AvatarFallback>
|
<AvatarFallback className="bg-green-100 text-green-700">{member.name[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -586,7 +586,7 @@ export function CompetitionDetailDialog({
|
|||||||
<div key={score.judgeId} className="border rounded-lg p-6">
|
<div key={score.judgeId} className="border rounded-lg p-6">
|
||||||
<div className="flex items-center space-x-4 mb-4">
|
<div className="flex items-center space-x-4 mb-4">
|
||||||
<Avatar className="w-12 h-12">
|
<Avatar className="w-12 h-12">
|
||||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className="bg-purple-100 text-purple-700 text-lg">
|
<AvatarFallback className="bg-purple-100 text-purple-700 text-lg">
|
||||||
{judge.name[0]}
|
{judge.name[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@@ -677,7 +677,7 @@ export function CompetitionDetailDialog({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={`/placeholder-40x40.png?height=40&width=40&text=${member.name[0]}`} />
|
<AvatarImage src={member.avatar} />
|
||||||
<AvatarFallback className="bg-green-100 text-green-700">{member.name[0]}</AvatarFallback>
|
<AvatarFallback className="bg-green-100 text-green-700">{member.name[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
@@ -145,7 +145,7 @@ export function JudgeScoringDialog({ open, onOpenChange, appId, appName, judgeId
|
|||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
|
@@ -693,7 +693,7 @@ export function PopularityRankings() {
|
|||||||
{competitionJudges.map((judge) => (
|
{competitionJudges.map((judge) => (
|
||||||
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
<AvatarImage src={judge.avatar} />
|
||||||
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
@@ -309,7 +309,7 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
|
|||||||
{team.members.map((member: any, index: number) => (
|
{team.members.map((member: any, index: number) => (
|
||||||
<div key={member.id} className="flex items-center space-x-3 p-4 border rounded-lg">
|
<div key={member.id} className="flex items-center space-x-3 p-4 border rounded-lg">
|
||||||
<Avatar className="w-12 h-12">
|
<Avatar className="w-12 h-12">
|
||||||
<AvatarImage src={`/placeholder-40x40.png`} />
|
<AvatarImage src={member.avatar} />
|
||||||
<AvatarFallback className="bg-green-100 text-green-700 font-medium">
|
<AvatarFallback className="bg-green-100 text-green-700 font-medium">
|
||||||
{member.name[0]}
|
{member.name[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
|
@@ -882,6 +882,7 @@ export class JudgeService extends DatabaseServiceBase {
|
|||||||
const result = await db.queryOne<Judge>(sql, [id]);
|
const result = await db.queryOne<Judge>(sql, [id]);
|
||||||
if (result) {
|
if (result) {
|
||||||
result.expertise = this.parseExpertise(result.expertise as any);
|
result.expertise = this.parseExpertise(result.expertise as any);
|
||||||
|
result.is_active = Boolean(result.is_active); // 確保 is_active 是布爾值
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -892,7 +893,8 @@ export class JudgeService extends DatabaseServiceBase {
|
|||||||
const results = await db.query<Judge>(sql);
|
const results = await db.query<Judge>(sql);
|
||||||
return results.map(judge => ({
|
return results.map(judge => ({
|
||||||
...judge,
|
...judge,
|
||||||
expertise: this.parseExpertise(judge.expertise as any)
|
expertise: this.parseExpertise(judge.expertise as any),
|
||||||
|
is_active: Boolean(judge.is_active) // 確保 is_active 是布爾值
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,17 +1012,26 @@ export class TeamService extends DatabaseServiceBase {
|
|||||||
SELECT t.*,
|
SELECT t.*,
|
||||||
u.name as leader_name,
|
u.name as leader_name,
|
||||||
u.phone as leader_phone,
|
u.phone as leader_phone,
|
||||||
|
t.leader_id as leader,
|
||||||
COUNT(DISTINCT tm.id) as member_count,
|
COUNT(DISTINCT tm.id) as member_count,
|
||||||
COUNT(DISTINCT a.id) as app_count
|
COUNT(DISTINCT a.id) as app_count,
|
||||||
|
t.created_at as submissionDate
|
||||||
FROM teams t
|
FROM teams t
|
||||||
LEFT JOIN users u ON t.leader_id = u.id
|
LEFT JOIN users u ON t.leader_id = u.id
|
||||||
LEFT JOIN team_members tm ON t.id = tm.team_id
|
LEFT JOIN team_members tm ON t.id = tm.team_id
|
||||||
LEFT JOIN apps a ON t.id = a.team_id AND a.is_active = 1
|
LEFT JOIN apps a ON t.id = a.team_id AND a.is_active = 1
|
||||||
WHERE t.is_active = 1
|
WHERE t.is_active = 1
|
||||||
GROUP BY t.id
|
GROUP BY t.id, t.name, t.leader_id, t.department, t.contact_email, t.total_likes, t.is_active, t.created_at, t.updated_at, u.name, u.phone
|
||||||
ORDER BY t.created_at DESC
|
ORDER BY t.created_at DESC
|
||||||
`;
|
`;
|
||||||
const results = await DatabaseServiceBase.safeQuery(sql);
|
const results = await DatabaseServiceBase.safeQuery(sql);
|
||||||
|
console.log('🔍 getAllTeams 查詢結果:', results.slice(0, 2).map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
leader_name: r.leader_name,
|
||||||
|
member_count: r.member_count,
|
||||||
|
submissionDate: r.submissionDate
|
||||||
|
})));
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1104,9 +1115,11 @@ export class TeamService extends DatabaseServiceBase {
|
|||||||
for (const member of updates.members) {
|
for (const member of updates.members) {
|
||||||
if (member.user_id && member.role) {
|
if (member.user_id && member.role) {
|
||||||
console.log(`添加成員: ${member.user_id} (${member.role})`);
|
console.log(`添加成員: ${member.user_id} (${member.role})`);
|
||||||
|
// 生成唯一的 ID
|
||||||
|
const memberId = `tm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
await DatabaseServiceBase.safeInsert(
|
await DatabaseServiceBase.safeInsert(
|
||||||
'INSERT INTO team_members (team_id, user_id, role, joined_at) VALUES (?, ?, ?, NOW())',
|
'INSERT INTO team_members (id, team_id, user_id, role, joined_at) VALUES (?, ?, ?, ?, NOW())',
|
||||||
[id, member.user_id, member.role]
|
[memberId, id, member.user_id, member.role]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
50
scripts/insert-team-members.sql
Normal file
50
scripts/insert-team-members.sql
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- 插入團隊成員測試數據
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- 首先查看現有的團隊
|
||||||
|
SELECT '=== 現有團隊 ===' as info;
|
||||||
|
SELECT id, name, leader_id FROM teams WHERE is_active = 1;
|
||||||
|
|
||||||
|
-- 查看現有用戶
|
||||||
|
SELECT '=== 現有用戶 ===' as info;
|
||||||
|
SELECT id, name, department FROM users WHERE status = 'active' LIMIT 10;
|
||||||
|
|
||||||
|
-- 為每個團隊插入成員數據
|
||||||
|
-- 團隊 1: aaa (ID: t1757702332911zcl6iafq1)
|
||||||
|
INSERT IGNORE INTO team_members (id, team_id, user_id, role, joined_at) VALUES
|
||||||
|
('tm_1_1', 't1757702332911zcl6iafq1', 'db19b491-8f63-44b5-a28a-1f8eeb4fdd3c', '隊長', NOW()),
|
||||||
|
('tm_1_2', 't1757702332911zcl6iafq1', 'db19b491-8f63-44b5-a28a-1f8eeb4fdd3c', '成員', NOW());
|
||||||
|
|
||||||
|
-- 如果有其他團隊,請按照相同格式添加
|
||||||
|
-- 團隊 2: (如果有的話)
|
||||||
|
-- INSERT IGNORE INTO team_members (id, team_id, user_id, role, joined_at) VALUES
|
||||||
|
-- ('tm_2_1', 'team_id_2', 'user_id_2', '隊長', NOW()),
|
||||||
|
-- ('tm_2_2', 'team_id_2', 'user_id_3', '成員', NOW());
|
||||||
|
|
||||||
|
-- 驗證插入結果
|
||||||
|
SELECT '=== 團隊成員插入結果 ===' as info;
|
||||||
|
SELECT
|
||||||
|
tm.id,
|
||||||
|
tm.team_id,
|
||||||
|
t.name as team_name,
|
||||||
|
tm.user_id,
|
||||||
|
u.name as user_name,
|
||||||
|
tm.role,
|
||||||
|
tm.joined_at
|
||||||
|
FROM team_members tm
|
||||||
|
LEFT JOIN teams t ON tm.team_id = t.id
|
||||||
|
LEFT JOIN users u ON tm.user_id = u.id
|
||||||
|
ORDER BY tm.team_id, tm.role;
|
||||||
|
|
||||||
|
-- 檢查團隊成員統計
|
||||||
|
SELECT '=== 團隊成員統計 ===' as info;
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.name as team_name,
|
||||||
|
COUNT(tm.id) as member_count
|
||||||
|
FROM teams t
|
||||||
|
LEFT JOIN team_members tm ON t.id = tm.team_id
|
||||||
|
WHERE t.is_active = 1
|
||||||
|
GROUP BY t.id, t.name
|
||||||
|
ORDER BY t.name;
|
82
scripts/insert-test-team-members.js
Normal file
82
scripts/insert-test-team-members.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
|
||||||
|
async function insertTeamMembers() {
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host: 'mysql.theaken.com',
|
||||||
|
port: 33306,
|
||||||
|
user: 'AI_Platform',
|
||||||
|
password: 'Aa123456',
|
||||||
|
database: 'db_AI_Platform',
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
timezone: '+08:00'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 開始插入團隊成員測試數據...');
|
||||||
|
|
||||||
|
// 查看現有團隊
|
||||||
|
const [teams] = await connection.execute('SELECT id, name, leader_id FROM teams WHERE is_active = 1 LIMIT 5');
|
||||||
|
console.log('現有團隊:', teams);
|
||||||
|
|
||||||
|
// 查看現有用戶
|
||||||
|
const [users] = await connection.execute('SELECT id, name FROM users WHERE status = "active" LIMIT 5');
|
||||||
|
console.log('現有用戶:', users);
|
||||||
|
|
||||||
|
if (teams.length > 0 && users.length > 0) {
|
||||||
|
// 為每個團隊插入成員
|
||||||
|
for (let i = 0; i < Math.min(teams.length, 3); i++) {
|
||||||
|
const team = teams[i];
|
||||||
|
const teamId = team.id;
|
||||||
|
|
||||||
|
// 插入隊長(使用團隊的 leader_id)
|
||||||
|
await connection.execute(
|
||||||
|
'INSERT IGNORE INTO team_members (id, team_id, user_id, role, joined_at) VALUES (?, ?, ?, ?, NOW())',
|
||||||
|
[`tm_${Date.now()}_${i}_1`, teamId, team.leader_id, '隊長']
|
||||||
|
);
|
||||||
|
|
||||||
|
// 插入成員(使用其他用戶)
|
||||||
|
for (let j = 1; j < Math.min(users.length, 3); j++) {
|
||||||
|
if (users[j].id !== team.leader_id) {
|
||||||
|
await connection.execute(
|
||||||
|
'INSERT IGNORE INTO team_members (id, team_id, user_id, role, joined_at) VALUES (?, ?, ?, ?, NOW())',
|
||||||
|
[`tm_${Date.now()}_${i}_${j+1}`, teamId, users[j].id, '成員']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 團隊 ${team.name} 成員插入成功`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驗證結果
|
||||||
|
const [members] = await connection.execute(`
|
||||||
|
SELECT tm.*, t.name as team_name, u.name as user_name
|
||||||
|
FROM team_members tm
|
||||||
|
LEFT JOIN teams t ON tm.team_id = t.id
|
||||||
|
LEFT JOIN users u ON tm.user_id = u.id
|
||||||
|
ORDER BY tm.team_id
|
||||||
|
`);
|
||||||
|
console.log('📊 團隊成員統計:', members);
|
||||||
|
|
||||||
|
// 檢查團隊成員數量
|
||||||
|
const [counts] = await connection.execute(`
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.name as team_name,
|
||||||
|
COUNT(tm.id) as member_count
|
||||||
|
FROM teams t
|
||||||
|
LEFT JOIN team_members tm ON t.id = tm.team_id
|
||||||
|
WHERE t.is_active = 1
|
||||||
|
GROUP BY t.id, t.name
|
||||||
|
ORDER BY t.name
|
||||||
|
`);
|
||||||
|
console.log('📈 團隊成員數量:', counts);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 錯誤:', error);
|
||||||
|
} finally {
|
||||||
|
await connection.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertTeamMembers();
|
70
scripts/test-team-data.js
Normal file
70
scripts/test-team-data.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
|
||||||
|
async function testTeamData() {
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host: 'mysql.theaken.com',
|
||||||
|
port: 33306,
|
||||||
|
user: 'AI_Platform',
|
||||||
|
password: 'Aa123456',
|
||||||
|
database: 'db_AI_Platform',
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
timezone: '+08:00'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 測試團隊數據查詢...');
|
||||||
|
|
||||||
|
// 使用與 getAllTeams 相同的查詢
|
||||||
|
const [results] = await connection.execute(`
|
||||||
|
SELECT t.*,
|
||||||
|
u.name as leader_name,
|
||||||
|
u.phone as leader_phone,
|
||||||
|
t.leader_id as leader,
|
||||||
|
COUNT(DISTINCT tm.id) as member_count,
|
||||||
|
COUNT(DISTINCT a.id) as app_count,
|
||||||
|
t.created_at as submissionDate
|
||||||
|
FROM teams t
|
||||||
|
LEFT JOIN users u ON t.leader_id = u.id
|
||||||
|
LEFT JOIN team_members tm ON t.id = tm.team_id
|
||||||
|
LEFT JOIN apps a ON t.id = a.team_id AND a.is_active = 1
|
||||||
|
WHERE t.is_active = 1
|
||||||
|
GROUP BY t.id, t.name, t.leader_id, t.department, t.contact_email, t.total_likes, t.is_active, t.created_at, t.updated_at, u.name, u.phone
|
||||||
|
ORDER BY t.created_at DESC
|
||||||
|
LIMIT 5
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('📊 查詢結果:');
|
||||||
|
results.forEach((team, index) => {
|
||||||
|
console.log(`\n團隊 ${index + 1}:`);
|
||||||
|
console.log(` 名稱: ${team.name}`);
|
||||||
|
console.log(` 隊長ID: ${team.leader}`);
|
||||||
|
console.log(` 隊長姓名: ${team.leader_name || 'NULL'}`);
|
||||||
|
console.log(` 成員數量: ${team.member_count || 0}`);
|
||||||
|
console.log(` 提交日期: ${team.submissionDate || 'NULL'}`);
|
||||||
|
console.log(` 部門: ${team.department}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 檢查特定團隊的成員
|
||||||
|
if (results.length > 0) {
|
||||||
|
const teamId = results[0].id;
|
||||||
|
const [members] = await connection.execute(`
|
||||||
|
SELECT tm.*, u.name as user_name
|
||||||
|
FROM team_members tm
|
||||||
|
LEFT JOIN users u ON tm.user_id = u.id
|
||||||
|
WHERE tm.team_id = ?
|
||||||
|
`, [teamId]);
|
||||||
|
|
||||||
|
console.log(`\n🔍 團隊 ${results[0].name} 的成員:`);
|
||||||
|
members.forEach(member => {
|
||||||
|
console.log(` - ${member.user_name} (${member.role})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 錯誤:', error);
|
||||||
|
} finally {
|
||||||
|
await connection.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testTeamData();
|
@@ -24,6 +24,7 @@ export interface JudgeScore {
|
|||||||
// 新增團隊成員接口
|
// 新增團隊成員接口
|
||||||
export interface TeamMember {
|
export interface TeamMember {
|
||||||
id: string
|
id: string
|
||||||
|
user_id?: string // 添加可選的 user_id 字段
|
||||||
name: string
|
name: string
|
||||||
department: string
|
department: string
|
||||||
role: string // 角色:隊長、成員等
|
role: string // 角色:隊長、成員等
|
||||||
|
Reference in New Issue
Block a user