diff --git a/app/api/criteria-templates/[id]/route.ts b/app/api/criteria-templates/[id]/route.ts new file mode 100644 index 0000000..69c1455 --- /dev/null +++ b/app/api/criteria-templates/[id]/route.ts @@ -0,0 +1,166 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CriteriaTemplateService, CriteriaItemService } from '@/lib/services/database'; + +// GET - 獲取特定評分標準模板 +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const templateId = parseInt(params.id); + + if (isNaN(templateId)) { + return NextResponse.json( + { success: false, error: '無效的模板 ID' }, + { status: 400 } + ); + } + + const template = await CriteriaTemplateService.findWithItems(templateId); + + if (!template) { + return NextResponse.json( + { success: false, error: '找不到指定的評分標準模板' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + data: template + }); + } catch (error) { + console.error('獲取評分標準模板失敗:', error); + return NextResponse.json( + { success: false, error: '獲取評分標準模板失敗' }, + { status: 500 } + ); + } +} + +// PUT - 更新評分標準模板 +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const templateId = parseInt(params.id); + + if (isNaN(templateId)) { + return NextResponse.json( + { success: false, error: '無效的模板 ID' }, + { status: 400 } + ); + } + + const body = await request.json(); + const { name, description, items } = body; + + if (!name || !items || !Array.isArray(items)) { + return NextResponse.json( + { success: false, error: '請提供模板名稱和評分項目' }, + { status: 400 } + ); + } + + // 驗證權重總和 + const totalWeight = items.reduce((sum: number, item: any) => sum + (item.weight || 0), 0); + if (Math.abs(totalWeight - 100) > 0.01) { + return NextResponse.json( + { success: false, error: '權重總和必須等於 100%' }, + { status: 400 } + ); + } + + // 驗證所有項目都有名稱 + if (items.some((item: any) => !item.name?.trim())) { + return NextResponse.json( + { success: false, error: '所有評分項目都必須有名稱' }, + { status: 400 } + ); + } + + // 使用事務更新模板和項目 + const { transaction } = await import('@/lib/database'); + + await transaction(async (connection) => { + // 更新模板 + await connection.execute( + 'UPDATE criteria_templates SET name = ?, description = ?, total_weight = ?, updated_at = NOW() WHERE id = ?', + [name, description || '', totalWeight, templateId] + ); + + // 刪除舊的評分項目 + await connection.execute( + 'DELETE FROM criteria_items WHERE template_id = ?', + [templateId] + ); + + // 創建新的評分項目 + for (let i = 0; i < items.length; i++) { + const item = items[i]; + await connection.execute( + 'INSERT INTO criteria_items (template_id, name, description, weight, max_score, sort_order) VALUES (?, ?, ?, ?, ?, ?)', + [ + templateId, + item.name, + item.description || '', + item.weight, + item.maxScore || 10, + i + 1 + ] + ); + } + }); + + return NextResponse.json({ + success: true, + message: '評分標準模板更新成功' + }); + } catch (error) { + console.error('更新評分標準模板失敗:', error); + return NextResponse.json( + { success: false, error: '更新評分標準模板失敗' }, + { status: 500 } + ); + } +} + +// DELETE - 刪除評分標準模板 +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const templateId = parseInt(params.id); + + if (isNaN(templateId)) { + return NextResponse.json( + { success: false, error: '無效的模板 ID' }, + { status: 400 } + ); + } + + // 檢查是否為預設模板 + const template = await CriteriaTemplateService.findById(templateId); + if (template?.is_default) { + return NextResponse.json( + { success: false, error: '無法刪除預設模板' }, + { status: 400 } + ); + } + + await CriteriaTemplateService.delete(templateId); + + return NextResponse.json({ + success: true, + message: '評分標準模板刪除成功' + }); + } catch (error) { + console.error('刪除評分標準模板失敗:', error); + return NextResponse.json( + { success: false, error: '刪除評分標準模板失敗' }, + { status: 500 } + ); + } +} diff --git a/app/api/criteria-templates/default/route.ts b/app/api/criteria-templates/default/route.ts new file mode 100644 index 0000000..33d1fae --- /dev/null +++ b/app/api/criteria-templates/default/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from 'next/server'; +import { CriteriaTemplateService } from '@/lib/services/database'; + +// GET - 獲取預設評分標準模板 +export async function GET() { + try { + const template = await CriteriaTemplateService.findDefault(); + + if (!template) { + return NextResponse.json( + { success: false, error: '找不到預設評分標準模板' }, + { status: 404 } + ); + } + + const templateWithItems = await CriteriaTemplateService.findWithItems(template.id); + + return NextResponse.json({ + success: true, + data: templateWithItems + }); + } catch (error) { + console.error('獲取預設評分標準模板失敗:', error); + return NextResponse.json( + { success: false, error: '獲取預設評分標準模板失敗' }, + { status: 500 } + ); + } +} diff --git a/app/api/criteria-templates/route.ts b/app/api/criteria-templates/route.ts new file mode 100644 index 0000000..81f6bea --- /dev/null +++ b/app/api/criteria-templates/route.ts @@ -0,0 +1,126 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CriteriaTemplateService, CriteriaItemService } from '@/lib/services/database'; + +// GET - 獲取所有評分標準模板 +export async function GET() { + try { + // 這裡暫時使用 user_id = 1,實際應用中應該從 session 獲取 + const templates = await CriteriaTemplateService.findByUserId(1); + + // 獲取每個模板的評分項目 + const templatesWithItems = await Promise.all( + templates.map(async (template) => { + const items = await CriteriaItemService.findByTemplateId(template.id); + return { ...template, items }; + }) + ); + + return NextResponse.json({ + success: true, + data: templatesWithItems + }); + } catch (error) { + console.error('獲取評分標準模板失敗:', error); + return NextResponse.json( + { success: false, error: '獲取評分標準模板失敗' }, + { status: 500 } + ); + } +} + +// POST - 創建或更新評分標準模板(單一模板模式) +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { name, description, items } = body; + + if (!name || !items || !Array.isArray(items)) { + return NextResponse.json( + { success: false, error: '請提供模板名稱和評分項目' }, + { status: 400 } + ); + } + + // 驗證權重總和 + const totalWeight = items.reduce((sum: number, item: any) => sum + (Number(item.weight) || 0), 0); + if (Math.abs(totalWeight - 100) > 0.01) { + return NextResponse.json( + { success: false, error: '權重總和必須等於 100%' }, + { status: 400 } + ); + } + + // 驗證所有項目都有名稱 + if (items.some((item: any) => !item.name?.trim())) { + return NextResponse.json( + { success: false, error: '所有評分項目都必須有名稱' }, + { status: 400 } + ); + } + + // 使用事務創建或更新模板 + const { transaction } = await import('@/lib/database'); + + const result = await transaction(async (connection) => { + // 檢查是否已存在模板 + const [existingTemplates] = await connection.execute( + 'SELECT id FROM criteria_templates WHERE user_id = ? LIMIT 1', + [1] + ); + + let templateId; + + if (existingTemplates.length > 0) { + // 更新現有模板 + templateId = existingTemplates[0].id; + await connection.execute( + 'UPDATE criteria_templates SET name = ?, description = ?, total_weight = ?, updated_at = NOW() WHERE id = ?', + [name, description || '', totalWeight, templateId] + ); + + // 刪除舊的評分項目 + await connection.execute( + 'DELETE FROM criteria_items WHERE template_id = ?', + [templateId] + ); + } else { + // 創建新模板 + const [templateResult] = await connection.execute( + 'INSERT INTO criteria_templates (user_id, name, description, is_default, is_public, total_weight) VALUES (?, ?, ?, ?, ?, ?)', + [1, name, description || '', 1, 1, totalWeight] + ); + templateId = (templateResult as any).insertId; + } + + // 創建評分項目 + for (let i = 0; i < items.length; i++) { + const item = items[i]; + await connection.execute( + 'INSERT INTO criteria_items (template_id, name, description, weight, max_score, sort_order) VALUES (?, ?, ?, ?, ?, ?)', + [ + templateId, + item.name, + item.description || '', + Number(item.weight) || 0, + Number(item.maxScore) || 10, + i + 1 + ] + ); + } + + return templateId; + }); + + return NextResponse.json({ + success: true, + data: { id: result }, + message: '評分標準模板儲存成功' + }); + } catch (error) { + console.error('儲存評分標準模板失敗:', error); + return NextResponse.json( + { success: false, error: '儲存評分標準模板失敗' }, + { status: 500 } + ); + } +} diff --git a/app/criteria/page.tsx b/app/criteria/page.tsx index eafbf0e..ab3a293 100644 --- a/app/criteria/page.tsx +++ b/app/criteria/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { Sidebar } from "@/components/sidebar" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" @@ -9,60 +9,93 @@ import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Slider } from "@/components/ui/slider" import { Badge } from "@/components/ui/badge" -import { Plus, Trash2, Save, RotateCcw, FileText } from "lucide-react" +import { Plus, Trash2, Save, RotateCcw, FileText, Loader2 } from "lucide-react" import { useToast } from "@/hooks/use-toast" interface CriteriaItem { - id: string + id: number | string name: string description: string weight: number maxScore: number } -const defaultCriteria: CriteriaItem[] = [ - { - id: "1", - name: "內容品質", - description: "內容的準確性、完整性和專業度", - weight: 25, - maxScore: 10, - }, - { - id: "2", - name: "視覺設計", - description: "版面設計、色彩搭配和視覺效果", - weight: 20, - maxScore: 10, - }, - { - id: "3", - name: "邏輯結構", - description: "內容組織的邏輯性和條理性", - weight: 20, - maxScore: 10, - }, - { - id: "4", - name: "創新性", - description: "創意思維和獨特觀點的展現", - weight: 15, - maxScore: 10, - }, - { - id: "5", - name: "實用性", - description: "內容的實際應用價值和可操作性", - weight: 20, - maxScore: 10, - }, -] +interface CriteriaTemplate { + id: number + name: string + description: string + total_weight: number + items: CriteriaItem[] +} export default function CriteriaPage() { - const [criteria, setCriteria] = useState(defaultCriteria) - const [templateName, setTemplateName] = useState("預設評分標準") + const [criteria, setCriteria] = useState([]) + const [templateName, setTemplateName] = useState("") + const [templateDescription, setTemplateDescription] = useState("") + const [isLoading, setIsLoading] = useState(true) + const [isSaving, setIsSaving] = useState(false) + const [currentTemplate, setCurrentTemplate] = useState(null) const { toast } = useToast() + // 載入評分標準 + useEffect(() => { + loadTemplate() + }, []) + + const loadTemplate = async () => { + try { + setIsLoading(true) + const response = await fetch('/api/criteria-templates') + const result = await response.json() + + if (result.success && result.data && result.data.length > 0) { + // 載入第一個模板(單一模板模式) + const template = result.data[0] + setCurrentTemplate(template) + setTemplateName(template.name) + setTemplateDescription(template.description || '') + setCriteria(template.items || []) + } else { + // 如果沒有模板,載入預設模板 + await loadDefaultTemplate() + } + } catch (error) { + console.error('載入評分標準失敗:', error) + // 如果載入失敗,嘗試載入預設模板 + await loadDefaultTemplate() + } finally { + setIsLoading(false) + } + } + + const loadDefaultTemplate = async () => { + try { + const response = await fetch('/api/criteria-templates/default') + const result = await response.json() + + if (result.success && result.data) { + const template = result.data + setCurrentTemplate(template) + setTemplateName(template.name) + setTemplateDescription(template.description || '') + setCriteria(template.items || []) + } else { + // 如果連預設模板都沒有,使用空模板 + setCurrentTemplate(null) + setTemplateName('') + setTemplateDescription('') + setCriteria([]) + } + } catch (error) { + console.error('載入預設評分標準失敗:', error) + // 使用空模板 + setCurrentTemplate(null) + setTemplateName('') + setTemplateDescription('') + setCriteria([]) + } + } + const addCriteria = () => { const newCriteria: CriteriaItem = { id: Date.now().toString(), @@ -83,12 +116,12 @@ export default function CriteriaPage() { } const updateWeight = (id: string, weight: number[]) => { - updateCriteria(id, "weight", weight[0]) + updateCriteria(id, "weight", Number(weight[0]) || 0) } - const totalWeight = criteria.reduce((sum, item) => sum + item.weight, 0) + const totalWeight = criteria.reduce((sum, item) => sum + (Number(item.weight) || 0), 0) - const saveCriteria = () => { + const saveCriteria = async () => { if (totalWeight !== 100) { toast({ title: "權重設定錯誤", @@ -107,20 +140,98 @@ export default function CriteriaPage() { return } - // 這裡會連接到後端 API 儲存評分標準 - toast({ - title: "儲存成功", - description: "評分標準已成功儲存", - }) + if (!templateName.trim()) { + toast({ + title: "設定不完整", + description: "請填寫模板名稱", + variant: "destructive", + }) + return + } + + try { + setIsSaving(true) + + const templateData = { + name: templateName.trim(), + description: templateDescription.trim(), + items: criteria.map(item => ({ + name: item.name.trim(), + description: item.description.trim(), + weight: item.weight, + maxScore: item.maxScore + })) + } + + // 單一模板模式,總是使用 POST 進行覆蓋 + const url = '/api/criteria-templates' + const method = 'POST' + + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(templateData), + }) + + const result = await response.json() + + if (result.success) { + toast({ + title: "儲存成功", + description: "評分標準已成功儲存", + }) + + // 重新載入資料 + await loadTemplate() + } else { + throw new Error(result.error || '儲存失敗') + } + } catch (error) { + console.error('儲存評分標準失敗:', error) + toast({ + title: "儲存失敗", + description: error instanceof Error ? error.message : "無法儲存評分標準", + variant: "destructive", + }) + } finally { + setIsSaving(false) + } } - const resetToDefault = () => { - setCriteria(defaultCriteria) - setTemplateName("預設評分標準") - toast({ - title: "已重置", - description: "評分標準已重置為預設值", - }) + const resetToDefault = async () => { + try { + await loadDefaultTemplate() + toast({ + title: "已重置", + description: "評分標準已重置為預設值", + }) + } catch (error) { + toast({ + title: "重置失敗", + description: "無法重置評分標準", + variant: "destructive", + }) + } + } + + if (isLoading) { + return ( +
+ +
+
+
+
+ +

載入評分標準中...

+
+
+
+
+
+ ) } return ( @@ -140,16 +251,29 @@ export default function CriteriaPage() { - 標準模板名稱 + 標準模板資訊 - - setTemplateName(e.target.value)} - placeholder="輸入評分標準名稱" - className="max-w-md" - /> + +
+ + setTemplateName(e.target.value)} + placeholder="輸入評分標準名稱" + /> +
+
+ +