diff options
Diffstat (limited to 'lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx')
| -rw-r--r-- | lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx | 395 |
1 files changed, 0 insertions, 395 deletions
diff --git a/lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx b/lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx deleted file mode 100644 index baf4a583..00000000 --- a/lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx +++ /dev/null @@ -1,395 +0,0 @@ -"use client" - -import * as React from "react" -import { SelectTrigger } from "@radix-ui/react-select" -import { type Table } from "@tanstack/react-table" -import { - ArrowUp, - CheckCircle2, - Download, - Loader, - Trash2, - X, - Mail, -} from "lucide-react" -import { toast } from "sonner" - -import { exportTableToExcel } from "@/lib/export" -import { Button } from "@/components/ui/button" -import { Portal } from "@/components/ui/portal" -import { - Select, - SelectContent, - SelectGroup, - SelectItem, -} from "@/components/ui/select" -import { Separator } from "@/components/ui/separator" -import { - Tooltip, - TooltipContent, - 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 { VendorCandidatesWithVendorInfo, vendorCandidates } from "@/db/schema/vendors" -import { - bulkUpdateVendorCandidateStatus, - removeCandidates, -} from "../service" - -/** - * 테이블 상단/하단에 고정되는 Floating Bar - * 상태 일괄 변경, 초대, 삭제, Export 등을 수행 - */ -interface CandidatesTableFloatingBarProps { - table: Table<VendorCandidatesWithVendorInfo> -} - -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) - - // 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 (ActionConfirmDialog) 제어 - const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false) - const [confirmProps, setConfirmProps] = React.useState<{ - title: string - description?: string - onConfirm: () => Promise<void> | void - }>({ - title: "", - description: "", - onConfirm: () => {}, - }) - - /** - * 1) 삭제 버튼 클릭 시 Confirm Dialog 열기 - */ - function handleDeleteConfirm() { - setAction("delete") - - setConfirmProps({ - title: `Delete ${rows.length} candidate${ - rows.length > 1 ? "s" : "" - }?`, - description: "This action cannot be undone.", - onConfirm: async () => { - startTransition(async () => { - 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("Candidates deleted successfully") - table.toggleAllRowsSelected(false) - setConfirmDialogOpen(false) - }) - }, - }) - setConfirmDialogOpen(true) - } - - /** - * 2) 선택된 후보들의 상태 일괄 업데이트 - */ - function handleSelectStatus(newStatus: VendorCandidatesWithVendorInfo["status"]) { - setAction("update-status") - - setConfirmProps({ - 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) 초대하기 (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.", - 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 - } - toast.success("Invitation emails sent successfully") - table.toggleAllRowsSelected(false) - setConfirmDialogOpen(false) - }) - }, - }) - setConfirmDialogOpen(true) - } - - return ( - <> - {/* 선택된 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> - - <Separator orientation="vertical" className="hidden h-5 sm:block" /> - - {/* 우측 액션들: 초대, 상태변경, 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> - - {/* 상태 업데이트 (Select) */} - <Select - onValueChange={(value: VendorCandidatesWithVendorInfo["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> - - {/* 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> - - {/* 공용 Confirm Dialog */} - <ActionConfirmDialog - open={confirmDialogOpen} - onOpenChange={setConfirmDialogOpen} - title={confirmProps.title} - description={confirmProps.description} - onConfirm={confirmProps.onConfirm} - isLoading={ - isPending && - (action === "delete" || action === "update-status" || action === "invite") - } - confirmLabel={ - action === "delete" - ? "Delete" - : action === "update-status" - ? "Update" - : action === "invite" - ? "Invite" - : "Confirm" - } - confirmVariant={action === "delete" ? "destructive" : "default"} - /> - </> - ) -} |
