From 969c25b56f6d29d7ffa4bc2ce04c5fb4e5846b34 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 14 Aug 2025 11:54:47 +0000 Subject: (대표님) 정규벤더등록, 벤더문서관리, 벤더데이터입력, 첨부파일관리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document-status-dialog.tsx | 213 ++++--- .../registration-request-dialog.tsx | 691 +++++++++++++++++++++ 2 files changed, 827 insertions(+), 77 deletions(-) create mode 100644 components/vendor-regular-registrations/registration-request-dialog.tsx (limited to 'components/vendor-regular-registrations') diff --git a/components/vendor-regular-registrations/document-status-dialog.tsx b/components/vendor-regular-registrations/document-status-dialog.tsx index f8e2e1cd..db3defe6 100644 --- a/components/vendor-regular-registrations/document-status-dialog.tsx +++ b/components/vendor-regular-registrations/document-status-dialog.tsx @@ -8,13 +8,12 @@ import { } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { FileText, Download, CheckCircle, XCircle, Clock } from "lucide-react"; +import { FileText, Download, CheckCircle, XCircle, Clock, RefreshCw } from "lucide-react"; import { toast } from "sonner"; import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"; import { documentStatusColumns, - contractAgreementColumns, } from "@/config/vendorRegularRegistrationsColumnsConfig"; import { downloadFile } from "@/lib/file-download"; @@ -22,6 +21,7 @@ interface DocumentStatusDialogProps { open: boolean; onOpenChange: (open: boolean) => void; registration: VendorRegularRegistration | null; + onRefresh?: () => void; } const StatusIcon = ({ status }: { status: string | boolean }) => { @@ -68,63 +68,87 @@ export function DocumentStatusDialog({ open, onOpenChange, registration, + onRefresh, }: DocumentStatusDialogProps) { if (!registration) return null; + // 디버깅: registration 데이터 확인 + console.log(`📋 DocumentStatusDialog - Partners 등록 데이터:`, { + companyName: registration.companyName, + businessNumber: registration.businessNumber, + representative: registration.representative, + safetyQualificationContent: registration.safetyQualificationContent, + basicContractsLength: registration.basicContracts?.length || 0, + additionalInfo: registration.additionalInfo + }); + // 파일 다운로드 핸들러 - const handleFileDownload = async (docKey: string, fileIndex: number = 0) => { - try { - const files = registration.documentFiles[docKey as keyof typeof registration.documentFiles]; - if (!files || files.length === 0) { - toast.error("다운로드할 파일이 없습니다."); - return; - } + // const handleFileDownload = async (docKey: string, fileIndex: number = 0) => { + // try { + // const files = registration.documentFiles[docKey as keyof typeof registration.documentFiles]; + // if (!files || files.length === 0) { + // toast.error("다운로드할 파일이 없습니다."); + // return; + // } - const file = files[fileIndex]; - if (!file) { - toast.error("파일을 찾을 수 없습니다."); - return; - } + // const file = files[fileIndex]; + // if (!file) { + // toast.error("파일을 찾을 수 없습니다."); + // return; + // } - // filePath와 fileName 추출 - const filePath = file.filePath || file.path; - const fileName = file.originalFileName || file.fileName || file.name; + // // filePath와 fileName 추출 + // const filePath = file.filePath || file.path; + // const fileName = file.originalFileName || file.fileName || file.name; - if (!filePath || !fileName) { - toast.error("파일 정보가 올바르지 않습니다."); - return; - } + // if (!filePath || !fileName) { + // toast.error("파일 정보가 올바르지 않습니다."); + // return; + // } - console.log(`📥 파일 다운로드 시작:`, { filePath, fileName, docKey }); + // console.log(`📥 파일 다운로드 시작:`, { filePath, fileName, docKey }); - // downloadFile 함수를 사용하여 파일 다운로드 - const result = await downloadFile(filePath, fileName, { - showToast: true, - onError: (error) => { - console.error("파일 다운로드 오류:", error); - toast.error(`파일 다운로드 실패: ${error}`); - }, - onSuccess: (fileName, fileSize) => { - console.log(`✅ 파일 다운로드 성공:`, { fileName, fileSize }); - } - }); + // // downloadFile 함수를 사용하여 파일 다운로드 + // const result = await downloadFile(filePath, fileName, { + // showToast: true, + // onError: (error) => { + // console.error("파일 다운로드 오류:", error); + // toast.error(`파일 다운로드 실패: ${error}`); + // }, + // onSuccess: (fileName, fileSize) => { + // console.log(`✅ 파일 다운로드 성공:`, { fileName, fileSize }); + // } + // }); - if (!result.success) { - console.error("파일 다운로드 실패:", result.error); - } - } catch (error) { - console.error("파일 다운로드 중 오류 발생:", error); - toast.error("파일 다운로드 중 오류가 발생했습니다."); - } - }; + // if (!result.success) { + // console.error("파일 다운로드 실패:", result.error); + // } + // } catch (error) { + // console.error("파일 다운로드 중 오류 발생:", error); + // toast.error("파일 다운로드 중 오류가 발생했습니다."); + // } + // }; return ( - - - 문서/자료 접수 현황 - {registration.companyName} + +
+ + 문서/자료 접수 현황 - {registration.companyName} +
+ {onRefresh && ( + + )}
@@ -160,12 +184,18 @@ export function DocumentStatusDialog({
상태
제출일자
액션
- + {documentStatusColumns.map((doc) => { const isSubmitted = registration.documentSubmissions[ doc.key as keyof typeof registration.documentSubmissions ] as boolean; + // 내자인 경우 통장사본은 표시하지 않음 + const isForeign = registration.country !== 'KR'; + if (doc.key === 'bankCopy' && !isForeign) { + return null; + } + return (
{doc.label} + {doc.key === 'bankCopy' && isForeign && ( + (외자 필수) + )}
@@ -182,7 +215,7 @@ export function DocumentStatusDialog({ {isSubmitted ? "2024.01.01" : "-"}
- {isSubmitted && ( + {/* {isSubmitted && ( - )} + )} */}
); @@ -211,37 +244,63 @@ export function DocumentStatusDialog({
서약일자
액션
- {contractAgreementColumns.map((agreement) => { - const status = registration.contractAgreements[ - agreement.key as keyof typeof registration.contractAgreements - ] as string; - - return ( -
-
- - {agreement.label} -
-
- -
-
- {status === "completed" ? "2024.01.01" : "-"} -
-
- {status === "completed" && ( - - )} + {!registration.basicContracts || registration.basicContracts.length === 0 ? ( +
+ 요청된 기본계약이 없습니다. +
+ ) : ( + registration.basicContracts.map((contract, index) => { + const isCompleted = contract.status === "COMPLETED"; + + return ( +
+
+ + {contract.templateName || "템플릿명 없음"} +
+
+ +
+
+ {isCompleted && contract.createdAt + ? new Intl.DateTimeFormat('ko-KR').format(new Date(contract.createdAt)) + : "-" + } +
+
+ {isCompleted && ( + + )} +
-
- ); - })} + ); + }) + )} +
+ + + {/* 안전적격성 평가 */} +
+

안전적격성 평가

+
+
+
+ + 안전적격성 평가 +
+ +
+ {registration.safetyQualificationContent && ( +
+

{registration.safetyQualificationContent}

+
+ )}
diff --git a/components/vendor-regular-registrations/registration-request-dialog.tsx b/components/vendor-regular-registrations/registration-request-dialog.tsx new file mode 100644 index 00000000..2a79189a --- /dev/null +++ b/components/vendor-regular-registrations/registration-request-dialog.tsx @@ -0,0 +1,691 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { toast } from "sonner"; +import { FileText, Building2, User, Phone, Mail } from "lucide-react"; + +import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"; +import { fetchRegistrationRequestData } from "@/lib/vendor-regular-registrations/service"; + +interface RegistrationRequestDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + registration: VendorRegularRegistration | null; + onSubmit?: (data: RegistrationRequestData) => void; +} + +export interface RegistrationRequestData { + // 업체정보 + companyNameKor: string; + companyNameEng: string; + businessNumber: string; + corporateNumber: string; + majorItems: string; + establishmentDate: string; + + // 소재지 + headOfficeAddress: string; + headOfficePhone: string; + factoryAddress: string; + factoryPhone: string; + salesOfficeAddress: string; + salesOfficePhone: string; + + // 대표자 정보 + representativeNameKor: string; + representativeNameEng: string; + representativeContact: string; + representativeBirthDate: string; + representativeEmail: string; + isWorkingAtCompany: boolean; + isInternalPartner: boolean; + + // 대표자 경력 + representativeCareer: Array<{ + date: string; + content: string; + }>; + + // 업무담당자 + businessContacts: { + sales: { name: string; position: string; department: string; responsibility: string; email: string; }; + design: { name: string; position: string; department: string; responsibility: string; email: string; }; + delivery: { name: string; position: string; department: string; responsibility: string; email: string; }; + quality: { name: string; position: string; department: string; responsibility: string; email: string; }; + taxInvoice: { name: string; position: string; department: string; responsibility: string; email: string; }; + }; +} + +export function RegistrationRequestDialog({ + open, + onOpenChange, + registration, + onSubmit, +}: RegistrationRequestDialogProps) { + const [loading, setLoading] = useState(false); + const [dataLoading, setDataLoading] = useState(false); + const [formData, setFormData] = useState({ + // 업체정보 (기본값 설정) + companyNameKor: "", + companyNameEng: "", + businessNumber: "", + corporateNumber: "", + majorItems: "", + establishmentDate: "", + + // 소재지 + headOfficeAddress: "", + headOfficePhone: "", + factoryAddress: "", + factoryPhone: "", + salesOfficeAddress: "", + salesOfficePhone: "", + + // 대표자 정보 + representativeNameKor: "", + representativeNameEng: "", + representativeContact: "", + representativeBirthDate: "", + representativeEmail: "", + isWorkingAtCompany: false, + isInternalPartner: false, + + // 대표자 경력 + representativeCareer: [{ date: "", content: "" }], + + // 업무담당자 + businessContacts: { + sales: { name: "", position: "", department: "", responsibility: "", email: "" }, + design: { name: "", position: "", department: "", responsibility: "", email: "" }, + delivery: { name: "", position: "", department: "", responsibility: "", email: "" }, + quality: { name: "", position: "", department: "", responsibility: "", email: "" }, + taxInvoice: { name: "", position: "", department: "", responsibility: "", email: "" }, + }, + }); + + // 기존 데이터 로드 및 자동 입력 + useEffect(() => { + if (open && registration?.id) { + loadExistingData(); + } + }, [open, registration?.id]); + + const loadExistingData = async () => { + if (!registration?.id) return; + + setDataLoading(true); + try { + const result = await fetchRegistrationRequestData(registration.id); + if (result.success && result.data) { + const { vendor, businessContacts, additionalInfo } = result.data; + + // 업무담당자 데이터 변환 + const contactsMap = { + sales: { name: "", position: "", department: "", responsibility: "", email: "" }, + design: { name: "", position: "", department: "", responsibility: "", email: "" }, + delivery: { name: "", position: "", department: "", responsibility: "", email: "" }, + quality: { name: "", position: "", department: "", responsibility: "", email: "" }, + taxInvoice: { name: "", position: "", department: "", responsibility: "", email: "" }, + }; + + console.log("🔍 업무담당자 데이터 매핑:", { + businessContacts: businessContacts.map(c => ({ + contactType: c.contactType, + contactName: c.contactName + })), + contactsMapKeys: Object.keys(contactsMap) + }); + + businessContacts.forEach(contact => { + let mappedType = contact.contactType; + + // DB의 snake_case를 camelCase로 변환 + if (contact.contactType === 'tax_invoice') { + mappedType = 'taxInvoice'; + } + + if (mappedType in contactsMap) { + contactsMap[mappedType as keyof typeof contactsMap] = { + name: contact.contactName || "", + position: contact.position || "", + department: contact.department || "", + responsibility: contact.responsibility || "", + email: contact.email || "", + }; + + console.log(`✅ 매핑 성공: ${contact.contactType} -> ${mappedType}`, { + name: contact.contactName, + position: contact.position + }); + } else { + console.warn(`❌ 매핑 실패: ${contact.contactType} -> ${mappedType} not found in contactsMap`); + } + }); + + // 날짜 포맷팅 함수 + const formatDate = (date: Date | string | null) => { + if (!date) return ""; + try { + const d = new Date(date); + // Invalid Date 체크 + if (isNaN(d.getTime())) return ""; + return d.toISOString().split('T')[0]; // YYYY-MM-DD 형식 + } catch (error) { + console.warn("날짜 포맷팅 오류:", date, error); + return ""; + } + }; + + // 폼 데이터 업데이트 + setFormData({ + // 업체정보 + companyNameKor: vendor.vendorName || "", + companyNameEng: "", // TODO: 영문명이 있다면 추가 + businessNumber: vendor.taxId || "", + corporateNumber: vendor.corporateRegistrationNumber || "", + majorItems: registration.majorItems || "", + establishmentDate: "", // vendors 테이블에 establishmentDate 필드가 없음 + + // 소재지 (vendors 테이블에는 address, phone만 있음) + headOfficeAddress: vendor.address || "", + headOfficePhone: vendor.phone || "", + factoryAddress: "", // vendors 테이블에 공장주소 필드가 없음 + factoryPhone: "", // vendors 테이블에 공장전화 필드가 없음 + salesOfficeAddress: "", // vendors 테이블에 영업소주소 필드가 없음 + salesOfficePhone: "", // vendors 테이블에 영업소전화 필드가 없음 + + // 대표자 정보 + representativeNameKor: vendor.representativeName || "", + representativeNameEng: "", // vendors 테이블에 영문명 필드가 없음 + representativeContact: vendor.representativePhone || "", + representativeBirthDate: vendor.representativeBirth || "", // 문자열로 직접 사용 + representativeEmail: vendor.representativeEmail || "", + isWorkingAtCompany: vendor.representativeWorkExpirence || false, + isInternalPartner: false, // vendors 테이블에 해당 필드가 없음 + + // 대표자 경력 (기본값 유지) + representativeCareer: [{ date: "", content: "" }], + + // 업무담당자 + businessContacts: contactsMap, + }); + + console.log("✅ 기존 데이터 로드 완료:", { + vendor: vendor.vendorName, + contactsCount: businessContacts.length, + hasAdditionalInfo: !!additionalInfo, + vendorData: { + representativeBirth: vendor.representativeBirth, + representativeBirthType: typeof vendor.representativeBirth, + createdAt: vendor.createdAt, + createdAtType: typeof vendor.createdAt + } + }); + + } else { + toast.error(result.error || "데이터 로드 실패"); + } + } catch (error) { + console.error("데이터 로드 오류:", error); + toast.error("데이터 로드 중 오류가 발생했습니다."); + } finally { + setDataLoading(false); + } + }; + + if (!registration) return null; + + const handleInputChange = (field: string, value: any) => { + setFormData(prev => ({ + ...prev, + [field]: value + })); + }; + + const handleBusinessContactChange = (type: keyof typeof formData.businessContacts, field: 'name' | 'position' | 'department' | 'responsibility' | 'email', value: string) => { + setFormData(prev => ({ + ...prev, + businessContacts: { + ...prev.businessContacts, + [type]: { + ...prev.businessContacts[type], + [field]: value + } + } + })); + }; + + const handleCareerChange = (index: number, field: 'date' | 'content', value: string) => { + setFormData(prev => ({ + ...prev, + representativeCareer: prev.representativeCareer.map((career, i) => + i === index ? { ...career, [field]: value } : career + ) + })); + }; + + const addCareerEntry = () => { + setFormData(prev => ({ + ...prev, + representativeCareer: [...prev.representativeCareer, { date: "", content: "" }] + })); + }; + + const removeCareerEntry = (index: number) => { + if (formData.representativeCareer.length > 1) { + setFormData(prev => ({ + ...prev, + representativeCareer: prev.representativeCareer.filter((_, i) => i !== index) + })); + } + }; + + const handleSubmit = async () => { + setLoading(true); + try { + // 필수 필드 검증 + const requiredFields = [ + { field: formData.companyNameKor, name: "업체명(국문)" }, + { field: formData.businessNumber, name: "사업자번호" }, + { field: formData.representativeNameKor, name: "대표자 성명(국문)" }, + { field: formData.representativeContact, name: "대표자 연락처" }, + { field: formData.representativeBirthDate, name: "대표자 생년월일" }, + { field: formData.representativeEmail, name: "대표자 이메일" }, + ]; + + const missingFields = requiredFields.filter(({ field }) => !field?.trim()); + if (missingFields.length > 0) { + toast.error(`다음 필수 항목을 입력해주세요: ${missingFields.map(f => f.name).join(", ")}`); + return; + } + + if (onSubmit) { + await onSubmit(formData); + } else { + // TODO: 실제 정규업체 등록 요청 API 호출 + toast.success("정규업체 등록 요청이 제출되었습니다."); + onOpenChange(false); + } + } catch (error) { + console.error("정규업체 등록 요청 오류:", error); + toast.error("정규업체 등록 요청 중 오류가 발생했습니다."); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + 정규업체 등록 요청 + +

+ 정규업체 등록을 요청할 협력업체 정보 확인 및 누락정보를 입력하세요. +

+
+ + {dataLoading ? ( +
+
+
+

기존 데이터를 불러오는 중...

+
+
+ ) : ( +
+ {/* 신규 협력사 등록 카드 */} + + + + + 신규 협력사 등록 카드 + + + + + {/* 업체정보 */} +
+

업체정보

+
+
+ + handleInputChange('companyNameKor', e.target.value)} + className="bg-yellow-50" + /> +
+
+ + handleInputChange('businessNumber', e.target.value)} + className="bg-yellow-50" + /> +
+
+ + handleInputChange('companyNameEng', e.target.value)} + /> +
+
+ + handleInputChange('corporateNumber', e.target.value)} + /> +
+
+ + handleInputChange('majorItems', e.target.value)} + /> +
+
+ + handleInputChange('establishmentDate', e.target.value)} + /> +
+
+
+ + {/* 소재지 */} +
+

소재지

+
+
+
+ + handleInputChange('headOfficeAddress', e.target.value)} + placeholder="PQ 내 회사주소" + /> +
+
+ + handleInputChange('headOfficePhone', e.target.value)} + placeholder="PQ 내 회사전화" + /> +
+
+
+
+ + handleInputChange('factoryAddress', e.target.value)} + placeholder="PQ 내 공장주소" + /> +
+
+ + handleInputChange('factoryPhone', e.target.value)} + placeholder="PQ 내 공장전화" + /> +
+
+
+
+ + handleInputChange('salesOfficeAddress', e.target.value)} + placeholder="필요시 입력" + /> +
+
+ + handleInputChange('salesOfficePhone', e.target.value)} + placeholder="필요시 입력" + /> +
+
+
+
+ + {/* 대표자 정보 */} +
+

대표자 정보

+
+
+ + handleInputChange('representativeNameKor', e.target.value)} + className="bg-yellow-50" + /> +
+
+ + handleInputChange('representativeContact', e.target.value)} + className="bg-yellow-50" + placeholder="010-0000-0000" + /> +
+
+ + handleInputChange('representativeNameEng', e.target.value)} + /> +
+
+ + handleInputChange('representativeBirthDate', e.target.value)} + className="bg-yellow-50" + /> +
+
+ + handleInputChange('representativeEmail', e.target.value)} + className="bg-yellow-50" + /> +
+
+ +
+
+ handleInputChange('isWorkingAtCompany', checked)} + /> + +
+
+ handleInputChange('isInternalPartner', checked)} + /> + +
+
+
+ + {/* 대표자 경력 */} +
+

대표자 경력

+
+ {formData.representativeCareer.map((career, index) => ( +
+
+ handleCareerChange(index, 'date', e.target.value)} + /> +
+
+ handleCareerChange(index, 'content', e.target.value)} + /> +
+ {formData.representativeCareer.length > 1 && ( + + )} +
+ ))} + +
+
+ + {/* 업무담당자 */} +
+

업무담당자

+
+ {Object.entries(formData.businessContacts).map(([type, contact]) => { + const labels = { + sales: "영업", + design: "설계", + delivery: "납기", + quality: "품질", + taxInvoice: "세금계산서" + }; + + return ( +
+
{labels[type as keyof typeof labels]} 담당자
+
+
+ + handleBusinessContactChange(type as keyof typeof formData.businessContacts, 'name', e.target.value)} + /> +
+
+ + handleBusinessContactChange(type as keyof typeof formData.businessContacts, 'position', e.target.value)} + placeholder="예: 부장, 과장" + /> +
+
+ + handleBusinessContactChange(type as keyof typeof formData.businessContacts, 'department', e.target.value)} + placeholder="예: 영업부, 기술부" + /> +
+
+ + handleBusinessContactChange(type as keyof typeof formData.businessContacts, 'responsibility', e.target.value)} + placeholder="예: 고객 대응, 품질 관리" + /> +
+
+ + handleBusinessContactChange(type as keyof typeof formData.businessContacts, 'email', e.target.value)} + placeholder="example@company.com" + /> +
+
+
+ ); + })} +
+
+ +
+
+
+ )} + + + + + +
+
+ ); +} -- cgit v1.2.3