diff options
Diffstat (limited to 'lib/rfq-last/attachment/vendor-response-table.tsx')
| -rw-r--r-- | lib/rfq-last/attachment/vendor-response-table.tsx | 296 |
1 files changed, 175 insertions, 121 deletions
diff --git a/lib/rfq-last/attachment/vendor-response-table.tsx b/lib/rfq-last/attachment/vendor-response-table.tsx index 076fb153..47a23d18 100644 --- a/lib/rfq-last/attachment/vendor-response-table.tsx +++ b/lib/rfq-last/attachment/vendor-response-table.tsx @@ -17,7 +17,9 @@ import { FileCode, Building2, Calendar, - AlertCircle, X + AlertCircle, + X, + CheckCircle2 } from "lucide-react"; import { format, formatDistanceToNow, isValid, isBefore, isAfter } from "date-fns"; import { ko } from "date-fns/locale"; @@ -43,7 +45,7 @@ import type { DataTableRowAction, } from "@/types/table"; import { cn } from "@/lib/utils"; -import { getRfqVendorAttachments, updateAttachmentTypes } from "@/lib/rfq-last/service"; +import { confirmVendorDocuments, getRfqVendorAttachments, updateAttachmentTypes } from "@/lib/rfq-last/service"; import { downloadFile } from "@/lib/file-download"; import { toast } from "sonner"; import { @@ -61,7 +63,16 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; - +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; // 타입 정의 interface VendorAttachment { @@ -153,53 +164,81 @@ export function VendorResponseTable({ const [data, setData] = React.useState<VendorAttachment[]>(initialData); const [isRefreshing, setIsRefreshing] = React.useState(false); const [selectedRows, setSelectedRows] = React.useState<VendorAttachment[]>([]); - - - const [isUpdating, setIsUpdating] = React.useState(false); const [showTypeDialog, setShowTypeDialog] = React.useState(false); const [selectedType, setSelectedType] = React.useState<"구매" | "설계" | "">(""); - const [selectedVendor, setSelectedVendor] = React.useState<string | null>(null); + const [showConfirmDialog, setShowConfirmDialog] = React.useState(false); + const [isConfirming, setIsConfirming] = React.useState(false); + const [confirmedVendors, setConfirmedVendors] = React.useState<Set<number>>(new Set()); const filteredData = React.useMemo(() => { if (!selectedVendor) return data; return data.filter(item => item.vendorName === selectedVendor); }, [data, selectedVendor]); + // 현재 선택된 벤더의 ID 가져오기 + const selectedVendorId = React.useMemo(() => { + if (!selectedVendor) return null; + const vendorItem = data.find(item => item.vendorName === selectedVendor); + return vendorItem?.vendorId || null; + }, [selectedVendor, data]); - - // 데이터 새로고침 - const handleRefresh = React.useCallback(async () => { - setIsRefreshing(true); - try { - const result = await getRfqVendorAttachments(rfqId); - if (result.vendorSuccess && result.vendorData) { - setData(result.vendorData); - toast.success("데이터를 새로고침했습니다."); - } else { - toast.error("데이터를 불러오는데 실패했습니다."); - } - } catch (error) { - console.error("Refresh error:", error); - toast.error("새로고침 중 오류가 발생했습니다."); - } finally { - setIsRefreshing(false); + // 데이터 새로고침 + const handleRefresh = React.useCallback(async () => { + setIsRefreshing(true); + try { + const result = await getRfqVendorAttachments(rfqId); + if (result.vendorSuccess && result.vendorData) { + setData(result.vendorData); + toast.success("데이터를 새로고침했습니다."); + } else { + toast.error("데이터를 불러오는데 실패했습니다."); } - }, [rfqId]); + } catch (error) { + console.error("Refresh error:", error); + toast.error("새로고침 중 오류가 발생했습니다."); + } finally { + setIsRefreshing(false); + } + }, [rfqId]); + + const toggleVendorFilter = (vendor: string) => { + if (selectedVendor === vendor) { + setSelectedVendor(null); // 이미 선택된 벤더를 다시 클릭하면 필터 해제 + } else { + setSelectedVendor(vendor); + // 필터 변경 시 선택 초기화 (옵션) + setSelectedRows([]); + } + }; + + // 문서 확정 처리 + const handleConfirmDocuments = React.useCallback(async () => { + if (!selectedVendorId || !selectedVendor) return; - const toggleVendorFilter = (vendor: string) => { - if (selectedVendor === vendor) { - setSelectedVendor(null); // 이미 선택된 벤더를 다시 클릭하면 필터 해제 + setIsConfirming(true); + try { + const result = await confirmVendorDocuments(rfqId, selectedVendorId); + + if (result.success) { + toast.success(result.message); + setConfirmedVendors(prev => new Set(prev).add(selectedVendorId)); + setShowConfirmDialog(false); + // 데이터 새로고침 + await handleRefresh(); } else { - setSelectedVendor(vendor); - // 필터 변경 시 선택 초기화 (옵션) - setSelectedRows([]); + toast.error(result.message); } - }; + } catch (error) { + toast.error("문서 확정 중 오류가 발생했습니다."); + } finally { + setIsConfirming(false); + } + }, [selectedVendorId, selectedVendor, rfqId, handleRefresh]); - // 문서 유형 일괄 변경 - const handleBulkTypeChange = React.useCallback(async () => { + // 문서 유형 일괄 변경 + const handleBulkTypeChange = React.useCallback(async () => { if (!selectedType || selectedRows.length === 0) return; setIsUpdating(true); @@ -225,8 +264,6 @@ export function VendorResponseTable({ } }, [selectedType, selectedRows, handleRefresh]); - - // 액션 처리 const handleAction = React.useCallback(async (action: DataTableRowAction<VendorAttachment>) => { const attachment = action.row.original; @@ -352,56 +389,6 @@ export function VendorResponseTable({ }, size: 300, }, - // { - // accessorKey: "description", - // header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="설명" />, - // cell: ({ row }) => ( - // <div className="max-w-[200px] truncate" title={row.original.description || ""}> - // {row.original.description || "-"} - // </div> - // ), - // size: 200, - // }, - // { - // accessorKey: "validTo", - // header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="유효기간" />, - // cell: ({ row }) => { - // const { validFrom, validTo } = row.original; - // const validity = checkValidity(validTo); - - // if (!validTo) return <span className="text-muted-foreground">-</span>; - - // return ( - // <TooltipProvider> - // <Tooltip> - // <TooltipTrigger asChild> - // <div className="flex items-center gap-2"> - // {validity === "expired" && ( - // <AlertCircle className="h-4 w-4 text-red-500" /> - // )} - // {validity === "expiring-soon" && ( - // <AlertCircle className="h-4 w-4 text-yellow-500" /> - // )} - // <span className={cn( - // "text-sm", - // validity === "expired" && "text-red-500", - // validity === "expiring-soon" && "text-yellow-500" - // )}> - // {format(new Date(validTo), "yyyy-MM-dd")} - // </span> - // </div> - // </TooltipTrigger> - // <TooltipContent> - // <p>유효기간: {validFrom ? format(new Date(validFrom), "yyyy-MM-dd") : "?"} ~ {format(new Date(validTo), "yyyy-MM-dd")}</p> - // {validity === "expired" && <p className="text-red-500">만료됨</p>} - // {validity === "expiring-soon" && <p className="text-yellow-500">곧 만료 예정</p>} - // </TooltipContent> - // </Tooltip> - // </TooltipProvider> - // ); - // }, - // size: 120, - // }, { accessorKey: "responseStatus", header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="응답 상태" />, @@ -496,11 +483,6 @@ export function VendorResponseTable({ options: [ { label: "구매", value: "구매" }, { label: "설계", value: "설계" }, - // { label: "인증서", value: "인증서" }, - // { label: "카탈로그", value: "카탈로그" }, - // { label: "도면", value: "도면" }, - // { label: "테스트성적서", value: "테스트성적서" }, - // { label: "기타", value: "기타" }, ] }, { id: "documentNo", label: "문서번호", type: "text" }, @@ -518,8 +500,6 @@ export function VendorResponseTable({ { label: "취소", value: "취소" }, ] }, - // { id: "validFrom", label: "유효시작일", type: "date" }, - // { id: "validTo", label: "유효종료일", type: "date" }, { id: "uploadedAt", label: "업로드일", type: "date" }, ]; @@ -579,17 +559,42 @@ export function VendorResponseTable({ <span className="text-sm font-medium text-muted-foreground"> 벤더별 필터 </span> - {selectedVendor && ( - <Button - variant="ghost" - size="sm" - onClick={() => setSelectedVendor(null)} - className="h-7 px-2 text-xs" - > - <X className="h-3 w-3 mr-1" /> - 필터 초기화 - </Button> - )} + <div className="flex items-center gap-2"> + {/* 선택된 벤더의 문서 확정 버튼 */} + {selectedVendor && selectedVendorId && ( + <Button + variant="default" + size="sm" + onClick={() => setShowConfirmDialog(true)} + className="h-7" + disabled={confirmedVendors.has(selectedVendorId)} + > + {confirmedVendors.has(selectedVendorId) ? ( + <> + <CheckCircle2 className="h-3 w-3 mr-1" /> + 확정완료 + </> + ) : ( + <> + <CheckCircle2 className="h-3 w-3 mr-1" /> + {selectedVendor} 문서 확정 + </> + )} + </Button> + )} + + {selectedVendor && ( + <Button + variant="ghost" + size="sm" + onClick={() => setSelectedVendor(null)} + className="h-7 px-2 text-xs" + > + <X className="h-3 w-3 mr-1" /> + 필터 초기화 + </Button> + )} + </div> </div> {/* 벤더 버튼들 */} @@ -607,20 +612,29 @@ export function VendorResponseTable({ </Button> {/* 각 벤더별 버튼 */} - {Array.from(vendorCounts.entries()).map(([vendor, count]) => ( - <Button - key={vendor} - variant={selectedVendor === vendor ? "default" : "outline"} - size="sm" - onClick={() => toggleVendorFilter(vendor)} - className="h-7" - > - <Building2 className="h-3 w-3 mr-1" /> - <span className="text-xs"> - {vendor} ({count}) - </span> - </Button> - ))} + {Array.from(vendorCounts.entries()).map(([vendor, count]) => { + const vendorItem = data.find(item => item.vendorName === vendor); + const vendorId = vendorItem?.vendorId; + const isConfirmed = vendorId ? confirmedVendors.has(vendorId) : false; + + return ( + <Button + key={vendor} + variant={selectedVendor === vendor ? "default" : "outline"} + size="sm" + onClick={() => toggleVendorFilter(vendor)} + className="h-7 relative" + > + {isConfirmed && ( + <CheckCircle2 className="h-3 w-3 mr-1 text-green-600" /> + )} + <Building2 className="h-3 w-3 mr-1" /> + <span className="text-xs"> + {vendor} ({count}) + </span> + </Button> + ); + })} </div> {/* 현재 필터 상태 표시 */} @@ -636,7 +650,7 @@ export function VendorResponseTable({ <ClientDataTable columns={columns} - data={filteredData} // 필터링된 데이터 사용 + data={filteredData} advancedFilterFields={advancedFilterFields} autoSizeColumns={true} compact={true} @@ -650,8 +664,8 @@ export function VendorResponseTable({ {additionalActions} </ClientDataTable> - {/* 문서 유형 변경 다이얼로그 */} - <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}> + {/* 문서 유형 변경 다이얼로그 */} + <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}> <DialogContent className="sm:max-w-[425px]"> <DialogHeader> <DialogTitle>문서 유형 변경</DialogTitle> @@ -724,6 +738,46 @@ export function VendorResponseTable({ </DialogFooter> </DialogContent> </Dialog> + + {/* 문서 확정 확인 다이얼로그 */} + <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>문서 확정 확인</AlertDialogTitle> + <AlertDialogDescription className="space-y-2"> + <p> + <span className="font-semibold">{selectedVendor}</span> 벤더의 모든 문서를 확정하시겠습니까? + </p> + <p className="text-sm text-muted-foreground"> + 이 작업은 해당 벤더의 모든 응답 문서를 확정 처리합니다. + </p> + <p className="text-sm text-yellow-600"> + ⚠️ 확정 후에는 되돌릴 수 없습니다. + </p> + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel disabled={isConfirming}>취소</AlertDialogCancel> + <AlertDialogAction + onClick={handleConfirmDocuments} + disabled={isConfirming} + className="bg-blue-600 hover:bg-blue-700" + > + {isConfirming ? ( + <> + <RefreshCw className="mr-2 h-4 w-4 animate-spin" /> + 확정 중... + </> + ) : ( + <> + <CheckCircle2 className="mr-2 h-4 w-4" /> + 확정 + </> + )} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> </div> ); }
\ No newline at end of file |
