import 'package:flutter/material.dart'; import 'package:table_calendar/table_calendar.dart'; import '../../../common/theme/theme.dart'; import '../spaces/space.dart'; /// Hasil selection, sebagai pengganti [DateRangePickerSelectionChangedArgs] /// dari Syncfusion. Gunakan class ini di pemanggil. class DateRangeSelection { final DateTime? startDate; final DateTime? endDate; const DateRangeSelection({this.startDate, this.endDate}); bool get isValid => startDate != null && endDate != null; } class DateRangePickerModal { 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 = AppColor.primary, Function(DateTime? startDate, DateTime? endDate)? onChanged, }) async { return await showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) => _DateRangePickerDialog( title: title, initialStartDate: initialStartDate, initialEndDate: initialEndDate, minDate: minDate, maxDate: maxDate, confirmText: confirmText, cancelText: cancelText, primaryColor: primaryColor, onChanged: onChanged, ), ); } } class _DateRangePickerDialog 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 _DateRangePickerDialog({ required this.title, this.initialStartDate, this.initialEndDate, this.minDate, this.maxDate, required this.confirmText, required this.cancelText, required this.primaryColor, this.onChanged, }); @override State<_DateRangePickerDialog> createState() => _DateRangePickerDialogState(); } class _DateRangePickerDialogState extends State<_DateRangePickerDialog> with TickerProviderStateMixin { DateTime _focusedDay = DateTime.now(); DateTime? _rangeStart; DateTime? _rangeEnd; late AnimationController _animationController; late Animation _scaleAnimation; late Animation _fadeAnimation; @override void initState() { super.initState(); // Restore initial range jika ada _rangeStart = widget.initialStartDate; _rangeEnd = widget.initialEndDate; _focusedDay = widget.initialStartDate ?? DateTime.now(); _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( CurvedAnimation(parent: _animationController, curve: Curves.elasticOut), ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } /// Manual range selection logic: /// - Tap pertama → set rangeStart, clear rangeEnd /// - Tap kedua → set rangeEnd (jika setelah start), atau reset start void _onDaySelected(DateTime selectedDay, DateTime focusedDay) { setState(() { _focusedDay = focusedDay; if (_rangeStart == null || (_rangeStart != null && _rangeEnd != null)) { // Mulai range baru _rangeStart = selectedDay; _rangeEnd = null; } else { // Sudah ada start, tentukan end if (selectedDay.isBefore(_rangeStart!)) { // Jika pilih tanggal sebelum start → jadikan start baru _rangeStart = selectedDay; _rangeEnd = null; } else if (isSameDay(selectedDay, _rangeStart)) { // Tap hari yang sama → reset _rangeStart = null; _rangeEnd = null; } else { _rangeEnd = selectedDay; } } }); } 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}'; } String get _selectionText { if (_rangeStart != null && _rangeEnd != null) { return '${_formatDate(_rangeStart!)} - ${_formatDate(_rangeEnd!)}'; } else if (_rangeStart != null) { return '${_formatDate(_rangeStart!)} - Pilih tanggal akhir'; } return 'Belum ada tanggal dipilih'; } bool get _isValidSelection => _rangeStart != null && _rangeEnd != null; /// Apakah hari ada di dalam range (eksklusif start & end) // bool _isInRange(DateTime day) { // if (_rangeStart == null || _rangeEnd == null) return false; // return day.isAfter(_rangeStart!) && day.isBefore(_rangeEnd!); // } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animationController, builder: (context, child) { return FadeTransition( opacity: _fadeAnimation, child: ScaleTransition( scale: _scaleAnimation, child: Dialog( backgroundColor: Colors.transparent, elevation: 0, insetPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 24, ), child: Container( width: MediaQuery.of(context).size.width, constraints: BoxConstraints( maxWidth: 400, maxHeight: MediaQuery.of(context).size.height * 0.85, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Header ────────────────────────────────────────── Container( padding: const EdgeInsets.all(20), decoration: const BoxDecoration( color: AppColor.primary, borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), ), child: Row( children: [ const Icon( Icons.calendar_today_rounded, color: Colors.white, size: 24, ), const SizedBox(width: 12), Expanded( child: Text( widget.title, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ], ), ), // ── Scrollable Content ─────────────────────────────── Flexible( child: SingleChildScrollView( child: Column( children: [ const SpaceHeight(16), // ── TableCalendar ──────────────────────────── Padding( padding: const EdgeInsets.symmetric( horizontal: 16, ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.grey.withOpacity(0.2), ), ), child: TableCalendar( firstDay: widget.minDate ?? DateTime.utc(2000, 1, 1), lastDay: widget.maxDate ?? DateTime.utc(2100, 12, 31), focusedDay: _focusedDay, rangeStartDay: _rangeStart, rangeEndDay: _rangeEnd, rangeSelectionMode: RangeSelectionMode.toggledOn, onDaySelected: _onDaySelected, onRangeSelected: (start, end, focusedDay) { // table_calendar juga emit ini saat range mode, // kita tangani di onDaySelected saja. }, onPageChanged: (focusedDay) { _focusedDay = focusedDay; }, selectedDayPredicate: (day) => false, headerStyle: HeaderStyle( formatButtonVisible: false, titleCentered: true, titleTextStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87, ), leftChevronIcon: Icon( Icons.chevron_left_rounded, color: widget.primaryColor, ), rightChevronIcon: Icon( Icons.chevron_right_rounded, color: widget.primaryColor, ), ), daysOfWeekStyle: DaysOfWeekStyle( weekdayStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: widget.primaryColor, ), weekendStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: widget.primaryColor.withOpacity( 0.6, ), ), ), calendarStyle: CalendarStyle( // Today todayDecoration: BoxDecoration( border: Border.all( color: widget.primaryColor, width: 1.5, ), shape: BoxShape.circle, ), todayTextStyle: TextStyle( color: widget.primaryColor, fontWeight: FontWeight.bold, ), // Range start rangeStartDecoration: BoxDecoration( color: widget.primaryColor, shape: BoxShape.circle, ), rangeStartTextStyle: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), // Range end rangeEndDecoration: BoxDecoration( color: widget.primaryColor, shape: BoxShape.circle, ), rangeEndTextStyle: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), // Within range withinRangeDecoration: BoxDecoration( color: widget.primaryColor.withOpacity( 0.15, ), shape: BoxShape.rectangle, ), withinRangeTextStyle: TextStyle( color: widget.primaryColor, fontWeight: FontWeight.w500, fontSize: 14, ), // Outside outsideDaysVisible: false, ), ), ), ), // ── Selection Info ─────────────────────────── Container( width: double.infinity, padding: const EdgeInsets.all(12), margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: widget.primaryColor.withOpacity(0.1), 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: 4), Text( _selectionText, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ], ), ), ], ), ), ), // ── Action Buttons ─────────────────────────────────── Padding( padding: const EdgeInsets.all(20), child: Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.of(context).pop(), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric( vertical: 14, ), side: BorderSide(color: Colors.grey.shade400), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text( widget.cancelText, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey, ), ), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton( onPressed: _isValidSelection ? () { widget.onChanged?.call( _rangeStart, _rangeEnd, ); Navigator.of(context).pop( DateRangeSelection( startDate: _rangeStart, endDate: _rangeEnd, ), ); } : null, style: ElevatedButton.styleFrom( backgroundColor: widget.primaryColor, padding: const EdgeInsets.symmetric( vertical: 14, ), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), disabledBackgroundColor: Colors.grey.shade300, ), child: Text( widget.confirmText, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _isValidSelection ? Colors.white : Colors.grey.shade600, ), ), ), ), ], ), ), ], ), ), ), ), ); }, ); } }