diff options
Diffstat (limited to 'lib/rfq-last')
| -rw-r--r-- | lib/rfq-last/service.ts | 229 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table-toolbar-actions.tsx | 59 | ||||
| -rw-r--r-- | lib/rfq-last/table/update-general-rfq-dialog.tsx | 749 |
3 files changed, 1030 insertions, 7 deletions
diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 8475aac0..2c1aa2ca 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -5515,4 +5515,233 @@ export async function getVendorDocumentConfirmStatus( console.error("문서 확정 상태 조회 중 오류:", error); return { isConfirmed: false, count: 0 }; } +} + +// 일반견적 수정 입력 인터페이스 +interface UpdateGeneralRfqInput { + id: number; // 수정할 RFQ ID + rfqType: string; + rfqTitle: string; + dueDate: Date; + picUserId: number; + projectId?: number; + remark?: string; + items: Array<{ + itemCode: string; + itemName: string; + materialCode?: string; + materialName?: string; + quantity: number; + uom: string; + remark?: string; + }>; + updatedBy: number; +} + +// 일반견적 수정 서버 액션 +export async function updateGeneralRfqAction(input: UpdateGeneralRfqInput) { + try { + // 트랜잭션으로 처리 + const result = await db.transaction(async (tx) => { + // 1. 기존 RFQ 조회 (존재 확인 및 상태 확인) + const existingRfq = await tx + .select() + .from(rfqsLast) + .where(eq(rfqsLast.id, input.id)) + .limit(1); + + if (!existingRfq || existingRfq.length === 0) { + throw new Error("수정할 일반견적을 찾을 수 없습니다"); + } + + const rfq = existingRfq[0]; + + // 상태 검증 (RFQ 생성 상태만 수정 가능) + if (rfq.status !== "RFQ 생성") { + throw new Error("RFQ 생성 상태인 일반견적만 수정할 수 있습니다"); + } + + // 2. 구매 담당자 정보 조회 + const picUser = await tx + .select({ + name: users.name, + email: users.email, + userCode: users.userCode + }) + .from(users) + .where(eq(users.id, input.picUserId)) + .limit(1); + + if (!picUser || picUser.length === 0) { + throw new Error("구매 담당자를 찾을 수 없습니다"); + } + + // 3. userCode 확인 (3자리) + const userCode = picUser[0].userCode; + if (!userCode || userCode.length !== 3) { + throw new Error("구매 담당자의 userCode가 올바르지 않습니다 (3자리 필요)"); + } + + // 4. 대표 아이템 정보 추출 (첫 번째 아이템) + const representativeItem = input.items[0]; + + // 5. rfqsLast 테이블 업데이트 + const [updatedRfq] = await tx + .update(rfqsLast) + .set({ + rfqType: input.rfqType, + rfqTitle: input.rfqTitle, + dueDate: input.dueDate, + projectId: input.projectId || null, + itemCode: representativeItem.itemCode, + itemName: representativeItem.itemName, + pic: input.picUserId, + picCode: userCode, + picName: picUser[0].name || '', + remark: input.remark || null, + updatedBy: input.updatedBy, + updatedAt: new Date(), + }) + .where(eq(rfqsLast.id, input.id)) + .returning(); + + // 6. 기존 rfqPrItems 삭제 후 재삽입 + await tx + .delete(rfqPrItems) + .where(eq(rfqPrItems.rfqsLastId, input.id)); + + // 7. rfqPrItems 테이블에 아이템들 재삽입 + const prItemsData = input.items.map((item, index) => ({ + rfqsLastId: input.id, + rfqItem: `${index + 1}`.padStart(3, '0'), // 001, 002, ... + prItem: null, // 일반견적에서는 PR 아이템 번호를 null로 설정 + prNo: null, // 일반견적에서는 PR 번호를 null로 설정 + + materialCode: item.materialCode || item.itemCode, // SAP 자재코드 (없으면 자재그룹코드 사용) + materialCategory: item.itemCode, // 자재그룹코드 + materialDescription: item.materialName || item.itemName, // SAP 자재명 (없으면 자재그룹명 사용) + quantity: item.quantity, + uom: item.uom, + + majorYn: index === 0, // 첫 번째 아이템을 주요 아이템으로 설정 + remark: item.remark || null, + })); + + await tx.insert(rfqPrItems).values(prItemsData); + + return updatedRfq; + }); + + return { + success: true, + message: "일반견적이 성공적으로 수정되었습니다", + data: { + id: result.id, + rfqCode: result.rfqCode, + }, + }; + + } catch (error) { + console.error("일반견적 수정 오류:", error); + + if (error instanceof Error) { + return { + success: false, + error: error.message, + }; + } + + return { + success: false, + error: "일반견적 수정 중 오류가 발생했습니다", + }; + } +} + +// 일반견적 수정용 데이터 조회 함수 +export async function getGeneralRfqForUpdate(rfqId: number) { + try { + // RFQ 기본 정보 조회 + const rfqData = await db + .select({ + id: rfqsLast.id, + rfqCode: rfqsLast.rfqCode, + rfqType: rfqsLast.rfqType, + rfqTitle: rfqsLast.rfqTitle, + status: rfqsLast.status, + dueDate: rfqsLast.dueDate, + projectId: rfqsLast.projectId, + pic: rfqsLast.pic, + picCode: rfqsLast.picCode, + picName: rfqsLast.picName, + remark: rfqsLast.remark, + createdAt: rfqsLast.createdAt, + updatedAt: rfqsLast.updatedAt, + }) + .from(rfqsLast) + .where( + and( + eq(rfqsLast.id, rfqId), + eq(rfqsLast.status, "RFQ 생성") // RFQ 생성 상태만 조회 + ) + ) + .limit(1); + + if (!rfqData || rfqData.length === 0) { + return { + success: false, + error: "수정할 일반견적을 찾을 수 없거나 수정할 수 없는 상태입니다", + }; + } + + const rfq = rfqData[0]; + + // RFQ 아이템들 조회 + const items = await db + .select({ + rfqItem: rfqPrItems.rfqItem, + materialCode: rfqPrItems.materialCode, + materialCategory: rfqPrItems.materialCategory, + materialDescription: rfqPrItems.materialDescription, + quantity: rfqPrItems.quantity, + uom: rfqPrItems.uom, + remark: rfqPrItems.remark, + }) + .from(rfqPrItems) + .where(eq(rfqPrItems.rfqsLastId, rfqId)) + .orderBy(rfqPrItems.rfqItem); + + // 아이템 데이터를 폼 형식으로 변환 + const formItems = items.map(item => ({ + itemCode: item.materialCategory || "", // 자재그룹코드 + itemName: item.materialDescription || "", // 자재그룹명 + materialCode: item.materialCode || "", // SAP 자재코드 + materialName: item.materialDescription || "", // SAP 자재명 (설명으로 사용) + quantity: Math.floor(Number(item.quantity)), // 소수점 제거 + uom: item.uom, + remark: item.remark || "", + })); + + return { + success: true, + data: { + id: rfq.id, + rfqCode: rfq.rfqCode, + rfqType: rfq.rfqType, + rfqTitle: rfq.rfqTitle, + dueDate: rfq.dueDate, + picUserId: rfq.pic, + projectId: rfq.projectId, + remark: rfq.remark, + items: formItems, + }, + }; + + } catch (error) { + console.error("일반견적 조회 오류:", error); + return { + success: false, + error: "일반견적 조회 중 오류가 발생했습니다", + }; + } }
\ No newline at end of file diff --git a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx index 00c41402..148336fb 100644 --- a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx +++ b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx @@ -3,10 +3,11 @@ import * as React from "react"; import { Table } from "@tanstack/react-table"; import { Button } from "@/components/ui/button"; -import { Users, RefreshCw, FileDown, Plus } from "lucide-react"; +import { Users, RefreshCw, FileDown, Plus, Edit } from "lucide-react"; import { RfqsLastView } from "@/db/schema"; import { RfqAssignPicDialog } from "./rfq-assign-pic-dialog"; import { CreateGeneralRfqDialog } from "./create-general-rfq-dialog"; // 추가 +import { UpdateGeneralRfqDialog } from "./update-general-rfq-dialog"; // 수정용 import { Badge } from "@/components/ui/badge"; import { Tooltip, @@ -21,12 +22,14 @@ interface RfqTableToolbarActionsProps<TData> { onRefresh?: () => void; } -export function RfqTableToolbarActions<TData>({ - table, +export function RfqTableToolbarActions<TData>({ + table, rfqCategory = "itb", - onRefresh + onRefresh }: RfqTableToolbarActionsProps<TData>) { const [showAssignDialog, setShowAssignDialog] = React.useState(false); + const [showUpdateDialog, setShowUpdateDialog] = React.useState(false); + const [selectedRfqForUpdate, setSelectedRfqForUpdate] = React.useState<number | null>(null); console.log(rfqCategory) @@ -41,6 +44,9 @@ export function RfqTableToolbarActions<TData>({ (row.status === "RFQ 생성" || row.status === "구매담당지정") ); + // 수정 가능한 RFQ (general 카테고리에서 RFQ 생성 상태인 항목, 단일 선택만) + const updatableRfq = rfqCategory === "general" && rows.length === 1 && rows[0].status === "RFQ 생성" ? rows[0] : null; + return { ids: rows.map(row => row.id), codes: rows.map(row => row.rfqCode || ""), @@ -51,9 +57,12 @@ export function RfqTableToolbarActions<TData>({ // 담당자 지정 가능한 ITB (상태가 "RFQ 생성" 또는 "구매담당지정"인 ITB) assignableItbCount: assignableRows.length, assignableIds: assignableRows.map(row => row.id), - assignableCodes: assignableRows.map(row => row.rfqCode || "") + assignableCodes: assignableRows.map(row => row.rfqCode || ""), + // 수정 가능한 RFQ 정보 + updatableRfq: updatableRfq, + canUpdate: updatableRfq !== null, }; - }, [selectedRows]); + }, [selectedRows, rfqCategory]); // 담당자 지정 가능 여부 체크 (상태가 "RFQ 생성" 또는 "구매담당지정"인 ITB가 있는지) const canAssignPic = selectedRfqData.assignableItbCount > 0; @@ -69,6 +78,20 @@ export function RfqTableToolbarActions<TData>({ onRefresh?.(); // 테이블 데이터 새로고침 }; + const handleUpdateGeneralRfqSuccess = () => { + // 테이블 선택 초기화 + table.toggleAllPageRowsSelected(false); + // 데이터 새로고침 + onRefresh?.(); + }; + + const handleUpdateClick = () => { + if (selectedRfqData.updatableRfq) { + setSelectedRfqForUpdate(selectedRfqData.updatableRfq.id); + setShowUpdateDialog(true); + } + }; + return ( <> <div className="flex items-center gap-2"> @@ -131,7 +154,21 @@ export function RfqTableToolbarActions<TData>({ </Button> {rfqCategory === "general" && ( - <CreateGeneralRfqDialog onSuccess={handleCreateGeneralRfqSuccess} /> + <> + <CreateGeneralRfqDialog onSuccess={handleCreateGeneralRfqSuccess} /> + {/* 일반견적 수정 버튼 - 선택된 항목이 1개이고 RFQ 생성 상태일 때만 활성화 */} + {selectedRfqData.canUpdate && ( + <Button + variant="outline" + size="sm" + onClick={handleUpdateClick} + className="flex items-center gap-2" + > + <Edit className="h-4 w-4" /> + 일반견적 수정 + </Button> + )} + </> )} <Button variant="outline" @@ -153,6 +190,14 @@ export function RfqTableToolbarActions<TData>({ selectedRfqCodes={selectedRfqData.assignableCodes} onSuccess={handleAssignSuccess} /> + + {/* 일반견적 수정 다이얼로그 */} + <UpdateGeneralRfqDialog + open={showUpdateDialog} + onOpenChange={setShowUpdateDialog} + rfqId={selectedRfqForUpdate || 0} + onSuccess={handleUpdateGeneralRfqSuccess} + /> </> ); }
\ No newline at end of file diff --git a/lib/rfq-last/table/update-general-rfq-dialog.tsx b/lib/rfq-last/table/update-general-rfq-dialog.tsx new file mode 100644 index 00000000..161a2840 --- /dev/null +++ b/lib/rfq-last/table/update-general-rfq-dialog.tsx @@ -0,0 +1,749 @@ +"use client"; + +import * as React from "react" +import { useForm, useFieldArray } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { format } from "date-fns" +import { CalendarIcon, Plus, Loader2, Trash2, PlusCircle } from "lucide-react" +import { useSession } from "next-auth/react" + +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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Calendar } from "@/components/ui/calendar" +import { Badge } from "@/components/ui/badge" +import { cn } from "@/lib/utils" +import { toast } from "sonner" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { updateGeneralRfqAction, getGeneralRfqForUpdate } from "../service" +import { MaterialGroupSelectorDialogSingle } from "@/components/common/material/material-group-selector-dialog-single" +import { MaterialSearchItem } from "@/lib/material/material-group-service" // 단순 타입 임포트 목적 +import { MaterialSelectorDialogSingle } from "@/components/common/selectors/material/material-selector-dialog-single" +import { MaterialSearchItem as SAPMaterialSearchItem } from "@/components/common/selectors/material/material-service" +import { ProjectSelector } from "@/components/ProjectSelector" +import { + PurchaseGroupCodeSingleSelector, + PurchaseGroupCodeWithUser +} from "@/components/common/selectors/purchase-group-code" + +// 아이템 스키마 (수정용) +const updateItemSchema = z.object({ + itemCode: z.string().optional(), + itemName: z.string().min(1, "자재명을 입력해주세요"), + materialCode: z.string().optional(), + materialName: z.string().optional(), + quantity: z.number().min(1, "수량은 1 이상이어야 합니다"), + uom: z.string().min(1, "단위를 입력해주세요"), + remark: z.string().optional(), +}) + +// 일반견적 수정 폼 스키마 +const updateGeneralRfqSchema = z.object({ + rfqType: z.string().min(1, "견적 종류를 선택해주세요"), + rfqTitle: z.string().min(1, "견적명을 입력해주세요"), + dueDate: z.date({ + required_error: "제출마감일을 선택해주세요", + }), + picUserId: z.number().min(1, "견적담당자를 선택해주세요"), + projectId: z.number().optional(), + remark: z.string().optional(), + items: z.array(updateItemSchema).min(1, "최소 하나의 자재를 추가해주세요"), +}) + +type UpdateGeneralRfqFormValues = z.infer<typeof updateGeneralRfqSchema> + +interface UpdateGeneralRfqDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + rfqId: number; + onSuccess?: () => void; +} + +export function UpdateGeneralRfqDialog({ + open, + onOpenChange, + rfqId, + onSuccess +}: UpdateGeneralRfqDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [isLoadingData, setIsLoadingData] = React.useState(false) + const [selectedPurchaseGroupCode, setSelectedPurchaseGroupCode] = React.useState<PurchaseGroupCodeWithUser | undefined>(undefined) + const [selectorOpen, setSelectorOpen] = React.useState(false) + const { data: session } = useSession() + + const userId = React.useMemo(() => { + return session?.user?.id ? Number(session.user.id) : null; + }, [session]); + + const form = useForm<UpdateGeneralRfqFormValues>({ + resolver: zodResolver(updateGeneralRfqSchema), + defaultValues: { + rfqType: "", + rfqTitle: "", + dueDate: undefined, + picUserId: userId || undefined, + projectId: undefined, + remark: "", + items: [ + { + itemCode: "", + itemName: "", + materialCode: "", + materialName: "", + quantity: 1, + uom: "", + remark: "", + }, + ], + }, + }) + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "items", + }) + + // 견적 종류 변경 + const handleRfqTypeChange = (value: string) => { + form.setValue("rfqType", value) + } + + // 구매그룹코드 선택 핸들러 + const handlePurchaseGroupCodeSelect = React.useCallback((code: PurchaseGroupCodeWithUser) => { + setSelectedPurchaseGroupCode(code) + + // 사용자 정보가 있으면 폼에 설정 + if (code.user) { + form.setValue("picUserId", code.user.id) + } else { + // 유저 정보가 없는 경우 경고 + toast.warning( + `해당 구매그룹코드(${code.PURCHASE_GROUP_CODE})의 사번 정보의 유저가 없습니다`, + { + description: `사번: ${code.EMPLOYEE_NUMBER}`, + duration: 5000, + } + ) + } + }, [form]) + + // 데이터 로드 함수 + const loadRfqData = React.useCallback(async () => { + if (!rfqId || !open) return + + setIsLoadingData(true) + try { + const result = await getGeneralRfqForUpdate(rfqId) + + if (result.success && result.data) { + const data = result.data + + // 폼 데이터 설정 + form.reset({ + rfqType: data.rfqType, + rfqTitle: data.rfqTitle, + dueDate: new Date(data.dueDate), + picUserId: data.picUserId, + projectId: data.projectId, + remark: data.remark || "", + items: data.items.length > 0 ? data.items : [ + { + itemCode: "", + itemName: "", + materialCode: "", + materialName: "", + quantity: 1, + uom: "", + remark: "", + }, + ], + }) + + // 구매그룹코드 정보도 초기화 (필요시) + // TODO: picUserId로부터 구매그룹코드 정보를 조회하여 설정 + + } else { + toast.error(result.error || "일반견적 데이터를 불러올 수 없습니다") + onOpenChange(false) + } + } catch (error) { + console.error("데이터 로드 오류:", error) + toast.error("일반견적 데이터를 불러오는 중 오류가 발생했습니다") + onOpenChange(false) + } finally { + setIsLoadingData(false) + } + }, [rfqId, open, form, onOpenChange]) + + // 다이얼로그 열림/닫힘 처리 + React.useEffect(() => { + if (open && rfqId) { + loadRfqData() + } else if (!open) { + // 다이얼로그가 닫힐 때 폼 초기화 + form.reset({ + rfqType: "", + rfqTitle: "", + dueDate: undefined, + picUserId: userId || undefined, + projectId: undefined, + remark: "", + items: [ + { + itemCode: "", + itemName: "", + materialCode: "", + materialName: "", + quantity: 1, + uom: "", + remark: "", + }, + ], + }) + setSelectedPurchaseGroupCode(undefined) + } + }, [open, rfqId, form, userId, loadRfqData]) + + const onSubmit = async (data: UpdateGeneralRfqFormValues) => { + if (!userId) { + toast.error("로그인이 필요합니다") + return + } + + if (!rfqId) { + toast.error("수정할 일반견적 ID가 없습니다") + return + } + + setIsLoading(true) + + try { + // 서버 액션 호출 + const result = await updateGeneralRfqAction({ + id: rfqId, + rfqType: data.rfqType, + rfqTitle: data.rfqTitle, + dueDate: data.dueDate, + picUserId: data.picUserId, + projectId: data.projectId, + remark: data.remark || "", + items: data.items as Array<{ + itemCode: string; + itemName: string; + materialCode?: string; + materialName?: string; + quantity: number; + uom: string; + remark?: string; + }>, + updatedBy: userId, + }) + + if (result.success) { + toast.success(result.message) + + // 다이얼로그 닫기 + onOpenChange(false) + + // 성공 콜백 실행 + if (onSuccess) { + onSuccess() + } + + } else { + toast.error(result.error || "일반견적 수정에 실패했습니다") + } + + } catch (error) { + console.error('일반견적 수정 오류:', error) + toast.error("일반견적 수정에 실패했습니다", { + description: "알 수 없는 오류가 발생했습니다", + }) + } finally { + setIsLoading(false) + } + } + + // 아이템 추가 + const handleAddItem = () => { + append({ + itemCode: "", + itemName: "", + materialCode: "", + materialName: "", + quantity: 1, + uom: "", + remark: "", + }) + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl h-[90vh] flex flex-col"> + {/* 고정된 헤더 */} + <DialogHeader className="flex-shrink-0"> + <DialogTitle>일반견적 수정</DialogTitle> + <DialogDescription> + 기존 일반견적을 수정합니다. 필수 정보를 입력해주세요. + </DialogDescription> + </DialogHeader> + + {/* 스크롤 가능한 컨텐츠 영역 */} + <ScrollArea className="flex-1 px-1"> + {isLoadingData ? ( + <div className="flex items-center justify-center py-8"> + <Loader2 className="h-6 w-6 animate-spin mr-2" /> + <span>데이터를 불러오는 중...</span> + </div> + ) : ( + <Form {...form}> + <form id="updateGeneralRfqForm" onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 py-2"> + + {/* 기본 정보 섹션 */} + <div className="space-y-4"> + <h3 className="text-lg font-semibold">기본 정보</h3> + + <div className="grid grid-cols-2 gap-4"> + {/* 견적 종류 */} + <div className="space-y-2"> + <FormField + control={form.control} + name="rfqType" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel> + 견적 종류 <span className="text-red-500">*</span> + </FormLabel> + <Select onValueChange={handleRfqTypeChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="견적 종류 선택" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="단가계약">단가계약</SelectItem> + <SelectItem value="매각계약">매각계약</SelectItem> + <SelectItem value="일반계약">일반계약</SelectItem> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + </div> + + {/* 제출마감일 */} + <FormField + control={form.control} + name="dueDate" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel> + 제출마감일 <span className="text-red-500">*</span> + </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") + ) : ( + <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="rfqTitle" + render={({ field }) => ( + <FormItem> + <FormLabel> + 견적명 <span className="text-red-500">*</span> + </FormLabel> + <FormControl> + <Input + placeholder="예: 2025년 1분기 사무용품 구매 견적" + {...field} + /> + </FormControl> + <FormDescription> + 견적의 목적이나 내용을 간단명료하게 입력해주세요 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 프로젝트 선택 */} + <FormField + control={form.control} + name="projectId" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel>프로젝트</FormLabel> + <FormControl> + <ProjectSelector + selectedProjectId={field.value} + onProjectSelect={(project) => field.onChange(project.id)} + placeholder="프로젝트 선택 (선택사항)..." + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 구매 담당자 - 구매그룹코드 선택기 */} + <FormField + control={form.control} + name="picUserId" + render={() => ( + <FormItem className="flex flex-col"> + <FormLabel> + 견적담당자 (구매그룹코드) <span className="text-red-500">*</span> + </FormLabel> + <FormControl> + <Button + type="button" + variant="outline" + className="w-full justify-start h-auto min-h-[36px]" + onClick={() => setSelectorOpen(true)} + > + {selectedPurchaseGroupCode ? ( + <div className="flex flex-col items-start gap-1 w-full"> + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="font-mono text-xs"> + {selectedPurchaseGroupCode.PURCHASE_GROUP_CODE} + </Badge> + <span className="text-sm">{selectedPurchaseGroupCode.DISPLAY_NAME}</span> + </div> + {selectedPurchaseGroupCode.user && ( + <div className="text-xs text-muted-foreground"> + 담당자: {selectedPurchaseGroupCode.user.name} ({selectedPurchaseGroupCode.user.email}) + </div> + )} + {!selectedPurchaseGroupCode.user && ( + <div className="text-xs text-orange-600"> + ⚠️ 연결된 사용자가 없습니다 + </div> + )} + </div> + ) : ( + <span className="text-muted-foreground text-sm"> + 구매그룹코드를 선택하세요 + </span> + )} + </Button> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 비고 */} + <FormField + control={form.control} + name="remark" + render={({ field }) => ( + <FormItem> + <FormLabel>비고</FormLabel> + <FormControl> + <Textarea + placeholder="추가 비고사항을 입력하세요" + className="resize-none" + rows={3} + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <Separator /> + + {/* 아이템 정보 섹션 - 컴팩트한 UI */} + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <h3 className="text-lg font-semibold">자재 정보</h3> + <Button + type="button" + variant="outline" + size="sm" + onClick={handleAddItem} + > + <PlusCircle className="mr-2 h-4 w-4" /> + 자재 추가 + </Button> + </div> + + <div className="space-y-3"> + {fields.map((field, index) => ( + <div key={field.id} className="border rounded-lg p-3 bg-gray-50/50"> + <div className="flex items-center justify-between mb-3"> + <span className="text-sm font-medium text-gray-700"> + 자재 #{index + 1} + </span> + {fields.length > 1 && ( + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => remove(index)} + className="h-6 w-6 p-0 text-destructive hover:text-destructive" + > + <Trash2 className="h-3 w-3" /> + </Button> + )} + </div> + + {/* 자재그룹 선택 - 그리드 외부 */} + <div className="mb-3"> + <FormLabel className="text-xs"> + 자재그룹(자재그룹명) <span className="text-red-500">*</span> + </FormLabel> + <div className="mt-1"> + <MaterialGroupSelectorDialogSingle + triggerLabel="자재그룹 선택" + selectedMaterial={(() => { + const itemCode = form.watch(`items.${index}.itemCode`); + const itemName = form.watch(`items.${index}.itemName`); + if (itemCode && itemName) { + return { + materialGroupCode: itemCode, + materialGroupDescription: itemName, + displayText: `${itemCode} - ${itemName}` + } as MaterialSearchItem; + } + return null; + })()} + onMaterialSelect={(material) => { + form.setValue(`items.${index}.itemCode`, material?.materialGroupCode || ''); + form.setValue(`items.${index}.itemName`, material?.materialGroupDescription || ''); + }} + placeholder="자재그룹을 검색하세요..." + title="자재그룹 선택" + description="원하는 자재그룹을 검색하고 선택해주세요." + triggerVariant="outline" + /> + </div> + </div> + + {/* 자재코드 선택 - 그리드 외부 */} + <div className="mb-3"> + <FormLabel className="text-xs"> + 자재코드(자재명) + </FormLabel> + <div className="mt-1"> + <MaterialSelectorDialogSingle + triggerLabel="자재코드 선택" + selectedMaterial={(() => { + const materialCode = form.watch(`items.${index}.materialCode`); + const materialName = form.watch(`items.${index}.materialName`); + if (materialCode && materialName) { + return { + materialCode: materialCode, + materialName: materialName, + displayText: `${materialCode} - ${materialName}` + } as SAPMaterialSearchItem; + } + return null; + })()} + onMaterialSelect={(material) => { + form.setValue(`items.${index}.materialCode`, material?.materialCode || ''); + form.setValue(`items.${index}.materialName`, material?.materialName || ''); + }} + placeholder="자재코드를 검색하세요..." + title="자재코드 선택" + description="원하는 자재코드를 검색하고 선택해주세요." + triggerVariant="outline" + /> + </div> + </div> + + <div className="grid grid-cols-2 gap-3"> + {/* 수량 */} + <FormField + control={form.control} + name={`items.${index}.quantity`} + render={({ field }) => ( + <FormItem> + <FormLabel className="text-xs"> + 수량 <span className="text-red-500">*</span> + </FormLabel> + <FormControl> + <Input + type="number" + min="1" + placeholder="1" + className="h-8 text-sm" + {...field} + onChange={(e) => field.onChange(Number(e.target.value))} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 단위 */} + <FormField + control={form.control} + name={`items.${index}.uom`} + render={({ field }) => ( + <FormItem> + <FormLabel className="text-xs"> + 단위 <span className="text-red-500">*</span> + </FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger className="h-8 text-sm"> + <SelectValue placeholder="단위 선택" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="EA">EA (Each)</SelectItem> + <SelectItem value="KG">KG (Kilogram)</SelectItem> + <SelectItem value="M">M (Meter)</SelectItem> + <SelectItem value="L">L (Liter)</SelectItem> + <SelectItem value="PC">PC (Piece)</SelectItem> + <SelectItem value="BOX">BOX (Box)</SelectItem> + <SelectItem value="SET">SET (Set)</SelectItem> + <SelectItem value="LOT">LOT (Lot)</SelectItem> + <SelectItem value="PCS">PCS (Pieces)</SelectItem> + <SelectItem value="TON">TON (Ton)</SelectItem> + <SelectItem value="G">G (Gram)</SelectItem> + <SelectItem value="ML">ML (Milliliter)</SelectItem> + <SelectItem value="CM">CM (Centimeter)</SelectItem> + <SelectItem value="MM">MM (Millimeter)</SelectItem> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + </div> + + {/* 비고 - 별도 행에 배치 */} + <div className="mt-3"> + <FormField + control={form.control} + name={`items.${index}.remark`} + render={({ field }) => ( + <FormItem> + <FormLabel className="text-xs">비고</FormLabel> + <FormControl> + <Input + placeholder="자재별 비고사항" + className="h-8 text-sm" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> + ))} + </div> + </div> + </form> + </Form> + )} + </ScrollArea> + + {/* 고정된 푸터 */} + <DialogFooter className="flex-shrink-0"> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading || isLoadingData} + > + 취소 + </Button> + <Button + type="submit" + form="updateGeneralRfqForm" + onClick={form.handleSubmit(onSubmit)} + disabled={isLoading || isLoadingData} + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {isLoading ? "수정 중..." : "일반견적 수정"} + </Button> + </DialogFooter> + + {/* 구매그룹코드 선택 다이얼로그 */} + <PurchaseGroupCodeSingleSelector + open={selectorOpen} + onOpenChange={setSelectorOpen} + selectedCode={selectedPurchaseGroupCode} + onCodeSelect={handlePurchaseGroupCodeSelect} + title="견적 담당자 선택" + description="일반견적의 담당자를 구매그룹코드로 선택하세요" + showConfirmButtons={false} + /> + </DialogContent> + </Dialog> + ) +} |
