From f7cafeb5838ca1d8f60ad189165b33f49c9e122c Mon Sep 17 00:00:00 2001 From: Efril Date: Sun, 1 Mar 2026 19:11:08 +0700 Subject: [PATCH] change package calendar --- .../components/picker/date_range_picker.dart | 262 ++++++++++++------ pubspec.lock | 42 +-- pubspec.yaml | 2 +- 3 files changed, 194 insertions(+), 112 deletions(-) diff --git a/lib/presentation/components/picker/date_range_picker.dart b/lib/presentation/components/picker/date_range_picker.dart index 81d671b..dfe9d9b 100644 --- a/lib/presentation/components/picker/date_range_picker.dart +++ b/lib/presentation/components/picker/date_range_picker.dart @@ -1,11 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:syncfusion_flutter_datepicker/datepicker.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({ + static Future show({ required BuildContext context, String title = 'Pilih Rentang Tanggal', DateTime? initialStartDate, @@ -17,7 +28,7 @@ class DateRangePickerModal { Color primaryColor = AppColor.primary, Function(DateTime? startDate, DateTime? endDate)? onChanged, }) async { - return await showDialog( + return await showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) => _DateRangePickerDialog( @@ -64,7 +75,10 @@ class _DateRangePickerDialog extends StatefulWidget { class _DateRangePickerDialogState extends State<_DateRangePickerDialog> with TickerProviderStateMixin { - DateRangePickerSelectionChangedArgs? _selectionChangedArgs; + DateTime _focusedDay = DateTime.now(); + DateTime? _rangeStart; + DateTime? _rangeEnd; + late AnimationController _animationController; late Animation _scaleAnimation; late Animation _fadeAnimation; @@ -72,6 +86,12 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> @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, @@ -91,25 +111,32 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> super.dispose(); } - void _onSelectionChanged(DateRangePickerSelectionChangedArgs args) { + /// 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(() { - _selectionChangedArgs = args; - }); + _focusedDay = focusedDay; - // Note: onChanged callback is now called only when confirm button is pressed - // This allows users to see real-time selection without triggering callbacks - } - - 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!); + 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; + } } - } - return 'Belum ada tanggal dipilih'; + }); } String _formatDate(DateTime date) { @@ -130,14 +157,23 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> 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; + String get _selectionText { + if (_rangeStart != null && _rangeEnd != null) { + return '${_formatDate(_rangeStart!)} - ${_formatDate(_rangeEnd!)}'; + } else if (_rangeStart != null) { + return '${_formatDate(_rangeStart!)} - Pilih tanggal akhir'; } - return false; + 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( @@ -174,7 +210,7 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Header + // ── Header ────────────────────────────────────────── Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -193,7 +229,7 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> ), child: Row( children: [ - Icon( + const Icon( Icons.calendar_today_rounded, color: Colors.white, size: 24, @@ -213,80 +249,130 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> ), ), - // Scrollable Content + // ── Scrollable Content ─────────────────────────────── Flexible( child: SingleChildScrollView( child: Column( children: [ SpaceHeight(16), - // Date Picker + + // ── TableCalendar ──────────────────────────── Padding( padding: const EdgeInsets.symmetric( horizontal: 16, ), child: 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( + 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, + ), ), - monthViewSettings: - DateRangePickerMonthViewSettings( - viewHeaderStyle: - DateRangePickerViewHeaderStyle( - backgroundColor: Colors.grey - .withOpacity(0.1), - textStyle: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - 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, ), - selectionTextStyle: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 14, + ), ), - rangeTextStyle: TextStyle( - color: widget.primaryColor, - fontWeight: FontWeight.w500, - fontSize: 14, + 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 + + // ── Selection Info ─────────────────────────── Container( width: double.infinity, padding: const EdgeInsets.all(12), @@ -311,7 +397,7 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> ), const SizedBox(height: 4), Text( - _getSelectionText(), + _selectionText, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -326,7 +412,7 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> ), ), - // Action Buttons + // ── Action Buttons ─────────────────────────────────── Padding( padding: const EdgeInsets.all(20), child: Row( @@ -358,20 +444,16 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog> 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); + widget.onChanged?.call( + _rangeStart, + _rangeEnd, + ); + Navigator.of(context).pop( + DateRangeSelection( + startDate: _rangeStart, + endDate: _rangeEnd, + ), + ); } : null, style: ElevatedButton.styleFrom( diff --git a/pubspec.lock b/pubspec.lock index 75eb6b7..934ab5f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -788,10 +788,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1112,6 +1112,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + simple_gesture_detector: + dependency: transitive + description: + name: simple_gesture_detector + sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 + url: "https://pub.dev" + source: hosted + version: "0.2.1" sky_engine: dependency: transitive description: flutter @@ -1221,22 +1229,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - syncfusion_flutter_core: - dependency: transitive - description: - name: syncfusion_flutter_core - sha256: d03c43f577cdbe020d1632bece00cbf8bec4a7d0ab123923b69141b5fec35420 - url: "https://pub.dev" - source: hosted - version: "31.2.3" - syncfusion_flutter_datepicker: - dependency: "direct main" - description: - name: syncfusion_flutter_datepicker - sha256: f6277bd71a6d04785d7359c8caf373acae07132f1cc453b835173261dbd5ddb6 - url: "https://pub.dev" - source: hosted - version: "31.2.3" synchronized: dependency: transitive description: @@ -1245,6 +1237,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.0" + table_calendar: + dependency: "direct main" + description: + name: table_calendar + sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" + url: "https://pub.dev" + source: hosted + version: "3.2.0" term_glyph: dependency: transitive description: @@ -1257,10 +1257,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" time: dependency: transitive description: @@ -1399,4 +1399,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.1" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5008cf1..8ae724d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,12 +37,12 @@ dependencies: cached_network_image: ^3.4.1 shimmer: ^3.0.0 dropdown_search: ^5.0.6 - syncfusion_flutter_datepicker: ^31.2.3 fl_chart: ^1.1.1 permission_handler: ^12.0.1 print_bluetooth_thermal: ^1.1.7 flutter_esc_pos_network: ^1.0.3 esc_pos_utils_plus: ^2.0.4 + table_calendar: ^3.1.2 dev_dependencies: flutter_test: