summaryrefslogtreecommitdiff
path: root/lib/basic-contract/status-detail
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract/status-detail')
-rw-r--r--lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx225
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx84
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-table.tsx2
3 files changed, 268 insertions, 43 deletions
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 c71be9d1..3e965fac 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
@@ -2,7 +2,7 @@
import * as React from "react"
import { type Table } from "@tanstack/react-table"
-import { Download, FileDown, Mail, CheckCircle, AlertTriangle, Send, Check, FileSignature } from "lucide-react"
+import { Download, FileDown, Mail, CheckCircle, AlertTriangle, Send, Check, FileSignature, FileText, ExternalLink, Globe } from "lucide-react"
import { exportTableToExcel } from "@/lib/export"
import { downloadFile } from "@/lib/file-download"
@@ -18,15 +18,16 @@ import {
DialogTitle,
} from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
-import { prepareFinalApprovalAction, quickFinalApprovalAction, resendContractsAction } from "../service"
+import { prepareFinalApprovalAction, quickFinalApprovalAction, resendContractsAction, updateLegalReviewStatusFromSSLVW } from "../service"
import { BasicContractSignDialog } from "../vendor-table/basic-contract-sign-dialog"
import { SSLVWPurInqReqDialog } from "@/components/common/legal/sslvw-pur-inq-req-dialog"
interface BasicContractDetailTableToolbarActionsProps {
table: Table<BasicContractView>
+ gtcData?: Record<number, { gtcDocumentId: number | null; hasComments: boolean }>
}
-export function BasicContractDetailTableToolbarActions({ table }: BasicContractDetailTableToolbarActionsProps) {
+export function BasicContractDetailTableToolbarActions({ table, gtcData = {} }: BasicContractDetailTableToolbarActionsProps) {
// 선택된 행들 가져오기
const selectedRows = table.getSelectedRowModel().rows
const hasSelectedRows = selectedRows.length > 0
@@ -34,6 +35,7 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
// 다이얼로그 상태
const [resendDialog, setResendDialog] = React.useState(false)
const [finalApproveDialog, setFinalApproveDialog] = React.useState(false)
+ const [legalReviewDialog, setLegalReviewDialog] = React.useState(false)
const [loading, setLoading] = React.useState(false)
const [buyerSignDialog, setBuyerSignDialog] = React.useState(false)
const [contractsToSign, setContractsToSign] = React.useState<any[]>([])
@@ -56,6 +58,42 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
return true;
});
+ // 법무검토 요청 가능 여부
+ // 1. 협의 완료됨 (negotiationCompletedAt 있음) OR
+ // 2. 협의 없음 (코멘트 없음, hasComments: false)
+ // 협의 중 (negotiationCompletedAt 없고 코멘트 있음)은 불가
+ const canRequestLegalReview = hasSelectedRows && selectedRows.some(row => {
+ const contract = row.original;
+ // 이미 법무검토 요청된 계약서는 제외
+ if (contract.legalReviewRequestedAt) {
+ return false;
+ }
+ // 이미 최종승인 완료된 계약서는 제외
+ if (contract.completedAt) {
+ return false;
+ }
+
+ // 협의 완료된 경우 → 가능
+ if (contract.negotiationCompletedAt) {
+ return true;
+ }
+
+ // 협의 완료되지 않은 경우
+ // GTC 템플릿인 경우 코멘트 존재 여부 확인
+ if (contract.templateName?.includes('GTC')) {
+ const contractGtcData = gtcData[contract.id];
+ // 코멘트가 없으면 가능 (협의 없음)
+ if (contractGtcData && !contractGtcData.hasComments) {
+ return true;
+ }
+ // 코멘트가 있으면 불가 (협의 중)
+ return false;
+ }
+
+ // GTC가 아닌 경우는 협의 완료 여부만 확인
+ return false;
+ });
+
// 필터링된 계약서들 계산
const resendContracts = selectedRows.map(row => row.original)
@@ -75,6 +113,40 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
!contract.legalReviewRequestedAt && !contract.legalReviewCompletedAt
);
+ // 법무검토 요청 가능한 계약서들
+ const legalReviewContracts = selectedRows
+ .map(row => row.original)
+ .filter(contract => {
+ // 이미 법무검토 요청됨
+ if (contract.legalReviewRequestedAt) {
+ return false;
+ }
+ // 이미 최종승인 완료됨
+ if (contract.completedAt) {
+ return false;
+ }
+
+ // 협의 완료된 경우
+ if (contract.negotiationCompletedAt) {
+ return true;
+ }
+
+ // 협의 완료되지 않은 경우
+ // GTC 템플릿인 경우 코멘트 없으면 가능
+ if (contract.templateName?.includes('GTC')) {
+ const contractGtcData = gtcData[contract.id];
+ // 코멘트가 없으면 가능 (협의 없음)
+ if (contractGtcData && !contractGtcData.hasComments) {
+ return true;
+ }
+ // 코멘트가 있으면 불가 (협의 중)
+ return false;
+ }
+
+ // GTC가 아닌 경우는 협의 완료 여부만 확인
+ return false;
+ });
+
// 대량 재발송
const handleBulkResend = async () => {
if (!hasSelectedRows) {
@@ -252,6 +324,42 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
toast.success("모든 계약서의 최종승인이 완료되었습니다!")
}
+ // SSLVW 데이터 선택 확인 핸들러
+ const handleSSLVWConfirm = async (selectedSSLVWData: any[]) => {
+ if (!selectedSSLVWData || selectedSSLVWData.length === 0) {
+ toast.error("선택된 데이터가 없습니다.")
+ return
+ }
+
+ try {
+ setLoading(true)
+
+ // 선택된 계약서 ID들 추출
+ const selectedContractIds = selectedRows.map(row => row.original.id)
+
+ // 서버 액션 호출
+ const result = await updateLegalReviewStatusFromSSLVW(selectedSSLVWData, selectedContractIds)
+
+ if (result.success) {
+ toast.success(result.message)
+ // 테이블 데이터 갱신
+ table.toggleAllPageRowsSelected(false)
+ } else {
+ toast.error(result.message)
+ }
+
+ if (result.errors && result.errors.length > 0) {
+ toast.warning(`일부 처리 실패: ${result.errors.join(', ')}`)
+ }
+
+ } catch (error) {
+ console.error('SSLVW 확인 처리 실패:', error)
+ toast.error('법무검토 상태 업데이트 중 오류가 발생했습니다.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
// 빠른 승인 (서명 없이)
const confirmQuickApproval = async () => {
setLoading(true)
@@ -275,6 +383,45 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
}
}
+ // 법무검토 요청 링크 목록
+ const legalReviewLinks = [
+ {
+ id: 'domestic-contract',
+ label: '국내계약',
+ url: 'http://60.101.208.95:8080/#/pjt/register-inquiry/domestic-contract',
+ description: '삼성중공업 법무관리시스템 - 국내계약'
+ },
+ {
+ id: 'domestic-advice',
+ label: '국내자문',
+ url: 'http://60.101.208.95:8080/#/pjt/register-inquiry/domestic-advice',
+ description: '삼성중공업 법무관리시스템 - 국내자문'
+ },
+ {
+ id: 'overseas-contract',
+ label: '해외계약',
+ url: 'http://60.101.208.95:8080/#/pjt/register-inquiry/overseas-contract',
+ description: '삼성중공업 법무관리시스템 - 해외계약'
+ },
+ {
+ id: 'overseas-advice',
+ label: '해외자문',
+ url: 'http://60.101.208.95:8080/#/pjt/register-inquiry/overseas-advice',
+ description: '삼성중공업 법무관리시스템 - 해외자문'
+ }
+ ]
+
+ // 법무검토 요청
+ const handleRequestLegalReview = () => {
+ setLegalReviewDialog(true)
+ }
+
+ // 법무검토 링크 클릭 핸들러
+ const handleLegalReviewLinkClick = (url: string) => {
+ window.open(url, '_blank', 'noopener,noreferrer')
+ setLegalReviewDialog(false)
+ }
+
return (
<>
<div className="flex items-center gap-2">
@@ -314,7 +461,21 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
</Button>
{/* 법무검토 버튼 (SSLVW 데이터 조회) */}
- <SSLVWPurInqReqDialog />
+ <SSLVWPurInqReqDialog onConfirm={handleSSLVWConfirm} />
+
+ {/* 법무검토 요청 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleRequestLegalReview}
+ className="gap-2"
+ title="법무검토 요청 링크 선택"
+ >
+ <FileText className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">
+ 법무검토 요청
+ </span>
+ </Button>
{/* 최종승인 버튼 */}
<Button
@@ -413,6 +574,62 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
</DialogContent>
</Dialog>
+ {/* 법무검토 요청 다이얼로그 */}
+ <Dialog open={legalReviewDialog} onOpenChange={setLegalReviewDialog}>
+ <DialogContent className="max-w-2xl">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <FileText className="size-5" />
+ 법무검토 요청
+ </DialogTitle>
+ <DialogDescription>
+ 법무검토 요청 유형을 선택하세요. 선택한 링크가 새 창에서 열립니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div className="flex items-start gap-3 p-4 bg-blue-50 border border-blue-200 rounded-lg">
+ <Globe className="size-5 text-blue-600 flex-shrink-0 mt-0.5" />
+ <div>
+ <div className="font-medium text-blue-800">삼성중공업 법무관리시스템</div>
+ <div className="text-sm text-blue-700 mt-1">
+ 아래 링크 중 해당하는 유형을 선택하여 법무검토를 요청하세요.
+ </div>
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ {legalReviewLinks.map((link) => (
+ <button
+ key={link.id}
+ onClick={() => handleLegalReviewLinkClick(link.url)}
+ className="w-full flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-colors text-left group"
+ >
+ <div className="flex-1">
+ <div className="font-medium text-gray-900 group-hover:text-blue-700">
+ {link.label}
+ </div>
+ <div className="text-sm text-gray-500 mt-1">
+ {link.description}
+ </div>
+ </div>
+ <ExternalLink className="size-5 text-gray-400 group-hover:text-blue-600 flex-shrink-0 ml-4" />
+ </button>
+ ))}
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => setLegalReviewDialog(false)}
+ >
+ 닫기
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
{/* 최종승인 다이얼로그 */}
<Dialog open={finalApproveDialog} onOpenChange={setFinalApproveDialog}>
<DialogContent className="max-w-2xl">
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 0dd33bcb..5a875541 100644
--- a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx
+++ b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx
@@ -373,49 +373,57 @@ export function getDetailColumns({
minSize: 130,
},
- // 법무검토 요청일
+ // 법무검토 상태
{
- accessorKey: "legalReviewRequestedAt",
+ accessorKey: "legalReviewStatus",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="법무검토 요청" />
+ <DataTableColumnHeaderSimple column={column} title="법무검토 상태" />
),
cell: ({ row }) => {
- const date = row.getValue("legalReviewRequestedAt") as Date | null
- return date ? (
- <div className="text-sm text-purple-600">
- <div className="font-medium">요청됨</div>
- <div className="text-xs">{formatDateTime(date, "KR")}</div>
- </div>
- ) : (
- <div className="text-sm text-gray-400">-</div>
- )
- },
- minSize: 140,
- },
-
- // 법무검토 완료일
- {
- accessorKey: "legalReviewCompletedAt",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="법무검토 완료" />
- ),
- cell: ({ row }) => {
- const date = row.getValue("legalReviewCompletedAt") as Date | null
+ const status = row.getValue("legalReviewStatus") as string | null
const requestedDate = row.getValue("legalReviewRequestedAt") as Date | null
-
- return date ? (
- <div className="text-sm text-indigo-600">
- <div className="font-medium">완료</div>
- <div className="text-xs">{formatDateTime(date, "KR")}</div>
- </div>
- ) : requestedDate ? (
- <div className="text-sm text-orange-500">
- <div className="font-medium">진행중</div>
- <div className="text-xs">검토 대기</div>
- </div>
- ) : (
- <div className="text-sm text-gray-400">-</div>
- )
+ const completedDate = row.getValue("legalReviewCompletedAt") as Date | null
+
+ // 법무검토 상태 우선, 없으면 기존 로직으로 판단
+ if (status) {
+ const statusColors: Record<string, string> = {
+ '신규등록': 'text-blue-600',
+ '검토요청': 'text-purple-600',
+ '담당자배정': 'text-orange-600',
+ '검토중': 'text-yellow-600',
+ '답변완료': 'text-green-600',
+ '재검토요청': 'text-red-600',
+ '보류': 'text-gray-500',
+ '취소': 'text-red-700'
+ }
+
+ return (
+ <div className={`text-sm ${statusColors[status] || 'text-gray-600'}`}>
+ <div className="font-medium">{status}</div>
+ </div>
+ )
+ }
+
+ // legalWorks에 데이터가 없는 경우 기존 로직 사용
+ if (completedDate) {
+ return (
+ <div className="text-sm text-green-600">
+ <div className="font-medium">완료</div>
+ <div className="text-xs">{formatDateTime(completedDate, "KR")}</div>
+ </div>
+ )
+ } else if (requestedDate) {
+ return (
+ <div className="text-sm text-orange-600">
+ <div className="font-medium">진행중</div>
+ <div className="text-xs">검토 대기</div>
+ </div>
+ )
+ } else {
+ return (
+ <div className="text-sm text-gray-400">-</div>
+ )
+ }
},
minSize: 140,
},
diff --git a/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx b/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx
index 407463e4..0df46066 100644
--- a/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx
+++ b/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx
@@ -151,7 +151,7 @@ export function BasicContractsDetailTable({ templateId, promises }: BasicContrac
table={table}
filterFields={advancedFilterFields}
>
- <BasicContractDetailTableToolbarActions table={table} />
+ <BasicContractDetailTableToolbarActions table={table} gtcData={gtcData} />
</DataTableAdvancedToolbar>
</DataTable>
)