summaryrefslogtreecommitdiff
path: root/lib/pq/pq-review-table-new/vendors-table-columns.tsx
diff options
context:
space:
mode:
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.tsx1425
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