summaryrefslogtreecommitdiff
path: root/components/form-data-plant/form-data-table.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-23 10:10:21 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-23 10:10:21 +0000
commitf7f5069a2209cfa39b65f492f32270a5f554bed0 (patch)
tree933c731ec2cb7d8bc62219a0aeed45a5e97d5f15 /components/form-data-plant/form-data-table.tsx
parentd49ad5dee1e5a504e1321f6db802b647497ee9ff (diff)
(대표님) EDP 해양 관련 개발 사항들
Diffstat (limited to 'components/form-data-plant/form-data-table.tsx')
-rw-r--r--components/form-data-plant/form-data-table.tsx1377
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