From 603a158cf2f858d84bce96c2e5eb22e7fffff2a4 Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 1 Oct 2026 10:44:52 +0300 Subject: [PATCH] feat(diary): editing of previous and current finished days --- .../diary/presentation/bloc/diary_state.dart | 21 +- .../diary/presentation/diary_edit_access.dart | 38 +++ .../presentation/pages/diary_screen.dart | 112 ++++-- .../widgets/mood_rating_widget.dart | 171 +++++++--- .../widgets/photo_gallery_widget.dart | 45 ++- .../presentation/widgets/product_widget.dart | 138 +++++--- .../widgets/satiety_rating_widget.dart | 175 +++++++--- .../widgets/time_of_meal_picker_widget.dart | 20 +- .../presentation/diary_edit_access_test.dart | 59 ++++ .../pages/diary_screen_smoke_test.dart | 323 +++++++++++++++--- .../widgets/product_widget_smoke_test.dart | 239 ++++++++++--- 11 files changed, 1099 insertions(+), 242 deletions(-) create mode 100644 lib/features/diary/presentation/diary_edit_access.dart create mode 100644 test/features/diary/presentation/diary_edit_access_test.dart diff --git a/lib/features/diary/presentation/bloc/diary_state.dart b/lib/features/diary/presentation/bloc/diary_state.dart index 02209371..3e315668 100644 --- a/lib/features/diary/presentation/bloc/diary_state.dart +++ b/lib/features/diary/presentation/bloc/diary_state.dart @@ -2,6 +2,7 @@ import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/diary_edit_access.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'diary_state.freezed.dart'; @@ -12,7 +13,8 @@ abstract class DiaryState with _$DiaryState { const factory DiaryState.loading() = DiaryStateLoading; - const factory DiaryState.error({required String errorMessage}) = DiaryStateError; + const factory DiaryState.error({required String errorMessage}) = + DiaryStateError; const factory DiaryState.loaded({ required DiaryDayEntity diary, @@ -31,6 +33,19 @@ abstract class DiaryState with _$DiaryState { @Default(false) bool isLoadingAddingProductInMeal, }) = DiaryStateLoaded; - const factory DiaryState.loadedTomorrowWithDiet({required DiaryFutureDietDayEntity diary, required DateTime selectedDay, required int selectedMeal}) = - DiaryStateLoadedTomorrowWithDiet; + const factory DiaryState.loadedTomorrowWithDiet({ + required DiaryFutureDietDayEntity diary, + required DateTime selectedDay, + required int selectedMeal, + }) = DiaryStateLoadedTomorrowWithDiet; +} + +extension DiaryStateLoadedEditAccess on DiaryStateLoaded { + bool get canEditDiary => canEditDiaryDay( + isConsultantView: isConsultantView, + isDiaryCommented: isDiaryCommented, + isDiaryFinished: isDiaryFinished, + closed: diary.closed, + diaryDate: diary.date, + ); } diff --git a/lib/features/diary/presentation/diary_edit_access.dart b/lib/features/diary/presentation/diary_edit_access.dart new file mode 100644 index 00000000..235fafb9 --- /dev/null +++ b/lib/features/diary/presentation/diary_edit_access.dart @@ -0,0 +1,38 @@ +/// Finished and last-day-finished diaries stay editable on the calendar day +/// itself and on the next calendar day. Older finished days stay read-only. +const int diaryFinishedEditWindowInDays = 1; + +bool canEditDiaryDay({ + required bool isConsultantView, + required bool isDiaryCommented, + required bool isDiaryFinished, + required int closed, + required String diaryDate, + DateTime? now, +}) { + if (isConsultantView || isDiaryCommented || closed != 0) { + return false; + } + if (!isDiaryFinished) { + return true; + } + final day = _calendarDate(diaryDate); + if (day == null) { + return false; + } + final clock = now ?? DateTime.now(); + final today = DateTime(clock.year, clock.month, clock.day); + final oldestEditableDay = today.subtract( + const Duration(days: diaryFinishedEditWindowInDays), + ); + return !day.isBefore(oldestEditableDay) && !day.isAfter(today); +} + +DateTime? _calendarDate(String raw) { + final parsed = DateTime.tryParse(raw); + if (parsed == null) { + return null; + } + final local = parsed.toLocal(); + return DateTime(local.year, local.month, local.day); +} diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index 01c75db2..b91bcba7 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -74,7 +74,12 @@ class _DiaryScreenContentState extends State { } if (state is DiaryStateError) { return Scaffold( - body: DataErrorWidget(onReload: () => context.read().fetchDiaryV2(needLoading: false, diaryId: widget.diaryId)), + body: DataErrorWidget( + onReload: () => context.read().fetchDiaryV2( + needLoading: false, + diaryId: widget.diaryId, + ), + ), ); } @@ -84,8 +89,14 @@ class _DiaryScreenContentState extends State { ? AppBar(toolbarHeight: 0) : AppBar( title: Text( - state is DiaryStateLoaded ? DateTime.parse(state.diary.date).toLocal().format('d MMMM') : '', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + state is DiaryStateLoaded + ? DateTime.parse( + state.diary.date, + ).toLocal().format('d MMMM') + : '', + style: TextStyles.headingXLSemibold.copyWith( + color: appTheme.black, + ), ), centerTitle: true, ), @@ -95,26 +106,39 @@ class _DiaryScreenContentState extends State { child: Column( children: [ if (widget.diaryId == null) CalendarWidget(date: widget.date), - if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()], + if (state is DiaryStateLoading) ...[ + const SizedBox(height: 60), + const Loader(), + ], if (state is DiaryStateLoaded) WakeupAndRetireWidget( - isEnabled: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isWakeUpAndRetireEnabled, - disable: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, + isEnabled: + state.diary.typeOfDiary.hasChosenPlan || + state.isConsultantView || + state.isWakeUpAndRetireEnabled, + disable: !state.canEditDiary, ), const SizedBox(height: 12), if (state is DiaryStateLoaded) MealWidget( - showMood: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isMoodEnabled, - showAddButton: !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented, + showMood: + state.diary.typeOfDiary.hasChosenPlan || + state.isConsultantView || + state.isMoodEnabled, + showAddButton: state.canEditDiary, ), - if (state is DiaryStateLoadedTomorrowWithDiet) const MealWidget(showMood: false, showAddButton: false), + if (state is DiaryStateLoadedTomorrowWithDiet) + const MealWidget(showMood: false, showAddButton: false), if (state is DiaryStateLoaded) const CaloriesChartWidget(), const SizedBox(height: 12), Builder( builder: (context) { - if (state is DiaryStateLoaded && state.diary.typeOfDiary.hasChosenPlan) { + if (state is DiaryStateLoaded && + state.diary.typeOfDiary.hasChosenPlan) { if (state.diary.comment.isNotEmpty) { - return CommentForConsultantWidget(comment: state.diary.comment); + return CommentForConsultantWidget( + comment: state.diary.comment, + ); } if (state.isDiaryCommented || state.isDiaryFinished) { return const SizedBox.shrink(); @@ -124,7 +148,10 @@ class _DiaryScreenContentState extends State { return const SizedBox.shrink(); }, ), - if (state is DiaryStateLoaded && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) + if (state is DiaryStateLoaded && + !state.isConsultantView && + !state.isDiaryFinished && + !state.isDiaryCommented) Padding( padding: const EdgeInsets.only(top: 12), child: _isDiaryFinishing @@ -135,11 +162,13 @@ class _DiaryScreenContentState extends State { setState(() { _isDiaryFinishing = true; }); - final isFinished = await context.read().finishDiaryDay( - diaryId: state.diary.id, - wakeUp: state.diary.wakeup.toString(), - retire: state.diary.retire.toString(), - ); + final isFinished = await context + .read() + .finishDiaryDay( + diaryId: state.diary.id, + wakeUp: state.diary.wakeup.toString(), + retire: state.diary.retire.toString(), + ); if (!isFinished && context.mounted) { showErrorDialog(context); } @@ -149,33 +178,64 @@ class _DiaryScreenContentState extends State { }, ), ), - if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) ...[ + if (state is DiaryStateLoaded && + state.isConsultantView && + !state.isDiaryCommented) ...[ const SizedBox(height: 16), - const MainBackgroundBouble(padding: EdgeInsets.all(16), child: DayCommentWidget()), + const MainBackgroundBouble( + padding: EdgeInsets.all(16), + child: DayCommentWidget(), + ), ], - if (state is DiaryStateLoaded && state.consultantReview?.consultantComments.dayComment != null) ...[ + if (state is DiaryStateLoaded && + state.consultantReview?.consultantComments.dayComment != + null) ...[ const SizedBox(height: 12), MainBackgroundBouble( padding: const EdgeInsets.all(16), child: Column( children: [ - Text(s.dayComment, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), + Text( + s.dayComment, + style: TextStyles.textLSemibold.copyWith( + color: appTheme.black, + ), + ), const SizedBox(height: 12), - CommentContainer(comment: state.consultantReview?.consultantComments.dayComment ?? ''), + CommentContainer( + comment: + state + .consultantReview + ?.consultantComments + .dayComment ?? + '', + ), ], ), ), const SizedBox(height: 28), ], - if (state is DiaryStateLoaded && state.isConsultantView && state.isDiaryFinished && !state.isDiaryCommented) ...[ + if (state is DiaryStateLoaded && + state.isConsultantView && + state.isDiaryFinished && + !state.isDiaryCommented) ...[ const SizedBox(height: 16), CustomOutlinedButton( text: s.send, height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + buttonTextStyle: TextStyles.textMSemibold.copyWith( + color: appTheme.whiteA700, + ), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll( + appTheme.blueA400, + ), + ), onPressed: () async { - await context.read().submitComments(state.diary.id); + await context + .read() + .submitComments(state.diary.id); }, ), const SizedBox(height: 16), diff --git a/lib/features/diary/presentation/widgets/mood_rating_widget.dart b/lib/features/diary/presentation/widgets/mood_rating_widget.dart index d943eac2..c6057764 100644 --- a/lib/features/diary/presentation/widgets/mood_rating_widget.dart +++ b/lib/features/diary/presentation/widgets/mood_rating_widget.dart @@ -26,7 +26,8 @@ class _MoodRatingWidgetState extends State { final meals = state.diary.meals; if (selectedIndex >= 0 && selectedIndex < meals.length) { if (isFirstFetch[selectedIndex]) { - currentSelectedRating[selectedIndex] = meals[selectedIndex].mood.toDouble() + 1; + currentSelectedRating[selectedIndex] = + meals[selectedIndex].mood.toDouble() + 1; isFirstFetch[selectedIndex] = false; } } @@ -36,21 +37,30 @@ class _MoodRatingWidgetState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - state.isConsultantView ? 'Настроение пользователя после приема пищи' : 'Ваше настроение после приёма пищи', + state.isConsultantView + ? 'Настроение пользователя после приема пищи' + : 'Ваше настроение после приёма пищи', textAlign: TextAlign.center, style: const TextStyle(fontSize: 16), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SvgPicture.asset('assets/icons/mood_1.svg', height: 32, width: 32), + SvgPicture.asset( + 'assets/icons/mood_1.svg', + height: 32, + width: 32, + ), RatingBar.builder( - ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, - initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length) + ignoreGestures: !state.canEditDiary, + initialRating: + (selectedIndex >= 0 && + selectedIndex < state.diary.meals.length) ? state.diary.meals[selectedIndex].mood.toDouble() + 1 : 3, itemCount: 6, - itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6), + itemSize: + MediaQuery.of(context).size.width / 6 - (124 / 6), updateOnDrag: true, itemBuilder: (context, index) { switch (index) { @@ -58,82 +68,154 @@ class _MoodRatingWidgetState extends State { return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 1 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 1 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFFC82222), - border: currentSelectedRating[selectedIndex] == 1 - ? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 1 + ? Border.all( + width: 2, + color: const Color(0xFFAA3333), + strokeAlign: + BorderSide.strokeAlignOutside, + ) : null, - borderRadius: currentSelectedRating[selectedIndex] == 1 - ? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2)) - : const BorderRadius.horizontal(left: Radius.circular(8)), + borderRadius: + currentSelectedRating[selectedIndex] == 1 + ? const BorderRadius.horizontal( + left: Radius.circular(8), + right: Radius.circular(2), + ) + : const BorderRadius.horizontal( + left: Radius.circular(8), + ), ), ); case 1: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 2 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 2 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFFFF3E3E), - border: currentSelectedRating[selectedIndex] == 2 - ? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 2 + ? Border.all( + width: 2, + color: const Color(0xFF550000), + strokeAlign: + BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: + currentSelectedRating[selectedIndex] == 2 + ? BorderRadius.circular(2) : null, - borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null, ), ); case 2: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 3 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 3 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFFF8C926), - border: currentSelectedRating[selectedIndex] == 3 - ? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 3 + ? Border.all( + width: 2, + color: const Color(0xFF8C5D00), + strokeAlign: + BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: + currentSelectedRating[selectedIndex] == 3 + ? BorderRadius.circular(2) : null, - borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null, ), ); case 3: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 4 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 4 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFFFFF3BF), - border: currentSelectedRating[selectedIndex] == 4 - ? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 4 + ? Border.all( + width: 2, + color: const Color(0xFFB5911B), + strokeAlign: + BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: + currentSelectedRating[selectedIndex] == 4 + ? BorderRadius.circular(2) : null, - borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null, ), ); case 4: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 5 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 5 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFF62E833), - border: currentSelectedRating[selectedIndex] == 5 - ? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 5 + ? Border.all( + width: 2, + color: const Color(0xFF276918), + strokeAlign: + BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: + currentSelectedRating[selectedIndex] == 5 + ? BorderRadius.circular(2) : null, - borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null, ), ); default: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 6 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 6 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFF4DCE20), - border: currentSelectedRating[selectedIndex] == 6 - ? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 6 + ? Border.all( + width: 2, + color: const Color(0xFF184D12), + strokeAlign: + BorderSide.strokeAlignOutside, + ) : null, - borderRadius: currentSelectedRating[selectedIndex] == 6 - ? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8)) - : const BorderRadius.horizontal(right: Radius.circular(8)), + borderRadius: + currentSelectedRating[selectedIndex] == 6 + ? const BorderRadius.horizontal( + left: Radius.circular(2), + right: Radius.circular(8), + ) + : const BorderRadius.horizontal( + right: Radius.circular(8), + ), ), ); } @@ -142,12 +224,25 @@ class _MoodRatingWidgetState extends State { setState(() { currentSelectedRating[selectedIndex] = value; }); - EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () { - context.read().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1); - }); + EasyDebounce.debounce( + 'change-satiety', + const Duration(milliseconds: 250), + () { + context.read().fillMood( + diaryId: state.diary.id, + mood: + currentSelectedRating[selectedIndex].toInt() - + 1, + ); + }, + ); }, ), - SvgPicture.asset('assets/icons/mood_2.svg', height: 32, width: 32), + SvgPicture.asset( + 'assets/icons/mood_2.svg', + height: 32, + width: 32, + ), ], ), ], diff --git a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart index bdb7c53a..cbd65add 100644 --- a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart +++ b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart @@ -52,7 +52,9 @@ class _PhotoGalleryWidgetState extends State { AppRouter.navigatorKey.currentContext?.pop(); final picker = ImagePicker(); - XFile? image = await picker.pickImage(source: ImageSource.camera); + XFile? image = await picker.pickImage( + source: ImageSource.camera, + ); if (image != null) { diaryCubit.uploadPhotos( files: [File(image.path)], @@ -116,7 +118,8 @@ class _PhotoGalleryWidgetState extends State { builder: (context, state) { if (state is DiaryStateLoaded) { int mealIndex = state.selectedMeal - 1; - if (state.diary.meals[mealIndex].photos.isEmpty && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) { + if (state.diary.meals[mealIndex].photos.isEmpty && + state.canEditDiary) { return GestureDetector( onTap: () async { showImagePicker(state.diary); @@ -140,10 +143,11 @@ class _PhotoGalleryWidgetState extends State { ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( - imageUrl: '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}', + imageUrl: + '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}', ), ), - if (!state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) + if (state.canEditDiary) Padding( padding: const EdgeInsets.all(12), child: IconButton.filled( @@ -153,12 +157,24 @@ class _PhotoGalleryWidgetState extends State { ), onPressed: () async { await diaryCubit.deletePhoto( - photo: state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex], + photo: state + .diary + .meals[mealIndex] + .photos[currentSelectedPhotoIndex], diaryId: state.diary.id, ); setState(() { - if (currentSelectedPhotoIndex == state.diary.meals[mealIndex].photos.length - 1 && currentSelectedPhotoIndex != 0) { - currentSelectedPhotoIndex = state.diary.meals[mealIndex].photos.length - 2; + if (currentSelectedPhotoIndex == + state + .diary + .meals[mealIndex] + .photos + .length - + 1 && + currentSelectedPhotoIndex != 0) { + currentSelectedPhotoIndex = + state.diary.meals[mealIndex].photos.length - + 2; } }); }, @@ -178,8 +194,13 @@ class _PhotoGalleryWidgetState extends State { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ - for (int i = 0; i < state.diary.meals[mealIndex].photos.length + 1; i++) ...[ - if (i == state.diary.meals[mealIndex].photos.length && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) + for ( + int i = 0; + i < state.diary.meals[mealIndex].photos.length + 1; + i++ + ) ...[ + if (i == state.diary.meals[mealIndex].photos.length && + state.canEditDiary) Padding( padding: const EdgeInsets.only(left: 8), child: InkWell( @@ -212,13 +233,15 @@ class _PhotoGalleryWidgetState extends State { height: 52, width: 52, child: CachedNetworkImage( - imageUrl: '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[i].path}', + imageUrl: + '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[i].path}', fit: BoxFit.fitWidth, ), ), ), ), - if (i < state.diary.meals[mealIndex].photos.length - 1) const SizedBox(width: 8), + if (i < state.diary.meals[mealIndex].photos.length - 1) + const SizedBox(width: 8), ], ], ), diff --git a/lib/features/diary/presentation/widgets/product_widget.dart b/lib/features/diary/presentation/widgets/product_widget.dart index cdf34f28..ccf0d0dd 100644 --- a/lib/features/diary/presentation/widgets/product_widget.dart +++ b/lib/features/diary/presentation/widgets/product_widget.dart @@ -1,8 +1,6 @@ - import 'package:collection/collection.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; @@ -32,17 +30,26 @@ class _ProductWidgetState extends State { super.initState(); diaryCubit = context.read(); portionName = widget.portion.portion == 'gr' ? 'гр' : 'шт'; - isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false; + isEaten = widget.portion.portionState == PortionState.eatenFromDiet + ? true + : false; } Widget _buildNutritionalInfo() { if (widget.portion.productNutritionalValueId == null) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), child: const Text( 'Продукт отправлен на модерацию', - style: TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w500), + style: TextStyle( + color: Color(0xFF2F80ED), + fontSize: 12, + fontWeight: FontWeight.w500, + ), ), ); } @@ -57,7 +64,8 @@ class _ProductWidgetState extends State { calories = (caloriesInfo.calories * widget.portion.quantity) / 100; proteins = (caloriesInfo.proteins * widget.portion.quantity) / 100; fats = (caloriesInfo.fats * widget.portion.quantity) / 100; - carbohydrates = (caloriesInfo.carbohydrates * widget.portion.quantity) / 100; + carbohydrates = + (caloriesInfo.carbohydrates * widget.portion.quantity) / 100; } else { calories = caloriesInfo.energyOnPortion; proteins = caloriesInfo.proteinsOnPortion; @@ -81,10 +89,17 @@ class _ProductWidgetState extends State { Widget _buildNutritionChip(String label, int value) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), child: Text( '$label $value', - style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + style: const TextStyle( + color: Color(0xFF6D6F71), + fontSize: 12, + fontWeight: FontWeight.w500, + ), ), ); } @@ -93,16 +108,24 @@ class _ProductWidgetState extends State { void didUpdateWidget(ProductWidget oldWidget) { super.didUpdateWidget(oldWidget); portionName = widget.portion.portion == 'gr' ? 'гр' : 'шт'; - isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false; + isEaten = widget.portion.portionState == PortionState.eatenFromDiet + ? true + : false; } Future updateProductInMeal(DiaryStateLoaded state, PortionModel portion) { return showModalBottomSheet( useRootNavigator: true, context: context, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), isScrollControlled: true, - builder: (context) => AddProductModal(state: state, portion: portion, diaryCubit: diaryCubit), + builder: (context) => AddProductModal( + state: state, + portion: portion, + diaryCubit: diaryCubit, + ), ); } @@ -113,33 +136,36 @@ class _ProductWidgetState extends State { if (state is DiaryStateLoadedTomorrowWithDiet) { return ListTile( horizontalTitleGap: 0, - contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 12) : null, + contentPadding: widget.portion.fromDiet + ? const EdgeInsets.only(right: 24, left: 12) + : null, title: Text(widget.portion.productName), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + Text( + '${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}', + ), const SizedBox(height: 4), _buildNutritionalInfo(), ], ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), tileColor: const Color(0xFFF6FAFF), ); } if (state is DiaryStateLoaded) { - final isDiaryCommented = - state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented || - state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || - state.diary.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; - final isDiaryFinished = - state.diary.typeOfDiary.diaryState == DiaryStateEnum.finished || state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished; - final canEditPortion = !state.isConsultantView && !isDiaryCommented && !isDiaryFinished && !widget.portion.fromDiet; + final canEditPortion = state.canEditDiary && !widget.portion.fromDiet; final consultantComments = state.consultantReview?.consultantComments; - final canMarkAsEaten = !state.isConsultantView && !isDiaryCommented && !isDiaryFinished; + final canMarkAsEaten = state.canEditDiary; PortionComment? portionComment; if (consultantComments != null) { - portionComment = consultantComments.portionsComments.firstWhereOrNull((elem) => elem.portionId == widget.portion.id); + portionComment = consultantComments.portionsComments + .firstWhereOrNull( + (elem) => elem.portionId == widget.portion.id, + ); } if (widget.portion.portionState == PortionState.eatenDeleted) { return DottedBorder( @@ -151,27 +177,41 @@ class _ProductWidgetState extends State { color: Color(0xFF6D6F71), ), child: ListTile( - onTap: () => diaryCubit.restore(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id), - trailing: SvgPicture.asset('assets/icons/revert_product.svg'), + onTap: state.canEditDiary + ? () => diaryCubit.restore( + diaryId: state.diary.id, + mealId: state.selectedMeal, + portionId: widget.portion.id, + ) + : null, + trailing: state.canEditDiary + ? SvgPicture.asset('assets/icons/revert_product.svg') + : null, horizontalTitleGap: 0, - contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null, + contentPadding: widget.portion.fromDiet + ? const EdgeInsets.only(right: 24, left: 4) + : null, title: Text(widget.portion.productName), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + Text( + '${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}', + ), const SizedBox(height: 4), _buildNutritionalInfo(), ], ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), // ignore: use_full_hex_values_for_flutter_colors tileColor: const Color(0xfff6faff03).withValues(alpha: 0.01), ), ); } else { Widget? trailing; - if (state.isConsultantView && !isDiaryCommented) { + if (state.isConsultantView && !state.isDiaryCommented) { trailing = GestureDetector( onTap: () { setState(() { @@ -180,13 +220,15 @@ class _ProductWidgetState extends State { }, child: SvgPicture.asset('assets/icons/edit.svg'), ); - } else if (!isDiaryCommented && !widget.portion.fromDiet) { + } else if (canEditPortion) { trailing = const Icon(Icons.more_vert_rounded); } return Column( children: [ ListTile( - onTap: canEditPortion ? () => updateProductInMeal(state, widget.portion) : null, + onTap: canEditPortion + ? () => updateProductInMeal(state, widget.portion) + : null, trailing: trailing, leading: widget.portion.fromDiet ? Transform.scale( @@ -199,35 +241,55 @@ class _ProductWidgetState extends State { isEaten = value!; }); if (isEaten) { - diaryCubit.markEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); + diaryCubit.markEaten( + diaryId: state.diary.id, + mealId: state.selectedMeal, + portionId: widget.portion.id, + ); } else { - diaryCubit.unmarkEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); + diaryCubit.unmarkEaten( + diaryId: state.diary.id, + mealId: state.selectedMeal, + portionId: widget.portion.id, + ); } } : null, activeColor: appTheme.blueA400, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), side: const BorderSide(color: Color(0xFF809CB9)), ), ) : null, horizontalTitleGap: 0, - contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null, + contentPadding: widget.portion.fromDiet + ? const EdgeInsets.only(right: 24, left: 4) + : null, title: Text(widget.portion.productName), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + Text( + '${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}', + ), const SizedBox(height: 4), _buildNutritionalInfo(), ], ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), tileColor: const Color(0xFFF6FAFF), ), Padding( padding: const EdgeInsets.only(top: 8.0), - child: PortionCommentWidget(enabled: showCommentWidget, portionId: widget.portion.id, comment: portionComment?.comment), + child: PortionCommentWidget( + enabled: showCommentWidget, + portionId: widget.portion.id, + comment: portionComment?.comment, + ), ), ], ); diff --git a/lib/features/diary/presentation/widgets/satiety_rating_widget.dart b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart index d36ac75b..a463196d 100644 --- a/lib/features/diary/presentation/widgets/satiety_rating_widget.dart +++ b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart @@ -26,7 +26,8 @@ class _SatietyRatingWidgetState extends State { final meals = state.diary.meals; if (selectedIndex >= 0 && selectedIndex < meals.length) { if (isFirstFetch[selectedIndex]) { - currentSelectedRating[selectedIndex] = meals[selectedIndex].satiety.toDouble() + 1; + currentSelectedRating[selectedIndex] = + meals[selectedIndex].satiety.toDouble() + 1; isFirstFetch[selectedIndex] = false; } } @@ -36,21 +37,32 @@ class _SatietyRatingWidgetState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - state.isConsultantView ? 'Сытость пользователя после приема пищи' : 'Ваша сытость после приёма пищи', + state.isConsultantView + ? 'Сытость пользователя после приема пищи' + : 'Ваша сытость после приёма пищи', textAlign: TextAlign.center, style: const TextStyle(fontSize: 16), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SvgPicture.asset('assets/icons/satiety_emoji_left.svg', height: 32, width: 32), + SvgPicture.asset( + 'assets/icons/satiety_emoji_left.svg', + height: 32, + width: 32, + ), RatingBar.builder( - ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, - initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length) - ? state.diary.meals[selectedIndex].satiety.toDouble() + 1 + ignoreGestures: !state.canEditDiary, + initialRating: + (selectedIndex >= 0 && + selectedIndex < state.diary.meals.length) + ? state.diary.meals[selectedIndex].satiety + .toDouble() + + 1 : 3, itemCount: 6, - itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6), + itemSize: + MediaQuery.of(context).size.width / 6 - (124 / 6), updateOnDrag: true, itemBuilder: (context, index) { switch (index) { @@ -58,82 +70,154 @@ class _SatietyRatingWidgetState extends State { return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 1 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 1 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFFC82222), - border: currentSelectedRating[selectedIndex] == 1 - ? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 1 + ? Border.all( + width: 2, + color: const Color(0xFFAA3333), + strokeAlign: + BorderSide.strokeAlignOutside, + ) : null, - borderRadius: currentSelectedRating[selectedIndex] == 1 - ? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2)) - : const BorderRadius.horizontal(left: Radius.circular(8)), + borderRadius: + currentSelectedRating[selectedIndex] == 1 + ? const BorderRadius.horizontal( + left: Radius.circular(8), + right: Radius.circular(2), + ) + : const BorderRadius.horizontal( + left: Radius.circular(8), + ), ), ); case 1: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 2 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 2 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFFFF3E3E), - border: currentSelectedRating[selectedIndex] == 2 - ? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 2 + ? Border.all( + width: 2, + color: const Color(0xFF550000), + strokeAlign: + BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: + currentSelectedRating[selectedIndex] == 2 + ? BorderRadius.circular(2) : null, - borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null, ), ); case 2: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 3 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 3 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFFF8C926), - border: currentSelectedRating[selectedIndex] == 3 - ? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 3 + ? Border.all( + width: 2, + color: const Color(0xFF8C5D00), + strokeAlign: + BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: + currentSelectedRating[selectedIndex] == 3 + ? BorderRadius.circular(2) : null, - borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null, ), ); case 3: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 4 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 4 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFFFFF3BF), - border: currentSelectedRating[selectedIndex] == 4 - ? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 4 + ? Border.all( + width: 2, + color: const Color(0xFFB5911B), + strokeAlign: + BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: + currentSelectedRating[selectedIndex] == 4 + ? BorderRadius.circular(2) : null, - borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null, ), ); case 4: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 5 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 5 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFF62E833), - border: currentSelectedRating[selectedIndex] == 5 - ? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 5 + ? Border.all( + width: 2, + color: const Color(0xFF276918), + strokeAlign: + BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: + currentSelectedRating[selectedIndex] == 5 + ? BorderRadius.circular(2) : null, - borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null, ), ); default: return Container( width: MediaQuery.of(context).size.width / 6, height: 24, - margin: currentSelectedRating[selectedIndex] == 6 ? const EdgeInsets.symmetric(horizontal: 2) : null, + margin: currentSelectedRating[selectedIndex] == 6 + ? const EdgeInsets.symmetric(horizontal: 2) + : null, decoration: BoxDecoration( color: const Color(0xFF4DCE20), - border: currentSelectedRating[selectedIndex] == 6 - ? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside) + border: + currentSelectedRating[selectedIndex] == 6 + ? Border.all( + width: 2, + color: const Color(0xFF184D12), + strokeAlign: + BorderSide.strokeAlignOutside, + ) : null, - borderRadius: currentSelectedRating[selectedIndex] == 6 - ? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8)) - : const BorderRadius.horizontal(right: Radius.circular(8)), + borderRadius: + currentSelectedRating[selectedIndex] == 6 + ? const BorderRadius.horizontal( + left: Radius.circular(2), + right: Radius.circular(8), + ) + : const BorderRadius.horizontal( + right: Radius.circular(8), + ), ), ); } @@ -143,12 +227,25 @@ class _SatietyRatingWidgetState extends State { setState(() { currentSelectedRating[selectedIndex] = value; }); - EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () { - context.read().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1); - }); + EasyDebounce.debounce( + 'change-satiety', + const Duration(milliseconds: 250), + () { + context.read().fillSatiety( + diaryId: state.diary.id, + satiety: + currentSelectedRating[selectedIndex].toInt() - + 1, + ); + }, + ); }, ), - SvgPicture.asset('assets/icons/satiety_emoji_right.svg', height: 32, width: 32), + SvgPicture.asset( + 'assets/icons/satiety_emoji_right.svg', + height: 32, + width: 32, + ), ], ), ], diff --git a/lib/features/diary/presentation/widgets/time_of_meal_picker_widget.dart b/lib/features/diary/presentation/widgets/time_of_meal_picker_widget.dart index a9086d88..eb85f711 100644 --- a/lib/features/diary/presentation/widgets/time_of_meal_picker_widget.dart +++ b/lib/features/diary/presentation/widgets/time_of_meal_picker_widget.dart @@ -46,7 +46,8 @@ class _TimeOfMealPickerWidgetState extends State { Navigator.of(context).pop(); diaryCubit.fillTime( diaryId: diaryId, - time: '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:00', + time: + '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:00', ); }, borderRadius: BorderRadius.circular(20), @@ -69,7 +70,7 @@ class _TimeOfMealPickerWidgetState extends State { ), ), ), - const SizedBox(height: 30) + const SizedBox(height: 30), ], ), ), @@ -98,7 +99,9 @@ class _TimeOfMealPickerWidgetState extends State { ), ), InkWell( - onTap: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented ? null : () => showTimePicker(state.diary.id), + onTap: state.canEditDiary + ? () => showTimePicker(state.diary.id) + : null, borderRadius: BorderRadius.circular(100), child: Ink( height: 32, @@ -110,7 +113,16 @@ class _TimeOfMealPickerWidgetState extends State { alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - state.diary.meals[state.selectedMeal - 1].time.substring(0, state.diary.meals[state.selectedMeal - 1].time.length - 3), + state.diary.meals[state.selectedMeal - 1].time + .substring( + 0, + state + .diary + .meals[state.selectedMeal - 1] + .time + .length - + 3, + ), style: const TextStyle( color: Color(0xFF6D6F71), fontSize: 17, diff --git a/test/features/diary/presentation/diary_edit_access_test.dart b/test/features/diary/presentation/diary_edit_access_test.dart new file mode 100644 index 00000000..012fbbec --- /dev/null +++ b/test/features/diary/presentation/diary_edit_access_test.dart @@ -0,0 +1,59 @@ +import 'package:eatvio_app/features/diary/presentation/diary_edit_access.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final now = DateTime(2026, 9, 30, 12); + + bool canEdit({ + bool isConsultantView = false, + bool isDiaryCommented = false, + bool isDiaryFinished = false, + int closed = 0, + String diaryDate = '2026-09-28', + }) { + return canEditDiaryDay( + isConsultantView: isConsultantView, + isDiaryCommented: isDiaryCommented, + isDiaryFinished: isDiaryFinished, + closed: closed, + diaryDate: diaryDate, + now: now, + ); + } + + test('finished today and yesterday stay editable', () { + expect(canEdit(isDiaryFinished: true, diaryDate: '2026-09-30'), isTrue); + expect(canEdit(isDiaryFinished: true, diaryDate: '2026-09-29'), isTrue); + }); + + test('finished days older than yesterday stay read-only', () { + expect(canEdit(isDiaryFinished: true, diaryDate: '2026-09-28'), isFalse); + }); + + test('commented days stay read-only inside the window', () { + expect(canEdit(isDiaryCommented: true, diaryDate: '2026-09-30'), isFalse); + expect( + canEdit( + isDiaryCommented: true, + isDiaryFinished: true, + diaryDate: '2026-09-29', + ), + isFalse, + ); + }); + + test('an open day outside the window stays editable', () { + expect(canEdit(diaryDate: '2026-09-01'), isTrue); + }); + + test('a closed flag blocks editing even inside the window', () { + expect( + canEdit(isDiaryFinished: true, closed: 1, diaryDate: '2026-09-30'), + isFalse, + ); + }); + + test('consultant view stays read-only', () { + expect(canEdit(isConsultantView: true, diaryDate: '2026-09-30'), isFalse); + }); +} diff --git a/test/features/diary/presentation/pages/diary_screen_smoke_test.dart b/test/features/diary/presentation/pages/diary_screen_smoke_test.dart index 3b595b46..cf2f669a 100644 --- a/test/features/diary/presentation/pages/diary_screen_smoke_test.dart +++ b/test/features/diary/presentation/pages/diary_screen_smoke_test.dart @@ -25,16 +25,25 @@ import 'package:mockito/mockito.dart'; import 'diary_screen_smoke_test.mocks.dart'; -@GenerateNiceMocks([MockSpec(), MockSpec()]) +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) void main() { group('DiaryScreen smoke', () { setUpAll(() async { - dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + dotenv.loadFromString( + envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost', + ); }); setUp(() async { await sl.reset(); - sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + sl.registerLazySingleton( + () => + AppConfig(buildType: AppBuildType.tests) + ..setBaseUrl('http://localhost'), + ); }); Widget wrap(Widget child) { @@ -51,23 +60,53 @@ void main() { ); } - DiaryStateLoaded loadedState({required bool hasChosenPlan}) { + DiaryStateLoaded loadedState({ + required bool hasChosenPlan, + String date = '2025-01-01', + bool isDiaryFinished = false, + bool isDiaryCommented = false, + int closed = 0, + }) { final diary = DiaryDayEntity( id: 1, - date: '2025-01-01', + date: date, wakeup: '07:00', retire: '23:00', - closed: 0, + closed: closed, comment: '', - typeOfDiary: TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: hasChosenPlan, diaryState: null), - meals: const [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], + typeOfDiary: TypeOfDiaryModel( + hasChosenDiet: false, + hasChosenPlan: hasChosenPlan, + diaryState: null, + ), + meals: const [ + MealModel( + id: 1, + time: '08:00', + typeNumber: 1, + satiety: 0, + mood: 0, + portions: [], + photos: [], + ), + ], chosenPlanId: hasChosenPlan ? 10 : null, dayOfChosenDiet: null, diet: null, ); const nutrition = NutritionDataModel( - dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), - dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionIntake: NutritionModel( + energy: 0, + proteins: 0, + fats: 0, + carbohydrates: 0, + ), + dailyNutritionRecommendation: NutritionModel( + energy: 0, + proteins: 0, + fats: 0, + carbohydrates: 0, + ), ); return DiaryState.loaded( diary: diary, @@ -75,8 +114,8 @@ void main() { selectedDay: DateTime(2025, 1, 1), selectedMeal: 1, isConsultantView: false, - isDiaryFinished: false, - isDiaryCommented: false, + isDiaryFinished: isDiaryFinished, + isDiaryCommented: isDiaryCommented, isWakeUpAndRetireEnabled: true, isMoodEnabled: true, ) @@ -86,13 +125,26 @@ void main() { testWidgets('renders loading state', (tester) async { final diaryCubit = MockDiaryCubit(); when(diaryCubit.state).thenReturn(const DiaryState.loading()); - when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when( + diaryCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when( + diaryCubit.fetchDiaryV2( + needLoading: anyNamed('needLoading'), + diaryId: anyNamed('diaryId'), + ), + ).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); - when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); - when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.state).thenReturn( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); + when( + commentsCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(commentsCubit.close()).thenAnswer((_) async => Future.value()); sl.registerFactory(() => diaryCubit); @@ -107,13 +159,27 @@ void main() { testWidgets('renders loaded state (chosen plan)', (tester) async { final diaryCubit = MockDiaryCubit(); when(diaryCubit.state).thenReturn(loadedState(hasChosenPlan: true)); - when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when( + diaryCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when( + diaryCubit.fetchDiaryV2( + needLoading: anyNamed('needLoading'), + diaryId: anyNamed('diaryId'), + ), + ).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); - when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false)); - when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.state).thenReturn( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + dayCommentNeedsToBeFilled: false, + ), + ); + when( + commentsCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(commentsCubit.close()).thenAnswer((_) async => Future.value()); sl.registerFactory(() => diaryCubit); @@ -124,19 +190,37 @@ void main() { expect(find.byType(DiaryScreen), findsOneWidget); - expect(find.text('Дополнительная информация для консультанта'), findsOneWidget); + expect( + find.text('Дополнительная информация для консультанта'), + findsOneWidget, + ); }); testWidgets('renders error state and reload button', (tester) async { final diaryCubit = MockDiaryCubit(); - when(diaryCubit.state).thenReturn(const DiaryState.error(errorMessage: 'err')); - when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when( + diaryCubit.state, + ).thenReturn(const DiaryState.error(errorMessage: 'err')); + when( + diaryCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when( + diaryCubit.fetchDiaryV2( + needLoading: anyNamed('needLoading'), + diaryId: anyNamed('diaryId'), + ), + ).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); - when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); - when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.state).thenReturn( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); + when( + commentsCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(commentsCubit.close()).thenAnswer((_) async => Future.value()); sl.registerFactory(() => diaryCubit); @@ -153,19 +237,52 @@ void main() { final diaryCubit = MockDiaryCubit(); const futureDiary = DiaryFutureDietDayEntity( date: '2025-01-02', - typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null), - meals: [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], + typeOfDiary: TypeOfDiaryModel( + hasChosenDiet: true, + hasChosenPlan: false, + diaryState: null, + ), + meals: [ + MealModel( + id: 1, + time: '08:00', + typeNumber: 1, + satiety: 0, + mood: 0, + portions: [], + photos: [], + ), + ], diet: null, dayOfChosenDiet: 1, ); - when(diaryCubit.state).thenReturn(DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1)); - when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when(diaryCubit.state).thenReturn( + DiaryState.loadedTomorrowWithDiet( + diary: futureDiary, + selectedDay: DateTime(2025, 1, 2), + selectedMeal: 1, + ), + ); + when( + diaryCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when( + diaryCubit.fetchDiaryV2( + needLoading: anyNamed('needLoading'), + diaryId: anyNamed('diaryId'), + ), + ).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); - when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); - when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.state).thenReturn( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); + when( + commentsCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(commentsCubit.close()).thenAnswer((_) async => Future.value()); sl.registerFactory(() => diaryCubit); @@ -187,17 +304,35 @@ void main() { planName: 'Plan', planDuration: 30, currentPlanDay: 1, - consultantComments: ConsultantComments(dayComment: 'Nice', mealsComments: const [], portionsComments: const []), + consultantComments: ConsultantComments( + dayComment: 'Nice', + mealsComments: const [], + portionsComments: const [], + ), ), ); when(diaryCubit.state).thenReturn(state); - when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when( + diaryCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when( + diaryCubit.fetchDiaryV2( + needLoading: anyNamed('needLoading'), + diaryId: anyNamed('diaryId'), + ), + ).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); - when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false)); - when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.state).thenReturn( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + dayCommentNeedsToBeFilled: false, + ), + ); + when( + commentsCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(commentsCubit.close()).thenAnswer((_) async => Future.value()); when(commentsCubit.submitComments(any)).thenAnswer((_) async => true); @@ -215,13 +350,26 @@ void main() { final diaryCubit = MockDiaryCubit(); final state = loadedState(hasChosenPlan: false); when(diaryCubit.state).thenReturn(state); - when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when( + diaryCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when( + diaryCubit.fetchDiaryV2( + needLoading: anyNamed('needLoading'), + diaryId: anyNamed('diaryId'), + ), + ).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); - when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); - when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.state).thenReturn( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); + when( + commentsCubit.stream, + ).thenAnswer((_) => const Stream.empty()); when(commentsCubit.close()).thenAnswer((_) async => Future.value()); sl.registerFactory(() => diaryCubit); @@ -234,5 +382,96 @@ void main() { expect(find.byType(Text), findsWidgets); }); + + testWidgets('shows add button for a finished day inside the edit window', ( + tester, + ) async { + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + final diaryCubit = MockDiaryCubit(); + when(diaryCubit.state).thenReturn( + loadedState( + hasChosenPlan: false, + date: _calendarDate(yesterday), + isDiaryFinished: true, + ), + ); + when( + diaryCubit.stream, + ).thenAnswer((_) => const Stream.empty()); + when(diaryCubit.close()).thenAnswer((_) async => Future.value()); + when( + diaryCubit.fetchDiaryV2( + needLoading: anyNamed('needLoading'), + diaryId: anyNamed('diaryId'), + ), + ).thenAnswer((_) async => true); + + final commentsCubit = MockConsultantCommentsCubit(); + when(commentsCubit.state).thenReturn( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); + when( + commentsCubit.stream, + ).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + sl.registerFactory(() => diaryCubit); + sl.registerFactory(() => commentsCubit); + + await tester.pumpWidget(wrap(DiaryScreen(date: yesterday))); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.text('Добавить'), findsOneWidget); + expect(find.text('Закончить день'), findsNothing); + }); + + testWidgets('hides add button for a commented day', (tester) async { + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + final diaryCubit = MockDiaryCubit(); + when(diaryCubit.state).thenReturn( + loadedState( + hasChosenPlan: false, + date: _calendarDate(yesterday), + isDiaryCommented: true, + ), + ); + when( + diaryCubit.stream, + ).thenAnswer((_) => const Stream.empty()); + when(diaryCubit.close()).thenAnswer((_) async => Future.value()); + when( + diaryCubit.fetchDiaryV2( + needLoading: anyNamed('needLoading'), + diaryId: anyNamed('diaryId'), + ), + ).thenAnswer((_) async => true); + + final commentsCubit = MockConsultantCommentsCubit(); + when(commentsCubit.state).thenReturn( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); + when( + commentsCubit.stream, + ).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + sl.registerFactory(() => diaryCubit); + sl.registerFactory(() => commentsCubit); + + await tester.pumpWidget(wrap(DiaryScreen(date: yesterday))); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.text('Добавить'), findsNothing); + }); }); } + +String _calendarDate(DateTime date) { + final month = date.month.toString().padLeft(2, '0'); + final day = date.day.toString().padLeft(2, '0'); + return '${date.year}-$month-$day'; +} diff --git a/test/features/diary/presentation/widgets/product_widget_smoke_test.dart b/test/features/diary/presentation/widgets/product_widget_smoke_test.dart index d80c99a1..89ac08cd 100644 --- a/test/features/diary/presentation/widgets/product_widget_smoke_test.dart +++ b/test/features/diary/presentation/widgets/product_widget_smoke_test.dart @@ -1,4 +1,3 @@ - import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; @@ -27,7 +26,8 @@ class FakeDiaryCubit extends Cubit implements DiaryCubit { dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } -class FakeConsultantCommentsCubit extends Cubit implements ConsultantCommentsCubit { +class FakeConsultantCommentsCubit extends Cubit + implements ConsultantCommentsCubit { FakeConsultantCommentsCubit(super.initialState); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); @@ -60,9 +60,16 @@ void main() { '''; - final bundle = _FakeAssetBundle({'assets/icons/revert_product.svg': fakeSvg, 'assets/icons/edit.svg': fakeSvg}); + final bundle = _FakeAssetBundle({ + 'assets/icons/revert_product.svg': fakeSvg, + 'assets/icons/edit.svg': fakeSvg, + }); - Widget wrap({required DiaryCubit diaryCubit, required ConsultantCommentsCubit consultantCommentsCubit, required Widget child}) { + Widget wrap({ + required DiaryCubit diaryCubit, + required ConsultantCommentsCubit consultantCommentsCubit, + required Widget child, + }) { return DefaultAssetBundle( bundle: bundle, child: MaterialApp( @@ -77,7 +84,9 @@ void main() { home: MultiBlocProvider( providers: [ BlocProvider.value(value: diaryCubit), - BlocProvider.value(value: consultantCommentsCubit), + BlocProvider.value( + value: consultantCommentsCubit, + ), ], child: Scaffold(body: child), ), @@ -85,17 +94,36 @@ void main() { ); } - DiaryStateLoaded loadedState({required PortionModel portion, required bool consultantView}) { + DiaryStateLoaded loadedState({ + required PortionModel portion, + required bool consultantView, + String date = '2025-01-01', + bool isDiaryFinished = false, + bool isDiaryCommented = false, + int closed = 0, + }) { final diary = DiaryDayEntity( id: 1, - date: '2025-01-01', + date: date, wakeup: '07:00', retire: '23:00', - closed: 0, + closed: closed, comment: '', - typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started), + typeOfDiary: const TypeOfDiaryModel( + hasChosenDiet: false, + hasChosenPlan: true, + diaryState: DiaryStateEnum.started, + ), meals: [ - MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: const []), + MealModel( + id: 1, + time: '08:00', + typeNumber: 1, + satiety: 0, + mood: 0, + portions: [portion], + photos: const [], + ), ], chosenPlanId: 1, dayOfChosenDiet: null, @@ -103,8 +131,18 @@ void main() { ); const nutrition = NutritionDataModel( - dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), - dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionIntake: NutritionModel( + energy: 0, + proteins: 0, + fats: 0, + carbohydrates: 0, + ), + dailyNutritionRecommendation: NutritionModel( + energy: 0, + proteins: 0, + fats: 0, + carbohydrates: 0, + ), ); return DiaryState.loaded( @@ -113,13 +151,17 @@ void main() { selectedDay: DateTime(2025, 1, 1), selectedMeal: 1, isConsultantView: consultantView, - isDiaryFinished: false, - isDiaryCommented: false, + isDiaryFinished: isDiaryFinished, + isDiaryCommented: isDiaryCommented, consultantReview: ConsultantReviewModel( planName: 'Plan', planDuration: 10, currentPlanDay: 1, - consultantComments: ConsultantComments(dayComment: null, mealsComments: const [], portionsComments: const []), + consultantComments: ConsultantComments( + dayComment: null, + mealsComments: const [], + portionsComments: const [], + ), ), ) as DiaryStateLoaded; @@ -140,9 +182,21 @@ void main() { DiaryState.loadedTomorrowWithDiet( diary: const DiaryFutureDietDayEntity( date: '2025-01-02', - typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null), + typeOfDiary: TypeOfDiaryModel( + hasChosenDiet: true, + hasChosenPlan: false, + diaryState: null, + ), meals: [ - MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: []), + MealModel( + id: 1, + time: '08:00', + typeNumber: 1, + satiety: 0, + mood: 0, + portions: [portion], + photos: [], + ), ], diet: null, dayOfChosenDiet: 1, @@ -151,7 +205,11 @@ void main() { selectedMeal: 1, ), ); - final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + final commentsCubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); await tester.pumpWidget( wrap( @@ -177,8 +235,14 @@ void main() { fromDiet: false, portionState: PortionState.eatenDeleted, ); - final diaryCubit = FakeDiaryCubit(loadedState(portion: portion, consultantView: false)); - final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + final diaryCubit = FakeDiaryCubit( + loadedState(portion: portion, consultantView: false), + ); + final commentsCubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); await tester.pumpWidget( wrap( @@ -194,32 +258,89 @@ void main() { expect(find.byType(ProductWidget), findsOneWidget); }); - testWidgets('consultant view toggles comment widget and shows existing comment container', (tester) async { + testWidgets( + 'consultant view toggles comment widget and shows existing comment container', + (tester) async { + const portion = PortionModel( + id: 1, + productNutritionalValueId: null, + productCaloriesInfo: null, + productName: 'Portion', + quantity: 100, + portion: 'gr', + fromDiet: false, + portionState: PortionState.filledDiet, + ); + final state = loadedState(portion: portion, consultantView: true) + .copyWith( + consultantReview: ConsultantReviewModel( + planName: 'Plan', + planDuration: 10, + currentPlanDay: 1, + consultantComments: ConsultantComments( + dayComment: null, + mealsComments: const [], + portionsComments: [ + PortionComment(portionId: 1, comment: 'Looks good'), + ], + ), + ), + ); + + final diaryCubit = FakeDiaryCubit(state); + final commentsCubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); + + await tester.pumpWidget( + wrap( + diaryCubit: diaryCubit, + consultantCommentsCubit: commentsCubit, + child: const ProductWidget(portion: portion), + ), + ); + await tester.pump(); + + expect(find.byType(CommentContainer), findsOneWidget); + expect(find.text('Looks good'), findsOneWidget); + + // Tap edit icon to enable PortionCommentWidget. + await tester.tap(find.byType(GestureDetector).first); + await tester.pump(); + + expect(find.byType(PortionCommentWidget), findsOneWidget); + }, + ); + + testWidgets('can edit a portion on a finished day inside the edit window', ( + tester, + ) async { const portion = PortionModel( id: 1, productNutritionalValueId: null, productCaloriesInfo: null, - productName: 'Portion', + productName: 'Apple', quantity: 100, portion: 'gr', fromDiet: false, - portionState: PortionState.filledDiet, + portionState: PortionState.eaten, ); - final state = loadedState(portion: portion, consultantView: true).copyWith( - consultantReview: ConsultantReviewModel( - planName: 'Plan', - planDuration: 10, - currentPlanDay: 1, - consultantComments: ConsultantComments( - dayComment: null, - mealsComments: const [], - portionsComments: [PortionComment(portionId: 1, comment: 'Looks good')], - ), + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + final diaryCubit = FakeDiaryCubit( + loadedState( + portion: portion, + consultantView: false, + date: _calendarDate(yesterday), + isDiaryFinished: true, + ), + ); + final commentsCubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), ), ); - - final diaryCubit = FakeDiaryCubit(state); - final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); await tester.pumpWidget( wrap( @@ -230,15 +351,51 @@ void main() { ); await tester.pump(); + expect(tester.widget(find.byType(ListTile)).onTap, isNotNull); + }); - expect(find.byType(CommentContainer), findsOneWidget); - expect(find.text('Looks good'), findsOneWidget); + testWidgets('cannot edit a portion on a commented day', (tester) async { + const portion = PortionModel( + id: 1, + productNutritionalValueId: null, + productCaloriesInfo: null, + productName: 'Apple', + quantity: 100, + portion: 'gr', + fromDiet: false, + portionState: PortionState.eaten, + ); + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + final diaryCubit = FakeDiaryCubit( + loadedState( + portion: portion, + consultantView: false, + date: _calendarDate(yesterday), + isDiaryCommented: true, + ), + ); + final commentsCubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: ''), + ), + ); - // Tap edit icon to enable PortionCommentWidget. - await tester.tap(find.byType(GestureDetector).first); + await tester.pumpWidget( + wrap( + diaryCubit: diaryCubit, + consultantCommentsCubit: commentsCubit, + child: const ProductWidget(portion: portion), + ), + ); await tester.pump(); - expect(find.byType(PortionCommentWidget), findsOneWidget); + expect(tester.widget(find.byType(ListTile)).onTap, isNull); }); }); } + +String _calendarDate(DateTime date) { + final month = date.month.toString().padLeft(2, '0'); + final day = date.day.toString().padLeft(2, '0'); + return '${date.year}-$month-$day'; +}