diff options
Diffstat (limited to 'lib/rfq-last/attachment')
| -rw-r--r-- | lib/rfq-last/attachment/vendor-response-table.tsx | 387 |
1 files changed, 299 insertions, 88 deletions
diff --git a/lib/rfq-last/attachment/vendor-response-table.tsx b/lib/rfq-last/attachment/vendor-response-table.tsx index 6e1a02c8..f9388752 100644 --- a/lib/rfq-last/attachment/vendor-response-table.tsx +++ b/lib/rfq-last/attachment/vendor-response-table.tsx @@ -17,7 +17,7 @@ import { FileCode, Building2, Calendar, - AlertCircle + AlertCircle, X } from "lucide-react"; import { format, formatDistanceToNow, isValid, isBefore, isAfter } from "date-fns"; import { ko } from "date-fns/locale"; @@ -46,6 +46,22 @@ import { cn } from "@/lib/utils"; import { getRfqVendorAttachments } from "@/lib/rfq-last/service"; import { downloadFile } from "@/lib/file-download"; import { toast } from "sonner"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + // 타입 정의 interface VendorAttachment { @@ -138,24 +154,79 @@ export function VendorResponseTable({ const [isRefreshing, setIsRefreshing] = React.useState(false); const [selectedRows, setSelectedRows] = React.useState<VendorAttachment[]>([]); - // 데이터 새로고침 - const handleRefresh = React.useCallback(async () => { - setIsRefreshing(true); + + + const [isUpdating, setIsUpdating] = React.useState(false); + const [showTypeDialog, setShowTypeDialog] = React.useState(false); + const [selectedType, setSelectedType] = React.useState<"구매" | "설계" | "">(""); + console.log(data,"data") + + const [selectedVendor, setSelectedVendor] = React.useState<string | null>(null); + + const filteredData = React.useMemo(() => { + if (!selectedVendor) return data; + return data.filter(item => item.vendorName === selectedVendor); + }, [data, selectedVendor]); + + + + // 데이터 새로고침 + 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); + } + }, [rfqId]); + + const toggleVendorFilter = (vendor: string) => { + if (selectedVendor === vendor) { + setSelectedVendor(null); // 이미 선택된 벤더를 다시 클릭하면 필터 해제 + } else { + setSelectedVendor(vendor); + // 필터 변경 시 선택 초기화 (옵션) + setSelectedRows([]); + } + }; + + // 문서 유형 일괄 변경 + const handleBulkTypeChange = React.useCallback(async () => { + if (!selectedType || selectedRows.length === 0) return; + + setIsUpdating(true); try { - const result = await getRfqVendorAttachments(rfqId); - if (result.success && result.data) { - setData(result.data); - toast.success("데이터를 새로고침했습니다."); + const ids = selectedRows.map(row => row.id); + const result = await updateAttachmentTypes(ids, selectedType as "구매" | "설계"); + + if (result.success) { + toast.success(result.message); + // 데이터 새로고침 + await handleRefresh(); + // 선택 초기화 + setSelectedRows([]); + setShowTypeDialog(false); + setSelectedType(""); } else { - toast.error("데이터를 불러오는데 실패했습니다."); + toast.error(result.message); } } catch (error) { - console.error("Refresh error:", error); - toast.error("새로고침 중 오류가 발생했습니다."); + toast.error("문서 유형 변경 중 오류가 발생했습니다."); } finally { - setIsRefreshing(false); + setIsUpdating(false); } - }, [rfqId]); + }, [selectedType, selectedRows, handleRefresh]); + + // 액션 처리 const handleAction = React.useCallback(async (action: DataTableRowAction<VendorAttachment>) => { @@ -282,56 +353,56 @@ 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); + // { + // 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>; + // 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, - }, + // 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="응답 상태" />, @@ -424,13 +495,13 @@ export function VendorResponseTable({ label: "문서 유형", type: "select", options: [ - { label: "견적서", value: "견적서" }, - { label: "기술제안서", value: "기술제안서" }, - { label: "인증서", value: "인증서" }, - { label: "카탈로그", value: "카탈로그" }, - { label: "도면", value: "도면" }, - { label: "테스트성적서", value: "테스트성적서" }, - { label: "기타", value: "기타" }, + { label: "구매", value: "구매" }, + { label: "설계", value: "설계" }, + // { label: "인증서", value: "인증서" }, + // { label: "카탈로그", value: "카탈로그" }, + // { label: "도면", value: "도면" }, + // { label: "테스트성적서", value: "테스트성적서" }, + // { label: "기타", value: "기타" }, ] }, { id: "documentNo", label: "문서번호", type: "text" }, @@ -448,23 +519,35 @@ export function VendorResponseTable({ { label: "취소", value: "취소" }, ] }, - { id: "validFrom", label: "유효시작일", type: "date" }, - { id: "validTo", label: "유효종료일", type: "date" }, + // { id: "validFrom", label: "유효시작일", type: "date" }, + // { id: "validTo", label: "유효종료일", type: "date" }, { id: "uploadedAt", label: "업로드일", type: "date" }, ]; - // 추가 액션 버튼들 + // 추가 액션 버튼들 수정 const additionalActions = React.useMemo(() => ( <div className="flex items-center gap-2"> {selectedRows.length > 0 && ( - <Button - variant="outline" - size="sm" - onClick={handleBulkDownload} - > - <Download className="h-4 w-4 mr-2" /> - 다운로드 ({selectedRows.length}) - </Button> + <> + {/* 문서 유형 변경 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => setShowTypeDialog(true)} + > + <FileText className="h-4 w-4 mr-2" /> + 유형 변경 ({selectedRows.length}) + </Button> + + <Button + variant="outline" + size="sm" + onClick={handleBulkDownload} + > + <Download className="h-4 w-4 mr-2" /> + 다운로드 ({selectedRows.length}) + </Button> + </> )} <Button variant="outline" @@ -476,7 +559,7 @@ export function VendorResponseTable({ 새로고침 </Button> </div> - ), [selectedRows, isRefreshing, handleBulkDownload, handleRefresh]); + ), [selectedRows, isRefreshing, handleBulkDownload, handleRefresh]) // 벤더별 그룹 카운트 const vendorCounts = React.useMemo(() => { @@ -490,18 +573,71 @@ export function VendorResponseTable({ return ( <div className={cn("w-full space-y-4")}> - {/* 벤더별 요약 정보 */} - <div className="flex gap-2 flex-wrap"> - {Array.from(vendorCounts.entries()).map(([vendor, count]) => ( - <Badge key={vendor} variant="secondary"> - {vendor}: {count} - </Badge> - ))} + {/* 벤더 필터 섹션 */} + <div className="space-y-2"> + {/* 필터 헤더 */} + <div className="flex items-center justify-between"> + <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> + + {/* 벤더 버튼들 */} + <div className="flex gap-2 flex-wrap"> + {/* 전체 보기 버튼 */} + <Button + variant={selectedVendor === null ? "default" : "outline"} + size="sm" + onClick={() => setSelectedVendor(null)} + className="h-7" + > + <span className="text-xs"> + 전체 ({data.length}) + </span> + </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> + ))} + </div> + + {/* 현재 필터 상태 표시 */} + {selectedVendor && ( + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <AlertCircle className="h-3 w-3" /> + <span> + "{selectedVendor}" 벤더의 {filteredData.length}개 항목만 표시 중 + </span> + </div> + )} </div> <ClientDataTable columns={columns} - data={data} + data={filteredData} // 필터링된 데이터 사용 advancedFilterFields={advancedFilterFields} autoSizeColumns={true} compact={true} @@ -514,6 +650,81 @@ export function VendorResponseTable({ > {additionalActions} </ClientDataTable> + + {/* 문서 유형 변경 다이얼로그 */} + <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}> + <DialogContent className="sm:max-w-[425px]"> + <DialogHeader> + <DialogTitle>문서 유형 변경</DialogTitle> + <DialogDescription> + 선택한 {selectedRows.length}개 항목의 문서 유형을 변경합니다. + </DialogDescription> + </DialogHeader> + + <div className="grid gap-4 py-4"> + <div className="grid grid-cols-4 items-center gap-4"> + <label htmlFor="type" className="text-right"> + 문서 유형 + </label> + <Select + value={selectedType} + onValueChange={(value) => setSelectedType(value as "구매" | "설계")} + > + <SelectTrigger className="col-span-3"> + <SelectValue placeholder="문서 유형 선택" /> + </SelectTrigger> + <SelectContent> + <SelectItem value="구매">구매</SelectItem> + <SelectItem value="설계">설계</SelectItem> + </SelectContent> + </Select> + </div> + + {/* 현재 선택된 항목들의 정보 표시 */} + <div className="text-sm text-muted-foreground"> + <p>변경될 항목:</p> + <ul className="mt-2 max-h-32 overflow-y-auto space-y-1"> + {selectedRows.slice(0, 5).map((row) => ( + <li key={row.id} className="text-xs"> + • {row.vendorName} - {row.originalFileName} + </li> + ))} + {selectedRows.length > 5 && ( + <li className="text-xs italic"> + ... 외 {selectedRows.length - 5}개 + </li> + )} + </ul> + </div> + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => { + setShowTypeDialog(false); + setSelectedType(""); + }} + disabled={isUpdating} + > + 취소 + </Button> + <Button + onClick={handleBulkTypeChange} + disabled={!selectedType || isUpdating} + > + {isUpdating ? ( + <> + <RefreshCw className="mr-2 h-4 w-4 animate-spin" /> + 변경 중... + </> + ) : ( + "변경" + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> </div> ); }
\ No newline at end of file |
