Commit bcb756606d819ba4d7c6df7e8122e043d23d5a4b

Authored by 刘宏哲
1 parent 2426dcda

feat(app): update date picker

@@ -10,9 +10,15 @@ Future<DateTime?> showReportDatePickerSheet({ @@ -10,9 +10,15 @@ Future<DateTime?> showReportDatePickerSheet({
10 required ReportPeriod period, 10 required ReportPeriod period,
11 required DateTime selectedDate, 11 required DateTime selectedDate,
12 required DateTime weekStart, 12 required DateTime weekStart,
  13 + DateTime? minDate,
  14 + DateTime? maxDate,
13 }) async { 15 }) async {
14 final backgroundColor = const Color(0xFFF8F4FF); 16 final backgroundColor = const Color(0xFFF8F4FF);
15 final barrierAlpha = period == ReportPeriod.day ? 0.25 : 0.70; 17 final barrierAlpha = period == ReportPeriod.day ? 0.25 : 0.70;
  18 + final range = ReportDatePickerRange(
  19 + minDate: minDate ?? ReportDateRangeConfig.firstDate(),
  20 + maxDate: maxDate ?? ReportDateRangeConfig.lastDate(),
  21 + );
16 22
17 final picked = await showModalBottomSheet<DateTime>( 23 final picked = await showModalBottomSheet<DateTime>(
18 context: context, 24 context: context,
@@ -24,13 +30,25 @@ Future<DateTime?> showReportDatePickerSheet({ @@ -24,13 +30,25 @@ Future<DateTime?> showReportDatePickerSheet({
24 builder: (context) { 30 builder: (context) {
25 switch (period) { 31 switch (period) {
26 case ReportPeriod.day: 32 case ReportPeriod.day:
27 - return _DayDatePickerSheet(initialDate: selectedDate); 33 + return _DayDatePickerSheet(
  34 + initialDate: range.clamp(selectedDate),
  35 + range: range,
  36 + );
28 case ReportPeriod.week: 37 case ReportPeriod.week:
29 - return _WeekDatePickerSheet(initialWeekStart: weekStart); 38 + return _WeekDatePickerSheet(
  39 + initialWeekStart: range.clampWeekStart(weekStart),
  40 + range: range,
  41 + );
30 case ReportPeriod.month: 42 case ReportPeriod.month:
31 - return _MonthDatePickerSheet(initialDate: selectedDate); 43 + return _MonthDatePickerSheet(
  44 + initialDate: range.clampMonth(selectedDate),
  45 + range: range,
  46 + );
32 case ReportPeriod.year: 47 case ReportPeriod.year:
33 - return _YearDatePickerSheet(initialDate: selectedDate); 48 + return _YearDatePickerSheet(
  49 + initialDate: range.clampYear(selectedDate),
  50 + range: range,
  51 + );
34 } 52 }
35 }, 53 },
36 ); 54 );
@@ -43,10 +61,75 @@ Future<DateTime?> showReportDatePickerSheet({ @@ -43,10 +61,75 @@ Future<DateTime?> showReportDatePickerSheet({
43 return picked; 61 return picked;
44 } 62 }
45 63
  64 +class ReportDatePickerRange {
  65 + ReportDatePickerRange({
  66 + required DateTime minDate,
  67 + required DateTime maxDate,
  68 + }) : minDate = DateTime(minDate.year, minDate.month, minDate.day),
  69 + maxDate = DateTime(maxDate.year, maxDate.month, maxDate.day) {
  70 + if (this.maxDate.isBefore(this.minDate)) {
  71 + throw ArgumentError.value(
  72 + maxDate,
  73 + 'maxDate',
  74 + 'must not be before minDate',
  75 + );
  76 + }
  77 + }
  78 +
  79 + final DateTime minDate;
  80 + final DateTime maxDate;
  81 +
  82 + int get firstYear => minDate.year;
  83 +
  84 + int get lastYear => maxDate.year;
  85 +
  86 + DateTime get firstMonth => DateTime(minDate.year, minDate.month);
  87 +
  88 + DateTime get lastMonth => DateTime(maxDate.year, maxDate.month);
  89 +
  90 + DateTime get firstWeekStart => _weekStartOf(minDate);
  91 +
  92 + DateTime get lastWeekStart => _weekStartOf(maxDate);
  93 +
  94 + DateTime clamp(DateTime date) {
  95 + final normalized = DateTime(date.year, date.month, date.day);
  96 + if (normalized.isBefore(minDate)) return minDate;
  97 + if (normalized.isAfter(maxDate)) return maxDate;
  98 + return normalized;
  99 + }
  100 +
  101 + DateTime clampMonth(DateTime date) {
  102 + final normalized = DateTime(date.year, date.month);
  103 + if (normalized.isBefore(firstMonth)) return firstMonth;
  104 + if (normalized.isAfter(lastMonth)) return lastMonth;
  105 + return normalized;
  106 + }
  107 +
  108 + DateTime clampYear(DateTime date) {
  109 + return DateTime(date.year.clamp(firstYear, lastYear));
  110 + }
  111 +
  112 + DateTime clampWeekStart(DateTime date) {
  113 + final normalized = _weekStartOf(date);
  114 + if (normalized.isBefore(firstWeekStart)) return firstWeekStart;
  115 + if (normalized.isAfter(lastWeekStart)) return lastWeekStart;
  116 + return normalized;
  117 + }
  118 +
  119 + static DateTime _weekStartOf(DateTime date) {
  120 + final normalized = DateTime(date.year, date.month, date.day);
  121 + return normalized.subtract(Duration(days: normalized.weekday - 1));
  122 + }
  123 +}
  124 +
46 class _YearDatePickerSheet extends StatefulWidget { 125 class _YearDatePickerSheet extends StatefulWidget {
47 - const _YearDatePickerSheet({required this.initialDate}); 126 + const _YearDatePickerSheet({
  127 + required this.initialDate,
  128 + required this.range,
  129 + });
48 130
49 final DateTime initialDate; 131 final DateTime initialDate;
  132 + final ReportDatePickerRange range;
50 133
51 @override 134 @override
52 State<_YearDatePickerSheet> createState() => _YearDatePickerSheetState(); 135 State<_YearDatePickerSheet> createState() => _YearDatePickerSheetState();
@@ -62,8 +145,11 @@ class _YearDatePickerSheetState extends State<_YearDatePickerSheet> { @@ -62,8 +145,11 @@ class _YearDatePickerSheetState extends State<_YearDatePickerSheet> {
62 @override 145 @override
63 void initState() { 146 void initState() {
64 super.initState(); 147 super.initState();
65 - _yearRange = _DatePickerYearRange.current();  
66 - _year = widget.initialDate.year.clamp(_yearRange.first, _yearRange.last); 148 + _yearRange = _DatePickerYearRange(
  149 + first: widget.range.firstYear,
  150 + last: widget.range.lastYear,
  151 + );
  152 + _year = widget.range.clampYear(widget.initialDate).year;
67 _yearController = 153 _yearController =
68 FixedExtentScrollController(initialItem: _year - _yearRange.first); 154 FixedExtentScrollController(initialItem: _year - _yearRange.first);
69 } 155 }
@@ -109,7 +195,9 @@ class _YearDatePickerSheetState extends State<_YearDatePickerSheet> { @@ -109,7 +195,9 @@ class _YearDatePickerSheetState extends State<_YearDatePickerSheet> {
109 const Spacer(), 195 const Spacer(),
110 _ConfirmButton( 196 _ConfirmButton(
111 width: 280, 197 width: 280,
112 - onTap: () => Navigator.of(context).pop(DateTime(_year)), 198 + onTap: () => Navigator.of(context).pop(
  199 + widget.range.clampYear(DateTime(_year)),
  200 + ),
113 ), 201 ),
114 ], 202 ],
115 ), 203 ),
@@ -120,9 +208,13 @@ class _YearDatePickerSheetState extends State<_YearDatePickerSheet> { @@ -120,9 +208,13 @@ class _YearDatePickerSheetState extends State<_YearDatePickerSheet> {
120 } 208 }
121 209
122 class _DayDatePickerSheet extends StatefulWidget { 210 class _DayDatePickerSheet extends StatefulWidget {
123 - const _DayDatePickerSheet({required this.initialDate}); 211 + const _DayDatePickerSheet({
  212 + required this.initialDate,
  213 + required this.range,
  214 + });
124 215
125 final DateTime initialDate; 216 final DateTime initialDate;
  217 + final ReportDatePickerRange range;
126 218
127 @override 219 @override
128 State<_DayDatePickerSheet> createState() => _DayDatePickerSheetState(); 220 State<_DayDatePickerSheet> createState() => _DayDatePickerSheetState();
@@ -136,20 +228,30 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> { @@ -136,20 +228,30 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> {
136 late int _day; 228 late int _day;
137 late final _DatePickerYearRange _yearRange; 229 late final _DatePickerYearRange _yearRange;
138 late final FixedExtentScrollController _yearController; 230 late final FixedExtentScrollController _yearController;
139 - late final FixedExtentScrollController _monthController; 231 + late FixedExtentScrollController _monthController;
140 late FixedExtentScrollController _dayController; 232 late FixedExtentScrollController _dayController;
141 233
142 @override 234 @override
143 void initState() { 235 void initState() {
144 super.initState(); 236 super.initState();
145 - _yearRange = _DatePickerYearRange.current();  
146 - _year = widget.initialDate.year.clamp(_yearRange.first, _yearRange.last);  
147 - _month = widget.initialDate.month;  
148 - _day = widget.initialDate.day.clamp(1, _daysInMonth(_year, _month)); 237 + _yearRange = _DatePickerYearRange(
  238 + first: widget.range.firstYear,
  239 + last: widget.range.lastYear,
  240 + );
  241 + final initialDate = widget.range.clamp(widget.initialDate);
  242 + _year = initialDate.year;
  243 + _month = initialDate.month.clamp(
  244 + _firstSelectableMonth,
  245 + _lastSelectableMonth,
  246 + );
  247 + _day = initialDate.day.clamp(_firstSelectableDay, _lastSelectableDay);
149 _yearController = 248 _yearController =
150 FixedExtentScrollController(initialItem: _year - _yearRange.first); 249 FixedExtentScrollController(initialItem: _year - _yearRange.first);
151 - _monthController = FixedExtentScrollController(initialItem: _month - 1);  
152 - _dayController = FixedExtentScrollController(initialItem: _day - 1); 250 + _monthController = FixedExtentScrollController(
  251 + initialItem: _month - _firstSelectableMonth,
  252 + );
  253 + _dayController =
  254 + FixedExtentScrollController(initialItem: _day - _firstSelectableDay);
153 } 255 }
154 256
155 @override 257 @override
@@ -162,6 +264,10 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> { @@ -162,6 +264,10 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> {
162 264
163 @override 265 @override
164 Widget build(BuildContext context) { 266 Widget build(BuildContext context) {
  267 + final firstMonth = _firstSelectableMonth;
  268 + final lastMonth = _lastSelectableMonth;
  269 + final firstDay = _firstSelectableDay;
  270 + final lastDay = _lastSelectableDay;
165 final yearPicker = Expanded( 271 final yearPicker = Expanded(
166 child: _picker( 272 child: _picker(
167 controller: _yearController, 273 controller: _yearController,
@@ -173,7 +279,7 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> { @@ -173,7 +279,7 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> {
173 onSelectedItemChanged: (index) { 279 onSelectedItemChanged: (index) {
174 setState(() { 280 setState(() {
175 _year = _yearRange.first + index; 281 _year = _yearRange.first + index;
176 - _clampDay(); 282 + _clampMonthAndDay();
177 }); 283 });
178 }, 284 },
179 ), 285 ),
@@ -181,13 +287,13 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> { @@ -181,13 +287,13 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> {
181 final monthPicker = Expanded( 287 final monthPicker = Expanded(
182 child: _picker( 288 child: _picker(
183 controller: _monthController, 289 controller: _monthController,
184 - count: 12,  
185 - selectedIndex: _month - 1, 290 + count: lastMonth - firstMonth + 1,
  291 + selectedIndex: _month - firstMonth,
186 labelBuilder: (index) => 292 labelBuilder: (index) =>
187 - context.l10n.reportDatePickerMonthOption(index + 1), 293 + context.l10n.reportDatePickerMonthOption(firstMonth + index),
188 onSelectedItemChanged: (index) { 294 onSelectedItemChanged: (index) {
189 setState(() { 295 setState(() {
190 - _month = index + 1; 296 + _month = firstMonth + index;
191 _clampDay(); 297 _clampDay();
192 }); 298 });
193 }, 299 },
@@ -196,12 +302,12 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> { @@ -196,12 +302,12 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> {
196 final dayPicker = Expanded( 302 final dayPicker = Expanded(
197 child: _picker( 303 child: _picker(
198 controller: _dayController, 304 controller: _dayController,
199 - count: _daysInMonth(_year, _month),  
200 - selectedIndex: _day - 1, 305 + count: lastDay - firstDay + 1,
  306 + selectedIndex: _day - firstDay,
201 labelBuilder: (index) => 307 labelBuilder: (index) =>
202 - context.l10n.reportDatePickerDayOption(index + 1), 308 + context.l10n.reportDatePickerDayOption(firstDay + index),
203 onSelectedItemChanged: (index) { 309 onSelectedItemChanged: (index) {
204 - setState(() => _day = index + 1); 310 + setState(() => _day = firstDay + index);
205 }, 311 },
206 ), 312 ),
207 ); 313 );
@@ -233,8 +339,9 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> { @@ -233,8 +339,9 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> {
233 ), 339 ),
234 const Spacer(), 340 const Spacer(),
235 _ConfirmButton( 341 _ConfirmButton(
236 - onTap: () =>  
237 - Navigator.of(context).pop(DateTime(_year, _month, _day)), 342 + onTap: () => Navigator.of(context).pop(
  343 + widget.range.clamp(DateTime(_year, _month, _day)),
  344 + ),
238 ), 345 ),
239 ], 346 ],
240 ), 347 ),
@@ -265,8 +372,9 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> { @@ -265,8 +372,9 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> {
265 labelBuilder(index), 372 labelBuilder(index),
266 maxLines: 1, 373 maxLines: 1,
267 style: TextStyle( 374 style: TextStyle(
268 - color:  
269 - selected ? _ReportPickerColors.h1 : _ReportPickerColors.fade, 375 + color: selected
  376 + ? _ReportPickerColors.h1
  377 + : _ReportPickerColors.fade,
270 fontSize: selected ? 18 : 15, 378 fontSize: selected ? 18 : 15,
271 fontWeight: selected ? FontWeight.w600 : FontWeight.w400, 379 fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
272 height: 1.2, 380 height: 1.2,
@@ -277,23 +385,64 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> { @@ -277,23 +385,64 @@ class _DayDatePickerSheetState extends State<_DayDatePickerSheet> {
277 ); 385 );
278 } 386 }
279 387
  388 + int get _firstSelectableMonth {
  389 + return _year == widget.range.minDate.year ? widget.range.minDate.month : 1;
  390 + }
  391 +
  392 + int get _lastSelectableMonth {
  393 + return _year == widget.range.maxDate.year ? widget.range.maxDate.month : 12;
  394 + }
  395 +
  396 + int get _firstSelectableDay {
  397 + return _isSameMonth(_year, _month, widget.range.minDate)
  398 + ? widget.range.minDate.day
  399 + : 1;
  400 + }
  401 +
  402 + int get _lastSelectableDay {
  403 + return _isSameMonth(_year, _month, widget.range.maxDate)
  404 + ? widget.range.maxDate.day
  405 + : _daysInMonth(_year, _month);
  406 + }
  407 +
  408 + void _clampMonthAndDay() {
  409 + final firstMonth = _firstSelectableMonth;
  410 + final lastMonth = _lastSelectableMonth;
  411 + final nextMonth = _month.clamp(firstMonth, lastMonth);
  412 + _month = nextMonth;
  413 + _monthController.dispose();
  414 + _monthController =
  415 + FixedExtentScrollController(initialItem: _month - firstMonth);
  416 + _clampDay();
  417 + }
  418 +
280 void _clampDay() { 419 void _clampDay() {
281 - final days = _daysInMonth(_year, _month);  
282 - if (_day <= days) return;  
283 - _day = days; 420 + final firstDay = _firstSelectableDay;
  421 + final lastDay = _lastSelectableDay;
  422 + final nextDay = _day.clamp(firstDay, lastDay);
  423 + _day = nextDay;
284 _dayController.dispose(); 424 _dayController.dispose();
285 - _dayController = FixedExtentScrollController(initialItem: _day - 1); 425 + _dayController =
  426 + FixedExtentScrollController(initialItem: _day - firstDay);
286 } 427 }
287 428
288 static int _daysInMonth(int year, int month) { 429 static int _daysInMonth(int year, int month) {
289 return DateTime(year, month + 1, 0).day; 430 return DateTime(year, month + 1, 0).day;
290 } 431 }
  432 +
  433 + static bool _isSameMonth(int year, int month, DateTime date) {
  434 + return year == date.year && month == date.month;
  435 + }
291 } 436 }
292 437
293 class _MonthDatePickerSheet extends StatefulWidget { 438 class _MonthDatePickerSheet extends StatefulWidget {
294 - const _MonthDatePickerSheet({required this.initialDate}); 439 + const _MonthDatePickerSheet({
  440 + required this.initialDate,
  441 + required this.range,
  442 + });
295 443
296 final DateTime initialDate; 444 final DateTime initialDate;
  445 + final ReportDatePickerRange range;
297 446
298 @override 447 @override
299 State<_MonthDatePickerSheet> createState() => _MonthDatePickerSheetState(); 448 State<_MonthDatePickerSheet> createState() => _MonthDatePickerSheetState();
@@ -306,17 +455,26 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> { @@ -306,17 +455,26 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> {
306 late int _month; 455 late int _month;
307 late final _DatePickerYearRange _yearRange; 456 late final _DatePickerYearRange _yearRange;
308 late final FixedExtentScrollController _yearController; 457 late final FixedExtentScrollController _yearController;
309 - late final FixedExtentScrollController _monthController; 458 + late FixedExtentScrollController _monthController;
310 459
311 @override 460 @override
312 void initState() { 461 void initState() {
313 super.initState(); 462 super.initState();
314 - _yearRange = _DatePickerYearRange.current();  
315 - _year = widget.initialDate.year.clamp(_yearRange.first, _yearRange.last);  
316 - _month = widget.initialDate.month; 463 + _yearRange = _DatePickerYearRange(
  464 + first: widget.range.firstYear,
  465 + last: widget.range.lastYear,
  466 + );
  467 + final initialMonth = widget.range.clampMonth(widget.initialDate);
  468 + _year = initialMonth.year;
  469 + _month = initialMonth.month.clamp(
  470 + _firstSelectableMonth,
  471 + _lastSelectableMonth,
  472 + );
317 _yearController = 473 _yearController =
318 FixedExtentScrollController(initialItem: _year - _yearRange.first); 474 FixedExtentScrollController(initialItem: _year - _yearRange.first);
319 - _monthController = FixedExtentScrollController(initialItem: _month - 1); 475 + _monthController = FixedExtentScrollController(
  476 + initialItem: _month - _firstSelectableMonth,
  477 + );
320 } 478 }
321 479
322 @override 480 @override
@@ -328,6 +486,8 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> { @@ -328,6 +486,8 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> {
328 486
329 @override 487 @override
330 Widget build(BuildContext context) { 488 Widget build(BuildContext context) {
  489 + final firstMonth = _firstSelectableMonth;
  490 + final lastMonth = _lastSelectableMonth;
331 final yearPicker = Expanded( 491 final yearPicker = Expanded(
332 child: _picker( 492 child: _picker(
333 controller: _yearController, 493 controller: _yearController,
@@ -337,19 +497,22 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> { @@ -337,19 +497,22 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> {
337 _yearRange.first + index, 497 _yearRange.first + index,
338 ), 498 ),
339 onSelectedItemChanged: (index) { 499 onSelectedItemChanged: (index) {
340 - setState(() => _year = _yearRange.first + index); 500 + setState(() {
  501 + _year = _yearRange.first + index;
  502 + _clampMonth();
  503 + });
341 }, 504 },
342 ), 505 ),
343 ); 506 );
344 final monthPicker = Expanded( 507 final monthPicker = Expanded(
345 child: _picker( 508 child: _picker(
346 controller: _monthController, 509 controller: _monthController,
347 - count: 12,  
348 - selectedIndex: _month - 1, 510 + count: lastMonth - firstMonth + 1,
  511 + selectedIndex: _month - firstMonth,
349 labelBuilder: (index) => 512 labelBuilder: (index) =>
350 - context.l10n.reportDatePickerMonthOption(index + 1), 513 + context.l10n.reportDatePickerMonthOption(firstMonth + index),
351 onSelectedItemChanged: (index) { 514 onSelectedItemChanged: (index) {
352 - setState(() => _month = index + 1); 515 + setState(() => _month = firstMonth + index);
353 }, 516 },
354 ), 517 ),
355 ); 518 );
@@ -382,7 +545,9 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> { @@ -382,7 +545,9 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> {
382 const Spacer(), 545 const Spacer(),
383 _ConfirmButton( 546 _ConfirmButton(
384 width: 280, 547 width: 280,
385 - onTap: () => Navigator.of(context).pop(DateTime(_year, _month)), 548 + onTap: () => Navigator.of(context).pop(
  549 + widget.range.clampMonth(DateTime(_year, _month)),
  550 + ),
386 ), 551 ),
387 ], 552 ],
388 ), 553 ),
@@ -407,12 +572,34 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> { @@ -407,12 +572,34 @@ class _MonthDatePickerSheetState extends State<_MonthDatePickerSheet> {
407 itemExtent: _itemExtent, 572 itemExtent: _itemExtent,
408 ); 573 );
409 } 574 }
  575 +
  576 + int get _firstSelectableMonth {
  577 + return _year == widget.range.minDate.year ? widget.range.minDate.month : 1;
  578 + }
  579 +
  580 + int get _lastSelectableMonth {
  581 + return _year == widget.range.maxDate.year ? widget.range.maxDate.month : 12;
  582 + }
  583 +
  584 + void _clampMonth() {
  585 + final firstMonth = _firstSelectableMonth;
  586 + final lastMonth = _lastSelectableMonth;
  587 + final nextMonth = _month.clamp(firstMonth, lastMonth);
  588 + _month = nextMonth;
  589 + _monthController.dispose();
  590 + _monthController =
  591 + FixedExtentScrollController(initialItem: _month - firstMonth);
  592 + }
410 } 593 }
411 594
412 class _WeekDatePickerSheet extends StatefulWidget { 595 class _WeekDatePickerSheet extends StatefulWidget {
413 - const _WeekDatePickerSheet({required this.initialWeekStart}); 596 + const _WeekDatePickerSheet({
  597 + required this.initialWeekStart,
  598 + required this.range,
  599 + });
414 600
415 final DateTime initialWeekStart; 601 final DateTime initialWeekStart;
  602 + final ReportDatePickerRange range;
416 603
417 @override 604 @override
418 State<_WeekDatePickerSheet> createState() => _WeekDatePickerSheetState(); 605 State<_WeekDatePickerSheet> createState() => _WeekDatePickerSheetState();
@@ -477,8 +664,9 @@ class _WeekDatePickerSheetState extends State<_WeekDatePickerSheet> { @@ -477,8 +664,9 @@ class _WeekDatePickerSheetState extends State<_WeekDatePickerSheet> {
477 const Spacer(), 664 const Spacer(),
478 _ConfirmButton( 665 _ConfirmButton(
479 width: 280, 666 width: 280,
480 - onTap: () =>  
481 - Navigator.of(context).pop(_weekStarts[_selectedIndex]), 667 + onTap: () => Navigator.of(context).pop(
  668 + widget.range.clampWeekStart(_weekStarts[_selectedIndex]),
  669 + ),
482 ), 670 ),
483 ], 671 ],
484 ), 672 ),
@@ -488,8 +676,8 @@ class _WeekDatePickerSheetState extends State<_WeekDatePickerSheet> { @@ -488,8 +676,8 @@ class _WeekDatePickerSheetState extends State<_WeekDatePickerSheet> {
488 } 676 }
489 677
490 List<DateTime> _buildWeekStarts() { 678 List<DateTime> _buildWeekStarts() {
491 - final lastDate = ReportDateRangeConfig.lastWeekStart();  
492 - var cursor = ReportDateRangeConfig.firstWeekStart(); 679 + final lastDate = widget.range.lastWeekStart;
  680 + var cursor = widget.range.firstWeekStart;
493 final result = <DateTime>[]; 681 final result = <DateTime>[];
494 while (!cursor.isAfter(lastDate)) { 682 while (!cursor.isAfter(lastDate)) {
495 result.add(cursor); 683 result.add(cursor);
@@ -686,13 +874,6 @@ class _DatePickerYearRange { @@ -686,13 +874,6 @@ class _DatePickerYearRange {
686 final int last; 874 final int last;
687 875
688 int get count => last - first + 1; 876 int get count => last - first + 1;
689 -  
690 - factory _DatePickerYearRange.current() {  
691 - return _DatePickerYearRange(  
692 - first: ReportDateRangeConfig.firstYear(),  
693 - last: ReportDateRangeConfig.lastYear(),  
694 - );  
695 - }  
696 } 877 }
697 878
698 abstract final class _ReportPickerColors { 879 abstract final class _ReportPickerColors {
1 -import 'dart:async';  
2 -  
3 -import 'package:doublefeel_flutter/app/modules/friends/controllers/friends_controller.dart';  
4 -import 'package:doublefeel_flutter/app/modules/friends/data/friends_repository.dart';  
5 -import 'package:doublefeel_flutter/app/modules/friends/data/self_health_repository.dart';  
6 -import 'package:doublefeel_flutter/app/modules/friends/models/friend_health_data.dart';  
7 -import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';  
8 -import 'package:flutter_test/flutter_test.dart';  
9 -  
10 -void main() {  
11 - test('refresh loads friends and self health independently', () async {  
12 - final friendsRepository = _FakeFriendsRepository();  
13 - final selfHealthRepository = _FakeSelfHealthRepository();  
14 - final controller = FriendsController(  
15 - repository: friendsRepository,  
16 - selfHealthRepository: selfHealthRepository,  
17 - );  
18 -  
19 - final refresh = controller.refreshData();  
20 -  
21 - expect(friendsRepository.getFriendsCallCount, 1);  
22 - expect(selfHealthRepository.getHealthDataCallCount, 1);  
23 - expect(selfHealthRepository.getStressScoreCallCount, 1);  
24 - expect(controller.isLoading.value, isTrue);  
25 - expect(controller.isSelfHealthLoading.value, isTrue);  
26 -  
27 - selfHealthRepository.healthDataCompleter.complete(  
28 - const V2HealthData(steps: 8424, sleepScore: 92),  
29 - );  
30 - await Future<void>.delayed(Duration.zero);  
31 -  
32 - expect(controller.selfHealthData.value?.steps, 8424);  
33 - expect(controller.isSelfHealthLoading.value, isTrue);  
34 - expect(controller.isLoading.value, isTrue);  
35 -  
36 - selfHealthRepository.stressScoreCompleter.complete(  
37 - const V2StressScore(state: 2, comprehensiveScore: 72),  
38 - );  
39 - friendsRepository.friendsCompleter.complete(const []);  
40 - await refresh;  
41 -  
42 - expect(controller.selfStressScore.value?.comprehensiveScore, 72);  
43 - expect(controller.selfHealthUpdatedAt.value, isNotNull);  
44 - expect(controller.isLoading.value, isFalse);  
45 - expect(controller.isSelfHealthLoading.value, isFalse);  
46 - });  
47 -}  
48 -  
49 -class _FakeFriendsRepository implements FriendsRepository {  
50 - final friendsCompleter = Completer<List<FriendHealthData>>();  
51 - int getFriendsCallCount = 0;  
52 -  
53 - @override  
54 - Future<List<FriendHealthData>> getFriends() {  
55 - getFriendsCallCount++;  
56 - return friendsCompleter.future;  
57 - }  
58 -  
59 - @override  
60 - Future<void> deleteFriend(int userId) async {}  
61 -  
62 - @override  
63 - Future<void> selectWatchFaceFriend(int userId) async {}  
64 -  
65 - @override  
66 - Future<void> updateRemark(int userId, String remark) async {}  
67 -}  
68 -  
69 -class _FakeSelfHealthRepository implements SelfHealthRepository {  
70 - final healthDataCompleter = Completer<V2HealthData>();  
71 - final stressScoreCompleter = Completer<V2StressScore>();  
72 - int getHealthDataCallCount = 0;  
73 - int getStressScoreCallCount = 0;  
74 -  
75 - @override  
76 - Future<V2HealthData> getHealthData(DateTime date) {  
77 - getHealthDataCallCount++;  
78 - return healthDataCompleter.future;  
79 - }  
80 -  
81 - @override  
82 - Future<V2StressScore> getStressScore(DateTime date) {  
83 - getStressScoreCallCount++;  
84 - return stressScoreCompleter.future;  
85 - }  
86 -}