summaryrefslogtreecommitdiff
path: root/lib/data-table.ts
blob: 4ad57d76277bff99086299be6906ce029df1ccfd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import type { ColumnType, Filter, FilterOperator, } from "@/types/table"
import { type Column } from "@tanstack/react-table"

import { dataTableConfig } from "@/config/data-table"
import { FilterFn, Row  } from "@tanstack/react-table"

/**
 * Generate common pinning styles for a table column.
 *
 * This function calculates and returns CSS properties for pinned columns in a data table.
 * It handles both left and right pinning, applying appropriate styles for positioning,
 * shadows, and z-index. The function also considers whether the column is the last left-pinned
 * or first right-pinned column to apply specific shadow effects.
 *
 * @param options - The options for generating pinning styles.
 * @param options.column - The column object for which to generate styles.
 * @param options.withBorder - Whether to show a box shadow between pinned and scrollable columns.
 * @returns A React.CSSProperties object containing the calculated styles.
 */
export function getCommonPinningStylesWithBorder<TData>({
  column,
  withBorder = true,
}: {
  column: Column<TData>
  withBorder?: boolean
}): React.CSSProperties {
  const pinnedSide = column.getIsPinned() as "left" | "right" | false
     
  // 안전한 방식으로 위치 확인
  const isLastLeft = pinnedSide === "left" && column.getIsLastColumn?.("left")
  const isFirstRight = pinnedSide === "right" && column.getIsFirstColumn?.("right")
 
  const styles: React.CSSProperties = {
    /* ▒▒ sticky 위치 ▒▒ */
    position: pinnedSide ? "sticky" : "relative",
    left: pinnedSide === "left" ? `${column.getStart("left")}px` : undefined,
    right: pinnedSide === "right" ? `${column.getAfter("right")}px` : undefined,
         
    /* ▒▒ 기타 스타일 ▒▒ */
    width: column.getSize(),
    // 불투명한 배경색 설정 - 테이블의 배경색과 동일하게
    background: pinnedSide ? "hsl(var(--background))" : "transparent",
    zIndex: pinnedSide ? 1 : 0,
  }
 
  // 구분선 및 그림자 추가 (선택사항)
  // if (withBorder && pinnedSide) {
  //   if (pinnedSide === "left" && isLastLeft) {
  //     // 좌측 핀의 마지막 컬럼에만 우측 그림자 추가
  //     styles.boxShadow = "2px 0 4px -2px rgba(0, 0, 0, 0.1)"
  //     styles.borderRight = "1px solid hsl(var(--border))"
  //   } else if (pinnedSide === "right" && isFirstRight) {
  //     // 우측 핀의 첫 컬럼에만 좌측 그림자 추가
  //     styles.boxShadow = "-2px 0 4px -2px rgba(0, 0, 0, 0.1)"
  //     styles.borderLeft = "1px solid hsl(var(--border))"
  //   }
  // }
 
  return styles
}

// 디버깅을 위한 헬퍼 함수
export function debugPinningInfo<TData>(column: Column<TData>) {
  console.log("Column Debug Info:", {
    id: column.id,
    isPinned: column.getIsPinned(),
    start: column.getStart("left"),
    after: column.getAfter("right"),
    size: column.getSize(),
    isLastLeft: column.getIsLastColumn?.("left"),
    isFirstRight: column.getIsFirstColumn?.("right"),
  })
}
/**
 * Determine the default filter operator for a given column type.
 *
 * This function returns the most appropriate default filter operator based on the
 * column's data type. For text columns, it returns 'iLike' (case-insensitive like),
 * while for all other types, it returns 'eq' (equality).
 *
 * @param columnType - The type of the column (e.g., 'text', 'number', 'date', etc.).
 * @returns The default FilterOperator for the given column type.
 */
export function getDefaultFilterOperator(
  columnType: ColumnType
): FilterOperator {
  if (columnType === "text") {
    return "iLike"
  }

  return "eq"
}

/**
 * Retrieve the list of applicable filter operators for a given column type.
 *
 * This function returns an array of filter operators that are relevant and applicable
 * to the specified column type. It uses a predefined mapping of column types to
 * operator lists, falling back to text operators if an unknown column type is provided.
 *
 * @param columnType - The type of the column for which to get filter operators.
 * @returns An array of objects, each containing a label and value for a filter operator.
 */
export function getFilterOperators(columnType: ColumnType) {
  const operatorMap: Record<
    ColumnType,
    { label: string; value: FilterOperator }[]
  > = {
    text: dataTableConfig.textOperators,
    number: dataTableConfig.numericOperators,
    select: dataTableConfig.selectOperators,
    "multi-select": dataTableConfig.selectOperators,
    boolean: dataTableConfig.booleanOperators,
    date: dataTableConfig.dateOperators,
  }

  return operatorMap[columnType] ?? dataTableConfig.textOperators
}

/**
 * Filters out invalid or empty filters from an array of filters.
 *
 * This function processes an array of filters and returns a new array
 * containing only the valid filters. A filter is considered valid if:
 * - It has an 'isEmpty' or 'isNotEmpty' operator, or
 * - Its value is not empty (for array values, at least one element must be present;
 *   for other types, the value must not be an empty string, null, or undefined)
 *
 * @param filters - An array of Filter objects to be validated.
 * @returns A new array containing only the valid filters.
 */
export function getValidFilters<TData>(
  filters: Filter<TData>[]
): Filter<TData>[] {
  return filters?.filter(
    (filter) =>
      filter.operator === "isEmpty" ||
      filter.operator === "isNotEmpty" ||
      (Array.isArray(filter.value)
        ? filter.value.length > 0
        : filter.value !== "" &&
          filter.value !== null &&
          filter.value !== undefined)
  )
}

interface NumericFilterValue {
  operator: string
  inputValue?: number
}


export const numericFilter: FilterFn<any> = (
  row: Row<any>,
  columnId: string,
  filterValue: NumericFilterValue
) => {
  const rowValue = row.getValue(columnId)

  // handle "isEmpty" / "isNotEmpty"
  if (filterValue.operator === "isEmpty") {
    return rowValue == null || rowValue === ""
  } else if (filterValue.operator === "isNotEmpty") {
    return !(rowValue == null || rowValue === "")
  }

  // parse rowValue → numeric
  const numericRowVal =
    typeof rowValue === "number" ? rowValue : parseFloat(String(rowValue))

  if (isNaN(numericRowVal)) {
    // rowValue not a number
    return false
  }

  // parse filterValue.inputValue
  const filterNum = filterValue.inputValue
  if (filterNum == null || isNaN(filterNum)) {
    // if user didn’t actually type a number, match everything or nothing (your choice)
    return true
  }

  // compare based on operator
  switch (filterValue.operator) {
    case "eq":
      return numericRowVal === filterNum
    case "ne":
      return numericRowVal !== filterNum
    case "lt":
      return numericRowVal < filterNum
    case "lte":
      return numericRowVal <= filterNum
    case "gt":
      return numericRowVal > filterNum
    case "gte":
      return numericRowVal >= filterNum
    default:
      return true
  }
}