532 lines
16 KiB
Dart
532 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:syncfusion_flutter_datepicker/datepicker.dart';
|
|
|
|
import '../../../common/theme/theme.dart';
|
|
import '../bottom_sheet/date_range_bottom_sheet.dart';
|
|
|
|
class DateRangePickerField extends StatefulWidget {
|
|
final String? label;
|
|
final String placeholder;
|
|
final DateTime? startDate;
|
|
final DateTime? endDate;
|
|
final DateTime? minDate;
|
|
final DateTime? maxDate;
|
|
final Function(DateTime? startDate, DateTime? endDate)? onChanged;
|
|
final Color primaryColor;
|
|
final bool enabled;
|
|
final String? errorText;
|
|
final EdgeInsetsGeometry? padding;
|
|
final TextStyle? textStyle;
|
|
final TextStyle? placeholderStyle;
|
|
final BoxDecoration? decoration;
|
|
final double height;
|
|
|
|
const DateRangePickerField({
|
|
Key? key,
|
|
this.label,
|
|
this.placeholder = 'Pilih rentang tanggal',
|
|
this.startDate,
|
|
this.endDate,
|
|
this.minDate,
|
|
this.maxDate,
|
|
this.onChanged,
|
|
this.primaryColor = AppColor.primary,
|
|
this.enabled = true,
|
|
this.errorText,
|
|
this.padding,
|
|
this.textStyle,
|
|
this.placeholderStyle,
|
|
this.decoration,
|
|
this.height = 52.0,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<DateRangePickerField> createState() => _DateRangePickerFieldState();
|
|
}
|
|
|
|
class _DateRangePickerFieldState extends State<DateRangePickerField> {
|
|
bool _isPressed = false;
|
|
|
|
String get _displayText {
|
|
if (widget.startDate != null && widget.endDate != null) {
|
|
return '${_formatDate(widget.startDate!)} - ${_formatDate(widget.endDate!)}';
|
|
} else if (widget.startDate != null) {
|
|
return _formatDate(widget.startDate!);
|
|
}
|
|
return widget.placeholder;
|
|
}
|
|
|
|
bool get _hasValue {
|
|
return widget.startDate != null || widget.endDate != null;
|
|
}
|
|
|
|
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}';
|
|
}
|
|
|
|
Future<void> _showDateRangePicker() async {
|
|
if (!widget.enabled) return;
|
|
|
|
final result = await DateRangePickerBottomSheet.show(
|
|
context: context,
|
|
title: widget.label ?? 'Pilih Rentang Tanggal',
|
|
initialStartDate: widget.startDate,
|
|
initialEndDate: widget.endDate,
|
|
minDate: widget.minDate,
|
|
maxDate: widget.maxDate,
|
|
primaryColor: widget.primaryColor,
|
|
onChanged: widget.onChanged,
|
|
);
|
|
|
|
if (result != null && widget.onChanged != null) {
|
|
if (result.value is PickerDateRange) {
|
|
final PickerDateRange range = result.value;
|
|
widget.onChanged!(range.startDate, range.endDate);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final hasError = widget.errorText != null && widget.errorText!.isNotEmpty;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Label
|
|
if (widget.label != null) ...[
|
|
Text(
|
|
widget.label!,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: hasError ? AppColor.error : AppColor.textSecondary,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
|
|
// Input Field
|
|
GestureDetector(
|
|
onTap: _showDateRangePicker,
|
|
onTapDown: widget.enabled
|
|
? (_) => setState(() => _isPressed = true)
|
|
: null,
|
|
onTapUp: widget.enabled
|
|
? (_) => setState(() => _isPressed = false)
|
|
: null,
|
|
onTapCancel: widget.enabled
|
|
? () => setState(() => _isPressed = false)
|
|
: null,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 150),
|
|
height: widget.height,
|
|
padding:
|
|
widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration:
|
|
widget.decoration ??
|
|
BoxDecoration(
|
|
color: widget.enabled
|
|
? (_isPressed ? AppColor.backgroundLight : AppColor.white)
|
|
: AppColor.background,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: hasError
|
|
? AppColor.error
|
|
: (_isPressed ? widget.primaryColor : AppColor.border),
|
|
width: _isPressed ? 2 : 1,
|
|
),
|
|
boxShadow: _isPressed && widget.enabled
|
|
? [
|
|
BoxShadow(
|
|
color: widget.primaryColor.withOpacity(0.1),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Date Text
|
|
Expanded(
|
|
child: Text(
|
|
_displayText,
|
|
style:
|
|
widget.textStyle ??
|
|
TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: _hasValue
|
|
? FontWeight.w500
|
|
: FontWeight.w400,
|
|
color: widget.enabled
|
|
? (_hasValue
|
|
? AppColor.textPrimary
|
|
: AppColor.textSecondary)
|
|
: AppColor.textLight,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Calendar Icon
|
|
Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: widget.primaryColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
Icons.calendar_today_rounded,
|
|
size: 20,
|
|
color: widget.enabled
|
|
? widget.primaryColor
|
|
: AppColor.textLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Error Text
|
|
if (hasError) ...[
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
widget.errorText!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppColor.error,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Variasi dengan style yang berbeda
|
|
class DateRangePickerFieldOutlined extends StatefulWidget {
|
|
final String? label;
|
|
final String placeholder;
|
|
final DateTime? startDate;
|
|
final DateTime? endDate;
|
|
final DateTime? minDate;
|
|
final DateTime? maxDate;
|
|
final Function(DateTime? startDate, DateTime? endDate)? onChanged;
|
|
final Color primaryColor;
|
|
final bool enabled;
|
|
final String? errorText;
|
|
|
|
const DateRangePickerFieldOutlined({
|
|
Key? key,
|
|
this.label,
|
|
this.placeholder = 'Pilih rentang tanggal',
|
|
this.startDate,
|
|
this.endDate,
|
|
this.minDate,
|
|
this.maxDate,
|
|
this.onChanged,
|
|
this.primaryColor = AppColor.primary,
|
|
this.enabled = true,
|
|
this.errorText,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<DateRangePickerFieldOutlined> createState() =>
|
|
_DateRangePickerFieldOutlinedState();
|
|
}
|
|
|
|
class _DateRangePickerFieldOutlinedState
|
|
extends State<DateRangePickerFieldOutlined> {
|
|
bool _isFocused = false;
|
|
|
|
String get _displayText {
|
|
if (widget.startDate != null && widget.endDate != null) {
|
|
return '${_formatDate(widget.startDate!)} - ${_formatDate(widget.endDate!)}';
|
|
} else if (widget.startDate != null) {
|
|
return _formatDate(widget.startDate!);
|
|
}
|
|
return widget.placeholder;
|
|
}
|
|
|
|
bool get _hasValue {
|
|
return widget.startDate != null || widget.endDate != null;
|
|
}
|
|
|
|
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}';
|
|
}
|
|
|
|
Future<void> _showDateRangePicker() async {
|
|
if (!widget.enabled) return;
|
|
|
|
setState(() => _isFocused = true);
|
|
|
|
final result = await DateRangePickerBottomSheet.show(
|
|
context: context,
|
|
title: widget.label ?? 'Pilih Rentang Tanggal',
|
|
initialStartDate: widget.startDate,
|
|
initialEndDate: widget.endDate,
|
|
minDate: widget.minDate,
|
|
maxDate: widget.maxDate,
|
|
primaryColor: widget.primaryColor,
|
|
onChanged: widget.onChanged,
|
|
);
|
|
|
|
setState(() => _isFocused = false);
|
|
|
|
if (result != null && widget.onChanged != null) {
|
|
if (result.value is PickerDateRange) {
|
|
final PickerDateRange range = result.value;
|
|
widget.onChanged!(range.startDate, range.endDate);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final hasError = widget.errorText != null && widget.errorText!.isNotEmpty;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: _showDateRangePicker,
|
|
child: Container(
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: hasError
|
|
? AppColor.error
|
|
: (_isFocused || _hasValue
|
|
? widget.primaryColor
|
|
: AppColor.border),
|
|
width: _isFocused ? 2 : 1,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const SizedBox(width: 16),
|
|
|
|
// Date Text
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (widget.label != null && (_isFocused || _hasValue))
|
|
Text(
|
|
widget.label!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: hasError
|
|
? AppColor.error
|
|
: widget.primaryColor,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
Text(
|
|
_hasValue
|
|
? _displayText
|
|
: (widget.label ?? widget.placeholder),
|
|
style: TextStyle(
|
|
fontSize: _hasValue ? 16 : 16,
|
|
fontWeight: FontWeight.w400,
|
|
color: widget.enabled
|
|
? (_hasValue
|
|
? AppColor.textPrimary
|
|
: AppColor.textSecondary)
|
|
: AppColor.textLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Calendar Icon
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 16),
|
|
child: Icon(
|
|
Icons.calendar_today_rounded,
|
|
size: 24,
|
|
color: widget.enabled
|
|
? (_isFocused
|
|
? widget.primaryColor
|
|
: AppColor.textSecondary)
|
|
: AppColor.textLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Error Text
|
|
if (hasError) ...[
|
|
const SizedBox(height: 6),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 16),
|
|
child: Text(
|
|
widget.errorText!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppColor.error,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Usage Example Widget
|
|
class DateRangePickerExample extends StatefulWidget {
|
|
@override
|
|
_DateRangePickerExampleState createState() => _DateRangePickerExampleState();
|
|
}
|
|
|
|
class _DateRangePickerExampleState extends State<DateRangePickerExample> {
|
|
DateTime? _startDate;
|
|
DateTime? _endDate;
|
|
DateTime? _startDate2;
|
|
DateTime? _endDate2;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('Date Range Picker Example'),
|
|
backgroundColor: AppColor.primary,
|
|
foregroundColor: AppColor.white,
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Default Style',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
DateRangePickerField(
|
|
label: 'Periode Laporan',
|
|
placeholder: 'Pilih tanggal mulai - selesai',
|
|
startDate: _startDate,
|
|
endDate: _endDate,
|
|
primaryColor: AppColor.primary,
|
|
onChanged: (start, end) {
|
|
setState(() {
|
|
_startDate = start;
|
|
_endDate = end;
|
|
});
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
Text(
|
|
'Outlined Style',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
DateRangePickerFieldOutlined(
|
|
label: 'Rentang Waktu',
|
|
placeholder: 'Pilih rentang tanggal',
|
|
startDate: _startDate2,
|
|
endDate: _endDate2,
|
|
primaryColor: AppColor.secondary,
|
|
onChanged: (start, end) {
|
|
setState(() {
|
|
_startDate2 = start;
|
|
_endDate2 = end;
|
|
});
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Display selected dates
|
|
if (_startDate != null ||
|
|
_endDate != null ||
|
|
_startDate2 != null ||
|
|
_endDate2 != null)
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.background,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Selected Dates:',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
if (_startDate != null)
|
|
Text(
|
|
'Default: ${_startDate!} - ${_endDate ?? 'Not selected'}',
|
|
),
|
|
if (_startDate2 != null)
|
|
Text(
|
|
'Outlined: ${_startDate2!} - ${_endDate2 ?? 'Not selected'}',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|