change package calendar

This commit is contained in:
Efril 2026-03-01 19:11:08 +07:00
parent 37d747c0ba
commit f7cafeb583
3 changed files with 194 additions and 112 deletions

View File

@ -1,11 +1,22 @@
import 'package:flutter/material.dart'; 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 '../../../common/theme/theme.dart';
import '../spaces/space.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 { class DateRangePickerModal {
static Future<DateRangePickerSelectionChangedArgs?> show({ static Future<DateRangeSelection?> show({
required BuildContext context, required BuildContext context,
String title = 'Pilih Rentang Tanggal', String title = 'Pilih Rentang Tanggal',
DateTime? initialStartDate, DateTime? initialStartDate,
@ -17,7 +28,7 @@ class DateRangePickerModal {
Color primaryColor = AppColor.primary, Color primaryColor = AppColor.primary,
Function(DateTime? startDate, DateTime? endDate)? onChanged, Function(DateTime? startDate, DateTime? endDate)? onChanged,
}) async { }) async {
return await showDialog<DateRangePickerSelectionChangedArgs?>( return await showDialog<DateRangeSelection?>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext context) => _DateRangePickerDialog( builder: (BuildContext context) => _DateRangePickerDialog(
@ -64,7 +75,10 @@ class _DateRangePickerDialog extends StatefulWidget {
class _DateRangePickerDialogState extends State<_DateRangePickerDialog> class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
with TickerProviderStateMixin { with TickerProviderStateMixin {
DateRangePickerSelectionChangedArgs? _selectionChangedArgs; DateTime _focusedDay = DateTime.now();
DateTime? _rangeStart;
DateTime? _rangeEnd;
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> _scaleAnimation; late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation; late Animation<double> _fadeAnimation;
@ -72,6 +86,12 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Restore initial range jika ada
_rangeStart = widget.initialStartDate;
_rangeEnd = widget.initialEndDate;
_focusedDay = widget.initialStartDate ?? DateTime.now();
_animationController = AnimationController( _animationController = AnimationController(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
vsync: this, vsync: this,
@ -91,25 +111,32 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
super.dispose(); 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(() { setState(() {
_selectionChangedArgs = args; _focusedDay = focusedDay;
});
// Note: onChanged callback is now called only when confirm button is pressed if (_rangeStart == null || (_rangeStart != null && _rangeEnd != null)) {
// This allows users to see real-time selection without triggering callbacks // Mulai range baru
} _rangeStart = selectedDay;
_rangeEnd = null;
String _getSelectionText() { } else {
if (_selectionChangedArgs?.value is PickerDateRange) { // Sudah ada start, tentukan end
final PickerDateRange range = _selectionChangedArgs!.value; if (selectedDay.isBefore(_rangeStart!)) {
if (range.startDate != null && range.endDate != null) { // Jika pilih tanggal sebelum start jadikan start baru
return '${_formatDate(range.startDate!)} - ${_formatDate(range.endDate!)}'; _rangeStart = selectedDay;
} else if (range.startDate != null) { _rangeEnd = null;
return _formatDate(range.startDate!); } 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) { String _formatDate(DateTime date) {
@ -130,14 +157,23 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
return '${date.day} ${months[date.month - 1]} ${date.year}'; return '${date.day} ${months[date.month - 1]} ${date.year}';
} }
bool get _isValidSelection { String get _selectionText {
if (_selectionChangedArgs?.value is PickerDateRange) { if (_rangeStart != null && _rangeEnd != null) {
final PickerDateRange range = _selectionChangedArgs!.value; return '${_formatDate(_rangeStart!)} - ${_formatDate(_rangeEnd!)}';
return range.startDate != null && range.endDate != null; } 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedBuilder( return AnimatedBuilder(
@ -174,7 +210,7 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Header // Header
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -193,7 +229,7 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
), ),
child: Row( child: Row(
children: [ children: [
Icon( const Icon(
Icons.calendar_today_rounded, Icons.calendar_today_rounded,
color: Colors.white, color: Colors.white,
size: 24, size: 24,
@ -213,80 +249,130 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
), ),
), ),
// Scrollable Content // Scrollable Content
Flexible( Flexible(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
SpaceHeight(16), SpaceHeight(16),
// Date Picker
// TableCalendar
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
), ),
child: Container( child: Container(
height: 320,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: Colors.grey.withOpacity(0.2), color: Colors.grey.withOpacity(0.2),
), ),
), ),
child: SfDateRangePicker( child: TableCalendar(
onSelectionChanged: _onSelectionChanged, firstDay:
selectionMode: widget.minDate ??
DateRangePickerSelectionMode.range, DateTime.utc(2000, 1, 1),
initialSelectedRange: lastDay:
(widget.initialStartDate != null && widget.maxDate ??
widget.initialEndDate != null) DateTime.utc(2100, 12, 31),
? PickerDateRange( focusedDay: _focusedDay,
widget.initialStartDate, rangeStartDay: _rangeStart,
widget.initialEndDate, rangeEndDay: _rangeEnd,
) rangeSelectionMode:
: null, RangeSelectionMode.toggledOn,
minDate: widget.minDate, onDaySelected: _onDaySelected,
maxDate: widget.maxDate, onRangeSelected: (start, end, focusedDay) {
startRangeSelectionColor: widget.primaryColor, // table_calendar juga emit ini saat range mode,
endRangeSelectionColor: widget.primaryColor, // kita tangani di onDaySelected saja.
rangeSelectionColor: widget.primaryColor },
.withOpacity(0.2), onPageChanged: (focusedDay) {
todayHighlightColor: widget.primaryColor, _focusedDay = focusedDay;
headerStyle: DateRangePickerHeaderStyle( },
backgroundColor: Colors.transparent, selectedDayPredicate: (day) => false,
textAlign: TextAlign.center, headerStyle: HeaderStyle(
textStyle: const TextStyle( formatButtonVisible: false,
titleCentered: true,
titleTextStyle: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87, color: Colors.black87,
), ),
leftChevronIcon: Icon(
Icons.chevron_left_rounded,
color: widget.primaryColor,
),
rightChevronIcon: Icon(
Icons.chevron_right_rounded,
color: widget.primaryColor,
),
), ),
monthViewSettings: daysOfWeekStyle: DaysOfWeekStyle(
DateRangePickerMonthViewSettings( weekdayStyle: TextStyle(
viewHeaderStyle: fontSize: 12,
DateRangePickerViewHeaderStyle( fontWeight: FontWeight.w600,
backgroundColor: Colors.grey color: widget.primaryColor,
.withOpacity(0.1), ),
textStyle: TextStyle( weekendStyle: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: widget.primaryColor, color: widget.primaryColor.withOpacity(
), 0.6,
),
), ),
selectionTextStyle: const TextStyle( ),
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
), ),
rangeTextStyle: TextStyle( calendarStyle: CalendarStyle(
color: widget.primaryColor, // Today
fontWeight: FontWeight.w500, todayDecoration: BoxDecoration(
fontSize: 14, 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( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@ -311,7 +397,7 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
_getSelectionText(), _selectionText,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -326,7 +412,7 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
), ),
), ),
// Action Buttons // Action Buttons
Padding( Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Row( child: Row(
@ -358,20 +444,16 @@ class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
child: ElevatedButton( child: ElevatedButton(
onPressed: _isValidSelection onPressed: _isValidSelection
? () { ? () {
// Call onChanged when confirm button is pressed widget.onChanged?.call(
if (widget.onChanged != null && _rangeStart,
_selectionChangedArgs?.value _rangeEnd,
is PickerDateRange) { );
final PickerDateRange range = Navigator.of(context).pop(
_selectionChangedArgs!.value; DateRangeSelection(
widget.onChanged!( startDate: _rangeStart,
range.startDate, endDate: _rangeEnd,
range.endDate, ),
); );
}
Navigator.of(
context,
).pop(_selectionChangedArgs);
} }
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(

View File

@ -788,10 +788,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -1112,6 +1112,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -1221,22 +1229,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" 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: synchronized:
dependency: transitive dependency: transitive
description: description:
@ -1245,6 +1237,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.0" 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: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -1257,10 +1257,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" version: "0.7.7"
time: time:
dependency: transitive dependency: transitive
description: description:
@ -1399,4 +1399,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.9.0 <4.0.0" dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.1" flutter: ">=3.35.0"

View File

@ -37,12 +37,12 @@ dependencies:
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
shimmer: ^3.0.0 shimmer: ^3.0.0
dropdown_search: ^5.0.6 dropdown_search: ^5.0.6
syncfusion_flutter_datepicker: ^31.2.3
fl_chart: ^1.1.1 fl_chart: ^1.1.1
permission_handler: ^12.0.1 permission_handler: ^12.0.1
print_bluetooth_thermal: ^1.1.7 print_bluetooth_thermal: ^1.1.7
flutter_esc_pos_network: ^1.0.3 flutter_esc_pos_network: ^1.0.3
esc_pos_utils_plus: ^2.0.4 esc_pos_utils_plus: ^2.0.4
table_calendar: ^3.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: