summaryrefslogtreecommitdiff
path: root/lib/pcr/table/create-pcr-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-15 03:24:12 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-15 03:24:12 +0000
commit25b916d040a512cd5248dff319d727ae144d0652 (patch)
treeed65d637d4807ce4a1575f3080ff0df98c430ae4 /lib/pcr/table/create-pcr-dialog.tsx
parent3f293c90beb58ce206a66ff444d7acfc41b56429 (diff)
(최겸) 구매 PCR 개발(po -> pcr, ecc pcr-confirm test 필)
Diffstat (limited to 'lib/pcr/table/create-pcr-dialog.tsx')
-rw-r--r--lib/pcr/table/create-pcr-dialog.tsx642
1 files changed, 642 insertions, 0 deletions
diff --git a/lib/pcr/table/create-pcr-dialog.tsx b/lib/pcr/table/create-pcr-dialog.tsx
new file mode 100644
index 00000000..cddb20d3
--- /dev/null
+++ b/lib/pcr/table/create-pcr-dialog.tsx
@@ -0,0 +1,642 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+import { CalendarIcon, Loader2, Plus, Check, ChevronsUpDown } from "lucide-react"
+import { Calendar } from "@/components/ui/calendar"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+import { format } from "date-fns"
+import { ko } from "date-fns/locale"
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import * as z from "zod"
+import { createPcrPo, getVendorsForPcr } from "@/lib/pcr/service"
+import { PCR_CHANGE_TYPES } from "@/lib/pcr/types"
+import { cn } from "@/lib/utils"
+
+// PCR 생성 스키마
+const createPcrSchema = z.object({
+ changeType: z.string().optional(),
+ details: z.string().optional(),
+ project: z.string().optional(),
+ pcrRequestDate: z.date({
+ required_error: "PCR 요청일자를 선택해주세요.",
+ }),
+ poContractNumber: z.string().min(1, "PO/계약번호를 입력해주세요."),
+ revItemNumber: z.string().optional(),
+ purchaseContractManager: z.string().optional(),
+ pcrCreator: z.string().optional(),
+ poContractAmountBefore: z.number().optional(),
+ poContractAmountAfter: z.number().optional(),
+ contractCurrency: z.string().optional(),
+ pcrReason: z.string().optional(),
+ detailsReason: z.string().optional(),
+ rejectionReason: z.string().optional(),
+ pcrResponseDate: z.date().optional(),
+ vendorId: z.number({
+ required_error: "협력업체를 선택해주세요.",
+ }),
+})
+
+type CreatePcrFormValues = z.infer<typeof createPcrSchema>
+
+interface CreatePcrDialogProps {
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ isEvcpPage?: boolean
+ onSuccess?: () => void
+ currentVendorId?: number // Partners 페이지에서 현재 사용자의 vendorId
+}
+
+interface Vendor {
+ id: number
+ vendorName: string
+ vendorCode: string
+}
+
+export function CreatePcrDialog({
+ open,
+ onOpenChange,
+ isEvcpPage = false,
+ onSuccess,
+ currentVendorId,
+}: CreatePcrDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [internalOpen, setInternalOpen] = React.useState(false)
+ const [vendors, setVendors] = React.useState<Vendor[]>([])
+ const [vendorsLoading, setVendorsLoading] = React.useState(false)
+ const [vendorSearchOpen, setVendorSearchOpen] = React.useState(false)
+
+ const dialogOpen = open !== undefined ? open : internalOpen
+ const setDialogOpen = onOpenChange || setInternalOpen
+
+ const form = useForm<CreatePcrFormValues>({
+ resolver: zodResolver(createPcrSchema),
+ defaultValues: {
+ changeType: "OTHER",
+ contractCurrency: "KRW",
+ pcrRequestDate: new Date(),
+ },
+ })
+
+ const loadVendors = React.useCallback(async () => {
+ try {
+ setVendorsLoading(true)
+
+ let result
+ if (!isEvcpPage && currentVendorId) {
+ // Partners 페이지: 특정 vendorId만 조회
+ result = await getVendorsForPcr({
+ vendorId: currentVendorId,
+ limit: 1
+ })
+ } else {
+ // EvcP 페이지: 모든 협력업체 조회
+ result = await getVendorsForPcr({
+ limit: 1000
+ })
+ }
+
+ if (result.success) {
+ setVendors(result.data.map(vendor => ({
+ id: vendor.id,
+ vendorName: vendor.vendorName,
+ vendorCode: vendor.vendorCode || "",
+ })))
+
+ // Partners 페이지에서는 자동으로 해당 협력업체 선택
+ if (!isEvcpPage && currentVendorId && result.data.length > 0) {
+ form.setValue("vendorId", currentVendorId)
+ }
+ } else {
+ toast.error(result.error || "협력업체 목록을 불러오는 중 오류가 발생했습니다.")
+ }
+ } catch (error) {
+ console.error("협력업체 로드 오류:", error)
+ toast.error("협력업체 목록을 불러오는 중 오류가 발생했습니다.")
+ } finally {
+ setVendorsLoading(false)
+ }
+ }, [isEvcpPage, currentVendorId, form])
+
+ // 협력업체 데이터 로드
+ React.useEffect(() => {
+ if (dialogOpen) {
+ loadVendors()
+ }
+ }, [dialogOpen, loadVendors])
+
+ async function onSubmit(data: CreatePcrFormValues) {
+ try {
+ setIsLoading(true)
+
+ const result = await createPcrPo({
+ ...data,
+ pcrApprovalStatus: "PENDING",
+ contractCurrency: data.contractCurrency || "KRW",
+ changeType: data.changeType || "OTHER",
+ })
+
+ if (result.success) {
+ toast.success("PCR이 성공적으로 생성되었습니다.")
+ form.reset()
+ setDialogOpen(false)
+ onSuccess?.()
+ } else {
+ toast.error(result.error || "PCR 생성에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("PCR 생성 오류:", error)
+ toast.error("PCR 생성 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
+ <DialogTrigger asChild>
+ <Button variant="default" size="sm" className="gap-2">
+ <Plus className="size-4" />
+ PCR 생성
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>PCR 생성</DialogTitle>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+ {/* 기본 정보 섹션 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-medium">기본 정보</h3>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="changeType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>변경 구분</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="변경 구분을 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {Object.entries(PCR_CHANGE_TYPES).map(([key, value]) => (
+ <SelectItem key={key} value={key}>
+ {value}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="pcrRequestDate"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>PCR 요청일자 *</FormLabel>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ className={cn(
+ "w-full pl-3 text-left font-normal",
+ !field.value && "text-muted-foreground"
+ )}
+ >
+ {field.value ? (
+ format(field.value, "yyyy년 MM월 dd일", { locale: ko })
+ ) : (
+ <span>날짜를 선택하세요</span>
+ )}
+ <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar
+ mode="single"
+ selected={field.value}
+ onSelect={field.onChange}
+ disabled={(date) =>
+ date > new Date() || date < new Date("1900-01-01")
+ }
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ {/* 협력업체 선택 */}
+ <FormField
+ control={form.control}
+ name="vendorId"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>협력업체 *</FormLabel>
+ <Popover open={vendorSearchOpen} onOpenChange={setVendorSearchOpen}>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={vendorSearchOpen}
+ className={cn(
+ "w-full justify-between",
+ !field.value && "text-muted-foreground"
+ )}
+ >
+ {field.value
+ ? vendors.find((vendor) => vendor.id === field.value)?.vendorName
+ : "협력업체를 선택하세요"}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-full p-0" align="start">
+ <Command>
+ <CommandInput placeholder="협력업체 검색..." />
+ <CommandEmpty>
+ {vendorsLoading ? "로딩 중..." : "협력업체를 찾을 수 없습니다."}
+ </CommandEmpty>
+ <CommandGroup>
+ <CommandList>
+ {vendors.map((vendor) => (
+ <CommandItem
+ key={vendor.id}
+ value={`${vendor.vendorName} ${vendor.vendorCode}`}
+ onSelect={() => {
+ form.setValue("vendorId", vendor.id)
+ setVendorSearchOpen(false)
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ field.value === vendor.id ? "opacity-100" : "opacity-0"
+ )}
+ />
+ <div className="flex flex-col">
+ <span className="font-medium">{vendor.vendorName}</span>
+ {vendor.vendorCode && (
+ <span className="text-sm text-muted-foreground">
+ 코드: {vendor.vendorCode}
+ </span>
+ )}
+ </div>
+ </CommandItem>
+ ))}
+ </CommandList>
+ </CommandGroup>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="poContractNumber"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PO/계약번호 *</FormLabel>
+ <FormControl>
+ <Input placeholder="PO/계약번호를 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="revItemNumber"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Rev./품번</FormLabel>
+ <FormControl>
+ <Input placeholder="Rev./품번을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ <FormField
+ control={form.control}
+ name="project"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>프로젝트</FormLabel>
+ <FormControl>
+ <Input placeholder="프로젝트명을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="details"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>상세</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="상세 내용을 입력하세요"
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ {/* 담당자 정보 섹션 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-medium">담당자 정보</h3>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="purchaseContractManager"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>구매/계약 담당자</FormLabel>
+ <FormControl>
+ <Input placeholder="담당자명을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="pcrCreator"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PCR 생성자</FormLabel>
+ <FormControl>
+ <Input placeholder="생성자명을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* 금액 정보 섹션 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-medium">금액 정보</h3>
+ <div className="grid grid-cols-3 gap-4">
+ <FormField
+ control={form.control}
+ name="contractCurrency"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>계약 통화</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="통화를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="KRW">KRW</SelectItem>
+ <SelectItem value="USD">USD</SelectItem>
+ <SelectItem value="EUR">EUR</SelectItem>
+ <SelectItem value="JPY">JPY</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="poContractAmountBefore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PO/계약금액 (전)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ placeholder="금액을 입력하세요"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="poContractAmountAfter"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PO/계약금액 (후)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ placeholder="금액을 입력하세요"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* 사유 정보 섹션 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-medium">사유 정보</h3>
+ <div className="space-y-4">
+ <FormField
+ control={form.control}
+ name="pcrReason"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PCR 사유</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="PCR 사유를 입력하세요"
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="detailsReason"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>상세 사유</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="상세 사유를 입력하세요"
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="rejectionReason"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>거절 사유</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="거절 사유를 입력하세요"
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* PCR 회신일자 */}
+ <div className="space-y-4">
+ <FormField
+ control={form.control}
+ name="pcrResponseDate"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>PCR 회신일</FormLabel>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ className={cn(
+ "w-[200px] pl-3 text-left font-normal",
+ !field.value && "text-muted-foreground"
+ )}
+ >
+ {field.value ? (
+ format(field.value, "yyyy년 MM월 dd일", { locale: ko })
+ ) : (
+ <span>날짜를 선택하세요</span>
+ )}
+ <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar
+ mode="single"
+ selected={field.value}
+ onSelect={field.onChange}
+ disabled={(date) =>
+ date < new Date("1900-01-01")
+ }
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ {/* 버튼들 */}
+ <div className="flex justify-end gap-4 pt-4">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => setDialogOpen(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={isLoading}>
+ {isLoading ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ 생성 중...
+ </>
+ ) : (
+ "PCR 생성"
+ )}
+ </Button>
+ </div>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+}