feat: sales range date
This commit is contained in:
@@ -0,0 +1,531 @@
|
||||
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'}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user