diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/forms/stat.ts | 50 | ||||
| -rw-r--r-- | lib/pq/service.ts | 19 | ||||
| -rw-r--r-- | lib/rfq-last/service.ts | 1 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/rfq-vendor-table.tsx | 10 | ||||
| -rw-r--r-- | lib/tbe-last/service.ts | 21 | ||||
| -rw-r--r-- | lib/tbe-last/vendor/tbe-table.tsx | 8 | ||||
| -rw-r--r-- | lib/vendor-document-list/ship/import-from-dolce-button.tsx | 188 |
7 files changed, 194 insertions, 103 deletions
diff --git a/lib/forms/stat.ts b/lib/forms/stat.ts index 054f2462..f13bab61 100644 --- a/lib/forms/stat.ts +++ b/lib/forms/stat.ts @@ -218,39 +218,13 @@ export async function getVendorFormStatus(projectId?: number): Promise<VendorFor -export async function getFormStatusByVendor(projectId: number, formCode: string): Promise<FormStatusByVendor[]> { +export async function getFormStatusByVendor(projectId: number, contractItemId: number, formCode: string): Promise<FormStatusByVendor[]> { try { const session = await getServerSession(authOptions) if (!session?.user?.id) { throw new Error("인증이 필요합니다.") } - const vendorStatusList: FormStatusByVendor[] = [] - const vendorId = Number(session.user.companyId) - - const vendorContracts = await db - .select({ - id: contracts.id, - projectId: contracts.projectId - }) - .from(contracts) - .where( - and( - eq(contracts.vendorId, vendorId), - eq(contracts.projectId, projectId) - ) - ) - - const contractIds = vendorContracts.map(v => v.id) - - const contractItemsList = await db - .select({ - id: contractItems.id - }) - .from(contractItems) - .where(inArray(contractItems.contractId, contractIds)) - - const contractItemIds = contractItemsList.map(v => v.id) let vendorFormCount = 0 let vendorTagCount = 0 @@ -277,7 +251,7 @@ export async function getFormStatusByVendor(projectId: number, formCode: string) .from(forms) .where( and( - inArray(forms.contractItemId, contractItemIds), + eq(forms.contractItemId, contractItemId), eq(forms.formCode, formCode) ) ) @@ -294,28 +268,16 @@ export async function getFormStatusByVendor(projectId: number, formCode: string) .from(formEntries) .where( and( - inArray(formEntries.contractItemId, contractItemIds), + eq(formEntries.contractItemId, contractItemId), eq(formEntries.formCode, formCode) ) ) // 6. TAG별 편집 가능 필드 조회 - const editableFieldsByTag = new Map<string, string[]>() - - for (const contractItemId of contractItemIds) { - const tagFields = await getEditableFieldsByTag(contractItemId, projectId) - - tagFields.forEach((fields, tagNo) => { - if (!editableFieldsByTag.has(tagNo)) { - editableFieldsByTag.set(tagNo, fields) - } else { - const existingFields = editableFieldsByTag.get(tagNo) || [] - const mergedFields = [...new Set([...existingFields, ...fields])] - editableFieldsByTag.set(tagNo, mergedFields) - } - }) - } + const editableFieldsByTag = await getEditableFieldsByTag(contractItemId, projectId) + const vendorStatusList: VendorFormStatus[] = [] + for (const entry of entriesList) { const metaResult = await db .select({ diff --git a/lib/pq/service.ts b/lib/pq/service.ts index 67be5398..f58a1d4d 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -94,6 +94,10 @@ export async function getPQDataByVendorId( projectId?: number
): Promise<PQGroupData[]> {
try {
+ // 파라미터 유효성 검증
+ if (isNaN(vendorId)) {
+ throw new Error("Invalid vendorId parameter");
+ }
// 기본 쿼리 구성
const selectObj = {
criteriaId: pqCriterias.id,
@@ -1531,6 +1535,7 @@ export async function getAllPQsByVendorId(vendorId: number) { // 특정 PQ의 상세 정보 조회 (개별 PQ 페이지용)
export async function getPQById(pqSubmissionId: number, vendorId: number) {
try {
+
const pq = await db
.select({
id: vendorPQSubmissions.id,
@@ -1543,12 +1548,15 @@ export async function getPQById(pqSubmissionId: number, vendorId: number) { approvedAt: vendorPQSubmissions.approvedAt,
rejectedAt: vendorPQSubmissions.rejectedAt,
rejectReason: vendorPQSubmissions.rejectReason,
-
+
// 벤더 정보 (추가)
vendorName: vendors.vendorName,
vendorCode: vendors.vendorCode,
vendorStatus: vendors.status,
-
+ vendorCountry: vendors.country,
+ vendorEmail: vendors.email,
+ vendorPhone: vendors.phone,
+
// 프로젝트 정보 (조인)
projectName: projects.name,
projectCode: projects.code,
@@ -1564,11 +1572,11 @@ export async function getPQById(pqSubmissionId: number, vendorId: number) { )
.limit(1)
.then(rows => rows[0]);
-
+
if (!pq) {
throw new Error("PQ not found or access denied");
}
-
+
return pq;
} catch (error) {
console.error("Error fetching PQ by ID:", error);
@@ -4046,11 +4054,12 @@ export async function updatePqValidToAction(input: UpdatePqValidToInput) { }
}
+
// SHI 참석자 총 인원수 계산 함수
export async function getTotalShiAttendees(shiAttendees: Record<string, unknown> | null): Promise<number> {
if (!shiAttendees) return 0
-
+
let total = 0
Object.entries(shiAttendees).forEach(([key, value]) => {
if (value && typeof value === 'object' && 'checked' in value && 'count' in value) {
diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 78d2479a..f536a142 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -3292,6 +3292,7 @@ async function processSingleVendor({ currentUser, designAttachments }); + console.log("tbeSession 생성 완료", tbeSession); // 이메일 발송 처리 (사용자가 선택한 경우에만) let emailSent = null; if (hasToSendEmail) { diff --git a/lib/rfq-last/vendor/rfq-vendor-table.tsx b/lib/rfq-last/vendor/rfq-vendor-table.tsx index e5c1f51e..55549a6d 100644 --- a/lib/rfq-last/vendor/rfq-vendor-table.tsx +++ b/lib/rfq-last/vendor/rfq-vendor-table.tsx @@ -765,11 +765,11 @@ export function RfqVendorTable({ } const statusConfig = { - "진행중": { variant: "default", icon: <Clock className="h-3 w-3 mr-1" />, color: "text-blue-600" }, - "검토중": { variant: "secondary", icon: <Eye className="h-3 w-3 mr-1" />, color: "text-orange-600" }, - "보류": { variant: "outline", icon: <AlertCircle className="h-3 w-3 mr-1" />, color: "text-yellow-600" }, - "완료": { variant: "success", icon: <CheckCircle className="h-3 w-3 mr-1" />, color: "text-green-600" }, - "취소": { variant: "destructive", icon: <XCircle className="h-3 w-3 mr-1" />, color: "text-red-600" }, + "진행중": { variant: "default", icon: <Clock className="h-3 w-3 mr-1" />}, + "검토중": { variant: "secondary", icon: <Eye className="h-3 w-3 mr-1" /> }, + "보류": { variant: "outline", icon: <AlertCircle className="h-3 w-3 mr-1" /> }, + "완료": { variant: "success", icon: <CheckCircle className="h-3 w-3 mr-1" /> }, + "취소": { variant: "destructive", icon: <XCircle className="h-3 w-3 mr-1" /> }, }[status] || { variant: "outline", icon: null, color: "text-gray-600" }; return ( diff --git a/lib/tbe-last/service.ts b/lib/tbe-last/service.ts index da0a5a4c..346576e5 100644 --- a/lib/tbe-last/service.ts +++ b/lib/tbe-last/service.ts @@ -1,11 +1,11 @@ // lib/tbe-last/service.ts 'use server' -import { revalidatePath, unstable_cache } from "next/cache"; +import { revalidatePath, revalidateTag, unstable_cache } from "next/cache"; import db from "@/db/db"; import { and, desc, asc, eq, sql, or, isNull, isNotNull, ne, inArray } from "drizzle-orm"; import { tbeLastView, tbeDocumentsView } from "@/db/schema"; -import { rfqPrItems } from "@/db/schema/rfqLast"; +import { rfqPrItems, rfqsLast } from "@/db/schema/rfqLast"; import {rfqLastDetails, rfqLastTbeDocumentReviews, rfqLastTbePdftronComments, rfqLastTbeVendorDocuments,rfqLastTbeSessions } from "@/db/schema"; import { filterColumns } from "@/lib/filter-columns"; import { GetTBELastSchema } from "./validations"; @@ -320,10 +320,22 @@ export async function updateTbeEvaluation( // 상태 업데이트 if (data.status !== undefined) { updateData.status = data.status - + // 완료 상태로 변경 시 종료일 설정 if (data.status === "완료") { updateData.actualEndDate = new Date() + + // TBE 완료 시 연결된 RFQ 상태를 "TBE 완료"로 업데이트 + if (currentTbeSession.rfqsLastId) { + await db + .update(rfqsLast) + .set({ + status: "TBE 완료", + updatedBy: userId, + updatedAt: new Date() + }) + .where(eq(rfqsLast.id, currentTbeSession.rfqsLastId)) + } } } @@ -337,10 +349,11 @@ export async function updateTbeEvaluation( // 캐시 초기화 revalidateTag(`tbe-session-${tbeSessionId}`) revalidateTag(`tbe-sessions`) - + // RFQ 관련 캐시도 초기화 if (currentTbeSession.rfqsLastId) { revalidateTag(`rfq-${currentTbeSession.rfqsLastId}`) + revalidateTag(`rfqs`) } return { diff --git a/lib/tbe-last/vendor/tbe-table.tsx b/lib/tbe-last/vendor/tbe-table.tsx index d7ee0a06..48242088 100644 --- a/lib/tbe-last/vendor/tbe-table.tsx +++ b/lib/tbe-last/vendor/tbe-table.tsx @@ -21,6 +21,7 @@ import { VendorQADialog } from "./vendor-comment-dialog" import { VendorDocumentsSheet } from "./vendor-documents-sheet" import { VendorPrItemsDialog } from "./vendor-pr-items-dialog" import { getTBEforVendor } from "../vendor-tbe-service" +import { VendorEvaluationViewDialog } from "./vendor-evaluation-view-dialog" interface TbeVendorTableProps { promises: Promise<[ @@ -217,6 +218,13 @@ export function TbeVendorTable({ promises }: TbeVendorTableProps) { onOpenChange={setPrItemsOpen} rfqId={selectedRfqId} /> + {/* Evaluation View Dialog */} + <VendorEvaluationViewDialog + open={evaluationViewOpen} + onOpenChange={setEvaluationViewOpen} + selectedSession={selectedSession} + sessionDetail={sessionDetail} + /> </> ) }
\ No newline at end of file diff --git a/lib/vendor-document-list/ship/import-from-dolce-button.tsx b/lib/vendor-document-list/ship/import-from-dolce-button.tsx index fe7f55c7..76d66960 100644 --- a/lib/vendor-document-list/ship/import-from-dolce-button.tsx +++ b/lib/vendor-document-list/ship/import-from-dolce-button.tsx @@ -1,4 +1,4 @@ -// import-from-dolce-button.tsx - 최적화된 버전 +// import-from-dolce-button.tsx - 리비전/첨부파일 포함 버전 "use client" import * as React from "react" @@ -223,20 +223,30 @@ export function ImportFromDOLCEButton({ } }, [debouncedProjectIds, fetchAllImportStatus]) - - - // 🔥 전체 통계 메모이제이션 + // 🔥 전체 통계 메모이제이션 - 리비전과 첨부파일 추가 const totalStats = React.useMemo(() => { const statuses = Array.from(importStatusMap.values()) return statuses.reduce((acc, status) => ({ availableDocuments: acc.availableDocuments + (status.availableDocuments || 0), newDocuments: acc.newDocuments + (status.newDocuments || 0), updatedDocuments: acc.updatedDocuments + (status.updatedDocuments || 0), + availableRevisions: acc.availableRevisions + (status.availableRevisions || 0), + newRevisions: acc.newRevisions + (status.newRevisions || 0), + updatedRevisions: acc.updatedRevisions + (status.updatedRevisions || 0), + availableAttachments: acc.availableAttachments + (status.availableAttachments || 0), + newAttachments: acc.newAttachments + (status.newAttachments || 0), + updatedAttachments: acc.updatedAttachments + (status.updatedAttachments || 0), importEnabled: acc.importEnabled || status.importEnabled }), { availableDocuments: 0, newDocuments: 0, updatedDocuments: 0, + availableRevisions: 0, + newRevisions: 0, + updatedRevisions: 0, + availableAttachments: 0, + newAttachments: 0, + updatedAttachments: 0, importEnabled: false }) }, [importStatusMap]) @@ -347,7 +357,14 @@ export function ImportFromDOLCEButton({ } }, [projectIds, fetchAllImportStatus, onImportComplete, t]) - // 🔥 상태 뱃지 메모이제이션 + // 🔥 전체 변경 사항 계산 + const totalChanges = React.useMemo(() => { + return totalStats.newDocuments + totalStats.updatedDocuments + + totalStats.newRevisions + totalStats.updatedRevisions + + totalStats.newAttachments + totalStats.updatedAttachments + }, [totalStats]) + + // 🔥 상태 뱃지 메모이제이션 - 리비전과 첨부파일 포함 const statusBadge = React.useMemo(() => { if (loadingVendorProjects) { return <Badge variant="secondary">{t('dolceImport.status.loadingProjectInfo')}</Badge> @@ -365,7 +382,7 @@ export function ImportFromDOLCEButton({ return <Badge variant="secondary">{t('dolceImport.status.importDisabled')}</Badge> } - if (totalStats.newDocuments > 0 || totalStats.updatedDocuments > 0) { + if (totalChanges > 0) { return ( <Badge variant="samsung" className="gap-1"> <AlertTriangle className="w-3 h-3" /> @@ -380,10 +397,10 @@ export function ImportFromDOLCEButton({ {t('dolceImport.status.synchronized')} </Badge> ) - }, [loadingVendorProjects, statusLoading, importStatusMap.size, totalStats, projectIds.length, t]) + }, [loadingVendorProjects, statusLoading, importStatusMap.size, totalStats.importEnabled, totalChanges, projectIds.length, t]) - const canImport = totalStats.importEnabled && - (totalStats.newDocuments > 0 || totalStats.updatedDocuments > 0) + // 🔥 가져오기 가능 여부 - 리비전과 첨부파일도 체크 + const canImport = totalStats.importEnabled && totalChanges > 0 // 🔥 새로고침 핸들러 최적화 const handleRefresh = React.useCallback(() => { @@ -391,24 +408,20 @@ export function ImportFromDOLCEButton({ fetchAllImportStatus() }, [fetchAllImportStatus]) - - // 🔥 자동 동기화 실행 (기존 useEffect들 다음에 추가) - React.useEffect(() => { + // 🔥 자동 동기화 실행 (기존 useEffect들 다음에 추가) + React.useEffect(() => { // 조건: 가져오기 가능하고, 동기화할 항목이 있고, 현재 진행중이 아닐 때 - if (canImport && - (totalStats.newDocuments > 0 || totalStats.updatedDocuments > 0) && - !isImporting && - !isDialogOpen) { + if (canImport && totalChanges > 0 && !isImporting && !isDialogOpen) { // 상태 로딩이 완료된 후 잠깐 대기 (사용자가 상태를 확인할 수 있도록) const timer = setTimeout(() => { - console.log(`🔄 자동 동기화 시작: 새 문서 ${totalStats.newDocuments}개, 업데이트 ${totalStats.updatedDocuments}개`) + console.log(`🔄 자동 동기화 시작: ${totalChanges}개 항목`) // 동기화 시작 알림 toast.info( - '새로운 문서가 발견되어 자동 동기화를 시작합니다', + '새로운 변경사항이 발견되어 자동 동기화를 시작합니다', { - description: `새 문서 ${totalStats.newDocuments}개, 업데이트 ${totalStats.updatedDocuments}개`, + description: `총 ${totalChanges}개 항목 (문서/리비전/첨부파일)`, duration: 3000 } ) @@ -424,9 +437,8 @@ export function ImportFromDOLCEButton({ return () => clearTimeout(timer) } - }, [canImport, totalStats.newDocuments, totalStats.updatedDocuments, isImporting, isDialogOpen, handleImport]) + }, [canImport, totalChanges, isImporting, isDialogOpen, handleImport]) - // 로딩 중이거나 projectIds가 없으면 버튼을 표시하지 않음 if (projectIds.length === 0) { return null @@ -449,12 +461,12 @@ export function ImportFromDOLCEButton({ <Download className="w-4 h-4" /> )} <span className="hidden sm:inline">{t('dolceImport.buttons.getList')}</span> - {totalStats.newDocuments + totalStats.updatedDocuments > 0 && ( + {totalChanges > 0 && ( <Badge variant="samsung" className="h-5 w-5 p-0 text-xs flex items-center justify-center" > - {totalStats.newDocuments + totalStats.updatedDocuments} + {totalChanges} </Badge> )} </Button> @@ -493,21 +505,75 @@ export function ImportFromDOLCEButton({ <div className="space-y-3"> <Separator /> - <div className="grid grid-cols-2 gap-4 text-sm"> - <div> - <div className="text-muted-foreground">{t('dolceImport.labels.newDocuments')}</div> - <div className="font-medium">{totalStats.newDocuments || 0}</div> - </div> - <div> - <div className="text-muted-foreground">{t('dolceImport.labels.updates')}</div> - <div className="font-medium">{totalStats.updatedDocuments || 0}</div> + {/* 문서 정보 */} + <div className="space-y-2"> + <div className="font-medium text-sm">{t('dolceImport.labels.documents')}</div> + <div className="grid grid-cols-3 gap-3 text-sm"> + <div> + <div className="text-muted-foreground">{t('dolceImport.labels.total')}</div> + <div className="font-medium">{totalStats.availableDocuments || 0}</div> + </div> + <div> + <div className="text-muted-foreground">{t('dolceImport.labels.new')}</div> + <div className="font-medium text-green-600">{totalStats.newDocuments || 0}</div> + </div> + <div> + <div className="text-muted-foreground">{t('dolceImport.labels.updates')}</div> + <div className="font-medium text-blue-600">{totalStats.updatedDocuments || 0}</div> + </div> </div> </div> - <div className="text-sm"> - <div className="text-muted-foreground">{t('dolceImport.labels.totalDocuments')}</div> - <div className="font-medium">{totalStats.availableDocuments || 0}</div> - </div> + {/* 리비전 정보 */} + {(totalStats.availableRevisions > 0 || totalStats.newRevisions > 0 || totalStats.updatedRevisions > 0) && ( + <div className="space-y-2"> + <div className="font-medium text-sm">{t('dolceImport.labels.revisions')}</div> + <div className="grid grid-cols-3 gap-3 text-sm"> + <div> + <div className="text-muted-foreground">{t('dolceImport.labels.total')}</div> + <div className="font-medium">{totalStats.availableRevisions || 0}</div> + </div> + <div> + <div className="text-muted-foreground">{t('dolceImport.labels.new')}</div> + <div className="font-medium text-green-600">{totalStats.newRevisions || 0}</div> + </div> + <div> + <div className="text-muted-foreground">{t('dolceImport.labels.updates')}</div> + <div className="font-medium text-blue-600">{totalStats.updatedRevisions || 0}</div> + </div> + </div> + </div> + )} + + {/* 첨부파일 정보 */} + {(totalStats.availableAttachments > 0 || totalStats.newAttachments > 0 || totalStats.updatedAttachments > 0) && ( + <div className="space-y-2"> + <div className="font-medium text-sm">{t('dolceImport.labels.attachments')}</div> + <div className="grid grid-cols-3 gap-3 text-sm"> + <div> + <div className="text-muted-foreground">{t('dolceImport.labels.total')}</div> + <div className="font-medium">{totalStats.availableAttachments || 0}</div> + </div> + <div> + <div className="text-muted-foreground">{t('dolceImport.labels.new')}</div> + <div className="font-medium text-green-600">{totalStats.newAttachments || 0}</div> + </div> + <div> + <div className="text-muted-foreground">{t('dolceImport.labels.updates')}</div> + <div className="font-medium text-blue-600">{totalStats.updatedAttachments || 0}</div> + </div> + </div> + </div> + )} + + {/* 요약 */} + {totalChanges > 0 && ( + <div className="bg-muted/50 rounded-lg p-3"> + <div className="text-sm font-medium"> + {t('dolceImport.labels.totalChanges')}: <span className="text-primary">{totalChanges}</span> + </div> + </div> + )} {/* 각 프로젝트별 세부 정보 */} {projectIds.length > 1 && ( @@ -522,11 +588,29 @@ export function ImportFromDOLCEButton({ <div key={projectId} className="text-xs"> <div className="font-medium">{t('dolceImport.labels.projectLabel', { projectId })}</div> {status ? ( - <div className="text-muted-foreground"> - {t('dolceImport.descriptions.projectDetails', { - newDocuments: status.newDocuments, - updatedDocuments: status.updatedDocuments - })} + <div className="text-muted-foreground space-y-1"> + <div> + {t('dolceImport.descriptions.projectDocuments', { + newDocuments: status.newDocuments, + updatedDocuments: status.updatedDocuments + })} + </div> + {(status.newRevisions > 0 || status.updatedRevisions > 0) && ( + <div> + {t('dolceImport.descriptions.projectRevisions', { + newRevisions: status.newRevisions, + updatedRevisions: status.updatedRevisions + })} + </div> + )} + {(status.newAttachments > 0 || status.updatedAttachments > 0) && ( + <div> + {t('dolceImport.descriptions.projectAttachments', { + newAttachments: status.newAttachments, + updatedAttachments: status.updatedAttachments + })} + </div> + )} </div> ) : ( <div className="text-destructive">{t('dolceImport.status.statusCheckFailed')}</div> @@ -595,14 +679,28 @@ export function ImportFromDOLCEButton({ <div className="rounded-lg border p-4 space-y-3"> <div className="flex items-center justify-between text-sm"> <span>{t('dolceImport.labels.itemsToImport')}</span> - <span className="font-medium"> - {totalStats.newDocuments + totalStats.updatedDocuments} - </span> + <span className="font-medium">{totalChanges}</span> + </div> + + <div className="space-y-1 text-xs text-muted-foreground"> + {totalStats.newDocuments + totalStats.updatedDocuments > 0 && ( + <div> + • {t('dolceImport.labels.documents')}: {totalStats.newDocuments} {t('dolceImport.labels.new')}, {totalStats.updatedDocuments} {t('dolceImport.labels.updates')} + </div> + )} + {totalStats.newRevisions + totalStats.updatedRevisions > 0 && ( + <div> + • {t('dolceImport.labels.revisions')}: {totalStats.newRevisions} {t('dolceImport.labels.new')}, {totalStats.updatedRevisions} {t('dolceImport.labels.updates')} + </div> + )} + {totalStats.newAttachments + totalStats.updatedAttachments > 0 && ( + <div> + • {t('dolceImport.labels.attachments')}: {totalStats.newAttachments} {t('dolceImport.labels.new')}, {totalStats.updatedAttachments} {t('dolceImport.labels.updates')} + </div> + )} </div> <div className="text-xs text-muted-foreground"> - {t('dolceImport.descriptions.includesNewAndUpdated')} - <br /> {t('dolceImport.descriptions.b4DocumentsNote')} {projectIds.length > 1 && ( <> |
