summaryrefslogtreecommitdiff
path: root/lib/rfqs/tbe-table/invite-vendors-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfqs/tbe-table/invite-vendors-dialog.tsx')
-rw-r--r--lib/rfqs/tbe-table/invite-vendors-dialog.tsx203
1 files changed, 203 insertions, 0 deletions
diff --git a/lib/rfqs/tbe-table/invite-vendors-dialog.tsx b/lib/rfqs/tbe-table/invite-vendors-dialog.tsx
new file mode 100644
index 00000000..e38e0ede
--- /dev/null
+++ b/lib/rfqs/tbe-table/invite-vendors-dialog.tsx
@@ -0,0 +1,203 @@
+"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"
+
+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))
+ 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="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" />
+ Invite ({vendors.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Are you absolutely sure?</DialogTitle>
+ <DialogDescription>
+ This action cannot be undone. This will permanently invite{" "}
+ <span className="font-medium">{vendors.length}</span>
+ {vendors.length === 1 ? " vendor" : " vendors"}. 파일 첨부가 필수이므로 파일을 첨부해야지 버튼이 활성화됩니다.
+ </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>
+ <DrawerTitle>Are you absolutely sure?</DrawerTitle>
+ <DrawerDescription>
+ This action cannot be undone. This will permanently invite{" "}
+ <span className="font-medium">{vendors.length}</span>
+ {vendors.length === 1 ? " vendor" : " vendors"}.
+ </DrawerDescription>
+ </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