From 2c02afd48a4d9276a4f5c132e088540a578d0972 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 30 Sep 2025 10:08:53 +0000 Subject: (대표님) 폼리스트, spreadjs 관련 변경사항, 벤더문서 뷰 쿼리 수정, 이메일 템플릿 추가 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/document-class-option-add-dialog.tsx | 161 +++++++- lib/evaluation-criteria/stat.ts | 413 +++++++++++++++++++++ lib/forms/stat.ts | 209 ++++++++++- lib/mail/templates/pq.hbs | 3 - lib/mail/templates/vendor-rejected.hbs | 195 ++++++++++ lib/projects/service.ts | 23 ++ lib/rfq-last/attachment/vendor-response-table.tsx | 1 - lib/rfq-last/service.ts | 87 ++++- lib/rfq-last/validations.ts | 2 +- lib/rfq-last/vendor/rfq-vendor-table.tsx | 8 +- lib/sedp/get-form-tags.ts | 147 +++++++- lib/tbe-last/service.ts | 2 +- .../ship/send-to-shi-button.tsx | 70 ++-- lib/vendors/table/request-pq-dialog.tsx | 13 + 14 files changed, 1265 insertions(+), 69 deletions(-) create mode 100644 lib/evaluation-criteria/stat.ts create mode 100644 lib/mail/templates/vendor-rejected.hbs (limited to 'lib') diff --git a/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx b/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx index 93681c09..31337675 100644 --- a/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx @@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { toast } from "sonner" import * as z from "zod" -import { Plus } from "lucide-react" +import { Plus, Check, ChevronsUpDown } from "lucide-react" import { Button } from "@/components/ui/button" import { @@ -25,12 +25,62 @@ import { FormLabel, FormMessage, } from "@/components/ui/form" -import { Input } from "@/components/ui/input" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { cn } from "@/lib/utils" +import { useParams } from "next/navigation" import { createDocumentClassOptionItem } from "@/lib/docu-list-rule/document-class/service" +import { getProjectCode } from "@/lib/projects/service" + +// API 응답 타입 +interface ScheduleSetting { + COL_NM: string + DC_OBX_USE_YN: string + PROJ_COL_NM: string + PROJ_COL_NM_EN: string + SCD_VIEW_MGNT: string + USE_YN1: string + USE_YN2: string +} + +// 프로젝트 일정 설정을 가져오는 함수 +async function getProjectKindScheduleSetting(projectCode: string): Promise { + try { + const response = await fetch( + `http://60.100.99.217/DDP/Services/VNDRService.svc/GetProjectKindScheduleSetting?PROJ_NO=${projectCode}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ) + + if (!response.ok) { + throw new Error('Failed to fetch schedule settings') + } + + const data = await response.json() + return data.GetProjectKindScheduleSettingResult || [] + } catch (error) { + console.error('Error fetching schedule settings:', error) + return [] + } +} const createOptionSchema = z.object({ - optionCode: z.string().min(1, "코드는 필수입니다."), + optionCode: z.string().min(1, "옵션을 선택해주세요."), }) type CreateOptionSchema = z.infer @@ -42,7 +92,12 @@ interface DocumentClassOptionAddDialogProps { export function DocumentClassOptionAddDialog({ documentClassId, onSuccess }: DocumentClassOptionAddDialogProps) { const [open, setOpen] = React.useState(false) + const [comboboxOpen, setComboboxOpen] = React.useState(false) const [isPending, startTransition] = React.useTransition() + const [scheduleSettings, setScheduleSettings] = React.useState([]) + const [isLoading, setIsLoading] = React.useState(false) + const params = useParams() + const projectId = Number(params?.projectId) const form = useForm({ resolver: zodResolver(createOptionSchema), @@ -51,6 +106,35 @@ export function DocumentClassOptionAddDialog({ documentClassId, onSuccess }: Doc }, }) + // Dialog가 열릴 때 데이터 로드 + React.useEffect(() => { + if (open && projectId) { + loadScheduleSettings() + } + }, [open, projectId]) + + const loadScheduleSettings = async () => { + setIsLoading(true) + try { + // 먼저 projectId로 프로젝트 코드 가져오기 + const projectCode = await getProjectCode(projectId) + + if (!projectCode) { + toast.error("프로젝트 코드를 찾을 수 없습니다.") + return + } + + // 프로젝트 코드로 일정 설정 가져오기 + const settings = await getProjectKindScheduleSetting(projectCode) + setScheduleSettings(settings) + } catch (error) { + console.error("Error loading schedule settings:", error) + toast.error("옵션 목록을 불러오는 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + const handleSubmit = (data: CreateOptionSchema) => { startTransition(async () => { try { @@ -79,6 +163,10 @@ export function DocumentClassOptionAddDialog({ documentClassId, onSuccess }: Doc form.reset() } + const selectedOption = scheduleSettings.find( + (setting) => setting.COL_NM === form.watch("optionCode") + ) + return ( @@ -100,11 +188,66 @@ export function DocumentClassOptionAddDialog({ documentClassId, onSuccess }: Doc control={form.control} name="optionCode" render={({ field }) => ( - - 코드 - - - + + 옵션 선택 + + + + + + + + + + + {isLoading ? "로딩 중..." : "검색 결과가 없습니다."} + + + {scheduleSettings.map((setting) => ( + { + form.setValue("optionCode", setting.COL_NM) + setComboboxOpen(false) + }} + > + +
+ {setting.COL_NM} + + {setting.PROJ_COL_NM} + +
+
+ ))} +
+
+
+
)} @@ -122,4 +265,4 @@ export function DocumentClassOptionAddDialog({ documentClassId, onSuccess }: Doc
) -} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/evaluation-criteria/stat.ts b/lib/evaluation-criteria/stat.ts new file mode 100644 index 00000000..c39c8627 --- /dev/null +++ b/lib/evaluation-criteria/stat.ts @@ -0,0 +1,413 @@ +"use server" + +import db from "@/db/db" +import { vendors, contracts, contractItems, forms, formEntries, formMetas, tags, tagClasses, tagClassAttributes, projects } from "@/db/schema" +import { eq, and, inArray } from "drizzle-orm" +import { getEditableFieldsByTag } from "./services" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" + +interface VendorFormStatus { + vendorId: number + vendorName: string + formCount: number // 벤더가 가진 form 개수 + tagCount: number // 벤더가 가진 tag 개수 + totalFields: number // 입력해야 하는 총 필드 개수 + completedFields: number // 입력 완료된 필드 개수 + completionRate: number // 완료율 (%) +} + +export interface FormStatusByVendor { + tagCount: number; + totalFields: number; + completedFields: number; + completionRate: number; + upcomingCount: number; // 7일 이내 임박한 개수 + overdueCount: number; // 지연된 개수 +} + +export async function getProjectsWithContracts() { + try { + const projectList = await db + .selectDistinct({ + id: projects.id, + projectCode: projects.code, + projectName: projects.name, + }) + .from(projects) + .innerJoin(contracts, eq(contracts.projectId, projects.id)) + .orderBy(projects.code) + + return projectList + } catch (error) { + console.error('Error getting projects with contracts:', error) + throw new Error('계약이 있는 프로젝트 조회 중 오류가 발생했습니다.') + } +} + + + +export async function getVendorFormStatus(projectId?: number): Promise { + try { + // 1. 벤더 조회 쿼리 수정 + const vendorList = projectId + ? await db + .selectDistinct({ + vendorId: vendors.id, + vendorName: vendors.vendorName, + }) + .from(vendors) + .innerJoin(contracts, eq(contracts.vendorId, vendors.id)) + .where(eq(contracts.projectId, projectId)) + : await db + .selectDistinct({ + vendorId: vendors.id, + vendorName: vendors.vendorName, + }) + .from(vendors) + .innerJoin(contracts, eq(contracts.vendorId, vendors.id)) + + + const vendorStatusList: VendorFormStatus[] = [] + + for (const vendor of vendorList) { + let vendorFormCount = 0 + let vendorTagCount = 0 + let vendorTotalFields = 0 + let vendorCompletedFields = 0 + const uniqueTags = new Set() + + // 2. 계약 조회 시 projectId 필터 추가 + const vendorContracts = projectId + ? await db + .select({ + id: contracts.id, + projectId: contracts.projectId + }) + .from(contracts) + .where( + and( + eq(contracts.vendorId, vendor.vendorId), + eq(contracts.projectId, projectId) + ) + ) + : await db + .select({ + id: contracts.id, + projectId: contracts.projectId + }) + .from(contracts) + .where(eq(contracts.vendorId, vendor.vendorId)) + + + for (const contract of vendorContracts) { + // 3. 계약별 contractItems 조회 + const contractItemsList = await db + .select({ + id: contractItems.id + }) + .from(contractItems) + .where(eq(contractItems.contractId, contract.id)) + + for (const contractItem of contractItemsList) { + // 4. contractItem별 forms 조회 + const formsList = await db + .select({ + id: forms.id, + formCode: forms.formCode, + contractItemId: forms.contractItemId + }) + .from(forms) + .where(eq(forms.contractItemId, contractItem.id)) + + vendorFormCount += formsList.length + + // 5. formEntries 조회 + const entriesList = await db + .select({ + id: formEntries.id, + formCode: formEntries.formCode, + data: formEntries.data + }) + .from(formEntries) + .where(eq(formEntries.contractItemId, contractItem.id)) + + // 6. TAG별 편집 가능 필드 조회 + const editableFieldsByTag = await getEditableFieldsByTag(contractItem.id, contract.projectId) + + for (const entry of entriesList) { + // formMetas에서 해당 formCode의 columns 조회 + const metaResult = await db + .select({ + columns: formMetas.columns + }) + .from(formMetas) + .where( + and( + eq(formMetas.formCode, entry.formCode), + eq(formMetas.projectId, contract.projectId) + ) + ) + .limit(1) + + if (metaResult.length === 0) continue + + const metaColumns = metaResult[0].columns as any[] + + // shi가 'IN' 또는 'BOTH'인 필드 찾기 + const inputRequiredFields = metaColumns + .filter(col => col.shi === 'IN' || col.shi === 'BOTH') + .map(col => col.key) + + // entry.data 분석 (배열로 가정) + const dataArray = Array.isArray(entry.data) ? entry.data : [] + + for (const dataItem of dataArray) { + if (typeof dataItem !== 'object' || !dataItem) continue + + const tagNo = dataItem.TAG_NO + if (tagNo) { + uniqueTags.add(tagNo) + + // TAG별 편집 가능 필드 가져오기 + const tagEditableFields = editableFieldsByTag.get(tagNo) || [] + + // 최종 입력 필요 필드 = shi 기반 필드 + TAG 기반 편집 가능 필드 + const allRequiredFields = inputRequiredFields.filter(field => + tagEditableFields.includes(field) + ) + // 각 필드별 입력 상태 체크 + for (const fieldKey of allRequiredFields) { + vendorTotalFields++ + + const fieldValue = dataItem[fieldKey] + // 값이 있고, 빈 문자열이 아니고, null이 아니면 입력 완료 + if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { + vendorCompletedFields++ + } + } + } + } + } + } + } + + // 완료율 계산 + const completionRate = vendorTotalFields > 0 + ? Math.round((vendorCompletedFields / vendorTotalFields) * 100 * 10) / 10 + : 0 + + vendorStatusList.push({ + vendorId: vendor.vendorId, + vendorName: vendor.vendorName || '이름 없음', + formCount: vendorFormCount, + tagCount: uniqueTags.size, + totalFields: vendorTotalFields, + completedFields: vendorCompletedFields, + completionRate + }) + } + + return vendorStatusList + + } catch (error) { + console.error('Error getting vendor form status:', error) + throw new Error('벤더별 Form 입력 현황 조회 중 오류가 발생했습니다.') + } +} + + + +export async function getFormStatusByVendor(projectId: number, formCode: string): Promise { + 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 + let vendorTotalFields = 0 + let vendorCompletedFields = 0 + let vendorUpcomingCount = 0 // 7일 이내 임박한 개수 + let vendorOverdueCount = 0 // 지연된 개수 + const uniqueTags = new Set() + const processedTags = new Set() // 중복 처리 방지용 + + // 현재 날짜와 7일 후 날짜 계산 + const today = new Date() + today.setHours(0, 0, 0, 0) // 시간 부분 제거 + const sevenDaysLater = new Date(today) + sevenDaysLater.setDate(sevenDaysLater.getDate() + 7) + + // 4. contractItem별 forms 조회 + const formsList = await db + .select({ + id: forms.id, + formCode: forms.formCode, + contractItemId: forms.contractItemId + }) + .from(forms) + .where( + and( + inArray(forms.contractItemId, contractItemIds), + eq(forms.formCode, formCode) + ) + ) + + vendorFormCount += formsList.length + + // 5. formEntries 조회 + const entriesList = await db + .select({ + id: formEntries.id, + formCode: formEntries.formCode, + data: formEntries.data + }) + .from(formEntries) + .where( + and( + inArray(formEntries.contractItemId, contractItemIds), + eq(formEntries.formCode, formCode) + ) + ) + + // 6. TAG별 편집 가능 필드 조회 + const editableFieldsByTag = new Map() + + 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) + } + }) + } + + for (const entry of entriesList) { + const metaResult = await db + .select({ + columns: formMetas.columns + }) + .from(formMetas) + .where( + and( + eq(formMetas.formCode, entry.formCode), + eq(formMetas.projectId, projectId) + ) + ) + .limit(1) + + if (metaResult.length === 0) continue + + const metaColumns = metaResult[0].columns as any[] + + const inputRequiredFields = metaColumns + .filter(col => col.shi === 'IN' || col.shi === 'BOTH') + .map(col => col.key) + + const dataArray = Array.isArray(entry.data) ? entry.data : [] + + for (const dataItem of dataArray) { + if (typeof dataItem !== 'object' || !dataItem) continue + + const tagNo = dataItem.TAG_NO + if (tagNo) { + uniqueTags.add(tagNo) + + // TAG별 편집 가능 필드 가져오기 + const tagEditableFields = editableFieldsByTag.get(tagNo) || [] + + const allRequiredFields = inputRequiredFields.filter(field => + tagEditableFields.includes(field) + ) + + // 해당 TAG의 필드 완료 상태 체크 + let tagHasIncompleteFields = false + + for (const fieldKey of allRequiredFields) { + vendorTotalFields++ + + const fieldValue = dataItem[fieldKey] + if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { + vendorCompletedFields++ + } else { + tagHasIncompleteFields = true + } + } + + // 미완료 TAG에 대해서만 날짜 체크 (TAG당 한 번만 처리) + if (!processedTags.has(tagNo) && tagHasIncompleteFields) { + processedTags.add(tagNo) + + const targetDate = dataItem.target_date + if (targetDate) { + const target = new Date(targetDate) + target.setHours(0, 0, 0, 0) // 시간 부분 제거 + + if (target < today) { + // 미완료이면서 지연된 경우 (오늘보다 이전) + vendorOverdueCount++ + } else if (target >= today && target <= sevenDaysLater) { + // 미완료이면서 7일 이내 임박한 경우 + vendorUpcomingCount++ + } + } + } + } + } + } + + // 완료율 계산 + const completionRate = vendorTotalFields > 0 + ? Math.round((vendorCompletedFields / vendorTotalFields) * 100 * 10) / 10 + : 0 + + vendorStatusList.push({ + tagCount: uniqueTags.size, + totalFields: vendorTotalFields, + completedFields: vendorCompletedFields, + completionRate, + upcomingCount: vendorUpcomingCount, + overdueCount: vendorOverdueCount + }) + + return vendorStatusList + + } catch (error) { + console.error('Error getting vendor form status:', error) + throw new Error('벤더별 Form 입력 현황 조회 중 오류가 발생했습니다.') + } +} \ No newline at end of file diff --git a/lib/forms/stat.ts b/lib/forms/stat.ts index 80193c48..054f2462 100644 --- a/lib/forms/stat.ts +++ b/lib/forms/stat.ts @@ -2,8 +2,10 @@ import db from "@/db/db" import { vendors, contracts, contractItems, forms, formEntries, formMetas, tags, tagClasses, tagClassAttributes, projects } from "@/db/schema" -import { eq, and } from "drizzle-orm" +import { eq, and, inArray } from "drizzle-orm" import { getEditableFieldsByTag } from "./services" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" interface VendorFormStatus { vendorId: number @@ -15,6 +17,15 @@ interface VendorFormStatus { completionRate: number // 완료율 (%) } +export interface FormStatusByVendor { + tagCount: number; + totalFields: number; + completedFields: number; + completionRate: number; + upcomingCount: number; // 7일 이내 임박한 개수 + overdueCount: number; // 지연된 개수 +} + export async function getProjectsWithContracts() { try { const projectList = await db @@ -204,3 +215,199 @@ export async function getVendorFormStatus(projectId?: number): Promise { + 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 + let vendorTotalFields = 0 + let vendorCompletedFields = 0 + let vendorUpcomingCount = 0 // 7일 이내 임박한 개수 + let vendorOverdueCount = 0 // 지연된 개수 + const uniqueTags = new Set() + const processedTags = new Set() // 중복 처리 방지용 + + // 현재 날짜와 7일 후 날짜 계산 + const today = new Date() + today.setHours(0, 0, 0, 0) // 시간 부분 제거 + const sevenDaysLater = new Date(today) + sevenDaysLater.setDate(sevenDaysLater.getDate() + 7) + + // 4. contractItem별 forms 조회 + const formsList = await db + .select({ + id: forms.id, + formCode: forms.formCode, + contractItemId: forms.contractItemId + }) + .from(forms) + .where( + and( + inArray(forms.contractItemId, contractItemIds), + eq(forms.formCode, formCode) + ) + ) + + vendorFormCount += formsList.length + + // 5. formEntries 조회 + const entriesList = await db + .select({ + id: formEntries.id, + formCode: formEntries.formCode, + data: formEntries.data + }) + .from(formEntries) + .where( + and( + inArray(formEntries.contractItemId, contractItemIds), + eq(formEntries.formCode, formCode) + ) + ) + + // 6. TAG별 편집 가능 필드 조회 + const editableFieldsByTag = new Map() + + 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) + } + }) + } + + for (const entry of entriesList) { + const metaResult = await db + .select({ + columns: formMetas.columns + }) + .from(formMetas) + .where( + and( + eq(formMetas.formCode, entry.formCode), + eq(formMetas.projectId, projectId) + ) + ) + .limit(1) + + if (metaResult.length === 0) continue + + const metaColumns = metaResult[0].columns as any[] + + const inputRequiredFields = metaColumns + .filter(col => col.shi === 'IN' || col.shi === 'BOTH') + .map(col => col.key) + + const dataArray = Array.isArray(entry.data) ? entry.data : [] + + for (const dataItem of dataArray) { + if (typeof dataItem !== 'object' || !dataItem) continue + + const tagNo = dataItem.TAG_NO + if (tagNo) { + uniqueTags.add(tagNo) + + // TAG별 편집 가능 필드 가져오기 + const tagEditableFields = editableFieldsByTag.get(tagNo) || [] + + const allRequiredFields = inputRequiredFields.filter(field => + tagEditableFields.includes(field) + ) + + // 해당 TAG의 필드 완료 상태 체크 + let tagHasIncompleteFields = false + + for (const fieldKey of allRequiredFields) { + vendorTotalFields++ + + const fieldValue = dataItem[fieldKey] + if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { + vendorCompletedFields++ + } else { + tagHasIncompleteFields = true + } + } + + // 미완료 TAG에 대해서만 날짜 체크 (TAG당 한 번만 처리) + if (!processedTags.has(tagNo) && tagHasIncompleteFields) { + processedTags.add(tagNo) + + const targetDate = dataItem.DUE_DATE + if (targetDate) { + const target = new Date(targetDate) + target.setHours(0, 0, 0, 0) // 시간 부분 제거 + + if (target < today) { + // 미완료이면서 지연된 경우 (오늘보다 이전) + vendorOverdueCount++ + } else if (target >= today && target <= sevenDaysLater) { + // 미완료이면서 7일 이내 임박한 경우 + vendorUpcomingCount++ + } + } + } + } + } + } + + // 완료율 계산 + const completionRate = vendorTotalFields > 0 + ? Math.round((vendorCompletedFields / vendorTotalFields) * 100 * 10) / 10 + : 0 + + vendorStatusList.push({ + tagCount: uniqueTags.size, + totalFields: vendorTotalFields, + completedFields: vendorCompletedFields, + completionRate, + upcomingCount: vendorUpcomingCount, + overdueCount: vendorOverdueCount + }) + + return vendorStatusList + + } catch (error) { + console.error('Error getting vendor form status:', error) + throw new Error('벤더별 Form 입력 현황 조회 중 오류가 발생했습니다.') + } +} \ No newline at end of file diff --git a/lib/mail/templates/pq.hbs b/lib/mail/templates/pq.hbs index 02523696..abdff056 100644 --- a/lib/mail/templates/pq.hbs +++ b/lib/mail/templates/pq.hbs @@ -61,9 +61,6 @@ 아래의 해당 링크를 통해 당사 eVCP시스템에 접속하시어 요청드린 PQ 항목 및 자료에 대한 제출 요청드립니다.

-

- 별도의 견적을 제출하시어 당사에서 적극 검토할 수 있도록 협조 바랍니다. -

귀사의 제출 자료 및 정보는 아래의 제출 마감일 이전에 당사로 제출 되어야 하며, diff --git a/lib/mail/templates/vendor-rejected.hbs b/lib/mail/templates/vendor-rejected.hbs new file mode 100644 index 00000000..22a1d6f3 --- /dev/null +++ b/lib/mail/templates/vendor-rejected.hbs @@ -0,0 +1,195 @@ + + + + + + eVCP 메일 + + + +

+ + diff --git a/lib/projects/service.ts b/lib/projects/service.ts index 3f562e20..4685fce4 100644 --- a/lib/projects/service.ts +++ b/lib/projects/service.ts @@ -112,4 +112,27 @@ export async function getAllProjectInfoByProjectCode(projectCode: string) { .from(projects) .where(eq(projects.code, projectCode)) .limit(1); +} + +/** + * projectId로 프로젝트 코드를 가져오는 함수 + * @param projectId - 프로젝트 ID + * @returns 프로젝트 코드 또는 null + */ +export async function getProjectCode(projectId: number): Promise { + try { + const project = await db.project.findUnique({ + where: { + id: projectId, + }, + select: { + code: true, + }, + }) + + return project?.code || null + } catch (error) { + console.error("Error fetching project code:", error) + return null + } } \ No newline at end of file diff --git a/lib/rfq-last/attachment/vendor-response-table.tsx b/lib/rfq-last/attachment/vendor-response-table.tsx index 8be5210f..076fb153 100644 --- a/lib/rfq-last/attachment/vendor-response-table.tsx +++ b/lib/rfq-last/attachment/vendor-response-table.tsx @@ -159,7 +159,6 @@ export function VendorResponseTable({ const [isUpdating, setIsUpdating] = React.useState(false); const [showTypeDialog, setShowTypeDialog] = React.useState(false); const [selectedType, setSelectedType] = React.useState<"구매" | "설계" | "">(""); - console.log(data,"data") const [selectedVendor, setSelectedVendor] = React.useState(null); diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 8eed9bee..09d707d7 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -3643,7 +3643,7 @@ async function handleTbeSession({ sessionCode: sessionCode, sessionTitle: `${rfqData.rfqCode} - ${vendor.vendorName} 기술검토`, sessionType: "initial", - status: "준비중", + status: "생성중", evaluationResult: null, plannedStartDate: rfqData.dueDate ? addDays(new Date(rfqData.dueDate), 1) @@ -4738,13 +4738,12 @@ export async function updateShortList( // 트랜잭션으로 처리 const result = await db.transaction(async (tx) => { - // 해당 RFQ의 모든 벤더들의 shortList를 먼저 false로 설정 (선택적) - // 만약 선택된 것만 true로 하고 나머지는 그대로 두려면 이 부분 제거 + // 1. 해당 RFQ의 모든 벤더들의 shortList를 먼저 false로 설정 await tx .update(rfqLastDetails) .set({ shortList: false, - updatedBy: session.user.id, + updatedBy: Number(session.user.id), updatedAt: new Date() }) .where( @@ -4754,15 +4753,16 @@ export async function updateShortList( ) ); - // 선택된 벤더들의 shortList를 true로 설정 + // 2. 선택된 벤더들 처리 if (vendorIds.length > 0) { - const updates = await Promise.all( + // 2-1. 선택된 벤더들의 shortList를 true로 설정 + const updatedDetails = await Promise.all( vendorIds.map(vendorId => tx .update(rfqLastDetails) .set({ shortList: shortListStatus, - updatedBy: session.user.id, + updatedBy: Number(session.user.id), updatedAt: new Date() }) .where( @@ -4776,17 +4776,84 @@ export async function updateShortList( ) ); + // 2-2. TBE 세션 처리 (shortList가 true인 경우에만) + if (shortListStatus) { + // 각 벤더에 대한 rfqLastDetailsId 추출 + const detailsMap = new Map( + updatedDetails.flat().map(detail => [detail.vendorsId, detail.id]) + ); + + // TBE 세션 생성 또는 업데이트 + await Promise.all( + vendorIds.map(async (vendorId) => { + const rfqLastDetailsId = detailsMap.get(vendorId); + + if (!rfqLastDetailsId) { + console.warn(`rfqLastDetailsId not found for vendorId: ${vendorId}`); + return; + } + + // 기존 활성 TBE 세션이 있는지 확인 + const existingSession = await tx + .select() + .from(rfqLastTbeSessions) + .where( + and( + eq(rfqLastTbeSessions.rfqsLastId, rfqId), + eq(rfqLastTbeSessions.vendorId, vendorId), + inArray(rfqLastTbeSessions.status, ["생성중", "준비중", "진행중", "검토중", "보류"]) + ) + ) + .limit(1); + + if (existingSession.length > 0) { + // 기존 세션이 있으면 상태 업데이트 + await tx + .update(rfqLastTbeSessions) + .set({ + status: "준비중", + updatedBy: session.user.id, + updatedAt: new Date() + }) + .where(eq(rfqLastTbeSessions.id, existingSession[0].id)); + } + }) + ); + } else { + // shortList가 false인 경우, 해당 벤더들의 활성 TBE 세션을 취소 상태로 변경 + await Promise.all( + vendorIds.map(vendorId => + tx + .update(rfqLastTbeSessions) + .set({ + status: "취소", + updatedBy: Number(session.user.id), + updatedAt: new Date() + }) + .where( + and( + eq(rfqLastTbeSessions.rfqsLastId, rfqId), + eq(rfqLastTbeSessions.vendorId, vendorId), + inArray(rfqLastTbeSessions.status, ["생성중", "준비중", "진행중", "검토중", "보류"]) + ) + ) + ) + ); + } + return { success: true, - updatedCount: updates.length, - vendorIds + updatedCount: updatedDetails.length, + vendorIds, + tbeSessionsUpdated: shortListStatus }; } return { success: true, updatedCount: 0, - vendorIds: [] + vendorIds: [], + tbeSessionsUpdated: false }; }); diff --git a/lib/rfq-last/validations.ts b/lib/rfq-last/validations.ts index 5615db7a..6a5816d4 100644 --- a/lib/rfq-last/validations.ts +++ b/lib/rfq-last/validations.ts @@ -56,7 +56,7 @@ import { RfqLastAttachments } from "@/db/schema"; search: parseAsString.withDefault(""), // RFQ 카테고리 (전체/일반견적/ITB/RFQ) - rfqCategory: parseAsStringEnum(["all", "general", "itb", "rfq"]).withDefault("all"), + rfqCategory: parseAsStringEnum(["all", "general", "itb", "rfq"]), }); // ============= 타입 정의 ============= diff --git a/lib/rfq-last/vendor/rfq-vendor-table.tsx b/lib/rfq-last/vendor/rfq-vendor-table.tsx index 89a42602..17433773 100644 --- a/lib/rfq-last/vendor/rfq-vendor-table.tsx +++ b/lib/rfq-last/vendor/rfq-vendor-table.tsx @@ -360,7 +360,7 @@ export function RfqVendorTable({ // 선택된 벤더 ID들 추출 const selectedVendorIds = rfqCode?.startsWith("I") ? selectedRows - .filter(v => v.shortList) + // .filter(v => v.shortList) .map(row => row.vendorId) .filter(id => id != null) : selectedRows @@ -1218,7 +1218,7 @@ export function RfqVendorTable({ }, size: 80, }, - ...(rfqCode?.startsWith("I") ? [{ + ...(!rfqCode?.startsWith("F") ? [{ accessorKey: "shortList", filterFn: createFilterFn("boolean"), // boolean으로 변경 header: ({ column }) => , @@ -1482,7 +1482,7 @@ export function RfqVendorTable({ label: "스페어파트", type: "boolean" }, - ...(rfqCode?.startsWith("I") ? [{ + ...(!rfqCode?.startsWith("I") ? [{ id: "shortList", label: "Short List", type: "select", @@ -1577,7 +1577,7 @@ export function RfqVendorTable({ {/* Short List 확정 버튼 */} - {rfqCode?.startsWith("I") && + {!rfqCode?.startsWith("F") && - +
@@ -289,16 +289,16 @@ export function SendToSHIButton({ )}
- +
{t('shiSync.labels.overallStatus')} {getSyncStatusBadge()}
- +
- {t('shiSync.descriptions.targetInfo', { - contractCount: documentsContractIds.length, - targetSystem + {t('shiSync.descriptions.targetInfo', { + contractCount: documentsContractIds.length, + targetSystem })}
@@ -311,8 +311,8 @@ export function SendToSHIButton({ {t('shiSync.descriptions.statusCheckError')} {process.env.NODE_ENV === 'development' && (
- Debug: {t('shiSync.descriptions.contractsWithError', { - count: contractStatuses.filter(({ error }) => error).length + Debug: {t('shiSync.descriptions.contractsWithError', { + count: contractStatuses.filter(({ error }) => error).length })}
)} @@ -324,7 +324,7 @@ export function SendToSHIButton({ {!totalStats.hasError && documentsContractIds.length > 0 && (
- +
{t('shiSync.labels.pending')}
@@ -409,7 +409,7 @@ export function SendToSHIButton({ )} - +
- +
{t('shiSync.labels.targetContracts')} {t('shiSync.labels.contractCount', { count: documentsContractIds.length })}
- +
{t('shiSync.descriptions.includesChanges')}
diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx index 206846df..fd6da145 100644 --- a/lib/vendors/table/request-pq-dialog.tsx +++ b/lib/vendors/table/request-pq-dialog.tsx @@ -709,9 +709,22 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro date={dueDate ? new Date(dueDate) : undefined} onSelect={(date?: Date) => { if (date) { + // 현재 날짜 기준으로 이전 날짜는 선택 불가능 + const today = new Date() + today.setHours(0, 0, 0, 0) // 오늘 날짜의 시작 시간으로 설정 + + const selectedDate = new Date(date) + selectedDate.setHours(0, 0, 0, 0) // 선택된 날짜의 시작 시간으로 설정 + + if (selectedDate < today) { + toast.error("마감일은 오늘 날짜 이후로 선택해주세요.") + return + } else { + // 한국 시간대로 날짜 변환 (UTC 변환으로 인한 날짜 변경 방지) const kstDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000) setDueDate(kstDate.toISOString().slice(0, 10)) + } } else { setDueDate("") } -- cgit v1.2.3