"use client" import * as React from "react" import { CalendarIcon, X } from "lucide-react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { format } from "date-fns" import { z } from "zod" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription, } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Calendar } from "@/components/ui/calendar" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "sonner" import { getSiteVisitRequestAction } from "@/lib/site-visit/service" import { Dropzone, DropzoneDescription, DropzoneInput, DropzoneTitle, DropzoneUploadIcon, DropzoneZone, } from "@/components/ui/dropzone" // 방문실사 요청 폼 스키마 const siteVisitRequestSchema = z.object({ // 실사 기간 inspectionDuration: z.number().min(0.5, "실사 기간을 입력해주세요."), // 실사 요청일 requestedStartDate: z.date({ required_error: "실사 시작일을 선택해주세요.", }), requestedEndDate: z.date({ required_error: "실사 종료일을 선택해주세요.", }), // SHI 실사참석 예정부문 shiAttendees: z.object({ technicalSales: z.object({ checked: z.boolean().default(false), count: z.number().min(0, "참석 인원은 0명 이상이어야 합니다.").default(0), details: z.string().optional(), }).default({ checked: false, count: 0, details: "" }), design: z.object({ checked: z.boolean().default(false), count: z.number().min(0, "참석 인원은 0명 이상이어야 합니다.").default(0), details: z.string().optional(), }).default({ checked: false, count: 0, details: "" }), procurement: z.object({ checked: z.boolean().default(false), count: z.number().min(0, "참석 인원은 0명 이상이어야 합니다.").default(0), details: z.string().optional(), }).default({ checked: false, count: 0, details: "" }), quality: z.object({ checked: z.boolean().default(false), count: z.number().min(0, "참석 인원은 0명 이상이어야 합니다.").default(0), details: z.string().optional(), }).default({ checked: false, count: 0, details: "" }), production: z.object({ checked: z.boolean().default(false), count: z.number().min(0, "참석 인원은 0명 이상이어야 합니다.").default(0), details: z.string().optional(), }).default({ checked: false, count: 0, details: "" }), commissioning: z.object({ checked: z.boolean().default(false), count: z.number().min(0, "참석 인원은 0명 이상이어야 합니다.").default(0), details: z.string().optional(), }).default({ checked: false, count: 0, details: "" }), other: z.object({ checked: z.boolean().default(false), count: z.number().min(0, "참석 인원은 0명 이상이어야 합니다.").default(0), details: z.string().optional(), }).default({ checked: false, count: 0, details: "" }), }), // SHI 참석자 정보 (JSON 형태로 저장) - 기존 필드 유지 shiAttendeeDetails: z.string().optional(), // 협력업체 요청정보 및 자료 vendorRequests: z.object({ availableDates: z.boolean().default(false), factoryName: z.boolean().default(false), factoryLocation: z.boolean().default(false), factoryAddress: z.boolean().default(false), factoryPicName: z.boolean().default(false), factoryPicPhone: z.boolean().default(false), factoryPicEmail: z.boolean().default(false), factoryDirections: z.boolean().default(false), accessProcedure: z.boolean().default(false), other: z.boolean().default(false), }), // 기타 요청사항 otherVendorRequests: z.string().optional(), // 추가 요청사항 additionalRequests: z.string().optional(), }) type SiteVisitRequestFormValues = z.infer interface SiteVisitDialogProps { isOpen: boolean onClose: () => void onSubmit: (data: SiteVisitRequestFormValues, attachments?: File[]) => Promise investigation: { id: number investigationMethod?: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL" investigationAddress?: string vendorName: string vendorCode: string projectName?: string projectCode?: string pqItems?: string | null } } export function SiteVisitDialog({ isOpen, onClose, onSubmit, investigation, }: SiteVisitDialogProps) { const [isPending, setIsPending] = React.useState(false) const [selectedFiles, setSelectedFiles] = React.useState([]) const form = useForm({ resolver: zodResolver(siteVisitRequestSchema), defaultValues: { inspectionDuration: 1.0, requestedStartDate: undefined, requestedEndDate: undefined, shiAttendees: { technicalSales: { checked: false, count: 0, details: "" }, design: { checked: false, count: 0, details: "" }, procurement: { checked: false, count: 0, details: "" }, quality: { checked: false, count: 0, details: "" }, production: { checked: false, count: 0, details: "" }, commissioning: { checked: false, count: 0, details: "" }, other: { checked: false, count: 0, details: "" }, }, shiAttendeeDetails: "", vendorRequests: { availableDates: false, factoryName: false, factoryLocation: false, factoryAddress: false, factoryPicName: false, factoryPicPhone: false, factoryPicEmail: false, factoryDirections: false, accessProcedure: false, other: false, }, otherVendorRequests: "", additionalRequests: "", }, }) // Dialog가 열릴 때마다 폼 재설정 및 기존 요청 확인 React.useEffect(() => { if (isOpen) { // 기존 방문실사 요청이 있는지 확인 const checkExistingRequest = async () => { try { const existingRequest = await getSiteVisitRequestAction(investigation.id) if (existingRequest.success && existingRequest.data) { toast.error("이미 방문실사 요청이 존재합니다. 추가 요청은 불가능합니다.") onClose() return } } catch (error) { console.error("방문실사 요청 상태 확인 중 오류:", error) toast.error("방문실사 요청 상태 확인 중 오류가 발생했습니다.") onClose() return } } checkExistingRequest() form.reset({ inspectionDuration: 1.0, requestedStartDate: undefined, requestedEndDate: undefined, shiAttendees: { technicalSales: { checked: false, count: 0, details: "" }, design: { checked: false, count: 0, details: "" }, procurement: { checked: false, count: 0, details: "" }, quality: { checked: false, count: 0, details: "" }, production: { checked: false, count: 0, details: "" }, commissioning: { checked: false, count: 0, details: "" }, other: { checked: false, count: 0, details: "" }, }, shiAttendeeDetails: "", vendorRequests: { availableDates: false, factoryName: false, factoryLocation: false, factoryAddress: false, factoryPicName: false, factoryPicPhone: false, factoryPicEmail: false, factoryDirections: false, accessProcedure: false, other: false, }, otherVendorRequests: "", additionalRequests: "", }) setSelectedFiles([]) } }, [isOpen, form, investigation.id, onClose]) async function handleSubmit(data: SiteVisitRequestFormValues) { setIsPending(true) try { // 제출 전에 한 번 더 기존 요청이 있는지 확인 const existingRequest = await getSiteVisitRequestAction(investigation.id) if (existingRequest.success && existingRequest.data) { toast.error("이미 방문실사 요청이 존재합니다. 추가 요청은 불가능합니다.") onClose() return } await onSubmit(data, selectedFiles) toast.success("방문실사 요청이 성공적으로 발송되었습니다.") } catch (error) { toast.error("방문실사 요청 발송 중 오류가 발생했습니다.") console.error("방문실사 요청 오류:", error) } finally { setIsPending(false) } } const handleDropAccepted = (files: File[]) => { setSelectedFiles(prev => [...prev, ...files]) toast.success(`${files.length}개 파일이 추가되었습니다.`) } const handleDropRejected = (files: unknown[]) => { toast.error(`${files.length}개 파일이 거부되었습니다. 파일 크기나 형식을 확인해주세요.`) } const removeFile = (index: number) => { setSelectedFiles(prev => prev.filter((_, i) => i !== index)) } const getInvestigationMethodLabel = (method: string) => { switch (method) { case "PURCHASE_SELF_EVAL": return "구매자체평가" case "DOCUMENT_EVAL": return "서류평가" case "PRODUCT_INSPECTION": return "제품검사평가" case "SITE_VISIT_EVAL": return "방문실사평가" default: return method } } return ( !open && onClose()}> 방문실사 요청 생성 협력업체에 방문실사 요청을 생성하고, 협력업체가 입력할 정보 항목을 설정합니다.
{/* 대상업체 정보 */}
대상업체
{investigation.vendorName}
({investigation.vendorCode})
대상품목
{investigation.pqItems || "-"}
{/* 실사방법 */}
실사방법
{getInvestigationMethodLabel(investigation.investigationMethod || "")}
{/* 실사기간 */} ( 실사기간 (W/D 기준)
field.onChange(parseFloat(e.target.value) || 0)} disabled={isPending} className="w-24" />
)} /> {/* 실사요청일 */}
( 실사 시작일 date < new Date()} initialFocus /> )} /> ( 실사 종료일 date < new Date()} initialFocus /> )} />
{/* SHI 실사참석 예정부문 */}
SHI 실사참석 예정부문 ※ 필수값
삼성중공업에 어떤 부문의 담당자가 몇 명 실사 참석 예정인지에 대한 정보를 입력하세요.
참석여부 부문 참석인원 참석자 정보 {[ { key: "technicalSales", label: "기술영업" }, { key: "design", label: "설계" }, { key: "procurement", label: "구매" }, { key: "quality", label: "품질" }, { key: "production", label: "생산" }, { key: "commissioning", label: "시운전" }, { key: "other", label: "기타" }, ].map((item) => ( ( )} /> {item.label} (
field.onChange(parseInt(e.target.value) || 0)} disabled={isPending} className="w-16 h-8" />
)} />
( )} />
))}
{/* 전체 참석자 상세정보 */} ( 전체 참석자 상세정보 (선택사항)