diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-01 13:52:21 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-01 13:52:21 +0000 |
| commit | bac0228d21b7195065e9cddcc327ae33659c7bcc (patch) | |
| tree | 8f3016ae4533c8706d0c00a605d9b1d41968c2bc /lib/vendors/items-table | |
| parent | 2fdce8d7a57c792bba0ac36fa554dca9c9cc31e3 (diff) | |
(대표님) 20250601까지 작업사항
Diffstat (limited to 'lib/vendors/items-table')
| -rw-r--r-- | lib/vendors/items-table/delete-vendor-items-dialog.tsx | 182 | ||||
| -rw-r--r-- | lib/vendors/items-table/item-action-dialog.tsx | 248 | ||||
| -rw-r--r-- | lib/vendors/items-table/item-table-columns.tsx | 4 | ||||
| -rw-r--r-- | lib/vendors/items-table/item-table-toolbar-actions.tsx | 36 | ||||
| -rw-r--r-- | lib/vendors/items-table/item-table.tsx | 23 |
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 |
