summaryrefslogtreecommitdiff
path: root/lib/vendor-regular-registrations
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-07 12:01:16 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-07 12:01:16 +0900
commit18ca4ad784aeeab9ab7a13bbc8b3c13b42ca5e49 (patch)
tree6faf1a05d1ae296202ece5f4ca95b4d9c7a0488b /lib/vendor-regular-registrations
parent4b6ebdef8281a413fa2bfbdf8f5565eb8b106c62 (diff)
(김준회) 결재 미리보기 공통컴포넌트 중복 제거, 기존 코드의 미리보기 호출부 수정, 템플릿 작성 가이드 간략히 추가, 결재 미리보기시 첨부파일 편집 처리
Diffstat (limited to 'lib/vendor-regular-registrations')
-rw-r--r--lib/vendor-regular-registrations/handlers.ts151
-rw-r--r--lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx663
2 files changed, 419 insertions, 395 deletions
diff --git a/lib/vendor-regular-registrations/handlers.ts b/lib/vendor-regular-registrations/handlers.ts
index 95acde23..7490d81a 100644
--- a/lib/vendor-regular-registrations/handlers.ts
+++ b/lib/vendor-regular-registrations/handlers.ts
@@ -107,7 +107,12 @@ export async function mapRegistrationToTemplateVariables(payload: {
const { requestData, requestedAt, vendorId } = payload;
// vendors 테이블에서 추가 정보 가져오기
- let vendorInfo: any = {};
+ let vendorInfo: {
+ postalCode?: string | null;
+ businessSize?: string | null;
+ addressDetail?: string | null;
+ } = {};
+
if (vendorId) {
try {
const vendorResult = await db
@@ -115,7 +120,6 @@ export async function mapRegistrationToTemplateVariables(payload: {
postalCode: vendors.postalCode,
businessSize: vendors.businessSize,
addressDetail: vendors.addressDetail,
- // FAX, 사업유형, 산업유형은 vendors 테이블에 없으므로 빈 값으로 처리
})
.from(vendors)
.where(eq(vendors.id, vendorId))
@@ -126,6 +130,7 @@ export async function mapRegistrationToTemplateVariables(payload: {
console.warn('[Template Variables] Failed to fetch vendor info:', error);
}
}
+
// 추가정보 조회
let additionalInfo = {
businessType: '',
@@ -137,77 +142,101 @@ export async function mapRegistrationToTemplateVariables(payload: {
};
if (vendorId) {
- const additionalInfoResult = await db
- .select({
- businessType: vendorAdditionalInfo.businessType,
- industryType: vendorAdditionalInfo.industryType,
- companySize: vendorAdditionalInfo.companySize,
- revenue: vendorAdditionalInfo.revenue,
- factoryEstablishedDate: vendorAdditionalInfo.factoryEstablishedDate,
- preferredContractTerms: vendorAdditionalInfo.preferredContractTerms,
- })
- .from(vendorAdditionalInfo)
- .where(eq(vendorAdditionalInfo.vendorId, vendorId))
- .limit(1);
+ try {
+ const additionalInfoResult = await db
+ .select({
+ businessType: vendorAdditionalInfo.businessType,
+ industryType: vendorAdditionalInfo.industryType,
+ companySize: vendorAdditionalInfo.companySize,
+ revenue: vendorAdditionalInfo.revenue,
+ factoryEstablishedDate: vendorAdditionalInfo.factoryEstablishedDate,
+ preferredContractTerms: vendorAdditionalInfo.preferredContractTerms,
+ })
+ .from(vendorAdditionalInfo)
+ .where(eq(vendorAdditionalInfo.vendorId, vendorId))
+ .limit(1);
- additionalInfo = additionalInfoResult[0] || additionalInfo;
+ const info = additionalInfoResult[0];
+ if (info) {
+ additionalInfo = {
+ businessType: info.businessType ?? '',
+ industryType: info.industryType ?? '',
+ companySize: info.companySize ?? '',
+ revenue: info.revenue ?? '',
+ factoryEstablishedDate: info.factoryEstablishedDate ?? '',
+ preferredContractTerms: info.preferredContractTerms ?? '',
+ };
+ }
+ } catch (error) {
+ console.warn('[Template Variables] Failed to fetch additional info:', error);
+ }
}
console.log('[Template Variables] Additional info:', additionalInfo);
+
+ // 변수명은 공백 없이 깔끔하게 정의 (template-utils.ts에서 자동으로 trim 처리됨)
const variables = {
- // 협력업체 기본정보 (템플릿의 정확한 변수명 사용)
- ' 협력업체 기본정보-사업자번호 ': requestData.businessNumber || '',
- ' 협력업체 기본정보-업체명 ': requestData.companyNameKor || '',
- ' 협력업체 기본정보-대표자명 ': requestData.representativeNameKor || '',
- ' 협력업체 기본정보 대표전화 ': requestData.headOfficePhone || '',
- ' 협력업체 기본정보 -FAX ': '', // FAX 정보는 vendors 테이블에 없으므로 빈 문자열
- ' 협력업체 기본정보 -E-mail ': requestData.representativeEmail || '',
- ' 협력업체 기본정보-우편번호 ': vendorInfo.postalCode || '', // vendors 테이블에서 우편번호 가져오기
- ' 협력업체 기본정보-회사주소': requestData.headOfficeAddress || '',
- ' 협력업체 기본정보-상세주소': vendorInfo.addressDetail || '', // 상세주소는 벤더 상세주소로
- ' 협력업체 기본정보-사업유형': additionalInfo.businessType || '', // 주요품목을 사업유형으로 사용
- ' 협력업체 기본정보-산업유형': additionalInfo.industryType || '', // 주요품목을 산업유형으로도 사용
- ' 협력업체 기본정보-회사규모': additionalInfo.companySize || '', // 기업규모
+ // 협력업체 기본정보
+ '협력업체기본정보-사업자번호': requestData.businessNumber || '',
+ '협력업체기본정보-업체명': requestData.companyNameKor || '',
+ '협력업체기본정보-대표자명': requestData.representativeNameKor || '',
+ '협력업체기본정보-대표전화': requestData.headOfficePhone || '',
+ '협력업체기본정보-FAX': '', // FAX 정보는 vendors 테이블에 없으므로 빈 문자열
+ '협력업체기본정보-Email': requestData.representativeEmail || '',
+ '협력업체기본정보-우편번호': vendorInfo.postalCode || '',
+ '협력업체기본정보-회사주소': requestData.headOfficeAddress || '',
+ '협력업체기본정보-상세주소': vendorInfo.addressDetail || '',
+ '협력업체기본정보-사업유형': additionalInfo.businessType || '',
+ '협력업체기본정보-산업유형': additionalInfo.industryType || '',
+ '협력업체기본정보-회사규모': additionalInfo.companySize || '',
- // 담당자 연락처 (각 담당자별로 동일한 정보 반복 - 템플릿에서 여러 번 사용됨)
- ' 협력업체 관리-상세보기-영업담당자-담당자명 ': requestData.businessContacts.sales.name || '',
- ' 협력업체 관리-상세보기-영업담당자-직급 ': requestData.businessContacts.sales.position || '',
- ' 협력업체 관리-상세보기-영업담당자-부서 ': requestData.businessContacts.sales.department || '',
- ' 협력업체 관리-상세보기-영업담당자-담당업무 ': requestData.businessContacts.sales.responsibility || '',
- ' 협력업체 관리-상세보기-영업담당자-이메일 ': requestData.businessContacts.sales.email || '',
- ' 협력업체 관리-상세보기-설계담당자-담당자명 ': requestData.businessContacts.design.name || '',
- ' 협력업체 관리-상세보기-설계담당자-직급 ': requestData.businessContacts.design.position || '',
- ' 협력업체 관리-상세보기-설계담당자-부서 ': requestData.businessContacts.design.department || '',
- ' 협력업체 관리-상세보기-설계담당자-담당업무 ': requestData.businessContacts.design.responsibility || '',
- ' 협력업체 관리-상세보기-설계담당자-이메일 ': requestData.businessContacts.design.email || '',
- ' 협력업체 관리-상세보기-납기담당자-담당자명 ': requestData.businessContacts.delivery.name || '',
- ' 협력업체 관리-상세보기-납기담당자-직급 ': requestData.businessContacts.delivery.position || '',
- ' 협력업체 관리-상세보기-납기담당자-부서 ': requestData.businessContacts.delivery.department || '',
- ' 협력업체 관리-상세보기-납기담당자-담당업무 ': requestData.businessContacts.delivery.responsibility || '',
- ' 협력업체 관리-상세보기-납기담당자-이메일 ': requestData.businessContacts.delivery.email || '',
- ' 협력업체 관리-상세보기-품질담당자-담당자명 ': requestData.businessContacts.quality.name || '',
- ' 협력업체 관리-상세보기-품질담당자-직급 ': requestData.businessContacts.quality.position || '',
- ' 협력업체 관리-상세보기-품질담당자-부서 ': requestData.businessContacts.quality.department || '',
- ' 협력업체 관리-상세보기-품질담당자-담당업무 ': requestData.businessContacts.quality.responsibility || '',
- ' 협력업체 관리-상세보기-품질담당자-이메일 ': requestData.businessContacts.quality.email || '',
- ' 협력업체 관리-상세보기-세금계산서담당자-담당자명 ': requestData.businessContacts.taxInvoice.name || '',
- ' 협력업체 관리-상세보기-세금계산서담당자-직급 ': requestData.businessContacts.taxInvoice.position || '',
- ' 협력업체 관리-상세보기-세금계산서담당자-부서 ': requestData.businessContacts.taxInvoice.department || '',
- ' 협력업체 관리-상세보기-세금계산서담당자-담당업무 ': requestData.businessContacts.taxInvoice.responsibility || '',
- ' 협력업체 관리-상세보기-세금계산서담당자-이메일 ': requestData.businessContacts.taxInvoice.email || '',
+ // 담당자 연락처 - 영업담당자
+ '영업담당자-담당자명': requestData.businessContacts.sales.name || '',
+ '영업담당자-직급': requestData.businessContacts.sales.position || '',
+ '영업담당자-부서': requestData.businessContacts.sales.department || '',
+ '영업담당자-담당업무': requestData.businessContacts.sales.responsibility || '',
+ '영업담당자-이메일': requestData.businessContacts.sales.email || '',
+
+ // 담당자 연락처 - 설계담당자
+ '설계담당자-담당자명': requestData.businessContacts.design.name || '',
+ '설계담당자-직급': requestData.businessContacts.design.position || '',
+ '설계담당자-부서': requestData.businessContacts.design.department || '',
+ '설계담당자-담당업무': requestData.businessContacts.design.responsibility || '',
+ '설계담당자-이메일': requestData.businessContacts.design.email || '',
+
+ // 담당자 연락처 - 납기담당자
+ '납기담당자-담당자명': requestData.businessContacts.delivery.name || '',
+ '납기담당자-직급': requestData.businessContacts.delivery.position || '',
+ '납기담당자-부서': requestData.businessContacts.delivery.department || '',
+ '납기담당자-담당업무': requestData.businessContacts.delivery.responsibility || '',
+ '납기담당자-이메일': requestData.businessContacts.delivery.email || '',
+
+ // 담당자 연락처 - 품질담당자
+ '품질담당자-담당자명': requestData.businessContacts.quality.name || '',
+ '품질담당자-직급': requestData.businessContacts.quality.position || '',
+ '품질담당자-부서': requestData.businessContacts.quality.department || '',
+ '품질담당자-담당업무': requestData.businessContacts.quality.responsibility || '',
+ '품질담당자-이메일': requestData.businessContacts.quality.email || '',
+
+ // 담당자 연락처 - 세금계산서담당자
+ '세금계산서담당자-담당자명': requestData.businessContacts.taxInvoice.name || '',
+ '세금계산서담당자-직급': requestData.businessContacts.taxInvoice.position || '',
+ '세금계산서담당자-부서': requestData.businessContacts.taxInvoice.department || '',
+ '세금계산서담당자-담당업무': requestData.businessContacts.taxInvoice.responsibility || '',
+ '세금계산서담당자-이메일': requestData.businessContacts.taxInvoice.email || '',
- // 기본계약서 현황 (정규업체 등록 시점에는 아직 계약서가 없으므로 빈 값)
- '정규업체등록관리-문서현황-계약동의현황-계약유형 ': '정규업체 등록 요청',
- '정규업체등록관리-문서현황-계약동의현황-상태 ': '등록 대기',
- '정규업체등록관리-문서현황-계약동의현황-서약일자 ': new Date(requestedAt).toLocaleDateString('ko-KR'),
+ // 기본계약서 현황
+ '계약유형': '정규업체 등록 요청',
+ '계약상태': '등록 대기',
+ '서약일자': new Date(requestedAt).toLocaleDateString('ko-KR'),
};
// 디버깅을 위한 로그 출력
console.log('[Template Variables] Generated variables:', Object.keys(variables));
console.log('[Template Variables] Sample values:', {
- companyName: variables[' 협력업체 기본정보-업체명 '],
- businessNumber: variables[' 협력업체 기본정보-사업자번호 '],
- representative: variables[' 협력업체 기본정보-대표자명 '],
+ companyName: variables['협력업체기본정보-업체명'],
+ businessNumber: variables['협력업체기본정보-사업자번호'],
+ representative: variables['협력업체기본정보-대표자명'],
});
return variables;
diff --git a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx
index f40a41f7..f879f065 100644
--- a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx
+++ b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx
@@ -1,334 +1,329 @@
-"use client"
-
-import { type Table } from "@tanstack/react-table"
-import { toast } from "sonner"
-
-import { Button } from "@/components/ui/button"
-import { Mail, FileWarning, Scale, FileText } from "lucide-react"
-import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"
-import {
- sendMissingContractRequestEmails,
- sendAdditionalInfoRequestEmails,
- skipLegalReview
-} from "../service"
-import { useState } from "react"
-import { SkipReasonDialog } from "@/components/vendor-regular-registrations/skip-reason-dialog"
-import { RegistrationRequestDialog } from "@/components/vendor-regular-registrations/registration-request-dialog"
-import { ApprovalPreviewDialog } from "@/components/approval/ApprovalPreviewDialog"
-import { useRouter } from "next/navigation"
-import { useSession } from "next-auth/react"
-import { registerVendorWithApproval } from "../approval-actions"
-import { mapRegistrationToTemplateVariables } from "../handlers"
-import type { RegistrationRequestData } from "@/components/vendor-regular-registrations/registration-request-dialog"
-
-interface VendorRegularRegistrationsTableToolbarActionsProps {
- table: Table<VendorRegularRegistration>
-}
-
-export function VendorRegularRegistrationsTableToolbarActions({
- table,
-}: VendorRegularRegistrationsTableToolbarActionsProps) {
- const router = useRouter()
- const { data: session } = useSession()
-
- const [syncLoading, setSyncLoading] = useState<{
- missingContract: boolean;
- additionalInfo: boolean;
- legalSkip: boolean;
- registrationRequest: boolean;
- }>({
- missingContract: false,
- additionalInfo: false,
- legalSkip: false,
- registrationRequest: false,
- })
-
- const [skipDialogs, setSkipDialogs] = useState<{
- legalReview: boolean;
- }>({
- legalReview: false,
- })
-
- // 2-step 결재 프로세스를 위한 상태
- const [registrationRequestDialog, setRegistrationRequestDialog] = useState<{
- open: boolean;
- registration: VendorRegularRegistration | null;
- }>({
- open: false,
- registration: null,
- })
-
- const [approvalDialog, setApprovalDialog] = useState<{
- open: boolean;
- registration: VendorRegularRegistration | null;
- }>({
- open: false,
- registration: null,
- })
-
- // 결재를 위한 중간 상태 저장
- const [registrationFormData, setRegistrationFormData] = useState<RegistrationRequestData | null>(null)
- const [approvalVariables, setApprovalVariables] = useState<Record<string, string>>({})
-
- const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original)
-
-
-
- const handleSendMissingContractRequest = async () => {
- if (selectedRows.length === 0) {
- toast.error("이메일을 발송할 업체를 선택해주세요.")
- return
- }
-
- setSyncLoading(prev => ({ ...prev, missingContract: true }))
- try {
- const vendorIds = selectedRows.map(row => row.vendorId)
- const result = await sendMissingContractRequestEmails(vendorIds)
-
- if (result.success) {
- toast.success(result.message)
- } else {
- toast.error(result.error)
- }
- } catch (error) {
- console.error("Error sending missing contract request:", error)
- toast.error("누락계약요청 이메일 발송 중 오류가 발생했습니다.")
- } finally {
- setSyncLoading(prev => ({ ...prev, missingContract: false }))
- }
- }
-
- const handleSendAdditionalInfoRequest = async () => {
- if (selectedRows.length === 0) {
- toast.error("이메일을 발송할 업체를 선택해주세요.")
- return
- }
-
- setSyncLoading(prev => ({ ...prev, additionalInfo: true }))
- try {
- const vendorIds = selectedRows.map(row => row.vendorId)
- const result = await sendAdditionalInfoRequestEmails(vendorIds)
-
- if (result.success) {
- toast.success(result.message)
- } else {
- toast.error(result.error)
- }
- } catch (error) {
- console.error("Error sending additional info request:", error)
- toast.error("추가정보요청 이메일 발송 중 오류가 발생했습니다.")
- } finally {
- setSyncLoading(prev => ({ ...prev, additionalInfo: false }))
- }
- }
-
- const handleLegalReviewSkip = async (reason: string) => {
- const cpReviewRows = selectedRows.filter(row => row.status === "cp_review");
- if (cpReviewRows.length === 0) {
- toast.error("CP검토 상태인 업체를 선택해주세요.");
- return;
- }
-
- setSyncLoading(prev => ({ ...prev, legalSkip: true }));
- try {
- const vendorIds = cpReviewRows.map(row => row.vendorId);
- const result = await skipLegalReview(vendorIds, reason);
-
- if (result.success) {
- toast.success(result.message);
- router.refresh();
- } else {
- toast.error(result.error);
- }
- } catch (error) {
- console.error("Error skipping legal review:", error);
- toast.error("법무검토 Skip 처리 중 오류가 발생했습니다.");
- } finally {
- setSyncLoading(prev => ({ ...prev, legalSkip: false }));
- }
- };
-
- // 등록요청 핸들러 - Step 1: 정보 입력
- const handleRegistrationRequest = () => {
- const approvalReadyRows = selectedRows.filter(row => row.status === "approval_ready");
-
- if (approvalReadyRows.length === 0) {
- toast.error("조건충족 상태의 벤더를 선택해주세요.");
- return;
- }
-
- if (approvalReadyRows.length > 1) {
- toast.error("정규업체 등록 요청은 한 번에 하나씩만 가능합니다.");
- return;
- }
-
- setRegistrationRequestDialog({
- open: true,
- registration: approvalReadyRows[0],
- });
- };
-
- // 등록요청 정보 입력 완료 - Step 1에서 Step 2로 전환
- const handleRegistrationRequestSubmit = async (requestData: RegistrationRequestData) => {
- if (!registrationRequestDialog.registration || !session?.user) return;
-
- try {
- // 폼 데이터 저장
- setRegistrationFormData(requestData);
-
- // 결재 템플릿 변수 생성
- const requestedAt = new Date();
- const variables = await mapRegistrationToTemplateVariables({
- requestData,
- requestedAt,
- });
-
- setApprovalVariables(variables);
-
- // RegistrationRequestDialog 닫고 ApprovalPreviewDialog 열기
- setRegistrationRequestDialog({ open: false, registration: null });
- setApprovalDialog({
- open: true,
- registration: registrationRequestDialog.registration,
- });
- } catch (error) {
- console.error("결재 준비 중 오류 발생:", error);
- toast.error("결재 준비 중 오류가 발생했습니다.");
- }
- };
-
- // 결재 상신 - Step 2: 결재선 선택 후 최종 상신
- const handleApprovalSubmit = async (approvers: any[]) => {
- if (!approvalDialog.registration || !registrationFormData || !session?.user) {
- toast.error("세션 정보가 없습니다.");
- return;
- }
-
- setSyncLoading(prev => ({ ...prev, registrationRequest: true }));
- try {
- // 결재선에서 EP ID 추출 (상신자 제외)
- const approverEpIds = approvers
- .filter((line) => line.seq !== "0" && line.epId)
- .map((line) => line.epId!);
-
- // 결재 워크플로우 시작
- const result = await registerVendorWithApproval({
- registrationId: approvalDialog.registration.id,
- requestData: registrationFormData,
- vendorId: approvalDialog.registration.vendorId, // vendors 테이블에서 정보를 가져오기 위한 vendorId
- currentUser: {
- id: Number(session.user.id),
- epId: session.user.epId || null,
- email: session.user.email || undefined,
- },
- approvers: approverEpIds,
- });
-
- if (result.status === 'pending_approval') {
- // 성공 시에만 상태 초기화 및 페이지 리로드
- setRegistrationFormData(null);
- setApprovalVariables({});
- setApprovalDialog({ open: false, registration: null });
- toast.success("정규업체 등록 결재가 상신되었습니다.");
- router.refresh();
- }
- } catch (error) {
- console.error("결재 상신 중 오류:", error);
- toast.error("결재 상신 중 오류가 발생했습니다.");
- } finally {
- setSyncLoading(prev => ({ ...prev, registrationRequest: false }));
- }
- };
-
- // CP검토 상태인 선택된 행들 개수
- const cpReviewCount = selectedRows.filter(row => row.status === "cp_review").length;
-
- // 조건충족 상태인 선택된 행들 개수
- const approvalReadyCount = selectedRows.filter(row => row.status === "approval_ready").length;
-
- return (
- <div className="flex items-center gap-2">
- <Button
- variant="outline"
- size="sm"
- onClick={handleSendMissingContractRequest}
- disabled={syncLoading.missingContract || selectedRows.length === 0}
- >
- <FileWarning className="mr-2 h-4 w-4" />
- {syncLoading.missingContract ? "발송 중..." : "누락계약요청"}
- </Button>
-
- <Button
- variant="outline"
- size="sm"
- onClick={handleSendAdditionalInfoRequest}
- disabled={syncLoading.additionalInfo || selectedRows.length === 0}
- >
- <Mail className="mr-2 h-4 w-4" />
- {syncLoading.additionalInfo ? "발송 중..." : "추가정보요청"}
- </Button>
-
- <Button
- variant="outline"
- size="sm"
- onClick={() => setSkipDialogs(prev => ({ ...prev, legalReview: true }))}
- disabled={syncLoading.legalSkip || cpReviewCount === 0}
- >
- <Scale className="mr-2 h-4 w-4" />
- {syncLoading.legalSkip ? "처리 중..." : "GTC Skip"}
- </Button>
-
- <Button
- variant="default"
- size="sm"
- onClick={handleRegistrationRequest}
- disabled={syncLoading.registrationRequest || approvalReadyCount === 0}
- >
- <FileText className="mr-2 h-4 w-4" />
- {syncLoading.registrationRequest ? "처리 중..." : "등록요청"}
- </Button>
-
- <SkipReasonDialog
- open={skipDialogs.legalReview}
- onOpenChange={(open) => setSkipDialogs(prev => ({ ...prev, legalReview: open }))}
- title="GTC Skip"
- description={`선택된 ${cpReviewCount}개 업체의 GTC를 Skip하고 CP완료 상태로 변경합니다. Skip 사유를 입력해주세요.`}
- onConfirm={handleLegalReviewSkip}
- loading={syncLoading.legalSkip}
- />
-
- <RegistrationRequestDialog
- open={registrationRequestDialog.open}
- onOpenChange={(open) => setRegistrationRequestDialog(prev => ({ ...prev, open }))}
- registration={registrationRequestDialog.registration}
- onSubmit={handleRegistrationRequestSubmit}
- />
-
- {/* 결재 미리보기 Dialog - 정규업체 등록 */}
- {session?.user && approvalDialog.registration && (
- <ApprovalPreviewDialog
- open={approvalDialog.open}
- onOpenChange={(open) => {
- setApprovalDialog(prev => ({ ...prev, open }));
- if (!open) {
- // 다이얼로그가 닫히면 폼 데이터도 초기화
- setRegistrationFormData(null);
- setApprovalVariables({});
- }
- }}
- templateName="정규업체 등록"
- variables={approvalVariables}
- title={`정규업체 등록 - ${approvalDialog.registration.companyName}`}
- description={`${approvalDialog.registration.companyName} 정규업체 등록 요청`}
- currentUser={{
- id: Number(session.user.id),
- epId: session.user.epId || null,
- name: session.user.name || null,
- email: session.user.email || '',
- }}
- onSubmit={handleApprovalSubmit}
- />
- )}
- </div>
- )
-}
+"use client"
+
+import { type Table } from "@tanstack/react-table"
+import { toast } from "sonner"
+
+import { Button } from "@/components/ui/button"
+import { Mail, FileWarning, Scale, FileText } from "lucide-react"
+import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"
+import {
+ sendMissingContractRequestEmails,
+ sendAdditionalInfoRequestEmails,
+ skipLegalReview
+} from "../service"
+import { useState } from "react"
+import { SkipReasonDialog } from "@/components/vendor-regular-registrations/skip-reason-dialog"
+import { RegistrationRequestDialog } from "@/components/vendor-regular-registrations/registration-request-dialog"
+import { ApprovalPreviewDialog } from "@/lib/approval/approval-preview-dialog"
+import { useRouter } from "next/navigation"
+import { useSession } from "next-auth/react"
+import { registerVendorWithApproval } from "../approval-actions"
+import { mapRegistrationToTemplateVariables } from "../handlers"
+import type { RegistrationRequestData } from "@/components/vendor-regular-registrations/registration-request-dialog"
+
+interface VendorRegularRegistrationsTableToolbarActionsProps {
+ table: Table<VendorRegularRegistration>
+}
+
+export function VendorRegularRegistrationsTableToolbarActions({
+ table,
+}: VendorRegularRegistrationsTableToolbarActionsProps) {
+ const router = useRouter()
+ const { data: session } = useSession()
+
+ const [syncLoading, setSyncLoading] = useState<{
+ missingContract: boolean;
+ additionalInfo: boolean;
+ legalSkip: boolean;
+ registrationRequest: boolean;
+ }>({
+ missingContract: false,
+ additionalInfo: false,
+ legalSkip: false,
+ registrationRequest: false,
+ })
+
+ const [skipDialogs, setSkipDialogs] = useState<{
+ legalReview: boolean;
+ }>({
+ legalReview: false,
+ })
+
+ // 2-step 결재 프로세스를 위한 상태
+ const [registrationRequestDialog, setRegistrationRequestDialog] = useState<{
+ open: boolean;
+ registration: VendorRegularRegistration | null;
+ }>({
+ open: false,
+ registration: null,
+ })
+
+ const [approvalDialog, setApprovalDialog] = useState<{
+ open: boolean;
+ registration: VendorRegularRegistration | null;
+ }>({
+ open: false,
+ registration: null,
+ })
+
+ // 결재를 위한 중간 상태 저장
+ const [registrationFormData, setRegistrationFormData] = useState<RegistrationRequestData | null>(null)
+ const [approvalVariables, setApprovalVariables] = useState<Record<string, string>>({})
+
+ const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original)
+
+
+
+ const handleSendMissingContractRequest = async () => {
+ if (selectedRows.length === 0) {
+ toast.error("이메일을 발송할 업체를 선택해주세요.")
+ return
+ }
+
+ setSyncLoading(prev => ({ ...prev, missingContract: true }))
+ try {
+ const vendorIds = selectedRows.map(row => row.vendorId)
+ const result = await sendMissingContractRequestEmails(vendorIds)
+
+ if (result.success) {
+ toast.success(result.message)
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ console.error("Error sending missing contract request:", error)
+ toast.error("누락계약요청 이메일 발송 중 오류가 발생했습니다.")
+ } finally {
+ setSyncLoading(prev => ({ ...prev, missingContract: false }))
+ }
+ }
+
+ const handleSendAdditionalInfoRequest = async () => {
+ if (selectedRows.length === 0) {
+ toast.error("이메일을 발송할 업체를 선택해주세요.")
+ return
+ }
+
+ setSyncLoading(prev => ({ ...prev, additionalInfo: true }))
+ try {
+ const vendorIds = selectedRows.map(row => row.vendorId)
+ const result = await sendAdditionalInfoRequestEmails(vendorIds)
+
+ if (result.success) {
+ toast.success(result.message)
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ console.error("Error sending additional info request:", error)
+ toast.error("추가정보요청 이메일 발송 중 오류가 발생했습니다.")
+ } finally {
+ setSyncLoading(prev => ({ ...prev, additionalInfo: false }))
+ }
+ }
+
+ const handleLegalReviewSkip = async (reason: string) => {
+ const cpReviewRows = selectedRows.filter(row => row.status === "cp_review");
+ if (cpReviewRows.length === 0) {
+ toast.error("CP검토 상태인 업체를 선택해주세요.");
+ return;
+ }
+
+ setSyncLoading(prev => ({ ...prev, legalSkip: true }));
+ try {
+ const vendorIds = cpReviewRows.map(row => row.vendorId);
+ const result = await skipLegalReview(vendorIds, reason);
+
+ if (result.success) {
+ toast.success(result.message);
+ router.refresh();
+ } else {
+ toast.error(result.error);
+ }
+ } catch (error) {
+ console.error("Error skipping legal review:", error);
+ toast.error("법무검토 Skip 처리 중 오류가 발생했습니다.");
+ } finally {
+ setSyncLoading(prev => ({ ...prev, legalSkip: false }));
+ }
+ };
+
+ // 등록요청 핸들러 - Step 1: 정보 입력
+ const handleRegistrationRequest = () => {
+ const approvalReadyRows = selectedRows.filter(row => row.status === "approval_ready");
+
+ if (approvalReadyRows.length === 0) {
+ toast.error("조건충족 상태의 벤더를 선택해주세요.");
+ return;
+ }
+
+ if (approvalReadyRows.length > 1) {
+ toast.error("정규업체 등록 요청은 한 번에 하나씩만 가능합니다.");
+ return;
+ }
+
+ setRegistrationRequestDialog({
+ open: true,
+ registration: approvalReadyRows[0],
+ });
+ };
+
+ // 등록요청 정보 입력 완료 - Step 1에서 Step 2로 전환
+ const handleRegistrationRequestSubmit = async (requestData: RegistrationRequestData) => {
+ if (!registrationRequestDialog.registration || !session?.user) return;
+
+ try {
+ // 폼 데이터 저장
+ setRegistrationFormData(requestData);
+
+ // 결재 템플릿 변수 생성 (vendorId 포함)
+ const requestedAt = new Date();
+ const variables = await mapRegistrationToTemplateVariables({
+ requestData,
+ requestedAt,
+ vendorId: registrationRequestDialog.registration.vendorId, // vendors 테이블에서 추가 정보 가져오기
+ });
+
+ setApprovalVariables(variables);
+
+ // RegistrationRequestDialog 닫고 ApprovalPreviewDialog 열기
+ setRegistrationRequestDialog({ open: false, registration: null });
+ setApprovalDialog({
+ open: true,
+ registration: registrationRequestDialog.registration,
+ });
+ } catch (error) {
+ console.error("결재 준비 중 오류 발생:", error);
+ toast.error("결재 준비 중 오류가 발생했습니다.");
+ }
+ };
+
+ // 결재 상신 - Step 2: 결재선 선택 후 최종 상신
+ const handleApprovalSubmit = async ({ approvers, title, attachments }: { approvers: string[], title: string, attachments?: File[] }) => {
+ if (!approvalDialog.registration || !registrationFormData || !session?.user) {
+ toast.error("세션 정보가 없습니다.");
+ return;
+ }
+
+ setSyncLoading(prev => ({ ...prev, registrationRequest: true }));
+ try {
+ // 결재 워크플로우 시작 (approvers는 이미 EP ID 배열)
+ const result = await registerVendorWithApproval({
+ registrationId: approvalDialog.registration.id,
+ requestData: registrationFormData,
+ vendorId: approvalDialog.registration.vendorId, // vendors 테이블에서 정보를 가져오기 위한 vendorId
+ currentUser: {
+ id: Number(session.user.id),
+ epId: session.user.epId || null,
+ email: session.user.email || undefined,
+ },
+ approvers: approvers,
+ });
+
+ if (result.status === 'pending_approval') {
+ // 성공 시에만 상태 초기화 및 페이지 리로드
+ setRegistrationFormData(null);
+ setApprovalVariables({});
+ setApprovalDialog({ open: false, registration: null });
+ toast.success("정규업체 등록 결재가 상신되었습니다.");
+ router.refresh();
+ }
+ } catch (error) {
+ console.error("결재 상신 중 오류:", error);
+ toast.error("결재 상신 중 오류가 발생했습니다.");
+ } finally {
+ setSyncLoading(prev => ({ ...prev, registrationRequest: false }));
+ }
+ };
+
+ // CP검토 상태인 선택된 행들 개수
+ const cpReviewCount = selectedRows.filter(row => row.status === "cp_review").length;
+
+ // 조건충족 상태인 선택된 행들 개수
+ const approvalReadyCount = selectedRows.filter(row => row.status === "approval_ready").length;
+
+ return (
+ <div className="flex items-center gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleSendMissingContractRequest}
+ disabled={syncLoading.missingContract || selectedRows.length === 0}
+ >
+ <FileWarning className="mr-2 h-4 w-4" />
+ {syncLoading.missingContract ? "발송 중..." : "누락계약요청"}
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleSendAdditionalInfoRequest}
+ disabled={syncLoading.additionalInfo || selectedRows.length === 0}
+ >
+ <Mail className="mr-2 h-4 w-4" />
+ {syncLoading.additionalInfo ? "발송 중..." : "추가정보요청"}
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setSkipDialogs(prev => ({ ...prev, legalReview: true }))}
+ disabled={syncLoading.legalSkip || cpReviewCount === 0}
+ >
+ <Scale className="mr-2 h-4 w-4" />
+ {syncLoading.legalSkip ? "처리 중..." : "GTC Skip"}
+ </Button>
+
+ <Button
+ variant="default"
+ size="sm"
+ onClick={handleRegistrationRequest}
+ disabled={syncLoading.registrationRequest || approvalReadyCount === 0}
+ >
+ <FileText className="mr-2 h-4 w-4" />
+ {syncLoading.registrationRequest ? "처리 중..." : "등록요청"}
+ </Button>
+
+ <SkipReasonDialog
+ open={skipDialogs.legalReview}
+ onOpenChange={(open) => setSkipDialogs(prev => ({ ...prev, legalReview: open }))}
+ title="GTC Skip"
+ description={`선택된 ${cpReviewCount}개 업체의 GTC를 Skip하고 CP완료 상태로 변경합니다. Skip 사유를 입력해주세요.`}
+ onConfirm={handleLegalReviewSkip}
+ loading={syncLoading.legalSkip}
+ />
+
+ <RegistrationRequestDialog
+ open={registrationRequestDialog.open}
+ onOpenChange={(open) => setRegistrationRequestDialog(prev => ({ ...prev, open }))}
+ registration={registrationRequestDialog.registration}
+ onSubmit={handleRegistrationRequestSubmit}
+ />
+
+ {/* 결재 미리보기 Dialog - 정규업체 등록 */}
+ {session?.user && session.user.epId && approvalDialog.registration && (
+ <ApprovalPreviewDialog
+ open={approvalDialog.open}
+ onOpenChange={(open) => {
+ setApprovalDialog(prev => ({ ...prev, open }));
+ if (!open) {
+ // 다이얼로그가 닫히면 폼 데이터도 초기화
+ setRegistrationFormData(null);
+ setApprovalVariables({});
+ }
+ }}
+ templateName="정규업체 등록"
+ variables={approvalVariables}
+ title={`정규업체 등록 - ${approvalDialog.registration.companyName}`}
+ currentUser={{
+ id: Number(session.user.id),
+ epId: session.user.epId,
+ name: session.user.name || undefined,
+ email: session.user.email || undefined,
+ }}
+ onConfirm={handleApprovalSubmit}
+ />
+ )}
+ </div>
+ )
+}