diff options
| -rw-r--r-- | app/[lng]/evcp/(evcp)/pq_new/page.tsx | 4 | ||||
| -rw-r--r-- | app/[lng]/procurement/(procurement)/pq_new/page.tsx | 4 | ||||
| -rw-r--r-- | components/pq-input/pq-input-tabs.tsx | 17 | ||||
| -rw-r--r-- | components/pq-input/pq-review-wrapper.tsx | 14 | ||||
| -rw-r--r-- | lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx | 10 | ||||
| -rw-r--r-- | lib/pq/pq-criteria/pq-table-column.tsx | 40 | ||||
| -rw-r--r-- | lib/pq/pq-review-table-new/vendors-table-columns.tsx | 10 | ||||
| -rw-r--r-- | lib/pq/service.ts | 81 | ||||
| -rw-r--r-- | lib/vendors/table/request-pq-dialog.tsx | 18 |
9 files changed, 138 insertions, 60 deletions
diff --git a/app/[lng]/evcp/(evcp)/pq_new/page.tsx b/app/[lng]/evcp/(evcp)/pq_new/page.tsx index 0e6d3196..6a992ee5 100644 --- a/app/[lng]/evcp/(evcp)/pq_new/page.tsx +++ b/app/[lng]/evcp/(evcp)/pq_new/page.tsx @@ -9,7 +9,7 @@ import { getPQSubmissions } from "@/lib/pq/service" import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table"
import { InformationButton } from "@/components/information/information-button"
export const metadata: Metadata = {
- title: "PQ 검토/실사 의뢰",
+ title: "협력업체 PQ/실사 현황",
description: "",
}
@@ -71,7 +71,7 @@ export default async function PQReviewPage(props: PQReviewPageProps) { <div>
<div className="flex items-center gap-2">
<h2 className="text-2xl font-bold tracking-tight">
- PQ 검토/실사 의뢰
+ 협력업체 PQ/실사 현황
</h2>
<InformationButton pagePath="evcp/pq_new" />
</div>
diff --git a/app/[lng]/procurement/(procurement)/pq_new/page.tsx b/app/[lng]/procurement/(procurement)/pq_new/page.tsx index 0e6d3196..6a992ee5 100644 --- a/app/[lng]/procurement/(procurement)/pq_new/page.tsx +++ b/app/[lng]/procurement/(procurement)/pq_new/page.tsx @@ -9,7 +9,7 @@ import { getPQSubmissions } from "@/lib/pq/service" import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table"
import { InformationButton } from "@/components/information/information-button"
export const metadata: Metadata = {
- title: "PQ 검토/실사 의뢰",
+ title: "협력업체 PQ/실사 현황",
description: "",
}
@@ -71,7 +71,7 @@ export default async function PQReviewPage(props: PQReviewPageProps) { <div>
<div className="flex items-center gap-2">
<h2 className="text-2xl font-bold tracking-tight">
- PQ 검토/실사 의뢰
+ 협력업체 PQ/실사 현황
</h2>
<InformationButton pagePath="evcp/pq_new" />
</div>
diff --git a/components/pq-input/pq-input-tabs.tsx b/components/pq-input/pq-input-tabs.tsx index 534e1a05..4e5272f5 100644 --- a/components/pq-input/pq-input-tabs.tsx +++ b/components/pq-input/pq-input-tabs.tsx @@ -315,12 +315,11 @@ export function PQInputTabs({ return } } else if (inputFormat === "TEXT_FILE") { - // 텍스트+파일 항목의 경우 텍스트 답변과 파일이 모두 있어야 함 - const hasFiles = answerData.uploadedFiles.length > 0 || answerData.newUploads.length > 0 - if (!answerData.answer || !hasFiles) { + // 텍스트+파일 항목의 경우 텍스트 답변만 있어야 함 (파일은 선택적) + if (!answerData.answer) { toast({ title: "필수 항목", - description: "필수 항목입니다. 텍스트 답변과 파일을 모두 입력해주세요.", + description: "필수 항목입니다. 텍스트 답변을 입력해주세요.", variant: "destructive", }) return @@ -671,7 +670,7 @@ export function PQInputTabs({ {/* 2-column grid */} <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pb-4"> {sortByCode(group.items).map((item) => { - const { criteriaId, code, checkPoint, description, contractInfo, additionalRequirement } = item + const { criteriaId, code, checkPoint, remarks, description, contractInfo, additionalRequirement } = item const answerIndex = getAnswerIndex(criteriaId) if (answerIndex === -1) return null @@ -707,6 +706,14 @@ export function PQInputTabs({ {description} </CardDescription> )} + {item.remarks && ( + <div className="mt-2 p-2 rounded-md"> + <p className="text-sm font-medium text-muted-foreground mb-1">Remark:</p> + <p className="text-sm whitespace-pre-wrap"> + {item.remarks} + </p> + </div> + )} </div> {/* Save Status & Button */} diff --git a/components/pq-input/pq-review-wrapper.tsx b/components/pq-input/pq-review-wrapper.tsx index cc0f1b40..ca5f314f 100644 --- a/components/pq-input/pq-review-wrapper.tsx +++ b/components/pq-input/pq-review-wrapper.tsx @@ -380,12 +380,14 @@ export function PQReviewWrapper({ {item.description} </CardDescription> )} - {/* <div className="text-sm text-muted-foreground"> - 생성일: {item.createdAt?.toLocaleString('ko-KR')} - </div> - <div className="text-sm text-muted-foreground"> - 수정일: {item.updatedAt?.toLocaleString('ko-KR')} - </div> */} + {item.remarks && ( + <div className="mt-2 p-2 rounded-md"> + <p className="text-sm font-medium text-muted-foreground mb-1">Remark:</p> + <p className="text-sm whitespace-pre-wrap"> + {item.remarks} + </p> + </div> + )} </div> {/* 항목 상태 표시 */} {!!item.answer || item.attachments.length > 0 ? ( diff --git a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx index 534a2705..635993fb 100644 --- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx @@ -35,7 +35,7 @@ import { getVendorAttachments, processBuyerSignatureAction } from "../service"; // 계약서 상태 타입 정의 interface ContractStatus { id: number; - status: 'pending' | 'completed' | 'error'; + status: 'pending' | 'completed' | 'error' | 'vendor_signed'; errorMessage?: string; } @@ -151,7 +151,9 @@ const canCompleteCurrentContract = React.useMemo(() => { }, [contracts, contractStatuses.length]); // 완료된 계약서 수 계산 - const completedCount = contractStatuses.filter(status => status.status === 'completed').length; + const completedCount = contractStatuses.filter(status => + status.status === 'completed' || status.status === 'vendor_signed' + ).length; const totalCount = contracts.length; const allCompleted = completedCount === totalCount && totalCount > 0; @@ -162,7 +164,9 @@ const canCompleteCurrentContract = React.useMemo(() => { // 다음 미완료 계약서 찾기 const getNextPendingContract = () => { - const pendingStatuses = contractStatuses.filter(status => status.status === 'pending'); + const pendingStatuses = contractStatuses.filter(status => + status.status === 'pending' || status.status === 'error' + ); if (pendingStatuses.length === 0) return null; const nextPendingId = pendingStatuses[0].id; diff --git a/lib/pq/pq-criteria/pq-table-column.tsx b/lib/pq/pq-criteria/pq-table-column.tsx index 924d80c4..de7396bf 100644 --- a/lib/pq/pq-criteria/pq-table-column.tsx +++ b/lib/pq/pq-criteria/pq-table-column.tsx @@ -125,26 +125,26 @@ export function getColumns({ minSize: 180, size: 180, }, - // { - // accessorKey: "remarks", - // header: ({ column }) => ( - // <DataTableColumnHeaderSimple column={column} title="SHI Comment" /> - // ), - // cell: ({ row }) => { - // const text = row.getValue("remarks") as string - // return ( - // <div style={{ whiteSpace: "pre-wrap" }}> - // {text || "-"} - // </div> - // ) - // }, - // meta: { - // excelHeader: "Remarks" - // }, - // enableResizing: true, - // minSize: 180, - // size: 180, - // }, + { + accessorKey: "remarks", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="비고" /> + ), + cell: ({ row }) => { + const text = row.getValue("remarks") as string + return ( + <div style={{ whiteSpace: "pre-wrap" }}> + {text || "-"} + </div> + ) + }, + meta: { + excelHeader: "Remarks" + }, + enableResizing: true, + minSize: 180, + size: 180, + }, { accessorKey: "inputFormat", header: ({ column }) => ( 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 449b69be..e3687f52 100644 --- a/lib/pq/pq-review-table-new/vendors-table-columns.tsx +++ b/lib/pq/pq-review-table-new/vendors-table-columns.tsx @@ -175,6 +175,8 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef <span className="text-xs text-muted-foreground">{row.original.vendorCode ? row.original.vendorCode : "-"}/{row.original.taxId}</span>
</div>
),
+ enableSorting: true,
+ enableHiding: true,
}
// PQ 유형 컬럼
@@ -200,6 +202,8 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
+ enableSorting: true,
+ enableHiding: true,
}
// 프로젝트 컬럼
@@ -225,6 +229,8 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef </div>
)
},
+ enableSorting: true,
+ enableHiding: true,
}
// 상태 컬럼
@@ -241,6 +247,8 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef const combinedStatus = getCombinedStatus(row.original);
return value.includes(combinedStatus.status);
},
+ enableSorting: true,
+ enableHiding: true,
};
// PQ 상태와 실사 상태를 결합하는 헬퍼 함수
@@ -362,6 +370,8 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef if (!investigation || !investigation.evaluationResult) return value.includes("null");
return value.includes(investigation.evaluationResult);
},
+ enableSorting: true,
+ enableHiding: true,
};
// 답변 수 컬럼
diff --git a/lib/pq/service.ts b/lib/pq/service.ts index 5870a77f..172542a3 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -18,7 +18,7 @@ import { vendorRegularRegistrations } from "@/db/schema/vendorRegistrations"; import { saveFile, saveDRMFile } from "@/lib/file-stroage";
import { GetVendorsSchema } from "../vendors/validations";
import { selectVendors } from "../vendors/repository";
-import { projects, users } from "@/db/schema";
+import { projects, users, roles, userRoles } from "@/db/schema";
import { headers } from 'next/headers';
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"
@@ -1763,8 +1763,20 @@ export async function getPQSubmissions(input: GetPQSubmissionsSchema) { // 7) 정렬 및 페이징 처리된 데이터 조회
const orderByColumns = input.sort.map((sort) => {
- const column = sort.id as keyof typeof vendorPQSubmissions.$inferSelect;
- return sort.desc ? desc(vendorPQSubmissions[column]) : asc(vendorPQSubmissions[column]);
+ const column = sort.id;
+
+ // JOIN된 테이블의 컬럼인 경우 적절한 테이블 참조
+ if (column === 'vendorName') {
+ return sort.desc ? desc(vendors.vendorName) : asc(vendors.vendorName);
+ } else if (column === 'projectName') {
+ return sort.desc ? desc(projects.name) : asc(projects.name);
+ } else if (column === 'requesterName') {
+ return sort.desc ? desc(users.name) : asc(users.name);
+ } else {
+ // vendorPQSubmissions 테이블의 컬럼인 경우
+ const dbColumn = column as keyof typeof vendorPQSubmissions.$inferSelect;
+ return sort.desc ? desc(vendorPQSubmissions[dbColumn]) : asc(vendorPQSubmissions[dbColumn]);
+ }
});
if (orderByColumns.length === 0) {
@@ -2818,40 +2830,75 @@ function getInvestigationMethodLabel(method: string): string { }
}
+// export async function getQMManagers() {
+// try {
+// // QM 부서 사용자만 필터링 (department 필드가 있다고 가정)
+// // 또는 QM 역할을 가진 사용자만 필터링 (role 필드가 있다고 가정)
+// const qmUsers = await db
+// .select({
+// id: users.id,
+// name: users.name,
+// email: users.email,
+// })
+// .from(users)
+// // .where(
+// // // 필요에 따라 조건 조정 (예: QM 부서 또는 특정 역할만)
+// // // eq(users.department, "QM") 또는
+// // // eq(users.role, "QM_MANAGER")
+// // // 테스트를 위해 모든 사용자 반환도 가능
+// // eq(users.active, true)
+// // )
+// .orderBy(users.name)
+
+// return {
+// data: qmUsers,
+// success: true
+// }
+// } catch (error) {
+// console.error("QM 담당자 목록 조회 오류:", error)
+// return {
+// data: [],
+// success: false,
+// error: error instanceof Error ? error.message : "QM 담당자 목록을 가져오는 중 오류가 발생했습니다."
+// }
+// }
+// }
export async function getQMManagers() {
try {
- // QM 부서 사용자만 필터링 (department 필드가 있다고 가정)
- // 또는 QM 역할을 가진 사용자만 필터링 (role 필드가 있다고 가정)
+ // QM 역할이 할당된 사용자들을 조회
const qmUsers = await db
.select({
id: users.id,
name: users.name,
email: users.email,
+ employeeNumber: users.employeeNumber,
+ deptName: users.deptName,
+ isActive: users.isActive,
})
.from(users)
- // .where(
- // // 필요에 따라 조건 조정 (예: QM 부서 또는 특정 역할만)
- // // eq(users.department, "QM") 또는
- // // eq(users.role, "QM_MANAGER")
- // // 테스트를 위해 모든 사용자 반환도 가능
- // eq(users.active, true)
- // )
+ .innerJoin(userRoles, eq(users.id, userRoles.userId))
+ .innerJoin(roles, eq(userRoles.roleId, roles.id))
+ .where(
+ and(
+ ilike(roles.name, "%QM%"), // "QM"이 포함된 역할명
+ eq(users.isActive, true) // 활성 사용자만
+ )
+ )
.orderBy(users.name)
- return {
+ return {
data: qmUsers,
- success: true
+ success: true
}
} catch (error) {
console.error("QM 담당자 목록 조회 오류:", error)
- return {
+ return {
data: [],
success: false,
- error: error instanceof Error ? error.message : "QM 담당자 목록을 가져오는 중 오류가 발생했습니다."
+ error: error instanceof Error ? error.message : "QM 담당자 목록을 가져오는 중 오류가 발생했습니다."
}
}
}
-
export async function getFactoryLocationAnswer(vendorId: number, projectId: number | null = null) {
try {
// 1. "Location of Factory" 체크포인트를 가진 criteria 찾기
diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx index aeb0c717..206846df 100644 --- a/lib/vendors/table/request-pq-dialog.tsx +++ b/lib/vendors/table/request-pq-dialog.tsx @@ -154,10 +154,14 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro if (isAllForeign) {
// 외자: 준법서약 (영문), GTC 선택 (GTC는 1개만 선택하도록)
- const foreignTemplates = templates.filter(template =>
- template.templateName?.includes('준법서약') && template.templateName?.includes('영문') ||
- template.templateName?.includes('gtc')
- )
+ // 비밀유지 계약서, 기술자료 요구서 제외
+ const foreignTemplates = templates.filter(template => {
+ const name = template.templateName?.toLowerCase() || ''
+ return (
+ (template.templateName?.includes('준법서약') && template.templateName?.includes('영문')) ||
+ template.templateName?.includes('gtc')
+ ) && !name.includes('비밀유지') && !name.includes('기술자료')
+ })
// GTC 템플릿 중 최신 리비전의 것만 선택
const gtcTemplates = foreignTemplates.filter(t => t.templateName?.includes('gtc'))
const nonGtcTemplates = foreignTemplates.filter(t => !t.templateName?.includes('gtc'))
@@ -173,9 +177,13 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro }
} else if (isAllDomestic) {
// 내자: 준법서약 (영문), GTC 제외한 모든 템플릿 선택
+ // 비밀유지 계약서, 기술자료 요구서 제외
const domesticTemplates = templates.filter(template => {
const name = template.templateName?.toLowerCase() || ''
- return !(name.includes('준법서약') && name.includes('영문')) && !name.includes('gtc')
+ return !(name.includes('준법서약') && name.includes('영문')) &&
+ !name.includes('gtc') &&
+ !name.includes('비밀유지') &&
+ !name.includes('기술자료')
})
setSelectedTemplateIds(domesticTemplates.map(t => t.id))
}
|
