summaryrefslogtreecommitdiff
path: root/lib/vendor-candidates/table/invite-candidates-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-candidates/table/invite-candidates-dialog.tsx')
-rw-r--r--lib/vendor-candidates/table/invite-candidates-dialog.tsx112
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" />