summaryrefslogtreecommitdiff
path: root/lib/rfq-last/attachment/vendor-response-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfq-last/attachment/vendor-response-table.tsx')
-rw-r--r--lib/rfq-last/attachment/vendor-response-table.tsx296
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