diff options
Diffstat (limited to 'lib/vendor-candidates/table/invite-candidates-dialog.tsx')
| -rw-r--r-- | lib/vendor-candidates/table/invite-candidates-dialog.tsx | 112 |
1 files changed, 97 insertions, 15 deletions
diff --git a/lib/vendor-candidates/table/invite-candidates-dialog.tsx b/lib/vendor-candidates/table/invite-candidates-dialog.tsx index 366b6f45..45cf13c3 100644 --- a/lib/vendor-candidates/table/invite-candidates-dialog.tsx +++ b/lib/vendor-candidates/table/invite-candidates-dialog.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { type Row } from "@tanstack/react-table" -import { Loader, Mail } from "lucide-react" +import { Loader, Mail, AlertCircle, XCircle } from "lucide-react" import { toast } from "sonner" import { useMediaQuery } from "@/hooks/use-media-query" @@ -27,9 +27,15 @@ import { 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> { @@ -46,12 +52,35 @@ export function InviteCandidatesDialog({ }: 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 userId = Number(session.user.id) const { error } = await bulkUpdateVendorCandidateStatus({ - ids: candidates.map((candidate) => candidate.id), + ids: candidatesWithEmail.map((candidate) => candidate.id), status: "INVITED", + userId, + comment: "Bulk invite action", }) if (error) { @@ -60,11 +89,72 @@ export function InviteCandidatesDialog({ } props.onOpenChange?.(false) - toast.success("Invitation emails sent") + + 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}> @@ -79,12 +169,8 @@ export function InviteCandidatesDialog({ <DialogContent> <DialogHeader> <DialogTitle>Send invitations?</DialogTitle> - <DialogDescription> - This will send invitation emails to{" "} - <span className="font-medium">{candidates.length}</span> - {candidates.length === 1 ? " candidate" : " candidates"} and change their status to INVITED. - </DialogDescription> </DialogHeader> + {DialogComponent} <DialogFooter className="gap-2 sm:space-x-0"> <DialogClose asChild> <Button variant="outline">Cancel</Button> @@ -93,7 +179,7 @@ export function InviteCandidatesDialog({ aria-label="Invite selected vendors" variant="default" onClick={onInvite} - disabled={isInvitePending} + disabled={disableInviteButton} > {isInvitePending && ( <Loader @@ -122,12 +208,8 @@ export function InviteCandidatesDialog({ <DrawerContent> <DrawerHeader> <DrawerTitle>Send invitations?</DrawerTitle> - <DrawerDescription> - This will send invitation emails to{" "} - <span className="font-medium">{candidates.length}</span> - {candidates.length === 1 ? " candidate" : " candidates"} and change their status to INVITED. - </DrawerDescription> </DrawerHeader> + {DialogComponent} <DrawerFooter className="gap-2 sm:space-x-0"> <DrawerClose asChild> <Button variant="outline">Cancel</Button> @@ -136,7 +218,7 @@ export function InviteCandidatesDialog({ aria-label="Invite selected vendors" variant="default" onClick={onInvite} - disabled={isInvitePending} + disabled={disableInviteButton} > {isInvitePending && ( <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> |
