// components/purchase-requests/edit-purchase-request-sheet.tsx "use client"; import * as React from "react"; import { useForm, useFieldArray } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetFooter, } from "@/components/ui/sheet"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { ProjectSelector } from "@/components/ProjectSelector"; import { PackageSelector } from "@/components/PackageSelector"; import type { PackageItem } from "@/lib/items/service"; import { MaterialGroupSelector } from "@/components/common/material/material-group-selector"; import { CalendarIcon, X, Plus, FileText, Package, Trash2, Upload, FileIcon, AlertCircle, Paperclip, Save } from "lucide-react"; import { format } from "date-fns"; import { cn } from "@/lib/utils"; import { updatePurchaseRequest } from "../service"; import { toast } from "sonner"; import { nanoid } from "nanoid"; import type { PurchaseRequestView } from "@/db/schema"; import { Dropzone, DropzoneDescription, DropzoneInput, DropzoneTitle, DropzoneUploadIcon, DropzoneZone, } from "@/components/ui/dropzone"; import { FileList, FileListAction, FileListDescription, FileListHeader, FileListIcon, FileListInfo, FileListItem, FileListName, FileListSize, } from "@/components/ui/file-list"; import { Progress } from "@/components/ui/progress"; import { Alert, AlertDescription } from "@/components/ui/alert"; const attachmentSchema = z.object({ fileName: z.string(), originalFileName: z.string(), filePath: z.string(), fileSize: z.number(), fileType: z.string(), }); const itemSchema = z.object({ id: z.string(), itemCode: z.string(), itemName: z.string().min(1, "아이템명을 입력해주세요"), specification: z.string().default(""), quantity: z.coerce.number().min(1, "수량은 1 이상이어야 합니다"), unit: z.string().min(1, "단위를 입력해주세요"), estimatedUnitPrice: z.coerce.number().optional(), remarks: z.string().optional(), }); const formSchema = z.object({ requestTitle: z.string().min(1, "요청 제목을 입력해주세요"), requestDescription: z.string().optional(), projectId: z.number({ required_error: "프로젝트를 선택해주세요", }), projectCode: z.string().min(1), projectName: z.string().min(1), projectCompany: z.string().optional(), projectSite: z.string().optional(), classNo: z.string().optional(), packageNo: z.string({ required_error: "패키지를 선택해주세요", }).min(1, "패키지를 선택해주세요"), packageName: z.string().min(1), majorItemMaterialCategory: z.string({ required_error: "자재그룹을 선택해주세요", }).min(1, "자재그룹을 선택해주세요"), majorItemMaterialDescription: z.string().min(1), smCode: z.string({ required_error: "SM 코드가 필요합니다", }).min(1, "SM 코드가 필요합니다"), estimatedBudget: z.string().optional(), requestedDeliveryDate: z.date().optional(), items: z.array(itemSchema).optional().default([]), }); type FormData = z.infer; interface EditPurchaseRequestSheetProps { request: PurchaseRequestView; open: boolean; onOpenChange: (open: boolean) => void; onSuccess?: () => void; } export function EditPurchaseRequestSheet({ request, open, onOpenChange, onSuccess, }: EditPurchaseRequestSheetProps) { const [isLoading, setIsLoading] = React.useState(false); const [activeTab, setActiveTab] = React.useState("basic"); const [selectedPackage, setSelectedPackage] = React.useState(null); const [selectedMaterials, setSelectedMaterials] = React.useState([]); const [resetKey, setResetKey] = React.useState(0); const [newFiles, setNewFiles] = React.useState([]); // 새로 추가할 파일 const [existingFiles, setExistingFiles] = React.useState[]>([]); // 기존 파일 const [isUploading, setIsUploading] = React.useState(false); const [uploadProgress, setUploadProgress] = React.useState(0); // 기존 아이템 처리 const existingItems = React.useMemo(() => { if (!request.items || !Array.isArray(request.items) || request.items.length === 0) { return []; } return request.items.map(item => ({ ...item, id: item.id || nanoid(), })); }, [request.items]); // 기존 첨부파일 로드 (실제로는 API에서 가져와야 함) React.useEffect(() => { // TODO: 기존 첨부파일 로드 API 호출 // setExistingFiles(request.attachments || []); }, [request.id]); // 기존 자재그룹 데이터 설정 React.useEffect(() => { if (request.majorItemMaterialCategory && request.majorItemMaterialDescription) { setSelectedMaterials([{ materialGroupCode: request.majorItemMaterialCategory, materialGroupDescription: request.majorItemMaterialDescription, displayText:`${request.majorItemMaterialCategory} - ${request.majorItemMaterialDescription}` }]); } }, [request.majorItemMaterialCategory, request.majorItemMaterialDescription]); // 기존 패키지 데이터 설정 React.useEffect(() => { if (request.packageNo && request.packageName) { setSelectedPackage({ packageCode: request.packageNo, description: request.packageName, smCode: request.smCode || "", } as PackageItem); } }, [request.packageNo, request.packageName, request.smCode]); const form = useForm({ resolver: zodResolver(formSchema), mode: "onChange", criteriaMode: "all", defaultValues: { requestTitle: request.requestTitle || "", requestDescription: request.requestDescription || "", projectId: request.projectId || undefined, projectCode: request.projectCode || "", projectName: request.projectName || "", projectCompany: request.projectCompany || "", projectSite: request.projectSite || "", classNo: request.classNo || "", packageNo: request.packageNo || "", packageName: request.packageName || "", majorItemMaterialCategory: request.majorItemMaterialCategory || "", majorItemMaterialDescription: request.majorItemMaterialDescription || "", smCode: request.smCode || "", estimatedBudget: request.estimatedBudget || "", requestedDeliveryDate: request.requestedDeliveryDate ? new Date(request.requestedDeliveryDate) : undefined, items: existingItems, }, }); const { fields, append, remove } = useFieldArray({ control: form.control, name: "items", }); // 프로젝트 선택 처리 const handleProjectSelect = (project: any) => { form.setValue("projectId", project.id); form.setValue("projectCode", project.projectCode); form.setValue("projectName", project.projectName); form.setValue("projectCompany", project.projectCompany); form.setValue("projectSite", project.projectSite); // 프로젝트 변경시 패키지 초기화 setSelectedPackage(null); form.setValue("packageNo", ""); form.setValue("packageName", ""); form.setValue("smCode", ""); // requestTitle 업데이트 updateRequestTitle(project.projectName, "", ""); }; // 패키지 선택 처리 const handlePackageSelect = (packageItem: PackageItem) => { setSelectedPackage(packageItem); form.setValue("packageNo", packageItem.packageCode); form.setValue("packageName", packageItem.description); // SM 코드가 패키지에 있으면 자동 설정 if (packageItem.smCode) { form.setValue("smCode", packageItem.smCode); } // requestTitle 업데이트 const projectName = form.getValues("projectName"); const materialDesc = form.getValues("majorItemMaterialDescription"); updateRequestTitle(projectName, packageItem.description, materialDesc); }; // 자재그룹 선택 처리 const handleMaterialsChange = (materials: any[]) => { setSelectedMaterials(materials); if (materials.length > 0) { const material = materials[0]; // single select이므로 첫번째 항목만 form.setValue("majorItemMaterialCategory", material.materialGroupCode); form.setValue("majorItemMaterialDescription", material.materialGroupDescription); // requestTitle 업데이트 const projectName = form.getValues("projectName"); const packageName = form.getValues("packageName"); updateRequestTitle(projectName, packageName, material.materialGroupDescription); } else { form.setValue("majorItemMaterialCategory", ""); form.setValue("majorItemMaterialDescription", ""); } }; // requestTitle 자동 생성 const updateRequestTitle = (projectName: string, packageName: string, materialDesc: string) => { const parts = []; if (projectName) parts.push(projectName); if (packageName) parts.push(packageName); if (materialDesc) parts.push(materialDesc); if (parts.length > 0) { const title = `${parts.join(" - ")} 구매 요청`; form.setValue("requestTitle", title); } }; const addItem = () => { append({ id: nanoid(), itemCode: "", itemName: "", specification: "", quantity: 1, unit: "개", estimatedUnitPrice: undefined, remarks: "", }); }; const calculateTotal = () => { const items = form.watch("items"); return items?.reduce((sum, item) => { const subtotal = (item.quantity || 0) * (item.estimatedUnitPrice || 0); return sum + subtotal; }, 0) || 0; }; // 파일 추가 핸들러 (로컬 상태로만 저장) const handleFileAdd = (files: File[]) => { if (files.length === 0) return; // 중복 파일 체크 const duplicates = files.filter(file => newFiles.some(existing => existing.name === file.name) ); if (duplicates.length > 0) { toast.warning(`${duplicates.length}개 파일이 이미 추가되어 있습니다`); } const uniqueFiles = files.filter(file => !newFiles.some(existing => existing.name === file.name) ); if (uniqueFiles.length > 0) { setNewFiles(prev => [...prev, ...uniqueFiles]); toast.success(`${uniqueFiles.length}개 파일이 추가되었습니다`); } }; // 새 파일 삭제 핸들러 const handleNewFileRemove = (fileName: string) => { setNewFiles(prev => prev.filter(f => f.name !== fileName)); }; // 기존 파일 삭제 핸들러 (실제로는 서버에 삭제 요청) const handleExistingFileRemove = (fileName: string) => { // TODO: 서버에 삭제 표시 setExistingFiles(prev => prev.filter(f => f.fileName !== fileName)); }; const onSubmit = async (values: FormData) => { try { setIsLoading(true); // 새 파일이 있으면 먼저 업로드 let uploadedAttachments: z.infer[] = []; if (newFiles.length > 0) { setIsUploading(true); setUploadProgress(10); const formData = new FormData(); newFiles.forEach(file => { formData.append("files", file); }); setUploadProgress(30); try { const response = await fetch("/api/upload/purchase-request", { method: "POST", body: formData, }); setUploadProgress(60); if (!response.ok) { throw new Error("파일 업로드 실패"); } const data = await response.json(); uploadedAttachments = data.files; setUploadProgress(80); } catch (error) { toast.error("파일 업로드 중 오류가 발생했습니다"); setIsUploading(false); setIsLoading(false); setUploadProgress(0); return; } } setUploadProgress(90); // 구매 요청 업데이트 (기존 파일 + 새로 업로드된 파일) const result = await updatePurchaseRequest(request.id, { ...values, items: values.items, attachments: [...existingFiles, ...uploadedAttachments], }); setUploadProgress(100); if (result.error) { toast.error(result.error); return; } toast.success("구매 요청이 수정되었습니다"); handleClose(); onSuccess?.(); } catch (error) { toast.error("구매 요청 수정에 실패했습니다"); console.error(error); } finally { setIsLoading(false); setIsUploading(false); setUploadProgress(0); } }; const handleClose = () => { form.reset(); setSelectedPackage(null); setSelectedMaterials([]); setNewFiles([]); setExistingFiles([]); setActiveTab("basic"); setResetKey((k) => k + 1); onOpenChange(false); }; const canEdit = request.status === "작성중"; const totalFileCount = existingFiles.length + newFiles.length; return ( 구매 요청 수정 요청번호: {request.requestCode} | 상태: {request.status} {!canEdit ? (

작성중 상태의 요청만 수정할 수 있습니다.

) : (
기본 정보 품목 정보 {fields.length > 0 && ( {fields.length} )} 첨부파일 {totalFileCount > 0 && ( {totalFileCount} )}
{/* 프로젝트 정보 */} 프로젝트 정보 * 프로젝트, 패키지, 자재그룹을 순서대로 선택하세요 {/* 프로젝트 선택 */} ( 프로젝트 * )} /> {/* 패키지 선택 */} ( 패키지 * )} /> {/* 자재그룹 선택 */} ( 자재그룹 * )} /> {/* SM 코드 */} ( SM 코드 * )} /> {/* 선택된 정보 표시 */} {form.watch("projectId") && (

프로젝트 코드

{form.watch("projectCode") || "-"}

프로젝트명

{form.watch("projectName") || "-"}

발주처

{form.watch("projectCompany") || "-"}

현장

{form.watch("projectSite") || "-"}

)}
{/* 기본 정보 */}
( 요청 제목 * 자동 생성된 제목을 수정할 수 있습니다 )} /> ( 요청 설명