@@ -3,7 +3,12 @@ import PivotTable from "./PivotTable";
33import Select from "./Select" ;
44import Button from "./Button" ;
55import { state } from "../App" ;
6- import { useEffect } from "react" ;
6+ import { type FilterConfig } from "../types/filters" ;
7+ import {
8+ getFieldType ,
9+ getOperatorsForField ,
10+ createFilterFunction ,
11+ } from "../util/filter-utils" ;
712
813interface FieldSelectorProps {
914 title : string ;
@@ -126,112 +131,132 @@ const AggregatorSelector = ({
126131 </ div >
127132) ;
128133
134+ // Reusable filter input component
135+ const FilterInput = ( {
136+ filter,
137+ index,
138+ onUpdate,
139+ } : {
140+ filter : FilterConfig ;
141+ index : number ;
142+ onUpdate : ( updates : Partial < FilterConfig > ) => void ;
143+ } ) => {
144+ const fieldType = filter . type || getFieldType ( filter . field ) ;
145+
146+ if ( fieldType === "date" ) {
147+ return (
148+ < div className = "flex space-x-2" >
149+ < input
150+ type = "date"
151+ value = { filter . value }
152+ onChange = { ( e ) => onUpdate ( { value : e . target . value } ) }
153+ className = "px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-gray-500 min-w-32"
154+ />
155+ { filter . operator === "between" && (
156+ < input
157+ type = "date"
158+ value = { filter . value2 || "" }
159+ onChange = { ( e ) => onUpdate ( { value2 : e . target . value } ) }
160+ className = "px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-gray-500 min-w-32"
161+ placeholder = "End date"
162+ />
163+ ) }
164+ </ div >
165+ ) ;
166+ }
167+
168+ return (
169+ < input
170+ type = "text"
171+ value = { filter . value }
172+ onChange = { ( e ) => onUpdate ( { value : e . target . value } ) }
173+ placeholder = "Value"
174+ className = "px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-gray-500 min-w-32"
175+ />
176+ ) ;
177+ } ;
178+
129179const FilterSelector = ( {
130180 filters,
131181 onFiltersChange,
132182 availableKeys,
133183} : {
134- filters : Array < { field : string ; operator : string ; value : string } > ;
135- onFiltersChange : (
136- filters : Array < { field : string ; operator : string ; value : string } >
137- ) => void ;
184+ filters : FilterConfig [ ] ;
185+ onFiltersChange : ( filters : FilterConfig [ ] ) => void ;
138186 availableKeys : string [ ] ;
139187} ) => {
140188 const addFilter = ( ) => {
141189 onFiltersChange ( [
142190 ...filters ,
143- { field : "" , operator : "contains" , value : "" } ,
191+ { field : "" , operator : "contains" , value : "" , type : "text" } ,
144192 ] ) ;
145193 } ;
146194
147195 const removeFilter = ( index : number ) => {
148196 onFiltersChange ( filters . filter ( ( _ , i ) => i !== index ) ) ;
149197 } ;
150198
151- const updateFilter = (
152- index : number ,
153- field : string ,
154- operator : string ,
155- value : string
156- ) => {
199+ const updateFilter = ( index : number , updates : Partial < FilterConfig > ) => {
157200 const newFilters = [ ...filters ] ;
158- newFilters [ index ] = { field , operator , value } ;
201+ newFilters [ index ] = { ... newFilters [ index ] , ... updates } ;
159202 onFiltersChange ( newFilters ) ;
160203 } ;
161204
162- const operators = [
163- { value : "==" , label : "equals" } ,
164- { value : "!=" , label : "not equals" } ,
165- { value : ">" , label : "greater than" } ,
166- { value : "<" , label : "less than" } ,
167- { value : ">=" , label : "greater than or equal" } ,
168- { value : "<=" , label : "less than or equal" } ,
169- { value : "contains" , label : "contains" } ,
170- { value : "!contains" , label : "not contains" } ,
171- ] ;
172-
173205 return (
174206 < div className = "mb-4" >
175207 < div className = "text-xs font-medium text-gray-700 mb-2" > Filters:</ div >
176208 < div className = "space-y-2" >
177- { filters . map ( ( filter , index ) => (
178- < div key = { index } className = "flex items-center space-x-2" >
179- < Select
180- value = { filter . field }
181- onChange = { ( e ) =>
182- updateFilter (
183- index ,
184- e . target . value ,
185- filter . operator ,
186- filter . value
187- )
188- }
189- size = "sm"
190- className = "min-w-48"
191- >
192- < option value = "" > Select a field...</ option >
193- { availableKeys ?. map ( ( key ) => (
194- < option key = { key } value = { key } >
195- { key }
196- </ option >
197- ) ) }
198- </ Select >
199- < Select
200- value = { filter . operator }
201- onChange = { ( e ) =>
202- updateFilter ( index , filter . field , e . target . value , filter . value )
203- }
204- size = "sm"
205- className = "min-w-32"
206- >
207- { operators . map ( ( op ) => (
208- < option key = { op . value } value = { op . value } >
209- { op . label }
210- </ option >
211- ) ) }
212- </ Select >
213- < input
214- type = "text"
215- value = { filter . value }
216- onChange = { ( e ) =>
217- updateFilter (
218- index ,
219- filter . field ,
220- filter . operator ,
221- e . target . value
222- )
223- }
224- placeholder = "Value"
225- className = "px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-gray-500 min-w-32"
226- />
227- < button
228- onClick = { ( ) => removeFilter ( index ) }
229- className = "text-xs text-red-600 hover:text-red-800 px-2 py-1"
230- >
231- Remove
232- </ button >
233- </ div >
234- ) ) }
209+ { filters . map ( ( filter , index ) => {
210+ const fieldType = filter . type || getFieldType ( filter . field ) ;
211+ const operators = getOperatorsForField ( filter . field , fieldType ) ;
212+
213+ return (
214+ < div key = { index } className = "flex items-center space-x-2" >
215+ < Select
216+ value = { filter . field }
217+ onChange = { ( e ) => {
218+ const newField = e . target . value ;
219+ const newType = getFieldType ( newField ) ;
220+ updateFilter ( index , { field : newField , type : newType } ) ;
221+ } }
222+ size = "sm"
223+ className = "min-w-48"
224+ >
225+ < option value = "" > Select a field...</ option >
226+ { availableKeys ?. map ( ( key ) => (
227+ < option key = { key } value = { key } >
228+ { key }
229+ </ option >
230+ ) ) }
231+ </ Select >
232+ < Select
233+ value = { filter . operator }
234+ onChange = { ( e ) =>
235+ updateFilter ( index , { operator : e . target . value } )
236+ }
237+ size = "sm"
238+ className = "min-w-32"
239+ >
240+ { operators . map ( ( op ) => (
241+ < option key = { op . value } value = { op . value } >
242+ { op . label }
243+ </ option >
244+ ) ) }
245+ </ Select >
246+ < FilterInput
247+ filter = { filter }
248+ index = { index }
249+ onUpdate = { ( updates ) => updateFilter ( index , updates ) }
250+ />
251+ < button
252+ onClick = { ( ) => removeFilter ( index ) }
253+ className = "text-xs text-red-600 hover:text-red-800 px-2 py-1"
254+ >
255+ Remove
256+ </ button >
257+ </ div >
258+ ) ;
259+ } ) }
235260 < button
236261 onClick = { addFilter }
237262 className = "text-xs text-blue-600 hover:text-blue-800 px-2 py-1"
@@ -244,10 +269,8 @@ const FilterSelector = ({
244269} ;
245270
246271const PivotTab = observer ( ( ) => {
247- // Use global state instead of local state
248272 const { pivotConfig } = state ;
249273
250- // Update global state when configuration changes
251274 const updateRowFields = ( index : number , value : string ) => {
252275 const newRowFields = [ ...pivotConfig . selectedRowFields ] ;
253276 newRowFields [ index ] = value ;
@@ -268,9 +291,7 @@ const PivotTab = observer(() => {
268291 state . updatePivotConfig ( { selectedAggregator : value } ) ;
269292 } ;
270293
271- const updateFilters = (
272- filters : Array < { field : string ; operator : string ; value : string } >
273- ) => {
294+ const updateFilters = ( filters : FilterConfig [ ] ) => {
274295 state . updatePivotConfig ( { filters } ) ;
275296 } ;
276297
@@ -304,47 +325,6 @@ const PivotTab = observer(() => {
304325
305326 const availableKeys = state . flattenedDatasetKeys ;
306327
307- // Create filter function from filter configuration
308- const createFilterFunction = (
309- filters : Array < { field : string ; operator : string ; value : string } >
310- ) => {
311- if ( filters . length === 0 ) return undefined ;
312-
313- return ( record : any ) => {
314- return filters . every ( ( filter ) => {
315- if ( ! filter . field || ! filter . value ) return true ; // Skip incomplete filters
316-
317- const fieldValue = record [ filter . field ] ;
318- const filterValue = filter . value ;
319-
320- switch ( filter . operator ) {
321- case "==" :
322- return String ( fieldValue ) === filterValue ;
323- case "!=" :
324- return String ( fieldValue ) !== filterValue ;
325- case ">" :
326- return Number ( fieldValue ) > Number ( filterValue ) ;
327- case "<" :
328- return Number ( fieldValue ) < Number ( filterValue ) ;
329- case ">=" :
330- return Number ( fieldValue ) >= Number ( filterValue ) ;
331- case "<=" :
332- return Number ( fieldValue ) <= Number ( filterValue ) ;
333- case "contains" :
334- return String ( fieldValue )
335- . toLowerCase ( )
336- . includes ( filterValue . toLowerCase ( ) ) ;
337- case "!contains" :
338- return ! String ( fieldValue )
339- . toLowerCase ( )
340- . includes ( filterValue . toLowerCase ( ) ) ;
341- default :
342- return true ;
343- }
344- } ) ;
345- } ;
346- } ;
347-
348328 return (
349329 < div >
350330 < div className = "text-xs text-gray-600 mb-2 max-w-2xl" >
0 commit comments