From ee77f36b1ceece1236d45fba102c3ea410acebc1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 11 Sep 2025 11:20:42 +0000 Subject: (최겸) 구매 계약 메인 및 상세 기능 개발(템플릿 연동 및 계약 전달 개발 필요) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/create-general-contract-dialog.tsx | 479 ++++++++++++++++++ .../main/general-contract-update-sheet.tsx | 420 ++++++++++++++++ .../main/general-contracts-table-columns.tsx | 547 +++++++++++++++++++++ .../general-contracts-table-toolbar-actions.tsx | 124 +++++ .../main/general-contracts-table.tsx | 220 +++++++++ 5 files changed, 1790 insertions(+) create mode 100644 lib/general-contracts/main/create-general-contract-dialog.tsx create mode 100644 lib/general-contracts/main/general-contract-update-sheet.tsx create mode 100644 lib/general-contracts/main/general-contracts-table-columns.tsx create mode 100644 lib/general-contracts/main/general-contracts-table-toolbar-actions.tsx create mode 100644 lib/general-contracts/main/general-contracts-table.tsx (limited to 'lib/general-contracts/main') diff --git a/lib/general-contracts/main/create-general-contract-dialog.tsx b/lib/general-contracts/main/create-general-contract-dialog.tsx new file mode 100644 index 00000000..b2f538c3 --- /dev/null +++ b/lib/general-contracts/main/create-general-contract-dialog.tsx @@ -0,0 +1,479 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { useSession } from "next-auth/react" +import { Plus } from "lucide-react" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Calendar } from "@/components/ui/calendar" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { CalendarIcon } from "lucide-react" +import { format } from "date-fns" +import { ko } from "date-fns/locale" +import { cn } from "@/lib/utils" +import { createContract, getVendors } from "@/lib/general-contracts/service" +import { + GENERAL_CONTRACT_CATEGORIES, + GENERAL_CONTRACT_TYPES, + GENERAL_EXECUTION_METHODS, + GENERAL_CONTRACT_SCOPES +} from "@/lib/general-contracts/types" + +interface CreateContractForm { + contractNumber: string + name: string + category: string + type: string + executionMethod: string + selectionMethod: string + vendorId: number | null + startDate: Date | undefined + endDate: Date | undefined + validityEndDate: Date | undefined + // contractScope: string + // specificationType: string + notes: string + linkedRfqOrItb: string + linkedBidNumber: string + linkedPoNumber: string +} + +export function CreateGeneralContractDialog() { + const router = useRouter() + const { data: session } = useSession() + const [open, setOpen] = React.useState(false) + const [isLoading, setIsLoading] = React.useState(false) + const [vendors, setVendors] = React.useState>([]) + + const [form, setForm] = React.useState({ + contractNumber: '', + name: '', + category: '', + type: '', + executionMethod: '', + selectionMethod: '', + vendorId: null, + startDate: undefined, + endDate: undefined, + validityEndDate: undefined, + // contractScope: '', + // specificationType: '', + notes: '', + linkedRfqOrItb: '', + linkedBidNumber: '', + linkedPoNumber: '', + }) + + // 업체 목록 조회 + React.useEffect(() => { + const fetchVendors = async () => { + try { + const vendorList = await getVendors() + setVendors(vendorList) + } catch (error) { + console.error('Error fetching vendors:', error) + } + } + fetchVendors() + }, []) + + const generateContractNumber = () => { + const now = new Date() + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const time = String(now.getHours()).padStart(2, '0') + String(now.getMinutes()).padStart(2, '0') + return `CT${year}${month}${day}${time}` + } + + const handleSubmit = async () => { + // 필수 필드 검증 + if (!form.name || !form.category || !form.type || !form.executionMethod || + !form.vendorId || !form.startDate || !form.endDate) { + toast.error("필수 항목을 모두 입력해주세요.") + return + } + + if (!form.validityEndDate) { + setForm(prev => ({ ...prev, validityEndDate: form.endDate })) + } + + try { + setIsLoading(true) + + const contractData = { + contractNumber: generateContractNumber(), + name: form.name, + category: form.category, + type: form.type, + executionMethod: form.executionMethod, + selectionMethod: form.selectionMethod, + vendorId: form.vendorId!, + startDate: form.startDate!.toISOString().split('T')[0], + endDate: form.endDate!.toISOString().split('T')[0], + validityEndDate: (form.validityEndDate || form.endDate!).toISOString().split('T')[0], + // contractScope: form.contractScope, + // specificationType: form.specificationType, + status: 'Draft', + registeredById: session?.user?.id ? parseInt(session.user.id) : 3, + lastUpdatedById: session?.user?.id ? parseInt(session.user.id) : 3, + notes: form.notes, + linkedRfqOrItb: form.linkedRfqOrItb, + linkedBidNumber: form.linkedBidNumber, + linkedPoNumber: form.linkedPoNumber, + } + + const newContract = await createContract(contractData) + + toast.success("새 계약이 생성되었습니다.") + setOpen(false) + resetForm() + + // 상세 페이지로 이동 + router.refresh() + } catch (error) { + console.error('Error creating contract:', error) + toast.error("계약 생성 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + const resetForm = () => { + setForm({ + contractNumber: '', + name: '', + category: '', + type: '', + executionMethod: '', + selectionMethod: '', + vendorId: null, + startDate: undefined, + endDate: undefined, + validityEndDate: undefined, + // contractScope: '', + // specificationType: '', + notes: '', + linkedRfqOrItb: '', + linkedBidNumber: '', + linkedPoNumber: '', + }) + } + + return ( + { + setOpen(newOpen) + if (!newOpen) resetForm() + }}> + + + + + + 새 계약 등록 + + 새로운 계약의 기본 정보를 입력하세요. + + + +
+
+ {/*
+ + setForm(prev => ({ ...prev, contractNumber: e.target.value }))} + placeholder="자동 생성됩니다" + /> +
*/} + +
+ + setForm(prev => ({ ...prev, name: e.target.value }))} + placeholder="계약명을 입력하세요" + /> +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+
+ + + + + + + setForm(prev => ({ ...prev, startDate: date }))} + initialFocus + /> + + +
+ +
+ + + + + + + setForm(prev => ({ ...prev, endDate: date }))} + initialFocus + /> + + +
+ +
+ + + + + + + setForm(prev => ({ ...prev, validityEndDate: date }))} + initialFocus + /> + + +
+
+ + {/*
+
+ + +
+ +
+ + +
+
*/} + +
+
+ + setForm(prev => ({ ...prev, linkedRfqOrItb: e.target.value }))} + placeholder="연계 견적/입찰번호를 입력하세요" + /> +
+ +
+ + setForm(prev => ({ ...prev, linkedBidNumber: e.target.value }))} + placeholder="연계 BID번호를 입력하세요" + /> +
+
+ +
+ + setForm(prev => ({ ...prev, linkedPoNumber: e.target.value }))} + placeholder="연계PO번호를 입력하세요" + /> +
+ +
+ +