nest-filter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,453 @@
1
+ import * as React from "react";
2
+ import { useState, useEffect, useCallback } from "react";
3
+ import {
4
+ FilterRule,
5
+ FilterGroup,
6
+ ColumnDefinition,
7
+ LogicalOperator,
8
+ FilterOperator,
9
+ FilterItem,
10
+ } from "../types";
11
+ import { Button } from "./ui/Button";
12
+ import { Input } from "./ui/Input";
13
+ import { Select } from "./ui/Select";
14
+ import { Dialog } from "./ui/Dialog";
15
+ import {
16
+ Plus,
17
+ Trash2,
18
+ Layers,
19
+ Binary,
20
+ FilterX,
21
+ ListFilter,
22
+ } from "lucide-react";
23
+ import { applyFilters } from "../utils/filterLogic";
24
+
25
+ interface AdvancedFilterProps<T> {
26
+ isOpen: boolean;
27
+ onClose: () => void;
28
+ data: T[];
29
+ columns: ColumnDefinition<T>[];
30
+ setFilteredData: (data: T[]) => void;
31
+ initialFilters?: FilterGroup<T>;
32
+ }
33
+
34
+ const getOperatorsForType = (
35
+ type: string
36
+ ): { value: FilterOperator; label: string }[] => {
37
+ switch (type) {
38
+ case "string":
39
+ return [
40
+ { value: "contains", label: "Contains" },
41
+ { value: "not_contains", label: "Does not contain" },
42
+ { value: "equals", label: "Exact match" },
43
+ ];
44
+ case "select":
45
+ return [
46
+ { value: "equals", label: "Is" },
47
+ { value: "not_equals", label: "Is not" },
48
+ ];
49
+ case "number":
50
+ return [
51
+ { value: "equals", label: "=" },
52
+ { value: "not_equals", label: "!=" },
53
+ { value: "gt", label: ">" },
54
+ { value: "lt", label: "<" },
55
+ { value: "gte", label: ">=" },
56
+ { value: "lte", label: "<=" },
57
+ ];
58
+ case "date":
59
+ return [
60
+ { value: "is", label: "On date" },
61
+ { value: "before", label: "Before" },
62
+ { value: "after", label: "After" },
63
+ ];
64
+ case "boolean":
65
+ return [
66
+ { value: "true", label: "True" },
67
+ { value: "false", label: "False" },
68
+ ];
69
+ default:
70
+ return [];
71
+ }
72
+ };
73
+
74
+ const FilterGroupUI = <T,>({
75
+ group,
76
+ depth,
77
+ columns,
78
+ onAddRule,
79
+ onAddGroup,
80
+ onUpdateRule,
81
+ onUpdateGroupLogic,
82
+ onRemoveItem,
83
+ }: {
84
+ group: FilterGroup<T>;
85
+ depth: number;
86
+ columns: ColumnDefinition<T>[];
87
+ onAddRule: (parentId: string) => void;
88
+ onAddGroup: (parentId: string) => void;
89
+ onUpdateRule: (ruleId: string, updates: Partial<FilterRule<T>>) => void;
90
+ onUpdateGroupLogic: (groupId: string, logic: LogicalOperator) => void;
91
+ onRemoveItem: (itemId: string) => void;
92
+ }) => {
93
+ return (
94
+ <div
95
+ className={`rounded-lg border border-slate-200 bg-slate-50/50 p-4 mb-4 ${
96
+ depth > 0 ? "ml-6 md:ml-10 relative border-l-2 border-l-slate-300" : ""
97
+ }`}
98
+ >
99
+ <div className="flex flex-wrap items-center justify-between gap-4 mb-4">
100
+ <div className="flex items-center gap-2">
101
+ <span className="text-[10px] font-bold uppercase tracking-widest text-slate-500">
102
+ Group Logic
103
+ </span>
104
+ <div className="flex rounded-md border border-slate-200 bg-white p-1 shadow-sm">
105
+ <button
106
+ onClick={() => onUpdateGroupLogic(group.id, "AND")}
107
+ className={`px-3 py-1 text-[10px] font-black rounded-sm transition-all ${
108
+ group.logic === "AND"
109
+ ? "bg-slate-900 text-slate-50"
110
+ : "text-slate-500 hover:text-slate-900"
111
+ }`}
112
+ >
113
+ AND
114
+ </button>
115
+ <button
116
+ onClick={() => onUpdateGroupLogic(group.id, "OR")}
117
+ className={`px-3 py-1 text-[10px] font-black rounded-sm transition-all ${
118
+ group.logic === "OR"
119
+ ? "bg-slate-900 text-slate-50"
120
+ : "text-slate-500 hover:text-slate-900"
121
+ }`}
122
+ >
123
+ OR
124
+ </button>
125
+ </div>
126
+ </div>
127
+
128
+ <div className="flex items-center gap-2">
129
+ <Button
130
+ variant="outline"
131
+ size="sm"
132
+ onClick={() => onAddRule(group.id)}
133
+ className="h-7 text-[10px] font-bold uppercase"
134
+ >
135
+ <Plus className="mr-1 h-3 w-3" /> Rule
136
+ </Button>
137
+ <Button
138
+ variant="outline"
139
+ size="sm"
140
+ onClick={() => onAddGroup(group.id)}
141
+ className="h-7 text-[10px] font-bold uppercase"
142
+ >
143
+ <Layers className="mr-1 h-3 w-3" /> Group
144
+ </Button>
145
+ {depth > 0 && (
146
+ <Button
147
+ variant="ghost"
148
+ size="sm"
149
+ onClick={() => onRemoveItem(group.id)}
150
+ className="text-slate-400"
151
+ >
152
+ <Trash2 className="h-5 w-5 text-red-500" />
153
+ </Button>
154
+ )}
155
+ </div>
156
+ </div>
157
+
158
+ <div className="space-y-3">
159
+ {group.items.map((item) => {
160
+ const column = columns.find(
161
+ (c) => c.id === (item.type === "rule" ? item.columnId : null)
162
+ );
163
+ return item.type === "rule" ? (
164
+ <div
165
+ key={item.id}
166
+ className="flex flex-col md:flex-row md:items-center gap-3 p-3 bg-white border border-slate-200 rounded-md shadow-sm"
167
+ >
168
+ <div className="flex-1 min-w-[140px]">
169
+ <Select
170
+ value={item.columnId as string}
171
+ options={columns.map((c) => ({
172
+ value: c.id as string,
173
+ label: c.label,
174
+ }))}
175
+ onValueChange={(val) => {
176
+ const colId = val as keyof T;
177
+ const col = columns.find((c) => c.id === colId);
178
+ onUpdateRule(item.id, {
179
+ columnId: colId,
180
+ operator:
181
+ col?.type === "date"
182
+ ? "is"
183
+ : col?.type === "number"
184
+ ? "equals"
185
+ : col?.type === "select"
186
+ ? "equals"
187
+ : "contains",
188
+ value: "",
189
+ });
190
+ }}
191
+ />
192
+ </div>
193
+ <div className="w-full md:w-44">
194
+ <Select
195
+ value={item.operator}
196
+ options={getOperatorsForType(column?.type || "string")}
197
+ onValueChange={(val) =>
198
+ onUpdateRule(item.id, { operator: val as FilterOperator })
199
+ }
200
+ />
201
+ </div>
202
+ <div className="flex-[1.5] min-w-[180px]">
203
+ {column?.type === "date" ? (
204
+ <Input
205
+ type="date"
206
+ value={item.value || ""}
207
+ onChange={(e) =>
208
+ onUpdateRule(item.id, { value: e.target.value })
209
+ }
210
+ />
211
+ ) : column?.type === "boolean" ? (
212
+ <div className="h-10 flex items-center px-3 border border-slate-200 rounded-md bg-slate-50 text-[10px] font-black uppercase tracking-tighter text-slate-400 italic">
213
+ <Binary className="h-3 w-3 mr-2" />
214
+ Binary state
215
+ </div>
216
+ ) : column?.type === "select" ? (
217
+ <Select
218
+ value={item.value || ""}
219
+ placeholder="Choose option..."
220
+ options={(column.options || []).map((opt) => ({
221
+ value: opt,
222
+ label: opt,
223
+ }))}
224
+ onValueChange={(val) =>
225
+ onUpdateRule(item.id, { value: val })
226
+ }
227
+ />
228
+ ) : (
229
+ <Input
230
+ placeholder="Search query..."
231
+ value={item.value || ""}
232
+ onChange={(e) =>
233
+ onUpdateRule(item.id, { value: e.target.value })
234
+ }
235
+ />
236
+ )}
237
+ </div>
238
+ <Button
239
+ variant="ghost"
240
+ size="icon"
241
+ onClick={() => onRemoveItem(item.id)}
242
+ className="h-9 w-9 text-slate-300 hover:text-red-500 hover:bg-red-50"
243
+ >
244
+ <Trash2 className="h-5 w-5 text-red-500" />
245
+ </Button>
246
+ </div>
247
+ ) : (
248
+ <FilterGroupUI
249
+ group={item}
250
+ depth={depth + 1}
251
+ columns={columns}
252
+ onAddRule={onAddRule}
253
+ onAddGroup={onAddGroup}
254
+ onUpdateRule={onUpdateRule}
255
+ onUpdateGroupLogic={onUpdateGroupLogic}
256
+ onRemoveItem={onRemoveItem}
257
+ />
258
+ );
259
+ })}
260
+ {group.items.length === 0 && (
261
+ <div className="flex flex-col items-center justify-center py-6 rounded-md border border-dashed border-slate-200 bg-white/50">
262
+ <ListFilter className="h-5 w-5 text-slate-300 mb-1" />
263
+ <p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">
264
+ Add a rule to filter
265
+ </p>
266
+ </div>
267
+ )}
268
+ </div>
269
+ </div>
270
+ );
271
+ };
272
+
273
+ export const AdvancedFilter = <T,>({
274
+ isOpen,
275
+ onClose,
276
+ data,
277
+ columns,
278
+ setFilteredData,
279
+ initialFilters,
280
+ }: AdvancedFilterProps<T>) => {
281
+ const [rootGroup, setRootGroup] = useState<FilterGroup<T>>({
282
+ type: "group",
283
+ id: "root",
284
+ logic: "AND",
285
+ items: [],
286
+ });
287
+
288
+ useEffect(() => {
289
+ if (initialFilters) setRootGroup(initialFilters);
290
+ }, [initialFilters, isOpen]);
291
+
292
+ const updateItemRecursively = useCallback(
293
+ (
294
+ group: FilterGroup<T>,
295
+ targetId: string,
296
+ updater: (item: FilterItem<T>) => FilterItem<T> | null
297
+ ): FilterGroup<T> => {
298
+ if (group.id === targetId) return updater(group) as FilterGroup<T>;
299
+ return {
300
+ ...group,
301
+ items: group.items
302
+ .map((item) => {
303
+ if (item.id === targetId) return updater(item);
304
+ if (item.type === "group")
305
+ return updateItemRecursively(item, targetId, updater);
306
+ return item;
307
+ })
308
+ .filter(Boolean) as FilterItem<T>[],
309
+ };
310
+ },
311
+ []
312
+ );
313
+
314
+ const handleAddRule = useCallback(
315
+ (parentId: string) => {
316
+ const firstCol = columns[0];
317
+ const newRule: FilterRule<T> = {
318
+ type: "rule",
319
+ id: Math.random().toString(36).substr(2, 9),
320
+ columnId: firstCol.id,
321
+ operator:
322
+ firstCol.type === "string"
323
+ ? "contains"
324
+ : firstCol.type === "select"
325
+ ? "equals"
326
+ : "equals",
327
+ value: "",
328
+ };
329
+ setRootGroup((prev) =>
330
+ updateItemRecursively(prev, parentId, (group) => ({
331
+ ...(group as FilterGroup<T>),
332
+ items: [...(group as FilterGroup<T>).items, newRule],
333
+ }))
334
+ );
335
+ },
336
+ [columns, updateItemRecursively]
337
+ );
338
+
339
+ const handleAddGroup = useCallback(
340
+ (parentId: string) => {
341
+ const newGroup: FilterGroup<T> = {
342
+ type: "group",
343
+ id: Math.random().toString(36).substr(2, 9),
344
+ logic: "AND",
345
+ items: [],
346
+ };
347
+ setRootGroup((prev) =>
348
+ updateItemRecursively(prev, parentId, (group) => ({
349
+ ...(group as FilterGroup<T>),
350
+ items: [...(group as FilterGroup<T>).items, newGroup],
351
+ }))
352
+ );
353
+ },
354
+ [updateItemRecursively]
355
+ );
356
+
357
+ const handleUpdateRule = useCallback(
358
+ (ruleId: string, updates: Partial<FilterRule<T>>) => {
359
+ setRootGroup((prev) =>
360
+ updateItemRecursively(
361
+ prev,
362
+ ruleId,
363
+ (rule) => ({ ...rule, ...updates } as FilterRule<T>)
364
+ )
365
+ );
366
+ },
367
+ [updateItemRecursively]
368
+ );
369
+
370
+ const handleUpdateGroupLogic = useCallback(
371
+ (groupId: string, logic: LogicalOperator) => {
372
+ setRootGroup((prev) =>
373
+ updateItemRecursively(
374
+ prev,
375
+ groupId,
376
+ (group) => ({ ...group, logic } as FilterGroup<T>)
377
+ )
378
+ );
379
+ },
380
+ [updateItemRecursively]
381
+ );
382
+
383
+ const handleRemoveItem = useCallback(
384
+ (itemId: string) => {
385
+ setRootGroup((prev) => updateItemRecursively(prev, itemId, () => null));
386
+ },
387
+ [updateItemRecursively]
388
+ );
389
+
390
+ const handleApply = () => {
391
+ const filtered = applyFilters(data, rootGroup, columns);
392
+ setFilteredData(filtered);
393
+ onClose();
394
+ };
395
+
396
+ const handleClear = () => {
397
+ const emptyGroup: FilterGroup<T> = {
398
+ type: "group",
399
+ id: "root",
400
+ logic: "AND",
401
+ items: [],
402
+ };
403
+ setRootGroup(emptyGroup);
404
+ setFilteredData(data);
405
+ onClose();
406
+ };
407
+
408
+ return (
409
+ <Dialog
410
+ open={isOpen}
411
+ onOpenChange={onClose}
412
+ title="Advanced Filters"
413
+ description="Refine your dataset using structured logic and multi-type comparisons."
414
+ >
415
+ <div className="max-h-[60vh] overflow-y-auto px-1 custom-scrollbar pr-3 pb-32">
416
+ <FilterGroupUI
417
+ group={rootGroup}
418
+ depth={0}
419
+ columns={columns}
420
+ onAddRule={handleAddRule}
421
+ onAddGroup={handleAddGroup}
422
+ onUpdateRule={handleUpdateRule}
423
+ onUpdateGroupLogic={handleUpdateGroupLogic}
424
+ onRemoveItem={handleRemoveItem}
425
+ />
426
+ </div>
427
+ <div className="flex w-full justify-between items-center gap-4">
428
+ <Button
429
+ variant="ghost"
430
+ onClick={handleClear}
431
+ className="text-slate-400 hover:text-red-500 font-black text-[10px] uppercase tracking-widest gap-2"
432
+ >
433
+ <FilterX className="h-4 w-4" /> Reset Filters
434
+ </Button>
435
+ <div className="flex gap-2">
436
+ <Button
437
+ variant="outline"
438
+ onClick={onClose}
439
+ className="font-bold h-11 px-6"
440
+ >
441
+ Cancel
442
+ </Button>
443
+ <Button
444
+ onClick={handleApply}
445
+ className="bg-slate-900 font-bold h-11 px-10 rounded-lg shadow-lg hover:shadow-xl transition-shadow"
446
+ >
447
+ Apply View
448
+ </Button>
449
+ </div>
450
+ </div>
451
+ </Dialog>
452
+ );
453
+ };
@@ -0,0 +1,37 @@
1
+
2
+ import React from 'react';
3
+
4
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
6
+ size?: 'default' | 'sm' | 'lg' | 'icon';
7
+ }
8
+
9
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
10
+ ({ className = '', variant = 'default', size = 'default', ...props }, ref) => {
11
+ const baseStyles = 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50';
12
+
13
+ const variants = {
14
+ default: 'bg-slate-900 text-slate-50 hover:bg-slate-900/90',
15
+ destructive: 'bg-red-500 text-slate-50 hover:bg-red-500/90',
16
+ outline: 'border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900',
17
+ secondary: 'bg-slate-100 text-slate-900 hover:bg-slate-100/80',
18
+ ghost: 'hover:bg-slate-100 hover:text-slate-900',
19
+ link: 'text-slate-900 underline-offset-4 hover:underline',
20
+ };
21
+
22
+ const sizes = {
23
+ default: 'h-10 px-4 py-2',
24
+ sm: 'h-9 rounded-md px-3',
25
+ lg: 'h-11 rounded-md px-8',
26
+ icon: 'h-10 w-10',
27
+ };
28
+
29
+ return (
30
+ <button
31
+ ref={ref}
32
+ className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
33
+ {...props}
34
+ />
35
+ );
36
+ }
37
+ );
@@ -0,0 +1,42 @@
1
+
2
+ import React from 'react';
3
+ import { X } from 'lucide-react';
4
+
5
+ interface DialogProps {
6
+ open: boolean;
7
+ onOpenChange: (open: boolean) => void;
8
+ title: string;
9
+ description?: string;
10
+ children: React.ReactNode;
11
+ footer?: React.ReactNode;
12
+ }
13
+
14
+ export const Dialog: React.FC<DialogProps> = ({ open, onOpenChange, title, description, children, footer }) => {
15
+ if (!open) return null;
16
+
17
+ return (
18
+ <div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-slate-950/80 p-4 pt-[5vh] backdrop-blur-sm animate-in fade-in duration-200">
19
+ <div className="relative w-full max-w-4xl rounded-lg border border-slate-200 bg-white p-6 shadow-lg animate-in zoom-in-95 duration-200">
20
+ <div className="flex flex-col space-y-1.5 text-center sm:text-left mb-6">
21
+ <h2 className="text-lg font-semibold leading-none tracking-tight">{title}</h2>
22
+ {description && <p className="text-sm text-slate-500">{description}</p>}
23
+ <button
24
+ onClick={() => onOpenChange(false)}
25
+ className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 disabled:pointer-events-none"
26
+ >
27
+ <X className="h-4 w-4" />
28
+ <span className="sr-only">Close</span>
29
+ </button>
30
+ </div>
31
+ <div className="py-2">
32
+ {children}
33
+ </div>
34
+ {footer && (
35
+ <div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-6">
36
+ {footer}
37
+ </div>
38
+ )}
39
+ </div>
40
+ </div>
41
+ );
42
+ };
@@ -0,0 +1,15 @@
1
+
2
+ import React from 'react';
3
+
4
+ export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
+ ({ className = '', type, ...props }, ref) => {
6
+ return (
7
+ <input
8
+ type={type}
9
+ className={`flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
10
+ ref={ref}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+ );
@@ -0,0 +1,63 @@
1
+
2
+ import React, { useState, useRef, useEffect } from 'react';
3
+ import { ChevronDown, Check } from 'lucide-react';
4
+
5
+ interface SelectProps {
6
+ value: string;
7
+ onValueChange: (value: string) => void;
8
+ options: { value: string; label: string }[];
9
+ placeholder?: string;
10
+ className?: string;
11
+ }
12
+
13
+ export const Select: React.FC<SelectProps> = ({ value, onValueChange, options, placeholder = "Select...", className = "" }) => {
14
+ const [isOpen, setIsOpen] = useState(false);
15
+ const containerRef = useRef<HTMLDivElement>(null);
16
+ const selected = options.find(o => o.value === value);
17
+
18
+ useEffect(() => {
19
+ const handleOutside = (e: MouseEvent) => {
20
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) setIsOpen(false);
21
+ };
22
+ document.addEventListener('mousedown', handleOutside);
23
+ return () => document.removeEventListener('mousedown', handleOutside);
24
+ }, []);
25
+
26
+ return (
27
+ <div className={`relative w-full ${className}`} ref={containerRef} style={{ zIndex: isOpen ? 60 : 1 }}>
28
+ <button
29
+ type="button"
30
+ onClick={() => setIsOpen(!isOpen)}
31
+ className="flex h-10 w-full items-center justify-between rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors hover:border-slate-300"
32
+ >
33
+ <span className="truncate">{selected ? selected.label : placeholder}</span>
34
+ <ChevronDown className={`h-4 w-4 opacity-50 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} />
35
+ </button>
36
+ {isOpen && (
37
+ <div className="absolute top-full left-0 z-50 mt-1 min-w-[8rem] w-full max-h-[200px] overflow-y-auto rounded-md border border-slate-200 bg-white text-slate-950 shadow-xl animate-in fade-in zoom-in-95 duration-100">
38
+ <div className="p-1">
39
+ {options.length > 0 ? options.map((opt) => (
40
+ <button
41
+ key={opt.value}
42
+ onClick={() => {
43
+ onValueChange(opt.value);
44
+ setIsOpen(false);
45
+ }}
46
+ className={`relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-slate-100 hover:text-slate-900 transition-colors ${opt.value === value ? 'bg-slate-100 font-medium' : ''}`}
47
+ >
48
+ {opt.value === value && (
49
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
50
+ <Check className="h-4 w-4 text-slate-900" />
51
+ </span>
52
+ )}
53
+ <span className="truncate">{opt.label}</span>
54
+ </button>
55
+ )) : (
56
+ <div className="py-2 px-2 text-xs text-slate-400 italic text-center">No options</div>
57
+ )}
58
+ </div>
59
+ </div>
60
+ )}
61
+ </div>
62
+ );
63
+ };
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './components/AdvancedFilter'
package/src/types.ts ADDED
@@ -0,0 +1,51 @@
1
+
2
+ export type ColumnType = 'string' | 'number' | 'date' | 'boolean' | 'select';
3
+
4
+ export type LogicalOperator = 'AND' | 'OR';
5
+
6
+ export type FilterOperator =
7
+ | 'equals' | 'not_equals' | 'contains' | 'not_contains'
8
+ | 'gt' | 'lt' | 'gte' | 'lte'
9
+ | 'before' | 'after' | 'is'
10
+ | 'true' | 'false';
11
+
12
+ export interface FilterRule<T = any> {
13
+ type: 'rule';
14
+ id: string;
15
+ columnId: keyof T;
16
+ operator: FilterOperator;
17
+ value: any;
18
+ }
19
+
20
+ export interface FilterGroup<T = any> {
21
+ type: 'group';
22
+ id: string;
23
+ logic: LogicalOperator;
24
+ items: (FilterRule<T> | FilterGroup<T>)[];
25
+ }
26
+
27
+ export type FilterItem<T = any> = FilterRule<T> | FilterGroup<T>;
28
+
29
+ export interface ColumnDefinition<T = any> {
30
+ id: keyof T;
31
+ label: string;
32
+ type: ColumnType;
33
+ options?: string[]; // Used when type is 'select'
34
+ }
35
+
36
+ export interface TableData {
37
+ id: number;
38
+ cost_centre: string;
39
+ cost_centre_desc: string;
40
+ cost_centre_limit: string | number;
41
+ cost_centre_owner: string;
42
+ approving_authority: string;
43
+ created_by: string;
44
+ timestamp: string;
45
+ is_archive: boolean;
46
+ year: string;
47
+ expense_type: string;
48
+ is_freeze: boolean;
49
+ status?: string; // New field for selection testing
50
+ [key: string]: any;
51
+ }