feat: new date picker and new logic

This commit is contained in:
2025-11-02 15:45:31 +03:30
parent 627497a840
commit 858fb48f68
13 changed files with 1090 additions and 247 deletions

View 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}';
}
}

View File

@@ -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';

View File

@@ -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,