diff options
Diffstat (limited to 'lib/tech-vendors/table/invite-tech-vendor-dialog.tsx')
| -rw-r--r-- | lib/tech-vendors/table/invite-tech-vendor-dialog.tsx | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/lib/tech-vendors/table/invite-tech-vendor-dialog.tsx b/lib/tech-vendors/table/invite-tech-vendor-dialog.tsx new file mode 100644 index 00000000..823b2f4d --- /dev/null +++ b/lib/tech-vendors/table/invite-tech-vendor-dialog.tsx @@ -0,0 +1,184 @@ +"use client"
+
+import * as React from "react"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import { Badge } from "@/components/ui/badge"
+import { Mail, Send, Loader2, UserCheck } from "lucide-react"
+import { toast } from "sonner"
+import { TechVendor } from "@/db/schema/techVendors"
+import { inviteTechVendor } from "../service"
+
+// 더 이상 폼 스키마 불필요
+
+interface InviteTechVendorDialogProps {
+ vendors: TechVendor[]
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ showTrigger?: boolean
+ onSuccess?: () => void
+}
+
+export function InviteTechVendorDialog({
+ vendors,
+ open,
+ onOpenChange,
+ showTrigger = true,
+ onSuccess,
+}: InviteTechVendorDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+
+ const onSubmit = async () => {
+ if (vendors.length === 0) {
+ toast.error("초대할 벤더를 선택해주세요.")
+ return
+ }
+
+ setIsLoading(true)
+ try {
+ let successCount = 0
+ let errorCount = 0
+
+ for (const vendor of vendors) {
+ try {
+ const result = await inviteTechVendor({
+ vendorId: vendor.id,
+ subject: "기술영업 협력업체 등록 초대",
+ message: `안녕하세요.
+
+저희 EVCP 플랫폼에서 기술영업 협력업체로 등록하여 다양한 프로젝트에 참여하실 수 있는 기회를 제공하고 있습니다.
+
+아래 링크를 통해 업체 정보를 등록하시기 바랍니다.
+
+감사합니다.`,
+ recipientEmail: vendor.email || "",
+ })
+
+ if (result.success) {
+ successCount++
+ } else {
+ errorCount++
+ console.error(`벤더 ${vendor.vendorName} 초대 실패:`, result.error)
+ }
+ } catch (error) {
+ errorCount++
+ console.error(`벤더 ${vendor.vendorName} 초대 실패:`, error)
+ }
+ }
+
+ if (successCount > 0) {
+ toast.success(`${successCount}개 업체에 초대 메일이 성공적으로 발송되었습니다.`)
+ onOpenChange?.(false)
+ onSuccess?.()
+ }
+
+ if (errorCount > 0) {
+ toast.error(`${errorCount}개 업체 초대 메일 발송에 실패했습니다.`)
+ }
+ } catch (error) {
+ console.error("초대 메일 발송 실패:", error)
+ toast.error("초대 메일 발송 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const getStatusBadge = (status: string) => {
+ const statusConfig = {
+ ACTIVE: { variant: "default" as const, text: "활성 상태", className: "bg-emerald-100 text-emerald-800" },
+ INACTIVE: { variant: "secondary" as const, text: "비활성 상태", className: "bg-gray-100 text-gray-800" },
+ PENDING_INVITE: { variant: "outline" as const, text: "초대 대기", className: "bg-blue-100 text-blue-800" },
+ INVITED: { variant: "outline" as const, text: "초대 완료", className: "bg-green-100 text-green-800" },
+ QUOTE_COMPARISON: { variant: "outline" as const, text: "견적 비교", className: "bg-purple-100 text-purple-800" },
+ BLACKLISTED: { variant: "destructive" as const, text: "거래 금지", className: "bg-red-100 text-red-800" },
+ }
+
+ const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.INACTIVE
+ return <Badge variant={config.variant} className={config.className}>{config.text}</Badge>
+ }
+
+ if (vendors.length === 0) {
+ return null
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ {showTrigger && (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm" className="gap-2">
+ <Mail className="h-4 w-4" />
+ 초대 메일 발송 ({vendors.length})
+ </Button>
+ </DialogTrigger>
+ )}
+ <DialogContent className="max-w-2xl">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <UserCheck className="h-5 w-5" />
+ 기술영업 벤더 초대
+ </DialogTitle>
+ <DialogDescription>
+ 선택한 {vendors.length}개 벤더에게 등록 초대 메일을 발송합니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-6">
+ {/* 벤더 정보 */}
+ <div className="rounded-lg border bg-muted/50 p-4">
+ <h3 className="font-semibold mb-2">초대 대상 벤더 ({vendors.length}개)</h3>
+ <div className="space-y-2 max-h-60 overflow-y-auto">
+ {vendors.map((vendor) => (
+ <div key={vendor.id} className="flex items-center justify-between p-2 border rounded bg-background">
+ <div className="flex-1">
+ <div className="font-medium">{vendor.vendorName}</div>
+ <div className="text-sm text-muted-foreground">
+ {vendor.email || "이메일 없음"}
+ </div>
+ </div>
+ <div className="flex items-center gap-2">
+ {getStatusBadge(vendor.status)}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* 초대 확인 */}
+ <div className="space-y-4">
+
+ <div className="flex justify-end gap-2">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange?.(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button onClick={onSubmit} disabled={isLoading}>
+ {isLoading ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ 발송 중...
+ </>
+ ) : (
+ <>
+ <Send className="mr-2 h-4 w-4" />
+ 초대 메일 발송
+ </>
+ )}
+ </Button>
+ </div>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+}
\ No newline at end of file |
