import * as React from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Input } from "@/components/ui/input"; import { Loader, RefreshCw, AlertCircle, CheckCircle, Info, EyeOff, ChevronDown, ChevronRight, Search } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; import { DataTableColumnJSON } from "./form-data-table-columns"; import { ExcelDownload } from "./sedp-excel-download"; import { Switch } from "../ui/switch"; import { Card, CardContent } from "@/components/ui/card"; import { useTranslation } from "@/i18n/client" import { useParams } from "next/navigation" import { fetchTagDataFromSEDP } from "@/lib/forms-plant/sedp-actions"; interface SEDPCompareDialogProps { isOpen: boolean; onClose: () => void; tableData: unknown[]; columnsJSON: DataTableColumnJSON[]; projectCode: string; formCode: string; projectType:string; packageCode:string; } interface ComparisonResult { tagNo: string; tagDesc: string; isMatching: boolean; attributes: { key: string; label: string; localValue: unknown; sedpValue: unknown; isMatching: boolean; uom?: string; }[]; } // Component for formatting display value with UOM const DisplayValue = ({ value, uom, isSedp = false }: { value: unknown; uom?: string; isSedp?: boolean }) => { if (value === "" || value === null || value === undefined) { return (empty); } // SEDP 값은 UOM을 표시하지 않음 (이미 포함되어 있다고 가정) if (isSedp) { return {value}; } // 로컬 값은 UOM과 함께 표시 return ( {value} {uom && {uom}} ); }; export function SEDPCompareDialog({ isOpen, onClose, tableData, columnsJSON, projectCode, formCode, projectType, packageCode }: SEDPCompareDialogProps) { const params = useParams() || {} const lng = params.lng ? String(params.lng) : "ko" const { t } = useTranslation(lng, "engineering") // 범례 컴포넌트 const ColorLegend = () => { return (
{t("labels.legend")}:
{t("labels.localValue")}
{t("labels.sedpValue")}
); }; // 확장 가능한 차이점 표시 컴포넌트 const DifferencesCard = ({ attributes, columnLabelMap, showOnlyDifferences }: { attributes: ComparisonResult['attributes']; columnLabelMap: Record; showOnlyDifferences: boolean; }) => { const attributesToShow = showOnlyDifferences ? attributes.filter(attr => !attr.isMatching) : attributes; if (attributesToShow.length === 0) { return (
{t("messages.allAttributesMatch")}
); } return (
{attributesToShow.map((attr) => (
{attr.label} {attr.uom && ({attr.uom})}
{attr.isMatching ? (
) : (
로컬:
SEDP:
)}
))}
); }; const [isLoading, setIsLoading] = React.useState(false); const [comparisonResults, setComparisonResults] = React.useState([]); const [activeTab, setActiveTab] = React.useState("all"); const [isExporting, setIsExporting] = React.useState(false); const [missingTags, setMissingTags] = React.useState<{ localOnly: { tagNo: string; tagDesc: string }[]; sedpOnly: { tagNo: string; tagDesc: string }[]; }>({ localOnly: [], sedpOnly: [] }); const [showOnlyDifferences, setShowOnlyDifferences] = React.useState(true); const [searchTerm, setSearchTerm] = React.useState(""); const [expandedRows, setExpandedRows] = React.useState>(new Set()); // Stats for summary const totalTags = comparisonResults.length; const matchingTags = comparisonResults.filter(r => r.isMatching).length; const nonMatchingTags = totalTags - matchingTags; const totalMissingTags = missingTags.localOnly.length + missingTags.sedpOnly.length; // Get column label map and UOM map for better display const { columnLabelMap, columnUomMap } = React.useMemo(() => { const labelMap: Record = {}; const uomMap: Record = {}; columnsJSON.forEach(col => { labelMap[col.key] = col.displayLabel || col.label; if (col.uom) { uomMap[col.key] = col.uom; } }); return { columnLabelMap: labelMap, columnUomMap: uomMap }; }, [columnsJSON]); // Filter and search results const filteredResults = React.useMemo(() => { let results = comparisonResults; // Filter by tab switch (activeTab) { case "matching": results = results.filter(r => r.isMatching); break; case "differences": results = results.filter(r => !r.isMatching); break; case "all": default: break; } // Apply search filter if (searchTerm.trim()) { const search = searchTerm.toLowerCase(); results = results.filter(r => r.tagNo.toLowerCase().includes(search) || r.tagDesc.toLowerCase().includes(search) ); } return results; }, [comparisonResults, activeTab, searchTerm]); // Toggle row expansion const toggleRowExpansion = (tagNo: string) => { const newExpanded = new Set(expandedRows); if (newExpanded.has(tagNo)) { newExpanded.delete(tagNo); } else { newExpanded.add(tagNo); } setExpandedRows(newExpanded); }; // Auto-expand rows with differences when switching to differences tab React.useEffect(() => { if (activeTab === "differences") { const newExpanded = new Set(); filteredResults.filter(r => !r.isMatching).forEach(r => { newExpanded.add(r.tagNo); }); setExpandedRows(newExpanded); } }, [activeTab, filteredResults]); const fetchAndCompareData = React.useCallback(async () => { if (!projectCode || !formCode) { toast.error("Project code or form code is missing"); return; } try { setIsLoading(true); // Fetch data from SEDP API const sedpData = await fetchTagDataFromSEDP(projectCode, formCode); // Get the table name from the response const tableName = Object.keys(sedpData)[0]; const sedpTagEntries = sedpData[tableName] || []; // Create a map of SEDP data by TAG_NO for quick lookup const sedpTagMap = new Map(); const packageCodeAttId = projectType === "ship" ? "CM3003" : "ME5074"; const tagEntries = sedpTagEntries.filter(entry => { if (Array.isArray(entry.ATTRIBUTES)) { const packageCodeAttr = entry.ATTRIBUTES.find(attr => attr.ATT_ID === packageCodeAttId); if (packageCodeAttr && packageCodeAttr.VALUE === packageCode) { return true; } } return false; }); tagEntries.forEach((entry: Record) => { const tagNo = entry.TAG_NO; const attributesMap = new Map(); // Convert attributes array to map for easier access if (Array.isArray(entry.ATTRIBUTES)) { entry.ATTRIBUTES.forEach((attr: Record) => { attributesMap.set(attr.ATT_ID, attr.VALUE); }); } sedpTagMap.set(tagNo, { tagDesc: entry.TAG_DESC, attributes: attributesMap }); }); // Create sets for finding missing tags const localTagNos = new Set(tableData.map(item => item.TAG_NO)); const sedpTagNos = new Set(sedpTagMap.keys()); // Find missing tags const localOnlyTags = tableData .filter(item => !sedpTagMap.has(item.TAG_NO)) .map(item => ({ tagNo: item.TAG_NO, tagDesc: item.TAG_DESC || "" })); const sedpOnlyTags = Array.from(sedpTagMap.entries()) .filter(([tagNo]) => !localTagNos.has(tagNo)) .map(([tagNo, data]) => ({ tagNo, tagDesc: data.tagDesc || "" })); setMissingTags({ localOnly: localOnlyTags, sedpOnly: sedpOnlyTags }); // Compare with local table data (only for tags that exist in both systems) const results: ComparisonResult[] = tableData .filter(localItem => sedpTagMap.has(localItem.TAG_NO)) .map(localItem => { const tagNo = localItem.TAG_NO; const sedpItem = sedpTagMap.get(tagNo); // Compare attributes const attributeComparisons = columnsJSON .filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC" && col.key !== "status"&& col.key !== "CLS_ID") .map(col => { const localValue = localItem[col.key]; const sedpValue = sedpItem.attributes.get(col.key); const uom = columnUomMap[col.key]; // Compare values (with type handling) let isMatching = false; // Special case: Empty SEDP value and 0 local value if ((sedpValue === "" || sedpValue === null || sedpValue === undefined) && (localValue === 0 || localValue === "0")) { isMatching = true; } else { // Standard string comparison for other cases const normalizedLocal = localValue === undefined || localValue === null ? "" : String(localValue).trim(); const normalizedSedp = sedpValue === undefined || sedpValue === null ? "" : String(sedpValue).trim(); isMatching = normalizedLocal === normalizedSedp; } return { key: col.key, label: columnLabelMap[col.key] || col.key, localValue, sedpValue, isMatching, uom }; }); // Item is matching if all attributes match const isItemMatching = attributeComparisons.every(attr => attr.isMatching); return { tagNo, tagDesc: localItem.TAG_DESC || "", isMatching: isItemMatching, attributes: attributeComparisons }; }); setComparisonResults(results); // Show summary in toast const matchCount = results.filter(r => r.isMatching).length; const nonMatchCount = results.length - matchCount; const missingCount = localOnlyTags.length + sedpOnlyTags.length; if (missingCount > 0) { toast.error(`Found ${missingCount} missing tags between systems`); } if (nonMatchCount > 0) { toast.warning(`Found ${nonMatchCount} tags with differences`); } else if (results.length > 0 && missingCount === 0) { toast.success(`All ${results.length} tags match with SEDP data`); } else if (results.length === 0 && missingCount === 0) { toast.info("No tags to compare"); } } catch (error) { console.error("SEDP comparison error:", error); toast.error(`Failed to compare with SEDP: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setIsLoading(false); } }, [projectCode, formCode, tableData, columnsJSON, columnLabelMap, columnUomMap]); // Fetch data when dialog opens React.useEffect(() => { if (isOpen) { fetchAndCompareData(); } }, [isOpen, fetchAndCompareData]); return ( {t("dialogs.sedpDataComparison")}
{/* 검색 입력 */}
setSearchTerm(e.target.value)} className="pl-8 w-64" />
{matchingTags} / {totalTags} 일치 {totalMissingTags > 0 ? `(${totalMissingTags} 누락)` : ''}
{/* 범례 */}
{t("tabs.allTags")} ({totalTags}) {t("tabs.differences")} ({nonMatchingTags}) {t("tabs.matching")} ({matchingTags}) 0 ? "text-red-500" : ""}> {t("tabs.missingTags")} ({totalMissingTags}) {isLoading ? (
{t("messages.dataComparing")}
) : activeTab === "missing" ? ( // Missing tags tab content
{missingTags.localOnly.length > 0 && (

{t("sections.localOnlyTags")} ({missingTags.localOnly.length})

{t("labels.tagNo")} {t("labels.description")} {missingTags.localOnly.map((tag) => ( {tag.tagNo} {tag.tagDesc} ))}
)} {missingTags.sedpOnly.length > 0 && (

{t("sections.sedpOnlyTags")} ({missingTags.sedpOnly.length})

{t("labels.tagNo")} {t("labels.description")} {missingTags.sedpOnly.map((tag) => ( {tag.tagNo} {tag.tagDesc} ))}
)} {totalMissingTags === 0 && (
{t("messages.allTagsExistInBothSystems")}
)}
) : filteredResults.length > 0 ? ( // 개선된 확장 가능한 테이블 구조
{t("labels.tagNo")} {t("labels.description")} {t("labels.status")} 차이점 개수 {filteredResults.map((result) => ( {/* 메인 행 */} toggleRowExpansion(result.tagNo)} > {result.attributes.some(attr => !attr.isMatching) ? ( expandedRows.has(result.tagNo) ? ( ) : ( ) ) : null} {result.tagNo}
{result.tagDesc}
{result.isMatching ? ( {t("labels.matching")} ) : ( {t("labels.different")} )} {!result.isMatching && ( {result.attributes.filter(attr => !attr.isMatching).length}{t("sections.differenceCount")} )}
{/* 확장된 차이점 표시 행 */} {expandedRows.has(result.tagNo) && ( )}
))}
) : (
{searchTerm ? t("messages.noSearchResults") : t("messages.noMatchingTags")}
)}
); }