summaryrefslogtreecommitdiff
path: root/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx')
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx770
1 files changed, 0 insertions, 770 deletions
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx
deleted file mode 100644
index 3205df08..00000000
--- a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx
+++ /dev/null
@@ -1,770 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import { Button } from '@/components/ui/button'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog'
-import { Badge } from '@/components/ui/badge'
-import { BiddingCompany } from './bidding-pre-quote-vendor-columns'
-import { sendPreQuoteInvitations, sendBiddingBasicContracts, getExistingBasicContractsForBidding } from '../service'
-import { getActiveContractTemplates } from '../../service'
-import { useToast } from '@/hooks/use-toast'
-import { useTransition } from 'react'
-import { Mail, Building2, Calendar, FileText, CheckCircle, Info, RefreshCw } from 'lucide-react'
-import { Progress } from '@/components/ui/progress'
-import { Separator } from '@/components/ui/separator'
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { cn } from '@/lib/utils'
-
-interface BiddingPreQuoteInvitationDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- companies: BiddingCompany[]
- biddingId: number
- biddingTitle: string
- projectName?: string
- onSuccess: () => void
-}
-
-interface BasicContractTemplate {
- id: number
- templateName: string
- revision: number
- status: string
- filePath: string | null
- validityPeriod: number | null
- legalReviewRequired: boolean
- createdAt: Date | null
-}
-
-interface SelectedContract {
- templateId: number
- templateName: string
- contractType: string // templateName을 contractType으로 사용
- checked: boolean
-}
-
-// PDF 생성 유틸리티 함수
-const generateBasicContractPdf = async (
- template: BasicContractTemplate,
- vendorId: number
-): Promise<{ buffer: number[]; fileName: string }> => {
- try {
- // 1. 템플릿 데이터 준비 (서버 API 호출)
- const prepareResponse = await fetch("/api/contracts/prepare-template", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- templateName: template.templateName,
- vendorId,
- }),
- });
-
- if (!prepareResponse.ok) {
- throw new Error("템플릿 준비 실패");
- }
-
- const { template: preparedTemplate, templateData } = await prepareResponse.json();
-
- // 2. 템플릿 파일 다운로드
- const templateResponse = await fetch("/api/contracts/get-template", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ templatePath: preparedTemplate.filePath }),
- });
-
- const templateBlob = await templateResponse.blob();
- const templateFile = new window.File([templateBlob], "template.docx", {
- type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
- });
-
- // 3. PDFTron WebViewer로 PDF 변환
- const { default: WebViewer } = await import("@pdftron/webviewer");
-
- const tempDiv = document.createElement('div');
- tempDiv.style.display = 'none';
- document.body.appendChild(tempDiv);
-
- try {
- const instance = await WebViewer(
- {
- path: "/pdftronWeb",
- licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
- fullAPI: true,
- },
- tempDiv
- );
-
- const { Core } = instance;
- const { createDocument } = Core;
-
- const templateDoc = await createDocument(templateFile, {
- filename: templateFile.name,
- extension: 'docx',
- });
-
- // 변수 치환 적용
- await templateDoc.applyTemplateValues(templateData);
-
- // PDF 변환
- const fileData = await templateDoc.getFileData();
- const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' });
-
- const fileName = `${template.templateName}_${Date.now()}.pdf`;
-
- return {
- buffer: Array.from(pdfBuffer), // Uint8Array를 일반 배열로 변환
- fileName
- };
-
- } finally {
- if (tempDiv.parentNode) {
- document.body.removeChild(tempDiv);
- }
- }
- } catch (error) {
- console.error(`기본계약 PDF 생성 실패 (${template.templateName}):`, error);
- throw error;
- }
-};
-
-export function BiddingPreQuoteInvitationDialog({
- open,
- onOpenChange,
- companies,
- biddingId,
- biddingTitle,
- projectName,
- onSuccess
-}: BiddingPreQuoteInvitationDialogProps) {
- const { toast } = useToast()
- const [isPending, startTransition] = useTransition()
- const [selectedCompanyIds, setSelectedCompanyIds] = React.useState<number[]>([])
- const [preQuoteDeadline, setPreQuoteDeadline] = React.useState('')
- const [additionalMessage, setAdditionalMessage] = React.useState('')
-
- // 기본계약 관련 상태
- const [existingContracts, setExistingContracts] = React.useState<any[]>([])
- const [isGeneratingPdfs, setIsGeneratingPdfs] = React.useState(false)
- const [pdfGenerationProgress, setPdfGenerationProgress] = React.useState(0)
- const [currentGeneratingContract, setCurrentGeneratingContract] = React.useState('')
-
- // 기본계약서 템플릿 관련 상태
- const [availableTemplates, setAvailableTemplates] = React.useState<BasicContractTemplate[]>([])
- const [selectedContracts, setSelectedContracts] = React.useState<SelectedContract[]>([])
- const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false)
-
- // 초대 가능한 업체들 (pending 상태인 업체들)
- const invitableCompanies = React.useMemo(() => companies.filter(company =>
- company.invitationStatus === 'pending' && company.companyName
- ), [companies])
-
- // 다이얼로그가 열릴 때 기존 계약 조회 및 템플릿 로드
- React.useEffect(() => {
- if (open) {
- const fetchInitialData = async () => {
- setIsLoadingTemplates(true);
- try {
- const [contractsResult, templatesData] = await Promise.all([
- getExistingBasicContractsForBidding(biddingId),
- getActiveContractTemplates()
- ]);
-
- // 기존 계약 조회 - 서버 액션 사용
- const existingContractsResult = await getExistingBasicContractsForBidding(biddingId);
- setExistingContracts(existingContractsResult.success ? existingContractsResult.contracts || [] : []);
-
- // 템플릿 로드 (4개 타입만 필터링)
- // 4개 템플릿 타입만 필터링: 비밀, General, Project, 기술자료
- const allowedTemplateNames = ['비밀', 'General GTC', '기술', '기술자료'];
- const filteredTemplates = (templatesData.templates || []).filter((template: any) =>
- allowedTemplateNames.some(allowedName =>
- template.templateName.includes(allowedName) ||
- allowedName.includes(template.templateName)
- )
- );
- setAvailableTemplates(filteredTemplates as BasicContractTemplate[]);
- const initialSelected = filteredTemplates.map((template: any) => ({
- templateId: template.id,
- templateName: template.templateName,
- contractType: template.templateName,
- checked: false
- }));
- setSelectedContracts(initialSelected);
-
- } catch (error) {
- console.error('초기 데이터 로드 실패:', error);
- toast({
- title: '오류',
- description: '기본 정보를 불러오는 데 실패했습니다.',
- variant: 'destructive',
- });
- setExistingContracts([]);
- setAvailableTemplates([]);
- setSelectedContracts([]);
- } finally {
- setIsLoadingTemplates(false);
- }
- }
- fetchInitialData();
- }
- }, [open, biddingId, toast]);
-
- const handleSelectAll = (checked: boolean | 'indeterminate') => {
- if (checked) {
- // 기존 계약이 없는 업체만 선택
- const availableCompanies = invitableCompanies.filter(company =>
- !existingContracts.some(ec => ec.vendorId === company.companyId)
- )
- setSelectedCompanyIds(availableCompanies.map(company => company.id))
- } else {
- setSelectedCompanyIds([])
- }
- }
-
- const handleSelectCompany = (companyId: number, checked: boolean) => {
- const company = invitableCompanies.find(c => c.id === companyId)
- const hasExistingContract = company ? existingContracts.some(ec => ec.vendorId === company.companyId) : false
-
- if (hasExistingContract) {
- toast({
- title: '선택 불가',
- description: '이미 기본계약서를 받은 업체는 다시 선택할 수 없습니다.',
- variant: 'default',
- })
- return
- }
-
- if (checked) {
- setSelectedCompanyIds(prev => [...prev, companyId])
- } else {
- setSelectedCompanyIds(prev => prev.filter(id => id !== companyId))
- }
- }
-
- // 기본계약서 선택 토글
- const toggleContractSelection = (templateId: number) => {
- setSelectedContracts(prev =>
- prev.map(contract =>
- contract.templateId === templateId
- ? { ...contract, checked: !contract.checked }
- : contract
- )
- )
- }
-
- // 모든 기본계약서 선택/해제
- const toggleAllContractSelection = (checked: boolean | 'indeterminate') => {
- setSelectedContracts(prev =>
- prev.map(contract => ({ ...contract, checked: !!checked }))
- )
- }
-
- const handleSendInvitations = () => {
- if (selectedCompanyIds.length === 0) {
- toast({
- title: '알림',
- description: '초대를 발송할 업체를 선택해주세요.',
- variant: 'default',
- })
- return
- }
-
- const selectedContractTemplates = selectedContracts.filter(c => c.checked);
- const companiesForContracts = invitableCompanies.filter(company => selectedCompanyIds.includes(company.id));
-
- const vendorsToGenerateContracts = companiesForContracts.filter(company =>
- !existingContracts.some(ec =>
- ec.vendorId === company.companyId && ec.biddingCompanyId === company.id
- )
- );
-
- startTransition(async () => {
- try {
- // 1. 사전견적 초대 발송
- const invitationResponse = await sendPreQuoteInvitations(
- selectedCompanyIds,
- preQuoteDeadline || undefined
- )
-
- if (!invitationResponse.success) {
- toast({
- title: '초대 발송 실패',
- description: invitationResponse.error,
- variant: 'destructive',
- })
- return
- }
-
- // 2. 기본계약 발송 (선택된 템플릿과 업체가 있는 경우)
- let contractResponse: Awaited<ReturnType<typeof sendBiddingBasicContracts>> | null = null
- if (selectedContractTemplates.length > 0 && selectedCompanyIds.length > 0) {
- setIsGeneratingPdfs(true)
- setPdfGenerationProgress(0)
-
- const generatedPdfsMap = new Map<string, { buffer: number[], fileName: string }>()
-
- let generatedCount = 0;
- for (const vendor of vendorsToGenerateContracts) {
- for (const contract of selectedContractTemplates) {
- setCurrentGeneratingContract(`${vendor.companyName} - ${contract.templateName}`);
- const templateDetails = availableTemplates.find(t => t.id === contract.templateId);
-
- if (templateDetails) {
- const pdfData = await generateBasicContractPdf(templateDetails, vendor.companyId);
- // sendBiddingBasicContracts와 동일한 키 형식 사용
- let contractType = '';
- if (contract.templateName.includes('비밀')) {
- contractType = 'NDA';
- } else if (contract.templateName.includes('General GTC')) {
- contractType = 'General_GTC';
- } else if (contract.templateName.includes('기술') && !contract.templateName.includes('기술자료')) {
- contractType = 'Project_GTC';
- } else if (contract.templateName.includes('기술자료')) {
- contractType = '기술자료';
- }
- const key = `${vendor.companyId}_${contractType}_${contract.templateName}`;
- generatedPdfsMap.set(key, pdfData);
- }
- }
- generatedCount++;
- setPdfGenerationProgress((generatedCount / vendorsToGenerateContracts.length) * 100);
- }
-
- setIsGeneratingPdfs(false);
-
- const vendorData = companiesForContracts.map(company => {
- // 선택된 템플릿에 따라 contractRequirements 동적으로 설정
- const contractRequirements = {
- ndaYn: selectedContractTemplates.some(c => c.templateName.includes('비밀')),
- generalGtcYn: selectedContractTemplates.some(c => c.templateName.includes('General GTC')),
- projectGtcYn: selectedContractTemplates.some(c => c.templateName.includes('기술') && !c.templateName.includes('기술자료')),
- agreementYn: selectedContractTemplates.some(c => c.templateName.includes('기술자료'))
- };
-
- return {
- vendorId: company.companyId,
- vendorName: company.companyName || '',
- vendorCode: company.companyCode,
- vendorCountry: '대한민국',
- selectedMainEmail: company.contactEmail || '',
- contactPerson: company.contactPerson,
- contactEmail: company.contactEmail,
- biddingCompanyId: company.id,
- biddingId: biddingId,
- hasExistingContracts: existingContracts.some(ec =>
- ec.vendorId === company.companyId && ec.biddingCompanyId === company.id
- ),
- contractRequirements,
- additionalEmails: [],
- customEmails: []
- };
- });
-
- const pdfsArray = Array.from(generatedPdfsMap.entries()).map(([key, data]) => ({
- key,
- buffer: data.buffer,
- fileName: data.fileName,
- }));
-
- console.log("Calling sendBiddingBasicContracts with biddingId:", biddingId);
- console.log("vendorData:", vendorData.map(v => ({ vendorId: v.vendorId, biddingCompanyId: v.biddingCompanyId, biddingId: v.biddingId })));
-
- contractResponse = await sendBiddingBasicContracts(
- biddingId,
- vendorData,
- pdfsArray,
- additionalMessage
- );
- }
-
- let successMessage = '사전견적 초대가 성공적으로 발송되었습니다.';
- if (contractResponse && contractResponse.success) {
- successMessage += `\n${contractResponse.message}`;
- }
-
- toast({
- title: '성공',
- description: successMessage,
- })
-
- // 상태 초기화
- setSelectedCompanyIds([]);
- setPreQuoteDeadline('');
- setAdditionalMessage('');
- setExistingContracts([]);
- setIsGeneratingPdfs(false);
- setPdfGenerationProgress(0);
- setCurrentGeneratingContract('');
- setSelectedContracts(prev => prev.map(c => ({ ...c, checked: false })));
-
- onOpenChange(false);
- onSuccess();
-
- } catch (error) {
- console.error('발송 실패:', error);
- toast({
- title: '오류',
- description: '발송 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
- variant: 'destructive',
- });
- setIsGeneratingPdfs(false);
- }
- })
- }
-
- const handleOpenChange = (open: boolean) => {
- onOpenChange(open)
- if (!open) {
- setSelectedCompanyIds([])
- setPreQuoteDeadline('')
- setAdditionalMessage('')
- setExistingContracts([])
- setIsGeneratingPdfs(false)
- setPdfGenerationProgress(0)
- setCurrentGeneratingContract('')
- setSelectedContracts([])
- }
- }
-
- const selectedContractCount = selectedContracts.filter(c => c.checked).length;
- const selectedCompanyCount = selectedCompanyIds.length;
- const companiesToReceiveContracts = invitableCompanies.filter(company => selectedCompanyIds.includes(company.id));
-
- // 기존 계약이 없는 업체들만 계산
- const availableCompanies = invitableCompanies.filter(company =>
- !existingContracts.some(ec => ec.vendorId === company.companyId)
- );
- const selectedAvailableCompanyCount = selectedCompanyIds.filter(id =>
- availableCompanies.some(company => company.id === id)
- ).length;
-
- // 선택된 업체들 중 기존 계약이 있는 업체들
- const selectedCompaniesWithExistingContracts = invitableCompanies.filter(company =>
- selectedCompanyIds.includes(company.id) &&
- existingContracts.some(ec => ec.vendorId === company.companyId)
- );
-
- return (
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className="sm:max-w-[800px] max-h-[90vh] flex flex-col">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <Mail className="w-5 h-5" />
- 사전견적 초대 및 기본계약 발송
- </DialogTitle>
- <DialogDescription>
- 선택한 업체들에게 사전견적 요청과 기본계약서를 발송합니다.
- </DialogDescription>
- </DialogHeader>
-
- <div className="flex-1 overflow-y-auto px-1" style={{ maxHeight: 'calc(70vh - 200px)' }}>
- <div className="space-y-6 pr-4">
- {/* 견적 마감일 설정 */}
- <div className="mb-6 p-4 border rounded-lg bg-muted/30">
- <Label htmlFor="preQuoteDeadline" className="text-sm font-medium mb-2 flex items-center gap-2">
- <Calendar className="w-4 h-4" />
- 견적 마감일
- </Label>
- <Input
- id="preQuoteDeadline"
- type="datetime-local"
- value={preQuoteDeadline}
- onChange={(e) => setPreQuoteDeadline(e.target.value)}
- className="w-full"
- />
- </div>
-
- {/* 기존 계약 정보 알림 */}
- {existingContracts.length > 0 && (
- <Alert className="border-orange-500 bg-orange-50">
- <Info className="h-4 w-4 text-orange-600" />
- <AlertTitle className="text-orange-800">기존 계약 정보</AlertTitle>
- <AlertDescription className="text-orange-700">
- 이미 기본계약을 받은 업체가 있습니다.
- 해당 업체들은 초대 대상에서 제외되며, 계약서 재생성도 건너뜁니다.
- </AlertDescription>
- </Alert>
- )}
-
- {/* 업체 선택 섹션 */}
- <Card className="border-2 border-dashed">
- <CardHeader className="pb-3">
- <CardTitle className="flex items-center gap-2 text-base">
- <Building2 className="h-5 w-5 text-green-600" />
- 초대 대상 업체
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {invitableCompanies.length === 0 ? (
- <div className="text-center py-8 text-muted-foreground">
- 초대 가능한 업체가 없습니다.
- </div>
- ) : (
- <>
- <div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
- <div className="flex items-center gap-2">
- <Checkbox
- id="select-all-companies"
- checked={selectedAvailableCompanyCount === availableCompanies.length && availableCompanies.length > 0}
- onCheckedChange={handleSelectAll}
- />
- <Label htmlFor="select-all-companies" className="font-medium">
- 전체 선택 ({availableCompanies.length}개 업체)
- </Label>
- </div>
- <Badge variant="outline">
- {selectedCompanyCount}개 선택됨
- </Badge>
- </div>
-
- <div className="space-y-3 max-h-80 overflow-y-auto">
- {invitableCompanies.map((company) => {
- const hasExistingContract = existingContracts.some(ec => ec.vendorId === company.companyId);
- return (
- <div key={company.id} className={cn("flex items-center space-x-3 p-3 border rounded-lg transition-colors",
- selectedCompanyIds.includes(company.id) && !hasExistingContract && "border-green-500 bg-green-50",
- hasExistingContract && "border-orange-500 bg-orange-50 opacity-75"
- )}>
- <Checkbox
- id={`company-${company.id}`}
- checked={selectedCompanyIds.includes(company.id)}
- disabled={hasExistingContract}
- onCheckedChange={(checked) => handleSelectCompany(company.id, !!checked)}
- />
- <div className="flex-1">
- <div className="flex items-center gap-2">
- <span className={cn("font-medium", hasExistingContract && "text-muted-foreground")}>
- {company.companyName}
- </span>
- <Badge variant="outline" className="text-xs">
- {company.companyCode}
- </Badge>
- {hasExistingContract && (
- <Badge variant="secondary" className="text-xs">
- <CheckCircle className="h-3 w-3 mr-1" />
- 계약 체결됨
- </Badge>
- )}
- </div>
- {hasExistingContract && (
- <p className="text-xs text-orange-600 mt-1">
- 이미 기본계약서를 받은 업체입니다. 선택에서 제외됩니다.
- </p>
- )}
- </div>
- </div>
- )
- })}
- </div>
- </>
- )}
- </CardContent>
- </Card>
-
- {/* 선택된 업체 중 기존 계약이 있는 경우 경고 */}
- {selectedCompaniesWithExistingContracts.length > 0 && (
- <Alert className="border-red-500 bg-red-50">
- <Info className="h-4 w-4 text-red-600" />
- <AlertTitle className="text-red-800">선택한 업체 중 제외될 업체</AlertTitle>
- <AlertDescription className="text-red-700">
- 선택한 {selectedCompaniesWithExistingContracts.length}개 업체가 이미 기본계약서를 받았습니다.
- 이 업체들은 초대 발송 및 계약서 생성에서 제외됩니다.
- <br />
- <strong>실제 발송 대상: {selectedCompanyCount - selectedCompaniesWithExistingContracts.length}개 업체</strong>
- </AlertDescription>
- </Alert>
- )}
-
- {/* 기본계약서 선택 섹션 */}
- <Separator />
- <Card className="border-2 border-dashed">
- <CardHeader className="pb-3">
- <CardTitle className="flex items-center gap-2 text-base">
- <FileText className="h-5 w-5 text-blue-600" />
- 기본계약서 선택 (선택된 업체에만 발송)
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {isLoadingTemplates ? (
- <div className="text-center py-6">
- <RefreshCw className="h-6 w-6 animate-spin mx-auto mb-2 text-blue-600" />
- <p className="text-sm text-muted-foreground">기본계약서 템플릿을 불러오는 중...</p>
- </div>
- ) : (
- <div className="space-y-4">
- {selectedCompanyCount === 0 && (
- <Alert className="border-red-500 bg-red-50">
- <Info className="h-4 w-4 text-red-600" />
- <AlertTitle className="text-red-800">알림</AlertTitle>
- <AlertDescription className="text-red-700">
- 기본계약서를 발송할 업체를 먼저 선택해주세요.
- </AlertDescription>
- </Alert>
- )}
- {availableTemplates.length === 0 ? (
- <div className="text-center py-8 text-muted-foreground">
- <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
- <p>사용 가능한 기본계약서 템플릿이 없습니다.</p>
- </div>
- ) : (
- <>
- <div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
- <div className="flex items-center gap-2">
- <Checkbox
- id="select-all-contracts"
- checked={selectedContracts.length > 0 && selectedContracts.every(c => c.checked)}
- onCheckedChange={toggleAllContractSelection}
- />
- <Label htmlFor="select-all-contracts" className="font-medium">
- 전체 선택 ({availableTemplates.length}개 템플릿)
- </Label>
- </div>
- <Badge variant="outline">
- {selectedContractCount}개 선택됨
- </Badge>
- </div>
- <div className="grid gap-3 max-h-60 overflow-y-auto">
- {selectedContracts.map((contract) => (
- <div
- key={contract.templateId}
- className={cn(
- "flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors cursor-pointer",
- contract.checked && "border-blue-500 bg-blue-50"
- )}
- onClick={() => toggleContractSelection(contract.templateId)}
- >
- <div className="flex items-center gap-3">
- <Checkbox
- id={`contract-${contract.templateId}`}
- checked={contract.checked}
- onCheckedChange={() => toggleContractSelection(contract.templateId)}
- />
- <div className="flex-1">
- <Label
- htmlFor={`contract-${contract.templateId}`}
- className="font-medium cursor-pointer"
- >
- {contract.templateName}
- </Label>
- <p className="text-xs text-muted-foreground mt-1">
- {contract.contractType}
- </p>
- </div>
- </div>
- </div>
- ))}
- </div>
- </>
- )}
- {selectedContractCount > 0 && (
- <div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-lg">
- <div className="flex items-center gap-2 mb-2">
- <CheckCircle className="h-4 w-4 text-green-600" />
- <span className="font-medium text-green-900 text-sm">
- 선택된 기본계약서 ({selectedContractCount}개)
- </span>
- </div>
- <ul className="space-y-1 text-xs text-green-800 list-disc list-inside">
- {selectedContracts.filter(c => c.checked).map((contract) => (
- <li key={contract.templateId}>
- {contract.templateName}
- </li>
- ))}
- </ul>
- </div>
- )}
- </div>
- )}
- </CardContent>
- </Card>
-
- {/* 추가 메시지 */}
- <div className="space-y-2">
- <Label htmlFor="contractMessage" className="text-sm font-medium">
- 계약서 추가 메시지 (선택사항)
- </Label>
- <textarea
- id="contractMessage"
- className="w-full min-h-[60px] p-3 text-sm border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary"
- placeholder="기본계약서와 함께 보낼 추가 메시지를 입력하세요..."
- value={additionalMessage}
- onChange={(e) => setAdditionalMessage(e.target.value)}
- />
- </div>
-
- {/* PDF 생성 진행 상황 */}
- {isGeneratingPdfs && (
- <Alert className="border-blue-500 bg-blue-50">
- <div className="space-y-3">
- <div className="flex items-center gap-2">
- <RefreshCw className="h-4 w-4 animate-spin text-blue-600" />
- <AlertTitle className="text-blue-800">기본계약서 생성 중</AlertTitle>
- </div>
- <AlertDescription>
- <div className="space-y-2">
- <p className="text-sm text-blue-700">{currentGeneratingContract}</p>
- <Progress value={pdfGenerationProgress} className="h-2" />
- <p className="text-xs text-blue-600">
- {Math.round(pdfGenerationProgress)}% 완료
- </p>
- </div>
- </AlertDescription>
- </div>
- </Alert>
- )}
- </div>
- </div>
-
- <DialogFooter className="flex-col sm:flex-row-reverse sm:justify-between items-center px-4 pt-4">
- <div className="flex gap-2 w-full sm:w-auto">
- <Button variant="outline" onClick={() => handleOpenChange(false)} className="w-full sm:w-auto">
- 취소
- </Button>
- <Button
- onClick={handleSendInvitations}
- disabled={isPending || selectedCompanyCount === 0 || isGeneratingPdfs}
- className="w-full sm:w-auto"
- >
- {isPending ? (
- <>
- <RefreshCw className="w-4 h-4 mr-2 animate-spin" />
- 발송 중...
- </>
- ) : (
- <>
- <Mail className="w-4 h-4 mr-2" />
- 초대 발송 및 계약서 생성
- </>
- )}
- </Button>
- </div>
- {/* {(selectedCompanyCount > 0 || selectedContractCount > 0) && (
- <div className="mt-4 sm:mt-0 text-sm text-muted-foreground">
- {selectedCompanyCount > 0 && (
- <p>
- <strong>{selectedCompanyCount}개 업체</strong>에 초대를 발송합니다.
- </p>
- )}
- {selectedContractCount > 0 && selectedCompanyCount > 0 && (
- <p>
- 이 중 <strong>{companiesToReceiveContracts.length}개 업체</strong>에 <strong>{selectedContractCount}개</strong>의 기본계약서를 발송합니다.
- </p>
- )}
- </div>
- )} */}
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file