diff options
Diffstat (limited to 'lib/tbe-last/table/email-documents-dialog.tsx')
| -rw-r--r-- | lib/tbe-last/table/email-documents-dialog.tsx | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/lib/tbe-last/table/email-documents-dialog.tsx b/lib/tbe-last/table/email-documents-dialog.tsx new file mode 100644 index 00000000..415cd428 --- /dev/null +++ b/lib/tbe-last/table/email-documents-dialog.tsx @@ -0,0 +1,334 @@ +// lib/tbe-last/table/dialogs/email-documents-dialog.tsx + +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { + FileText, + X, + Plus, + Mail, + Loader2, + AlertCircle, +} from "lucide-react" +import { toast } from "sonner" +import { sendDocumentsEmail } from "../service" + +interface EmailDocumentsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + selectedDocuments: any[] + sessionDetail: any + onSuccess?: () => void +} + +export function EmailDocumentsDialog({ + open, + onOpenChange, + selectedDocuments, + sessionDetail, + onSuccess +}: EmailDocumentsDialogProps) { + const [recipients, setRecipients] = React.useState<string[]>([]) + const [currentEmail, setCurrentEmail] = React.useState("") + const [ccRecipients, setCcRecipients] = React.useState<string[]>([]) + const [currentCc, setCurrentCc] = React.useState("") + const [comments, setComments] = React.useState("") + const [isLoading, setIsLoading] = React.useState(false) + + // 이메일 유효성 검사 + const validateEmail = (email: string) => { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return re.test(email) + } + + // 수신자 추가 + const handleAddRecipient = () => { + if (currentEmail && validateEmail(currentEmail)) { + if (!recipients.includes(currentEmail)) { + setRecipients([...recipients, currentEmail]) + setCurrentEmail("") + } else { + toast.error("이미 추가된 이메일입니다") + } + } else { + toast.error("올바른 이메일 주소를 입력하세요") + } + } + + // CC 수신자 추가 + const handleAddCc = () => { + if (currentCc && validateEmail(currentCc)) { + if (!ccRecipients.includes(currentCc)) { + setCcRecipients([...ccRecipients, currentCc]) + setCurrentCc("") + } else { + toast.error("이미 추가된 이메일입니다") + } + } else { + toast.error("올바른 이메일 주소를 입력하세요") + } + } + + // 수신자 제거 + const removeRecipient = (email: string) => { + setRecipients(recipients.filter(r => r !== email)) + } + + // CC 수신자 제거 + const removeCc = (email: string) => { + setCcRecipients(ccRecipients.filter(r => r !== email)) + } + + // 이메일 전송 + const handleSendEmail = async () => { + if (recipients.length === 0) { + toast.error("최소 한 명의 수신자를 추가하세요") + return + } + + if (selectedDocuments.length === 0) { + toast.error("선택된 문서가 없습니다") + return + } + + setIsLoading(true) + + try { + const result = await sendDocumentsEmail({ + to: recipients, + cc: ccRecipients.length > 0 ? ccRecipients : undefined, + documents: selectedDocuments.map(doc => ({ + documentId: doc.documentId, + documentReviewId: doc.documentReviewId, + documentName: doc.documentName, + filePath: doc.filePath, + documentType: doc.documentType, + documentSource: doc.documentSource, + reviewStatus: doc.reviewStatus, + })), + comments, + sessionInfo: { + sessionId: sessionDetail?.session?.tbeSessionId, + sessionTitle: sessionDetail?.session?.title, + buyerName: sessionDetail?.session?.buyerName, + vendorName: sessionDetail?.session?.vendorName, + } + }) + + if (result.success) { + toast.success("이메일이 성공적으로 전송되었습니다") + onSuccess?.() + onOpenChange(false) + + // 초기화 + setRecipients([]) + setCcRecipients([]) + setComments("") + setCurrentEmail("") + setCurrentCc("") + } else { + throw new Error(result.error || "이메일 전송 실패") + } + } catch (error) { + console.error("Email send error:", error) + toast.error(error instanceof Error ? error.message : "이메일 전송 중 오류가 발생했습니다") + } finally { + setIsLoading(false) + } + } + + // 파일 크기 포맷 + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i] + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-2xl"> + <DialogHeader> + <DialogTitle>Send Documents via Email</DialogTitle> + <DialogDescription> + 선택한 {selectedDocuments.length}개의 문서를 이메일로 전송합니다 + </DialogDescription> + </DialogHeader> + + <div className="grid gap-4 py-4"> + {/* 수신자 입력 */} + <div className="grid gap-2"> + <Label htmlFor="recipients">수신자 (To) *</Label> + <div className="flex gap-2"> + <Input + id="recipients" + type="email" + placeholder="이메일 주소 입력" + value={currentEmail} + onChange={(e) => setCurrentEmail(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleAddRecipient() + } + }} + /> + <Button + type="button" + size="sm" + onClick={handleAddRecipient} + variant="outline" + > + <Plus className="h-4 w-4" /> + </Button> + </div> + <div className="flex flex-wrap gap-2 mt-2"> + {recipients.map((email) => ( + <Badge key={email} variant="secondary" className="gap-1"> + {email} + <X + className="h-3 w-3 cursor-pointer hover:text-destructive" + onClick={() => removeRecipient(email)} + /> + </Badge> + ))} + </div> + </div> + + {/* CC 입력 */} + <div className="grid gap-2"> + <Label htmlFor="cc">참조 (CC)</Label> + <div className="flex gap-2"> + <Input + id="cc" + type="email" + placeholder="이메일 주소 입력 (선택사항)" + value={currentCc} + onChange={(e) => setCurrentCc(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleAddCc() + } + }} + /> + <Button + type="button" + size="sm" + onClick={handleAddCc} + variant="outline" + > + <Plus className="h-4 w-4" /> + </Button> + </div> + <div className="flex flex-wrap gap-2 mt-2"> + {ccRecipients.map((email) => ( + <Badge key={email} variant="secondary" className="gap-1"> + {email} + <X + className="h-3 w-3 cursor-pointer hover:text-destructive" + onClick={() => removeCc(email)} + /> + </Badge> + ))} + </div> + </div> + + {/* 코멘트 입력 */} + <div className="grid gap-2"> + <Label htmlFor="comments">메시지</Label> + <Textarea + id="comments" + placeholder="추가 메시지를 입력하세요 (선택사항)" + value={comments} + onChange={(e) => setComments(e.target.value)} + rows={4} + /> + </div> + + {/* 첨부 파일 목록 */} + <div className="grid gap-2"> + <Label>첨부 파일 ({selectedDocuments.length}개)</Label> + <ScrollArea className="h-[200px] w-full rounded-md border p-4"> + <div className="space-y-2"> + {selectedDocuments.map((doc, index) => ( + <div key={doc.documentReviewId} className="flex items-center gap-2 p-2 rounded-md bg-muted/50"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <div className="flex-1 min-w-0"> + <p className="text-sm font-medium truncate">{doc.documentName}</p> + <div className="flex items-center gap-2 text-xs text-muted-foreground"> + <span>{doc.documentType}</span> + <span>•</span> + <Badge variant={doc.documentSource === "buyer" ? "default" : "secondary"} className="text-xs"> + {doc.documentSource} + </Badge> + {doc.reviewStatus && ( + <> + <span>•</span> + <span>{doc.reviewStatus}</span> + </> + )} + </div> + </div> + </div> + ))} + </div> + </ScrollArea> + </div> + + {/* 경고 메시지 */} + {selectedDocuments.some(doc => doc.reviewStatus === "반려") && ( + <div className="flex items-start gap-2 p-3 rounded-md bg-destructive/10 text-destructive"> + <AlertCircle className="h-4 w-4 mt-0.5" /> + <p className="text-sm"> + 반려된 문서가 포함되어 있습니다. 계속 진행하시겠습니까? + </p> + </div> + )} + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + onClick={handleSendEmail} + disabled={isLoading || recipients.length === 0} + > + {isLoading ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + 전송 중... + </> + ) : ( + <> + <Mail className="mr-2 h-4 w-4" /> + 이메일 전송 + </> + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
