"use client" import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm, useFieldArray } from "react-hook-form" import { useRouter, useSearchParams, useParams } from "next/navigation" import i18nIsoCountries from "i18n-iso-countries" import enLocale from "i18n-iso-countries/langs/en.json" import koLocale from "i18n-iso-countries/langs/ko.json" import { Button } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { toast } from "@/hooks/use-toast" import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover" import { Command, CommandList, CommandInput, CommandEmpty, CommandGroup, CommandItem, } from "@/components/ui/command" import { Check, ChevronsUpDown, Loader2, Plus, X } from "lucide-react" import { cn } from "@/lib/utils" import { useTranslation } from "@/i18n/client" import { createVendor, getVendorTypes } from "@/lib/vendors/service" import { createVendorSchema, CreateVendorSchema } from "@/lib/vendors/validations" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Dropzone, DropzoneZone, DropzoneInput, DropzoneUploadIcon, DropzoneTitle, DropzoneDescription, } from "@/components/ui/dropzone" import { FileList, FileListItem, FileListHeader, FileListIcon, FileListInfo, FileListName, FileListDescription, FileListAction, } from "@/components/ui/file-list" import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" import prettyBytes from "pretty-bytes" i18nIsoCountries.registerLocale(enLocale) i18nIsoCountries.registerLocale(koLocale) const locale = "ko" const countryMap = i18nIsoCountries.getNames(locale, { select: "official" }) const countryArray = Object.entries(countryMap).map(([code, label]) => ({ code, label, })) // Sort countries to put Korea first, then alphabetically const sortedCountryArray = [...countryArray].sort((a, b) => { // Put Korea (KR) at the top if (a.code === "KR") return -1; if (b.code === "KR") return 1; // Otherwise sort alphabetically return a.label.localeCompare(b.label); }); // Add English names for Korean locale const enhancedCountryArray = sortedCountryArray.map(country => ({ ...country, label: locale === "ko" && country.code === "KR" ? "대한민국 (South Korea)" : country.label })); // Comprehensive list of country dial codes const countryDialCodes: { [key: string]: string } = { AF: "+93", AL: "+355", DZ: "+213", AS: "+1-684", AD: "+376", AO: "+244", AI: "+1-264", AG: "+1-268", AR: "+54", AM: "+374", AW: "+297", AU: "+61", AT: "+43", AZ: "+994", BS: "+1-242", BH: "+973", BD: "+880", BB: "+1-246", BY: "+375", BE: "+32", BZ: "+501", BJ: "+229", BM: "+1-441", BT: "+975", BO: "+591", BA: "+387", BW: "+267", BR: "+55", BN: "+673", BG: "+359", BF: "+226", BI: "+257", KH: "+855", CM: "+237", CA: "+1", CV: "+238", KY: "+1-345", CF: "+236", TD: "+235", CL: "+56", CN: "+86", CO: "+57", KM: "+269", CG: "+242", CD: "+243", CR: "+506", CI: "+225", HR: "+385", CU: "+53", CY: "+357", CZ: "+420", DK: "+45", DJ: "+253", DM: "+1-767", DO: "+1-809", EC: "+593", EG: "+20", SV: "+503", GQ: "+240", ER: "+291", EE: "+372", ET: "+251", FJ: "+679", FI: "+358", FR: "+33", GA: "+241", GM: "+220", GE: "+995", DE: "+49", GH: "+233", GR: "+30", GD: "+1-473", GT: "+502", GN: "+224", GW: "+245", GY: "+592", HT: "+509", HN: "+504", HK: "+852", HU: "+36", IS: "+354", IN: "+91", ID: "+62", IR: "+98", IQ: "+964", IE: "+353", IL: "+972", IT: "+39", JM: "+1-876", JP: "+81", JO: "+962", KZ: "+7", KE: "+254", KI: "+686", KR: "+82", KW: "+965", KG: "+996", LA: "+856", LV: "+371", LB: "+961", LS: "+266", LR: "+231", LY: "+218", LI: "+423", LT: "+370", LU: "+352", MK: "+389", MG: "+261", MW: "+265", MY: "+60", MV: "+960", ML: "+223", MT: "+356", MH: "+692", MR: "+222", MU: "+230", MX: "+52", FM: "+691", MD: "+373", MC: "+377", MN: "+976", ME: "+382", MA: "+212", MZ: "+258", MM: "+95", NA: "+264", NR: "+674", NP: "+977", NL: "+31", NZ: "+64", NI: "+505", NE: "+227", NG: "+234", NU: "+683", KP: "+850", NO: "+47", OM: "+968", PK: "+92", PW: "+680", PS: "+970", PA: "+507", PG: "+675", PY: "+595", PE: "+51", PH: "+63", PL: "+48", PT: "+351", PR: "+1-787", QA: "+974", RO: "+40", RU: "+7", RW: "+250", KN: "+1-869", LC: "+1-758", VC: "+1-784", WS: "+685", SM: "+378", ST: "+239", SA: "+966", SN: "+221", RS: "+381", SC: "+248", SL: "+232", SG: "+65", SK: "+421", SI: "+386", SB: "+677", SO: "+252", ZA: "+27", SS: "+211", ES: "+34", LK: "+94", SD: "+249", SR: "+597", SZ: "+268", SE: "+46", CH: "+41", SY: "+963", TW: "+886", TJ: "+992", TZ: "+255", TH: "+66", TL: "+670", TG: "+228", TK: "+690", TO: "+676", TT: "+1-868", TN: "+216", TR: "+90", TM: "+993", TV: "+688", UG: "+256", UA: "+380", AE: "+971", GB: "+44", US: "+1", UY: "+598", UZ: "+998", VU: "+678", VA: "+39-06", VE: "+58", VN: "+84", YE: "+967", ZM: "+260", ZW: "+263" }; const MAX_FILE_SIZE = 3e9 export function JoinForm() { const params = useParams() || {}; const lng = params.lng ? String(params.lng) : "ko"; const { t } = useTranslation(lng, "translation") const router = useRouter() const searchParams = useSearchParams() || new URLSearchParams(); const defaultTaxId = searchParams.get("taxID") ?? "" // Define VendorType interface interface VendorType { id: number; code: string; nameKo: string; nameEn: string; } // Vendor Types state with proper typing const [vendorTypes, setVendorTypes] = React.useState([]) const [isLoadingVendorTypes, setIsLoadingVendorTypes] = React.useState(true) // File states const [selectedFiles, setSelectedFiles] = React.useState([]) const [isSubmitting, setIsSubmitting] = React.useState(false) // Fetch vendor types on component mount React.useEffect(() => { async function loadVendorTypes() { setIsLoadingVendorTypes(true) try { const result = await getVendorTypes() if (result.data) { setVendorTypes(result.data) } } catch (error) { console.error("Failed to load vendor types:", error) toast({ variant: "destructive", title: "Error", description: "Failed to load vendor types", }) } finally { setIsLoadingVendorTypes(false) } } loadVendorTypes() }, []) // React Hook Form const form = useForm({ resolver: zodResolver(createVendorSchema), defaultValues: { vendorName: "", vendorTypeId: undefined, items: "", taxId: defaultTaxId, address: "", email: "", phone: "", country: "", representativeName: "", representativeBirth: "", representativeEmail: "", representativePhone: "", corporateRegistrationNumber: "", attachedFiles: undefined, // contacts (no isPrimary) contacts: [ { contactName: "", contactPosition: "", contactEmail: "", contactPhone: "", }, ], }, mode: "onChange", }) const isFormValid = form.formState.isValid console.log("Form errors:", form.formState.errors); console.log("Form values:", form.getValues()); console.log("Form valid:", form.formState.isValid); // Field array for contacts const { fields: contactFields, append: addContact, remove: removeContact } = useFieldArray({ control: form.control, name: "contacts", }) // Dropzone handlers const handleDropAccepted = (acceptedFiles: File[]) => { const newFiles = [...selectedFiles, ...acceptedFiles] setSelectedFiles(newFiles) form.setValue("attachedFiles", newFiles, { shouldValidate: true }) } const handleDropRejected = (fileRejections: any[]) => { fileRejections.forEach((rej) => { toast({ variant: "destructive", title: "File Error", description: `${rej.file.name}: ${rej.errors[0]?.message || "Upload failed"}`, }) }) } const removeFile = (index: number) => { const updated = [...selectedFiles] updated.splice(index, 1) setSelectedFiles(updated) form.setValue("attachedFiles", updated, { shouldValidate: true }) } // Submit async function onSubmit(values: CreateVendorSchema) { setIsSubmitting(true) try { const mainFiles = values.attachedFiles ? Array.from(values.attachedFiles as FileList) : [] const vendorData = { vendorName: values.vendorName, vendorTypeId: values.vendorTypeId, items: values.items, vendorCode: values.vendorCode, website: values.website, taxId: values.taxId, address: values.address, email: values.email, phone: values.phone, country: values.country, status: "PENDING_REVIEW" as const, representativeName: values.representativeName || "", representativeBirth: values.representativeBirth || "", representativeEmail: values.representativeEmail || "", representativePhone: values.representativePhone || "", corporateRegistrationNumber: values.corporateRegistrationNumber || "" } const result = await createVendor({ vendorData, files: mainFiles, contacts: values.contacts, }) if (!result.error) { toast({ title: "등록 완료", description: "회사 등록이 완료되었습니다. (status=PENDING_REVIEW)", }) router.push("/ko/partners") } else { toast({ variant: "destructive", title: "오류", description: result.error || "등록에 실패했습니다.", }) } } catch (error: any) { console.error(error) toast({ variant: "destructive", title: "서버 에러", description: error.message || "에러가 발생했습니다.", }) } finally { setIsSubmitting(false) } } // Get country code for phone number placeholder const getPhonePlaceholder = (countryCode: string) => { if (!countryCode || !countryDialCodes[countryCode]) return "전화번호"; return `${countryDialCodes[countryCode]} 전화번호`; }; // Render return (

{defaultTaxId}{" "} {t("joinForm.title", { defaultValue: "Vendor Administrator Creation", })}

{t("joinForm.description", { defaultValue: "Please provide basic company information and attach any required documents (e.g., business registration). We will review and approve as soon as possible.", })}

{/* ───────────────────────────────────────── Basic Info ───────────────────────────────────────── */}

기본 정보

{/* Vendor Type - New Field */} { const selectedType = vendorTypes.find(type => type.id === field.value); const displayName = lng === "ko" ? (selectedType?.nameKo || "") : (selectedType?.nameEn || ""); return ( 업체유형 No vendor type found. {vendorTypes.map((type) => ( field.onChange(type.id)} > {lng === "ko" ? type.nameKo : type.nameEn} ))} ); }} /> {/* vendorName */} ( 업체명 {form.watch("country") === "KR" ? "사업자 등록증에 표기된 정확한 회사명을 입력하세요." : "해외 업체의 경우 영문 회사명을 입력하세요."} )} /> {/* Items - New Field */} ( 공급품목 공급 가능한 제품/서비스를 입력하세요 )} /> {/* Address */} ( 주소 )} /> {/* Country - Updated with enhanced list */} { const selectedCountry = enhancedCountryArray.find( (c) => c.code === field.value ) return ( 국가 No country found. {enhancedCountryArray.map((country) => ( field.onChange(country.code) } > {country.label} ))} ) }} /> {/* Phone - Updated with country code hint */} ( 대표 전화 )} /> {/* Email - Updated with company domain guidance */} ( 대표 이메일 회사 도메인 이메일을 사용하세요. (naver.com, gmail.com, daum.net 등의 개인 이메일은 지양해주세요) )} /> {/* Website */} ( 웹사이트 )} />
{/* ───────────────────────────────────────── 담당자 정보 (contacts) ───────────────────────────────────────── */}

담당자 정보 (최소 1명)

{contactFields.map((contact, index) => (
{/* contactName - All required now */} ( 담당자명 )} /> {/* contactPosition - Now required */} ( 직급 / 부서 )} /> {/* contactEmail */} ( 이메일 )} /> {/* contactPhone - Now required */} ( 전화번호 )} />
{/* Remove contact button row */} {contactFields.length > 1 && (
)}
))}
{/* ───────────────────────────────────────── 한국 사업자 (country === "KR") ───────────────────────────────────────── */} {form.watch("country") === "KR" && (

한국 사업자 정보

{/* 대표자 등... all now required for Korean companies */}
( 대표자 이름 )} /> ( 대표자 생년월일 )} /> ( 대표자 이메일 )} /> ( 대표자 전화번호 )} /> ( 법인등록번호 )} />
)} {/* ───────────────────────────────────────── 첨부파일 (사업자등록증 등) ───────────────────────────────────────── */}

기타 첨부파일

( 첨부 파일 사업자등록증, ISO 9001 인증서, 회사 브로셔, 기본 소개자료 등을 첨부해주세요. {({ maxSize }) => (
파일 업로드 드래그 또는 클릭 {maxSize ? ` (최대: ${prettyBytes(maxSize)})` : null}
)}
{selectedFiles.length > 0 && (
{selectedFiles.map((file, i) => ( {file.name} {prettyBytes(file.size)} removeFile(i)}> ))}
)}
)} />
{/* ───────────────────────────────────────── Submit ───────────────────────────────────────── */}
) }