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: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<DateRangePickerSelectionChangedArgs?> show({
static Future<DateRangeSelection?> 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<DateRangePickerSelectionChangedArgs?>(
return await showDialog<DateRangeSelection?>(
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<double> _scaleAnimation;
late Animation<double> _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;
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;
}
}
});
// 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!);
}
}
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,
),
monthViewSettings:
DateRangePickerMonthViewSettings(
viewHeaderStyle:
DateRangePickerViewHeaderStyle(
backgroundColor: Colors.grey
.withOpacity(0.1),
textStyle: TextStyle(
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,
),
),
selectionTextStyle: const TextStyle(
),
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,
),
rangeTextStyle: TextStyle(
// 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,
widget.onChanged?.call(
_rangeStart,
_rangeEnd,
);
Navigator.of(context).pop(
DateRangeSelection(
startDate: _rangeStart,
endDate: _rangeEnd,
),
);
}
Navigator.of(
context,
).pop(_selectionChangedArgs);
}
: null,
style: ElevatedButton.styleFrom(

View File

@ -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"

View File

@ -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: