diff options
Diffstat (limited to 'lib/evaluation-submit/table')
3 files changed, 1190 insertions, 0 deletions
diff --git a/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx b/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx new file mode 100644 index 00000000..1ec0284f --- /dev/null +++ b/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx @@ -0,0 +1,556 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { + Ellipsis, + InfoIcon, + PenToolIcon, + FileTextIcon, + ClipboardListIcon, + CheckIcon, + XIcon, + ClockIcon, + Send, + User, + Calendar +} from "lucide-react" + +import { formatDate, formatCurrency } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Badge } from "@/components/ui/badge" +import { useRouter } from "next/navigation" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { ReviewerEvaluationView } from "@/db/schema" + + +type NextRouter = ReturnType<typeof useRouter>; + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ReviewerEvaluationView> | null>> + router: NextRouter; + +} + +/** + * 평가 진행 상태에 따른 배지 스타일 + */ +const getProgressBadge = (isCompleted: boolean, completedAt: Date | null) => { + if (isCompleted && completedAt) { + return { + variant: "default" as const, + icon: <CheckIcon className="h-3 w-3" />, + label: "완료", + className: "bg-green-100 text-green-800 border-green-200" + } + } else { + return { + variant: "secondary" as const, + icon: <ClockIcon className="h-3 w-3" />, + label: "미완료" + } + } +} + +/** + * 정기평가 상태에 따른 배지 스타일 + */ +const getPeriodicStatusBadge = (status: string) => { + switch (status) { + case 'PENDING': + return { + variant: "secondary" as const, + icon: <ClockIcon className="h-3 w-3" />, + label: "대기중" + } + + case 'PENDING_SUBMISSION': + return { + variant: "secondary" as const, + icon: <ClockIcon className="h-3 w-3" />, + label: "업체 제출 대기중" + } + case 'IN_PROGRESS': + return { + variant: "default" as const, + icon: <PenToolIcon className="h-3 w-3" />, + label: "진행중" + } + case 'REVIEW': + return { + variant: "outline" as const, + icon: <ClipboardListIcon className="h-3 w-3" />, + label: "검토중" + } + case 'COMPLETED': + return { + variant: "default" as const, + icon: <CheckIcon className="h-3 w-3" />, + label: "완료", + className: "bg-green-100 text-green-800 border-green-200" + } + default: + return { + variant: "secondary" as const, + icon: null, + label: status + } + } +} + +/** + * 평가 제출 테이블 컬럼 정의 + */ +export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef<ReviewerEvaluationView>[] { + + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef<ReviewerEvaluationView> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + } + + // ---------------------------------------------------------------- + // 2) 기본 정보 컬럼들 + // ---------------------------------------------------------------- + const basicColumns: ColumnDef<ReviewerEvaluationView>[] = [ + { + accessorKey: "evaluationYear", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="평가연도" /> + ), + cell: ({ row }) => ( + <Badge variant="outline"> + {row.getValue("evaluationYear")}년 + </Badge> + ), + size: 80, + }, + + { + id: "vendorInfo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="협력업체" /> + ), + cell: ({ row }) => { + const vendorName = row.original.vendorName; + const vendorCode = row.original.vendorCode; + const domesticForeign = row.original.domesticForeign; + + return ( + <div className="space-y-1"> + <div className="font-medium">{vendorName}</div> + <div className="text-sm text-muted-foreground"> + {vendorCode} • {domesticForeign === 'DOMESTIC' ? 'D' : 'F'} + </div> + </div> + ); + }, + enableSorting: false, + size: 200, + }, + + + { + id: "materialType", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재구분" /> + ), + cell: ({ row }) => { + const materialType = row.original.materialType; + const material = materialType ==="BULK" ? "벌크": materialType ==="EQUIPMENT" ? "기자재" :"기자재/벌크" + + return ( + <div className="space-y-1"> + <div className="font-medium">{material}</div> + + </div> + ); + }, + enableSorting: false, + }, + + { + id: "division", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="division" /> + ), + cell: ({ row }) => { + const division = row.original.division; + const divisionKR = division === "PLANT"?"해양":"조선"; + + return ( + <div className="space-y-1"> + <div className="font-medium">{divisionKR}</div> + + </div> + ); + }, + enableSorting: false, + }, + ] + + // ---------------------------------------------------------------- + // 3) 상태 정보 컬럼들 + // ---------------------------------------------------------------- + const statusColumns: ColumnDef<ReviewerEvaluationView>[] = [ + { + id: "evaluationProgress", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="평가 진행상태" /> + ), + cell: ({ row }) => { + const isCompleted = row.original.isCompleted; + const completedAt = row.original.completedAt; + const badgeInfo = getProgressBadge(isCompleted, completedAt); + + return ( + <div className="space-y-1"> + <Badge + variant={badgeInfo.variant} + className={`flex items-center gap-1 ${badgeInfo.className || ''}`} + > + {badgeInfo.icon} + {badgeInfo.label} + </Badge> + {completedAt && ( + <div className="text-xs text-muted-foreground"> + {formatDate(completedAt,"KR")} + </div> + )} + </div> + ); + }, + size: 130, + }, + + { + id: "periodicStatus", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="정기평가 상태" /> + ), + cell: ({ row }) => { + const status = row.original.periodicStatus; + const badgeInfo = getPeriodicStatusBadge(status); + + return ( + <Badge + variant={badgeInfo.variant} + className={`flex items-center gap-1 ${badgeInfo.className || ''}`} + > + {badgeInfo.icon} + {badgeInfo.label} + </Badge> + ); + }, + size: 120, + }, + + // { + // id: "submissionInfo", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="제출정보" /> + // ), + // cell: ({ row }) => { + // // const submissionDate = row.original.submittedAt; + // const completedAt = row.original.completedAt; + + // return ( + // <div className="space-y-1"> + // <div className="flex items-center gap-1"> + // <Badge variant={submissionDate ? "default" : "secondary"}> + // {submissionDate ? "제출완료" : "미제출"} + // </Badge> + // </div> + + // {completedAt && ( + // <div className="text-xs text-muted-foreground"> + // 평가완료: {formatDate(completedAt, "KR")} + // </div> + // )} + // {/* {submissionDate && ( + // <div className="text-xs text-muted-foreground"> + // 제출: {formatDate(submissionDate, "KR")} + // </div> + // )} */} + + // </div> + // ); + // }, + // enableSorting: false, + // size: 140, + // }, + ] + + // ---------------------------------------------------------------- + // 4) 점수 및 평가 정보 컬럼들 + // ---------------------------------------------------------------- + const scoreColumns: ColumnDef<ReviewerEvaluationView>[] = [ + { + id: "periodicScores", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="정기평가 점수" /> + ), + cell: ({ row }) => { + const finalScore = row.original.periodicFinalScore; + const finalGrade = row.original.periodicFinalGrade; + const evaluationScore = row.original.periodicEvaluationScore; + const evaluationGrade = row.original.periodicEvaluationGrade; + + return ( + <div className="text-center space-y-1"> + {finalScore && finalGrade ? ( + <div className="space-y-1"> + <div className="font-medium text-blue-600"> + 최종: {parseFloat(finalScore.toString()).toFixed(1)}점 + </div> + <Badge variant="outline">{finalGrade}</Badge> + </div> + ) : evaluationScore && evaluationGrade ? ( + <div className="space-y-1"> + <div className="font-medium"> + {parseFloat(evaluationScore.toString()).toFixed(1)}점 + </div> + <Badge variant="outline">{evaluationGrade}</Badge> + </div> + ) : ( + <span className="text-muted-foreground">미산정</span> + )} + </div> + ); + }, + enableSorting: false, + size: 120, + }, + + ] + + + + // ---------------------------------------------------------------- + // 6) 메타데이터 컬럼들 + // ---------------------------------------------------------------- + const metaColumns: ColumnDef<ReviewerEvaluationView>[] = [ + { + accessorKey: "reviewerEvaluationCreatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="생성일" /> + ), + cell: ({ row }) => { + const date = row.getValue("reviewerEvaluationCreatedAt") as Date; + return formatDate(date); + }, + size: 140, + }, + { + accessorKey: "reviewerEvaluationUpdatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="수정일" /> + ), + cell: ({ row }) => { + const date = row.getValue("reviewerEvaluationUpdatedAt") as Date; + return formatDate(date); + }, + size: 140, + }, + ] + + // ---------------------------------------------------------------- + // 7) actions 컬럼 (드롭다운 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef<ReviewerEvaluationView> = { + id: "actions", + header: "작업", + enableHiding: false, + cell: function Cell({ row }) { + const isCompleted = row.original.isCompleted; + const reviewerEvaluationId = row.original.reviewerEvaluationId; + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" size="icon"> + <Ellipsis className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem + onClick={() => router.push(`/evcp/evaluation-input/${reviewerEvaluationId}`)} + > + {isCompleted ? "완료된 평가보기":"평가 작성하기"} + </DropdownMenuItem> + + </DropdownMenuContent> + </DropdownMenu> + ) + }, + size: 80, + } + + // ---------------------------------------------------------------- + // 8) 최종 컬럼 배열 + // ---------------------------------------------------------------- + return [ + selectColumn, + ...basicColumns, + { + id: "statusInfo", + header: "상태 정보", + columns: statusColumns, + }, + { + id: "scoreInfo", + header: "점수 및 평가", + columns: scoreColumns, + }, + + { + id: "metadata", + header: "메타데이터", + columns: metaColumns, + }, + actionsColumn, + ] +} + +// ---------------------------------------------------------------- +// 9) 컬럼 설정 (필터링용) +// ---------------------------------------------------------------- +export const evaluationSubmissionsColumnsConfig = [ + { + id: "reviewerEvaluationId", + label: "평가 ID", + group: "기본 정보", + type: "text", + excelHeader: "Evaluation ID", + }, + { + id: "vendorName", + label: "협력업체명", + group: "기본 정보", + type: "text", + excelHeader: "Vendor Name", + }, + { + id: "vendorCode", + label: "협력업체 코드", + group: "기본 정보", + type: "text", + excelHeader: "Vendor Code", + }, + { + id: "evaluationYear", + label: "평가연도", + group: "기본 정보", + type: "number", + excelHeader: "Evaluation Year", + }, + { + id: "departmentCode", + label: "부서코드", + group: "기본 정보", + type: "text", + excelHeader: "Department Code", + }, + { + id: "isCompleted", + label: "완료 여부", + group: "상태 정보", + type: "select", + options: [ + { label: "완료", value: "true" }, + { label: "미완료", value: "false" }, + ], + excelHeader: "Is Completed", + }, + { + id: "periodicStatus", + label: "정기평가 상태", + group: "상태 정보", + type: "select", + options: [ + { label: "대기중", value: "PENDING" }, + { label: "진행중", value: "IN_PROGRESS" }, + { label: "검토중", value: "REVIEW" }, + { label: "완료", value: "COMPLETED" }, + ], + excelHeader: "Periodic Status", + }, + { + id: "documentsSubmitted", + label: "문서 제출여부", + group: "상태 정보", + type: "select", + options: [ + { label: "제출완료", value: "true" }, + { label: "미제출", value: "false" }, + ], + excelHeader: "Documents Submitted", + }, + { + id: "periodicFinalScore", + label: "최종점수", + group: "점수 정보", + type: "number", + excelHeader: "Final Score", + }, + { + id: "periodicFinalGrade", + label: "최종등급", + group: "점수 정보", + type: "text", + excelHeader: "Final Grade", + }, + { + id: "reviewerEvaluationCreatedAt", + label: "생성일", + group: "메타데이터", + type: "date", + excelHeader: "Created At", + }, + { + id: "reviewerEvaluationUpdatedAt", + label: "수정일", + group: "메타데이터", + type: "date", + excelHeader: "Updated At", + }, +] as const;
\ No newline at end of file diff --git a/lib/evaluation-submit/table/evaluation-submit-dialog.tsx b/lib/evaluation-submit/table/evaluation-submit-dialog.tsx new file mode 100644 index 00000000..20ed5f30 --- /dev/null +++ b/lib/evaluation-submit/table/evaluation-submit-dialog.tsx @@ -0,0 +1,353 @@ +"use client" + +import * as React from "react" +import { + AlertTriangleIcon, + CheckCircleIcon, + SendIcon, + XCircleIcon, + FileTextIcon, + ClipboardListIcon, + LoaderIcon +} from "lucide-react" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/components/ui/alert" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { toast } from "sonner" + +// Progress 컴포넌트 (간단한 구현) +function Progress({ value, className }: { value: number; className?: string }) { + return ( + <div className={`w-full bg-gray-200 rounded-full overflow-hidden ${className}`}> + <div + className={`h-full bg-blue-600 transition-all duration-300 ${ + value === 100 ? 'bg-green-500' : value >= 50 ? 'bg-blue-500' : 'bg-yellow-500' + }`} + style={{ width: `${Math.min(100, Math.max(0, value))}%` }} + /> + </div> + ) +} + +import { + getEvaluationSubmissionCompleteness, + updateEvaluationSubmissionStatus +} from "../service" +import type { EvaluationSubmissionWithVendor } from "../service" + +interface EvaluationSubmissionDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + submission: EvaluationSubmissionWithVendor | null + onSuccess: () => void +} + +type CompletenessData = { + general: { + total: number + completed: number + percentage: number + isComplete: boolean + } + esg: { + total: number + completed: number + percentage: number + averageScore: number + isComplete: boolean + } + overall: { + isComplete: boolean + totalItems: number + completedItems: number + } +} + +export function EvaluationSubmissionDialog({ + open, + onOpenChange, + submission, + onSuccess, +}: EvaluationSubmissionDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [isSubmitting, setIsSubmitting] = React.useState(false) + const [completeness, setCompleteness] = React.useState<CompletenessData | null>(null) + + // 완성도 데이터 로딩 + React.useEffect(() => { + if (open && submission?.id) { + loadCompleteness() + } + }, [open, submission?.id]) + + const loadCompleteness = async () => { + if (!submission?.id) return + + setIsLoading(true) + try { + const data = await getEvaluationSubmissionCompleteness(submission.id) + setCompleteness(data) + } catch (error) { + console.error('Error loading completeness:', error) + toast.error('완성도 정보를 불러오는데 실패했습니다.') + } finally { + setIsLoading(false) + } + } + + // 제출하기 + const handleSubmit = async () => { + if (!submission?.id || !completeness) return + + if (!completeness.overall.isComplete) { + toast.error('모든 평가 항목을 완료해야 제출할 수 있습니다.') + return + } + + setIsSubmitting(true) + try { + await updateEvaluationSubmissionStatus(submission.id, 'submitted') + toast.success('평가가 성공적으로 제출되었습니다.') + onSuccess() + } catch (error: any) { + console.error('Error submitting evaluation:', error) + toast.error(error.message || '제출에 실패했습니다.') + } finally { + setIsSubmitting(false) + } + } + + const isKorean = submission?.vendor.countryCode === 'KR' + + if (isLoading) { + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[500px]"> + <div className="flex items-center justify-center py-8"> + <div className="text-center space-y-4"> + <LoaderIcon className="h-8 w-8 animate-spin mx-auto" /> + <p>완성도를 확인하는 중...</p> + </div> + </div> + </DialogContent> + </Dialog> + ) + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[600px]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <SendIcon className="h-5 w-5" /> + 평가 제출하기 + </DialogTitle> + <DialogDescription> + {submission?.vendor.vendorName}의 {submission?.evaluationYear}년 평가를 제출합니다. + </DialogDescription> + </DialogHeader> + + {completeness && ( + <div className="space-y-6"> + {/* 전체 완성도 카드 */} + <Card> + <CardHeader> + <CardTitle className="text-base flex items-center justify-between"> + <span>전체 완성도</span> + <Badge + variant={completeness.overall.isComplete ? "default" : "secondary"} + className={ + completeness.overall.isComplete + ? "bg-green-100 text-green-800 border-green-200" + : "" + } + > + {completeness.overall.isComplete ? "완료" : "미완료"} + </Badge> + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="space-y-2"> + <div className="flex items-center justify-between text-sm"> + <span>전체 진행률</span> + <span className="font-medium"> + {completeness.overall.completedItems}/{completeness.overall.totalItems}개 완료 + </span> + </div> + <Progress + value={ + completeness.overall.totalItems > 0 + ? (completeness.overall.completedItems / completeness.overall.totalItems) * 100 + : 0 + } + className="h-2" + /> + <p className="text-xs text-muted-foreground"> + {completeness.overall.totalItems > 0 + ? Math.round((completeness.overall.completedItems / completeness.overall.totalItems) * 100) + : 0}% 완료 + </p> + </div> + </CardContent> + </Card> + + {/* 세부 완성도 */} + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + {/* 일반평가 */} + <Card> + <CardHeader className="pb-3"> + <CardTitle className="text-sm flex items-center gap-2"> + <FileTextIcon className="h-4 w-4" /> + 일반평가 + {completeness.general.isComplete ? ( + <CheckCircleIcon className="h-4 w-4 text-green-600" /> + ) : ( + <XCircleIcon className="h-4 w-4 text-red-600" /> + )} + </CardTitle> + </CardHeader> + <CardContent className="space-y-3"> + <div className="space-y-1"> + <div className="flex items-center justify-between text-xs"> + <span>응답 완료</span> + <span className="font-medium"> + {completeness.general.completed}/{completeness.general.total}개 + </span> + </div> + <Progress value={completeness.general.percentage} className="h-1" /> + <p className="text-xs text-muted-foreground"> + {completeness.general.percentage.toFixed(0)}% 완료 + </p> + </div> + + {!completeness.general.isComplete && ( + <p className="text-xs text-red-600"> + {completeness.general.total - completeness.general.completed}개 항목이 미완료입니다. + </p> + )} + </CardContent> + </Card> + + {/* ESG평가 */} + {isKorean ? ( + <Card> + <CardHeader className="pb-3"> + <CardTitle className="text-sm flex items-center gap-2"> + <ClipboardListIcon className="h-4 w-4" /> + ESG평가 + {completeness.esg.isComplete ? ( + <CheckCircleIcon className="h-4 w-4 text-green-600" /> + ) : ( + <XCircleIcon className="h-4 w-4 text-red-600" /> + )} + </CardTitle> + </CardHeader> + <CardContent className="space-y-3"> + <div className="space-y-1"> + <div className="flex items-center justify-between text-xs"> + <span>응답 완료</span> + <span className="font-medium"> + {completeness.esg.completed}/{completeness.esg.total}개 + </span> + </div> + <Progress value={completeness.esg.percentage} className="h-1" /> + <p className="text-xs text-muted-foreground"> + {completeness.esg.percentage.toFixed(0)}% 완료 + </p> + </div> + + {completeness.esg.completed > 0 && ( + <div className="text-xs"> + <span className="text-muted-foreground">평균 점수: </span> + <span className="font-medium text-blue-600"> + {completeness.esg.averageScore.toFixed(1)}점 + </span> + </div> + )} + + {!completeness.esg.isComplete && ( + <p className="text-xs text-red-600"> + {completeness.esg.total - completeness.esg.completed}개 항목이 미완료입니다. + </p> + )} + </CardContent> + </Card> + ) : ( + <Card> + <CardHeader className="pb-3"> + <CardTitle className="text-sm flex items-center gap-2"> + <ClipboardListIcon className="h-4 w-4" /> + ESG평가 + </CardTitle> + </CardHeader> + <CardContent> + <div className="text-center text-muted-foreground"> + <Badge variant="outline">해당없음</Badge> + <p className="text-xs mt-2">한국 업체가 아니므로 ESG 평가가 제외됩니다.</p> + </div> + </CardContent> + </Card> + )} + </div> + + {/* 제출 상태 알림 */} + {completeness.overall.isComplete ? ( + <Alert> + <CheckCircleIcon className="h-4 w-4" /> + <AlertTitle>제출 준비 완료</AlertTitle> + <AlertDescription> + 모든 평가 항목이 완료되었습니다. 제출하시겠습니까? + </AlertDescription> + </Alert> + ) : ( + <Alert variant="destructive"> + <AlertTriangleIcon className="h-4 w-4" /> + <AlertTitle>제출 불가</AlertTitle> + <AlertDescription> + 아직 완료되지 않은 평가 항목이 있습니다. 모든 항목을 완료한 후 제출해 주세요. + </AlertDescription> + </Alert> + )} + </div> + )} + + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 취소 + </Button> + <Button + onClick={handleSubmit} + disabled={!completeness?.overall.isComplete || isSubmitting} + className="min-w-[100px]" + > + {isSubmitting ? ( + <> + <LoaderIcon className="mr-2 h-4 w-4 animate-spin" /> + 제출 중... + </> + ) : ( + <> + <SendIcon className="mr-2 h-4 w-4" /> + 제출하기 + </> + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file diff --git a/lib/evaluation-submit/table/submit-table.tsx b/lib/evaluation-submit/table/submit-table.tsx new file mode 100644 index 00000000..9000c48b --- /dev/null +++ b/lib/evaluation-submit/table/submit-table.tsx @@ -0,0 +1,281 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" + +import { getSHIEvaluationSubmissions } from "../service" +import { getColumns } from "./evaluation-submissions-table-columns" +import { useRouter } from "next/navigation" +import { ReviewerEvaluationView } from "@/db/schema" + +interface EvaluationSubmissionsTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getSHIEvaluationSubmissions>>, + ] + > +} + +export function SHIEvaluationSubmissionsTable({ promises }: EvaluationSubmissionsTableProps) { + // 1. 데이터 로딩 상태 관리 + const [isLoading, setIsLoading] = React.useState(true) + const [tableData, setTableData] = React.useState<{ + data: ReviewerEvaluationView[] + pageCount: number + }>({ data: [], pageCount: 0 }) + const router = useRouter() + + console.log(tableData) + + + // 2. 행 액션 상태 관리 + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<ReviewerEvaluationView> | null>(null) + + // 3. Promise 해결을 useEffect로 처리 + React.useEffect(() => { + promises + .then(([result]) => { + setTableData(result) + setIsLoading(false) + }) + // .catch((error) => { + // console.error('Failed to load evaluation submissions:', error) + // setIsLoading(false) + // }) + }, [promises]) + + // 4. 컬럼 정의 + const columns = React.useMemo( + () => getColumns({ setRowAction , router}), + [setRowAction, router] + ) + + // 5. 필터 필드 정의 + const filterFields: DataTableFilterField<ReviewerEvaluationView>[] = [ + { + id: "isCompleted", + label: "완료상태", + placeholder: "완료상태 선택...", + }, + { + id: "periodicStatus", + label: "정기평가 상태", + placeholder: "상태 선택...", + }, + { + id: "evaluationYear", + label: "평가연도", + placeholder: "연도 선택...", + }, + { + id: "departmentCode", + label: "담당부서", + placeholder: "부서 선택...", + }, + ] + + const advancedFilterFields: DataTableAdvancedFilterField<ReviewerEvaluationView>[] = [ + { + id: "reviewerEvaluationId", + label: "평가 ID", + type: "text", + }, + { + id: "vendorName", + label: "협력업체명", + type: "text", + }, + { + id: "vendorCode", + label: "협력업체 코드", + type: "text", + }, + { + id: "evaluationYear", + label: "평가연도", + type: "number", + }, + { + id: "departmentCode", + label: "부서코드", + type: "select", + options: [ + { label: "구매평가", value: "ORDER_EVAL" }, + { label: "조달평가", value: "PROCUREMENT_EVAL" }, + { label: "품질평가", value: "QUALITY_EVAL" }, + { label: "CS평가", value: "CS_EVAL" }, + { label: "관리자", value: "ADMIN_EVAL" }, + ], + }, + { + id: "division", + label: "사업부", + type: "select", + options: [ + { label: "조선", value: "SHIP" }, + { label: "플랜트", value: "PLANT" }, + ], + }, + { + id: "materialType", + label: "자재유형", + type: "select", + options: [ + { label: "장비", value: "EQUIPMENT" }, + { label: "벌크", value: "BULK" }, + { label: "장비+벌크", value: "EQUIPMENT_BULK" }, + ], + }, + { + id: "domesticForeign", + label: "국내/해외", + type: "select", + options: [ + { label: "국내", value: "DOMESTIC" }, + { label: "해외", value: "FOREIGN" }, + ], + }, + { + id: "isCompleted", + label: "평가완료 여부", + type: "select", + options: [ + { label: "완료", value: "true" }, + { label: "미완료", value: "false" }, + ], + }, + { + id: "periodicStatus", + label: "정기평가 상태", + type: "select", + options: [ + { label: "대기중", value: "PENDING" }, + { label: "진행중", value: "IN_PROGRESS" }, + { label: "검토중", value: "REVIEW" }, + { label: "완료", value: "COMPLETED" }, + ], + }, + { + id: "documentsSubmitted", + label: "문서 제출여부", + type: "select", + options: [ + { label: "제출완료", value: "true" }, + { label: "미제출", value: "false" }, + ], + }, + { + id: "periodicFinalScore", + label: "최종점수", + type: "number", + }, + { + id: "periodicFinalGrade", + label: "최종등급", + type: "text", + }, + { + id: "ldClaimCount", + label: "LD 클레임 건수", + type: "number", + }, + { + id: "submissionDate", + label: "제출일", + type: "date", + }, + { + id: "submissionDeadline", + label: "제출마감일", + type: "date", + }, + { + id: "completedAt", + label: "완료일시", + type: "date", + }, + { + id: "reviewerEvaluationCreatedAt", + label: "생성일", + type: "date", + }, + { + id: "reviewerEvaluationUpdatedAt", + label: "수정일", + type: "date", + }, + ] + + // 6. 데이터 테이블 설정 + const { table } = useDataTable({ + data: tableData.data, + columns, + pageCount: tableData.pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "reviewerEvaluationUpdatedAt", desc: true }], + columnPinning: { left: ["select"], right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.reviewerEvaluationId), + shallow: false, + clearOnDefault: true, + }) + + // 7. 데이터 새로고침 함수 + const handleRefresh = React.useCallback(() => { + setIsLoading(true) + router.refresh() + }, [router]) + + // 8. 각종 성공 핸들러 + const handleActionSuccess = React.useCallback(() => { + setRowAction(null) + table.resetRowSelection() + handleRefresh() + }, [handleRefresh, table]) + + // 9. 로딩 상태 표시 + if (isLoading) { + return ( + <div className="flex items-center justify-center h-32"> + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div> + <span className="ml-2">평가 제출 목록을 불러오는 중...</span> + </div> + ) + } + + return ( + <> + {/* 메인 테이블 */} + <DataTable table={table}> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + {/* 추가 툴바 버튼들이 필요하면 여기에 */} + </DataTableAdvancedToolbar> + </DataTable> + + {/* 행 액션 모달들 - 필요에 따라 구현 */} + {/* {rowAction?.type === "view_detail" && ( + <EvaluationDetailDialog + row={rowAction.row} + onClose={() => setRowAction(null)} + onSuccess={handleActionSuccess} + /> + )} */} + </> + ) +}
\ No newline at end of file |
