@@ -34,6 +34,25 @@ const FILTER_SUGGESTIONS = [
34
34
} ,
35
35
] ;
36
36
37
+ // Date range suggestions for quick date filtering
38
+ const DATE_RANGE_SUGGESTIONS = [
39
+ {
40
+ id : 'thismonth' ,
41
+ label : 'Denne måneden' ,
42
+ sql : `TIMESTAMP(DATE_TRUNC(CURRENT_DATE(), MONTH))`
43
+ } ,
44
+ {
45
+ id : 'lastmonth' ,
46
+ label : 'Forrige måned' ,
47
+ sql : `TIMESTAMP(DATE_TRUNC(DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH), MONTH))`
48
+ } ,
49
+ {
50
+ id : 'thisyear' ,
51
+ label : 'I år' ,
52
+ sql : `TIMESTAMP(DATE_TRUNC(CURRENT_DATE(), YEAR))`
53
+ }
54
+ ] ;
55
+
37
56
// Modified interface to receive date range info
38
57
interface ChartFiltersProps {
39
58
filters : Filter [ ] ;
@@ -54,6 +73,8 @@ const ChartFilters = ({
54
73
const [ customPeriodInputs , setCustomPeriodInputs ] = useState < Record < number , { amount : string , unit : string } > > ( { } ) ;
55
74
// Change to store single string instead of array
56
75
const [ appliedSuggestion , setAppliedSuggestion ] = useState < string > ( '' ) ;
76
+ // Add state for selected date range
77
+ const [ selectedDateRange , setSelectedDateRange ] = useState < string > ( '' ) ;
57
78
58
79
// Change addFilter to accept a column parameter
59
80
const addFilter = ( column : string ) => {
@@ -62,6 +83,89 @@ const ChartFilters = ({
62
83
}
63
84
} ;
64
85
86
+ // Find the date filter index in the filters array
87
+ const getDateFilterIndex = ( ) : number => {
88
+ return filters . findIndex ( filter => filter . column === 'created_at' ) ;
89
+ } ;
90
+
91
+ // Apply a date range filter
92
+ const applyDateRange = ( rangeId : string ) => {
93
+ if ( rangeId === 'all' ) {
94
+ // "Hele perioden" option - remove any existing date filters
95
+ setSelectedDateRange ( 'all' ) ;
96
+ const newFilters = filters . filter ( f => f . column !== 'created_at' ) ;
97
+ setFilters ( newFilters ) ;
98
+ return ;
99
+ }
100
+
101
+ if ( selectedDateRange === rangeId ) {
102
+ // Deselect current date range - go back to "Hele perioden"
103
+ setSelectedDateRange ( 'all' ) ;
104
+
105
+ // Remove the date filter
106
+ const newFilters = filters . filter ( f => f . column !== 'created_at' ) ;
107
+ setFilters ( newFilters ) ;
108
+ } else {
109
+ // Select the new date range
110
+ setSelectedDateRange ( rangeId ) ;
111
+
112
+ if ( rangeId === 'custom' ) {
113
+ // Special case for custom period
114
+ const dateFilterIndex = getDateFilterIndex ( ) ;
115
+ const defaultDays = Math . min ( 7 , maxDaysAvailable ) ;
116
+ const sql = `TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -${ defaultDays } DAY)` ;
117
+
118
+ if ( dateFilterIndex >= 0 ) {
119
+ // Update existing date filter
120
+ updateFilter ( dateFilterIndex , {
121
+ operator : '>=' ,
122
+ value : sql ,
123
+ dateRangeType : 'custom'
124
+ } ) ;
125
+ setCustomPeriodInputs ( prev => ( {
126
+ ...prev ,
127
+ [ dateFilterIndex ] : { amount : defaultDays . toString ( ) , unit : 'DAY' }
128
+ } ) ) ;
129
+ } else {
130
+ // Add new date filter
131
+ const newFilter = {
132
+ column : 'created_at' ,
133
+ operator : '>=' ,
134
+ value : sql ,
135
+ dateRangeType : 'custom'
136
+ } ;
137
+ setFilters ( [ ...filters , newFilter ] ) ;
138
+
139
+ // Initialize custom period inputs for the new filter
140
+ // This will be picked up by useEffect
141
+ }
142
+ } else {
143
+ // Regular preset date range
144
+ const dateRange = DATE_RANGE_SUGGESTIONS . find ( dr => dr . id === rangeId ) ;
145
+ if ( ! dateRange ) return ;
146
+
147
+ const dateFilterIndex = getDateFilterIndex ( ) ;
148
+
149
+ if ( dateFilterIndex >= 0 ) {
150
+ // Update existing date filter
151
+ updateFilter ( dateFilterIndex , {
152
+ operator : '>=' ,
153
+ value : dateRange . sql ,
154
+ dateRangeType : 'preset'
155
+ } ) ;
156
+ } else {
157
+ // Add new date filter
158
+ setFilters ( [ ...filters , {
159
+ column : 'created_at' ,
160
+ operator : '>=' ,
161
+ value : dateRange . sql ,
162
+ dateRangeType : 'preset'
163
+ } ] ) ;
164
+ }
165
+ }
166
+ }
167
+ } ;
168
+
65
169
const removeFilter = ( index : number ) => {
66
170
const filterToRemove = filters [ index ] ;
67
171
// Check if this filter was added by a suggestion
@@ -76,6 +180,12 @@ const ChartFilters = ({
76
180
if ( isSuggestionFilter ) {
77
181
setAppliedSuggestion ( '' ) ;
78
182
}
183
+
184
+ // If removing date filter, clear date range selection
185
+ if ( filterToRemove . column === 'created_at' ) {
186
+ setSelectedDateRange ( '' ) ;
187
+ }
188
+
79
189
setFilters ( filters . filter ( ( _ , i ) => i !== index ) ) ;
80
190
} ;
81
191
@@ -148,6 +258,15 @@ const ChartFilters = ({
148
258
useEffect ( ( ) => {
149
259
filters . forEach ( ( filter , index ) => {
150
260
if ( filter . column === 'created_at' && ! customPeriodInputs [ index ] ) {
261
+ // If it's a preset date range, find and select it
262
+ if ( filter . dateRangeType === 'preset' ) {
263
+ const matchingRange = DATE_RANGE_SUGGESTIONS . find ( dr => dr . sql === filter . value ) ;
264
+ if ( matchingRange ) {
265
+ setSelectedDateRange ( matchingRange . id ) ;
266
+ return ;
267
+ }
268
+ }
269
+
151
270
// Set a sensible default - 7 days or maxDaysAvailable, whichever is smaller
152
271
const defaultDays = Math . min ( 7 , maxDaysAvailable ) ;
153
272
setCustomPeriodInputs ( prev => ( {
@@ -159,7 +278,8 @@ const ChartFilters = ({
159
278
const sql = `TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -${ defaultDays } DAY)` ;
160
279
updateFilter ( index , {
161
280
operator : '>=' , // Default to >= for date filters
162
- value : sql
281
+ value : sql ,
282
+ dateRangeType : 'custom'
163
283
} ) ;
164
284
}
165
285
}
@@ -168,6 +288,9 @@ const ChartFilters = ({
168
288
169
289
// Update custom period values
170
290
const updateCustomPeriod = ( index : number , field : 'amount' | 'unit' , value : string ) => {
291
+ // Clear selected date range when using custom period
292
+ setSelectedDateRange ( '' ) ;
293
+
171
294
const currentValues = customPeriodInputs [ index ] || { amount : '7' , unit : 'DAY' } ;
172
295
const newValues = { ...currentValues , [ field ] : value } ;
173
296
setCustomPeriodInputs ( {
@@ -186,7 +309,7 @@ const ChartFilters = ({
186
309
sql = `TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -${ amount * 7 } DAY)` ;
187
310
break ;
188
311
case 'MONTH' :
189
- // Use DATE_ADD and convert to TIMESTAMP for consistent typing
312
+ // Use DATE_ADD and convert to TIMESTAMP for consistent typingent typing
190
313
sql = `TIMESTAMP(DATE_SUB(CURRENT_DATE(), INTERVAL ${ amount } MONTH))` ;
191
314
break ;
192
315
default :
@@ -195,10 +318,39 @@ const ChartFilters = ({
195
318
}
196
319
197
320
updateFilter ( index , {
198
- value : sql
321
+ value : sql ,
322
+ dateRangeType : 'custom'
199
323
} ) ;
200
324
} ;
201
325
326
+ // Add helper to check if custom period is active
327
+ const isCustomPeriodActive = ( ) : boolean => {
328
+ return filters . some ( filter =>
329
+ filter . column === 'created_at' &&
330
+ filter . dateRangeType === 'custom'
331
+ ) ;
332
+ } ;
333
+
334
+ // Convert max days available to a specific date
335
+ const getStartDateDisplay = ( ) : string => {
336
+ if ( ! maxDaysAvailable ) return 'Velg nettside for å se tilgjengelig data.' ;
337
+
338
+ const startDate = new Date ( ) ;
339
+ startDate . setDate ( startDate . getDate ( ) - maxDaysAvailable ) ;
340
+
341
+ // Format date as DD.MM.YYYY (Norwegian format)
342
+ const day = String ( startDate . getDate ( ) ) . padStart ( 2 , '0' ) ;
343
+ const month = String ( startDate . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
344
+ const year = startDate . getFullYear ( ) ;
345
+
346
+ return `Data er tilgjengelig fra ${ day } .${ month } .${ year } til i dag.` ;
347
+ } ;
348
+
349
+ // Update to check if any date filter exists
350
+ const hasDateFilter = ( ) : boolean => {
351
+ return filters . some ( filter => filter . column === 'created_at' ) ;
352
+ } ;
353
+
202
354
return (
203
355
< section >
204
356
< Heading level = "2" size = "small" spacing >
@@ -240,6 +392,55 @@ const ChartFilters = ({
240
392
</ div >
241
393
</ div >
242
394
395
+ { /* Date Range Quick Picker */ }
396
+ < div className = "mb-4" >
397
+ < Heading level = "3" size = "xsmall" spacing >
398
+ Datoområde
399
+ </ Heading >
400
+
401
+ < div className = "flex flex-wrap gap-2 mt-2" >
402
+ { /* Add "Hele perioden" button as the first option */ }
403
+ < button
404
+ className = { `px-3 py-2 rounded-md text-sm border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 ${
405
+ ! hasDateFilter ( ) || selectedDateRange === 'all'
406
+ ? 'bg-blue-600 text-white border-blue-700'
407
+ : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
408
+ } `}
409
+ onClick = { ( ) => applyDateRange ( 'all' ) }
410
+ >
411
+ Alt
412
+ </ button >
413
+ { DATE_RANGE_SUGGESTIONS . map ( ( dateRange ) => (
414
+ < button
415
+ key = { dateRange . id }
416
+ className = { `px-3 py-2 rounded-md text-sm border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 ${
417
+ selectedDateRange === dateRange . id
418
+ ? 'bg-blue-600 text-white border-blue-700'
419
+ : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
420
+ } `}
421
+ onClick = { ( ) => applyDateRange ( dateRange . id ) }
422
+ >
423
+ { dateRange . label }
424
+ </ button >
425
+ ) ) }
426
+ < button
427
+ className = { `px-3 py-2 rounded-md text-sm border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 ${
428
+ isCustomPeriodActive ( )
429
+ ? 'bg-blue-600 text-white border-blue-700'
430
+ : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
431
+ } `}
432
+ onClick = { ( ) => applyDateRange ( 'custom' ) }
433
+ >
434
+ Egendefinert
435
+ </ button >
436
+ </ div >
437
+
438
+ { /* Show info about available date range with actual date */ }
439
+ < div className = "mt-2 text-xs text-gray-600" >
440
+ { getStartDateDisplay ( ) }
441
+ </ div >
442
+ </ div >
443
+
243
444
{ /* Static Filters */ }
244
445
< div className = "mt-6" >
245
446
< Heading level = "3" size = "xsmall" spacing >
@@ -249,8 +450,8 @@ const ChartFilters = ({
249
450
Statiske filtre er låst til grafen eller tabellen du lager.
250
451
</ p >
251
452
252
- { /* Replace button with dropdown and button combo like in Summarize.tsx */ }
253
- < div className = "flex gap-2 items-center bg-white p-3 rounded-md border" >
453
+ { /* Replace button with dropdown and button combo like in Summarize.tsx */ }
454
+ < div className = "flex gap-2 items-center bg-white p-3 rounded-md border" >
254
455
< Select
255
456
label = "Legg til filter"
256
457
onChange = { ( e ) => {
@@ -371,40 +572,63 @@ const ChartFilters = ({
371
572
{ /* Simplified Date Input for created_at */ }
372
573
{ filter . column === 'created_at' && (
373
574
< div className = "mt-3" >
374
- < div className = "flex items-end gap-2" >
375
- < TextField
376
- label = "Tid tilbake"
377
- value = { customPeriodInputs [ index ] ?. amount || '7' }
378
- onChange = { ( e ) => updateCustomPeriod ( index , 'amount' , e . target . value ) }
379
- type = "number"
380
- min = "1"
381
- max = { ( customPeriodInputs [ index ] ?. unit || 'DAY' ) === 'DAY' ? maxDaysAvailable : undefined }
382
- size = "small"
383
- className = "w-24"
384
- />
385
- < Select
386
- label = "Tidsenhet"
387
- value = { customPeriodInputs [ index ] ?. unit || 'DAY' }
388
- onChange = { ( e ) => updateCustomPeriod ( index , 'unit' , e . target . value ) }
389
- size = "small"
390
- >
391
- { TIME_UNITS . map ( unit => (
392
- < option key = { unit . value } value = { unit . value } >
393
- { unit . label }
394
- </ option >
395
- ) ) }
396
- </ Select >
397
- < div className = "text-sm self-center ml-2" >
398
- fra nåværende tidspunkt
575
+ { filter . dateRangeType === 'preset' ? (
576
+ < div className = "flex items-center" >
577
+ < div className = "text-sm text-blue-600 font-medium" >
578
+ Bruker forhåndsdefinert periode: { " " }
579
+ { DATE_RANGE_SUGGESTIONS . find ( dr => dr . id === selectedDateRange ) ?. label || 'Egendefinert' }
580
+ </ div >
581
+ < Button
582
+ variant = "tertiary-neutral"
583
+ size = "small"
584
+ onClick = { ( ) => {
585
+ // Switch back to custom mode
586
+ setSelectedDateRange ( '' ) ;
587
+ const defaultDays = Math . min ( 7 , maxDaysAvailable ) ;
588
+ const sql = `TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -${ defaultDays } DAY)` ;
589
+ updateFilter ( index , {
590
+ value : sql ,
591
+ dateRangeType : 'custom'
592
+ } ) ;
593
+ setCustomPeriodInputs ( prev => ( {
594
+ ...prev ,
595
+ [ index ] : { amount : defaultDays . toString ( ) , unit : 'DAY' }
596
+ } ) ) ;
597
+ } }
598
+ className = "ml-2"
599
+ >
600
+ Tilpass
601
+ </ Button >
399
602
</ div >
400
- </ div >
401
- { /* Add info about available date range */ }
402
- < div className = "mt-2 text-xs text-gray-600" >
403
- { maxDaysAvailable ?
404
- `Data er tilgjengelig for de siste ${ maxDaysAvailable } dagene.` :
405
- 'Velg nettside for å se tilgjengelig data.'
406
- }
407
- </ div >
603
+ ) : (
604
+ < div className = "flex items-end gap-2" >
605
+ < TextField
606
+ label = "Tid tilbake"
607
+ value = { customPeriodInputs [ index ] ?. amount || '7' }
608
+ onChange = { ( e ) => updateCustomPeriod ( index , 'amount' , e . target . value ) }
609
+ type = "number"
610
+ min = "1"
611
+ max = { ( customPeriodInputs [ index ] ?. unit || 'DAY' ) === 'DAY' ? maxDaysAvailable : undefined }
612
+ size = "small"
613
+ className = "w-24"
614
+ />
615
+ < Select
616
+ label = "Tidsenhet"
617
+ value = { customPeriodInputs [ index ] ?. unit || 'DAY' }
618
+ onChange = { ( e ) => updateCustomPeriod ( index , 'unit' , e . target . value ) }
619
+ size = "small"
620
+ >
621
+ { TIME_UNITS . map ( unit => (
622
+ < option key = { unit . value } value = { unit . value } >
623
+ { unit . label }
624
+ </ option >
625
+ ) ) }
626
+ </ Select >
627
+ < div className = "text-sm self-center ml-2" >
628
+ fra nåværende tidspunkt
629
+ </ div >
630
+ </ div >
631
+ ) }
408
632
</ div >
409
633
) }
410
634
{ /* Event name combobox on its own row */ }
0 commit comments