diff options
Diffstat (limited to 'lib/tech-vendor-candidates/table/invite-candidates-dialog.tsx')
| -rw-r--r-- | lib/tech-vendor-candidates/table/invite-candidates-dialog.tsx | 230 |
1 files changed, 0 insertions, 230 deletions
diff --git a/lib/tech-vendor-candidates/table/invite-candidates-dialog.tsx b/lib/tech-vendor-candidates/table/invite-candidates-dialog.tsx deleted file mode 100644 index 570cf96a..00000000 --- a/lib/tech-vendor-candidates/table/invite-candidates-dialog.tsx +++ /dev/null @@ -1,230 +0,0 @@ -"use client" - -import * as React from "react" -import { type Row } from "@tanstack/react-table" -import { Loader, Mail, AlertCircle, XCircle } 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 { - Alert, - AlertTitle, - AlertDescription -} from "@/components/ui/alert" - -import { VendorCandidates } from "@/db/schema/vendors" -import { bulkUpdateVendorCandidateStatus } from "../service" -import { useSession } from "next-auth/react" // next-auth 세션 훅 - -interface InviteCandidatesDialogProps - extends React.ComponentPropsWithoutRef<typeof Dialog> { - candidates: Row<VendorCandidates>["original"][] - showTrigger?: boolean - onSuccess?: () => void -} - -export function InviteCandidatesDialog({ - candidates, - showTrigger = true, - onSuccess, - ...props -}: InviteCandidatesDialogProps) { - const [isInvitePending, startInviteTransition] = React.useTransition() - const isDesktop = useMediaQuery("(min-width: 640px)") - const { data: session, status } = useSession() - - // 후보자를 상태별로 분류 - const discardedCandidates = candidates.filter(candidate => candidate.status === "DISCARDED") - const nonDiscardedCandidates = candidates.filter(candidate => candidate.status !== "DISCARDED") - - // 이메일 유무에 따라 초대 가능한 후보자 분류 (DISCARDED가 아닌 후보자 중에서) - const candidatesWithEmail = nonDiscardedCandidates.filter(candidate => candidate.contactEmail) - const candidatesWithoutEmail = nonDiscardedCandidates.filter(candidate => !candidate.contactEmail) - - // 각 카테고리 수 - const invitableCount = candidatesWithEmail.length - const hasUninvitableCandidates = candidatesWithoutEmail.length > 0 - const hasDiscardedCandidates = discardedCandidates.length > 0 - - function onInvite() { - startInviteTransition(async () => { - // 이메일이 있고 DISCARDED가 아닌 후보자만 상태 업데이트 - - if (!session?.user?.id) { - toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.") - return - } - - const { error } = await bulkUpdateVendorCandidateStatus({ - ids: candidatesWithEmail.map((candidate) => candidate.id), - status: "INVITED", - }) - - if (error) { - toast.error(error) - return - } - - props.onOpenChange?.(false) - - if (invitableCount === 0) { - toast.warning("No invitation sent - no eligible candidates with email addresses") - } else { - let skipMessage = "" - - if (hasUninvitableCandidates && hasDiscardedCandidates) { - skipMessage = ` ${candidatesWithoutEmail.length} candidates without email and ${discardedCandidates.length} discarded candidates were skipped.` - } else if (hasUninvitableCandidates) { - skipMessage = ` ${candidatesWithoutEmail.length} candidates without email were skipped.` - } else if (hasDiscardedCandidates) { - skipMessage = ` ${discardedCandidates.length} discarded candidates were skipped.` - } - - toast.success(`Invitation emails sent to ${invitableCount} candidates.${skipMessage}`) - } - - onSuccess?.() - }) - } - - // 초대 버튼 비활성화 조건 - const disableInviteButton = isInvitePending || invitableCount === 0 - - const DialogComponent = ( - <> - <div className="space-y-4"> - {/* 이메일 없는 후보자 알림 */} - {hasUninvitableCandidates && ( - <Alert> - <AlertCircle className="h-4 w-4" /> - <AlertTitle>Missing Email Addresses</AlertTitle> - <AlertDescription> - {candidatesWithoutEmail.length} candidate{candidatesWithoutEmail.length > 1 ? 's' : ''} {candidatesWithoutEmail.length > 1 ? 'don't' : 'doesn't'} have email addresses and won't receive invitations. - </AlertDescription> - </Alert> - )} - - {/* 폐기된 후보자 알림 */} - {hasDiscardedCandidates && ( - <Alert variant="destructive"> - <XCircle className="h-4 w-4" /> - <AlertTitle>Discarded Candidates</AlertTitle> - <AlertDescription> - {discardedCandidates.length} candidate{discardedCandidates.length > 1 ? 's have' : ' has'} been discarded and won't receive invitations. - </AlertDescription> - </Alert> - )} - - <DialogDescription> - {invitableCount > 0 ? ( - <> - This will send invitation emails to{" "} - <span className="font-medium">{invitableCount}</span> - {invitableCount === 1 ? " candidate" : " candidates"} and change their status to INVITED. - </> - ) : ( - <> - No candidates can be invited because none of the selected candidates have valid email addresses or they have been discarded. - </> - )} - </DialogDescription> - </div> - </> - ) - - if (isDesktop) { - return ( - <Dialog {...props}> - {showTrigger ? ( - <DialogTrigger asChild> - <Button variant="outline" size="sm" className="gap-2"> - <Mail className="size-4" aria-hidden="true" /> - Invite ({candidates.length}) - </Button> - </DialogTrigger> - ) : null} - <DialogContent> - <DialogHeader> - <DialogTitle>Send invitations?</DialogTitle> - </DialogHeader> - {DialogComponent} - <DialogFooter className="gap-2 sm:space-x-0"> - <DialogClose asChild> - <Button variant="outline">Cancel</Button> - </DialogClose> - <Button - aria-label="Invite selected vendors" - variant="default" - onClick={onInvite} - disabled={disableInviteButton} - > - {isInvitePending && ( - <Loader - className="mr-2 size-4 animate-spin" - aria-hidden="true" - /> - )} - Send Invitations - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ) - } - - return ( - <Drawer {...props}> - {showTrigger ? ( - <DrawerTrigger asChild> - <Button variant="outline" size="sm" className="gap-2"> - <Mail className="size-4" aria-hidden="true" /> - Invite ({candidates.length}) - </Button> - </DrawerTrigger> - ) : null} - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Send invitations?</DrawerTitle> - </DrawerHeader> - {DialogComponent} - <DrawerFooter className="gap-2 sm:space-x-0"> - <DrawerClose asChild> - <Button variant="outline">Cancel</Button> - </DrawerClose> - <Button - aria-label="Invite selected vendors" - variant="default" - onClick={onInvite} - disabled={disableInviteButton} - > - {isInvitePending && ( - <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> - )} - Send Invitations - </Button> - </DrawerFooter> - </DrawerContent> - </Drawer> - ) -}
\ No newline at end of file |
