diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-04 09:39:21 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-04 09:39:21 +0000 |
| commit | 53ad72732f781e6c6d5ddb3776ea47aec010af8e (patch) | |
| tree | e676287827f8634be767a674b8ad08b6ed7eb3e6 /lib/pq/pq-review-table-new/vendors-table-columns.tsx | |
| parent | 3e4d15271322397764601dee09441af8a5b3adf5 (diff) | |
(최겸) PQ/실사 수정 및 개발
Diffstat (limited to 'lib/pq/pq-review-table-new/vendors-table-columns.tsx')
| -rw-r--r-- | lib/pq/pq-review-table-new/vendors-table-columns.tsx | 1425 |
1 files changed, 786 insertions, 639 deletions
diff --git a/lib/pq/pq-review-table-new/vendors-table-columns.tsx b/lib/pq/pq-review-table-new/vendors-table-columns.tsx index 6bfa8c7f..d99f201e 100644 --- a/lib/pq/pq-review-table-new/vendors-table-columns.tsx +++ b/lib/pq/pq-review-table-new/vendors-table-columns.tsx @@ -1,640 +1,787 @@ -"use client" - -import * as React from "react" -import { type DataTableRowAction } from "@/types/table" -import { type ColumnDef } from "@tanstack/react-table" -import { Ellipsis, Eye, PaperclipIcon, FileEdit } from "lucide-react" - -import { formatDate } from "@/lib/utils" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Checkbox } from "@/components/ui/checkbox" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header" -import { useRouter } from "next/navigation" - -// PQ 제출 타입 정의 -export interface PQSubmission { - id: number - pqNumber: string - type: string - status: string - requesterName: string | null // 요청자 이름 - createdAt: Date - updatedAt: Date - submittedAt: Date | null - approvedAt: Date | null - rejectedAt: Date | null - rejectReason: string | null - vendorId: number - vendorName: string - vendorCode: string - taxId: string - vendorStatus: string - projectId: number | null - projectName: string | null - projectCode: string | null - answerCount: number - attachmentCount: number - pqStatus: string - pqTypeLabel: string - investigation: { - id: number - investigationStatus: string - requesterName: string | null // 실사 요청자 이름 - evaluationType: "SITE_AUDIT" | "QM_SELF_AUDIT" | null - qmManagerId: number | null - qmManagerName: string | null // QM 담당자 이름 - qmManagerEmail: string | null // QM 담당자 이메일 - investigationAddress: string | null - investigationMethod: string | null - scheduledStartAt: Date | null - scheduledEndAt: Date | null - requestedAt: Date | null - confirmedAt: Date | null - completedAt: Date | null - forecastedAt: Date | null - evaluationScore: number | null - evaluationResult: "APPROVED" | "SUPPLEMENT" | "REJECTED" | null - investigationNotes: string | null - } | null - // 통합 상태를 위한 새 필드 - combinedStatus: { - status: string - label: string - variant: "default" | "outline" | "secondary" | "destructive" | "success" - } -} - -type NextRouter = ReturnType<typeof useRouter>; - -interface GetColumnsProps { - setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<PQSubmission> | null>>; - router: NextRouter; -} - -// 상태에 따른 Badge 변형 결정 함수 -function getStatusBadge(status: string) { - switch (status) { - case "REQUESTED": - return <Badge variant="outline">요청됨</Badge> - case "IN_PROGRESS": - return <Badge variant="secondary">진행 중</Badge> - case "SUBMITTED": - return <Badge>제출됨</Badge> - case "APPROVED": - return <Badge variant="success">승인됨</Badge> - case "REJECTED": - return <Badge variant="destructive">거부됨</Badge> - default: - return <Badge variant="outline">{status}</Badge> - } -} - -/** - * tanstack table 컬럼 정의 - */ -export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef<PQSubmission>[] { - // ---------------------------------------------------------------- - // 1) select 컬럼 (체크박스) - // ---------------------------------------------------------------- - const selectColumn: ColumnDef<PQSubmission> = { - 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" - /> - ), - size: 40, - enableSorting: false, - enableHiding: false, - } - - // ---------------------------------------------------------------- - // 2) 일반 컬럼들 - // -------------------------- - // -------------------------------------- - - const pqNoColumn: ColumnDef<PQSubmission> = { - accessorKey: "pqNumber", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="PQ No." /> - ), - cell: ({ row }) => ( - <div className="flex flex-col"> - <span className="font-medium">{row.getValue("pqNumber")}</span> - </div> - ), - } - - // 협력업체 컬럼 - const vendorColumn: ColumnDef<PQSubmission> = { - accessorKey: "vendorName", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="협력업체" /> - ), - cell: ({ row }) => ( - <div className="flex flex-col"> - <span className="font-medium">{row.getValue("vendorName")}</span> - <span className="text-xs text-muted-foreground">{row.original.vendorCode ? row.original.vendorCode : "-"}/{row.original.taxId}</span> - </div> - ), - } - - // PQ 유형 컬럼 - const typeColumn: ColumnDef<PQSubmission> = { - accessorKey: "type", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="PQ 유형" /> - ), - cell: ({ row }) => { - return ( - <div className="flex items-center"> - <Badge variant={row.original.type === "PROJECT" ? "default" : "outline"}> - {row.original.pqTypeLabel} - </Badge> - </div> - ) - }, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)) - }, - } - - // 프로젝트 컬럼 - const projectColumn: ColumnDef<PQSubmission> = { - accessorKey: "projectName", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="프로젝트" /> - ), - cell: ({ row }) => { - const projectName = row.original.projectName - const projectCode = row.original.projectCode - - if (!projectName) { - return <span className="text-muted-foreground">-</span> - } - - return ( - <div className="flex flex-col"> - <span>{projectName}</span> - {projectCode && ( - <span className="text-xs text-muted-foreground">{projectCode}</span> - )} - </div> - ) - }, - } - - // 상태 컬럼 - const statusColumn: ColumnDef<PQSubmission> = { - accessorKey: "combinedStatus", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="진행현황" /> - ), - cell: ({ row }) => { - const combinedStatus = getCombinedStatus(row.original); - return <Badge variant={combinedStatus.variant}>{combinedStatus.label}</Badge>; - }, - filterFn: (row, id, value) => { - const combinedStatus = getCombinedStatus(row.original); - return value.includes(combinedStatus.status); - }, - }; - - // PQ 상태와 실사 상태를 결합하는 헬퍼 함수 - function getCombinedStatus(submission: PQSubmission) { - // PQ가 승인되지 않은 경우, PQ 상태를 우선 표시 - if (submission.status !== "APPROVED") { - switch (submission.status) { - case "REQUESTED": - return { status: "PQ_REQUESTED", label: "PQ 요청됨", variant: "outline" as const }; - case "IN_PROGRESS": - return { status: "PQ_IN_PROGRESS", label: "PQ 진행 중", variant: "secondary" as const }; - case "SUBMITTED": - return { status: "PQ_SUBMITTED", label: "PQ 제출됨", variant: "default" as const }; - case "REJECTED": - return { status: "PQ_REJECTED", label: "PQ 거부됨", variant: "destructive" as const }; - default: - return { status: submission.status, label: submission.status, variant: "outline" as const }; - } - } - - // PQ가 승인되었지만 실사가 없는 경우 - if (!submission.investigation) { - return { status: "PQ_APPROVED", label: "PQ 승인됨", variant: "success" as const }; - } - - // PQ가 승인되고 실사가 있는 경우 - switch (submission.investigation.investigationStatus) { - case "PLANNED": - return { status: "INVESTIGATION_PLANNED", label: "실사 계획됨", variant: "outline" as const }; - case "IN_PROGRESS": - return { status: "INVESTIGATION_IN_PROGRESS", label: "실사 진행 중", variant: "secondary" as const }; - case "COMPLETED": - // 실사 완료 후 평가 결과에 따라 다른 상태 표시 - if (submission.investigation.evaluationResult) { - switch (submission.investigation.evaluationResult) { - case "APPROVED": - return { status: "INVESTIGATION_APPROVED", label: "실사 승인", variant: "success" as const }; - case "SUPPLEMENT": - return { status: "INVESTIGATION_SUPPLEMENT", label: "실사 보완필요", variant: "secondary" as const }; - case "REJECTED": - return { status: "INVESTIGATION_REJECTED", label: "실사 불가", variant: "destructive" as const }; - default: - return { status: "INVESTIGATION_COMPLETED", label: "실사 완료", variant: "default" as const }; - } - } - return { status: "INVESTIGATION_COMPLETED", label: "실사 완료", variant: "default" as const }; - case "CANCELED": - return { status: "INVESTIGATION_CANCELED", label: "실사 취소됨", variant: "destructive" as const }; - default: - return { - status: `INVESTIGATION_${submission.investigation.investigationStatus}`, - label: `실사 ${submission.investigation.investigationStatus}`, - variant: "outline" as const - }; - } - } - - const evaluationTypeColumn: ColumnDef<PQSubmission> = { - accessorKey: "evaluationType", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="평가 유형" /> - ), - cell: ({ row }) => { - const investigation = row.original.investigation; - - if (!investigation || !investigation.evaluationType) { - return <span className="text-muted-foreground">-</span>; - } - - switch (investigation.evaluationType) { - case "SITE_AUDIT": - return <Badge variant="outline">실사의뢰평가</Badge>; - case "QM_SELF_AUDIT": - return <Badge variant="secondary">QM자체평가</Badge>; - default: - return <span>{investigation.evaluationType}</span>; - } - }, - filterFn: (row, id, value) => { - const investigation = row.original.investigation; - if (!investigation || !investigation.evaluationType) return value.includes("null"); - return value.includes(investigation.evaluationType); - }, - }; - - - const evaluationResultColumn: ColumnDef<PQSubmission> = { - accessorKey: "evaluationResult", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="평가 결과" /> - ), - cell: ({ row }) => { - const investigation = row.original.investigation; - - if (!investigation || !investigation.evaluationResult) { - return <span className="text-muted-foreground">-</span>; - } - - switch (investigation.evaluationResult) { - case "APPROVED": - return <Badge variant="success">승인</Badge>; - case "SUPPLEMENT": - return <Badge variant="secondary">보완</Badge>; - case "REJECTED": - return <Badge variant="destructive">불가</Badge>; - default: - return <span>{investigation.evaluationResult}</span>; - } - }, - filterFn: (row, id, value) => { - const investigation = row.original.investigation; - if (!investigation || !investigation.evaluationResult) return value.includes("null"); - return value.includes(investigation.evaluationResult); - }, - }; - - // 답변 수 컬럼 - const answerCountColumn: ColumnDef<PQSubmission> = { - accessorKey: "answerCount", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="답변 수" /> - ), - cell: ({ row }) => { - return ( - <div className="flex items-center gap-2"> - <span>{row.original.answerCount}</span> - </div> - ) - }, - } - - const investigationAddressColumn: ColumnDef<PQSubmission> = { - accessorKey: "investigationAddress", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="실사 주소" /> - ), - cell: ({ row }) => { - const investigation = row.original.investigation; - - if (!investigation || !investigation.evaluationType) { - return <span className="text-muted-foreground">-</span>; - } - - return ( - <div className="flex items-center gap-2"> - <span>{investigation.investigationAddress}</span> - </div> - ) - }, - } - - const investigationNotesColumn: ColumnDef<PQSubmission> = { - accessorKey: "investigationNotes", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="QM 의견" /> - ), - cell: ({ row }) => { - const investigation = row.original.investigation; - - if (!investigation || !investigation.investigationNotes) { - return <span className="text-muted-foreground">-</span>; - } - - return ( - <div className="flex items-center gap-2"> - <span>{investigation.investigationNotes}</span> - </div> - ) - }, - } - - - const investigationRequestedAtColumn: ColumnDef<PQSubmission> = { - accessorKey: "investigationRequestedAt", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="실사 의뢰일" /> - ), - cell: ({ row }) => { - const investigation = row.original.investigation; - - if (!investigation || !investigation.requestedAt) { - return <span className="text-muted-foreground">-</span>; - } - const dateVal = investigation.requestedAt - - return ( - <div className="flex items-center gap-2"> - <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span> - </div> - ) - }, - } - - - const investigationForecastedAtColumn: ColumnDef<PQSubmission> = { - accessorKey: "investigationForecastedAt", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="실사 예정일" /> - ), - cell: ({ row }) => { - const investigation = row.original.investigation; - - if (!investigation || !investigation.forecastedAt) { - return <span className="text-muted-foreground">-</span>; - } - const dateVal = investigation.forecastedAt - - return ( - <div className="flex items-center gap-2"> - <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span> - </div> - ) - }, - } - - const investigationConfirmedAtColumn: ColumnDef<PQSubmission> = { - accessorKey: "investigationConfirmedAt", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="실사 확정일" /> - ), - cell: ({ row }) => { - const investigation = row.original.investigation; - - if (!investigation || !investigation.confirmedAt) { - return <span className="text-muted-foreground">-</span>; - } - const dateVal = investigation.confirmedAt - - return ( - <div className="flex items-center gap-2"> - <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span> - </div> - ) - }, - } - - const investigationCompletedAtColumn: ColumnDef<PQSubmission> = { - accessorKey: "investigationCompletedAt", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="실제 실사일" /> - ), - cell: ({ row }) => { - const investigation = row.original.investigation; - - if (!investigation || !investigation.completedAt) { - return <span className="text-muted-foreground">-</span>; - } - const dateVal = investigation.completedAt - - return ( - <div className="flex items-center gap-2"> - <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span> - </div> - ) - }, - } - - // 제출일 컬럼 - const createdAtColumn: ColumnDef<PQSubmission> = { - accessorKey: "createdAt", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="PQ 전송일" /> - ), - cell: ({ row }) => { - const dateVal = row.original.createdAt as Date - return formatDate(dateVal, 'KR') - }, - } - - // 제출일 컬럼 - const submittedAtColumn: ColumnDef<PQSubmission> = { - accessorKey: "submittedAt", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="PQ 회신일" /> - ), - cell: ({ row }) => { - const dateVal = row.original.submittedAt as Date - return dateVal ? formatDate(dateVal, 'KR') : "-" - }, - } - - // 승인/거부일 컬럼 - const approvalDateColumn: ColumnDef<PQSubmission> = { - accessorKey: "approvedAt", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="PQ 승인/거부일" /> - ), - cell: ({ row }) => { - if (row.original.approvedAt) { - return <span className="text-green-600">{formatDate(row.original.approvedAt, "KR")}</span> - } - if (row.original.rejectedAt) { - return <span className="text-red-600">{formatDate(row.original.rejectedAt, "KR")}</span> - } - return "-" - }, - } - - // ---------------------------------------------------------------- - // 3) actions 컬럼 (Dropdown 메뉴) - // ---------------------------------------------------------------- - const actionsColumn: ColumnDef<PQSubmission> = { - id: "actions", - enableHiding: false, - cell: function Cell({ row }) { - const pq = row.original - const isSubmitted = pq.status === "SUBMITTED" - const reviewUrl = `/evcp/pq_new/${pq.vendorId}/${pq.id}` - - return ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - aria-label="Open menu" - variant="ghost" - className="flex size-8 p-0 data-[state=open]:bg-muted" - > - <Ellipsis className="size-4" aria-hidden="true" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end" className="w-40"> - <DropdownMenuItem - onSelect={() => { - router.push(reviewUrl); - }} - > - {isSubmitted ? ( - <> - <FileEdit className="mr-2 h-4 w-4" /> - 검토 - </> - ) : ( - <> - <Eye className="mr-2 h-4 w-4" /> - 보기 - </> - )} - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - ) - }, - size: 40, - } - - // 요청자 컬럼 추가 -const requesterColumn: ColumnDef<PQSubmission> = { - accessorKey: "requesterName", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="PQ/실사 요청자" /> - ), - cell: ({ row }) => { - // PQ 요청자와 실사 요청자를 모두 표시 - const pqRequesterName = row.original.requesterName; - const investigationRequesterName = row.original.investigation?.requesterName; - - // 상태에 따라 적절한 요청자 표시 - const status = getCombinedStatus(row.original).status; - - if (status.startsWith('INVESTIGATION_') && investigationRequesterName) { - return <span>{investigationRequesterName}</span>; - } - - return pqRequesterName - ? <span>{pqRequesterName}</span> - : <span className="text-muted-foreground">-</span>; - }, -}; -const qmManagerColumn: ColumnDef<PQSubmission> = { - accessorKey: "qmManager", - header: ({ column }) => ( - <DataTableColumnHeader column={column} title="QM 담당자" /> - ), - cell: ({ row }) => { - const investigation = row.original.investigation; - - if (!investigation || !investigation.qmManagerName) { - return <span className="text-muted-foreground">-</span>; - } - - return ( - <div className="flex flex-col"> - <span>{investigation.qmManagerName}</span> - {investigation.qmManagerEmail && ( - <span className="text-xs text-muted-foreground">{investigation.qmManagerEmail}</span> - )} - </div> - ); - }, -}; - - - // ---------------------------------------------------------------- - // 4) 최종 컬럼 배열 - // ---------------------------------------------------------------- - return [ - selectColumn, - statusColumn, // 통합된 진행현황 컬럼 - pqNoColumn, - vendorColumn, - investigationAddressColumn, - typeColumn, - projectColumn, - createdAtColumn, - submittedAtColumn, - approvalDateColumn, - answerCountColumn, - evaluationTypeColumn, // 평가 유형 컬럼 - investigationForecastedAtColumn, - investigationRequestedAtColumn, - investigationConfirmedAtColumn, - investigationCompletedAtColumn, - evaluationResultColumn, // 평가 결과 컬럼 - requesterColumn, - qmManagerColumn, - investigationNotesColumn, - actionsColumn, - ]; +"use client"
+
+import * as React from "react"
+import { type DataTableRowAction } from "@/types/table"
+import { type ColumnDef } from "@tanstack/react-table"
+import { Ellipsis, Eye, FileEdit, Trash2, Building2, FileText, Edit } from "lucide-react"
+
+import { formatDate } from "@/lib/utils"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header";
+import { useRouter } from "next/navigation"
+import { PQDeleteDialog } from "@/components/pq-input/pq-delete-dialog"
+
+// PQ 제출 타입 정의
+export interface PQSubmission {
+ // PQ 제출 정보
+ id: number
+ pqNumber: string
+ type: string
+ status: string
+ requesterName: string | null // 요청자 이름
+ createdAt: Date
+ updatedAt: Date
+ submittedAt: Date | null
+ approvedAt: Date | null
+ rejectedAt: Date | null
+ rejectReason: string | null
+
+ // 협력업체 정보
+ vendorId: number
+ vendorName: string
+ vendorCode: string
+ taxId: string
+ vendorStatus: string
+ email: string
+ // 프로젝트 정보
+ projectId: number | null
+ projectName: string | null
+ projectCode: string | null
+
+ // 답변 정보
+ answerCount: number
+ attachmentCount: number
+
+ // PQ 상태
+ pqStatus: string
+ pqTypeLabel: string
+
+ // PQ 대상품목
+ pqItems: string | null
+
+ // 방문실사 요청 정보
+ siteVisitRequestId: number | null // 방문실사 요청 ID
+
+ // 실사 정보
+ investigation: {
+ id: number
+ investigationStatus: string
+ requesterName: string | null // 실사 요청자 이름
+ evaluationType: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL" | null
+ qmManagerId: number | null
+ qmManagerName: string | null // QM 담당자 이름
+ qmManagerEmail: string | null // QM 담당자 이메일
+ investigationAddress: string | null
+ investigationMethod: string | null
+ scheduledStartAt: Date | null
+ scheduledEndAt: Date | null
+ requestedAt: Date | null
+ confirmedAt: Date | null
+ completedAt: Date | null
+ forecastedAt: Date | null
+ evaluationScore: number | null
+ evaluationResult: "APPROVED" | "SUPPLEMENT" | "REJECTED" | "RESULT_SENT" | null
+ investigationNotes: string | null
+ } | null
+ // 통합 상태를 위한 새 필드
+ combinedStatus: {
+ status: string
+ label: string
+ variant: "default" | "outline" | "secondary" | "destructive" | "success"
+ }
+}
+
+type NextRouter = ReturnType<typeof useRouter>;
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<PQSubmission> | null>>;
+ router: NextRouter;
+}
+
+// 상태에 따른 Badge 변형 결정 함수
+function getStatusBadge(status: string) {
+ switch (status) {
+ case "REQUESTED":
+ return <Badge variant="outline">요청됨</Badge>
+ case "IN_PROGRESS":
+ return <Badge variant="secondary">진행 중</Badge>
+ case "SUBMITTED":
+ return <Badge>제출됨</Badge>
+ case "APPROVED":
+ return <Badge variant="success">승인됨</Badge>
+ case "REJECTED":
+ return <Badge variant="destructive">거부됨</Badge>
+ default:
+ return <Badge variant="outline">{status}</Badge>
+ }
+}
+
+/**
+ * tanstack table 컬럼 정의
+ */
+export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef<PQSubmission>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<PQSubmission> = {
+ 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"
+ />
+ ),
+ size: 40,
+ enableSorting: false,
+ enableHiding: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) 일반 컬럼들
+ // --------------------------
+ // --------------------------------------
+
+ const pqNoColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "pqNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PQ No." />
+ ),
+ cell: ({ row }) => (
+ <div className="flex flex-col">
+ <span className="font-medium">{row.getValue("pqNumber")}</span>
+ </div>
+ ),
+ }
+
+ // 협력업체 컬럼
+ const vendorColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "vendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체" />
+ ),
+ cell: ({ row }) => (
+ <div className="flex flex-col">
+ <span className="font-medium">{row.getValue("vendorName")}</span>
+ <span className="text-xs text-muted-foreground">{row.original.vendorCode ? row.original.vendorCode : "-"}/{row.original.taxId}</span>
+ </div>
+ ),
+ }
+
+ // PQ 유형 컬럼
+ const typeColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "type",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PQ 유형" />
+ ),
+ cell: ({ row }) => {
+ const { type, pqTypeLabel } = row.original;
+ let label = pqTypeLabel;
+ if (type === "NON_INSPECTION") {
+ label = "미실사 PQ";
+ }
+ return (
+ <div className="flex items-center">
+ <Badge variant={type === "PROJECT" ? "default" : "outline"}>
+ {label}
+ </Badge>
+ </div>
+ );
+ },
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id));
+ },
+ }
+
+ // 프로젝트 컬럼
+ const projectColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "projectName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="프로젝트" />
+ ),
+ cell: ({ row }) => {
+ const projectName = row.original.projectName
+ const projectCode = row.original.projectCode
+
+ if (!projectName) {
+ return <span className="text-muted-foreground">-</span>
+ }
+
+ return (
+ <div className="flex flex-col">
+ <span>{projectName}</span>
+ {projectCode && (
+ <span className="text-xs text-muted-foreground">{projectCode}</span>
+ )}
+ </div>
+ )
+ },
+ }
+
+ // 상태 컬럼
+ const statusColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "combinedStatus",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="진행현황" />
+ ),
+ cell: ({ row }) => {
+ const combinedStatus = getCombinedStatus(row.original);
+ return <Badge variant={combinedStatus.variant}>{combinedStatus.label}</Badge>;
+ },
+ filterFn: (row, id, value) => {
+ const combinedStatus = getCombinedStatus(row.original);
+ return value.includes(combinedStatus.status);
+ },
+ };
+
+ // PQ 상태와 실사 상태를 결합하는 헬퍼 함수
+ function getCombinedStatus(submission: PQSubmission) {
+ // PQ가 승인되지 않은 경우, PQ 상태를 우선 표시
+ if (submission.status !== "APPROVED") {
+ switch (submission.status) {
+ case "REQUESTED":
+ return { status: "PQ_REQUESTED", label: "PQ 요청됨", variant: "outline" as const };
+ case "IN_PROGRESS":
+ return { status: "PQ_IN_PROGRESS", label: "PQ 진행 중", variant: "secondary" as const };
+ case "SUBMITTED":
+ return { status: "PQ_SUBMITTED", label: "PQ 제출됨", variant: "default" as const };
+ case "REJECTED":
+ return { status: "PQ_REJECTED", label: "PQ 거부됨", variant: "destructive" as const };
+ default:
+ return { status: submission.status, label: submission.status, variant: "outline" as const };
+ }
+ }
+
+ // PQ가 승인되었지만 실사가 없는 경우
+ if (!submission.investigation) {
+ return { status: "PQ_APPROVED", label: "PQ 승인됨", variant: "success" as const };
+ }
+
+ // PQ가 승인되고 실사가 있는 경우
+ switch (submission.investigation.investigationStatus) {
+ case "PLANNED":
+ return { status: "INVESTIGATION_PLANNED", label: "실사 계획됨", variant: "outline" as const };
+ case "IN_PROGRESS":
+ return { status: "INVESTIGATION_IN_PROGRESS", label: "실사 진행 중", variant: "secondary" as const };
+ case "COMPLETED":
+ // 실사 완료 후 평가 결과에 따라 다른 상태 표시
+ if (submission.investigation.evaluationResult) {
+ switch (submission.investigation.evaluationResult) {
+ case "APPROVED":
+ return { status: "INVESTIGATION_APPROVED", label: "실사 승인", variant: "success" as const };
+ case "SUPPLEMENT":
+ return { status: "INVESTIGATION_SUPPLEMENT", label: "실사 보완필요", variant: "secondary" as const };
+ case "REJECTED":
+ return { status: "INVESTIGATION_REJECTED", label: "실사 불가", variant: "destructive" as const };
+ default:
+ return { status: "INVESTIGATION_COMPLETED", label: "실사 완료", variant: "default" as const };
+ }
+ }
+ return { status: "INVESTIGATION_COMPLETED", label: "실사 완료", variant: "default" as const };
+ case "CANCELED":
+ return { status: "INVESTIGATION_CANCELED", label: "실사 취소됨", variant: "destructive" as const };
+ case "RESULT_SENT":
+ return { status: "INVESTIGATION_RESULT_SENT", label: "실사 결과 발송", variant: "success" as const };
+ default:
+ return {
+ status: `INVESTIGATION_${submission.investigation.investigationStatus}`,
+ label: `실사 ${submission.investigation.investigationStatus}`,
+ variant: "outline" as const
+ };
+ }
+ }
+
+ const evaluationTypeColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "evaluationType",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="평가 유형" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.evaluationType) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+
+ switch (investigation.evaluationType) {
+ case "PURCHASE_SELF_EVAL":
+ return <Badge variant="outline">구매자체평가</Badge>;
+ case "DOCUMENT_EVAL":
+ return <Badge variant="secondary">서류평가</Badge>;
+ case "PRODUCT_INSPECTION":
+ return <Badge variant="default">제품검사평가</Badge>;
+ case "SITE_VISIT_EVAL":
+ return <Badge variant="destructive">방문실사평가</Badge>;
+ default:
+ return <span>{investigation.evaluationType}</span>;
+ }
+ },
+ filterFn: (row, id, value) => {
+ const investigation = row.original.investigation;
+ if (!investigation || !investigation.evaluationType) return value.includes("null");
+ return value.includes(investigation.evaluationType);
+ },
+ };
+
+
+ const evaluationResultColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "evaluationResult",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="평가 결과" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.evaluationResult) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+
+ switch (investigation.evaluationResult) {
+ case "APPROVED":
+ return <Badge variant="success">승인</Badge>;
+ case "SUPPLEMENT":
+ return <Badge variant="secondary">보완</Badge>;
+ case "REJECTED":
+ return <Badge variant="destructive">불가</Badge>;
+ default:
+ return <span>{investigation.evaluationResult}</span>;
+ }
+ },
+ filterFn: (row, id, value) => {
+ const investigation = row.original.investigation;
+ if (!investigation || !investigation.evaluationResult) return value.includes("null");
+ return value.includes(investigation.evaluationResult);
+ },
+ };
+
+ // 답변 수 컬럼
+ const answerCountColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "answerCount",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="답변 수" />
+ ),
+ cell: ({ row }) => {
+ return (
+ <div className="flex items-center gap-2">
+ <span>{row.original.answerCount}</span>
+ </div>
+ )
+ },
+ }
+
+ const investigationAddressColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "investigationAddress",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="실사 주소" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.evaluationType) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+ <span>{investigation.investigationAddress}</span>
+ </div>
+ )
+ },
+ }
+
+ const investigationNotesColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "investigationNotes",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="QM 의견" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.investigationNotes) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+ <span>{investigation.investigationNotes}</span>
+ </div>
+ )
+ },
+ }
+ const investigationMethodColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "investigationMethod",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="QM실사방법" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+ if (!investigation || !investigation.investigationMethod) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+
+ switch (investigation.investigationMethod) {
+ case "PURCHASE_SELF_EVAL":
+ return <Badge variant="outline">구매자체평가</Badge>;
+ case "DOCUMENT_EVAL":
+ return <Badge variant="secondary">서류평가</Badge>;
+ case "PRODUCT_INSPECTION":
+ return <Badge variant="default">제품검사평가</Badge>;
+ case "SITE_VISIT_EVAL":
+ return <Badge variant="destructive">방문실사평가</Badge>;
+ default:
+ return <span>{investigation.investigationMethod}</span>;
+ }
+ },
+ }
+
+ // 실사품목 컬럼
+ const pqItemsColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "pqItems",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="실사품목" />
+ ),
+ cell: ({ row }) => {
+ const pqItems = row.original.pqItems;
+
+ if (!pqItems) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+ <span className="text-sm">{pqItems}</span>
+ </div>
+ )
+ },
+ }
+
+
+ const investigationRequestedAtColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "investigationRequestedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="실사 의뢰일" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.requestedAt) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+ const dateVal = investigation.requestedAt
+
+ return (
+ <div className="flex items-center gap-2">
+ <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span>
+ </div>
+ )
+ },
+ }
+
+
+ const investigationForecastedAtColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "investigationForecastedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="실사 예정일" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.forecastedAt) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+ const dateVal = investigation.forecastedAt
+
+ return (
+ <div className="flex items-center gap-2">
+ <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span>
+ </div>
+ )
+ },
+ }
+
+ const investigationConfirmedAtColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "investigationConfirmedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="실사 확정일" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.confirmedAt) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+ const dateVal = investigation.confirmedAt
+
+ return (
+ <div className="flex items-center gap-2">
+ <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span>
+ </div>
+ )
+ },
+ }
+
+ const investigationCompletedAtColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "investigationCompletedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="실제 실사일" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.completedAt) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+ const dateVal = investigation.completedAt
+
+ return (
+ <div className="flex items-center gap-2">
+ <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span>
+ </div>
+ )
+ },
+ }
+
+ // 제출일 컬럼
+ const createdAtColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "createdAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PQ 전송일" />
+ ),
+ cell: ({ row }) => {
+ const dateVal = row.original.createdAt as Date
+ return formatDate(dateVal, 'KR')
+ },
+ }
+
+ // 제출일 컬럼
+ const submittedAtColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "submittedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PQ 회신일" />
+ ),
+ cell: ({ row }) => {
+ const dateVal = row.original.submittedAt as Date
+ return dateVal ? formatDate(dateVal, 'KR') : "-"
+ },
+ }
+
+ // 승인/거부일 컬럼
+ const approvalDateColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "approvedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PQ 승인/거부일" />
+ ),
+ cell: ({ row }) => {
+ if (row.original.approvedAt) {
+ return <span className="text-green-600">{formatDate(row.original.approvedAt)}</span>
+ }
+ if (row.original.rejectedAt) {
+ return <span className="text-red-600">{formatDate(row.original.rejectedAt)}</span>
+ }
+ return "-"
+ },
+ }
+
+ // ----------------------------------------------------------------
+ // 3) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<PQSubmission> = {
+ id: "actions",
+ enableHiding: false,
+ cell: function Cell({ row }) {
+ const pq = row.original
+ const isSubmitted = pq.status === "SUBMITTED"
+ const reviewUrl = `/evcp/pq_new/${pq.vendorId}/${pq.id}`
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ aria-label="Open menu"
+ variant="ghost"
+ className="flex size-8 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="size-4" aria-hidden="true" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-40">
+ <DropdownMenuItem
+ onSelect={() => {
+ router.push(reviewUrl);
+ }}
+ >
+ {isSubmitted ? (
+ <>
+ <FileEdit className="mr-2 h-4 w-4" />
+ 검토
+ </>
+ ) : (
+ <>
+ <Eye className="mr-2 h-4 w-4" />
+ 보기
+ </>
+ )}
+ </DropdownMenuItem>
+
+ {/* 방문실사 버튼 - 제품검사평가 또는 방문실사평가인 경우에만 표시 */}
+ {pq.investigation &&
+ (pq.investigation.investigationMethod === "PRODUCT_INSPECTION" ||
+ pq.investigation.investigationMethod === "SITE_VISIT_EVAL") && (
+ <>
+ <DropdownMenuItem
+ onSelect={(e) => {
+ e.preventDefault();
+ // 방문실사 다이얼로그 열기 로직
+ setRowAction({
+ type: "site-visit",
+ row: row.original
+ });
+ }}
+ >
+ <Building2 className="mr-2 h-4 w-4" />
+ 방문실사
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ onSelect={(e) => {
+ e.preventDefault();
+ // 협력업체 정보 조회 다이얼로그 열기 로직
+ setRowAction({
+ type: "vendor-info-view",
+ row: row.original
+ });
+ }}
+ >
+ <FileText className="mr-2 h-4 w-4" />
+ 협력업체 정보 조회
+ </DropdownMenuItem>
+ </>
+ )}
+
+ {/* 실사 정보 수정 버튼 - 구매자체평가인 경우에만 표시 */}
+ {pq.investigation &&
+ pq.investigation.investigationMethod === "PURCHASE_SELF_EVAL" && (
+ <DropdownMenuItem
+ onSelect={(e) => {
+ e.preventDefault();
+ // 실사 정보 수정 다이얼로그 열기 로직
+ setRowAction({
+ type: "edit-investigation",
+ row: row.original
+ });
+ }}
+ >
+ <Edit className="mr-2 h-4 w-4" />
+ 실사 정보 수정
+ </DropdownMenuItem>
+ )}
+
+ {/* 삭제 메뉴 - REQUESTED 상태일 때만 표시 */}
+ {pq.status === "REQUESTED" && (
+ <PQDeleteDialog
+ pqSubmissionId={pq.id}
+ status={pq.status}
+ >
+ <DropdownMenuItem
+ onSelect={(e) => {
+ e.preventDefault();
+ }}
+ className="text-destructive focus:text-destructive"
+ >
+ <Trash2 className="mr-2 h-4 w-4" />
+ 삭제
+ </DropdownMenuItem>
+ </PQDeleteDialog>
+ )}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ size: 40,
+ }
+
+ // 요청자 컬럼 추가
+const requesterColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "requesterName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PQ/실사 요청자" />
+ ),
+ cell: ({ row }) => {
+ // PQ 요청자와 실사 요청자를 모두 표시
+ const pqRequesterName = row.original.requesterName;
+ const investigationRequesterName = row.original.investigation?.requesterName;
+
+ // 상태에 따라 적절한 요청자 표시
+ const status = getCombinedStatus(row.original).status;
+
+ if (status.startsWith('INVESTIGATION_') && investigationRequesterName) {
+ return <span>{investigationRequesterName}</span>;
+ }
+
+ return pqRequesterName
+ ? <span>{pqRequesterName}</span>
+ : <span className="text-muted-foreground">-</span>;
+ },
+};
+const qmManagerColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "qmManager",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="QM 담당자" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.qmManagerName) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+
+ return (
+ <div className="flex flex-col">
+ <span>{investigation.qmManagerName}</span>
+ {investigation.qmManagerEmail && (
+ <span className="text-xs text-muted-foreground">{investigation.qmManagerEmail}</span>
+ )}
+ </div>
+ );
+ },
+};
+
+
+ // ----------------------------------------------------------------
+ // 4) 최종 컬럼 배열
+ // ----------------------------------------------------------------
+ return [
+ selectColumn,
+ statusColumn, // 통합된 진행현황 컬럼
+ pqNoColumn,
+ vendorColumn,
+ investigationAddressColumn,
+ typeColumn,
+ projectColumn,
+ pqItemsColumn, // 실사품목 컬럼
+ createdAtColumn,
+ submittedAtColumn,
+ approvalDateColumn,
+ answerCountColumn,
+ evaluationTypeColumn, // 평가 유형 컬럼
+ investigationMethodColumn,
+ investigationForecastedAtColumn,
+ investigationRequestedAtColumn,
+ investigationConfirmedAtColumn,
+ investigationCompletedAtColumn,
+ evaluationResultColumn, // 평가 결과 컬럼
+ requesterColumn,
+ qmManagerColumn,
+ investigationNotesColumn,
+ actionsColumn,
+ ];
}
\ No newline at end of file |
