377 lines
10 KiB
Dart
377 lines
10 KiB
Dart
/// Date Picker Input Field
|
|
///
|
|
/// Input field that opens a date picker dialog when tapped.
|
|
/// Displays dates in Vietnamese format (dd/MM/yyyy).
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import '../../core/utils/formatters.dart';
|
|
import '../../core/utils/validators.dart';
|
|
import '../../core/constants/ui_constants.dart';
|
|
|
|
/// Date picker input field
|
|
class DatePickerField extends StatefulWidget {
|
|
final TextEditingController? controller;
|
|
final String? labelText;
|
|
final String? hintText;
|
|
final DateTime? initialDate;
|
|
final DateTime? firstDate;
|
|
final DateTime? lastDate;
|
|
final ValueChanged<DateTime>? onDateSelected;
|
|
final FormFieldValidator<String>? validator;
|
|
final bool enabled;
|
|
final bool required;
|
|
final Widget? prefixIcon;
|
|
final Widget? suffixIcon;
|
|
final InputDecoration? decoration;
|
|
|
|
const DatePickerField({
|
|
super.key,
|
|
this.controller,
|
|
this.labelText,
|
|
this.hintText,
|
|
this.initialDate,
|
|
this.firstDate,
|
|
this.lastDate,
|
|
this.onDateSelected,
|
|
this.validator,
|
|
this.enabled = true,
|
|
this.required = true,
|
|
this.prefixIcon,
|
|
this.suffixIcon,
|
|
this.decoration,
|
|
});
|
|
|
|
@override
|
|
State<DatePickerField> createState() => _DatePickerFieldState();
|
|
}
|
|
|
|
class _DatePickerFieldState extends State<DatePickerField> {
|
|
late TextEditingController _controller;
|
|
bool _isControllerInternal = false;
|
|
DateTime? _selectedDate;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedDate = widget.initialDate;
|
|
|
|
if (widget.controller == null) {
|
|
_controller = TextEditingController(
|
|
text: _selectedDate != null
|
|
? DateFormatter.formatDate(_selectedDate!)
|
|
: '',
|
|
);
|
|
_isControllerInternal = true;
|
|
} else {
|
|
_controller = widget.controller!;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (_isControllerInternal) {
|
|
_controller.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _selectDate(BuildContext context) async {
|
|
if (!widget.enabled) return;
|
|
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _selectedDate ?? DateTime.now(),
|
|
firstDate: widget.firstDate ?? DateTime(1900),
|
|
lastDate: widget.lastDate ?? DateTime(2100),
|
|
locale: const Locale('vi', 'VN'),
|
|
builder: (context, child) {
|
|
return Theme(
|
|
data: Theme.of(context).copyWith(
|
|
colorScheme: ColorScheme.light(
|
|
primary: Theme.of(context).primaryColor,
|
|
),
|
|
),
|
|
child: child!,
|
|
);
|
|
},
|
|
);
|
|
|
|
if (picked != null && picked != _selectedDate) {
|
|
setState(() {
|
|
_selectedDate = picked;
|
|
_controller.text = DateFormatter.formatDate(picked);
|
|
});
|
|
widget.onDateSelected?.call(picked);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TextFormField(
|
|
controller: _controller,
|
|
readOnly: true,
|
|
enabled: widget.enabled,
|
|
onTap: () => _selectDate(context),
|
|
decoration:
|
|
widget.decoration ??
|
|
InputDecoration(
|
|
labelText: widget.labelText ?? 'Ngày',
|
|
hintText: widget.hintText ?? 'dd/MM/yyyy',
|
|
prefixIcon: widget.prefixIcon ?? const FaIcon(FontAwesomeIcons.calendar, size: 20),
|
|
suffixIcon: widget.suffixIcon,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
|
),
|
|
contentPadding: InputFieldSpecs.contentPadding,
|
|
),
|
|
validator: widget.validator ?? (widget.required ? Validators.date : null),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Date range picker field
|
|
class DateRangePickerField extends StatefulWidget {
|
|
final String? labelText;
|
|
final String? hintText;
|
|
final DateTimeRange? initialRange;
|
|
final DateTime? firstDate;
|
|
final DateTime? lastDate;
|
|
final ValueChanged<DateTimeRange>? onRangeSelected;
|
|
final bool enabled;
|
|
final Widget? prefixIcon;
|
|
|
|
const DateRangePickerField({
|
|
super.key,
|
|
this.labelText,
|
|
this.hintText,
|
|
this.initialRange,
|
|
this.firstDate,
|
|
this.lastDate,
|
|
this.onRangeSelected,
|
|
this.enabled = true,
|
|
this.prefixIcon,
|
|
});
|
|
|
|
@override
|
|
State<DateRangePickerField> createState() => _DateRangePickerFieldState();
|
|
}
|
|
|
|
class _DateRangePickerFieldState extends State<DateRangePickerField> {
|
|
late TextEditingController _controller;
|
|
DateTimeRange? _selectedRange;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedRange = widget.initialRange;
|
|
_controller = TextEditingController(
|
|
text: _selectedRange != null
|
|
? '${DateFormatter.formatDate(_selectedRange!.start)} - ${DateFormatter.formatDate(_selectedRange!.end)}'
|
|
: '',
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _selectDateRange(BuildContext context) async {
|
|
if (!widget.enabled) return;
|
|
|
|
final DateTimeRange? picked = await showDateRangePicker(
|
|
context: context,
|
|
initialDateRange: _selectedRange,
|
|
firstDate: widget.firstDate ?? DateTime(1900),
|
|
lastDate: widget.lastDate ?? DateTime(2100),
|
|
locale: const Locale('vi', 'VN'),
|
|
builder: (context, child) {
|
|
return Theme(
|
|
data: Theme.of(context).copyWith(
|
|
colorScheme: ColorScheme.light(
|
|
primary: Theme.of(context).primaryColor,
|
|
),
|
|
),
|
|
child: child!,
|
|
);
|
|
},
|
|
);
|
|
|
|
if (picked != null && picked != _selectedRange) {
|
|
setState(() {
|
|
_selectedRange = picked;
|
|
_controller.text =
|
|
'${DateFormatter.formatDate(picked.start)} - ${DateFormatter.formatDate(picked.end)}';
|
|
});
|
|
widget.onRangeSelected?.call(picked);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TextFormField(
|
|
controller: _controller,
|
|
readOnly: true,
|
|
enabled: widget.enabled,
|
|
onTap: () => _selectDateRange(context),
|
|
decoration: InputDecoration(
|
|
labelText: widget.labelText ?? 'Khoảng thời gian',
|
|
hintText: widget.hintText ?? 'Chọn khoảng thời gian',
|
|
prefixIcon: widget.prefixIcon ?? const FaIcon(FontAwesomeIcons.calendarDays, size: 20),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
|
),
|
|
contentPadding: InputFieldSpecs.contentPadding,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Date of birth picker field
|
|
class DateOfBirthField extends StatelessWidget {
|
|
final TextEditingController? controller;
|
|
final String? labelText;
|
|
final String? hintText;
|
|
final ValueChanged<DateTime>? onDateSelected;
|
|
final FormFieldValidator<String>? validator;
|
|
final bool enabled;
|
|
final int minAge;
|
|
|
|
const DateOfBirthField({
|
|
super.key,
|
|
this.controller,
|
|
this.labelText,
|
|
this.hintText,
|
|
this.onDateSelected,
|
|
this.validator,
|
|
this.enabled = true,
|
|
this.minAge = 18,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final now = DateTime.now();
|
|
final maxDate = DateTime(now.year - minAge, now.month, now.day);
|
|
final minDate = DateTime(now.year - 100, now.month, now.day);
|
|
|
|
return DatePickerField(
|
|
controller: controller,
|
|
labelText: labelText ?? 'Ngày sinh',
|
|
hintText: hintText ?? 'dd/MM/yyyy',
|
|
initialDate: maxDate,
|
|
firstDate: minDate,
|
|
lastDate: maxDate,
|
|
onDateSelected: onDateSelected,
|
|
validator: validator ?? (value) => Validators.age(value, minAge: minAge),
|
|
enabled: enabled,
|
|
prefixIcon: const FaIcon(FontAwesomeIcons.cakeCandles, size: 20),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Time picker field
|
|
class TimePickerField extends StatefulWidget {
|
|
final TextEditingController? controller;
|
|
final String? labelText;
|
|
final String? hintText;
|
|
final TimeOfDay? initialTime;
|
|
final ValueChanged<TimeOfDay>? onTimeSelected;
|
|
final bool enabled;
|
|
final Widget? prefixIcon;
|
|
|
|
const TimePickerField({
|
|
super.key,
|
|
this.controller,
|
|
this.labelText,
|
|
this.hintText,
|
|
this.initialTime,
|
|
this.onTimeSelected,
|
|
this.enabled = true,
|
|
this.prefixIcon,
|
|
});
|
|
|
|
@override
|
|
State<TimePickerField> createState() => _TimePickerFieldState();
|
|
}
|
|
|
|
class _TimePickerFieldState extends State<TimePickerField> {
|
|
late TextEditingController _controller;
|
|
bool _isControllerInternal = false;
|
|
TimeOfDay? _selectedTime;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedTime = widget.initialTime;
|
|
|
|
if (widget.controller == null) {
|
|
_controller = TextEditingController(
|
|
text: _selectedTime != null
|
|
? '${_selectedTime!.hour.toString().padLeft(2, '0')}:${_selectedTime!.minute.toString().padLeft(2, '0')}'
|
|
: '',
|
|
);
|
|
_isControllerInternal = true;
|
|
} else {
|
|
_controller = widget.controller!;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (_isControllerInternal) {
|
|
_controller.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _selectTime(BuildContext context) async {
|
|
if (!widget.enabled) return;
|
|
|
|
final TimeOfDay? picked = await showTimePicker(
|
|
context: context,
|
|
initialTime: _selectedTime ?? TimeOfDay.now(),
|
|
builder: (context, child) {
|
|
return Theme(
|
|
data: Theme.of(context).copyWith(
|
|
colorScheme: ColorScheme.light(
|
|
primary: Theme.of(context).primaryColor,
|
|
),
|
|
),
|
|
child: child!,
|
|
);
|
|
},
|
|
);
|
|
|
|
if (picked != null && picked != _selectedTime) {
|
|
setState(() {
|
|
_selectedTime = picked;
|
|
_controller.text =
|
|
'${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}';
|
|
});
|
|
widget.onTimeSelected?.call(picked);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TextFormField(
|
|
controller: _controller,
|
|
readOnly: true,
|
|
enabled: widget.enabled,
|
|
onTap: () => _selectTime(context),
|
|
decoration: InputDecoration(
|
|
labelText: widget.labelText ?? 'Thời gian',
|
|
hintText: widget.hintText ?? 'HH:mm',
|
|
prefixIcon: widget.prefixIcon ?? const FaIcon(FontAwesomeIcons.clock, size: 20),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
|
),
|
|
contentPadding: InputFieldSpecs.contentPadding,
|
|
),
|
|
);
|
|
}
|
|
}
|