"use client" import * as React from "react" import { useForm, useWatch } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { SaveIcon, CheckIcon, XIcon, BarChart3Icon, TrendingUpIcon, DownloadIcon } from "lucide-react" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Separator } from "@/components/ui/separator" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { toast } from "sonner" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion" import ExcelJS from "exceljs" import { getEsgEvaluationFormData, saveEsgEvaluationResponse, recalculateEvaluationProgress, EsgEvaluationFormData } from "../service" import { EvaluationSubmissionWithVendor } from "../service" interface EsgEvaluationFormSheetProps { open: boolean onOpenChange: (open: boolean) => void submission: EvaluationSubmissionWithVendor | null onSuccess: () => void } // 폼 스키마 정의 const formSchema = z.object({ responses: z.array(z.object({ itemId: z.number(), selectedOptionId: z.number().optional(), selectedScore: z.number().default(0), additionalComments: z.string().optional(), })) }) type FormData = z.infer export function EsgEvaluationFormSheet({ open, onOpenChange, submission, onSuccess, }: EsgEvaluationFormSheetProps) { const [isLoading, setIsLoading] = React.useState(false) const [isSaving, setIsSaving] = React.useState(false) const [formData, setFormData] = React.useState(null) const [currentScores, setCurrentScores] = React.useState>({}) const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { responses: [] } }) // 현재 폼 값을 실시간으로 감시 const watchedResponses = useWatch({ control: form.control, name: 'responses' }) // 데이터 로딩 React.useEffect(() => { if (open && submission?.id) { loadFormData() } }, [open, submission?.id]) const loadFormData = async () => { if (!submission?.id) return setIsLoading(true) try { const data = await getEsgEvaluationFormData(submission.id) setFormData(data) // 폼 초기값 설정 const responses: any[] = [] const scores: Record = {} data.evaluations.forEach(evaluation => { evaluation.items.forEach(item => { responses.push({ itemId: item.item.id, selectedOptionId: item.response?.esgAnswerOptionId, selectedScore: item.response?.selectedScore || 0, additionalComments: item.response?.additionalComments || '', }) if (item.response?.selectedScore) { scores[item.item.id] = item.response.selectedScore } }) }) setCurrentScores(scores) form.reset({ responses }) } catch (error) { console.error('Error loading ESG form data:', error) toast.error('ESG 평가 데이터를 불러오는데 실패했습니다.') } finally { setIsLoading(false) } } // 개별 응답 저장 const handleSaveResponse = async (itemId: number, optionId: number, score: number) => { if (!submission?.id) return try { const formResponse = form.getValues('responses').find(r => r.itemId === itemId) await saveEsgEvaluationResponse({ submissionId: submission.id, esgEvaluationItemId: itemId, esgAnswerOptionId: optionId, selectedScore: score, additionalComments: formResponse?.additionalComments || '', }) // 현재 점수 업데이트 setCurrentScores(prev => ({ ...prev, [itemId]: score })) // 평균 점수 재계산 await recalculateEvaluationProgress(submission.id) toast.success('응답이 저장되었습니다.') } catch (error) { console.error('Error saving ESG response:', error) toast.error('응답 저장에 실패했습니다.') } } // 선택 변경 핸들러 const handleOptionChange = (itemId: number, optionId: string, score: number) => { const responseIndex = form.getValues('responses').findIndex(r => r.itemId === itemId) if (responseIndex >= 0) { form.setValue(`responses.${responseIndex}.selectedOptionId`, parseInt(optionId)) form.setValue(`responses.${responseIndex}.selectedScore`, score) } // 자동 저장 handleSaveResponse(itemId, parseInt(optionId), score) } // 전체 저장 const onSubmit = async (data: FormData) => { if (!submission?.id || !formData) return setIsSaving(true) try { // 모든 응답을 순차적으로 저장 for (const response of data.responses) { if (response.selectedOptionId && response.selectedScore > 0) { await saveEsgEvaluationResponse({ submissionId: submission.id, esgEvaluationItemId: response.itemId, esgAnswerOptionId: response.selectedOptionId, selectedScore: response.selectedScore, additionalComments: response.additionalComments || '', }) } } // 평균 점수 재계산 await recalculateEvaluationProgress(submission.id) toast.success('모든 ESG 자가평가서가 저장되었습니다.') onSuccess() } catch (error) { console.error('Error saving all ESG responses:', error) toast.error('ESG 자가평가서 저장에 실패했습니다.') } finally { setIsSaving(false) } } // ESG 평가 데이터 내보내기 const handleExportData = async () => { if (!formData) return // 워크북 및 워크시트 생성 const workbook = new ExcelJS.Workbook() const worksheet = workbook.addWorksheet('ESG평가') // 헤더 정의 const headers = ['카테고리', '점검항목', '평가항목', '평가항목설명', '답변옵션', '옵션점수'] // 데이터 준비 const data: any[] = [] let lastKey = '' formData.evaluations.forEach(evaluation => { evaluation.items.forEach(item => { item.answerOptions.forEach(option => { // A-D열의 중복 체크를 위한 키 생성 const currentKey = `${evaluation.evaluation.category}|${evaluation.evaluation.inspectionItem}|${item.item.evaluationItem}|${item.item.evaluationItemDescription}` // 중복된 경우 A-D열을 빈 값으로 설정 const row = lastKey === currentKey ? ['', '', '', '', option.answerText, option.score] : [ evaluation.evaluation.category, evaluation.evaluation.inspectionItem, item.item.evaluationItem, item.item.evaluationItemDescription || '', option.answerText, option.score ] data.push(row) lastKey = currentKey }) }) }) // 헤더 추가 및 스타일 적용 worksheet.addRow(headers) const headerRow = worksheet.getRow(1) headerRow.eachCell((cell) => { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'D3D3D3' } // 회색 배경 } cell.font = { bold: true } cell.alignment = { vertical: 'middle', horizontal: 'center' } }) // 데이터 행 추가 data.forEach((row: any) => { worksheet.addRow(row) }) // 컬럼 너비 자동 조정 worksheet.columns.forEach((column, colIndex) => { let maxWidth = headers[colIndex].length data.forEach((row: any) => { const cellValue = row[colIndex] ? String(row[colIndex]) : '' maxWidth = Math.max(maxWidth, cellValue.length) }) column.width = maxWidth + 5 // 여유 공간 추가 }) // 파일 생성 및 다운로드 const buffer = await workbook.xlsx.writeBuffer() const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) const link = document.createElement('a') const url = URL.createObjectURL(blob) link.setAttribute('href', url) link.setAttribute('download', `ESG평가문항_${formData.submission.vendorName}_${new Date().toISOString().split('T')[0]}.xlsx`) link.style.visibility = 'hidden' document.body.appendChild(link) link.click() document.body.removeChild(link) toast.success('ESG 자가평가서 문항이 다운로드되었습니다.') } // 진행률 및 점수 계산 const getProgress = () => { if (!formData) return { completed: 0, total: 0, percentage: 0, averageScore: 0, maxAverageScore: 0, totalPossibleScore: 0, actualTotalScore: 0 } let total = 0 let completed = 0 let totalScore = 0 // 숫자로 초기화 let maxTotalScore = 0 formData.evaluations.forEach(evaluation => { evaluation.items.forEach(item => { total++ // 최대 점수 계산 (모든 항목에 대해) const maxOptionScore = Math.max(...item.answerOptions.map(opt => parseFloat(opt.score.toString()))) maxTotalScore += maxOptionScore // 응답이 있는 경우에만 완료된 것으로 계산 const currentScore = currentScores[item.item.id] if (currentScore !== undefined && currentScore >= 0) { completed++ // 숫자로 명시적 변환하여 더하기 totalScore += Number(currentScore) || 0 console.log(`Adding score: ${Number(currentScore)}, Total so far: ${totalScore}`) } }) }) const percentage = total > 0 ? Math.round((completed / total) * 100) : 0 // 응답한 항목들에 대해서만 평균 계산 (0으로 나누기 방지) const averageScore = completed > 0 ? totalScore / completed : 0 // 전체 항목 기준 최대 평균 점수 const maxAverageScore = total > 0 ? maxTotalScore / total : 0 return { completed, total, percentage, averageScore, maxAverageScore, totalPossibleScore: maxTotalScore, actualTotalScore: totalScore } } const progress = getProgress() if (isLoading) { return (

ESG 자가평가서 데이터를 불러오는 중...

) } return (
ESG 자가평가서 작성 {formData?.submission.vendorName}의 ESG 자가평가서를 작성해주세요.
우측의 "내보내기" 버튼을 클릭하시면 전체 질문을 엑셀로 다운로드 받아 답변 작성에 참고할 수 있습니다.
{formData && ( <> {/* 진행률 및 점수 표시 */}
진행률
{progress.completed}/{progress.total}

{progress.percentage}% 완료

평균 점수
{progress.averageScore.toFixed(1)} / {progress.maxAverageScore.toFixed(1)}
0 ? (progress.averageScore / progress.maxAverageScore) * 100 : 0}%` }} />

{progress.completed > 0 ? `${progress.completed}개 항목 평균` : '응답 없음'}

{/* 스크롤 가능한 폼 영역 */}
`evaluation-${i}`)}> {formData.evaluations.map((evaluation, evalIndex) => (
{evaluation.evaluation.serialNumber}
{evaluation.evaluation.category}
{evaluation.evaluation.inspectionItem}
{evaluation.items.filter(item => currentScores[item.item.id] >= 0 ).length}/{evaluation.items.length}
{evaluation.items.map((item, itemIndex) => { const responseIndex = form.getValues('responses').findIndex( r => r.itemId === item.item.id ) // watchedResponses에서 현재 응답 찾기 const currentResponse = watchedResponses?.find(r => r.itemId === item.item.id) const selectedOptionId = currentResponse?.selectedOptionId?.toString() || '' return ( {item.item.evaluationItem} {currentScores[item.item.id] > 0 && ( {currentScores[item.item.id]}점 )} {item.item.evaluationItemDescription && (

{item.item.evaluationItemDescription}

)}
{/* 답변 옵션들 */} { const option = item.answerOptions.find( opt => opt.id === parseInt(value) ) if (option) { handleOptionChange( item.item.id, value, parseFloat(option.score.toString()) ) } }} >
{item.answerOptions.map((option) => (
))}
{/* 추가 의견 */} {responseIndex >= 0 && ( ( 추가 의견 (선택사항)