From 36dd60ca6fce7712b35e6d7c1b9602710f442ada Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Wed, 28 May 2025 12:26:28 +0000
Subject: (최겸) 기술영업 해양 rfq 개발v1
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/rfqs-tech/tbe-table/invite-vendors-dialog.tsx | 227 ++++++++++++++++++++++
 1 file changed, 227 insertions(+)
 create mode 100644 lib/rfqs-tech/tbe-table/invite-vendors-dialog.tsx

(limited to 'lib/rfqs-tech/tbe-table/invite-vendors-dialog.tsx')

diff --git a/lib/rfqs-tech/tbe-table/invite-vendors-dialog.tsx b/lib/rfqs-tech/tbe-table/invite-vendors-dialog.tsx
new file mode 100644
index 00000000..f7aa957c
--- /dev/null
+++ b/lib/rfqs-tech/tbe-table/invite-vendors-dialog.tsx
@@ -0,0 +1,227 @@
+"use client"
+
+import * as React from "react"
+import { type Row } from "@tanstack/react-table"
+import { Loader, Send } from "lucide-react"
+import { toast } from "sonner"
+
+import { useMediaQuery } from "@/hooks/use-media-query"
+import { Button } from "@/components/ui/button"
+import {
+  Dialog,
+  DialogClose,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+  Drawer,
+  DrawerClose,
+  DrawerContent,
+  DrawerDescription,
+  DrawerFooter,
+  DrawerHeader,
+  DrawerTitle,
+  DrawerTrigger,
+} from "@/components/ui/drawer"
+
+import { Input } from "@/components/ui/input"
+
+import { VendorWithTbeFields } from "@/config/vendorTbeColumnsConfig"
+import { inviteTbeVendorsAction } from "../service"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Badge } from "@/components/ui/badge"
+import { Label } from "@/components/ui/label"
+
+interface InviteVendorsDialogProps
+  extends React.ComponentPropsWithoutRef<typeof Dialog> {
+  vendors: Row<VendorWithTbeFields>["original"][]
+  rfqId: number
+  showTrigger?: boolean
+  onSuccess?: () => void
+}
+
+export function InviteVendorsDialog({
+  vendors,
+  rfqId,
+  showTrigger = true,
+  onSuccess,
+  ...props
+}: InviteVendorsDialogProps) {
+  const [isInvitePending, startInviteTransition] = React.useTransition()
+
+
+  // multiple 파일을 받을 state
+  const [files, setFiles] = React.useState<FileList | null>(null)
+
+  // 미디어쿼리 (desktop 여부)
+  const isDesktop = useMediaQuery("(min-width: 640px)")
+
+  function onInvite() {
+    startInviteTransition(async () => {
+      // 파일이 선택되지 않았다면 에러
+      if (!files || files.length === 0) {
+        toast.error("Please attach TBE files before inviting.")
+        return
+      }
+
+      // FormData 생성
+      const formData = new FormData()
+      formData.append("rfqId", String(rfqId))
+      
+      console.log("Invite Debug:", {
+        rfqId,
+        vendors,
+        files
+      })
+
+      vendors.forEach((vendor) => {
+        formData.append("vendorIds[]", String(vendor.id))
+      })
+
+      // multiple 파일
+      for (let i = 0; i < files.length; i++) {
+        formData.append("tbeFiles", files[i]) // key는 동일하게 "tbeFiles"
+      }
+
+      // 서버 액션 호출
+      const { error } = await inviteTbeVendorsAction(formData)
+
+      if (error) {
+        toast.error(error)
+        return
+      }
+
+      // 성공
+      props.onOpenChange?.(false)
+      toast.success("Vendors invited with TBE!")
+      onSuccess?.()
+    })
+  }
+
+  // 파일 선택 UI
+  const fileInput = (
+<>
+    <div className="space-y-2">
+    <Label>선택된 협력업체 ({vendors.length})</Label>
+    <ScrollArea className="h-20 border rounded-md p-2">
+      <div className="flex flex-wrap gap-2">
+        {vendors.map((vendor, index) => (
+          <Badge key={index} variant="secondary" className="py-1">
+            {vendor.vendorName || `협력업체 #${vendor.vendorCode}`}
+          </Badge>
+        ))}
+      </div>
+    </ScrollArea>
+    <p className="text-[0.8rem] font-medium text-muted-foreground">
+      선택된 모든 협력업체의 등록된 연락처에게 TBE 평가 알림이 전송됩니다.
+    </p>
+  </div>
+
+    <div className="mb-4">
+      <label className="mb-2 block font-medium">TBE Sheets</label>
+      <Input
+        type="file"
+        multiple
+        onChange={(e) => {
+          setFiles(e.target.files)
+        }}
+      />
+    </div>
+    </>
+  )
+
+  // Desktop Dialog
+  if (isDesktop) {
+    return (
+      <Dialog {...props}>
+        {showTrigger ? (
+          <DialogTrigger asChild>
+            <Button variant="outline" size="sm">
+              <Send className="mr-2 size-4" aria-hidden="true" />
+              TBE 평가 생성 ({vendors.length})
+            </Button>
+          </DialogTrigger>
+        ) : null}
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>TBE 평가 시트 전송</DialogTitle>
+            <DialogDescription>
+              선택한 {vendors.length}개 협력업체에 대한 기술 평가 시트와 알림을 전송합니다. 파일 첨부가 필수이므로 파일을 첨부해야지 버튼이 활성화됩니다.
+            </DialogDescription>
+          </DialogHeader>
+
+          {/* 파일 첨부 */}
+          {fileInput}
+
+          <DialogFooter className="gap-2 sm:space-x-0">
+            <DialogClose asChild>
+              <Button variant="outline">Cancel</Button>
+            </DialogClose>
+            <Button
+              aria-label="Invite selected rows"
+              variant="destructive"
+              onClick={onInvite}
+              // 파일이 없거나 초대 진행중이면 비활성화
+              disabled={isInvitePending || !files || files.length === 0}
+            >
+              {isInvitePending && (
+                <Loader
+                  className="mr-2 size-4 animate-spin"
+                  aria-hidden="true"
+                />
+              )}
+              Invite
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    )
+  }
+
+  // Mobile Drawer
+  return (
+    <Drawer {...props}>
+      {showTrigger ? (
+        <DrawerTrigger asChild>
+          <Button variant="outline" size="sm">
+            <Send className="mr-2 size-4" aria-hidden="true" />
+            Invite ({vendors.length})
+          </Button>
+        </DrawerTrigger>
+      ) : null}
+      <DrawerContent>
+        <DrawerHeader>
+          <DialogTitle>TBE 평가 시트 전송</DialogTitle>
+          <DialogDescription>
+            선택한 {vendors.length}개 협력업체에 대한 기술 평가 시트와 알림을 전송합니다. 파일 첨부가 필수이므로 파일을 첨부해야지 버튼이 활성화됩니다.
+          </DialogDescription>
+        </DrawerHeader>
+
+        {/* 파일 첨부 */}
+        {fileInput}
+
+        <DrawerFooter className="gap-2 sm:space-x-0">
+          <DrawerClose asChild>
+            <Button variant="outline">Cancel</Button>
+          </DrawerClose>
+          <Button
+            aria-label="Invite selected rows"
+            variant="destructive"
+            onClick={onInvite}
+            // 파일이 없거나 초대 진행중이면 비활성화
+            disabled={isInvitePending || !files || files.length === 0}
+          >
+            {isInvitePending && (
+              <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+            )}
+            Invite
+          </Button>
+        </DrawerFooter>
+      </DrawerContent>
+    </Drawer>
+  )
+}
\ No newline at end of file
-- 
cgit v1.2.3

