diff options
Diffstat (limited to 'lib/tech-vendors/table/tech-vendors-table.tsx')
| -rw-r--r-- | lib/tech-vendors/table/tech-vendors-table.tsx | 470 |
1 files changed, 276 insertions, 194 deletions
diff --git a/lib/tech-vendors/table/tech-vendors-table.tsx b/lib/tech-vendors/table/tech-vendors-table.tsx index a8e18501..7f9625cf 100644 --- a/lib/tech-vendors/table/tech-vendors-table.tsx +++ b/lib/tech-vendors/table/tech-vendors-table.tsx @@ -1,195 +1,277 @@ -"use client" - -import * as React from "react" -import { useRouter } from "next/navigation" -import type { - DataTableAdvancedFilterField, - DataTableFilterField, - DataTableRowAction, -} from "@/types/table" - -import { useDataTable } from "@/hooks/use-data-table" -import { DataTable } from "@/components/data-table/data-table" -import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" -import { getColumns } from "./tech-vendors-table-columns" -import { getTechVendors, getTechVendorStatusCounts } from "../service" -import { TechVendor, techVendors, TechVendorWithAttachments } from "@/db/schema/techVendors" -import { TechVendorsTableToolbarActions } from "./tech-vendors-table-toolbar-actions" -import { UpdateVendorSheet } from "./update-vendor-sheet" -import { getVendorStatusIcon } from "../utils" -import { TechVendorPossibleItemsViewDialog } from "./tech-vendor-possible-items-view-dialog" -// import { ViewTechVendorLogsDialog } from "./view-tech-vendors-logs-dialog" - -interface TechVendorsTableProps { - promises: Promise< - [ - Awaited<ReturnType<typeof getTechVendors>>, - Awaited<ReturnType<typeof getTechVendorStatusCounts>> - ] - > -} - -export function TechVendorsTable({ promises }: TechVendorsTableProps) { - // Suspense로 받아온 데이터 - const [{ data, pageCount }, statusCounts] = React.use(promises) - const [isCompact, setIsCompact] = React.useState<boolean>(false) - - const [rowAction, setRowAction] = React.useState<DataTableRowAction<TechVendor> | null>(null) - const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) - const [selectedVendorForItems, setSelectedVendorForItems] = React.useState<TechVendor | null>(null) - - // **router** 획득 - const router = useRouter() - - // openItemsDialog 함수 정의 - const openItemsDialog = React.useCallback((vendor: TechVendor) => { - setSelectedVendorForItems(vendor) - setItemsDialogOpen(true) - }, []) - - // getColumns() 호출 시, router와 openItemsDialog를 주입 - const columns = React.useMemo( - () => getColumns({ setRowAction, router, openItemsDialog }), - [setRowAction, router, openItemsDialog] - ) - - // 상태 한글 변환 유틸리티 함수 - const getStatusDisplay = (status: string): string => { - const statusMap: Record<string, string> = { - "ACTIVE": "활성 상태", - "INACTIVE": "비활성 상태", - "BLACKLISTED": "거래 금지", - "PENDING_INVITE": "초대 대기", - "INVITED": "초대 완료", - "QUOTE_COMPARISON": "견적 비교", - }; - - return statusMap[status] || status; - }; - - const filterFields: DataTableFilterField<TechVendorWithAttachments>[] = [ - { - id: "status", - label: "상태", - options: techVendors.status.enumValues.map((status) => ({ - label: getStatusDisplay(status), - value: status, - count: statusCounts[status], - })), - }, - - { id: "vendorCode", label: "업체 코드" }, - ] - - const advancedFilterFields: DataTableAdvancedFilterField<TechVendorWithAttachments>[] = [ - { id: "vendorName", label: "업체명", type: "text" }, - { id: "vendorCode", label: "업체코드", type: "text" }, - { id: "email", label: "이메일", type: "text" }, - { id: "country", label: "국가", type: "text" }, - { - id: "status", - label: "업체승인상태", - type: "multi-select", - options: techVendors.status.enumValues.map((status) => ({ - label: getStatusDisplay(status), - value: status, - count: statusCounts[status], - icon: getVendorStatusIcon(status), - })), - }, - { - id: "techVendorType", - label: "벤더 타입", - type: "multi-select", - options: [ - { label: "조선", value: "조선" }, - { label: "해양TOP", value: "해양TOP" }, - { label: "해양HULL", value: "해양HULL" }, - ], - }, - { - id: "workTypes", - label: "Work Type", - type: "multi-select", - options: [ - // 조선 workTypes - { label: "기장", value: "기장" }, - { label: "전장", value: "전장" }, - { label: "선실", value: "선실" }, - { label: "배관", value: "배관" }, - { label: "철의", value: "철의" }, - // 해양TOP workTypes - { label: "TM", value: "TM" }, - { label: "TS", value: "TS" }, - { label: "TE", value: "TE" }, - { label: "TP", value: "TP" }, - // 해양HULL workTypes - { label: "HA", value: "HA" }, - { label: "HE", value: "HE" }, - { label: "HH", value: "HH" }, - { label: "HM", value: "HM" }, - { label: "NC", value: "NC" }, - ], - }, - { id: "createdAt", label: "등록일", type: "date" }, - { id: "updatedAt", label: "수정일", type: "date" }, - ] - - const { table } = useDataTable({ - data, - columns, - pageCount, - filterFields, - enablePinning: true, - enableAdvancedFilter: true, - initialState: { - sorting: [{ id: "createdAt", desc: true }], - columnPinning: { right: ["actions", "possibleItems"] }, - }, - getRowId: (originalRow) => String(originalRow.id), - shallow: false, - clearOnDefault: true, - }) - - const handleCompactChange = React.useCallback((compact: boolean) => { - setIsCompact(compact) - }, []) - - // 테이블 새로고침 핸들러 - const handleRefresh = React.useCallback(() => { - router.refresh() - }, [router]) - - - return ( - <> - <DataTable - table={table} - compact={isCompact} - // floatingBar={<TechVendorsTableFloatingBar table={table} />} - > - <DataTableAdvancedToolbar - table={table} - filterFields={advancedFilterFields} - shallow={false} - enableCompactToggle={true} - compactStorageKey="techVendorsTableCompact" - onCompactChange={handleCompactChange} - > - <TechVendorsTableToolbarActions table={table} onRefresh={handleRefresh} /> - </DataTableAdvancedToolbar> - </DataTable> - <UpdateVendorSheet - open={rowAction?.type === "update"} - onOpenChange={() => setRowAction(null)} - vendor={rowAction?.row.original ?? null} - /> - <TechVendorPossibleItemsViewDialog - open={itemsDialogOpen} - onOpenChange={setItemsDialogOpen} - vendor={selectedVendorForItems} - /> - - </> - ) +"use client"
+
+import * as React from "react"
+import { useRouter } from "next/navigation"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+import { cn } from "@/lib/utils"
+import { PanelLeftClose, PanelLeftOpen } from "lucide-react"
+
+import { useDataTable } from "@/hooks/use-data-table"
+import { DataTable } from "@/components/data-table/data-table"
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
+import { Button } from "@/components/ui/button"
+import { getColumns } from "./tech-vendors-table-columns"
+import { getTechVendors, getTechVendorStatusCounts } from "../service"
+import { TechVendor, techVendors, TechVendorWithAttachments } from "@/db/schema/techVendors"
+import { TechVendorsTableToolbarActions } from "./tech-vendors-table-toolbar-actions"
+import { UpdateVendorSheet } from "./update-vendor-sheet"
+import { getVendorStatusIcon } from "../utils"
+import { TechVendorsFilterSheet } from "./tech-vendors-filter-sheet"
+// import { ViewTechVendorLogsDialog } from "./view-tech-vendors-logs-dialog"
+
+// 필터 패널 관련 상수
+const FILTER_PANEL_WIDTH = 400;
+const LAYOUT_HEADER_HEIGHT = 60;
+const LOCAL_HEADER_HEIGHT = 60;
+const FIXED_FILTER_HEIGHT = "calc(100vh - 120px)";
+
+interface TechVendorsTableProps {
+ promises: Promise<
+ [
+ Awaited<ReturnType<typeof getTechVendors>>,
+ Awaited<ReturnType<typeof getTechVendorStatusCounts>>
+ ]
+ >
+ className?: string;
+ calculatedHeight?: string;
+}
+
+export function TechVendorsTable({
+ promises,
+ className,
+ calculatedHeight
+}: TechVendorsTableProps) {
+ // Suspense로 받아온 데이터
+ const [{ data, pageCount }, statusCounts] = React.use(promises)
+ const [isCompact, setIsCompact] = React.useState<boolean>(false)
+
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<TechVendor> | null>(null)
+
+ // 필터 패널 상태
+ const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false)
+
+ // **router** 획득
+ const router = useRouter()
+
+ // getColumns() 호출 시, router를 주입
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction, router }),
+ [setRowAction, router]
+ )
+
+ // 상태 한글 변환 유틸리티 함수
+ const getStatusDisplay = (status: string): string => {
+ const statusMap: Record<string, string> = {
+ "ACTIVE": "활성 상태",
+ "INACTIVE": "비활성 상태",
+ "BLACKLISTED": "거래 금지",
+ "PENDING_INVITE": "초대 대기",
+ "INVITED": "초대 완료",
+ "QUOTE_COMPARISON": "견적 비교",
+ };
+
+ return statusMap[status] || status;
+ };
+
+ const filterFields: DataTableFilterField<TechVendorWithAttachments>[] = [
+ {
+ id: "status",
+ label: "상태",
+ options: techVendors.status.enumValues.map((status) => ({
+ label: getStatusDisplay(status),
+ value: status,
+ count: statusCounts[status],
+ })),
+ },
+
+ { id: "vendorCode", label: "업체 코드" },
+ ]
+
+ const advancedFilterFields: DataTableAdvancedFilterField<TechVendorWithAttachments>[] = [
+ { id: "vendorName", label: "업체명", type: "text" },
+ { id: "vendorCode", label: "업체코드", type: "text" },
+ { id: "email", label: "이메일", type: "text" },
+ { id: "country", label: "국가", type: "text" },
+ {
+ id: "status",
+ label: "업체승인상태",
+ type: "multi-select",
+ options: techVendors.status.enumValues.map((status) => ({
+ label: getStatusDisplay(status),
+ value: status,
+ count: statusCounts[status],
+ icon: getVendorStatusIcon(status),
+ })),
+ },
+ {
+ id: "techVendorType",
+ label: "벤더 타입",
+ type: "multi-select",
+ options: [
+ { label: "조선", value: "조선" },
+ { label: "해양TOP", value: "해양TOP" },
+ { label: "해양HULL", value: "해양HULL" },
+ ],
+ },
+ {
+ id: "workTypes",
+ label: "Work Type",
+ type: "multi-select",
+ options: [
+ // 조선 workTypes
+ { label: "기장", value: "기장" },
+ { label: "전장", value: "전장" },
+ { label: "선실", value: "선실" },
+ { label: "배관", value: "배관" },
+ { label: "철의", value: "철의" },
+ { label: "선체", value: "선체" },
+ // 해양TOP workTypes
+ { label: "TM", value: "TM" },
+ { label: "TS", value: "TS" },
+ { label: "TE", value: "TE" },
+ { label: "TP", value: "TP" },
+ // 해양HULL workTypes
+ { label: "HA", value: "HA" },
+ { label: "HE", value: "HE" },
+ { label: "HH", value: "HH" },
+ { label: "HM", value: "HM" },
+ { label: "NC", value: "NC" },
+ { label: "HP", value: "HP" },
+ { label: "HO", value: "HO" },
+ ],
+ },
+ { id: "createdAt", label: "등록일", type: "date" },
+ { id: "updatedAt", label: "수정일", type: "date" },
+ ]
+
+ const { table } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "createdAt", desc: true }],
+ columnPinning: { right: ["actions", "possibleItems"] },
+ },
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ const handleCompactChange = React.useCallback((compact: boolean) => {
+ setIsCompact(compact)
+ }, [])
+
+ // 테이블 새로고침 핸들러
+ const handleRefresh = React.useCallback(() => {
+ router.refresh()
+ }, [router])
+
+ // 필터 패널 검색 핸들러
+ const handleSearch = React.useCallback(() => {
+ router.refresh()
+ }, [router])
+
+ return (
+ <div
+ className={cn("flex flex-col relative", className)}
+ style={{ height: calculatedHeight }}
+ >
+ {/* Filter Panel */}
+ <div
+ className={cn(
+ "fixed left-0 bg-background border-r z-30 flex flex-col transition-all duration-300 ease-in-out overflow-hidden",
+ isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0"
+ )}
+ style={{
+ width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px',
+ top: `${LAYOUT_HEADER_HEIGHT*2}px`,
+ height: FIXED_FILTER_HEIGHT
+ }}
+ >
+ {/* Filter Content */}
+ <div className="h-full">
+ <TechVendorsFilterSheet
+ isOpen={isFilterPanelOpen}
+ onClose={() => setIsFilterPanelOpen(false)}
+ onSearch={handleSearch}
+ isLoading={false}
+ />
+ </div>
+ </div>
+
+ {/* Main Content */}
+ <div
+ className="flex flex-col transition-all duration-300 ease-in-out"
+ style={{
+ width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : '100%',
+ marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px',
+ height: '100%'
+ }}
+ >
+ {/* Header Bar - 고정 높이 */}
+ <div
+ className="flex items-center justify-between p-4 bg-background border-b"
+ style={{
+ height: `${LOCAL_HEADER_HEIGHT}px`,
+ flexShrink: 0
+ }}
+ >
+ <div className="flex items-center gap-3">
+ <Button
+ variant="outline"
+ size="sm"
+ type='button'
+ onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
+ className="flex items-center shadow-sm"
+ >
+ {isFilterPanelOpen ? <PanelLeftClose className="size-4"/> : <PanelLeftOpen className="size-4"/>}
+ </Button>
+ </div>
+
+ {/* Right side info
+ <div className="text-sm text-muted-foreground">
+ {data && (
+ <span>총 {data.length || 0}건</span>
+ )}
+ </div> */}
+ </div>
+
+ {/* DataTable */}
+ <div className="flex-1 overflow-hidden">
+ <DataTable
+ table={table}
+ compact={isCompact}
+ // floatingBar={<TechVendorsTableFloatingBar table={table} />}
+ >
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ enableCompactToggle={true}
+ compactStorageKey="techVendorsTableCompact"
+ onCompactChange={handleCompactChange}
+ >
+ <TechVendorsTableToolbarActions
+ table={table}
+ onRefresh={handleRefresh}
+ />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+ </div>
+ </div>
+
+ <UpdateVendorSheet
+ open={rowAction?.type === "update"}
+ onOpenChange={() => setRowAction(null)}
+ vendor={rowAction?.row.original ?? null}
+ />
+ </div>
+ )
}
\ No newline at end of file |
