diff options
| -rw-r--r-- | components/common/legal/cpvw-wab-qust-list-view-dialog.tsx | 364 | ||||
| -rw-r--r-- | db/schema/basicContractDocumnet.ts | 15 | ||||
| -rw-r--r-- | lib/basic-contract/cpvw-service.ts | 236 | ||||
| -rw-r--r-- | lib/basic-contract/service.ts | 267 | ||||
| -rw-r--r-- | lib/basic-contract/sslvw-service.ts | 126 | ||||
| -rw-r--r-- | lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx | 209 | ||||
| -rw-r--r-- | lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx | 39 | ||||
| -rw-r--r-- | types/table.d.ts | 2 |
8 files changed, 1184 insertions, 74 deletions
diff --git a/components/common/legal/cpvw-wab-qust-list-view-dialog.tsx b/components/common/legal/cpvw-wab-qust-list-view-dialog.tsx new file mode 100644 index 00000000..aeefbb84 --- /dev/null +++ b/components/common/legal/cpvw-wab-qust-list-view-dialog.tsx @@ -0,0 +1,364 @@ +"use client" + +import * as React from "react" +import { Loader, Database, Check } from "lucide-react" +import { toast } from "sonner" +import { + useReactTable, + getCoreRowModel, + getPaginationRowModel, + getFilteredRowModel, + ColumnDef, + flexRender, +} from "@tanstack/react-table" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Checkbox } from "@/components/ui/checkbox" +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area" + +import { getCPVWWabQustListViewData, CPVWWabQustListView } from "@/lib/basic-contract/cpvw-service" + +interface CPVWWabQustListViewDialogProps { + onConfirm?: (selectedRows: CPVWWabQustListView[]) => void + requireSingleSelection?: boolean + triggerDisabled?: boolean + triggerTitle?: string +} + +export function CPVWWabQustListViewDialog({ + onConfirm, + requireSingleSelection = false, + triggerDisabled = false, + triggerTitle, +}: CPVWWabQustListViewDialogProps) { + const [open, setOpen] = React.useState(false) + const [isLoading, setIsLoading] = React.useState(false) + const [data, setData] = React.useState<CPVWWabQustListView[]>([]) + const [error, setError] = React.useState<string | null>(null) + const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({}) + + const loadData = async () => { + setIsLoading(true) + setError(null) + try { + const result = await getCPVWWabQustListViewData() + if (result.success) { + setData(result.data) + if (result.isUsingFallback) { + toast.info("테스트 데이터를 표시합니다.") + } + } else { + setError(result.error || "데이터 로딩 실패") + toast.error(result.error || "데이터 로딩 실패") + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "알 수 없는 오류" + setError(errorMessage) + toast.error(errorMessage) + } finally { + setIsLoading(false) + } + } + + React.useEffect(() => { + if (open) { + loadData() + } else { + // 다이얼로그 닫힐 때 데이터 초기화 + setData([]) + setError(null) + setRowSelection({}) + } + }, [open]) + + // 테이블 컬럼 정의 (동적 생성) + const columns = React.useMemo<ColumnDef<CPVWWabQustListView>[]>(() => { + if (data.length === 0) return [] + + const dataKeys = Object.keys(data[0]) + + return [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="모든 행 선택" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="행 선택" + /> + ), + enableSorting: false, + enableHiding: false, + }, + ...dataKeys.map((key) => ({ + accessorKey: key, + header: key, + cell: ({ getValue }: any) => { + const value = getValue() + return value !== null && value !== undefined ? String(value) : "" + }, + })), + ] + }, [data]) + + // 테이블 인스턴스 생성 + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onRowSelectionChange: setRowSelection, + state: { + rowSelection, + }, + }) + + // 선택된 행들 가져오기 + const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original) + + // 확인 버튼 핸들러 + const handleConfirm = () => { + if (selectedRows.length === 0) { + toast.error("행을 선택해주세요.") + return + } + + if (requireSingleSelection && selectedRows.length !== 1) { + toast.error("하나의 행만 선택해주세요.") + return + } + + if (onConfirm) { + onConfirm(selectedRows) + toast.success( + requireSingleSelection + ? "선택한 행으로 준법문의 상태를 동기화합니다." + : `${selectedRows.length}개의 행을 선택했습니다.` + ) + } else { + // 임시로 선택된 데이터 콘솔 출력 + console.log("선택된 행들:", selectedRows) + toast.success(`${selectedRows.length}개의 행이 선택되었습니다. (콘솔 확인)`) + } + + setOpen(false) + } + + return ( + <Dialog open={open} onOpenChange={setOpen}> + <DialogTrigger asChild> + <Button + variant="outline" + size="sm" + disabled={triggerDisabled} + title={triggerTitle} + > + <Database className="mr-2 size-4" aria-hidden="true" /> + 준법문의 요청 데이터 조회 + </Button> + </DialogTrigger> + <DialogContent className="max-w-7xl h-[90vh] flex flex-col overflow-hidden"> + <DialogHeader> + <DialogTitle>준법문의 요청 데이터</DialogTitle> + <DialogDescription> + 준법문의 요청 데이터를 조회합니다. + {data.length > 0 && ` (${data.length}건, ${selectedRows.length}개 선택됨)`} + </DialogDescription> + </DialogHeader> + + <div className="flex flex-col flex-1 min-h-0"> + {isLoading ? ( + <div className="flex items-center justify-center flex-1 min-h-[200px]"> + <Loader className="mr-2 size-6 animate-spin" /> + <span>데이터 로딩 중...</span> + </div> + ) : error ? ( + <div className="flex items-center justify-center flex-1 min-h-[200px] text-red-500"> + <span>오류: {error}</span> + </div> + ) : data.length === 0 ? ( + <div className="flex items-center justify-center flex-1 min-h-[200px] text-muted-foreground"> + <span>데이터가 없습니다.</span> + </div> + ) : ( + <div className="flex flex-col flex-1 min-h-0"> + {/* 테이블 영역 - 스크롤 가능 */} + <ScrollArea className="flex-1 overflow-auto border rounded-md"> + <Table className="min-w-full"> + <TableHeader className="sticky top-0 bg-background z-10"> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => ( + <TableHead key={header.id} className="font-medium bg-background"> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ))} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id} className="text-sm"> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + 데이터가 없습니다. + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + <ScrollBar orientation="horizontal" /> + </ScrollArea> + + {/* 페이지네이션 컨트롤 - 고정 영역 */} + <div className="flex items-center justify-between px-2 py-4 border-t bg-background flex-shrink-0"> + <div className="flex-1 text-sm text-muted-foreground"> + {table.getFilteredSelectedRowModel().rows.length}개 행 선택됨 + </div> + <div className="flex items-center space-x-6 lg:space-x-8"> + <div className="flex items-center space-x-2"> + <p className="text-sm font-medium">페이지당 행 수</p> + <select + value={table.getState().pagination.pageSize} + onChange={(e) => { + table.setPageSize(Number(e.target.value)) + }} + className="h-8 w-[70px] rounded border border-input bg-transparent px-3 py-1 text-sm ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2" + > + {[10, 20, 30, 40, 50].map((pageSize) => ( + <option key={pageSize} value={pageSize}> + {pageSize} + </option> + ))} + </select> + </div> + <div className="flex w-[100px] items-center justify-center text-sm font-medium"> + {table.getState().pagination.pageIndex + 1} /{" "} + {table.getPageCount()} + </div> + <div className="flex items-center space-x-2"> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">첫 페이지로</span> + {"<<"} + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <span className="sr-only">이전 페이지</span> + {"<"} + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">다음 페이지</span> + {">"} + </Button> + <Button + variant="outline" + className="h-8 w-8 p-0" + onClick={() => table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + <span className="sr-only">마지막 페이지로</span> + {">>"} + </Button> + </div> + </div> + </div> + </div> + )} + </div> + + <DialogFooter className="gap-2 flex-shrink-0"> + <Button variant="outline" onClick={() => setOpen(false)}> + 닫기 + </Button> + <Button onClick={loadData} disabled={isLoading} variant="outline"> + {isLoading ? ( + <> + <Loader className="mr-2 size-4 animate-spin" /> + 로딩 중... + </> + ) : ( + "새로고침" + )} + </Button> + <Button + onClick={handleConfirm} + disabled={ + requireSingleSelection + ? selectedRows.length !== 1 + : selectedRows.length === 0 + } + > + <Check className="mr-2 size-4" /> + 확인 ({selectedRows.length}) + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} + diff --git a/db/schema/basicContractDocumnet.ts b/db/schema/basicContractDocumnet.ts index 944c4b2c..e571c7e0 100644 --- a/db/schema/basicContractDocumnet.ts +++ b/db/schema/basicContractDocumnet.ts @@ -67,6 +67,12 @@ export const basicContract = pgTable('basic_contract', { legalReviewRegNo: varchar('legal_review_reg_no', { length: 100 }), // 법무 시스템 REG_NO legalReviewProgressStatus: varchar('legal_review_progress_status', { length: 255 }), // PRGS_STAT_DSC 값 + // 준법문의 관련 필드 + complianceReviewRequestedAt: timestamp('compliance_review_requested_at'), // 준법문의 요청일 + complianceReviewCompletedAt: timestamp('compliance_review_completed_at'), // 준법문의 완료일 + complianceReviewRegNo: varchar('compliance_review_reg_no', { length: 100 }), // 준법문의 시스템 REG_NO + complianceReviewProgressStatus: varchar('compliance_review_progress_status', { length: 255 }), // 준법문의 PRGS_STAT_DSC 값 + createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow(), completedAt: timestamp('completed_at'), // 계약 체결 완료 날짜 @@ -99,6 +105,12 @@ export const basicContractView = pgView('basic_contract_view').as((qb) => { legalReviewRegNo: sql<string | null>`${basicContract.legalReviewRegNo}`.as('legal_review_reg_no'), legalReviewProgressStatus: sql<string | null>`${basicContract.legalReviewProgressStatus}`.as('legal_review_progress_status'), + // 준법문의 관련 필드 + complianceReviewRequestedAt: sql<Date | null>`${basicContract.complianceReviewRequestedAt}`.as('compliance_review_requested_at'), + complianceReviewCompletedAt: sql<Date | null>`${basicContract.complianceReviewCompletedAt}`.as('compliance_review_completed_at'), + complianceReviewRegNo: sql<string | null>`${basicContract.complianceReviewRegNo}`.as('compliance_review_reg_no'), + complianceReviewProgressStatus: sql<string | null>`${basicContract.complianceReviewProgressStatus}`.as('compliance_review_progress_status'), + createdAt: sql<Date>`${basicContract.createdAt}`.as('created_at'), updatedAt: sql<Date>`${basicContract.updatedAt}`.as('updated_at'), completedAt: sql<Date | null>`${basicContract.completedAt}`.as('completed_at'), @@ -121,6 +133,9 @@ export const basicContractView = pgView('basic_contract_view').as((qb) => { // 법무검토 상태 (PRGS_STAT_DSC 동기화 값) legalReviewStatus: sql<string | null>`${basicContract.legalReviewProgressStatus}`.as('legal_review_status'), + + // 준법문의 상태 (PRGS_STAT_DSC 동기화 값) + complianceReviewStatus: sql<string | null>`${basicContract.complianceReviewProgressStatus}`.as('compliance_review_status'), // 템플릿 파일 정보 templateFilePath: sql<string | null>`${basicContractTemplates.filePath}`.as('template_file_path'), diff --git a/lib/basic-contract/cpvw-service.ts b/lib/basic-contract/cpvw-service.ts new file mode 100644 index 00000000..6d249002 --- /dev/null +++ b/lib/basic-contract/cpvw-service.ts @@ -0,0 +1,236 @@ +"use server" + +import { oracleKnex } from '@/lib/oracle-db/db' + +// CPVW_WAB_QUST_LIST_VIEW 테이블 데이터 타입 (실제 테이블 구조에 맞게 조정 필요) +export interface CPVWWabQustListView { + [key: string]: string | number | Date | null | undefined +} + +// 테스트 환경용 폴백 데이터 (실제 CPVW_WAB_QUST_LIST_VIEW 테이블 구조에 맞춤) +const FALLBACK_TEST_DATA: CPVWWabQustListView[] = [ + { + REG_NO: '1030', + INQ_TP: 'OC', + INQ_TP_DSC: '해외계약', + TIT: 'Contrack of Sale', + REQ_DGR: '2', + REQR_NM: '김원식', + REQ_DT: '20130829', + REVIEW_TERM_DT: '20130902', + RVWR_NM: '김미정', + CNFMR_NM: '안한진', + APPR_NM: '염정훈', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '검토중', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' + }, + { + REG_NO: '1076', + INQ_TP: 'IC', + INQ_TP_DSC: '국내계약', + TIT: 'CAISSON PIPE 복관 계약서 검토 요청件', + REQ_DGR: '1', + REQR_NM: '서권환', + REQ_DT: '20130821', + REVIEW_TERM_DT: '20130826', + RVWR_NM: '이택준', + CNFMR_NM: '이택준', + APPR_NM: '전상용', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '완료', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' + }, + { + REG_NO: '1100', + INQ_TP: 'IC', + INQ_TP_DSC: '국내계약', + TIT: '(7102) HVAC 작업계약', + REQ_DGR: '1', + REQR_NM: '신동동', + REQ_DT: '20130826', + REVIEW_TERM_DT: '20130829', + RVWR_NM: '이두리', + CNFMR_NM: '이두리', + APPR_NM: '전상용', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '완료', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' + }, + { + REG_NO: '1105', + INQ_TP: 'IC', + INQ_TP_DSC: '국내계약', + TIT: 'Plate 가공계약서 검토 요청건', + REQ_DGR: '1', + REQR_NM: '서권환', + REQ_DT: '20130826', + REVIEW_TERM_DT: '20130829', + RVWR_NM: '백영국', + CNFMR_NM: '백영국', + APPR_NM: '전상용', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '완료', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' + }, + { + REG_NO: '1106', + INQ_TP: 'IC', + INQ_TP_DSC: '국내계약', + TIT: 'SHELL FLNG, V-BRACE 제작 계약서 검토件', + REQ_DGR: '1', + REQR_NM: '성기승', + REQ_DT: '20130826', + REVIEW_TERM_DT: '20130830', + RVWR_NM: '이두리', + CNFMR_NM: '이두리', + APPR_NM: '전상용', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '완료', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' + } +] + +const normalizeOracleRows = (rows: Array<Record<string, unknown>>): CPVWWabQustListView[] => { + return rows.map((item) => { + const convertedItem: CPVWWabQustListView = {} + for (const [key, value] of Object.entries(item)) { + if (value instanceof Date) { + convertedItem[key] = value + } else if (value === null) { + convertedItem[key] = null + } else { + convertedItem[key] = String(value) + } + } + return convertedItem + }) +} + +/** + * CPVW_WAB_QUST_LIST_VIEW 테이블 전체 조회 + * @returns 테이블 데이터 배열 + */ +export async function getCPVWWabQustListViewData(): Promise<{ + success: boolean + data: CPVWWabQustListView[] + error?: string + isUsingFallback?: boolean +}> { + try { + console.log('📋 [getCPVWWabQustListViewData] CPVW_WAB_QUST_LIST_VIEW 테이블 조회 시작...') + + const result = await oracleKnex.raw(` + SELECT * + FROM CPVW_WAB_QUST_LIST_VIEW + WHERE ROWNUM < 100 + ORDER BY 1 + `) + + // Oracle raw query의 결과는 rows 배열에 들어있음 + const rows = (result.rows || result) as Array<Record<string, unknown>> + + console.log(`✅ [getCPVWWabQustListViewData] 조회 성공 - ${rows.length}건`) + + // 데이터 타입 변환 (필요에 따라 조정) + const cleanedResult = normalizeOracleRows(rows) + + return { + success: true, + data: cleanedResult, + isUsingFallback: false + } + } catch (error) { + console.error('❌ [getCPVWWabQustListViewData] 오류:', error) + console.log('🔄 [getCPVWWabQustListViewData] 폴백 테스트 데이터 사용') + return { + success: true, + data: FALLBACK_TEST_DATA, + isUsingFallback: true + } + } +} + +export async function getCPVWWabQustListViewByRegNo(regNo: string): Promise<{ + success: boolean + data?: CPVWWabQustListView + error?: string + isUsingFallback?: boolean +}> { + if (!regNo) { + return { + success: false, + error: 'REG_NO는 필수입니다.' + } + } + + try { + console.log(`[getCPVWWabQustListViewByRegNo] REG_NO=${regNo} 조회`) + const result = await oracleKnex.raw( + ` + SELECT * + FROM CPVW_WAB_QUST_LIST_VIEW + WHERE REG_NO = :regNo + `, + { regNo } + ) + + const rows = (result.rows || result) as Array<Record<string, unknown>> + const cleanedResult = normalizeOracleRows(rows) + + if (cleanedResult.length === 0) { + // 데이터가 없을 때 폴백 테스트 데이터에서 찾기 + console.log(`[getCPVWWabQustListViewByRegNo] 데이터 없음, 폴백 테스트 데이터에서 검색: REG_NO=${regNo}`) + const fallbackData = FALLBACK_TEST_DATA.find(item => + String(item.REG_NO) === String(regNo) + ) + + if (fallbackData) { + console.log(`[getCPVWWabQustListViewByRegNo] 폴백 테스트 데이터에서 찾음: REG_NO=${regNo}`) + return { + success: true, + data: fallbackData, + isUsingFallback: true + } + } + + return { + success: false, + error: '해당 REG_NO에 대한 데이터가 없습니다.' + } + } + + return { + success: true, + data: cleanedResult[0], + isUsingFallback: false + } + } catch (error) { + console.error('[getCPVWWabQustListViewByRegNo] 오류:', error) + console.log(`[getCPVWWabQustListViewByRegNo] 폴백 테스트 데이터에서 검색: REG_NO=${regNo}`) + + // 오류 발생 시 폴백 테스트 데이터에서 찾기 + const fallbackData = FALLBACK_TEST_DATA.find(item => + String(item.REG_NO) === String(regNo) + ) + + if (fallbackData) { + console.log(`[getCPVWWabQustListViewByRegNo] 폴백 테스트 데이터에서 찾음: REG_NO=${regNo}`) + return { + success: true, + data: fallbackData, + isUsingFallback: true + } + } + + return { + success: false, + error: error instanceof Error ? error.message : 'REG_NO 조회 중 오류가 발생했습니다.' + } + } +} diff --git a/lib/basic-contract/service.ts b/lib/basic-contract/service.ts index 6f4e5d53..12278c54 100644 --- a/lib/basic-contract/service.ts +++ b/lib/basic-contract/service.ts @@ -2862,6 +2862,10 @@ export async function requestLegalReviewAction( } } +// ⚠️ SSLVW(법무관리시스템) PRGS_STAT_DSC 문자열을 그대로 저장하는 함수입니다. +// - 상태 텍스트 및 완료 여부는 외부 시스템에 의존하므로 신뢰도가 100%는 아니고, +// - 여기에서 관리하는 값들은 UI 표시/참고용으로만 사용해야 합니다. +// - 최종 승인 차단 등 핵심 비즈니스 로직에서는 SSLVW 쪽 완료 시간을 직접 신뢰하지 않습니다. const persistLegalReviewStatus = async ({ contractId, regNo, @@ -2904,6 +2908,121 @@ const persistLegalReviewStatus = async ({ } /** + * 준법문의 요청 서버 액션 + */ +export async function requestComplianceInquiryAction( + contractIds: number[] +): Promise<{ success: boolean; message: string }> { + const session = await getServerSession(authOptions) + + if (!session?.user) { + return { + success: false, + message: "로그인이 필요합니다." + } + } + + // 계약서 정보 조회 + const contracts = await db + .select({ + id: basicContractView.id, + complianceReviewRequestedAt: basicContractView.complianceReviewRequestedAt, + }) + .from(basicContractView) + .where(inArray(basicContractView.id, contractIds)) + + if (contracts.length === 0) { + return { + success: false, + message: "선택된 계약서를 찾을 수 없습니다." + } + } + + // 준법문의 요청 가능한 계약서 필터링 (이미 요청되지 않은 것만) + const eligibleContracts = contracts.filter(contract => + !contract.complianceReviewRequestedAt + ) + + if (eligibleContracts.length === 0) { + return { + success: false, + message: "준법문의 요청 가능한 계약서가 없습니다." + } + } + + const currentDate = new Date() + + // 트랜잭션으로 처리 + await db.transaction(async (tx) => { + for (const contract of eligibleContracts) { + await tx + .update(basicContract) + .set({ + complianceReviewRequestedAt: currentDate, + updatedAt: currentDate, + }) + .where(eq(basicContract.id, contract.id)) + } + }) + + revalidateTag("basic-contracts") + + return { + success: true, + message: `${eligibleContracts.length}건의 준법문의 요청이 완료되었습니다.` + } +} + +/** + * 준법문의 상태 저장 (준법문의 전용 필드 사용) + */ +const persistComplianceReviewStatus = async ({ + contractId, + regNo, + progressStatus, +}: { + contractId: number + regNo: string + progressStatus: string +}) => { + const now = new Date() + + // 완료 상태 확인 (법무검토와 동일한 패턴) + // ⚠️ CPVW PRGS_STAT_DSC 문자열을 기반으로 한 best-effort 휴리스틱입니다. + // - 외부 시스템의 상태 텍스트에 의존하므로 신뢰도가 100%는 아니고, + // - 여기에서 설정하는 완료 시간(complianceReviewCompletedAt)은 UI 표시용으로만 사용해야 합니다. + // - 버튼 활성화, 서버 액션 차단, 필터 조건 등 핵심 비즈니스 로직에서는 + // 이 값을 신뢰하지 않도록 합니다. + // 완료 상태 확인 (법무검토와 동일한 패턴) + const isCompleted = progressStatus && ( + progressStatus.includes('완료') || + progressStatus.includes('승인') || + progressStatus.includes('종료') + ) + + await db.transaction(async (tx) => { + // 준법문의 상태 업데이트 (준법문의 전용 필드 사용) + const updateData: any = { + complianceReviewRegNo: regNo, + complianceReviewProgressStatus: progressStatus, + updatedAt: now, + } + + // 완료 상태인 경우 완료일 설정 + if (isCompleted) { + updateData.complianceReviewCompletedAt = now + } + + await tx + .update(basicContract) + .set(updateData) + .where(eq(basicContract.id, contractId)) + }) + + revalidateTag("basic-contracts") +} + +/** * SSLVW 데이터로부터 법무검토 상태 업데이트 * @param sslvwData 선택된 SSLVW 데이터 배열 * @param selectedContractIds 선택된 계약서 ID 배열 @@ -3033,6 +3152,137 @@ export async function updateLegalReviewStatusFromSSLVW( } } +/** + * CPVW 데이터로부터 준법문의 상태 업데이트 + * @param cpvwData 선택된 CPVW 데이터 배열 + * @param selectedContractIds 선택된 계약서 ID 배열 + * @returns 성공 여부 및 메시지 + */ +export async function updateComplianceReviewStatusFromCPVW( + cpvwData: Array<{ REG_NO?: string; reg_no?: string; PRGS_STAT_DSC?: string; prgs_stat_dsc?: string; [key: string]: any }>, + selectedContractIds: number[] +): Promise<{ success: boolean; message: string; updatedCount: number; errors: string[] }> { + try { + console.log(`[updateComplianceReviewStatusFromCPVW] CPVW 데이터로부터 준법문의 상태 업데이트 시작`) + + if (!cpvwData || cpvwData.length === 0) { + return { + success: false, + message: 'CPVW 데이터가 없습니다.', + updatedCount: 0, + errors: [] + } + } + + if (!selectedContractIds || selectedContractIds.length === 0) { + return { + success: false, + message: '선택된 계약서가 없습니다.', + updatedCount: 0, + errors: [] + } + } + + if (selectedContractIds.length !== 1) { + return { + success: false, + message: '한 개의 계약서만 선택해 주세요.', + updatedCount: 0, + errors: [] + } + } + + if (cpvwData.length !== 1) { + return { + success: false, + message: '준법문의 시스템 데이터도 한 건만 선택해 주세요.', + updatedCount: 0, + errors: [] + } + } + + const contractId = selectedContractIds[0] + const cpvwItem = cpvwData[0] + const regNo = String( + cpvwItem.REG_NO ?? + cpvwItem.reg_no ?? + cpvwItem.RegNo ?? + '' + ).trim() + const progressStatus = String( + cpvwItem.PRGS_STAT_DSC ?? + cpvwItem.prgs_stat_dsc ?? + cpvwItem.PrgsStatDsc ?? + '' + ).trim() + + if (!regNo) { + return { + success: false, + message: 'REG_NO 값을 찾을 수 없습니다.', + updatedCount: 0, + errors: [] + } + } + + if (!progressStatus) { + return { + success: false, + message: 'PRGS_STAT_DSC 값을 찾을 수 없습니다.', + updatedCount: 0, + errors: [] + } + } + + const contract = await db + .select({ + id: basicContract.id, + complianceReviewRegNo: basicContract.complianceReviewRegNo, + }) + .from(basicContract) + .where(eq(basicContract.id, contractId)) + .limit(1) + + if (!contract[0]) { + return { + success: false, + message: `계약서(${contractId})를 찾을 수 없습니다.`, + updatedCount: 0, + errors: [] + } + } + + if (contract[0].complianceReviewRegNo && contract[0].complianceReviewRegNo !== regNo) { + console.warn(`[updateComplianceReviewStatusFromCPVW] REG_NO가 변경됩니다: ${contract[0].complianceReviewRegNo} -> ${regNo}`) + } + + // 준법문의 상태 업데이트 + await persistComplianceReviewStatus({ + contractId, + regNo, + progressStatus, + }) + + console.log(`[updateComplianceReviewStatusFromCPVW] 완료: 계약서 ${contractId}, REG_NO ${regNo}, 상태 ${progressStatus}`) + + return { + success: true, + message: '준법문의 상태가 업데이트되었습니다.', + updatedCount: 1, + errors: [] + } + + } catch (error) { + console.error('[updateComplianceReviewStatusFromCPVW] 오류:', error) + return { + success: false, + message: '준법문의 상태 업데이트 중 오류가 발생했습니다.', + updatedCount: 0, + errors: [error instanceof Error ? error.message : '알 수 없는 오류'] + } + } +} + export async function refreshLegalReviewStatusFromOracle(contractId: number): Promise<{ success: boolean message: string @@ -3274,12 +3524,9 @@ export async function processBuyerSignatureAction( } } - if (contractData.legalReviewRequestedAt && !contractData.legalReviewCompletedAt) { - return { - success: false, - message: "법무검토가 완료되지 않았습니다." - } - } + // ⚠️ 법무검토 완료 여부는 SSLVW 상태/시간에 의존하므로 + // 여기서는 legalReviewCompletedAt 기반으로 최종승인을 막지 않습니다. + // (법무 상태는 UI에서 참고 정보로만 사용) // 파일 저장 로직 (기존 파일 덮어쓰기) const saveResult = await saveBuffer({ @@ -3373,9 +3620,9 @@ export async function prepareFinalApprovalAction( if (contract.completedAt !== null || !contract.signedFilePath) { return false } - if (contract.legalReviewRequestedAt && !contract.legalReviewCompletedAt) { - return false - } + // ⚠️ 법무검토 완료 여부는 SSLVW 상태/시간에 의존하므로 + // 여기서는 legalReviewCompletedAt 기반으로 필터링하지 않습니다. + // (법무 상태는 UI에서 참고 정보로만 사용) return true }) @@ -3949,6 +4196,8 @@ export async function saveGtcDocumentAction({ buyerSignedAt: null, legalReviewRequestedAt: null, legalReviewCompletedAt: null, + complianceReviewRequestedAt: null, + complianceReviewCompletedAt: null, updatedAt: new Date() }) .where(eq(basicContract.id, documentId)) diff --git a/lib/basic-contract/sslvw-service.ts b/lib/basic-contract/sslvw-service.ts index 38ecb67d..08b43f82 100644 --- a/lib/basic-contract/sslvw-service.ts +++ b/lib/basic-contract/sslvw-service.ts @@ -10,18 +10,89 @@ export interface SSLVWPurInqReq { // 테스트 환경용 폴백 데이터 const FALLBACK_TEST_DATA: SSLVWPurInqReq[] = [ { - id: 1, - request_number: 'REQ001', - status: 'PENDING', - created_date: new Date('2025-01-01'), - description: '테스트 요청 1' + REG_NO: '1030', + INQ_TP: 'OC', + INQ_TP_DSC: '해외계약', + TIT: 'Contrack of Sale', + REQ_DGR: '2', + REQR_NM: '김원식', + REQ_DT: '20130829', + REVIEW_TERM_DT: '20130902', + RVWR_NM: '김미정', + CNFMR_NM: '안한진', + APPR_NM: '염정훈', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '검토중이라고', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' }, { - id: 2, - request_number: 'REQ002', - status: 'APPROVED', - created_date: new Date('2025-01-02'), - description: '테스트 요청 2' + REG_NO: '1076', + INQ_TP: 'IC', + INQ_TP_DSC: '국내계약', + TIT: 'CAISSON PIPE 복관 계약서 검토 요청件', + REQ_DGR: '1', + REQR_NM: '서권환', + REQ_DT: '20130821', + REVIEW_TERM_DT: '20130826', + RVWR_NM: '이택준', + CNFMR_NM: '이택준', + APPR_NM: '전상용', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '완료', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' + }, + { + REG_NO: '1100', + INQ_TP: 'IC', + INQ_TP_DSC: '국내계약', + TIT: '(7102) HVAC 작업계약', + REQ_DGR: '1', + REQR_NM: '신동동', + REQ_DT: '20130826', + REVIEW_TERM_DT: '20130829', + RVWR_NM: '이두리', + CNFMR_NM: '이두리', + APPR_NM: '전상용', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '완료', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' + }, + { + REG_NO: '1105', + INQ_TP: 'IC', + INQ_TP_DSC: '국내계약', + TIT: 'Plate 가공계약서 검토 요청건', + REQ_DGR: '1', + REQR_NM: '서권환', + REQ_DT: '20130826', + REVIEW_TERM_DT: '20130829', + RVWR_NM: '백영국', + CNFMR_NM: '백영국', + APPR_NM: '전상용', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '완료', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' + }, + { + REG_NO: '1106', + INQ_TP: 'IC', + INQ_TP_DSC: '국내계약', + TIT: 'SHELL FLNG, V-BRACE 제작 계약서 검토件', + REQ_DGR: '1', + REQR_NM: '성기승', + REQ_DT: '20130826', + REVIEW_TERM_DT: '20130830', + RVWR_NM: '이두리', + CNFMR_NM: '이두리', + APPR_NM: '전상용', + PRGS_STAT: 'E', + PRGS_STAT_DSC: '완료', + REGR_DPTCD: 'D602058000', + REGR_DEPTNM: '구매1팀(사외계약)' } ] @@ -89,6 +160,7 @@ export async function getSSLVWPurInqReqByRegNo(regNo: string): Promise<{ success: boolean data?: SSLVWPurInqReq error?: string + isUsingFallback?: boolean }> { if (!regNo) { return { @@ -112,6 +184,21 @@ export async function getSSLVWPurInqReqByRegNo(regNo: string): Promise<{ const cleanedResult = normalizeOracleRows(rows) if (cleanedResult.length === 0) { + // 데이터가 없을 때 폴백 테스트 데이터에서 찾기 + console.log(`[getSSLVWPurInqReqByRegNo] 데이터 없음, 폴백 테스트 데이터에서 검색: REG_NO=${regNo}`) + const fallbackData = FALLBACK_TEST_DATA.find(item => + String(item.REG_NO) === String(regNo) + ) + + if (fallbackData) { + console.log(`[getSSLVWPurInqReqByRegNo] 폴백 테스트 데이터에서 찾음: REG_NO=${regNo}`) + return { + success: true, + data: fallbackData, + isUsingFallback: true + } + } + return { success: false, error: '해당 REG_NO에 대한 데이터가 없습니다.' @@ -120,10 +207,27 @@ export async function getSSLVWPurInqReqByRegNo(regNo: string): Promise<{ return { success: true, - data: cleanedResult[0] + data: cleanedResult[0], + isUsingFallback: false } } catch (error) { console.error('[getSSLVWPurInqReqByRegNo] 오류:', error) + console.log(`[getSSLVWPurInqReqByRegNo] 폴백 테스트 데이터에서 검색: REG_NO=${regNo}`) + + // 오류 발생 시 폴백 테스트 데이터에서 찾기 + const fallbackData = FALLBACK_TEST_DATA.find(item => + String(item.REG_NO) === String(regNo) + ) + + if (fallbackData) { + console.log(`[getSSLVWPurInqReqByRegNo] 폴백 테스트 데이터에서 찾음: REG_NO=${regNo}`) + return { + success: true, + data: fallbackData, + isUsingFallback: true + } + } + return { success: false, error: error instanceof Error ? error.message : 'REG_NO 조회 중 오류가 발생했습니다.' diff --git a/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx b/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx index 575582cf..3e7caee1 100644 --- a/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx +++ b/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx @@ -18,9 +18,10 @@ import { DialogTitle, } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" -import { prepareFinalApprovalAction, quickFinalApprovalAction, resendContractsAction, updateLegalReviewStatusFromSSLVW } from "../service" +import { prepareFinalApprovalAction, quickFinalApprovalAction, resendContractsAction, updateLegalReviewStatusFromSSLVW, updateComplianceReviewStatusFromCPVW, requestComplianceInquiryAction } from "../service" import { BasicContractSignDialog } from "../vendor-table/basic-contract-sign-dialog" import { SSLVWPurInqReqDialog } from "@/components/common/legal/sslvw-pur-inq-req-dialog" +import { CPVWWabQustListViewDialog } from "@/components/common/legal/cpvw-wab-qust-list-view-dialog" import { prepareRedFlagResolutionApproval, requestRedFlagResolution } from "@/lib/compliance/red-flag-resolution" import { useRouter } from "next/navigation" import { useSession } from "next-auth/react" @@ -81,24 +82,26 @@ export function BasicContractDetailTableToolbarActions({ if (contract.completedAt !== null || !contract.signedFilePath) { return false; } - if (contract.legalReviewRequestedAt && !contract.legalReviewCompletedAt) { - return false; - } + // ⚠️ 법무/준법문의 완료 여부는 SSLVW/CPVW 상태 및 완료 시간에 의존하므로, + // 여기서는 legalReviewCompletedAt / complianceReviewCompletedAt 기반으로 + // 최종 승인 버튼을 막지 않습니다. (상태/시간은 UI 참고용으로만 사용) return true; }); - // 법무검토 요청 가능 여부 - // 1. 협의 완료됨 (negotiationCompletedAt 있음) OR - // 2. 협의 없음 (코멘트 없음, hasComments: false) + // 법무검토 요청 가능 여부 (준법서약 템플릿이 아닐 때만) + // 1. 협력업체 서명 완료 (vendorSignedAt 있음) + // 2. 협의 완료됨 (negotiationCompletedAt 있음) OR + // 3. 협의 없음 (코멘트 없음, hasComments: false) // 협의 중 (negotiationCompletedAt 없고 코멘트 있음)은 불가 - const canRequestLegalReview = hasSelectedRows && selectedRows.some(row => { + const canRequestLegalReview = !isComplianceTemplate && hasSelectedRows && selectedRows.some(row => { const contract = row.original; - // 이미 법무검토 요청된 계약서는 제외 - if (contract.legalReviewRequestedAt) { - return false; - } - // 이미 최종승인 완료된 계약서는 제외 - if (contract.completedAt) { + + // 필수 조건 확인: 최종승인 미완료, 법무검토 미요청, 협력업체 서명 완료 + if ( + contract.legalReviewRequestedAt || + contract.completedAt || + !contract.vendorSignedAt + ) { return false; } @@ -123,6 +126,35 @@ export function BasicContractDetailTableToolbarActions({ return false; }); + // 준법문의 버튼 활성화 가능 여부 + // 1. 협력업체 서명 완료 (vendorSignedAt 있음) + // 2. 협의 완료 (negotiationCompletedAt 있음) + // 3. 레드플래그 해소됨 (redFlagResolutionData에서 resolved 상태) + // 4. 이미 준법문의 요청되지 않음 (complianceReviewRequestedAt 없음) + const canRequestComplianceInquiry = hasSelectedRows && selectedRows.some(row => { + const contract = row.original; + + // 필수 조건 확인: 준법서약 템플릿, 최종승인 미완료, 협력업체 서명 완료, 협의 완료, 준법문의 미요청 + if ( + !isComplianceTemplate || + contract.completedAt || + !contract.vendorSignedAt || + !contract.negotiationCompletedAt || + contract.complianceReviewRequestedAt + ) { + return false; + } + + // 레드플래그 해소 확인 + const resolution = redFlagResolutionData[contract.id]; + // 레드플래그가 있는 경우, 해소되어야 함 + if (redFlagData[contract.id] === true && !resolution?.resolved) { + return false; + } + + return true; + }); + // 필터링된 계약서들 계산 const resendContracts = selectedRows.map(row => row.original) @@ -394,6 +426,47 @@ export function BasicContractDetailTableToolbarActions({ } } + // CPVW 데이터 선택 확인 핸들러 + const handleCPVWConfirm = async (selectedCPVWData: any[]) => { + if (!selectedCPVWData || selectedCPVWData.length === 0) { + toast.error("선택된 데이터가 없습니다.") + return + } + + if (selectedRows.length !== 1) { + toast.error("계약서 한 건을 선택해주세요.") + return + } + + try { + setLoading(true) + + // 선택된 계약서 ID들 추출 + const selectedContractIds = selectedRows.map(row => row.original.id) + + // 서버 액션 호출 + const result = await updateComplianceReviewStatusFromCPVW(selectedCPVWData, selectedContractIds) + + if (result.success) { + toast.success(result.message) + router.refresh() + table.toggleAllPageRowsSelected(false) + } else { + toast.error(result.message) + } + + if (result.errors && result.errors.length > 0) { + toast.warning(`일부 처리 실패: ${result.errors.join(', ')}`) + } + + } catch (error) { + console.error('CPVW 확인 처리 실패:', error) + toast.error('준법문의 상태 업데이트 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + // 빠른 승인 (서명 없이) const confirmQuickApproval = async () => { setLoading(true) @@ -541,9 +614,26 @@ export function BasicContractDetailTableToolbarActions({ const complianceInquiryUrl = 'http://60.101.207.55/Inquiry/Write/InquiryWrite.aspx' // 법무검토 요청 / 준법문의 - const handleRequestLegalReview = () => { + const handleRequestLegalReview = async () => { if (isComplianceTemplate) { - window.open(complianceInquiryUrl, '_blank', 'noopener,noreferrer') + // 준법문의: 요청일 기록 후 외부 URL 열기 + const selectedContractIds = selectedRows.map(row => row.original.id) + try { + setLoading(true) + const result = await requestComplianceInquiryAction(selectedContractIds) + if (result.success) { + toast.success(result.message) + router.refresh() + window.open(complianceInquiryUrl, '_blank', 'noopener,noreferrer') + } else { + toast.error(result.message) + } + } catch (error) { + console.error('준법문의 요청 처리 실패:', error) + toast.error('준법문의 요청 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } return } setLegalReviewDialog(true) @@ -617,31 +707,72 @@ export function BasicContractDetailTableToolbarActions({ </span> </Button> - {/* 법무검토 버튼 (SSLVW 데이터 조회) */} - <SSLVWPurInqReqDialog - onConfirm={handleSSLVWConfirm} - requireSingleSelection - triggerDisabled={selectedRows.length !== 1 || loading} - triggerTitle={ - selectedRows.length !== 1 - ? "계약서 한 건을 선택해주세요" - : undefined - } - /> + {/* 법무검토 버튼 (SSLVW 데이터 조회) - 준법서약 템플릿이 아닐 때만 표시 */} + {!isComplianceTemplate && ( + <SSLVWPurInqReqDialog + onConfirm={handleSSLVWConfirm} + requireSingleSelection + triggerDisabled={selectedRows.length !== 1 || loading} + triggerTitle={ + selectedRows.length !== 1 + ? "계약서 한 건을 선택해주세요" + : undefined + } + /> + )} + + {/* 준법문의 요청 데이터 조회 버튼 (준법서약 템플릿만) */} + {isComplianceTemplate && ( + <CPVWWabQustListViewDialog + onConfirm={handleCPVWConfirm} + requireSingleSelection + triggerDisabled={selectedRows.length !== 1 || loading} + triggerTitle={ + selectedRows.length !== 1 + ? "계약서 한 건을 선택해주세요" + : undefined + } + /> + )} {/* 법무검토 요청 / 준법문의 버튼 */} - <Button - variant="outline" - size="sm" - onClick={handleRequestLegalReview} - className="gap-2" - title={isComplianceTemplate ? "준법문의 링크로 이동" : "법무검토 요청 링크 선택"} - > - <FileText className="size-4" aria-hidden="true" /> - <span className="hidden sm:inline"> - {isComplianceTemplate ? "준법문의" : "법무검토 요청"} - </span> - </Button> + {isComplianceTemplate ? ( + <Button + variant="outline" + size="sm" + onClick={handleRequestLegalReview} + className="gap-2" + disabled={!canRequestComplianceInquiry || loading} + title={ + !canRequestComplianceInquiry + ? "협력업체 서명 완료, 협의 완료, 레드플래그 해소가 필요합니다" + : "준법문의 링크로 이동" + } + > + <FileText className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline"> + 준법문의 + </span> + </Button> + ) : ( + <Button + variant="outline" + size="sm" + onClick={handleRequestLegalReview} + className="gap-2" + disabled={!canRequestLegalReview || loading} + title={ + !canRequestLegalReview + ? "협력업체 서명 완료 및 협의 완료가 필요합니다" + : "법무검토 요청 링크 선택" + } + > + <FileText className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline"> + 법무검토 요청 + </span> + </Button> + )} {/* 최종승인 버튼 */} <Button diff --git a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx index aab808b8..de6ba1a9 100644 --- a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx +++ b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx @@ -553,8 +553,8 @@ export function getDetailColumns({ minSize: 130, }, - // 법무검토 상태 - { + // 법무검토 상태 (준법서약 템플릿이 아닐 때만 표시) + ...(!isComplianceTemplate ? [{ accessorKey: "legalReviewStatus", header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="법무검토 상태" /> @@ -571,7 +571,30 @@ export function getDetailColumns({ return <div className="text-sm text-gray-400">-</div> }, minSize: 140, + }] : []), + + // 준법문의 상태 (준법서약 템플릿일 때만 표시) + ...(isComplianceTemplate ? [{ + accessorKey: "complianceReviewStatus", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="준법문의 상태" /> + ), + cell: ({ row }) => { + const status = row.getValue("complianceReviewStatus") as string | null + + // PRGS_STAT_DSC 연동값 우선 표시 + if (status) { + return <div className="text-sm text-gray-800">{status}</div> + } + + // 동기화된 값이 없으면 빈 값 처리 + return <div className="text-sm text-gray-400">-</div> + }, + minSize: 140, }, + // Red Flag 컬럼들 (준법서약 템플릿일 때만 표시) + redFlagColumn, + redFlagResolutionColumn] : []), // 계약완료일 { @@ -659,17 +682,5 @@ export function getDetailColumns({ actionsColumn, ] - // 준법서약 템플릿인 경우 Red Flag 컬럼과 해제 컬럼을 법무검토 상태 뒤에 추가 - if (isComplianceTemplate) { - const legalReviewStatusIndex = baseColumns.findIndex((col) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (col as any).accessorKey === 'legalReviewStatus' - }) - - if (legalReviewStatusIndex !== -1) { - baseColumns.splice(legalReviewStatusIndex + 1, 0, redFlagColumn, redFlagResolutionColumn) - } - } - return baseColumns }
\ No newline at end of file diff --git a/types/table.d.ts b/types/table.d.ts index d4053cf1..9fc96687 100644 --- a/types/table.d.ts +++ b/types/table.d.ts @@ -54,7 +54,7 @@ export type Filter<TData> = Prettify< export interface DataTableRowAction<TData> { row: Row<TData> - type:"add_stage"|"specification_meeting"|"clone"|"viewVariables"|"variableSettings"|"addSubClause"|"createRevision"|"duplicate"|"dispose"|"restore"|"download_report"|"submit" |"general_evaluation"| "general_evaluation"|"esg_evaluation" |"schedule"| "view"| "upload" | "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" | "vendor-submission" + type:"add_stage"|"specification_meeting"|"clone"|"viewVariables"|"variableSettings"|"addSubClause"|"createRevision"|"duplicate"|"dispose"|"restore"|"download_report"|"submit" |"general_evaluation"| "general_evaluation"|"esg_evaluation" |"schedule"| "view"| "upload" | "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" | "vendor-submission" | "resend" } export interface QueryBuilderOpts { |
