From e0dfb55c5457aec489fc084c4567e791b4c65eb1 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Wed, 26 Mar 2025 00:37:41 +0000
Subject: 3/25 까지의 대표님 작업사항
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/tasks/table/tasks-table-floating-bar.tsx | 354 +++++++++++++++++++++++++++
 1 file changed, 354 insertions(+)
 create mode 100644 lib/tasks/table/tasks-table-floating-bar.tsx

(limited to 'lib/tasks/table/tasks-table-floating-bar.tsx')

diff --git a/lib/tasks/table/tasks-table-floating-bar.tsx b/lib/tasks/table/tasks-table-floating-bar.tsx
new file mode 100644
index 00000000..6d367f81
--- /dev/null
+++ b/lib/tasks/table/tasks-table-floating-bar.tsx
@@ -0,0 +1,354 @@
+"use client"
+
+import * as React from "react"
+import { tasks, type Task } from "@/db/schema/tasks"
+import { SelectTrigger } from "@radix-ui/react-select"
+import { type Table } from "@tanstack/react-table"
+import {
+  ArrowUp,
+  CheckCircle2,
+  Download,
+  Loader,
+  Trash2,
+  X,
+} 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 { removeTasks, modifiTasks } from "@/lib//tasks/service"
+import { DeleteTasksDialog } from "./delete-tasks-dialog"
+import { ActionConfirmDialog } from "@/components/ui/action-dialog"
+
+interface TasksTableFloatingBarProps {
+  table: Table<Task>
+}
+
+
+export function TasksTableFloatingBar({ table }: TasksTableFloatingBarProps) {
+  const rows = table.getFilteredSelectedRowModel().rows
+
+  const [isPending, startTransition] = React.useTransition()
+  const [action, setAction] = React.useState<
+    "update-status" | "update-priority" | "export" | "delete"
+  >()
+  const [popoverOpen, setPopoverOpen] = React.useState(false)
+
+  // 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])
+
+
+
+  // 공용 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 removeTasks({
+            ids: rows.map((row) => row.original.id),
+          })
+          if (error) {
+            toast.error(error)
+            return
+          }
+          toast.success("Users deleted")
+          table.toggleAllRowsSelected(false)
+          setConfirmDialogOpen(false)
+        })
+      },
+    })
+    setConfirmDialogOpen(true)
+  }
+
+  // 2) 
+  function handleSelectStatus(newStatus: Task["status"]) {
+    setAction("update-status")
+
+    setConfirmProps({
+      title: `Update ${rows.length} task${rows.length > 1 ? "s" : ""} with status: ${newStatus}?`,
+      description: "This action will override their current status.",
+      onConfirm: async () => {
+        startTransition(async () => {
+          const { error } = await modifiTasks({
+            ids: rows.map((row) => row.original.id),
+            status: newStatus,
+          })
+          if (error) {
+            toast.error(error)
+            return
+          }
+          toast.success("Tasks updated")
+          setConfirmDialogOpen(false)
+        })
+      },
+    })
+    setConfirmDialogOpen(true)
+  }
+
+    // 3) 
+    function handleSelectPriority(newPriority: Task["priority"]) {
+      setAction("update-priority")
+  
+      setConfirmProps({
+        title: `Update ${rows.length} task${rows.length > 1 ? "s" : ""} with priority: ${newPriority}?`,
+        description: "This action will override their current priority.",
+        onConfirm: async () => {
+          startTransition(async () => {
+            const { error } = await modifiTasks({
+              ids: rows.map((row) => row.original.id),
+              priority: newPriority,
+            })
+            if (error) {
+              toast.error(error)
+              return
+            }
+            toast.success("Tasks 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">
+              <Select
+                onValueChange={(value: Task["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>
+                    {tasks.status.enumValues.map((status) => (
+                      <SelectItem
+                        key={status}
+                        value={status}
+                        className="capitalize"
+                      >
+                        {status}
+                      </SelectItem>
+                    ))}
+                  </SelectGroup>
+                </SelectContent>
+              </Select>
+              <Select
+                onValueChange={(value: Task["priority"]) => {
+                  handleSelectPriority(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-priority" ? (
+                          <Loader
+                            className="size-3.5 animate-spin"
+                            aria-hidden="true"
+                          />
+                        ) : (
+                          <ArrowUp 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 priority</p>
+                  </TooltipContent>
+                </Tooltip>
+                <SelectContent align="center">
+                  <SelectGroup>
+                    {tasks.priority.enumValues.map((priority) => (
+                      <SelectItem
+                        key={priority}
+                        value={priority}
+                        className="capitalize"
+                      >
+                        {priority}
+                      </SelectItem>
+                    ))}
+                  </SelectGroup>
+                </SelectContent>
+              </Select>
+              <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 tasks</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 tasks</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-priority" || action === "update-status")}
+        confirmLabel={
+          action === "delete"
+            ? "Delete"
+            : action === "update-priority" || action === "update-status"
+            ? "Update"
+            : "Confirm"
+        }
+        confirmVariant={
+          action === "delete" ? "destructive" : "default"
+        }
+      />
+    </Portal>
+  )
+}
-- 
cgit v1.2.3

