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 >
0 commit comments