summaryrefslogtreecommitdiff
path: root/lib/procurement-rfqs/table/detail-table/rfq-detail-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/procurement-rfqs/table/detail-table/rfq-detail-table.tsx')
-rw-r--r--lib/procurement-rfqs/table/detail-table/rfq-detail-table.tsx521
1 files changed, 521 insertions, 0 deletions
diff --git a/lib/procurement-rfqs/table/detail-table/rfq-detail-table.tsx b/lib/procurement-rfqs/table/detail-table/rfq-detail-table.tsx
new file mode 100644
index 00000000..ad9a19e7
--- /dev/null
+++ b/lib/procurement-rfqs/table/detail-table/rfq-detail-table.tsx
@@ -0,0 +1,521 @@
+"use client"
+
+import * as React from "react"
+import { useEffect, useState } from "react"
+import {
+ DataTableRowAction,
+ getRfqDetailColumns,
+ RfqDetailView
+} from "./rfq-detail-column"
+import { toast } from "sonner"
+
+import { Skeleton } from "@/components/ui/skeleton"
+import { Card, CardContent } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { ProcurementRfqsView } from "@/db/schema"
+import {
+ fetchCurrencies,
+ fetchIncoterms,
+ fetchPaymentTerms,
+ fetchRfqDetails,
+ fetchVendors,
+ fetchUnreadMessages
+} from "@/lib/procurement-rfqs/services"
+import { ClientDataTable } from "@/components/client-data-table/data-table"
+import { AddVendorDialog } from "./add-vendor-dialog"
+import { Button } from "@/components/ui/button"
+import { Loader2, UserPlus, BarChart2 } from "lucide-react" // 아이콘 추가
+import { DeleteRfqDetailDialog } from "./delete-vendor-dialog"
+import { UpdateRfqDetailSheet } from "./update-vendor-sheet"
+import { VendorCommunicationDrawer } from "./vendor-communication-drawer"
+import { VendorQuotationComparisonDialog } from "./vendor-quotation-comparison-dialog" // 새로운 컴포넌트 임포트
+
+// 프로퍼티 정의
+interface RfqDetailTablesProps {
+ selectedRfq: ProcurementRfqsView | null
+ maxHeight?: string | number
+}
+
+// 데이터 타입 정의
+interface Vendor {
+ id: number;
+ vendorName: string;
+ vendorCode: string | null; // Update this to allow null
+ // 기타 필요한 벤더 속성들
+}
+
+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)
+ // 상태 관리
+ const [isLoading, setIsLoading] = useState(false)
+ const [isRefreshing, setIsRefreshing] = 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)
+
+ // 벤더 커뮤니케이션 상태 관리
+ const [communicationDrawerOpen, setCommunicationDrawerOpen] = useState(false)
+ const [selectedVendor, setSelectedVendor] = useState<RfqDetailView | null>(null)
+
+ // 읽지 않은 메시지 개수
+ const [unreadMessages, setUnreadMessages] = useState<Record<number, number>>({})
+ const [isUnreadLoading, setIsUnreadLoading] = useState(false)
+
+ // 견적 비교 다이얼로그 상태 관리 (추가)
+ const [comparisonDialogOpen, setComparisonDialogOpen] = useState(false)
+
+ const existingVendorIds = React.useMemo(() => {
+ return details.map(detail => Number(detail.vendorId)).filter(Boolean);
+ }, [details]);
+
+ const handleAddVendor = async () => {
+ try {
+ setIsAdddialogLoading(true)
+
+ // 필요한 데이터 로드 (벤더, 통화, 지불조건, 인코텀즈)
+ const [vendorsData, currenciesData, paymentTermsData, incotermsData] = await Promise.all([
+ fetchVendors(),
+ fetchCurrencies(),
+ fetchPaymentTerms(),
+ fetchIncoterms()
+ ])
+
+ setVendors(vendorsData.data || [])
+ setCurrencies(currenciesData.data || [])
+ setPaymentTerms(paymentTermsData.data || [])
+ setIncoterms(incotermsData.data || [])
+
+ setVendorDialogOpen(true)
+ } catch (error) {
+ console.error("데이터 로드 오류:", error)
+ toast.error("벤더 정보를 불러오는 중 오류가 발생했습니다")
+ } finally {
+ setIsAdddialogLoading(false)
+ }
+ }
+
+ // 견적 비교 다이얼로그 열기 핸들러 (추가)
+ const handleOpenComparisonDialog = () => {
+ // 제출된 견적이 있는 벤더가 최소 1개 이상 있는지 확인
+ const hasSubmittedQuotations = details.some(detail =>
+ detail.hasQuotation && detail.quotationStatus === "Submitted"
+ );
+
+ if (!hasSubmittedQuotations) {
+ toast.warning("제출된 견적이 없습니다.");
+ return;
+ }
+
+ setComparisonDialogOpen(true);
+ }
+
+ // 읽지 않은 메시지 로드
+ const loadUnreadMessages = async () => {
+ if (!selectedRfq || !selectedRfq.id) return;
+
+ try {
+ setIsUnreadLoading(true);
+
+ // 읽지 않은 메시지 수 가져오기
+ const unreadData = await fetchUnreadMessages(selectedRfq.id);
+ setUnreadMessages(unreadData);
+ } catch (error) {
+ console.error("읽지 않은 메시지 로드 오류:", error);
+ // 조용히 실패 - 사용자에게 알림 표시하지 않음
+ } finally {
+ setIsUnreadLoading(false);
+ }
+ };
+
+ // 칼럼 정의 - unreadMessages 상태 전달
+ const columns = React.useMemo(() =>
+ getRfqDetailColumns({
+ setRowAction,
+ unreadMessages
+ }), [unreadMessages])
+
+ // 필터 필드 정의 (필터 사용 시)
+ const advancedFilterFields = React.useMemo(
+ () => [
+ {
+ id: "vendorName",
+ label: "벤더명",
+ type: "text",
+ },
+ {
+ id: "vendorCode",
+ label: "벤더 코드",
+ type: "text",
+ },
+ {
+ id: "currency",
+ label: "통화",
+ type: "text",
+ },
+ ],
+ []
+ )
+
+ // RFQ ID가 변경될 때 데이터 로드
+ useEffect(() => {
+ async function loadRfqDetails() {
+ if (!selectedRfq || !selectedRfq.id) {
+ setDetails([])
+ return
+ }
+
+ try {
+ setIsLoading(true)
+ const transformRfqDetails = (data: any[]): RfqDetailView[] => {
+ return data.map(item => ({
+ ...item,
+ // Convert vendorId from string|null to number|undefined
+ vendorId: item.vendorId ? Number(item.vendorId) : undefined,
+ // Transform any other fields that need type conversion
+ }));
+ };
+
+ // Then in your useEffect:
+ const result = await fetchRfqDetails(selectedRfq.id);
+ setDetails(transformRfqDetails(result.data));
+
+ // 읽지 않은 메시지 개수 로드
+ await loadUnreadMessages();
+ } catch (error) {
+ console.error("RFQ 디테일 로드 오류:", error)
+ setDetails([])
+ toast.error("RFQ 세부정보를 불러오는 중 오류가 발생했습니다")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ loadRfqDetails()
+ }, [selectedRfq])
+
+ // 주기적으로 읽지 않은 메시지 갱신 (60초마다)
+ useEffect(() => {
+ if (!selectedRfq || !selectedRfq.id) return;
+
+ const intervalId = setInterval(() => {
+ loadUnreadMessages();
+ }, 60000); // 60초마다 갱신
+
+ return () => clearInterval(intervalId);
+ }, [selectedRfq]);
+
+ // rowAction 처리
+ useEffect(() => {
+ if (!rowAction) return
+
+ const handleRowAction = async () => {
+ try {
+ // 통신 액션인 경우 드로어 열기
+ if (rowAction.type === "communicate") {
+ setSelectedVendor(rowAction.row.original);
+ setCommunicationDrawerOpen(true);
+
+ // 해당 벤더의 읽지 않은 메시지를 0으로 설정 (메시지를 읽은 것으로 간주)
+ const vendorId = rowAction.row.original.vendorId;
+ if (vendorId) {
+ setUnreadMessages(prev => ({
+ ...prev,
+ [vendorId]: 0
+ }));
+ }
+
+ // rowAction 초기화
+ setRowAction(null);
+ return;
+ }
+
+ // 다른 액션들은 기존과 동일하게 처리
+ setIsAdddialogLoading(true);
+
+ // 필요한 데이터 로드 (벤더, 통화, 지불조건, 인코텀즈)
+ const [vendorsData, currenciesData, paymentTermsData, incotermsData] = await Promise.all([
+ fetchVendors(),
+ fetchCurrencies(),
+ fetchPaymentTerms(),
+ fetchIncoterms()
+ ]);
+
+ setVendors(vendorsData.data || []);
+ setCurrencies(currenciesData.data || []);
+ setPaymentTerms(paymentTermsData.data || []);
+ setIncoterms(incotermsData.data || []);
+
+ // 이제 데이터가 로드되었으므로 필요한 작업 수행
+ if (rowAction.type === "update") {
+ setSelectedDetail(rowAction.row.original);
+ setUpdateSheetOpen(true);
+ } else if (rowAction.type === "delete") {
+ setSelectedDetail(rowAction.row.original);
+ setDeleteDialogOpen(true);
+ }
+ } catch (error) {
+ console.error("데이터 로드 오류:", error);
+ toast.error("데이터를 불러오는 중 오류가 발생했습니다");
+ } finally {
+ // communicate 타입이 아닌 경우에만 로딩 상태 변경
+ if (rowAction && rowAction.type !== "communicate") {
+ setIsAdddialogLoading(false);
+ }
+ }
+ };
+
+ handleRowAction();
+ }, [rowAction])
+
+ // RFQ가 선택되지 않은 경우
+ if (!selectedRfq) {
+ return (
+ <div className="flex h-full items-center justify-center text-muted-foreground">
+ RFQ를 선택하세요
+ </div>
+ )
+ }
+
+ // 로딩 중인 경우
+ if (isLoading) {
+ return (
+ <div className="p-4 space-y-4">
+ <Skeleton className="h-8 w-1/2" />
+ <Skeleton className="h-24 w-full" />
+ <Skeleton className="h-48 w-full" />
+ </div>
+ )
+ }
+
+ const handleRefreshData = async () => {
+ if (!selectedRfq || !selectedRfq.id) return
+
+ try {
+ setIsRefreshing(true)
+
+ const transformRfqDetails = (data: any[]): RfqDetailView[] => {
+ return data.map(item => ({
+ ...item,
+ // Convert vendorId from string|null to number|undefined
+ vendorId: item.vendorId ? Number(item.vendorId) : undefined,
+ // Transform any other fields that need type conversion
+ }));
+ };
+
+ // Then in your useEffect:
+ const result = await fetchRfqDetails(selectedRfq.id);
+ setDetails(transformRfqDetails(result.data));
+
+ // 읽지 않은 메시지 개수 업데이트
+ await loadUnreadMessages();
+
+ toast.success("데이터가 새로고침되었습니다")
+ } catch (error) {
+ console.error("RFQ 디테일 로드 오류:", error)
+ toast.error("데이터 새로고침 중 오류가 발생했습니다")
+ } finally {
+ setIsRefreshing(false)
+ }
+ }
+
+ // 전체 읽지 않은 메시지 수 계산
+ const totalUnreadMessages = Object.values(unreadMessages).reduce((sum, count) => sum + count, 0);
+
+ // 견적이 있는 벤더 수 계산
+ const vendorsWithQuotations = details.filter(detail => detail.hasQuotation && detail.quotationStatus === "Submitted").length;
+
+ return (
+ <div className="h-full overflow-hidden pt-4">
+
+ {/* 메시지 및 새로고침 영역 */}
+
+
+ {/* 테이블 또는 빈 상태 표시 */}
+ {details.length > 0 ? (
+
+ <ClientDataTable
+ columns={columns}
+ data={details}
+ advancedFilterFields={advancedFilterFields}
+ maxHeight={maxHeight}
+ >
+
+ <div className="flex justify-between items-center">
+ <div className="flex items-center gap-2 mr-2">
+ {totalUnreadMessages > 0 && (
+ <Badge variant="destructive" className="h-6">
+ 읽지 않은 메시지: {totalUnreadMessages}건
+ </Badge>
+ )}
+ {vendorsWithQuotations > 0 && (
+ <Badge variant="outline" className="h-6">
+ 견적 제출: {vendorsWithQuotations}개 벤더
+ </Badge>
+ )}
+ </div>
+ <div className="flex gap-2">
+ {/* 견적 비교 버튼 추가 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleOpenComparisonDialog}
+ className="gap-2"
+ disabled={
+ !selectedRfq ||
+ details.length === 0 ||
+ (!!selectedRfq.rfqSealedYn && selectedRfq.dueDate && new Date() < new Date(selectedRfq.dueDate))
+ }
+ >
+ <BarChart2 className="size-4" aria-hidden="true" />
+ <span>견적 비교</span>
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleRefreshData}
+ disabled={isRefreshing}
+ >
+ {isRefreshing ? (
+ <>
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
+ 새로고침 중...
+ </>
+ ) : (
+ '새로고침'
+ )}
+ </Button>
+ </div>
+ </div>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleAddVendor}
+ className="gap-2"
+ disabled={!selectedRfq || isAdddialogLoading}
+ >
+ {isAdddialogLoading ? (
+ <>
+ <Loader2 className="size-4 animate-spin" aria-hidden="true" />
+ <span>로딩 중...</span>
+ </>
+ ) : (
+ <>
+ <UserPlus className="size-4" aria-hidden="true" />
+ <span>벤더 추가</span>
+ </>
+ )}
+ </Button>
+ </ClientDataTable>
+
+ ) : (
+ <div className="flex h-48 items-center justify-center text-muted-foreground border rounded-md p-4">
+ <div className="flex flex-col items-center gap-4">
+ <p>해당 RFQ에 대한 협력업체가 정해지지 않았습니다. 아래 버튼을 이용하여 추가하시기 바랍니다.</p>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleAddVendor}
+ className="gap-2"
+ disabled={!selectedRfq || isAdddialogLoading}
+ >
+ {isAdddialogLoading ? (
+ <>
+ <Loader2 className="size-4 animate-spin" aria-hidden="true" />
+ <span>로딩 중...</span>
+ </>
+ ) : (
+ <>
+ <UserPlus className="size-4" aria-hidden="true" />
+ <span>협력업체 추가</span>
+ </>
+ )}
+ </Button>
+ </div>
+ </div>
+ )}
+
+ {/* 벤더 추가 다이얼로그 */}
+ <AddVendorDialog
+ open={vendorDialogOpen}
+ onOpenChange={(open) => {
+ setVendorDialogOpen(open);
+ if (!open) setIsAdddialogLoading(false);
+ }}
+ selectedRfq={selectedRfq}
+ vendors={vendors}
+ currencies={currencies}
+ paymentTerms={paymentTerms}
+ incoterms={incoterms}
+ onSuccess={handleRefreshData}
+ existingVendorIds={existingVendorIds}
+ />
+
+ {/* 벤더 정보 수정 시트 */}
+ <UpdateRfqDetailSheet
+ open={updateSheetOpen}
+ onOpenChange={setUpdateSheetOpen}
+ detail={selectedDetail}
+ vendors={vendors}
+ currencies={currencies}
+ paymentTerms={paymentTerms}
+ incoterms={incoterms}
+ onSuccess={handleRefreshData}
+ />
+
+ {/* 벤더 정보 삭제 다이얼로그 */}
+ <DeleteRfqDetailDialog
+ open={deleteDialogOpen}
+ onOpenChange={setDeleteDialogOpen}
+ detail={selectedDetail}
+ showTrigger={false}
+ onSuccess={handleRefreshData}
+ />
+
+ {/* 벤더 커뮤니케이션 드로어 */}
+ <VendorCommunicationDrawer
+ open={communicationDrawerOpen}
+ onOpenChange={(open) => {
+ setCommunicationDrawerOpen(open);
+ // 드로어가 닫힐 때 읽지 않은 메시지 개수 갱신
+ if (!open) loadUnreadMessages();
+ }}
+ selectedRfq={selectedRfq}
+ selectedVendor={selectedVendor}
+ onSuccess={handleRefreshData}
+ />
+
+ {/* 견적 비교 다이얼로그 추가 */}
+ <VendorQuotationComparisonDialog
+ open={comparisonDialogOpen}
+ onOpenChange={setComparisonDialogOpen}
+ selectedRfq={selectedRfq}
+ />
+ </div>
+ )
+} \ No newline at end of file