feat: new date picker and new logic
This commit is contained in:
427
packages/core/lib/presentation/widget/monthly_calender.dart
Normal file
427
packages/core/lib/presentation/widget/monthly_calender.dart
Normal file
@@ -0,0 +1,427 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'package:rasadyar_core/core.dart';
|
||||
|
||||
/// MonthlyDataCalendar - A Persian calendar date picker that displays data values under each day
|
||||
/// Shows a full month grid with day numbers and associated data values
|
||||
/// Only today and 2 days ago are enabled
|
||||
class MonthlyDataCalendar extends StatefulWidget {
|
||||
final Function(DayInfo)? onDateSelect;
|
||||
final Map<String, DayData>? dayData; // Map with date keys and data objects
|
||||
final String? selectedDate;
|
||||
final String label;
|
||||
|
||||
const MonthlyDataCalendar({
|
||||
super.key,
|
||||
this.onDateSelect,
|
||||
this.dayData,
|
||||
this.selectedDate,
|
||||
this.label = 'انتخاب تاریخ',
|
||||
});
|
||||
|
||||
@override
|
||||
State<MonthlyDataCalendar> createState() => _MonthlyDataCalendarState();
|
||||
}
|
||||
|
||||
class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
late Jalali _currentMonth;
|
||||
List<DayInfo?> _calendarDays = [];
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
|
||||
// Persian month names
|
||||
final List<String> _monthNames = [
|
||||
'فروردین',
|
||||
'اردیبهشت',
|
||||
'خرداد',
|
||||
'تیر',
|
||||
'مرداد',
|
||||
'شهریور',
|
||||
'مهر',
|
||||
'آبان',
|
||||
'آذر',
|
||||
'دی',
|
||||
'بهمن',
|
||||
'اسفند',
|
||||
];
|
||||
|
||||
// Day names for header
|
||||
final List<String> _dayNames = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentMonth = Jalali.now();
|
||||
_generateCalendar();
|
||||
_updateDisplayValue();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MonthlyDataCalendar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.selectedDate != widget.selectedDate || oldWidget.dayData != widget.dayData) {
|
||||
_generateCalendar();
|
||||
_updateDisplayValue();
|
||||
}
|
||||
}
|
||||
|
||||
DayData? _getDayData(String formattedDate) {
|
||||
return widget.dayData?[formattedDate];
|
||||
}
|
||||
|
||||
// Check if a date is enabled (today or 2 days ago)
|
||||
bool _isDateEnabled(Jalali date) {
|
||||
final today = Jalali.now();
|
||||
final twoDaysAgo = today.addDays(-2);
|
||||
final oneDayAgo = today.addDays(-1);
|
||||
|
||||
final dateStr = date.formatCompactDate();
|
||||
final todayStr = today.formatCompactDate();
|
||||
final twoDaysAgoStr = twoDaysAgo.formatCompactDate();
|
||||
final oneDayAgoStr = oneDayAgo.formatCompactDate();
|
||||
|
||||
return dateStr == todayStr || dateStr == twoDaysAgoStr || dateStr == oneDayAgoStr;
|
||||
}
|
||||
|
||||
void _generateCalendar() {
|
||||
final days = <DayInfo?>[];
|
||||
final year = _currentMonth.year;
|
||||
final month = _currentMonth.month;
|
||||
final daysInMonth = _currentMonth.monthLength;
|
||||
|
||||
// Get first day of month to determine starting position
|
||||
final firstDayOfMonth = Jalali(year, month, 1);
|
||||
final dayOfWeek = firstDayOfMonth.weekDay; // 1 = Saturday in shamsi_date
|
||||
|
||||
// Add empty cells for days before the first day of month
|
||||
for (int i = 1; i < dayOfWeek; i++) {
|
||||
days.add(null);
|
||||
}
|
||||
|
||||
// Add all days of the month
|
||||
for (int day = 1; day <= daysInMonth; day++) {
|
||||
final date = Jalali(year, month, day);
|
||||
final today = Jalali.now();
|
||||
final isEnabled = _isDateEnabled(date);
|
||||
final formattedDate = date.formatCompactDate();
|
||||
final data = _getDayData(formattedDate);
|
||||
final hasZeroValue = isEnabled && data != null && data.value == 0;
|
||||
|
||||
days.add(
|
||||
DayInfo(
|
||||
date: date,
|
||||
day: day,
|
||||
formattedDate: formattedDate,
|
||||
isToday: date.year == today.year && date.month == today.month && date.day == today.day,
|
||||
isEnabled: isEnabled,
|
||||
hasZeroValue: hasZeroValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_calendarDays = days;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleDayClick(DayInfo dayInfo) {
|
||||
if (dayInfo.isEnabled && !dayInfo.hasZeroValue && widget.onDateSelect != null) {
|
||||
widget.onDateSelect!(dayInfo);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleNextMonth() {
|
||||
setState(() {
|
||||
if (_currentMonth.month == 12) {
|
||||
_currentMonth = Jalali(_currentMonth.year + 1, 1, 1);
|
||||
} else {
|
||||
_currentMonth = Jalali(_currentMonth.year, _currentMonth.month + 1, 1);
|
||||
}
|
||||
_generateCalendar();
|
||||
});
|
||||
}
|
||||
|
||||
void _handlePrevMonth() {
|
||||
setState(() {
|
||||
if (_currentMonth.month == 1) {
|
||||
_currentMonth = Jalali(_currentMonth.year - 1, 12, 1);
|
||||
} else {
|
||||
_currentMonth = Jalali(_currentMonth.year, _currentMonth.month - 1, 1);
|
||||
}
|
||||
_generateCalendar();
|
||||
});
|
||||
}
|
||||
|
||||
bool _isSelected(String formattedDate) {
|
||||
return widget.selectedDate != null && widget.selectedDate == formattedDate;
|
||||
}
|
||||
|
||||
String _formatNumber(int? num) {
|
||||
if (num == null) return '';
|
||||
final formatter = intl.NumberFormat('#,###', 'fa');
|
||||
return formatter.format(num);
|
||||
}
|
||||
|
||||
void _updateDisplayValue() {
|
||||
if (widget.selectedDate == null || widget.selectedDate!.isEmpty) {
|
||||
_textController.text = '';
|
||||
return;
|
||||
}
|
||||
|
||||
final dayInfo = _calendarDays.firstWhere(
|
||||
(d) => d != null && d.formattedDate == widget.selectedDate,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (dayInfo != null) {
|
||||
final persianDay = _toPersianNumber(dayInfo.day);
|
||||
_textController.text = '$persianDay ${_monthNames[dayInfo.date.month - 1]}';
|
||||
} else {
|
||||
_textController.text = widget.selectedDate ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
String _toPersianNumber(int number) {
|
||||
const english = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
const persian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
|
||||
|
||||
String result = number.toString();
|
||||
for (int i = 0; i < english.length; i++) {
|
||||
result = result.replaceAll(english[i], persian[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void _showCalendarDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 650, maxHeight: 650),
|
||||
child: Card(
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
_buildDayNamesHeader(),
|
||||
const SizedBox(height: 8),
|
||||
_buildCalendarGrid(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: Color(0xFFF0F0F0), width: 2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(onPressed: _handlePrevMonth, icon: const Icon(Icons.chevron_right)),
|
||||
Text(
|
||||
'${_monthNames[_currentMonth.month - 1]} ${_toPersianNumber(_currentMonth.year)}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
color: Color(0xFF333333),
|
||||
),
|
||||
),
|
||||
IconButton(onPressed: _handleNextMonth, icon: const Icon(Icons.chevron_left)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDayNamesHeader() {
|
||||
return Row(
|
||||
children: _dayNames.map((dayName) {
|
||||
return Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
dayName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF666666),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCalendarGrid() {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 7,
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: _calendarDays.length,
|
||||
itemBuilder: (context, index) {
|
||||
final dayInfo = _calendarDays[index];
|
||||
|
||||
if (dayInfo == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return _buildDayCell(dayInfo);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDayCell(DayInfo dayInfo) {
|
||||
final data = _getDayData(dayInfo.formattedDate);
|
||||
final isSelectedDay = _isSelected(dayInfo.formattedDate);
|
||||
|
||||
Color bgColor = Colors.white;
|
||||
Color borderColor = const Color(0xFFE0E0E0);
|
||||
double opacity = 1.0;
|
||||
bool isClickable = true;
|
||||
|
||||
if (!dayInfo.isEnabled || dayInfo.hasZeroValue) {
|
||||
bgColor = const Color(0xFFF5F5F5);
|
||||
borderColor = const Color(0xFFD0D0D0);
|
||||
opacity = dayInfo.hasZeroValue ? 0.4 : 0.25;
|
||||
isClickable = false;
|
||||
} else if (isSelectedDay) {
|
||||
bgColor = const Color(0xFFE3F2FD);
|
||||
borderColor = const Color(0xFF1976D2);
|
||||
}
|
||||
|
||||
if (dayInfo.isToday && dayInfo.isEnabled && !dayInfo.hasZeroValue) {
|
||||
borderColor = const Color(0xFFFF9800);
|
||||
}
|
||||
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: InkWell(
|
||||
onTap: isClickable ? () => _handleDayClick(dayInfo) : null,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
border: Border.all(color: borderColor, width: 2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_toPersianNumber(dayInfo.day),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: dayInfo.isToday ? const Color(0xFFFF9800) : const Color(0xFF333333),
|
||||
),
|
||||
),
|
||||
if (data != null && data.value != null) ...[
|
||||
Text(
|
||||
_formatNumber(data.value),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF1976D2),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: _textController,
|
||||
readOnly: true,
|
||||
onTap: _showCalendarDialog,
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.label,
|
||||
hintText: 'انتخاب تاریخ...',
|
||||
prefixIcon: IconButton(
|
||||
icon: const Icon(Icons.calendar_month_outlined),
|
||||
onPressed: _showCalendarDialog,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: AppColor.darkGreyLight, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: AppColor.darkGreyLight, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: AppColor.darkGreyLight, width: 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper classes
|
||||
class DayInfo {
|
||||
final Jalali date;
|
||||
final int day;
|
||||
final String formattedDate;
|
||||
final bool isToday;
|
||||
final bool isEnabled;
|
||||
final bool hasZeroValue;
|
||||
|
||||
DayInfo({
|
||||
required this.date,
|
||||
required this.day,
|
||||
required this.formattedDate,
|
||||
required this.isToday,
|
||||
required this.isEnabled,
|
||||
required this.hasZeroValue,
|
||||
});
|
||||
}
|
||||
|
||||
class DayData {
|
||||
final int? value;
|
||||
|
||||
DayData({this.value});
|
||||
|
||||
factory DayData.fromJson(Map<String, dynamic> json) {
|
||||
return DayData(value: json['value1'] as int?);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'value1': value};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DayData{value: $value}';
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export 'loading/loading_widget.dart';
|
||||
// other
|
||||
export 'logo_widget.dart';
|
||||
export 'marquee/r_marquee.dart';
|
||||
export 'monthly_calender.dart';
|
||||
export 'overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown.dart';
|
||||
export 'overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown_logic.dart';
|
||||
export 'overlay_dropdown_widget/overlay_dropdown.dart';
|
||||
|
||||
@@ -67,7 +67,7 @@ Future<T?> gSafeCall<T>({
|
||||
|
||||
if (error is DioException && error.response?.statusCode == 401) {
|
||||
if (showError) {
|
||||
(onShowErrorMessage ?? _defaultShowErrorMessage)('خطا در احراز هویت');
|
||||
(onShowErrorMessage ?? defaultShowErrorMessage)('خطا در احراز هویت');
|
||||
}
|
||||
onError?.call(error, stackTrace);
|
||||
return null;
|
||||
@@ -76,7 +76,7 @@ Future<T?> gSafeCall<T>({
|
||||
if (retryCount > maxRetries || !_isRetryableError(error)) {
|
||||
if (showError) {
|
||||
final message = _getErrorMessage(error);
|
||||
(onShowErrorMessage ?? _defaultShowErrorMessage)(message);
|
||||
(onShowErrorMessage ?? defaultShowErrorMessage)(message);
|
||||
}
|
||||
onError?.call(error, stackTrace);
|
||||
return null;
|
||||
@@ -171,7 +171,7 @@ void defaultShowSuccessMessage(
|
||||
);
|
||||
}
|
||||
|
||||
void _defaultShowErrorMessage(String message) {
|
||||
void defaultShowErrorMessage(String message) {
|
||||
Get.snackbar(
|
||||
'خطا',
|
||||
message,
|
||||
|
||||
Reference in New Issue
Block a user