import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; class DateRangePickerBottomSheet { static Future show({ required BuildContext context, String title = 'Pilih Rentang Tanggal', DateTime? initialStartDate, DateTime? initialEndDate, DateTime? minDate, DateTime? maxDate, String confirmText = 'Pilih', String cancelText = 'Batal', Color primaryColor = Colors.blue, Function(DateTime? startDate, DateTime? endDate)? onChanged, }) async { return await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, isDismissible: false, enableDrag: false, builder: (BuildContext context) => _DateRangePickerBottomSheet( title: title, initialStartDate: initialStartDate, initialEndDate: initialEndDate, minDate: minDate, maxDate: maxDate, confirmText: confirmText, cancelText: cancelText, primaryColor: primaryColor, onChanged: onChanged, ), ); } } class _DateRangePickerBottomSheet extends StatefulWidget { final String title; final DateTime? initialStartDate; final DateTime? initialEndDate; final DateTime? minDate; final DateTime? maxDate; final String confirmText; final String cancelText; final Color primaryColor; final Function(DateTime? startDate, DateTime? endDate)? onChanged; const _DateRangePickerBottomSheet({ required this.title, this.initialStartDate, this.initialEndDate, this.minDate, this.maxDate, required this.confirmText, required this.cancelText, required this.primaryColor, this.onChanged, }); @override State<_DateRangePickerBottomSheet> createState() => _DateRangePickerBottomSheetState(); } class _DateRangePickerBottomSheetState extends State<_DateRangePickerBottomSheet> with TickerProviderStateMixin { DateRangePickerSelectionChangedArgs? _selectionChangedArgs; late AnimationController _animationController; late Animation _slideAnimation; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _slideAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), ); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _onSelectionChanged(DateRangePickerSelectionChangedArgs args) { setState(() { _selectionChangedArgs = args; }); } String _getSelectionText() { if (_selectionChangedArgs?.value is PickerDateRange) { final PickerDateRange range = _selectionChangedArgs!.value; if (range.startDate != null && range.endDate != null) { return '${_formatDate(range.startDate!)} - ${_formatDate(range.endDate!)}'; } else if (range.startDate != null) { return _formatDate(range.startDate!); } } return 'Belum ada tanggal dipilih'; } String _formatDate(DateTime date) { final months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des', ]; return '${date.day} ${months[date.month - 1]} ${date.year}'; } bool get _isValidSelection { if (_selectionChangedArgs?.value is PickerDateRange) { final PickerDateRange range = _selectionChangedArgs!.value; return range.startDate != null && range.endDate != null; } return false; } @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; final bottomSheetHeight = screenHeight * 0.75; return AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.translate( offset: Offset(0, _slideAnimation.value * bottomSheetHeight), child: Container( height: bottomSheetHeight, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Drag Handle Container( margin: const EdgeInsets.only(top: 12, bottom: 8), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), // Content Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( children: [ // Selection Info Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: widget.primaryColor.withOpacity(0.08), borderRadius: BorderRadius.circular(12), border: Border.all( color: widget.primaryColor.withOpacity(0.2), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Tanggal Terpilih:', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: widget.primaryColor, ), ), const SizedBox(height: 6), Text( _getSelectionText(), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ], ), ), const SizedBox(height: 20), // Date Picker Container( height: 320, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.grey.withOpacity(0.2), ), ), child: SfDateRangePicker( onSelectionChanged: _onSelectionChanged, selectionMode: DateRangePickerSelectionMode.range, initialSelectedRange: (widget.initialStartDate != null && widget.initialEndDate != null) ? PickerDateRange( widget.initialStartDate, widget.initialEndDate, ) : null, minDate: widget.minDate, maxDate: widget.maxDate, startRangeSelectionColor: widget.primaryColor, endRangeSelectionColor: widget.primaryColor, rangeSelectionColor: widget.primaryColor .withOpacity(0.2), todayHighlightColor: widget.primaryColor, headerStyle: DateRangePickerHeaderStyle( backgroundColor: Colors.transparent, textAlign: TextAlign.center, textStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87, ), ), monthViewSettings: DateRangePickerMonthViewSettings( viewHeaderStyle: DateRangePickerViewHeaderStyle( backgroundColor: Colors.grey.withOpacity(0.1), textStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: widget.primaryColor, ), ), ), selectionTextStyle: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), rangeTextStyle: TextStyle( color: widget.primaryColor, fontWeight: FontWeight.w500, fontSize: 14, ), ), ), ], ), ), ), // Bottom Fixed Action Buttons Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, border: Border( top: BorderSide( color: Colors.grey.withOpacity(0.2), width: 1, ), ), ), child: SafeArea( child: Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.of(context).pop(), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), side: BorderSide(color: Colors.grey.shade400), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text( widget.cancelText, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Colors.grey, ), ), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton( onPressed: _isValidSelection ? () { // Call onChanged when confirm button is pressed if (widget.onChanged != null && _selectionChangedArgs?.value is PickerDateRange) { final PickerDateRange range = _selectionChangedArgs!.value; widget.onChanged!( range.startDate, range.endDate, ); } Navigator.of( context, ).pop(_selectionChangedArgs); } : null, style: ElevatedButton.styleFrom( backgroundColor: widget.primaryColor, padding: const EdgeInsets.symmetric(vertical: 16), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), disabledBackgroundColor: Colors.grey.shade300, ), child: Text( widget.confirmText, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: _isValidSelection ? Colors.white : Colors.grey.shade600, ), ), ), ), ], ), ), ), ], ), ), ); }, ); } }