summaryrefslogtreecommitdiff
path: root/lib/admin-users/table/ausers-table-floating-bar.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/admin-users/table/ausers-table-floating-bar.tsx')
-rw-r--r--lib/admin-users/table/ausers-table-floating-bar.tsx389
1 files changed, 389 insertions, 0 deletions
diff --git a/lib/admin-users/table/ausers-table-floating-bar.tsx b/lib/admin-users/table/ausers-table-floating-bar.tsx
new file mode 100644
index 00000000..ae950252
--- /dev/null
+++ b/lib/admin-users/table/ausers-table-floating-bar.tsx
@@ -0,0 +1,389 @@
+"use client"
+
+import * as React from "react"
+import { userRoles, users, UserView, type User } from "@/db/schema/users"
+import { SelectTrigger } from "@radix-ui/react-select"
+import { type Table } from "@tanstack/react-table"
+import {
+ ArrowUp,
+ CheckCircle2,
+ Download,
+ Loader,
+ Trash2,
+ X, Check
+} 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 { modifiUsers, getAllCompanies, removeUsers } from "@/lib//admin-users/service"
+import { type Company } from "@/db/schema/companies"
+import {
+ Popover,
+ PopoverTrigger,
+ PopoverContent,
+} from "@/components/ui/popover"
+import {
+ Command,
+ CommandInput,
+ CommandList,
+ CommandGroup,
+ CommandItem,
+ CommandEmpty,
+} from "@/components/ui/command"
+import { cn } from "@/lib/utils"
+import { MultiSelect } from "@/components/ui/multi-select"
+import { ActionConfirmDialog } from "@/components/ui/action-dialog"
+
+interface AusersTableFloatingBarProps {
+ table: Table<UserView>
+}
+
+
+export function AusersTableFloatingBar({ table }: AusersTableFloatingBarProps) {
+ const rows = table.getFilteredSelectedRowModel().rows
+
+ const [isPending, startTransition] = React.useTransition()
+ const [action, setAction] = React.useState<
+ "update-company" | "update-roles" | "export" | "delete"
+ >()
+ const [companies, setCompanies] = React.useState<Company[]>([]) // 회사 목록
+
+ // Clear selection on Escape key press
+ React.useEffect(() => {
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ table.toggleAllRowsSelected(false)
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [table])
+
+ React.useEffect(() => {
+ // 회사 목록 불러오기 (예시)
+ getAllCompanies().then((res) => {
+ setCompanies(res)
+ })
+ }, [])
+
+ const [popoverOpen, setPopoverOpen] = React.useState(false)
+ const [rolesPopoverOpen, setRolesPopoverOpen] = React.useState(false)
+
+
+ // 공용 confirm dialog state
+ const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false)
+ const [confirmProps, setConfirmProps] = React.useState<{
+ title: string
+ description?: string
+ onConfirm: () => Promise<void> | void
+ }>({
+ title: "",
+ description: "",
+ onConfirm: () => { },
+ })
+
+ // 1) "삭제" Confirm 열기
+ function handleDeleteConfirm() {
+ setAction("delete")
+ setConfirmProps({
+ title: `Delete ${rows.length} user${rows.length > 1 ? "s" : ""}?`,
+ description: "This action cannot be undone.",
+ onConfirm: async () => {
+ startTransition(async () => {
+ const { error } = await removeUsers({
+ ids: rows.map((row) => row.original.user_id),
+ })
+ if (error) {
+ toast.error(error)
+ return
+ }
+ toast.success("Users deleted")
+ table.toggleAllRowsSelected(false)
+ setConfirmDialogOpen(false)
+ })
+ },
+ })
+ setConfirmDialogOpen(true)
+ }
+
+ // 2) "회사 업데이트"에서 회사 선택 시 → Confirm Dialog
+ function handleSelectCompany(comp: Company) {
+ setAction("update-company")
+ setPopoverOpen(false)
+
+ // Confirm Dialog에 전달할 내용
+ setConfirmProps({
+ title: `Update ${rows.length} user${rows.length > 1 ? "s" : ""} to "${comp.name}"?`,
+ description: `TaxID: ${comp.taxID}. This action will overwrite their current company.`,
+ onConfirm: async () => {
+ startTransition(async () => {
+ const { error } = await modifiUsers({
+ ids: rows.map((row) => row.original.user_id),
+ companyId: comp.id,
+ })
+ if (error) {
+ toast.error(error)
+ return
+ }
+ toast.success("Users updated")
+ setConfirmDialogOpen(false)
+ })
+ },
+ })
+ setConfirmDialogOpen(true)
+ }
+
+ // 3) "역할 업데이트" MultiSelect 후 → Confirm Dialog
+ function handleSelectRoles(newRoles: string[]) {
+ setAction("update-roles")
+ setRolesPopoverOpen(false)
+
+ setConfirmProps({
+ title: `Update ${rows.length} user${rows.length > 1 ? "s" : ""} with roles: ${newRoles.join(", ")}?`,
+ description: "This action will override their current roles.",
+ onConfirm: async () => {
+ startTransition(async () => {
+ const { error } = await modifiUsers({
+ ids: rows.map((row) => row.original.user_id),
+ roles: newRoles as ("admin" | "normal")[],
+ })
+ if (error) {
+ toast.error(error)
+ return
+ }
+ toast.success("Users updated")
+ setConfirmDialogOpen(false)
+ })
+ },
+ })
+ setConfirmDialogOpen(true)
+ }
+
+ 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">
+ <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
+
+ <Tooltip>
+ <PopoverTrigger 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-company" ? (
+ <Loader
+ className="size-3.5 animate-spin"
+ aria-hidden="true"
+ />
+ ) : (
+ <CheckCircle2
+ className="size-3.5"
+ aria-hidden="true"
+ />
+ )}
+ </Button>
+ </TooltipTrigger>
+ </PopoverTrigger>
+ <TooltipContent className="border bg-accent font-semibold text-foreground dark:bg-zinc-900">
+ <p>Update company</p>
+ </TooltipContent>
+ </Tooltip>
+
+ <PopoverContent className="w-80 p-0">
+ <Command>
+ <CommandInput placeholder="Search company..." className="h-9" />
+ <CommandList>
+ <CommandEmpty>No company found.</CommandEmpty>
+ <CommandGroup>
+ {companies.map((comp) => {
+ const label = `${comp.name} (${comp.taxID})`
+ return (
+ <CommandItem
+ key={comp.id}
+ value={label}
+ onSelect={() => handleSelectCompany(comp)}
+ >
+ {label}
+ </CommandItem>
+ )
+ })}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+
+ <Popover open={rolesPopoverOpen} onOpenChange={setRolesPopoverOpen}>
+
+ <Tooltip>
+ <PopoverTrigger 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-roles" ? (
+ <Loader
+ className="size-3.5 animate-spin"
+ aria-hidden="true"
+ />
+ ) : (
+ <ArrowUp className="size-3.5" aria-hidden="true" />
+
+ )}
+ </Button>
+ </TooltipTrigger>
+ </PopoverTrigger>
+ <TooltipContent className="border bg-accent font-semibold text-foreground dark:bg-zinc-900">
+ <p>Update roles</p>
+ </TooltipContent>
+ </Tooltip>
+ <PopoverContent>
+ <MultiSelect
+ defaultValue={["999999999"]}
+ options={[
+ /* ... */
+ { value: "999999999", label: "admin" }
+ ]}
+ onValueChange={(newRoles) => {
+ handleSelectRoles(newRoles)
+ }}
+ />
+ </PopoverContent>
+
+ </Popover>
+
+
+ <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 users</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 users</p>
+ </TooltipContent>
+ </Tooltip>
+ </div>
+ </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-company" || action === "update-roles")}
+ confirmLabel={
+ action === "delete"
+ ? "Delete"
+ : action === "update-company" || action === "update-roles"
+ ? "Update"
+ : "Confirm"
+ }
+ confirmVariant={
+ action === "delete" ? "destructive" : "default"
+ }
+ />
+ </Portal>
+ )
+}