Commit 72cf904b7f2962e70e6a2dca0d81709e8cfa64d0

Authored by 刘宏哲
1 parent 8c7326af

feat(app): update purchase view

@@ -107,4 +107,9 @@ class PlatformHostApiImpl(private val application: android.app.Application) : Pl @@ -107,4 +107,9 @@ class PlatformHostApiImpl(private val application: android.app.Application) : Pl
107 callback: (Result<String?>) -> Unit 107 callback: (Result<String?>) -> Unit
108 ) { 108 ) {
109 } 109 }
  110 +
  111 + override fun jumpAppSetting(): Boolean {
  112 + return true
  113 + }
  114 +
110 } 115 }
@@ -117,13 +117,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { @@ -117,13 +117,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource {
117 time.isBefore(date.add(const Duration(days: 1)))) 117 time.isBefore(date.add(const Duration(days: 1))))
118 ActivityBurnHeartRatePoint(time: time, bpm: bpm.toDouble()), 118 ActivityBurnHeartRatePoint(time: time, bpm: bpm.toDouble()),
119 ]..sort((a, b) => a.time.compareTo(b.time)); 119 ]..sort((a, b) => a.time.compareTo(b.time));
120 - final sleepTimes = <DateTime>[  
121 - for (final item in data.sleepTimeList ?? const <SleepTimeList>[])  
122 - if (_parseDateTime(item.fromTime, fallbackDate: date) case final from?)  
123 - from,  
124 - for (final item in data.sleepTimeList ?? const <SleepTimeList>[])  
125 - if (_parseDateTime(item.toTime, fallbackDate: date) case final to?) to,  
126 - ]..sort(); 120 + final sleepRange = _dailySleepRange(date, data.sleepTimeList);
127 121
128 return ActivityBurnReport( 122 return ActivityBurnReport(
129 date: date, 123 date: date,
@@ -140,13 +134,47 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { @@ -140,13 +134,47 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource {
140 heartRate: ActivityBurnHeartRateSummary( 134 heartRate: ActivityBurnHeartRateSummary(
141 startTime: date, 135 startTime: date,
142 endTime: points.isEmpty ? null : points.last.time, 136 endTime: points.isEmpty ? null : points.last.time,
143 - sleepStartTime: sleepTimes.isEmpty ? null : sleepTimes.first,  
144 - sleepEndTime: sleepTimes.isEmpty ? null : sleepTimes.last, 137 + sleepStartTime: sleepRange?.start,
  138 + sleepEndTime: sleepRange?.end,
145 points: points, 139 points: points,
146 ), 140 ),
147 ); 141 );
148 } 142 }
149 143
  144 + _SleepRange? _dailySleepRange(
  145 + DateTime date,
  146 + List<SleepTimeList>? sleepTimeList,
  147 + ) {
  148 + final dayStart = _normalizeDate(date);
  149 + final dayEnd = dayStart.add(const Duration(days: 1));
  150 + DateTime? earliestStart;
  151 + DateTime? latestEndInDay;
  152 +
  153 + for (final item in sleepTimeList ?? const <SleepTimeList>[]) {
  154 + final from = _parseDateTime(item.fromTime, fallbackDate: dayStart);
  155 + final to = _parseDateTime(item.toTime, fallbackDate: dayStart);
  156 + if (from == null || to == null || !to.isAfter(from)) continue;
  157 + if (!from.isBefore(dayEnd) || !to.isAfter(dayStart)) continue;
  158 +
  159 + final visibleStart = from.isBefore(dayStart) ? dayStart : from;
  160 + if (earliestStart == null || visibleStart.isBefore(earliestStart)) {
  161 + earliestStart = visibleStart;
  162 + }
  163 +
  164 + final visibleEnd = to.isAfter(dayEnd) ? dayEnd : to;
  165 + if (!visibleEnd.isBefore(dayStart) && !visibleEnd.isAfter(dayEnd)) {
  166 + if (latestEndInDay == null || visibleEnd.isAfter(latestEndInDay)) {
  167 + latestEndInDay = visibleEnd;
  168 + }
  169 + }
  170 + }
  171 +
  172 + final start = earliestStart;
  173 + final end = latestEndInDay;
  174 + if (start == null || end == null || !end.isAfter(start)) return null;
  175 + return _SleepRange(start, end);
  176 + }
  177 +
150 Value? _dailyOverallValues( 178 Value? _dailyOverallValues(
151 DateTime date, 179 DateTime date,
152 List<OverallList>? overallList, 180 List<OverallList>? overallList,
@@ -287,6 +315,13 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { @@ -287,6 +315,13 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource {
287 date.year * 10000 + date.month * 100 + date.day; 315 date.year * 10000 + date.month * 100 + date.day;
288 } 316 }
289 317
  318 +class _SleepRange {
  319 + const _SleepRange(this.start, this.end);
  320 +
  321 + final DateTime start;
  322 + final DateTime end;
  323 +}
  324 +
290 class MockActivityBurnReportDataSource implements ActivityBurnReportDataSource { 325 class MockActivityBurnReportDataSource implements ActivityBurnReportDataSource {
291 const MockActivityBurnReportDataSource(); 326 const MockActivityBurnReportDataSource();
292 327
@@ -49,109 +49,47 @@ class FriendsTab extends GetView<FriendsController> { @@ -49,109 +49,47 @@ class FriendsTab extends GetView<FriendsController> {
49 final stressValue = stressScore?.state == 0 49 final stressValue = stressScore?.state == 0
50 ? null 50 ? null
51 : stressScore?.comprehensiveScore?.toDouble(); 51 : stressScore?.comprehensiveScore?.toDouble();
52 -  
53 - return Stack(  
54 - children: [  
55 - RefreshIndicator(  
56 - onRefresh: controller.refreshData,  
57 - child: ListView(  
58 - physics: const AlwaysScrollableScrollPhysics(),  
59 - padding: EdgeInsets.fromLTRB(  
60 - 0,  
61 - 0,  
62 - 0,  
63 - hasFriends ? 188.dp : 172, 52 + final selfHealthCard = _SelfHealthCard(
  53 + name: currentUser?.nickname?.trim().isNotEmpty == true
  54 + ? currentUser!.nickname!.trim()
  55 + : '-',
  56 + avatarUrl: currentUser?.avatar,
  57 + updatedAt: updatedAt == null
  58 + ? context.l10n.friendsWaitingForData
  59 + : context.l10n.friendsUpdatedAt(
  60 + DateFormat('HH:mm').format(updatedAt),
64 ), 61 ),
65 - children: [  
66 - _SelfHealthCard(  
67 - name:  
68 - currentUser?.nickname?.trim().isNotEmpty ==  
69 - true  
70 - ? currentUser!.nickname!.trim()  
71 - : '-',  
72 - avatarUrl: currentUser?.avatar,  
73 - updatedAt: updatedAt == null  
74 - ? context.l10n.friendsWaitingForData  
75 - : context.l10n.friendsUpdatedAt(  
76 - DateFormat('HH:mm').format(updatedAt),  
77 - ),  
78 - sleepQualityScore:  
79 - _normalizedScore(healthData?.sleepScore),  
80 - steps: healthData?.steps == null  
81 - ? null  
82 - : context.l10n.friendsStepCount(  
83 - healthData!.steps!,  
84 - ),  
85 - statusText: stressValue == null  
86 - ? context.l10n.friendsWaitingForData  
87 - : HrvStressLevel.fromRealtimeStress(  
88 - stressValue,  
89 - ).label,  
90 - stressValue: stressValue,  
91 - isLoading: isSelfInitialLoading,  
92 - ),  
93 - if (isFriendsInitialLoading)  
94 - const _FriendsLoadingIndicator()  
95 - else if (!hasFriends)  
96 - const _EmptyFriendsView()  
97 - else ...[  
98 - const SizedBox(height: 12),  
99 - ...friends.map(  
100 - (friend) => Padding(  
101 - padding: const EdgeInsets.fromLTRB(  
102 - 16,  
103 - 0,  
104 - 16,  
105 - 12,  
106 - ),  
107 - child: FriendHealthCard(  
108 - name: friend.name,  
109 - remark: friend.remark,  
110 - avatarUrl: friend.avatarUrl,  
111 - updatedAt: friend.updatedAt,  
112 - sleepQualityScore:  
113 - friend.sleepQualityScore,  
114 - steps: friend.steps,  
115 - statusText: friend.statusText,  
116 - stressValue: friend.stressValue,  
117 - isOnWatchFace: friend.isOnWatchFace,  
118 - onTap: () => _openFriendHome(friend),  
119 - onMoreSelected: (action) {  
120 - _handleFriendAction(action, friend);  
121 - },  
122 - ),  
123 - ),  
124 - ),  
125 - ],  
126 - if (!hasFriends && !isFriendsInitialLoading) ...[  
127 - const SizedBox(height: 8),  
128 - _AddFriendButton(  
129 - enabled: !isFull,  
130 - onTap: isFull ? null : _handleAddFriend,  
131 - ),  
132 - if (isFull) const _FullFriendsTip(),  
133 - ],  
134 - ],  
135 - ),  
136 - ),  
137 - if (hasFriends)  
138 - Positioned(  
139 - left: 0,  
140 - right: 0,  
141 - bottom: _floatingAddButtonBottom(context),  
142 - child: Column(  
143 - children: [  
144 - _AddFriendButton(  
145 - enabled: !isFull,  
146 - friendCount: friends.length,  
147 - maxFriends: FriendsController.maxFriends,  
148 - onTap: isFull ? null : _handleAddFriend,  
149 - ),  
150 - if (isFull) const _FullFriendsTip(),  
151 - ], 62 + sleepQualityScore:
  63 + _normalizedScore(healthData?.sleepScore),
  64 + steps: healthData?.steps == null
  65 + ? null
  66 + : context.l10n.friendsStepCount(
  67 + healthData!.steps!,
152 ), 68 ),
153 - ),  
154 - ], 69 + statusText: stressValue == null
  70 + ? context.l10n.friendsWaitingForData
  71 + : HrvStressLevel.fromRealtimeStress(
  72 + stressValue,
  73 + ).label,
  74 + stressValue: stressValue,
  75 + isLoading: isSelfInitialLoading,
  76 + );
  77 +
  78 + if (isFriendsInitialLoading) {
  79 + return _buildLoadingFriendsView(context, selfHealthCard);
  80 + }
  81 + if (!hasFriends) {
  82 + return _buildEmptyFriendsView(
  83 + context,
  84 + selfHealthCard,
  85 + isFull: isFull,
  86 + );
  87 + }
  88 + return _buildFriendsListView(
  89 + context,
  90 + selfHealthCard,
  91 + friends: friends,
  92 + isFull: isFull,
155 ); 93 );
156 }, 94 },
157 ), 95 ),
@@ -163,13 +101,127 @@ class FriendsTab extends GetView<FriendsController> { @@ -163,13 +101,127 @@ class FriendsTab extends GetView<FriendsController> {
163 ); 101 );
164 } 102 }
165 103
  104 + Widget _buildLoadingFriendsView(
  105 + BuildContext context,
  106 + Widget selfHealthCard,
  107 + ) {
  108 + return RefreshIndicator(
  109 + onRefresh: controller.refreshData,
  110 + child: ListView(
  111 + physics: const AlwaysScrollableScrollPhysics(),
  112 + padding: EdgeInsets.fromLTRB(
  113 + 0,
  114 + 0,
  115 + 0,
  116 + _emptyListBottomPadding(context),
  117 + ),
  118 + children: [
  119 + selfHealthCard,
  120 + const _FriendsLoadingIndicator(),
  121 + ],
  122 + ),
  123 + );
  124 + }
  125 +
  126 + Widget _buildEmptyFriendsView(
  127 + BuildContext context,
  128 + Widget selfHealthCard, {
  129 + required bool isFull,
  130 + }) {
  131 + return RefreshIndicator(
  132 + onRefresh: controller.refreshData,
  133 + child: ListView(
  134 + physics: const AlwaysScrollableScrollPhysics(),
  135 + padding: EdgeInsets.fromLTRB(
  136 + 0,
  137 + 0,
  138 + 0,
  139 + _emptyListBottomPadding(context),
  140 + ),
  141 + children: [
  142 + selfHealthCard,
  143 + _EmptyFriendsView(
  144 + enabled: !isFull,
  145 + onTap: isFull ? null : _handleAddFriend,
  146 + ),
  147 + ],
  148 + ),
  149 + );
  150 + }
  151 +
  152 + Widget _buildFriendsListView(
  153 + BuildContext context,
  154 + Widget selfHealthCard, {
  155 + required List<FriendHealthData> friends,
  156 + required bool isFull,
  157 + }) {
  158 + return Stack(
  159 + children: [
  160 + RefreshIndicator(
  161 + onRefresh: controller.refreshData,
  162 + child: ListView(
  163 + physics: const AlwaysScrollableScrollPhysics(),
  164 + padding: EdgeInsets.fromLTRB(0, 0, 0, 188.dp),
  165 + children: [
  166 + selfHealthCard,
  167 + const SizedBox(height: 12),
  168 + ...friends.map(
  169 + (friend) => Padding(
  170 + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
  171 + child: FriendHealthCard(
  172 + name: friend.name,
  173 + remark: friend.remark,
  174 + avatarUrl: friend.avatarUrl,
  175 + updatedAt: friend.updatedAt,
  176 + sleepQualityScore: friend.sleepQualityScore,
  177 + steps: friend.steps,
  178 + statusText: friend.statusText,
  179 + stressValue: friend.stressValue,
  180 + isOnWatchFace: friend.isOnWatchFace,
  181 + onTap: () => _openFriendHome(friend),
  182 + onMoreSelected: (action) {
  183 + _handleFriendAction(action, friend);
  184 + },
  185 + ),
  186 + ),
  187 + ),
  188 + ],
  189 + ),
  190 + ),
  191 + Positioned(
  192 + left: 0,
  193 + right: 0,
  194 + bottom: _floatingAddButtonBottom(context),
  195 + child: Column(
  196 + children: [
  197 + _AddFriendButton(
  198 + enabled: !isFull,
  199 + friendCount: friends.length,
  200 + maxFriends: FriendsController.maxFriends,
  201 + onTap: isFull ? null : _handleAddFriend,
  202 + ),
  203 + if (isFull) const _FullFriendsTip(),
  204 + ],
  205 + ),
  206 + ),
  207 + ],
  208 + );
  209 + }
  210 +
  211 + double _emptyListBottomPadding(BuildContext context) {
  212 + return _tabBarAvoidanceBottom(context) + 24;
  213 + }
  214 +
166 double _floatingAddButtonBottom(BuildContext context) { 215 double _floatingAddButtonBottom(BuildContext context) {
  216 + return _tabBarAvoidanceBottom(context) + 23.dp;
  217 + }
  218 +
  219 + double _tabBarAvoidanceBottom(BuildContext context) {
167 const tabBarHeight = 56.0; 220 const tabBarHeight = 56.0;
168 const tabBarBottomPadding = 8.0; 221 const tabBarBottomPadding = 8.0;
169 return MediaQuery.viewPaddingOf(context).bottom + 222 return MediaQuery.viewPaddingOf(context).bottom +
170 tabBarHeight + 223 tabBarHeight +
171 - tabBarBottomPadding +  
172 - 23.dp; 224 + tabBarBottomPadding;
173 } 225 }
174 226
175 Future<void> _handleAddFriend() async { 227 Future<void> _handleAddFriend() async {
@@ -324,7 +376,13 @@ _navigatorToPrivacySettings() { @@ -324,7 +376,13 @@ _navigatorToPrivacySettings() {
324 } 376 }
325 377
326 class _EmptyFriendsView extends StatelessWidget { 378 class _EmptyFriendsView extends StatelessWidget {
327 - const _EmptyFriendsView(); 379 + const _EmptyFriendsView({
  380 + required this.enabled,
  381 + this.onTap,
  382 + });
  383 +
  384 + final bool enabled;
  385 + final VoidCallback? onTap;
328 386
329 @override 387 @override
330 Widget build(BuildContext context) { 388 Widget build(BuildContext context) {
@@ -351,6 +409,12 @@ class _EmptyFriendsView extends StatelessWidget { @@ -351,6 +409,12 @@ class _EmptyFriendsView extends StatelessWidget {
351 fontWeight: FontWeight.w400, 409 fontWeight: FontWeight.w400,
352 ), 410 ),
353 ), 411 ),
  412 + const SizedBox(height: 12),
  413 + _AddFriendButton(
  414 + enabled: enabled,
  415 + onTap: onTap,
  416 + ),
  417 + if (!enabled) const _FullFriendsTip(),
354 ], 418 ],
355 ), 419 ),
356 ); 420 );
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; @@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
4 import 'package:get/get.dart'; 4 import 'package:get/get.dart';
5 5
6 import '../../../routes/app_pages.dart'; 6 import '../../../routes/app_pages.dart';
  7 +import '../../purchase/purchase_route_args.dart';
7 import '../../app_review_prompt/logic/app_review_prompt_logic.dart'; 8 import '../../app_review_prompt/logic/app_review_prompt_logic.dart';
8 import '../../report_common/models/report_period.dart'; 9 import '../../report_common/models/report_period.dart';
9 import 'trend/trend_controller.dart'; 10 import 'trend/trend_controller.dart';
@@ -39,7 +40,10 @@ class HomeController extends GetxController { @@ -39,7 +40,10 @@ class HomeController extends GetxController {
39 date: today, 40 date: today,
40 userId: userId, 41 userId: userId,
41 ); 42 );
42 - await Get.toNamed(Routes.PURCHASE); 43 + await Get.toNamed(
  44 + Routes.PURCHASE,
  45 + arguments: {PurchaseRouteArgs.showCloseButton: true},
  46 + );
43 return true; 47 return true;
44 } 48 }
45 49
@@ -42,9 +42,10 @@ class HrvReportLogic extends ReportPeriodLogic { @@ -42,9 +42,10 @@ class HrvReportLogic extends ReportPeriodLogic {
42 (nextPeriod == ReportPeriod.week || nextPeriod == ReportPeriod.month)) { 42 (nextPeriod == ReportPeriod.week || nextPeriod == ReportPeriod.month)) {
43 final now = DateTime.now(); 43 final now = DateTime.now();
44 final selectedYear = selectedDate.value.year; 44 final selectedYear = selectedDate.value.year;
45 - selectedDate.value = selectedYear == now.year 45 + final date = selectedYear == now.year
46 ? DateTime(now.year, now.month, now.day) 46 ? DateTime(now.year, now.month, now.day)
47 : DateTime(selectedYear); 47 : DateTime(selectedYear);
  48 + return selectQuery(nextPeriod, date, forceRefresh: forceRefresh);
48 } 49 }
49 return super.selectPeriod(nextPeriod, forceRefresh: forceRefresh); 50 return super.selectPeriod(nextPeriod, forceRefresh: forceRefresh);
50 } 51 }
@@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
4 import 'package:doublefeel_flutter/r.dart'; 4 import 'package:doublefeel_flutter/r.dart';
5 import 'package:flutter/material.dart'; 5 import 'package:flutter/material.dart';
6 import 'package:get/get.dart'; 6 import 'package:get/get.dart';
  7 +import 'package:lottie/lottie.dart';
7 8
8 import '../controllers/premium_activated_controller.dart'; 9 import '../controllers/premium_activated_controller.dart';
9 10
@@ -59,49 +60,54 @@ class PremiumActivatedView extends GetView<PremiumActivatedController> { @@ -59,49 +60,54 @@ class PremiumActivatedView extends GetView<PremiumActivatedController> {
59 } 60 }
60 61
61 Widget _buildContentView(BuildContext context) { 62 Widget _buildContentView(BuildContext context) {
62 - return Column(  
63 - mainAxisAlignment: MainAxisAlignment.start,  
64 - crossAxisAlignment: CrossAxisAlignment.center, 63 + return Stack(
65 children: [ 64 children: [
66 - SizedBox(height: 150.h),  
67 - const _FloatingPremiumIcon(),  
68 - SizedBox(height: 84.h),  
69 - Text(  
70 - context.l10n.premiumActivatedTitle,  
71 - style: const TextStyle(  
72 - fontSize: 20,  
73 - color: Color(0xFF0F0F11),  
74 - fontWeight: FontWeightExt.semibold),  
75 - textAlign: TextAlign.center,  
76 - ),  
77 - SizedBox(height: 8),  
78 - Padding(  
79 - padding: const EdgeInsets.symmetric(horizontal: 24.0),  
80 - child: Text(context.l10n.premiumActivatedDescription,  
81 - style: const TextStyle(fontSize: 14, color: Color(0xFF0F0F11)),  
82 - textAlign: TextAlign.center),  
83 - ),  
84 - Expanded(child: Container()),  
85 - GestureDetector(  
86 - onTap: () {  
87 - controller.executeContinueLogic(context);  
88 - },  
89 - child: Container(  
90 - width: 280.dp,  
91 - height: 48.dp,  
92 - decoration: BoxDecoration(  
93 - color: Color(0xFF845EEE),  
94 - borderRadius: BorderRadius.circular(24.dp), 65 + Lottie.asset('assets/lottie/scatter_flowers.json'),
  66 + Column(
  67 + mainAxisAlignment: MainAxisAlignment.start,
  68 + crossAxisAlignment: CrossAxisAlignment.center,
  69 + children: [
  70 + SizedBox(height: 150.h),
  71 + const _FloatingPremiumIcon(),
  72 + SizedBox(height: 84.h),
  73 + Text(
  74 + context.l10n.premiumActivatedTitle,
  75 + style: const TextStyle(
  76 + fontSize: 20,
  77 + color: Color(0xFF0F0F11),
  78 + fontWeight: FontWeightExt.semibold),
  79 + textAlign: TextAlign.center,
95 ), 80 ),
96 - alignment: Alignment.center,  
97 - child: Text(context.l10n.premiumActivatedContinue,  
98 - style: const TextStyle(  
99 - fontSize: 16,  
100 - color: Colors.white,  
101 - fontWeight: FontWeightExt.bold)),  
102 - ), 81 + SizedBox(height: 8),
  82 + Padding(
  83 + padding: const EdgeInsets.symmetric(horizontal: 24.0),
  84 + child: Text(context.l10n.premiumActivatedDescription,
  85 + style: const TextStyle(fontSize: 14, color: Color(0xFF0F0F11)),
  86 + textAlign: TextAlign.center),
  87 + ),
  88 + Expanded(child: Container()),
  89 + GestureDetector(
  90 + onTap: () {
  91 + controller.executeContinueLogic(context);
  92 + },
  93 + child: Container(
  94 + width: 280.dp,
  95 + height: 48.dp,
  96 + decoration: BoxDecoration(
  97 + color: Color(0xFF845EEE),
  98 + borderRadius: BorderRadius.circular(24.dp),
  99 + ),
  100 + alignment: Alignment.center,
  101 + child: Text(context.l10n.premiumActivatedContinue,
  102 + style: const TextStyle(
  103 + fontSize: 16,
  104 + color: Colors.white,
  105 + fontWeight: FontWeightExt.bold)),
  106 + ),
  107 + ),
  108 + SizedBox(height: 36),
  109 + ],
103 ), 110 ),
104 - SizedBox(height: 36),  
105 ], 111 ],
106 ); 112 );
107 } 113 }
  1 +abstract final class PurchaseRouteArgs {
  2 + static const showCloseButton = 'showCloseButton';
  3 +}
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; @@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
6 import 'package:get/get.dart'; 6 import 'package:get/get.dart';
7 7
8 import '../controllers/purchase_controller.dart'; 8 import '../controllers/purchase_controller.dart';
  9 +import '../purchase_route_args.dart';
9 import '../widgets/purchase_bottom_bar.dart'; 10 import '../widgets/purchase_bottom_bar.dart';
10 import '../widgets/purchase_colors.dart'; 11 import '../widgets/purchase_colors.dart';
11 import '../widgets/purchase_plan_card.dart'; 12 import '../widgets/purchase_plan_card.dart';
@@ -176,6 +177,9 @@ class _PurchaseNavigationBar extends StatelessWidget { @@ -176,6 +177,9 @@ class _PurchaseNavigationBar extends StatelessWidget {
176 @override 177 @override
177 Widget build(BuildContext context) { 178 Widget build(BuildContext context) {
178 final topInset = MediaQuery.paddingOf(context).top; 179 final topInset = MediaQuery.paddingOf(context).top;
  180 + final arguments = Get.arguments;
  181 + final showCloseButton = arguments is Map &&
  182 + arguments[PurchaseRouteArgs.showCloseButton] == true;
179 return Obx( 183 return Obx(
180 () => AnimatedContainer( 184 () => AnimatedContainer(
181 duration: const Duration(milliseconds: 160), 185 duration: const Duration(milliseconds: 160),
@@ -188,7 +192,9 @@ class _PurchaseNavigationBar extends StatelessWidget { @@ -188,7 +192,9 @@ class _PurchaseNavigationBar extends StatelessWidget {
188 IconButton( 192 IconButton(
189 onPressed: Get.back, 193 onPressed: Get.back,
190 icon: Image.asset( 194 icon: Image.asset(
191 - 'assets/images/common/ic_nav_back.webp', 195 + showCloseButton
  196 + ? R.assetsImagesCloseIcon
  197 + : R.assetsImagesNavBackIcon,
192 width: 24, 198 width: 24,
193 height: 24, 199 height: 24,
194 ), 200 ),
@@ -16,6 +16,7 @@ abstract class ReportPeriodLogic { @@ -16,6 +16,7 @@ abstract class ReportPeriodLogic {
16 final selectedPeriod = ReportPeriod.day.obs; 16 final selectedPeriod = ReportPeriod.day.obs;
17 final selectedDate = _today().obs; 17 final selectedDate = _today().obs;
18 final isLoading = false.obs; 18 final isLoading = false.obs;
  19 + final Map<ReportPeriod, DateTime> _datesByPeriod = {};
19 20
20 List<ReportPeriod> get supportedPeriods => ReportPeriod.values; 21 List<ReportPeriod> get supportedPeriods => ReportPeriod.values;
21 22
@@ -48,8 +49,9 @@ abstract class ReportPeriodLogic { @@ -48,8 +49,9 @@ abstract class ReportPeriodLogic {
48 49
49 void initializeQuery(ReportPeriod period, DateTime date) { 50 void initializeQuery(ReportPeriod period, DateTime date) {
50 final nextPeriod = normalizePeriod(period); 51 final nextPeriod = normalizePeriod(period);
  52 + _seedPeriodDates(date);
51 selectedPeriod.value = nextPeriod; 53 selectedPeriod.value = nextPeriod;
52 - selectedDate.value = normalizeDate(nextPeriod, date); 54 + selectedDate.value = _datesByPeriod[nextPeriod]!;
53 } 55 }
54 56
55 Future<void> selectQuery( 57 Future<void> selectQuery(
@@ -65,8 +67,10 @@ abstract class ReportPeriodLogic { @@ -65,8 +67,10 @@ abstract class ReportPeriodLogic {
65 selectedDate.value == nextDate) { 67 selectedDate.value == nextDate) {
66 return Future.value(); 68 return Future.value();
67 } 69 }
  70 + _datesByPeriod[selectedPeriod.value] = selectedDate.value;
68 selectedPeriod.value = nextPeriod; 71 selectedPeriod.value = nextPeriod;
69 selectedDate.value = nextDate; 72 selectedDate.value = nextDate;
  73 + _datesByPeriod[nextPeriod] = nextDate;
70 return loadReport(); 74 return loadReport();
71 } 75 }
72 76
@@ -126,7 +130,11 @@ abstract class ReportPeriodLogic { @@ -126,7 +130,11 @@ abstract class ReportPeriodLogic {
126 if (!shouldRefresh && selectedPeriod.value == nextPeriod) { 130 if (!shouldRefresh && selectedPeriod.value == nextPeriod) {
127 return Future.value(); 131 return Future.value();
128 } 132 }
  133 + _datesByPeriod[selectedPeriod.value] = selectedDate.value;
129 selectedPeriod.value = nextPeriod; 134 selectedPeriod.value = nextPeriod;
  135 + selectedDate.value = _datesByPeriod[nextPeriod] ??
  136 + normalizeDate(nextPeriod, selectedDate.value);
  137 + _datesByPeriod[nextPeriod] = selectedDate.value;
130 return loadReport(); 138 return loadReport();
131 } 139 }
132 140
@@ -138,6 +146,7 @@ abstract class ReportPeriodLogic { @@ -138,6 +146,7 @@ abstract class ReportPeriodLogic {
138 final nextDate = ReportDateRangeConfig.clampDate(date); 146 final nextDate = ReportDateRangeConfig.clampDate(date);
139 if (!shouldRefresh && selectedDate.value == nextDate) return Future.value(); 147 if (!shouldRefresh && selectedDate.value == nextDate) return Future.value();
140 selectedDate.value = nextDate; 148 selectedDate.value = nextDate;
  149 + _datesByPeriod[selectedPeriod.value] = nextDate;
141 return loadReport(); 150 return loadReport();
142 } 151 }
143 152
@@ -153,6 +162,7 @@ abstract class ReportPeriodLogic { @@ -153,6 +162,7 @@ abstract class ReportPeriodLogic {
153 ); 162 );
154 if (!shouldRefresh && weekStart == nextWeekStart) return Future.value(); 163 if (!shouldRefresh && weekStart == nextWeekStart) return Future.value();
155 selectedDate.value = nextWeekStart; 164 selectedDate.value = nextWeekStart;
  165 + _datesByPeriod[selectedPeriod.value] = nextWeekStart;
156 return loadReport(); 166 return loadReport();
157 } 167 }
158 168
@@ -169,6 +179,7 @@ abstract class ReportPeriodLogic { @@ -169,6 +179,7 @@ abstract class ReportPeriodLogic {
169 ); 179 );
170 if (!shouldRefresh && monthStart == nextMonth) return Future.value(); 180 if (!shouldRefresh && monthStart == nextMonth) return Future.value();
171 selectedDate.value = nextMonth; 181 selectedDate.value = nextMonth;
  182 + _datesByPeriod[selectedPeriod.value] = nextMonth;
172 return loadReport(); 183 return loadReport();
173 } 184 }
174 185
@@ -185,6 +196,7 @@ abstract class ReportPeriodLogic { @@ -185,6 +196,7 @@ abstract class ReportPeriodLogic {
185 return Future.value(); 196 return Future.value();
186 } 197 }
187 selectedDate.value = DateTime(nextYear); 198 selectedDate.value = DateTime(nextYear);
  199 + _datesByPeriod[selectedPeriod.value] = selectedDate.value;
188 return loadReport(); 200 return loadReport();
189 } 201 }
190 202
@@ -212,6 +224,7 @@ abstract class ReportPeriodLogic { @@ -212,6 +224,7 @@ abstract class ReportPeriodLogic {
212 final candidate = _previousCandidate; 224 final candidate = _previousCandidate;
213 if (_isBeforeRange(candidate)) return Future.value(); 225 if (_isBeforeRange(candidate)) return Future.value();
214 selectedDate.value = candidate; 226 selectedDate.value = candidate;
  227 + _datesByPeriod[selectedPeriod.value] = candidate;
215 return loadReport(); 228 return loadReport();
216 } 229 }
217 230
@@ -219,6 +232,7 @@ abstract class ReportPeriodLogic { @@ -219,6 +232,7 @@ abstract class ReportPeriodLogic {
219 final candidate = _nextCandidate; 232 final candidate = _nextCandidate;
220 if (_isAfterRange(candidate)) return Future.value(); 233 if (_isAfterRange(candidate)) return Future.value();
221 selectedDate.value = candidate; 234 selectedDate.value = candidate;
  235 + _datesByPeriod[selectedPeriod.value] = candidate;
222 return loadReport(); 236 return loadReport();
223 } 237 }
224 238
@@ -226,6 +240,12 @@ abstract class ReportPeriodLogic { @@ -226,6 +240,12 @@ abstract class ReportPeriodLogic {
226 240
227 void dispose() {} 241 void dispose() {}
228 242
  243 + void _seedPeriodDates(DateTime date) {
  244 + for (final period in supportedPeriods) {
  245 + _datesByPeriod[period] = normalizeDate(period, date);
  246 + }
  247 + }
  248 +
229 DateTime get _previousCandidate => switch (selectedPeriod.value) { 249 DateTime get _previousCandidate => switch (selectedPeriod.value) {
230 ReportPeriod.year => DateTime(selectedDate.value.year - 1), 250 ReportPeriod.year => DateTime(selectedDate.value.year - 1),
231 ReportPeriod.month => 251 ReportPeriod.month =>
@@ -1205,13 +1205,6 @@ class _ChartPlotFrame extends StatelessWidget { @@ -1205,13 +1205,6 @@ class _ChartPlotFrame extends StatelessWidget {
1205 ), 1205 ),
1206 ), 1206 ),
1207 ), 1207 ),
1208 - Padding(  
1209 - padding: const EdgeInsets.only(  
1210 - left: horizontalInset,  
1211 - right: rightAxisWidth,  
1212 - ),  
1213 - child: child,  
1214 - ),  
1215 Positioned.fill( 1208 Positioned.fill(
1216 bottom: bottomTitleHeight, 1209 bottom: bottomTitleHeight,
1217 child: CustomPaint( 1210 child: CustomPaint(
@@ -1222,6 +1215,13 @@ class _ChartPlotFrame extends StatelessWidget { @@ -1222,6 +1215,13 @@ class _ChartPlotFrame extends StatelessWidget {
1222 ), 1215 ),
1223 ), 1216 ),
1224 ), 1217 ),
  1218 + Padding(
  1219 + padding: const EdgeInsets.only(
  1220 + left: horizontalInset,
  1221 + right: rightAxisWidth,
  1222 + ),
  1223 + child: child,
  1224 + ),
1225 Positioned.fill( 1225 Positioned.fill(
1226 bottom: bottomTitleHeight, 1226 bottom: bottomTitleHeight,
1227 child: _RightAxisLabels( 1227 child: _RightAxisLabels(
1 class R { 1 class R {
2 static final String assetsImagesNavBackIcon = 2 static final String assetsImagesNavBackIcon =
3 'assets/images/common/ic_nav_back.webp'; 3 'assets/images/common/ic_nav_back.webp';
  4 + static final String assetsImagesCloseIcon =
  5 + 'assets/images/common/ic_close.png';
4 static final String assetsImagesHealthGoodArrow = 6 static final String assetsImagesHealthGoodArrow =
5 'assets/images/common/ic_health_good_arrow.webp'; 7 'assets/images/common/ic_health_good_arrow.webp';
6 static final String assetsImagesHealthBadArrow = 8 static final String assetsImagesHealthBadArrow =