summaryrefslogtreecommitdiff
path: root/lib/vendor-candidates/table/candidates-table-floating-bar.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-candidates/table/candidates-table-floating-bar.tsx')
-rw-r--r--lib/vendor-candidates/table/candidates-table-floating-bar.tsx416
1 files changed, 237 insertions, 179 deletions
diff --git a/lib/vendor-candidates/table/candidates-table-floating-bar.tsx b/lib/vendor-candidates/table/candidates-table-floating-bar.tsx
index 2696292d..baf4a583 100644
--- a/lib/vendor-candidates/table/candidates-table-floating-bar.tsx
+++ b/lib/vendor-candidates/table/candidates-table-floating-bar.tsx
@@ -30,37 +30,48 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip"
import { Kbd } from "@/components/kbd"
+import { useSession } from "next-auth/react" // next-auth 세션 훅
import { ActionConfirmDialog } from "@/components/ui/action-dialog"
-import { vendorCandidates, VendorCandidates } from "@/db/schema/vendors"
-import { bulkUpdateVendorCandidateStatus, removeCandidates, updateVendorCandidate } from "../service"
+import { VendorCandidatesWithVendorInfo, vendorCandidates } from "@/db/schema/vendors"
+import {
+ bulkUpdateVendorCandidateStatus,
+ removeCandidates,
+} from "../service"
+/**
+ * 테이블 상단/하단에 고정되는 Floating Bar
+ * 상태 일괄 변경, 초대, 삭제, Export 등을 수행
+ */
interface CandidatesTableFloatingBarProps {
- table: Table<VendorCandidates>
+ table: Table<VendorCandidatesWithVendorInfo>
}
-export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloatingBarProps) {
+export function VendorCandidateTableFloatingBar({
+ table,
+}: CandidatesTableFloatingBarProps) {
const rows = table.getFilteredSelectedRowModel().rows
+ const { data: session, status } = useSession()
+ // React 18의 startTransition 사용 (isPending으로 트랜지션 상태 확인)
const [isPending, startTransition] = React.useTransition()
const [action, setAction] = React.useState<
"update-status" | "export" | "delete" | "invite"
>()
const [popoverOpen, setPopoverOpen] = React.useState(false)
- // Clear selection on Escape key press
+ // ESC 키로 selection 해제
React.useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
table.toggleAllRowsSelected(false)
}
}
-
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [table])
- // 공용 confirm dialog state
+ // 공용 Confirm Dialog (ActionConfirmDialog) 제어
const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false)
const [confirmProps, setConfirmProps] = React.useState<{
title: string
@@ -69,25 +80,41 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati
}>({
title: "",
description: "",
- onConfirm: () => { },
+ onConfirm: () => {},
})
- // 1) "삭제" Confirm 열기
+ /**
+ * 1) 삭제 버튼 클릭 시 Confirm Dialog 열기
+ */
function handleDeleteConfirm() {
setAction("delete")
+
setConfirmProps({
- title: `Delete ${rows.length} user${rows.length > 1 ? "s" : ""}?`,
+ title: `Delete ${rows.length} candidate${
+ rows.length > 1 ? "s" : ""
+ }?`,
description: "This action cannot be undone.",
onConfirm: async () => {
startTransition(async () => {
- const { error } = await removeCandidates({
- ids: rows.map((row) => row.original.id),
- })
+ if (!session?.user?.id) {
+ toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.")
+ return
+ }
+ const userId = Number(session.user.id)
+
+ // removeCandidates 호출 시 userId를 넘긴다고 가정
+ const { error } = await removeCandidates(
+ {
+ ids: rows.map((row) => row.original.id),
+ },
+ userId
+ )
+
if (error) {
toast.error(error)
return
}
- toast.success("Users deleted")
+ toast.success("Candidates deleted successfully")
table.toggleAllRowsSelected(false)
setConfirmDialogOpen(false)
})
@@ -96,43 +123,71 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati
setConfirmDialogOpen(true)
}
- // 2) 상태 업데이트
- function handleSelectStatus(newStatus: VendorCandidates["status"]) {
+ /**
+ * 2) 선택된 후보들의 상태 일괄 업데이트
+ */
+ function handleSelectStatus(newStatus: VendorCandidatesWithVendorInfo["status"]) {
setAction("update-status")
setConfirmProps({
- title: `Update ${rows.length} candidate${rows.length > 1 ? "s" : ""} with status: ${newStatus}?`,
+ title: `Update ${rows.length} candidate${
+ rows.length > 1 ? "s" : ""
+ } with status: ${newStatus}?`,
description: "This action will override their current status.",
onConfirm: async () => {
startTransition(async () => {
+ if (!session?.user?.id) {
+ toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.")
+ return
+ }
+ const userId = Number(session.user.id)
+
const { error } = await bulkUpdateVendorCandidateStatus({
ids: rows.map((row) => row.original.id),
status: newStatus,
+ userId,
+ comment: `Bulk status update to ${newStatus}`,
})
+
if (error) {
toast.error(error)
return
}
toast.success("Candidates updated")
setConfirmDialogOpen(false)
+ table.toggleAllRowsSelected(false)
})
},
})
setConfirmDialogOpen(true)
}
- // 3) 초대하기 (INVITED 상태로 바꾸고 이메일 전송)
+ /**
+ * 3) 초대하기 (status = "INVITED" + 이메일 발송)
+ */
function handleInvite() {
setAction("invite")
setConfirmProps({
- title: `Invite ${rows.length} candidate${rows.length > 1 ? "s" : ""}?`,
- description: "This will change their status to INVITED and send invitation emails.",
+ title: `Invite ${rows.length} candidate${
+ rows.length > 1 ? "s" : ""
+ }?`,
+ description:
+ "This will change their status to INVITED and send invitation emails.",
onConfirm: async () => {
startTransition(async () => {
+ if (!session?.user?.id) {
+ toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.")
+ return
+ }
+ const userId = Number(session.user.id)
+
const { error } = await bulkUpdateVendorCandidateStatus({
ids: rows.map((row) => row.original.id),
status: "INVITED",
+ userId,
+ comment: "Bulk invite action",
})
+
if (error) {
toast.error(error)
return
@@ -147,166 +202,168 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati
}
return (
- <Portal >
- <div className="fixed inset-x-0 bottom-10 z-50 mx-auto w-fit px-2.5" style={{ bottom: '1.5rem' }}>
- <div className="w-full overflow-x-auto">
- <div className="mx-auto flex w-fit items-center gap-2 rounded-md border bg-background p-2 text-foreground shadow">
- <div className="flex h-7 items-center rounded-md border border-dashed pl-2.5 pr-1">
- <span className="whitespace-nowrap text-xs">
- {rows.length} selected
- </span>
- <Separator orientation="vertical" className="ml-2 mr-1" />
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- size="icon"
- className="size-5 hover:border"
- onClick={() => table.toggleAllRowsSelected(false)}
- >
- <X className="size-3.5 shrink-0" aria-hidden="true" />
- </Button>
- </TooltipTrigger>
- <TooltipContent className="flex items-center border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-zinc-900">
- <p className="mr-2">Clear selection</p>
- <Kbd abbrTitle="Escape" variant="outline">
- Esc
- </Kbd>
- </TooltipContent>
- </Tooltip>
- </div>
- <Separator orientation="vertical" className="hidden h-5 sm:block" />
- <div className="flex items-center gap-1.5">
- {/* 초대하기 버튼 (새로 추가) */}
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="secondary"
- size="sm"
- className="h-7 border"
- onClick={handleInvite}
- disabled={isPending}
- >
- {isPending && action === "invite" ? (
- <Loader
- className="mr-1 size-3.5 animate-spin"
- aria-hidden="true"
- />
- ) : (
- <Mail className="mr-1 size-3.5" aria-hidden="true" />
- )}
- <span>Invite</span>
- </Button>
- </TooltipTrigger>
- <TooltipContent className="border bg-accent font-semibold text-foreground dark:bg-zinc-900">
- <p>Send invitation emails</p>
- </TooltipContent>
- </Tooltip>
+ <>
+ {/* 선택된 row가 있을 때 표시되는 Floating Bar */}
+ <div className="flex justify-center w-full my-4">
+ <div className="flex items-center gap-2 rounded-md border bg-background p-2 text-foreground shadow">
+ {/* 선택된 갯수 표시 + Clear selection 버튼 */}
+ <div className="flex h-7 items-center rounded-md border border-dashed pl-2.5 pr-1">
+ <span className="whitespace-nowrap text-xs">
+ {rows.length} selected
+ </span>
+ <Separator orientation="vertical" className="ml-2 mr-1" />
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="ghost"
+ size="icon"
+ className="size-5 hover:border"
+ onClick={() => table.toggleAllRowsSelected(false)}
+ >
+ <X className="size-3.5 shrink-0" aria-hidden="true" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent className="flex items-center border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-zinc-900">
+ <p className="mr-2">Clear selection</p>
+ <Kbd abbrTitle="Escape" variant="outline">
+ Esc
+ </Kbd>
+ </TooltipContent>
+ </Tooltip>
+ </div>
- <Select
- onValueChange={(value: VendorCandidates["status"]) => {
- handleSelectStatus(value)
- }}
- >
- <Tooltip>
- <SelectTrigger asChild>
- <TooltipTrigger asChild>
- <Button
- variant="secondary"
- size="icon"
- className="size-7 border data-[state=open]:bg-accent data-[state=open]:text-accent-foreground"
- disabled={isPending}
- >
- {isPending && action === "update-status" ? (
- <Loader
- className="size-3.5 animate-spin"
- aria-hidden="true"
- />
- ) : (
- <CheckCircle2
- className="size-3.5"
- aria-hidden="true"
- />
- )}
- </Button>
- </TooltipTrigger>
- </SelectTrigger>
- <TooltipContent className="border bg-accent font-semibold text-foreground dark:bg-zinc-900">
- <p>Update status</p>
- </TooltipContent>
- </Tooltip>
- <SelectContent align="center">
- <SelectGroup>
- {vendorCandidates.status.enumValues.map((status) => (
- <SelectItem
- key={status}
- value={status}
- className="capitalize"
- >
- {status}
- </SelectItem>
- ))}
- </SelectGroup>
- </SelectContent>
- </Select>
+ <Separator orientation="vertical" className="hidden h-5 sm:block" />
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="secondary"
- size="icon"
- className="size-7 border"
- onClick={() => {
- setAction("export")
+ {/* 우측 액션들: 초대, 상태변경, Export, 삭제 */}
+ <div className="flex items-center gap-1.5">
+ {/* 초대하기 */}
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="secondary"
+ size="sm"
+ className="h-7 border"
+ onClick={handleInvite}
+ disabled={isPending}
+ >
+ {isPending && action === "invite" ? (
+ <Loader
+ className="mr-1 size-3.5 animate-spin"
+ aria-hidden="true"
+ />
+ ) : (
+ <Mail className="mr-1 size-3.5" aria-hidden="true" />
+ )}
+ <span>Invite</span>
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent className="border bg-accent font-semibold text-foreground dark:bg-zinc-900">
+ <p>Send invitation emails</p>
+ </TooltipContent>
+ </Tooltip>
- startTransition(() => {
- exportTableToExcel(table, {
- excludeColumns: ["select", "actions"],
- onlySelected: true,
- })
- })
- }}
- disabled={isPending}
- >
- {isPending && action === "export" ? (
- <Loader
- className="size-3.5 animate-spin"
- aria-hidden="true"
- />
- ) : (
- <Download className="size-3.5" aria-hidden="true" />
- )}
- </Button>
- </TooltipTrigger>
- <TooltipContent className="border bg-accent font-semibold text-foreground dark:bg-zinc-900">
- <p>Export candidates</p>
- </TooltipContent>
- </Tooltip>
-
+ {/* 상태 업데이트 (Select) */}
+ <Select
+ onValueChange={(value: VendorCandidatesWithVendorInfo["status"]) => {
+ handleSelectStatus(value)
+ }}
+ >
<Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="secondary"
- size="icon"
- className="size-7 border"
- onClick={handleDeleteConfirm}
- disabled={isPending}
- >
- {isPending && action === "delete" ? (
- <Loader
- className="size-3.5 animate-spin"
- aria-hidden="true"
- />
- ) : (
- <Trash2 className="size-3.5" aria-hidden="true" />
- )}
- </Button>
- </TooltipTrigger>
+ <SelectTrigger asChild>
+ <TooltipTrigger asChild>
+ <Button
+ variant="secondary"
+ size="icon"
+ className="size-7 border data-[state=open]:bg-accent data-[state=open]:text-accent-foreground"
+ disabled={isPending}
+ >
+ {isPending && action === "update-status" ? (
+ <Loader
+ className="size-3.5 animate-spin"
+ aria-hidden="true"
+ />
+ ) : (
+ <CheckCircle2 className="size-3.5" aria-hidden="true" />
+ )}
+ </Button>
+ </TooltipTrigger>
+ </SelectTrigger>
<TooltipContent className="border bg-accent font-semibold text-foreground dark:bg-zinc-900">
- <p>Delete candidates</p>
+ <p>Update status</p>
</TooltipContent>
</Tooltip>
- </div>
+ <SelectContent align="center">
+ <SelectGroup>
+ {vendorCandidates.status.enumValues.map((status) => (
+ <SelectItem
+ key={status}
+ value={status}
+ className="capitalize"
+ >
+ {status}
+ </SelectItem>
+ ))}
+ </SelectGroup>
+ </SelectContent>
+ </Select>
+
+ {/* Export 버튼 */}
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="secondary"
+ size="icon"
+ className="size-7 border"
+ onClick={() => {
+ setAction("export")
+ startTransition(() => {
+ exportTableToExcel(table, {
+ excludeColumns: ["select", "actions"],
+ onlySelected: true,
+ })
+ })
+ }}
+ disabled={isPending}
+ >
+ {isPending && action === "export" ? (
+ <Loader
+ className="size-3.5 animate-spin"
+ aria-hidden="true"
+ />
+ ) : (
+ <Download className="size-3.5" aria-hidden="true" />
+ )}
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent className="border bg-accent font-semibold text-foreground dark:bg-zinc-900">
+ <p>Export candidates</p>
+ </TooltipContent>
+ </Tooltip>
+
+ {/* 삭제 버튼 */}
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="secondary"
+ size="icon"
+ className="size-7 border"
+ onClick={handleDeleteConfirm}
+ disabled={isPending}
+ >
+ {isPending && action === "delete" ? (
+ <Loader
+ className="size-3.5 animate-spin"
+ aria-hidden="true"
+ />
+ ) : (
+ <Trash2 className="size-3.5" aria-hidden="true" />
+ )}
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent className="border bg-accent font-semibold text-foreground dark:bg-zinc-900">
+ <p>Delete candidates</p>
+ </TooltipContent>
+ </Tooltip>
</div>
</div>
</div>
@@ -318,7 +375,10 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati
title={confirmProps.title}
description={confirmProps.description}
onConfirm={confirmProps.onConfirm}
- isLoading={isPending && (action === "delete" || action === "update-status" || action === "invite")}
+ isLoading={
+ isPending &&
+ (action === "delete" || action === "update-status" || action === "invite")
+ }
confirmLabel={
action === "delete"
? "Delete"
@@ -328,10 +388,8 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati
? "Invite"
: "Confirm"
}
- confirmVariant={
- action === "delete" ? "destructive" : "default"
- }
+ confirmVariant={action === "delete" ? "destructive" : "default"}
/>
- </Portal>
+ </>
)
-} \ No newline at end of file
+}