runable
This commit is contained in:
375
lib/shared/widgets/date_picker_field.dart
Normal file
375
lib/shared/widgets/date_picker_field.dart
Normal file
@@ -0,0 +1,375 @@
|
||||
/// 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 '../../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 Icon(Icons.calendar_today),
|
||||
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 Icon(Icons.date_range),
|
||||
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 Icon(Icons.cake),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 Icon(Icons.access_time),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
),
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user