diff options
Diffstat (limited to 'lib/bidding/list/edit-bidding-sheet.tsx')
| -rw-r--r-- | lib/bidding/list/edit-bidding-sheet.tsx | 505 |
1 files changed, 505 insertions, 0 deletions
diff --git a/lib/bidding/list/edit-bidding-sheet.tsx b/lib/bidding/list/edit-bidding-sheet.tsx new file mode 100644 index 00000000..f3bc1805 --- /dev/null +++ b/lib/bidding/list/edit-bidding-sheet.tsx @@ -0,0 +1,505 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Loader2 } from "lucide-react" +import { toast } from "sonner" +import { useSession } from "next-auth/react" + +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Switch } from "@/components/ui/switch" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" + +import { updateBidding, type UpdateBiddingInput } from "@/lib/bidding/service" +import { + updateBiddingSchema, + type UpdateBiddingSchema +} from "@/lib/bidding/validation" +import { BiddingListView } from "@/db/schema" +import { + biddingStatusLabels, + contractTypeLabels, + biddingTypeLabels, + awardCountLabels +} from "@/db/schema" + +interface EditBiddingSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + bidding: BiddingListView | null + onSuccess?: () => void +} + +export function EditBiddingSheet({ + open, + onOpenChange, + bidding, + onSuccess +}: EditBiddingSheetProps) { + const router = useRouter() + const [isSubmitting, setIsSubmitting] = React.useState(false) + const { data: session } = useSession() + + const form = useForm<UpdateBiddingSchema>({ + resolver: zodResolver(updateBiddingSchema), + defaultValues: { + biddingNumber: "", + revision: 0, + projectName: "", + itemName: "", + title: "", + description: "", + content: "", + + contractType: "general", + biddingType: "equipment", + awardCount: "single", + contractPeriod: "", + + preQuoteDate: "", + biddingRegistrationDate: "", + submissionStartDate: "", + submissionEndDate: "", + evaluationDate: "", + + hasSpecificationMeeting: false, + hasPrDocument: false, + prNumber: "", + + currency: "KRW", + budget: "", + targetPrice: "", + finalBidPrice: "", + + status: "bidding_generated", + isPublic: false, + managerName: "", + managerEmail: "", + managerPhone: "", + + remarks: "", + }, + }) + + // 시트가 열릴 때 기존 데이터로 폼 초기화 + React.useEffect(() => { + if (open && bidding) { + const formatDateForInput = (date: Date | string | null): string => { + if (!date) return "" + try { + const d = new Date(date) + if (isNaN(d.getTime())) return "" + return d.toISOString().slice(0, 16) // YYYY-MM-DDTHH:mm + } catch { + return "" + } + } + + const formatDateOnlyForInput = (date: Date | string | null): string => { + if (!date) return "" + try { + const d = new Date(date) + if (isNaN(d.getTime())) return "" + return d.toISOString().slice(0, 10) // YYYY-MM-DD + } catch { + return "" + } + } + + form.reset({ + biddingNumber: bidding.biddingNumber || "", + revision: bidding.revision || 0, + projectName: bidding.projectName || "", + itemName: bidding.itemName || "", + title: bidding.title || "", + description: bidding.description || "", + content: bidding.content || "", + + contractType: bidding.contractType || "general", + biddingType: bidding.biddingType || "equipment", + awardCount: bidding.awardCount || "single", + contractPeriod: bidding.contractPeriod || "", + + preQuoteDate: formatDateOnlyForInput(bidding.preQuoteDate), + biddingRegistrationDate: formatDateOnlyForInput(bidding.biddingRegistrationDate), + submissionStartDate: formatDateForInput(bidding.submissionStartDate), + submissionEndDate: formatDateForInput(bidding.submissionEndDate), + evaluationDate: formatDateForInput(bidding.evaluationDate), + + hasSpecificationMeeting: bidding.hasSpecificationMeeting || false, + hasPrDocument: bidding.hasPrDocument || false, + prNumber: bidding.prNumber || "", + + currency: bidding.currency || "KRW", + budget: bidding.budget?.toString() || "", + targetPrice: bidding.targetPrice?.toString() || "", + finalBidPrice: bidding.finalBidPrice?.toString() || "", + + status: bidding.status || "bidding_generated", + isPublic: bidding.isPublic || false, + managerName: bidding.managerName || "", + managerEmail: bidding.managerEmail || "", + managerPhone: bidding.managerPhone || "", + + remarks: bidding.remarks || "", + }) + } + }, [open, bidding, form]) + + // 폼 제출 + async function onSubmit(data: UpdateBiddingSchema) { + if (!bidding) return + + setIsSubmitting(true) + try { + const userId = session?.user?.id?.toString() || "1" + const input: UpdateBiddingInput = { + id: bidding.id, + ...data, + } + + const result = await updateBidding(input, userId) + + if (result.success) { + toast.success(result.message) + onOpenChange(false) + onSuccess?.() + } else { + toast.error(result.error || "입찰 수정에 실패했습니다.") + } + } catch (error) { + console.error("Error updating bidding:", error) + toast.error("입찰 수정 중 오류가 발생했습니다.") + } finally { + setIsSubmitting(false) + } + } + + // 시트 닫기 핸들러 + const handleOpenChange = (open: boolean) => { + onOpenChange(open) + if (!open) { + form.reset() + } + } + + if (!bidding) { + return null + } + + return ( + <Sheet open={open} onOpenChange={handleOpenChange}> + <SheetContent className="flex flex-col h-full sm:max-w-2xl overflow-hidden"> + <SheetHeader className="flex-shrink-0 text-left pb-6"> + <SheetTitle>입찰 수정</SheetTitle> + <SheetDescription> + 입찰 정보를 수정합니다. ({bidding.biddingNumber}) + </SheetDescription> + </SheetHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0"> + {/* 스크롤 가능한 컨텐츠 영역 */} + <div className="flex-1 overflow-y-auto pr-2 -mr-2"> + <div className="space-y-6"> + {/* 기본 정보 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">기본 정보</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="biddingNumber" + render={({ field }) => ( + <FormItem> + <FormLabel>입찰번호</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="revision" + render={({ field }) => ( + <FormItem> + <FormLabel>리비전</FormLabel> + <FormControl> + <Input + type="number" + min="0" + {...field} + onChange={(e) => field.onChange(parseInt(e.target.value) || 0)} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <FormField + control={form.control} + name="title" + render={({ field }) => ( + <FormItem> + <FormLabel>입찰명</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="projectName" + render={({ field }) => ( + <FormItem> + <FormLabel>프로젝트명</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="itemName" + render={({ field }) => ( + <FormItem> + <FormLabel>품목명</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </CardContent> + </Card> + + {/* 계약 정보 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">계약 정보</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="contractType" + render={({ field }) => ( + <FormItem> + <FormLabel>계약구분</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + </FormControl> + <SelectContent> + {Object.entries(contractTypeLabels).map(([value, label]) => ( + <SelectItem key={value} value={value}> + {label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="biddingType" + render={({ field }) => ( + <FormItem> + <FormLabel>입찰유형</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + </FormControl> + <SelectContent> + {Object.entries(biddingTypeLabels).map(([value, label]) => ( + <SelectItem key={value} value={value}> + {label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <FormField + control={form.control} + name="status" + render={({ field }) => ( + <FormItem> + <FormLabel>상태</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + </FormControl> + <SelectContent> + {Object.entries(biddingStatusLabels).map(([value, label]) => ( + <SelectItem key={value} value={value}> + {label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + </CardContent> + </Card> + + {/* 담당자 정보 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">담당자 정보</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <FormField + control={form.control} + name="managerName" + render={({ field }) => ( + <FormItem> + <FormLabel>담당자명</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="managerEmail" + render={({ field }) => ( + <FormItem> + <FormLabel>이메일</FormLabel> + <FormControl> + <Input type="email" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="managerPhone" + render={({ field }) => ( + <FormItem> + <FormLabel>전화번호</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </CardContent> + </Card> + + {/* 비고 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">비고</CardTitle> + </CardHeader> + <CardContent> + <FormField + control={form.control} + name="remarks" + render={({ field }) => ( + <FormItem> + <FormControl> + <Textarea + placeholder="추가 메모나 특이사항" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </CardContent> + </Card> + </div> + </div> + + {/* 고정된 버튼 영역 */} + <div className="flex-shrink-0 flex justify-end gap-3 pt-6 mt-6 border-t bg-background"> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={isSubmitting} + > + 취소 + </Button> + <Button + type="submit" + disabled={isSubmitting} + > + {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + 수정 + </Button> + </div> + </form> + </Form> + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file |
