summaryrefslogtreecommitdiff
path: root/lib/vendors/items-table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendors/items-table')
-rw-r--r--lib/vendors/items-table/delete-vendor-items-dialog.tsx182
-rw-r--r--lib/vendors/items-table/item-action-dialog.tsx248
-rw-r--r--lib/vendors/items-table/item-table-columns.tsx4
-rw-r--r--lib/vendors/items-table/item-table-toolbar-actions.tsx36
-rw-r--r--lib/vendors/items-table/item-table.tsx23
5 files changed, 473 insertions, 20 deletions
diff --git a/lib/vendors/items-table/delete-vendor-items-dialog.tsx b/lib/vendors/items-table/delete-vendor-items-dialog.tsx
new file mode 100644
index 00000000..bcc84cc8
--- /dev/null
+++ b/lib/vendors/items-table/delete-vendor-items-dialog.tsx
@@ -0,0 +1,182 @@
+"use client"
+
+import * as React from "react"
+import { VendorItemsView } from "@/db/schema/vendors"
+import { type Row } from "@tanstack/react-table"
+import { Loader, Trash } from "lucide-react"
+import { toast } from "sonner"
+
+import { useMediaQuery } from "@/hooks/use-media-query"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from "@/components/ui/drawer"
+
+import { removeVendorItems } from "../service"
+
+interface DeleteVendorItemsDialogProps
+ extends React.ComponentPropsWithoutRef<typeof Dialog> {
+ vendorId: number
+ items: Row<VendorItemsView>["original"][]
+ showTrigger?: boolean
+ onSuccess?: () => void
+}
+
+export function DeleteVendorItemsDialog({
+ vendorId,
+ items,
+ showTrigger = true,
+ onSuccess,
+ ...props
+}: DeleteVendorItemsDialogProps) {
+ const [isDeletePending, startDeleteTransition] = React.useTransition()
+ const isDesktop = useMediaQuery("(min-width: 640px)")
+
+ function onDelete() {
+ startDeleteTransition(async () => {
+ const { error } = await removeVendorItems({
+ itemCodes: items.map((item) => item.itemCode).filter(Boolean) as string[],
+ vendorId,
+ })
+
+ if (error) {
+ toast.error(error)
+ return
+ }
+
+ props.onOpenChange?.(false)
+ toast.success(
+ items.length === 1
+ ? "Item deleted successfully"
+ : `${items.length} items deleted successfully`
+ )
+ onSuccess?.()
+ })
+ }
+
+ if (isDesktop) {
+ return (
+ <Dialog {...props}>
+ {showTrigger ? (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ Delete ({items.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Are you absolutely sure?</DialogTitle>
+ <DialogDescription>
+ This action cannot be undone. This will permanently delete{" "}
+ <span className="font-medium">{items.length}</span>
+ {items.length === 1 ? " item" : " items"} from this vendor.
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* 삭제될 아이템 목록 미리보기 */}
+ <div className="max-h-32 overflow-y-auto rounded-md border p-2">
+ {items.map((item, index) => (
+ <div key={item.itemCode || index} className="flex justify-between text-sm py-1">
+ <span className="font-medium truncate">{item.itemName}</span>
+ <span className="text-muted-foreground ml-2 flex-shrink-0">
+ {item.itemCode}
+ </span>
+ </div>
+ ))}
+ </div>
+
+ <DialogFooter className="gap-2 sm:space-x-0">
+ <DialogClose asChild>
+ <Button variant="outline">Cancel</Button>
+ </DialogClose>
+ <Button
+ aria-label="Delete selected items"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ Delete
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger ? (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ Delete ({items.length})
+ </Button>
+ </DrawerTrigger>
+ ) : null}
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>Are you absolutely sure?</DrawerTitle>
+ <DrawerDescription>
+ This action cannot be undone. This will permanently delete{" "}
+ <span className="font-medium">{items.length}</span>
+ {items.length === 1 ? " item" : " items"} from this vendor.
+ </DrawerDescription>
+ </DrawerHeader>
+
+ {/* 삭제될 아이템 목록 미리보기 */}
+ <div className="max-h-32 overflow-y-auto rounded-md border p-2 mx-4">
+ {items.map((item, index) => (
+ <div key={item.itemCode || index} className="flex justify-between text-sm py-1">
+ <span className="font-medium truncate">{item.itemName}</span>
+ <span className="text-muted-foreground ml-2 flex-shrink-0">
+ {item.itemCode}
+ </span>
+ </div>
+ ))}
+ </div>
+
+ <DrawerFooter className="gap-2 sm:space-x-0">
+ <DrawerClose asChild>
+ <Button variant="outline">Cancel</Button>
+ </DrawerClose>
+ <Button
+ aria-label="Delete selected items"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ Delete
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+} \ No newline at end of file
diff --git a/lib/vendors/items-table/item-action-dialog.tsx b/lib/vendors/items-table/item-action-dialog.tsx
new file mode 100644
index 00000000..19df27f8
--- /dev/null
+++ b/lib/vendors/items-table/item-action-dialog.tsx
@@ -0,0 +1,248 @@
+// components/vendor-items/item-actions-dialogs.tsx
+"use client"
+
+import * as React from "react"
+import type { DataTableRowAction } from "@/types/table"
+import { VendorItemsView } from "@/db/schema/vendors"
+import { toast } from "sonner"
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import { Button } from "@/components/ui/button"
+import { Label } from "@/components/ui/label"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+
+import { updateVendorItem, deleteVendorItem, getItemsForVendor } from "../service"
+
+interface ItemActionsDialogsProps {
+ vendorId: number
+ rowAction: DataTableRowAction<VendorItemsView> | null
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<VendorItemsView> | null>>
+}
+
+export function ItemActionsDialogs({
+ vendorId,
+ rowAction,
+ setRowAction,
+}: ItemActionsDialogsProps) {
+ const [isUpdatePending, startUpdateTransition] = React.useTransition()
+ const [isDeletePending, startDeleteTransition] = React.useTransition()
+ const [availableMaterials, setAvailableMaterials] = React.useState<any[]>([])
+ const [selectedItemCode, setSelectedItemCode] = React.useState<string>("")
+
+ // 사용 가능한 재료 목록 로드
+ React.useEffect(() => {
+ if (rowAction?.type === "update") {
+ getItemsForVendor(vendorId).then((result) => {
+ if (result.data) {
+ setAvailableMaterials(result.data)
+ }
+ })
+ }
+ }, [rowAction, vendorId])
+
+ // Edit Dialog
+ const EditDialog = () => {
+ if (!rowAction || rowAction.type !== "update") return null
+
+ const item = rowAction.row.original
+
+ const handleSubmit = () => {
+ if (!selectedItemCode) {
+ toast.error("Please select a new item")
+ return
+ }
+
+ if (!item.itemCode) {
+ toast.error("Invalid item code")
+ return
+ }
+
+ startUpdateTransition(async () => {
+ const result = await updateVendorItem(vendorId, item.itemCode, selectedItemCode)
+
+ if (result.error) {
+ toast.error(result.error)
+ } else {
+ toast.success("Item updated successfully")
+ setRowAction(null)
+ }
+ })
+ }
+
+ return (
+ <Dialog
+ open={true}
+ onOpenChange={(open) => !open && setRowAction(null)}
+ >
+ <DialogContent className="sm:max-w-[425px]">
+ <DialogHeader>
+ <DialogTitle>Change Item</DialogTitle>
+ <DialogDescription>
+ Select a new item to replace "{item.itemName}" (Code: {item.itemCode || 'N/A'}).
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div className="space-y-2">
+ <Label>Current Item</Label>
+ <div className="p-2 bg-muted rounded-md">
+ <div className="font-medium">{item.itemName}</div>
+ <div className="text-sm text-muted-foreground">Code: {item.itemCode || 'N/A'}</div>
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="newItem">New Item</Label>
+ <Select value={selectedItemCode} onValueChange={setSelectedItemCode}>
+ <SelectTrigger>
+ <SelectValue placeholder="Select a new item" />
+ </SelectTrigger>
+ <SelectContent>
+ {availableMaterials.map((material) => (
+ <SelectItem key={material.itemCode} value={material.itemCode}>
+ <div>
+ <div className="font-medium">{material.itemName}</div>
+ <div className="text-sm text-muted-foreground">Code: {material.itemCode}</div>
+ </div>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => setRowAction(null)}
+ disabled={isUpdatePending}
+ >
+ Cancel
+ </Button>
+ <Button
+ onClick={handleSubmit}
+ disabled={isUpdatePending || !selectedItemCode}
+ >
+ {isUpdatePending ? "Updating..." : "Update Item"}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ // Delete Dialog
+ const DeleteDialog = () => {
+ if (!rowAction || rowAction.type !== "delete") return null
+
+ const item = rowAction.row.original
+
+ const handleDelete = () => {
+ if (!item.itemCode) {
+ toast.error("Invalid item code")
+ return
+ }
+
+ startDeleteTransition(async () => {
+ const result = await deleteVendorItem(vendorId, item.itemCode)
+
+ if (result.error) {
+ toast.error(result.error)
+ } else {
+ toast.success("Item deleted successfully")
+ setRowAction(null)
+ }
+ })
+ }
+
+ return (
+ <AlertDialog
+ open={true}
+ onOpenChange={(open) => !open && setRowAction(null)}
+ >
+ <AlertDialogContent>
+ return (
+ <AlertDialog
+ open={true}
+ onOpenChange={(open) => !open && setRowAction(null)}
+ >
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>Are you sure?</AlertDialogTitle>
+ <AlertDialogDescription>
+ This will permanently delete the item "{item.itemName}" (Code: {item.itemCode || 'N/A'}).
+ This action cannot be undone.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel disabled={isDeletePending}>
+ Cancel
+ </AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleDelete}
+ disabled={isDeletePending}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ {isDeletePending ? "Deleting..." : "Delete"}
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )
+ }
+
+ return (
+ <>
+ <EditDialog />
+ <DeleteDialog />
+ </>
+ )
+}
+ <AlertDialogFooter>
+ <AlertDialogCancel disabled={isDeletePending}>
+ Cancel
+ </AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleDelete}
+ disabled={isDeletePending}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ {isDeletePending ? "Deleting..." : "Delete"}
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )
+ }
+
+ return (
+ <>
+ <EditDialog />
+ <DeleteDialog />
+ </>
+ )
+} \ No newline at end of file
diff --git a/lib/vendors/items-table/item-table-columns.tsx b/lib/vendors/items-table/item-table-columns.tsx
index b5d26434..769722e4 100644
--- a/lib/vendors/items-table/item-table-columns.tsx
+++ b/lib/vendors/items-table/item-table-columns.tsx
@@ -24,10 +24,8 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
-import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"
-import { VendorItemsView, vendors } from "@/db/schema/vendors"
-import { modifyVendor } from "../service"
+import { VendorItemsView } from "@/db/schema/vendors"
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
import { vendorItemsColumnsConfig } from "@/config/vendorItemsColumnsConfig"
diff --git a/lib/vendors/items-table/item-table-toolbar-actions.tsx b/lib/vendors/items-table/item-table-toolbar-actions.tsx
index f7bd2bf6..8be67f71 100644
--- a/lib/vendors/items-table/item-table-toolbar-actions.tsx
+++ b/lib/vendors/items-table/item-table-toolbar-actions.tsx
@@ -1,3 +1,4 @@
+// components/vendor-items/item-table-toolbar-actions.tsx
"use client"
import * as React from "react"
@@ -8,21 +9,26 @@ import { toast } from "sonner"
import { exportTableToExcel } from "@/lib/export"
import { Button } from "@/components/ui/button"
-
// 만약 서버 액션이나 API 라우트를 이용해 업로드 처리한다면 import
import { importTasksExcel } from "@/lib/tasks/service" // 예시
-import { VendorItemsView } from "@/db/schema/vendors"
+import {
+ VendorItemsView
+} from "@/db/schema/vendors"
import { AddItemDialog } from "./add-item-dialog"
+import { DeleteVendorItemsDialog } from "./delete-vendor-items-dialog"
interface VendorsTableToolbarActionsProps {
table: Table<VendorItemsView>
vendorId: number
}
-export function VendorsTableToolbarActions({ table,vendorId }: VendorsTableToolbarActionsProps) {
+export function VendorsTableToolbarActions({ table, vendorId }: VendorsTableToolbarActionsProps) {
// 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식
const fileInputRef = React.useRef<HTMLInputElement>(null)
+ // 선택된 행들 가져오기
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+
// 파일이 선택되었을 때 처리
async function onFileChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0]
@@ -55,7 +61,6 @@ export function VendorsTableToolbarActions({ table,vendorId }: VendorsTableToolb
} catch (err) {
toast.error("파일 업로드 중 오류가 발생했습니다.")
-
}
}
@@ -66,10 +71,21 @@ export function VendorsTableToolbarActions({ table,vendorId }: VendorsTableToolb
return (
<div className="flex items-center gap-2">
-
- <AddItemDialog vendorId={vendorId}/>
-
- {/** 3) Import 버튼 (파일 업로드) */}
+ <AddItemDialog vendorId={vendorId} />
+
+ {/* 삭제 버튼 - 선택된 행이 있을 때만 표시 */}
+ {selectedRows.length > 0 && (
+ <DeleteVendorItemsDialog
+ vendorId={vendorId}
+ items={selectedRows.map((row) => row.original)}
+ onSuccess={() => {
+ // 삭제 성공 후 선택 해제
+ table.toggleAllPageRowsSelected(false)
+ }}
+ />
+ )}
+
+ {/** Import 버튼 (파일 업로드) */}
<Button variant="outline" size="sm" className="gap-2" onClick={handleImportClick}>
<Upload className="size-4" aria-hidden="true" />
<span className="hidden sm:inline">Import</span>
@@ -86,13 +102,13 @@ export function VendorsTableToolbarActions({ table,vendorId }: VendorsTableToolb
onChange={onFileChange}
/>
- {/** 4) Export 버튼 */}
+ {/** Export 버튼 */}
<Button
variant="outline"
size="sm"
onClick={() =>
exportTableToExcel(table, {
- filename: "tasks",
+ filename: "vendor-items",
excludeColumns: ["select", "actions"],
})
}
diff --git a/lib/vendors/items-table/item-table.tsx b/lib/vendors/items-table/item-table.tsx
index d8cd0ea2..a58a136c 100644
--- a/lib/vendors/items-table/item-table.tsx
+++ b/lib/vendors/items-table/item-table.tsx
@@ -13,8 +13,11 @@ import { DataTable } from "@/components/data-table/data-table"
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
import { useFeatureFlags } from "./feature-flags-provider"
import { getColumns } from "./item-table-columns"
-import { getVendorItems, } from "../service"
-import { VendorItemsView, vendors } from "@/db/schema/vendors"
+import {
+ getVendorItems,
+} from "../service"
+import { VendorItemsView } from "@/db/schema/vendors"
+import { ItemActionsDialogs } from "./item-action-dialog"
import { VendorsTableToolbarActions } from "./item-table-toolbar-actions"
interface VendorsTableProps {
@@ -22,25 +25,24 @@ interface VendorsTableProps {
[
Awaited<ReturnType<typeof getVendorItems>>,
]
- >,
- vendorId:number
+ >,
+ vendorId: number
}
-export function VendorItemsTable({ promises , vendorId}: VendorsTableProps) {
+export function VendorItemsTable({ promises, vendorId }: VendorsTableProps) {
const { featureFlags } = useFeatureFlags()
// Suspense로 받아온 데이터
const [{ data, pageCount }] = React.use(promises)
const [rowAction, setRowAction] = React.useState<DataTableRowAction<VendorItemsView> | null>(null)
- // getColumns() 호출 시, router를 주입
+ // getColumns() 호출 시, setRowAction을 주입
const columns = React.useMemo(
() => getColumns({ setRowAction }),
[setRowAction]
)
const filterFields: DataTableFilterField<VendorItemsView>[] = [
-
]
const advancedFilterFields: DataTableAdvancedFilterField<VendorItemsView>[] = [
@@ -80,6 +82,13 @@ export function VendorItemsTable({ promises , vendorId}: VendorsTableProps) {
<VendorsTableToolbarActions table={table} vendorId={vendorId} />
</DataTableAdvancedToolbar>
</DataTable>
+
+ {/* 수정/삭제 다이얼로그 추가 */}
+ <ItemActionsDialogs
+ vendorId={vendorId}
+ rowAction={rowAction}
+ setRowAction={setRowAction}
+ />
</>
)
} \ No newline at end of file