apskel-owner-flutter/lib/presentation/components/field/date_range_picker_field.dart
2025-08-18 16:43:07 +07:00

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'}',
),
],
),
),
],
),
),
);
}
}