Skip to content

Commit f122171

Browse files
committed
fix: add calendar filter
1 parent 77760dc commit f122171

7 files changed

Lines changed: 352 additions & 17 deletions

File tree

resources/views/analytics.blade.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@
88
</div>
99
<div class="w-full sm:w-auto">
1010
<form method="GET" action="{{ route(config('request-analytics.route.name')) }}" class="flex items-center gap-2 flex-wrap">
11-
<select name="date_range" class="bg-white border border-gray-300 rounded-lg px-3 py-2.5 pr-6 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 h-10">
12-
<option value="7" {{ $dateRange == 7 ? 'selected' : '' }}>Last 7 days</option>
13-
<option value="30" {{ $dateRange == 30 ? 'selected' : '' }}>Last 30 days</option>
14-
<option value="90" {{ $dateRange == 90 ? 'selected' : '' }}>Last 90 days</option>
15-
<option value="365" {{ $dateRange == 365 ? 'selected' : '' }}>Last year</option>
16-
</select>
11+
<x-request-analytics::core.calendar-filter
12+
:dateRange="$dateRange"
13+
:startDate="request('start_date')"
14+
:endDate="request('end_date')"
15+
/>
1716
<select name="request_category" class="bg-white border border-gray-300 rounded-lg px-3 py-2.5 pr-6 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 h-10">
1817
<option value="" {{ !request('request_category') ? 'selected' : '' }}>All Requests</option>
1918
<option value="web" {{ request('request_category') == 'web' ? 'selected' : '' }}>Web Only</option>
2019
<option value="api" {{ request('request_category') == 'api' ? 'selected' : '' }}>API Only</option>
2120
</select>
22-
<x-request-analytics::core.button type="submit" color="primary">Apply</x-request-analytics::core.button>
2321
</form>
2422
</div>
2523
</div>
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
@props([
2+
'dateRange' => 30,
3+
'startDate' => null,
4+
'endDate' => null
5+
])
6+
7+
@php
8+
$currentDate = now();
9+
$presetRanges = [
10+
'last_24_hours' => ['label' => 'Last 24 hours', 'key' => 'D'],
11+
'last_7_days' => ['label' => 'Last 7 days', 'key' => 'W'],
12+
'last_30_days' => ['label' => 'Last 30 days', 'key' => 'T'],
13+
'last_3_months' => ['label' => 'Last 3 months', 'key' => 'D'],
14+
'last_12_months' => ['label' => 'Last 12 months', 'key' => 'A'],
15+
'month_to_date' => ['label' => 'Month to Date', 'key' => 'M'],
16+
'quarter_to_date' => ['label' => 'Quarter to Date', 'key' => 'Q'],
17+
'year_to_date' => ['label' => 'Year to Date', 'key' => 'A']
18+
];
19+
@endphp
20+
21+
<div class="relative" x-data="calendarFilter()" x-init="init()">
22+
<!-- Main Filter Button -->
23+
<div class="flex items-center gap-2">
24+
<button
25+
type="button"
26+
@click="showPresets = !showPresets"
27+
class="inline-flex items-center gap-2 bg-white border border-gray-300 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 h-10 hover:bg-gray-50"
28+
>
29+
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
30+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
31+
</svg>
32+
<span x-text="currentLabel"></span>
33+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
35+
</svg>
36+
</button>
37+
</div>
38+
39+
<!-- Dropdown Overlay -->
40+
<div
41+
x-show="showPresets"
42+
x-transition:enter="transition ease-out duration-200"
43+
x-transition:enter-start="opacity-0 translate-y-1"
44+
x-transition:enter-end="opacity-100 translate-y-0"
45+
x-transition:leave="transition ease-in duration-150"
46+
x-transition:leave-start="opacity-100 translate-y-0"
47+
x-transition:leave-end="opacity-0 translate-y-1"
48+
@click.away="showPresets = false"
49+
class="absolute right-0 top-full mt-2 bg-white rounded-lg shadow-lg border border-gray-200 z-50 min-w-[800px]"
50+
x-cloak
51+
>
52+
<div class="flex">
53+
<!-- Left: Date Range Presets -->
54+
<div class="w-64 p-4 border-r border-gray-200">
55+
@foreach($presetRanges as $key => $range)
56+
<button
57+
type="button"
58+
@click="selectPreset('{{ $key }}')"
59+
:class="selectedPreset === '{{ $key }}' ? 'bg-blue-50 text-blue-600' : 'text-gray-700 hover:bg-gray-50'"
60+
class="w-full text-left px-3 py-2 rounded-lg text-sm mb-1 flex items-center justify-between"
61+
>
62+
<span>{{ $range['label'] }}</span>
63+
<span class="text-xs font-mono bg-gray-100 px-2 py-1 rounded">{{ $range['key'] }}</span>
64+
</button>
65+
@endforeach
66+
</div>
67+
68+
<!-- Right: Calendar -->
69+
<div class="flex-1 p-4">
70+
<div class="flex justify-between items-center mb-4">
71+
<div class="flex items-center gap-4">
72+
<!-- Previous Month -->
73+
<button type="button" @click="prevMonth()" class="p-1 hover:bg-gray-100 rounded">
74+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
76+
</svg>
77+
</button>
78+
79+
<!-- Current Month/Year -->
80+
<h3 class="text-lg font-semibold" x-text="currentMonthYear"></h3>
81+
82+
<!-- Next Month -->
83+
<button type="button" @click="nextMonth()" class="p-1 hover:bg-gray-100 rounded">
84+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
86+
</svg>
87+
</button>
88+
</div>
89+
</div>
90+
91+
<!-- Calendar Grid -->
92+
<div class="grid grid-cols-7 gap-1 mb-4">
93+
<!-- Days of week -->
94+
<template x-for="day in ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']">
95+
<div class="h-8 flex items-center justify-center text-xs font-medium text-gray-500" x-text="day"></div>
96+
</template>
97+
98+
<!-- Calendar days -->
99+
<template x-for="day in calendarDays" :key="day.date">
100+
<button
101+
type="button"
102+
@click="selectDate(day.date)"
103+
:disabled="day.isDisabled"
104+
:class="{
105+
'bg-blue-600 text-white': day.isSelected,
106+
'bg-blue-100 text-blue-600': day.isInRange && !day.isSelected,
107+
'text-gray-300': day.isOtherMonth,
108+
'text-gray-900': !day.isOtherMonth && !day.isSelected && !day.isInRange,
109+
'hover:bg-blue-50': !day.isSelected && !day.isDisabled && !day.isOtherMonth,
110+
'cursor-not-allowed opacity-50': day.isDisabled
111+
}"
112+
class="h-8 w-8 flex items-center justify-center text-sm rounded-full transition-colors"
113+
x-text="day.day"
114+
></button>
115+
</template>
116+
</div>
117+
118+
<!-- Apply/Cancel buttons -->
119+
<div class="flex justify-end gap-2 pt-4 border-t border-gray-200">
120+
<button
121+
type="button"
122+
@click="cancel()"
123+
class="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg"
124+
>
125+
Cancel
126+
</button>
127+
<button
128+
type="button"
129+
@click="apply()"
130+
class="px-4 py-2 text-sm bg-blue-600 text-white hover:bg-blue-700 rounded-lg"
131+
>
132+
Apply
133+
</button>
134+
</div>
135+
</div>
136+
</div>
137+
</div>
138+
139+
<!-- Hidden inputs for form submission -->
140+
<input type="hidden" name="start_date" x-model="startDate">
141+
<input type="hidden" name="end_date" x-model="endDate">
142+
</div>
143+
144+
@push('scripts')
145+
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
146+
<script>
147+
function calendarFilter() {
148+
return {
149+
showPresets: false,
150+
selectedPreset: 'last_30_days',
151+
currentLabel: 'Last 30 days',
152+
startDate: '{{ $startDate }}',
153+
endDate: '{{ $endDate }}',
154+
tempStartDate: null,
155+
tempEndDate: null,
156+
currentMonth: new Date().getMonth(),
157+
currentYear: new Date().getFullYear(),
158+
calendarDays: [],
159+
160+
init() {
161+
this.updateCurrentLabel();
162+
this.generateCalendar();
163+
},
164+
165+
get currentMonthYear() {
166+
const date = new Date(this.currentYear, this.currentMonth);
167+
return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
168+
},
169+
170+
selectPreset(preset) {
171+
this.selectedPreset = preset;
172+
const now = new Date();
173+
174+
switch(preset) {
175+
case 'last_24_hours':
176+
this.tempStartDate = new Date(now.getTime() - 24*60*60*1000);
177+
this.tempEndDate = now;
178+
break;
179+
case 'last_7_days':
180+
this.tempStartDate = new Date(now.getTime() - 7*24*60*60*1000);
181+
this.tempEndDate = now;
182+
break;
183+
case 'last_30_days':
184+
this.tempStartDate = new Date(now.getTime() - 30*24*60*60*1000);
185+
this.tempEndDate = now;
186+
break;
187+
case 'last_3_months':
188+
this.tempStartDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
189+
this.tempEndDate = now;
190+
break;
191+
case 'last_12_months':
192+
this.tempStartDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
193+
this.tempEndDate = now;
194+
break;
195+
case 'month_to_date':
196+
this.tempStartDate = new Date(now.getFullYear(), now.getMonth(), 1);
197+
this.tempEndDate = now;
198+
break;
199+
case 'quarter_to_date':
200+
const quarter = Math.floor(now.getMonth() / 3);
201+
this.tempStartDate = new Date(now.getFullYear(), quarter * 3, 1);
202+
this.tempEndDate = now;
203+
break;
204+
case 'year_to_date':
205+
this.tempStartDate = new Date(now.getFullYear(), 0, 1);
206+
this.tempEndDate = now;
207+
break;
208+
}
209+
this.generateCalendar();
210+
},
211+
212+
selectDate(date) {
213+
if (!this.tempStartDate || (this.tempStartDate && this.tempEndDate)) {
214+
this.tempStartDate = new Date(date);
215+
this.tempEndDate = null;
216+
this.selectedPreset = 'custom';
217+
} else if (this.tempStartDate && !this.tempEndDate) {
218+
if (date >= this.tempStartDate) {
219+
this.tempEndDate = new Date(date);
220+
} else {
221+
this.tempEndDate = this.tempStartDate;
222+
this.tempStartDate = new Date(date);
223+
}
224+
}
225+
this.generateCalendar();
226+
},
227+
228+
prevMonth() {
229+
if (this.currentMonth === 0) {
230+
this.currentMonth = 11;
231+
this.currentYear--;
232+
} else {
233+
this.currentMonth--;
234+
}
235+
this.generateCalendar();
236+
},
237+
238+
nextMonth() {
239+
if (this.currentMonth === 11) {
240+
this.currentMonth = 0;
241+
this.currentYear++;
242+
} else {
243+
this.currentMonth++;
244+
}
245+
this.generateCalendar();
246+
},
247+
248+
generateCalendar() {
249+
this.calendarDays = [];
250+
251+
const firstDay = new Date(this.currentYear, this.currentMonth, 1);
252+
const lastDay = new Date(this.currentYear, this.currentMonth + 1, 0);
253+
const startDate = new Date(firstDay);
254+
startDate.setDate(startDate.getDate() - firstDay.getDay());
255+
256+
for (let i = 0; i < 42; i++) {
257+
const date = new Date(startDate);
258+
date.setDate(startDate.getDate() + i);
259+
260+
this.calendarDays.push({
261+
date: date,
262+
day: date.getDate(),
263+
isOtherMonth: date.getMonth() !== this.currentMonth,
264+
isSelected: this.isDateSelected(date),
265+
isInRange: this.isDateInRange(date),
266+
isDisabled: date > new Date()
267+
});
268+
}
269+
},
270+
271+
isDateSelected(date) {
272+
if (!this.tempStartDate) return false;
273+
const dateStr = this.formatDate(date);
274+
const startStr = this.formatDate(this.tempStartDate);
275+
const endStr = this.tempEndDate ? this.formatDate(this.tempEndDate) : null;
276+
277+
return dateStr === startStr || (endStr && dateStr === endStr);
278+
},
279+
280+
isDateInRange(date) {
281+
if (!this.tempStartDate || !this.tempEndDate) return false;
282+
return date >= this.tempStartDate && date <= this.tempEndDate;
283+
},
284+
285+
formatDate(date) {
286+
return date.toISOString().split('T')[0];
287+
},
288+
289+
updateCurrentLabel() {
290+
if (this.startDate && this.endDate) {
291+
const start = new Date(this.startDate);
292+
const end = new Date(this.endDate);
293+
this.currentLabel = `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`;
294+
} else {
295+
this.currentLabel = 'Last 30 days';
296+
}
297+
},
298+
299+
apply() {
300+
if (this.tempStartDate) {
301+
this.startDate = this.formatDate(this.tempStartDate);
302+
this.endDate = this.tempEndDate ? this.formatDate(this.tempEndDate) : this.formatDate(this.tempStartDate);
303+
this.updateCurrentLabel();
304+
305+
// Submit the form
306+
this.$el.closest('form').submit();
307+
}
308+
this.showPresets = false;
309+
},
310+
311+
cancel() {
312+
this.tempStartDate = this.startDate ? new Date(this.startDate) : null;
313+
this.tempEndDate = this.endDate ? new Date(this.endDate) : null;
314+
this.showPresets = false;
315+
}
316+
}
317+
}
318+
</script>
319+
@endpush
320+
321+
<style>
322+
[x-cloak] {
323+
display: none !important;
324+
}
325+
</style>

src/Controllers/RequestAnalyticsController.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,20 @@ public function __construct(protected DashboardAnalyticsService $dashboardServic
1414

1515
public function show(Request $request)
1616
{
17-
$dateRangeInput = $request->input('date_range', 30);
18-
$dateRange = is_numeric($dateRangeInput) && (int) $dateRangeInput > 0
19-
? (int) $dateRangeInput
20-
: 30;
21-
22-
$params = [
23-
'date_range' => $dateRange,
24-
'request_category' => $request->input('request_category', null),
25-
];
17+
$params = [];
18+
19+
if ($request->has('start_date') && $request->has('end_date')) {
20+
$params['start_date'] = $request->input('start_date');
21+
$params['end_date'] = $request->input('end_date');
22+
} else {
23+
$dateRangeInput = $request->input('date_range', 30);
24+
$dateRange = is_numeric($dateRangeInput) && (int) $dateRangeInput > 0
25+
? (int) $dateRangeInput
26+
: 30;
27+
$params['date_range'] = $dateRange;
28+
}
29+
30+
$params['request_category'] = $request->input('request_category', null);
2631

2732
$data = $this->dashboardService->getDashboardData($params);
2833

src/Http/Requests/OverviewRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public function rules(): array
1919
'date_range' => 'integer|min:1|max:365',
2020
'start_date' => 'date',
2121
'end_date' => 'date|after_or_equal:start_date',
22+
'request_category' => 'sometimes|string|in:web,api',
2223
'with_percentages' => 'sometimes|in:true,false,1,0',
2324
];
2425
}

src/Http/Requests/PageViewsRequest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public function rules(): array
1717
{
1818
return [
1919
'date_range' => 'integer|min:1|max:365',
20+
'start_date' => 'date',
21+
'end_date' => 'date|after_or_equal:start_date',
22+
'request_category' => 'sometimes|string|in:web,api',
2023
'path' => 'string',
2124
'page' => 'integer|min:1',
2225
'per_page' => 'integer|min:1|max:100',

0 commit comments

Comments
 (0)