diff options
Diffstat (limited to 'components/form-data-plant/form-data-table.tsx')
| -rw-r--r-- | components/form-data-plant/form-data-table.tsx | 1377 |
1 files changed, 1377 insertions, 0 deletions
diff --git a/components/form-data-plant/form-data-table.tsx b/components/form-data-plant/form-data-table.tsx new file mode 100644 index 00000000..9e7b3901 --- /dev/null +++ b/components/form-data-plant/form-data-table.tsx @@ -0,0 +1,1377 @@ +"use client"; + +import * as React from "react"; +import { useParams, useRouter, usePathname } from "next/navigation"; +import { useTranslation } from "@/i18n/client"; + +import { ClientDataTable } from "../client-data-table/data-table"; +import { + getColumns, + DataTableRowAction, + DataTableColumnJSON, + ColumnType, + Register, +} from "./form-data-table-columns"; +import type { DataTableAdvancedFilterField } from "@/types/table"; +import { Button } from "../ui/button"; +import { + Download, + Loader, + Upload, + Plus, + Tag, + TagsIcon, + FileOutput, + Clipboard, + Send, + GitCompareIcon, + RefreshCcw, + Trash2, + Eye, + FileText, + Target, + CheckCircle2, + AlertCircle, + Clock +} from "lucide-react"; +import { toast } from "sonner"; +import { + getPackageCodeById, + getProjectById, + getReportTempList, + sendFormDataToSEDP, + syncMissingTags, excludeFormDataByTags, getRegisters +} from "@/lib/forms-plant/services"; +import { UpdateTagSheet } from "./update-form-sheet"; +import { FormDataReportTempUploadDialog } from "./form-data-report-temp-upload-dialog"; +import { FormDataReportDialog } from "./form-data-report-dialog"; +import { FormDataReportBatchDialog } from "./form-data-report-batch-dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { AddFormTagDialog } from "./add-formTag-dialog"; +import { importExcelData } from "./import-excel-form"; +import { exportExcelData } from "./export-excel-form"; +import { SEDPConfirmationDialog, SEDPStatusDialog } from "./sedp-components"; +import { SEDPCompareDialog } from "./sedp-compare-dialog"; +import { DeleteFormDataDialog } from "./delete-form-data-dialog"; +import { TemplateViewDialog } from "./spreadJS-dialog"; +import { fetchTemplateFromSEDP } from "@/lib/forms-plant/sedp-actions"; +import { FormStatusByVendor, getFormStatusByVendor } from "@/lib/forms-plant/stat"; +import { + Card, + CardContent, + CardHeader, + CardTitle +} from "@/components/ui/card"; +import { XCircle } from "lucide-react"; // 기존 import 리스트에 추가 + +interface GenericData { + [key: string]: unknown; +} + +export interface DynamicTableProps { + dataJSON: GenericData[]; + columnsJSON: DataTableColumnJSON[]; + contractItemId: number; + formCode: string; + formId: number; + projectId: number; + formName?: string; + objectCode?: string; + mode: "IM" | "ENG"; // 모드 속성 + editableFieldsMap?: Map<string, string[]>; // 새로 추가 +} + +export default function DynamicTable({ + dataJSON, + columnsJSON, + contractItemId, + formCode, + formId, + projectId, + mode = "IM", // 기본값 설정 + formName = `${formCode}`, // Default form name based on formCode + editableFieldsMap = new Map(), // 새로 추가 +}: DynamicTableProps) { + const params = useParams(); + const router = useRouter(); + const lng = (params?.lng as string) || "ko"; + const { t } = useTranslation(lng, "engineering"); + + + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<GenericData> | null>(null); + const [tableData, setTableData] = React.useState<GenericData[]>(dataJSON); + + // 배치 선택 관련 상태 + const [selectedRowsData, setSelectedRowsData] = React.useState<GenericData[]>([]); + const [clearSelection, setClearSelection] = React.useState(false); + // 삭제 관련 상태 간소화 + const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); + const [deleteTarget, setDeleteTarget] = React.useState<GenericData[]>([]); + + const [formStats, setFormStats] = React.useState<FormStatusByVendor | null>(null); + const [isLoadingStats, setIsLoadingStats] = React.useState(true); + + const [activeFilter, setActiveFilter] = React.useState<string | null>(null); + const [filteredTableData, setFilteredTableData] = React.useState<GenericData[]>(tableData); + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}); + + const [isExcludingTags, setIsExcludingTags] = React.useState(false); + + const handleExcludeTags = async () => { + const selectedRows = getSelectedRowsData(); + + if (selectedRows.length === 0) { + toast.error(t("messages.noTagsSelected")); + return; + } + + // 확인 다이얼로그 + const confirmMessage = t("messages.confirmExclude", { + count: selectedRows.length + }) || `선택한 ${selectedRows.length}개의 태그를 제외 처리하시겠습니까?`; + + if (!confirm(confirmMessage)) { + return; + } + + setIsExcludingTags(true); + + try { + // TAG_NO 목록 추출 + const tagNumbers = selectedRows + .map(row => row.TAG_NO) + .filter(tagNo => tagNo !== null && tagNo !== undefined); + + if (tagNumbers.length === 0) { + toast.error(t("messages.noValidTags")); + return; + } + + // 서버 액션 호출 + const result = await excludeFormDataByTags({ + formCode, + contractItemId, + tagNumbers, + }); + + if (result.success) { + toast.success( + t("messages.tagsExcluded", { count: result.excludedCount }) || + `${result.excludedCount}개의 태그가 제외되었습니다.` + ); + + // 로컬 상태 업데이트 + setTableData(prev => + prev.map(item => { + if (tagNumbers.includes(item.TAG_NO)) { + return { + ...item, + status: 'excluded', + excludedAt: new Date().toISOString() + }; + } + return item; + }) + ); + + // 선택 상태 초기화 + setClearSelection(true); + setTimeout(() => setClearSelection(false), 100); + } else { + toast.error(result.error || t("messages.excludeFailed")); + } + } catch (error) { + console.error("Error excluding tags:", error); + toast.error(t("messages.excludeError") || "태그 제외 중 오류가 발생했습니다."); + } finally { + setIsExcludingTags(false); + } + }; + + // 필터링 로직 + React.useEffect(() => { + if (!activeFilter) { + setFilteredTableData(tableData); + return; + } + + const today = new Date(); + today.setHours(0, 0, 0, 0); + const sevenDaysLater = new Date(today); + sevenDaysLater.setDate(sevenDaysLater.getDate() + 7); + + let filtered = [...tableData]; + + switch (activeFilter) { + case 'completed': + // 모든 필수 필드가 완료된 태그만 표시 + filtered = tableData.filter(item => { + const tagEditableFields = editableFieldsMap.get(item.TAG_NO) || []; + return columnsJSON + .filter(col => (col.shi === 'IN' || col.shi === 'BOTH') && tagEditableFields.includes(col.key)) + .every(col => { + const value = item[col.key]; + return value !== undefined && value !== null && value !== ''; + }); + }); + break; + + case 'remaining': + // 미완료 필드가 있는 태그만 표시 + filtered = tableData.filter(item => { + const tagEditableFields = editableFieldsMap.get(item.TAG_NO) || []; + return columnsJSON + .filter(col => (col.shi === 'IN' || col.shi === 'BOTH') && tagEditableFields.includes(col.key)) + .some(col => { + const value = item[col.key]; + return value === undefined || value === null || value === ''; + }); + }); + break; + + case 'upcoming': + // 7일 이내 임박한 태그만 표시 + filtered = tableData.filter(item => { + const dueDate = item.DUE_DATE; + if (!dueDate) return false; + + const target = new Date(dueDate); + target.setHours(0, 0, 0, 0); + + // 미완료이면서 7일 이내인 경우 + const hasIncompleteFields = columnsJSON + .filter(col => col.shi === 'IN' || col.shi === 'BOTH') + .some(col => !item[col.key]); + + return hasIncompleteFields && target >= today && target <= sevenDaysLater; + }); + break; + + case 'overdue': + // 지연된 태그만 표시 + filtered = tableData.filter(item => { + const dueDate = item.DUE_DATE; + if (!dueDate) return false; + + const target = new Date(dueDate); + target.setHours(0, 0, 0, 0); + + // 미완료이면서 지연된 경우 + const hasIncompleteFields = columnsJSON + .filter(col => col.shi === 'IN' || col.shi === 'BOTH') + .some(col => !item[col.key]); + + return hasIncompleteFields && target < today; + }); + break; + + default: + filtered = tableData; + } + + setFilteredTableData(filtered); + }, [activeFilter, tableData, columnsJSON, editableFieldsMap]); + + // 카드 클릭 핸들러 + const handleCardClick = (filterType: string | null) => { + setActiveFilter(prev => prev === filterType ? null : filterType); + }; + + React.useEffect(() => { + const fetchFormStats = async () => { + try { + setIsLoadingStats(true); + // getFormStatusByVendor 서버 액션 직접 호출 + const data = await getFormStatusByVendor(projectId, contractItemId, formCode); + + if (data && data.length > 0) { + setFormStats(data[0]); + } + } catch (error) { + console.error("Failed to fetch form stats:", error); + toast.error("통계 데이터를 불러오는데 실패했습니다."); + } finally { + setIsLoadingStats(false); + } + }; + + if (projectId && formCode) { + fetchFormStats(); + } + }, [projectId, formCode]); + + // Update tableData when dataJSON changes + React.useEffect(() => { + setTableData(dataJSON); + }, [dataJSON]); + + // 폴링 상태 관리를 위한 ref + const pollingRef = React.useRef<NodeJS.Timeout | null>(null); + + // Separate loading states for different operations + const [isSyncingTags, setIsSyncingTags] = React.useState(false); + const [isImporting, setIsImporting] = React.useState(false); + const [isExporting, setIsExporting] = React.useState(false); + const [isSaving] = React.useState(false); + const [isSendingSEDP, setIsSendingSEDP] = React.useState(false); + const [isLoadingTags, setIsLoadingTags] = React.useState(false); + const [isLoadingTemplate, setIsLoadingTemplate] = React.useState(false); // 새로 추가 + + // Any operation in progress + const isAnyOperationPending = isSyncingTags || isImporting || isExporting || isSaving || isSendingSEDP || isLoadingTags || isLoadingTemplate || isExcludingTags; + + // SEDP dialogs state + const [sedpConfirmOpen, setSedpConfirmOpen] = React.useState(false); + const [sedpStatusOpen, setSedpStatusOpen] = React.useState(false); + const [sedpStatusData, setSedpStatusData] = React.useState({ + status: 'success' as 'success' | 'error' | 'partial', + message: '', + successCount: 0, + errorCount: 0, + totalCount: 0 + }); + + // SEDP compare dialog state + const [sedpCompareOpen, setSedpCompareOpen] = React.useState(false); + const [projectCode, setProjectCode] = React.useState<string>(''); + const [projectType, setProjectType] = React.useState<string>('plant'); + const [packageCode, setPackageCode] = React.useState<string>(''); + + // 새로 추가된 Template 다이얼로그 상태 + const [templateDialogOpen, setTemplateDialogOpen] = React.useState(false); + const [templateData, setTemplateData] = React.useState<unknown>(null); + + const [tempUpDialog, setTempUpDialog] = React.useState(false); + const [reportData, setReportData] = React.useState<GenericData[]>([]); + const [batchDownDialog, setBatchDownDialog] = React.useState(false); + const [tempCount, setTempCount] = React.useState(0); + const [addTagDialogOpen, setAddTagDialogOpen] = React.useState(false); + + const [registers, setRegisters] = React.useState<Register[]>([]); +const [isLoadingRegisters, setIsLoadingRegisters] = React.useState(false); + + + // TAG_NO가 있는 첫 번째 행의 shi 값 확인 + const isAddTagDisabled = React.useMemo(() => { + const firstRowWithTagNo = tableData.find(row => row.TAG_NO); + return firstRowWithTagNo?.shi === true; + }, [tableData]); + + // Clean up polling on unmount + React.useEffect(() => { + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + }; + }, []); + + React.useEffect(() => { + const getTempCount = async () => { + const tempList = await getReportTempList(contractItemId, formId); + setTempCount(tempList.length); + }; + + getTempCount(); + }, [contractItemId, formId, tempUpDialog]); + + React.useEffect(() => { + const getPackageCode = async () => { + try { + const packageCode = await getPackageCodeById(contractItemId); + setPackageCode(packageCode || ''); // 빈 문자열이나 다른 기본값 + } catch (error) { + console.error('패키지 조회 실패:', error); + setPackageCode(''); + } + }; + + getPackageCode(); + }, [contractItemId]) + // Get project code when component mounts + React.useEffect(() => { + const getProjectCode = async () => { + try { + const project = await getProjectById(projectId); + setProjectCode(project.code); + setProjectType(project.type); + } catch (error) { + console.error("Error fetching project code:", error); + toast.error("Failed to fetch project code"); + } + }; + + if (projectId) { + getProjectCode(); + } + }, [projectId]); + + // 선택된 행들의 실제 데이터 가져오기 + const getSelectedRowsData = React.useCallback(() => { + return selectedRowsData; + }, [selectedRowsData]); + + // 선택된 행 개수 계산 + const selectedRowCount = React.useMemo(() => { + return selectedRowsData.length; + }, [selectedRowsData]); + + // 프로젝트 코드를 가져오는 useEffect (기존 코드 참고) +React.useEffect(() => { + const fetchRegisters = async () => { + if (!projectCode) return; // projectCode가 있는지 확인 + + setIsLoadingRegisters(true); + try { + const registersData = await getRegisters(projectCode); + setRegisters(registersData); + console.log('✅ Registers loaded:', registersData.length); + } catch (error) { + console.error('❌ Failed to load registers:', error); + toast.error('레지스터 정보를 불러오는데 실패했습니다.'); + } finally { + setIsLoadingRegisters(false); + } + }; + + fetchRegisters(); +}, [projectCode]); + + + const columns = React.useMemo( + () => + getColumns({ + columnsJSON, + setRowAction, + setReportData, + tempCount, + onRowSelectionChange: setRowSelection, + templateData, // 기존 + registers, // 새로 추가 + }), + [columnsJSON, setRowAction, setReportData, tempCount, templateData, registers] + ); + + function mapColumnTypeToAdvancedFilterType( + columnType: ColumnType + ): DataTableAdvancedFilterField<GenericData>["type"] { + switch (columnType) { + case "STRING": + return "text"; + case "NUMBER": + return "number"; + case "LIST": + return "select"; + default: + return "text"; + } + } + + const advancedFilterFields = React.useMemo< + DataTableAdvancedFilterField<GenericData>[] + >(() => { + return columnsJSON.map((col) => ({ + id: col.key, + label: col.label, + type: mapColumnTypeToAdvancedFilterType(col.type), + options: + col.type === "LIST" + ? col.options?.map((v) => ({ label: v, value: v })) + : undefined, + })); + }, [columnsJSON]); + + // 새로 추가된 Template 가져오기 함수 + const handleGetTemplate = async () => { + + if (!projectCode) { + toast.error("Project code is not available"); + return; + } + + try { + setIsLoadingTemplate(true); + + const templateResult = await fetchTemplateFromSEDP(projectCode, formCode); + + // 🔍 전달되는 템플릿 데이터 로깅 + console.log('📊 Template data received from SEDP:', { + count: Array.isArray(templateResult) ? templateResult.length : 'not array', + isArray: Array.isArray(templateResult), + data: templateResult + }); + + if (Array.isArray(templateResult)) { + templateResult.forEach((tmpl, idx) => { + console.log(` [${idx}] TMPL_ID: ${tmpl?.TMPL_ID || 'MISSING'}, NAME: ${tmpl?.NAME || 'N/A'}, TYPE: ${tmpl?.TMPL_TYPE || 'N/A'}`); + }); + } + + setTemplateData(templateResult); + setTemplateDialogOpen(true); + + toast.success("Template data loaded successfully"); + } catch (error) { + console.error("Error fetching template:", error); + toast.error("Failed to fetch template from SEDP"); + } finally { + setIsLoadingTemplate(false); + } + }; + + // IM 모드: 태그 동기화 함수 + async function handleSyncTags() { + try { + setIsSyncingTags(true); + const result = await syncMissingTags(contractItemId, formCode); + + // Prepare the toast messages based on what changed + const changes = []; + if (result.createdCount > 0) + changes.push(`${result.createdCount}건 태그 생성`); + if (result.updatedCount > 0) + changes.push(`${result.updatedCount}건 태그 업데이트`); + if (result.deletedCount > 0) + changes.push(`${result.deletedCount}건 태그 삭제`); + + if (changes.length > 0) { + // If any changes were made, show success message and reload + toast.success(`동기화 완료: ${changes.join(", ")}`); + router.refresh(); // Use router.refresh instead of location.reload + } else { + // If no changes were made, show an info message + toast.info("변경사항이 없습니다. 모든 태그가 최신 상태입니다."); + } + } catch (err) { + console.error(err); + toast.error("태그 동기화 중 에러가 발생했습니다."); + } finally { + setIsSyncingTags(false); + } + } + + // ENG 모드: 태그 가져오기 함수 + const handleGetTags = async () => { + try { + setIsLoadingTags(true); + + // API 엔드포인트 호출 - 작업 시작만 요청 + const response = await fetch('/api/cron/form-tags/start', { + method: 'POST', + body: JSON.stringify({ projectCode, formCode, contractItemId }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to start tag import'); + } + + const data = await response.json(); + + // 작업 ID 저장 + if (data.syncId) { + toast.info('Tag import started. This may take a while...'); + + // 상태 확인을 위한 폴링 시작 + startPolling(data.syncId); + } else { + throw new Error('No import ID returned from server'); + } + } catch (error) { + console.error('Error starting tag import:', error); + toast.error( + error instanceof Error + ? error.message + : 'An error occurred while starting tag import' + ); + setIsLoadingTags(false); + } + }; + + const startPolling = (id: string) => { + // 이전 폴링이 있다면 제거 + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + + // 5초마다 상태 확인 + pollingRef.current = setInterval(async () => { + try { + const response = await fetch(`/api/cron/form-tags/status?id=${id}`); + + if (!response.ok) { + throw new Error('Failed to get tag import status'); + } + + const data = await response.json(); + + if (data.status === 'completed') { + // 폴링 중지 + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + + router.refresh(); + + // 상태 초기화 + setIsLoadingTags(false); + + // 성공 메시지 표시 + toast.success( + `Tags imported successfully! ${data.result?.processedCount || 0} items processed.` + ); + + } else if (data.status === 'failed') { + // 에러 처리 + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + + setIsLoadingTags(false); + toast.error(data.error || 'Import failed'); + } else if (data.status === 'processing') { + // 진행 상태 업데이트 (선택적) + if (data.progress) { + toast.info(`Import in progress: ${data.progress}%`, { + id: `import-progress-${id}`, + }); + } + } + } catch (error) { + console.error('Error checking importing status:', error); + } + }, 5000); // 5초마다 체크 + }; + + // Excel Import - Fixed version with proper loading state management + async function handleImportExcel(e: React.ChangeEvent<HTMLInputElement>) { + const file = e.target.files?.[0]; + if (!file) return; + + try { + // Don't set setIsImporting here - let importExcelData handle it completely + // setIsImporting(true); // Remove this line + + // Call the updated importExcelData function with editableFieldsMap + const result = await importExcelData({ + file, + tableData, + columnsJSON, + formCode, + contractItemId, + editableFieldsMap, // 추가: 편집 가능 필드 정보 전달 + onPendingChange: setIsImporting, // Let importExcelData handle loading state + onDataUpdate: (newData) => { + setTableData(Array.isArray(newData) ? newData : newData(tableData)); + } + }); + + // If import and save was successful, refresh the page + if (result.success) { + // Show additional info about skipped fields if any + if (result.skippedFields && result.skippedFields.length > 0) { + console.log("Import completed with some fields skipped:", result.skippedFields); + } + + // Ensure loading state is cleared before refresh + setIsImporting(false); + + // Add a small delay to ensure state update is processed + setTimeout(() => { + router.refresh(); + }, 100); + } + } catch (error) { + console.error("Import failed:", error); + toast.error("Failed to import Excel data"); + // Ensure loading state is cleared on error + setIsImporting(false); + } finally { + // Always clear the file input value + e.target.value = ""; + // Don't set setIsImporting(false) here since we handle it above + } + } + // SEDP Send handler (with confirmation) + function handleSEDPSendClick() { + if (tableData.length === 0) { + toast.error("No data to send to SEDP"); + return; + } + + // Open confirmation dialog + setSedpConfirmOpen(true); + } + + // Handle SEDP compare button click + function handleSEDPCompareClick() { + if (tableData.length === 0) { + toast.error("No data to compare with SEDP"); + return; + } + + if (!projectCode) { + toast.error("Project code is not available"); + return; + } + + // Open compare dialog + setSedpCompareOpen(true); + } + + // Actual SEDP send after confirmation + async function handleSEDPSendConfirmed() { + try { + setIsSendingSEDP(true); + + // Validate data + const invalidData = tableData.filter((item) => { + const tagNo = item.TAG_NO; + return !tagNo || (typeof tagNo === 'string' && !tagNo.trim()); + }); + if (invalidData.length > 0) { + toast.error(`태그 번호가 없는 항목이 ${invalidData.length}개 있습니다.`); + setSedpConfirmOpen(false); + return; + } + + // Then send to SEDP - pass formCode instead of formName + const sedpResult = await sendFormDataToSEDP( + formCode, // Send formCode instead of formName + projectId, // Project ID + contractItemId, + tableData.filter(v=>v.status !== 'excluded'), // Table data + columnsJSON // Column definitions + ); + + // Close confirmation dialog + setSedpConfirmOpen(false); + + // Set status data based on result + if (sedpResult.success) { + setSedpStatusData({ + status: 'success', + message: "Data successfully sent to SEDP", + successCount: tableData.length, + errorCount: 0, + totalCount: tableData.length + }); + } else { + setSedpStatusData({ + status: 'error', + message: sedpResult.message || "Failed to send data to SEDP", + successCount: 0, + errorCount: tableData.length, + totalCount: tableData.length + }); + } + + // Open status dialog to show result + setSedpStatusOpen(true); + + // Refresh the route to get fresh data + router.refresh(); + + } catch (err: unknown) { + console.error("SEDP error:", err); + + // Set error status + setSedpStatusData({ + status: 'error', + message: err instanceof Error ? err.message : "An unexpected error occurred", + successCount: 0, + errorCount: tableData.length, + totalCount: tableData.length + }); + + // Close confirmation and open status + setSedpConfirmOpen(false); + setSedpStatusOpen(true); + + } finally { + setIsSendingSEDP(false); + } + } + + // Template Export + async function handleExportExcel() { + try { + setIsExporting(true); + await exportExcelData({ + tableData, + columnsJSON, + formCode, + editableFieldsMap, + onPendingChange: setIsExporting + }); + } finally { + setIsExporting(false); + } + } + + // Handle batch document with smart selection logic + const handleBatchDocument = () => { + if (tempCount === 0) { + toast.error("업로드된 Template File이 없습니다."); + return; + } + + // 선택된 항목이 있으면 선택된 것만, 없으면 전체 사용 + const selectedData = getSelectedRowsData(); + if (selectedData.length > 0) { + toast.info(`선택된 ${selectedData.length}개 항목으로 배치 문서를 생성합니다.`); + } else { + toast.info(`전체 ${tableData.length}개 항목으로 배치 문서를 생성합니다.`); + } + + setBatchDownDialog(true); + }; + + // 개별 행 삭제 핸들러 + const handleDeleteRow = (rowData: GenericData) => { + setDeleteTarget([rowData]); + setDeleteDialogOpen(true); + }; + + // 배치 삭제 핸들러 + const handleBatchDelete = () => { + const selectedData = getSelectedRowsData(); + if (selectedData.length === 0) { + toast.error("삭제할 항목을 선택해주세요."); + return; + } + + setDeleteTarget(selectedData); + setDeleteDialogOpen(true); + }; + + // 삭제 성공 후 처리 + const handleDeleteSuccess = () => { + // 로컬 상태에서 삭제된 항목들 제거 + const tagNosToDelete = deleteTarget + .map(item => item.TAG_NO) + .filter(Boolean); + + setTableData(prev => + prev.filter(item => !tagNosToDelete.includes(item.TAG_NO)) + ); + + // 선택 상태 초기화 + setSelectedRowsData([]); + setClearSelection(prev => !prev); // ClientDataTable의 선택 상태 초기화 + + // 삭제 타겟 초기화 + setDeleteTarget([]); + }; + + // rowAction 처리 부분 수정 + React.useEffect(() => { + if (rowAction?.type === "delete") { + handleDeleteRow(rowAction.row.original); + setRowAction(null); // 액션 초기화 + } + }, [rowAction]); + + + return ( + <> + + <div className="mb-6"> + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5"> + {/* Total Tags Card - 클릭 시 전체 보기 */} + <Card + className={`cursor-pointer transition-all ${activeFilter === null ? 'ring-2 ring-primary' : 'hover:shadow-lg' + }`} + onClick={() => handleCardClick(null)} + > + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium"> + Total Tags + </CardTitle> + <FileText className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {isLoadingStats ? ( + <span className="animate-pulse">-</span> + ) : ( + formStats?.tagCount || 0 + )} + </div> + <p className="text-xs text-muted-foreground"> + {activeFilter === null ? 'Showing all' : 'Click to show all'} + </p> + </CardContent> + </Card> + + {/* Completed Fields Card */} + <Card + className={`cursor-pointer transition-all ${activeFilter === 'completed' ? 'ring-2 ring-green-600' : 'hover:shadow-lg' + }`} + onClick={() => handleCardClick('completed')} + > + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium"> + Completed + </CardTitle> + <CheckCircle2 className="h-4 w-4 text-green-600" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold text-green-600"> + {isLoadingStats ? ( + <span className="animate-pulse">-</span> + ) : ( + formStats?.completedFields || 0 + )} + </div> + <p className="text-xs text-muted-foreground"> + {activeFilter === 'completed' ? 'Filtering active' : 'Click to filter'} + </p> + </CardContent> + </Card> + + {/* Remaining Fields Card */} + <Card + className={`cursor-pointer transition-all ${activeFilter === 'remaining' ? 'ring-2 ring-blue-600' : 'hover:shadow-lg' + }`} + onClick={() => handleCardClick('remaining')} + > + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium"> + Remaining + </CardTitle> + <Clock className="h-4 w-4 text-muted-foreground" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {isLoadingStats ? ( + <span className="animate-pulse">-</span> + ) : ( + (formStats?.totalFields || 0) - (formStats?.completedFields || 0) + )} + </div> + <p className="text-xs text-muted-foreground"> + {activeFilter === 'remaining' ? 'Filtering active' : 'Click to filter'} + </p> + </CardContent> + </Card> + + {/* Upcoming Card */} + <Card + className={`cursor-pointer transition-all ${activeFilter === 'upcoming' ? 'ring-2 ring-yellow-600' : 'hover:shadow-lg' + }`} + onClick={() => handleCardClick('upcoming')} + > + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium"> + Upcoming + </CardTitle> + <AlertCircle className="h-4 w-4 text-yellow-600" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold text-yellow-600"> + {isLoadingStats ? ( + <span className="animate-pulse">-</span> + ) : ( + formStats?.upcomingCount || 0 + )} + </div> + <p className="text-xs text-muted-foreground"> + {activeFilter === 'upcoming' ? 'Filtering active' : 'Click to filter'} + </p> + </CardContent> + </Card> + + {/* Overdue Card */} + <Card + className={`cursor-pointer transition-all ${activeFilter === 'overdue' ? 'ring-2 ring-red-600' : 'hover:shadow-lg' + }`} + onClick={() => handleCardClick('overdue')} + > + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium"> + Overdue + </CardTitle> + <AlertCircle className="h-4 w-4 text-red-600" /> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold text-red-600"> + {isLoadingStats ? ( + <span className="animate-pulse">-</span> + ) : ( + formStats?.overdueCount || 0 + )} + </div> + <p className="text-xs text-muted-foreground"> + {activeFilter === 'overdue' ? 'Filtering active' : 'Click to filter'} + </p> + </CardContent> + </Card> + </div> + </div> + + + <ClientDataTable + data={filteredTableData} // tableData 대신 filteredTableData 사용 + columns={columns} + advancedFilterFields={advancedFilterFields} + autoSizeColumns + onSelectedRowsChange={setSelectedRowsData} + clearSelection={clearSelection} + > + {/* 필터 상태 표시 */} + {activeFilter && ( + <div className="flex items-center gap-2 mr-auto"> + <span className="text-sm text-muted-foreground"> + Filter: {activeFilter === 'completed' ? 'Completed' : + activeFilter === 'remaining' ? 'Remaining' : + activeFilter === 'upcoming' ? 'Upcoming (7 days)' : + activeFilter === 'overdue' ? 'Overdue' : 'All'} + </span> + <Button + variant="ghost" + size="sm" + onClick={() => setActiveFilter(null)} + > + Clear filter + </Button> + </div> + )} + {/* 선택된 항목 수 표시 (선택된 항목이 있을 때만) */} + {selectedRowCount > 0 && ( + <Button + variant="destructive" + size="sm" + onClick={handleBatchDelete} + > + <Trash2 className="mr-2 size-4" /> + {t("buttons.delete")} ({selectedRowCount}) + </Button> + )} + + {/* 버튼 그룹 */} + <div className="flex items-center gap-2"> + + {selectedRowCount > 0 && ( + <Button + variant="outline" + size="sm" + onClick={handleExcludeTags} + disabled={isAnyOperationPending} + className="border-orange-500 text-orange-600 hover:bg-orange-50" + > + {isExcludingTags ? ( + <Loader className="mr-2 size-4 animate-spin" /> + ) : ( + <XCircle className="mr-2 size-4" /> + )} + {t("buttons.excludeTags")} ({selectedRowCount}) + </Button> + )} + {/* 태그 관리 드롭다운 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm" disabled={isAnyOperationPending}> + {(isSyncingTags || isLoadingTags) ? ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + ) : + <TagsIcon className="size-4" />} + {t("buttons.tagOperations")} + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {/* 모드에 따라 다른 태그 작업 표시 */} + {mode === "IM" ? ( + <DropdownMenuItem onClick={handleSyncTags} disabled={isAnyOperationPending}> + <Tag className="mr-2 h-4 w-4" /> + {t("buttons.syncTags")} + </DropdownMenuItem> + ) : ( + <DropdownMenuItem onClick={handleGetTags} disabled={isAnyOperationPending}> + <RefreshCcw className="mr-2 h-4 w-4" /> + {t("buttons.getTags")} + </DropdownMenuItem> + )} + <DropdownMenuItem + onClick={() => setAddTagDialogOpen(true)} + disabled={isAnyOperationPending || isAddTagDisabled} + > + <Plus className="mr-2 h-4 w-4" /> + {t("buttons.addTags")} + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + + {/* 리포트 관리 드롭다운 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm" disabled={isAnyOperationPending}> + <Clipboard className="size-4" /> + {t("buttons.reportOperations")} + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={() => setTempUpDialog(true)} disabled={isAnyOperationPending}> + <Upload className="mr-2 h-4 w-4" /> + {t("buttons.uploadTemplate")} + </DropdownMenuItem> + <DropdownMenuItem onClick={handleBatchDocument} disabled={isAnyOperationPending}> + <FileOutput className="mr-2 h-4 w-4" /> + {t("buttons.batchDocument")} + {selectedRowCount > 0 && ( + <span className="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded"> + {selectedRowCount} + </span> + )} + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + + {/* IMPORT 버튼 (파일 선택) */} + <Button asChild variant="outline" size="sm" disabled={isAnyOperationPending}> + <label> + {isImporting ? ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + ) : ( + <Upload className="size-4" /> + )} + {t("buttons.import")} + <input + type="file" + accept=".xlsx,.xls" + onChange={handleImportExcel} + style={{ display: "none" }} + disabled={isAnyOperationPending} + /> + </label> + </Button> + + {/* EXPORT 버튼 */} + <Button + variant="outline" + size="sm" + onClick={handleExportExcel} + disabled={isAnyOperationPending} + > + {isExporting ? ( + <Loader className="mr-2 size-4 animate-spin" /> + ) : ( + <Download className="mr-2 size-4" /> + )} + {t("buttons.export")} + </Button> + + {/* Template 보기 버튼 */} + <Button + variant="outline" + size="sm" + onClick={handleGetTemplate} + disabled={isAnyOperationPending} + > + {isLoadingTemplate ? ( + <Loader className="mr-2 size-4 animate-spin" /> + ) : ( + <Eye className="mr-2 size-4" /> + )} + {t("buttons.viewTemplate")} + </Button> + + {/* COMPARE WITH SEDP 버튼 */} + <Button + variant="outline" + size="sm" + onClick={handleSEDPCompareClick} + disabled={isAnyOperationPending} + > + <GitCompareIcon className="mr-2 size-4" /> + {t("buttons.compareWithSEDP")} + </Button> + + {/* SEDP 전송 버튼 */} + <Button + variant="samsung" + size="sm" + onClick={handleSEDPSendClick} + disabled={isAnyOperationPending} + > + {isSendingSEDP ? ( + <> + <Loader className="mr-2 size-4 animate-spin" /> + {t("messages.sendingSEDP")} + </> + ) : ( + <> + <Send className="size-4" /> + {t("buttons.sendToSHI")} + </> + )} + </Button> + </div> + </ClientDataTable> + + {/* Modal dialog for tag update */} + <UpdateTagSheet + open={rowAction?.type === "update"} + onOpenChange={(open) => { + if (!open) setRowAction(null); + }} + columns={columnsJSON} + rowData={rowAction?.row.original ?? null} + formCode={formCode} + contractItemId={contractItemId} + editableFieldsMap={editableFieldsMap} + onUpdateSuccess={(updatedValues) => { + // Update the specific row in tableData when a single row is updated + if (rowAction?.row.original?.TAG_NO) { + const tagNo = rowAction.row.original.TAG_NO; + setTableData(prev => + prev.map(item => + item.TAG_NO === tagNo ? updatedValues : item + ) + ); + } + }} + /> + + <DeleteFormDataDialog + formData={deleteTarget} + formCode={formCode} + contractItemId={contractItemId} + open={deleteDialogOpen} + onOpenChange={(open) => { + if (!open) { + setDeleteDialogOpen(false); + setDeleteTarget([]); + } + }} + onSuccess={handleDeleteSuccess} + showTrigger={false} + /> + + {/* Dialog for adding tags */} + {/* <AddFormTagDialog + projectId={projectId} + formCode={formCode} + formName={`Form ${formCode}`} + contractItemId={contractItemId} + packageCode={packageCode} + open={addTagDialogOpen} + onOpenChange={setAddTagDialogOpen} + /> */} + + {/* 새로 추가된 Template 다이얼로그 */} + <TemplateViewDialog + isOpen={templateDialogOpen} + onClose={() => setTemplateDialogOpen(false)} + templateData={templateData} + selectedRow={selectedRowsData[0]} // SPR_ITM_LST_SETUP용 + tableData={tableData} // SPR_LST_SETUP용 - 새로 추가 + formCode={formCode} + contractItemId={contractItemId} + editableFieldsMap={editableFieldsMap} + columnsJSON={columnsJSON} + onUpdateSuccess={(updatedValues) => { + // 업데이트 로직도 수정해야 함 - 단일 행 또는 복수 행 처리 + if (Array.isArray(updatedValues)) { + // SPR_LST_SETUP의 경우 - 복수 행 업데이트 + const updatedData = [...tableData]; + updatedValues.forEach(updatedItem => { + const index = updatedData.findIndex(item => item.TAG_NO === updatedItem.TAG_NO); + if (index !== -1) { + updatedData[index] = updatedItem; + } + }); + setTableData(updatedData); + } else { + // SPR_ITM_LST_SETUP의 경우 - 단일 행 업데이트 + const tagNo = updatedValues.TAG_NO; + if (tagNo) { + setTableData(prev => + prev.map(item => + item.TAG_NO === tagNo ? updatedValues : item + ) + ); + } + } + }} + /> + + {/* SEDP Confirmation Dialog */} + <SEDPConfirmationDialog + isOpen={sedpConfirmOpen} + onClose={() => setSedpConfirmOpen(false)} + onConfirm={handleSEDPSendConfirmed} + formName={formName} + tagCount={tableData.filter(v=>v.status !=='excluded').length} + isLoading={isSendingSEDP} + /> + + {/* SEDP Status Dialog */} + <SEDPStatusDialog + isOpen={sedpStatusOpen} + onClose={() => setSedpStatusOpen(false)} + status={sedpStatusData.status} + message={sedpStatusData.message} + successCount={sedpStatusData.successCount} + errorCount={sedpStatusData.errorCount} + totalCount={sedpStatusData.totalCount} + /> + + {/* SEDP Compare Dialog */} + <SEDPCompareDialog + isOpen={sedpCompareOpen} + onClose={() => setSedpCompareOpen(false)} + tableData={tableData} + columnsJSON={columnsJSON} + projectCode={projectCode} + formCode={formCode} + projectType={projectType} + packageCode={packageCode} + /> + + {/* Other dialogs */} + {tempUpDialog && ( + <FormDataReportTempUploadDialog + columnsJSON={columnsJSON} + open={tempUpDialog} + setOpen={setTempUpDialog} + packageId={contractItemId} + formCode={formCode} + formId={formId} + uploaderType="vendor" + /> + )} + + {reportData.length > 0 && ( + <FormDataReportDialog + columnsJSON={columnsJSON} + reportData={reportData} + setReportData={setReportData} + packageId={contractItemId} + formCode={formCode} + formId={formId} + /> + )} + + {batchDownDialog && ( + <FormDataReportBatchDialog + open={batchDownDialog} + setOpen={setBatchDownDialog} + columnsJSON={columnsJSON} + reportData={selectedRowCount > 0 ? getSelectedRowsData() : tableData} + packageId={contractItemId} + formCode={formCode} + formId={formId} + /> + )} + </> + ); +}
\ No newline at end of file |
