diff options
Diffstat (limited to 'lib/techsales-rfq/table/detail-table')
4 files changed, 32 insertions, 547 deletions
diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx index c4a7edde..cfae0bd7 100644 --- a/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx +++ b/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx @@ -14,10 +14,11 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Ellipsis, MessageCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { useRouter } from "next/navigation"; export interface DataTableRowAction<TData> { row: Row<TData>; - type: "delete" | "update" | "communicate"; + type: "delete" | "communicate"; } // 벤더 견적 데이터 타입 정의 @@ -232,6 +233,13 @@ export function getRfqDetailColumns({ cell: function Cell({ row }) { const vendorId = row.original.vendorId; const unreadCount = vendorId ? unreadMessages[vendorId] || 0 : 0; + const router = useRouter(); + + const handleViewDetails = () => { + if (vendorId) { + router.push(`/ko/evcp/vendors/${vendorId}/info`); + } + }; return ( <div className="text-right flex items-center justify-end gap-1"> @@ -269,9 +277,12 @@ export function getRfqDetailColumns({ </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-[160px]"> <DropdownMenuItem - onClick={() => setRowAction({ row, type: "update" })} + onClick={handleViewDetails} + disabled={!vendorId} + className="gap-2" > - 벤더 수정 + {/* <Eye className="h-4 w-4" /> */} + 벤더 상세정보 </DropdownMenuItem> <DropdownMenuItem onClick={() => setRowAction({ row, type: "delete" })} diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx index 4f8ac37b..a2f012ad 100644 --- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx +++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx @@ -12,11 +12,10 @@ import { toast } from "sonner" import { Skeleton } from "@/components/ui/skeleton" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { Loader2, UserPlus, BarChart2, Send, Trash2, MessageCircle } from "lucide-react" +import { Loader2, UserPlus, BarChart2, Send, Trash2 } from "lucide-react" import { ClientDataTable } from "@/components/client-data-table/data-table" import { AddVendorDialog } from "./add-vendor-dialog" import { DeleteVendorDialog } from "./delete-vendor-dialog" -import { UpdateVendorSheet } from "./update-vendor-sheet" import { VendorCommunicationDrawer } from "./vendor-communication-drawer" import { VendorQuotationComparisonDialog } from "./vendor-quotation-comparison-dialog" @@ -41,28 +40,6 @@ interface RfqDetailTablesProps { maxHeight?: string | number } -// 데이터 타입 정의 -interface Vendor { - id: number; - vendorName: string; - vendorCode: string; - // 기타 필요한 벤더 속성들 -} - -interface Currency { - code: string; - name: string; -} - -interface PaymentTerm { - code: string; - description: string; -} - -interface Incoterm { - code: string; - description: string; -} export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps) { // console.log("selectedRfq", selectedRfq) @@ -71,14 +48,9 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps const [isLoading, setIsLoading] = useState(false) const [details, setDetails] = useState<RfqDetailView[]>([]) const [vendorDialogOpen, setVendorDialogOpen] = React.useState(false) - const [updateSheetOpen, setUpdateSheetOpen] = React.useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) const [selectedDetail, setSelectedDetail] = React.useState<RfqDetailView | null>(null) - const [vendors, setVendors] = React.useState<Vendor[]>([]) - const [currencies, setCurrencies] = React.useState<Currency[]>([]) - const [paymentTerms, setPaymentTerms] = React.useState<PaymentTerm[]>([]) - const [incoterms, setIncoterms] = React.useState<Incoterm[]>([]) const [isAdddialogLoading, setIsAdddialogLoading] = useState(false) const [rowAction, setRowAction] = React.useState<DataTableRowAction<RfqDetailView> | null>(null) @@ -159,21 +131,6 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps const handleAddVendor = useCallback(async () => { try { setIsAdddialogLoading(true) - - // TODO: 기술영업용 벤더, 통화, 지불조건, 인코텀즈 데이터 로드 함수 구현 필요 - // const [vendorsData, currenciesData, paymentTermsData, incotermsData] = await Promise.all([ - // fetchVendors(), - // fetchCurrencies(), - // fetchPaymentTerms(), - // fetchIncoterms() - // ]) - - // 임시 데이터 - setVendors([]) - setCurrencies([]) - setPaymentTerms([]) - setIncoterms([]) - setVendorDialogOpen(true) } catch (error) { console.error("데이터 로드 오류:", error) @@ -417,39 +374,16 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps return; } - // 다른 액션들은 기존과 동일하게 처리 - setIsAdddialogLoading(true); - - // TODO: 필요한 데이터 로드 (벤더, 통화, 지불조건, 인코텀즈) - // const [vendorsData, currenciesData, paymentTermsData, incotermsData] = await Promise.all([ - // fetchVendors(), - // fetchCurrencies(), - // fetchPaymentTerms(), - // fetchIncoterms() - // ]); - - // 임시 데이터 - setVendors([]); - setCurrencies([]); - setPaymentTerms([]); - setIncoterms([]); - - // 이제 데이터가 로드되었으므로 필요한 작업 수행 - if (rowAction.type === "update") { - setSelectedDetail(rowAction.row.original); - setUpdateSheetOpen(true); - } else if (rowAction.type === "delete") { + // 삭제 액션인 경우 + if (rowAction.type === "delete") { setSelectedDetail(rowAction.row.original); setDeleteDialogOpen(true); + setRowAction(null); + return; } } catch (error) { - console.error("데이터 로드 오류:", error); - toast.error("데이터를 불러오는 중 오류가 발생했습니다"); - } finally { - // communicate 타입이 아닌 경우에만 로딩 상태 변경 - if (rowAction && rowAction.type !== "communicate") { - setIsAdddialogLoading(false); - } + console.error("액션 처리 오류:", error); + toast.error("작업을 처리하는 중 오류가 발생했습니다"); } }; @@ -615,17 +549,6 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps onSuccess={handleRefreshData} /> - <UpdateVendorSheet - open={updateSheetOpen} - onOpenChange={setUpdateSheetOpen} - detail={selectedDetail} - vendors={vendors} - currencies={currencies} - paymentTerms={paymentTerms} - incoterms={incoterms} - onSuccess={handleRefreshData} - /> - <DeleteVendorDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen} diff --git a/lib/techsales-rfq/table/detail-table/update-vendor-sheet.tsx b/lib/techsales-rfq/table/detail-table/update-vendor-sheet.tsx deleted file mode 100644 index 0399f4df..00000000 --- a/lib/techsales-rfq/table/detail-table/update-vendor-sheet.tsx +++ /dev/null @@ -1,449 +0,0 @@ -"use client" - -import * as React from "react" -import { zodResolver } from "@hookform/resolvers/zod" -import { Check, ChevronsUpDown, Loader } from "lucide-react" -import { useForm } from "react-hook-form" -import { toast } from "sonner" -import { z } from "zod" - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, -} from "@/components/ui/command" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { - Sheet, - SheetClose, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet" -import { Checkbox } from "@/components/ui/checkbox" -import { ScrollArea } from "@/components/ui/scroll-area" - -import { RfqDetailView } from "./rfq-detail-column" -import { updateRfqDetail } from "@/lib/procurement-rfqs/services" - -// 폼 유효성 검증 스키마 -const updateRfqDetailSchema = z.object({ - vendorId: z.string().min(1, "벤더를 선택해주세요"), - currency: z.string().min(1, "통화를 선택해주세요"), - paymentTermsCode: z.string().min(1, "지불 조건을 선택해주세요"), - incotermsCode: z.string().min(1, "인코텀즈를 선택해주세요"), - incotermsDetail: z.string().optional(), - deliveryDate: z.string().optional(), - taxCode: z.string().optional(), - placeOfShipping: z.string().optional(), - placeOfDestination: z.string().optional(), - materialPriceRelatedYn: z.boolean().default(false), -}) - -type UpdateRfqDetailFormValues = z.infer<typeof updateRfqDetailSchema> - -// 데이터 타입 정의 -interface Vendor { - id: number; - vendorName: string; - vendorCode: string; -} - -interface Currency { - code: string; - name: string; -} - -interface PaymentTerm { - code: string; - description: string; -} - -interface Incoterm { - code: string; - description: string; -} - -interface UpdateRfqDetailSheetProps - extends React.ComponentPropsWithRef<typeof Sheet> { - detail: RfqDetailView | null; - vendors: Vendor[]; - currencies: Currency[]; - paymentTerms: PaymentTerm[]; - incoterms: Incoterm[]; - onSuccess?: () => void; -} - -export function UpdateVendorSheet({ - detail, - vendors, - currencies, - paymentTerms, - incoterms, - onSuccess, - ...props -}: UpdateRfqDetailSheetProps) { - const [isUpdatePending, startUpdateTransition] = React.useTransition() - const [vendorOpen, setVendorOpen] = React.useState(false) - - const form = useForm<UpdateRfqDetailFormValues>({ - resolver: zodResolver(updateRfqDetailSchema), - defaultValues: { - vendorId: detail?.vendorName ? String(vendors.find(v => v.vendorName === detail.vendorName)?.id || "") : "", - currency: detail?.currency || "", - paymentTermsCode: detail?.paymentTermsCode || "", - incotermsCode: detail?.incotermsCode || "", - incotermsDetail: detail?.incotermsDetail || "", - deliveryDate: detail?.deliveryDate ? new Date(detail.deliveryDate).toISOString().split('T')[0] : "", - taxCode: detail?.taxCode || "", - placeOfShipping: detail?.placeOfShipping || "", - placeOfDestination: detail?.placeOfDestination || "", - materialPriceRelatedYn: detail?.materialPriceRelatedYn || false, - }, - }) - - // detail이 변경될 때 form 값 업데이트 - React.useEffect(() => { - if (detail) { - const vendorId = vendors.find(v => v.vendorName === detail.vendorName)?.id - - form.reset({ - vendorId: vendorId ? String(vendorId) : "", - currency: detail.currency || "", - paymentTermsCode: detail.paymentTermsCode || "", - incotermsCode: detail.incotermsCode || "", - incotermsDetail: detail.incotermsDetail || "", - deliveryDate: detail.deliveryDate ? new Date(detail.deliveryDate).toISOString().split('T')[0] : "", - taxCode: detail.taxCode || "", - placeOfShipping: detail.placeOfShipping || "", - placeOfDestination: detail.placeOfDestination || "", - materialPriceRelatedYn: detail.materialPriceRelatedYn || false, - }) - } - }, [detail, form, vendors]) - - function onSubmit(values: UpdateRfqDetailFormValues) { - if (!detail) return - - startUpdateTransition(async () => { - try { - const result = await updateRfqDetail(detail.detailId, values) - - if (!result.success) { - toast.error(result.message || "수정 중 오류가 발생했습니다") - return - } - - props.onOpenChange?.(false) - toast.success("RFQ 벤더 정보가 수정되었습니다") - onSuccess?.() - } catch (error) { - console.error("RFQ 벤더 수정 오류:", error) - toast.error("수정 중 오류가 발생했습니다") - } - }) - } - - return ( - <Sheet {...props}> - <SheetContent className="flex w-full flex-col gap-6 sm:max-w-xl"> - <SheetHeader className="text-left"> - <SheetTitle>RFQ 벤더 정보 수정</SheetTitle> - <SheetDescription> - 벤더 정보를 수정하고 저장하세요 - </SheetDescription> - </SheetHeader> - <ScrollArea className="flex-1 pr-4"> - <Form {...form}> - <form - id="update-rfq-detail-form" - onSubmit={form.handleSubmit(onSubmit)} - className="flex flex-col gap-4" - > - {/* 검색 가능한 벤더 선택 필드 */} - <FormField - control={form.control} - name="vendorId" - render={({ field }) => ( - <FormItem className="flex flex-col"> - <FormLabel>벤더 <span className="text-red-500">*</span></FormLabel> - <Popover open={vendorOpen} onOpenChange={setVendorOpen}> - <PopoverTrigger asChild> - <FormControl> - <Button - variant="outline" - role="combobox" - aria-expanded={vendorOpen} - className={cn( - "w-full justify-between", - !field.value && "text-muted-foreground" - )} - > - {field.value - ? vendors.find((vendor) => String(vendor.id) === field.value) - ? `${vendors.find((vendor) => String(vendor.id) === field.value)?.vendorName} (${vendors.find((vendor) => String(vendor.id) === field.value)?.vendorCode})` - : "벤더를 선택하세요" - : "벤더를 선택하세요"} - <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> - </Button> - </FormControl> - </PopoverTrigger> - <PopoverContent className="w-[400px] p-0"> - <Command> - <CommandInput placeholder="벤더 검색..." /> - <CommandEmpty>검색 결과가 없습니다</CommandEmpty> - <ScrollArea className="h-60"> - <CommandGroup> - {vendors.map((vendor) => ( - <CommandItem - key={vendor.id} - value={`${vendor.vendorName} ${vendor.vendorCode}`} - onSelect={() => { - form.setValue("vendorId", String(vendor.id), { - shouldValidate: true, - }) - setVendorOpen(false) - }} - > - <Check - className={cn( - "mr-2 h-4 w-4", - String(vendor.id) === field.value - ? "opacity-100" - : "opacity-0" - )} - /> - {vendor.vendorName} ({vendor.vendorCode}) - </CommandItem> - ))} - </CommandGroup> - </ScrollArea> - </Command> - </PopoverContent> - </Popover> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="currency" - render={({ field }) => ( - <FormItem> - <FormLabel>통화 <span className="text-red-500">*</span></FormLabel> - <Select onValueChange={field.onChange} value={field.value}> - <FormControl> - <SelectTrigger> - <SelectValue placeholder="통화를 선택하세요" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {currencies.map((currency) => ( - <SelectItem key={currency.code} value={currency.code}> - {currency.name} ({currency.code}) - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="paymentTermsCode" - render={({ field }) => ( - <FormItem> - <FormLabel>지불 조건 <span className="text-red-500">*</span></FormLabel> - <Select onValueChange={field.onChange} value={field.value}> - <FormControl> - <SelectTrigger> - <SelectValue placeholder="지불 조건 선택" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {paymentTerms.map((term) => ( - <SelectItem key={term.code} value={term.code}> - {term.description} - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="incotermsCode" - render={({ field }) => ( - <FormItem> - <FormLabel>인코텀즈 <span className="text-red-500">*</span></FormLabel> - <Select onValueChange={field.onChange} value={field.value}> - <FormControl> - <SelectTrigger> - <SelectValue placeholder="인코텀즈 선택" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {incoterms.map((incoterm) => ( - <SelectItem key={incoterm.code} value={incoterm.code}> - {incoterm.description} - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - </div> - - <FormField - control={form.control} - name="incotermsDetail" - render={({ field }) => ( - <FormItem> - <FormLabel>인코텀즈 세부사항</FormLabel> - <FormControl> - <Input {...field} placeholder="인코텀즈 세부사항" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="deliveryDate" - render={({ field }) => ( - <FormItem> - <FormLabel>납품 예정일</FormLabel> - <FormControl> - <Input {...field} type="date" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="taxCode" - render={({ field }) => ( - <FormItem> - <FormLabel>세금 코드</FormLabel> - <FormControl> - <Input {...field} placeholder="세금 코드" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="placeOfShipping" - render={({ field }) => ( - <FormItem> - <FormLabel>선적지</FormLabel> - <FormControl> - <Input {...field} placeholder="선적지" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="placeOfDestination" - render={({ field }) => ( - <FormItem> - <FormLabel>도착지</FormLabel> - <FormControl> - <Input {...field} placeholder="도착지" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - - <FormField - control={form.control} - name="materialPriceRelatedYn" - render={({ field }) => ( - <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4"> - <FormControl> - <Checkbox - checked={field.value} - onCheckedChange={field.onChange} - /> - </FormControl> - <div className="space-y-1 leading-none"> - <FormLabel>자재 가격 관련 여부</FormLabel> - </div> - </FormItem> - )} - /> - </form> - </Form> - </ScrollArea> - <SheetFooter className="gap-2 pt-2 sm:space-x-0"> - <SheetClose asChild> - <Button type="button" variant="outline"> - 취소 - </Button> - </SheetClose> - <Button - type="submit" - form="update-rfq-detail-form" - disabled={isUpdatePending} - > - {isUpdatePending && ( - <Loader - className="mr-2 size-4 animate-spin" - aria-hidden="true" - /> - )} - 저장 - </Button> - </SheetFooter> - </SheetContent> - </Sheet> - ) -}
\ No newline at end of file diff --git a/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx b/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx index 51ef7b38..958cc8d1 100644 --- a/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx +++ b/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx @@ -37,7 +37,7 @@ import { } from "@/components/ui/dialog" import { formatDateTime } from "@/lib/utils" import { formatFileSize } from "@/lib/utils" // formatFileSize 유틸리티 임포트 -import { fetchVendorComments, markMessagesAsRead } from "@/lib/procurement-rfqs/services" +import { fetchTechSalesVendorComments, markTechSalesMessagesAsRead } from "@/lib/techsales-rfq/service" // 타입 정의 interface Comment { @@ -59,7 +59,7 @@ interface Attachment { id: number; fileName: string; fileSize: number; - fileType: string; + fileType: string | null; filePath: string; uploadedAt: Date; } @@ -99,8 +99,8 @@ async function sendComment(params: { }); } - // API 엔드포인트 구성 - const url = `/api/procurement-rfqs/${params.rfqId}/vendors/${params.vendorId}/comments`; + // API 엔드포인트 구성 - techSales용으로 변경 + const url = `/api/tech-sales-rfqs/${params.rfqId}/vendors/${params.vendorId}/comments`; // API 호출 const response = await fetch(url, { @@ -169,11 +169,11 @@ export function VendorCommunicationDrawer({ setIsLoading(true); // Server Action을 사용하여 코멘트 데이터 가져오기 - const commentsData = await fetchVendorComments(selectedRfq.id, selectedVendor.vendorId || 0); + const commentsData = await fetchTechSalesVendorComments(selectedRfq.id, selectedVendor.vendorId || 0); setComments(commentsData as Comment[]); // 구체적인 타입으로 캐스팅 // Server Action을 사용하여 읽지 않은 메시지를 읽음 상태로 변경 - await markMessagesAsRead(selectedRfq.id, selectedVendor.vendorId || 0); + await markTechSalesMessagesAsRead(selectedRfq.id, selectedVendor.vendorId || 0); } catch (error) { console.error("코멘트 로드 오류:", error); toast.error("메시지를 불러오는 중 오류가 발생했습니다"); @@ -269,15 +269,15 @@ export function VendorCommunicationDrawer({ const renderAttachmentPreviewDialog = () => { if (!selectedAttachment) return null; - const isImage = selectedAttachment.fileType.startsWith("image/"); - const isPdf = selectedAttachment.fileType.includes("pdf"); + const isImage = selectedAttachment.fileType?.startsWith("image/"); + const isPdf = selectedAttachment.fileType?.includes("pdf"); return ( <Dialog open={previewDialogOpen} onOpenChange={setPreviewDialogOpen}> <DialogContent className="max-w-3xl"> <DialogHeader> <DialogTitle className="flex items-center gap-2"> - {getFileIcon(selectedAttachment.fileType)} + {getFileIcon(selectedAttachment.fileType || '')} {selectedAttachment.fileName} </DialogTitle> <DialogDescription> @@ -300,7 +300,7 @@ export function VendorCommunicationDrawer({ /> ) : ( <div className="flex flex-col items-center gap-4 p-8"> - {getFileIcon(selectedAttachment.fileType)} + {getFileIcon(selectedAttachment.fileType || '')} <p className="text-muted-foreground text-sm">미리보기를 지원하지 않는 파일 형식입니다.</p> <Button variant="outline" @@ -398,7 +398,7 @@ export function VendorCommunicationDrawer({ className="flex items-center text-xs gap-2 mb-1 p-1 rounded hover:bg-black/5 cursor-pointer" onClick={() => handleAttachmentPreview(attachment)} > - {getFileIcon(attachment.fileType)} + {getFileIcon(attachment.fileType || '')} <span className="flex-1 truncate">{attachment.fileName}</span> <span className="text-xs opacity-70"> {formatFileSize(attachment.fileSize)} |
