feat(diary): editing of previous and current finished days

This commit is contained in:
misha
2026-10-01 10:44:52 +03:00
parent a1bae68307
commit 603a158cf2
11 changed files with 1099 additions and 242 deletions

View File

@@ -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/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/consultant_review_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_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'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'diary_state.freezed.dart'; part 'diary_state.freezed.dart';
@@ -12,7 +13,8 @@ abstract class DiaryState with _$DiaryState {
const factory DiaryState.loading() = DiaryStateLoading; 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({ const factory DiaryState.loaded({
required DiaryDayEntity diary, required DiaryDayEntity diary,
@@ -31,6 +33,19 @@ abstract class DiaryState with _$DiaryState {
@Default(false) bool isLoadingAddingProductInMeal, @Default(false) bool isLoadingAddingProductInMeal,
}) = DiaryStateLoaded; }) = DiaryStateLoaded;
const factory DiaryState.loadedTomorrowWithDiet({required DiaryFutureDietDayEntity diary, required DateTime selectedDay, required int selectedMeal}) = const factory DiaryState.loadedTomorrowWithDiet({
DiaryStateLoadedTomorrowWithDiet; 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,
);
} }

View File

@@ -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);
}

View File

@@ -74,7 +74,12 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
} }
if (state is DiaryStateError) { if (state is DiaryStateError) {
return Scaffold( return Scaffold(
body: DataErrorWidget(onReload: () => context.read<DiaryCubit>().fetchDiaryV2(needLoading: false, diaryId: widget.diaryId)), body: DataErrorWidget(
onReload: () => context.read<DiaryCubit>().fetchDiaryV2(
needLoading: false,
diaryId: widget.diaryId,
),
),
); );
} }
@@ -84,8 +89,14 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
? AppBar(toolbarHeight: 0) ? AppBar(toolbarHeight: 0)
: AppBar( : AppBar(
title: Text( title: Text(
state is DiaryStateLoaded ? DateTime.parse(state.diary.date).toLocal().format('d MMMM') : '', state is DiaryStateLoaded
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ? DateTime.parse(
state.diary.date,
).toLocal().format('d MMMM')
: '',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
), ),
centerTitle: true, centerTitle: true,
), ),
@@ -95,26 +106,39 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
child: Column( child: Column(
children: [ children: [
if (widget.diaryId == null) CalendarWidget(date: widget.date), 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) if (state is DiaryStateLoaded)
WakeupAndRetireWidget( WakeupAndRetireWidget(
isEnabled: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isWakeUpAndRetireEnabled, isEnabled:
disable: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, state.diary.typeOfDiary.hasChosenPlan ||
state.isConsultantView ||
state.isWakeUpAndRetireEnabled,
disable: !state.canEditDiary,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
if (state is DiaryStateLoaded) if (state is DiaryStateLoaded)
MealWidget( MealWidget(
showMood: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isMoodEnabled, showMood:
showAddButton: !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented, 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(), if (state is DiaryStateLoaded) const CaloriesChartWidget(),
const SizedBox(height: 12), const SizedBox(height: 12),
Builder( Builder(
builder: (context) { builder: (context) {
if (state is DiaryStateLoaded && state.diary.typeOfDiary.hasChosenPlan) { if (state is DiaryStateLoaded &&
state.diary.typeOfDiary.hasChosenPlan) {
if (state.diary.comment.isNotEmpty) { if (state.diary.comment.isNotEmpty) {
return CommentForConsultantWidget(comment: state.diary.comment); return CommentForConsultantWidget(
comment: state.diary.comment,
);
} }
if (state.isDiaryCommented || state.isDiaryFinished) { if (state.isDiaryCommented || state.isDiaryFinished) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -124,7 +148,10 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
return const SizedBox.shrink(); 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(
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
child: _isDiaryFinishing child: _isDiaryFinishing
@@ -135,11 +162,13 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
setState(() { setState(() {
_isDiaryFinishing = true; _isDiaryFinishing = true;
}); });
final isFinished = await context.read<DiaryCubit>().finishDiaryDay( final isFinished = await context
diaryId: state.diary.id, .read<DiaryCubit>()
wakeUp: state.diary.wakeup.toString(), .finishDiaryDay(
retire: state.diary.retire.toString(), diaryId: state.diary.id,
); wakeUp: state.diary.wakeup.toString(),
retire: state.diary.retire.toString(),
);
if (!isFinished && context.mounted) { if (!isFinished && context.mounted) {
showErrorDialog(context); showErrorDialog(context);
} }
@@ -149,33 +178,64 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
}, },
), ),
), ),
if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) ...[ if (state is DiaryStateLoaded &&
state.isConsultantView &&
!state.isDiaryCommented) ...[
const SizedBox(height: 16), 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), const SizedBox(height: 12),
MainBackgroundBouble( MainBackgroundBouble(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ 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), const SizedBox(height: 12),
CommentContainer(comment: state.consultantReview?.consultantComments.dayComment ?? ''), CommentContainer(
comment:
state
.consultantReview
?.consultantComments
.dayComment ??
'',
),
], ],
), ),
), ),
const SizedBox(height: 28), 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), const SizedBox(height: 16),
CustomOutlinedButton( CustomOutlinedButton(
text: s.send, text: s.send,
height: 52, height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), buttonTextStyle: TextStyles.textMSemibold.copyWith(
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), color: appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(
appTheme.blueA400,
),
),
onPressed: () async { onPressed: () async {
await context.read<ConsultantCommentsCubit>().submitComments(state.diary.id); await context
.read<ConsultantCommentsCubit>()
.submitComments(state.diary.id);
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@@ -26,7 +26,8 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
final meals = state.diary.meals; final meals = state.diary.meals;
if (selectedIndex >= 0 && selectedIndex < meals.length) { if (selectedIndex >= 0 && selectedIndex < meals.length) {
if (isFirstFetch[selectedIndex]) { if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = meals[selectedIndex].mood.toDouble() + 1; currentSelectedRating[selectedIndex] =
meals[selectedIndex].mood.toDouble() + 1;
isFirstFetch[selectedIndex] = false; isFirstFetch[selectedIndex] = false;
} }
} }
@@ -36,21 +37,30 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
state.isConsultantView ? 'Настроение пользователя после приема пищи' : 'Ваше настроение после приёма пищи', state.isConsultantView
? 'Настроение пользователя после приема пищи'
: 'Ваше настроение после приёма пищи',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ 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( RatingBar.builder(
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, ignoreGestures: !state.canEditDiary,
initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length) initialRating:
(selectedIndex >= 0 &&
selectedIndex < state.diary.meals.length)
? state.diary.meals[selectedIndex].mood.toDouble() + 1 ? state.diary.meals[selectedIndex].mood.toDouble() + 1
: 3, : 3,
itemCount: 6, itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6), itemSize:
MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true, updateOnDrag: true,
itemBuilder: (context, index) { itemBuilder: (context, index) {
switch (index) { switch (index) {
@@ -58,82 +68,154 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 1 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 1
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFC82222), color: const Color(0xFFC82222),
border: currentSelectedRating[selectedIndex] == 1 border:
? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 1
? Border.all(
width: 2,
color: const Color(0xFFAA3333),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 1 borderRadius:
? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2)) currentSelectedRating[selectedIndex] == 1
: const BorderRadius.horizontal(left: Radius.circular(8)), ? const BorderRadius.horizontal(
left: Radius.circular(8),
right: Radius.circular(2),
)
: const BorderRadius.horizontal(
left: Radius.circular(8),
),
), ),
); );
case 1: case 1:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 2 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 2
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFF3E3E), color: const Color(0xFFFF3E3E),
border: currentSelectedRating[selectedIndex] == 2 border:
? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 2
? Border.all(
width: 2,
color: const Color(0xFF550000),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null,
borderRadius:
currentSelectedRating[selectedIndex] == 2
? BorderRadius.circular(2)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
), ),
); );
case 2: case 2:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 3 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 3
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8C926), color: const Color(0xFFF8C926),
border: currentSelectedRating[selectedIndex] == 3 border:
? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 3
? Border.all(
width: 2,
color: const Color(0xFF8C5D00),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null,
borderRadius:
currentSelectedRating[selectedIndex] == 3
? BorderRadius.circular(2)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
), ),
); );
case 3: case 3:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 4 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 4
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFF3BF), color: const Color(0xFFFFF3BF),
border: currentSelectedRating[selectedIndex] == 4 border:
? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 4
? Border.all(
width: 2,
color: const Color(0xFFB5911B),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null,
borderRadius:
currentSelectedRating[selectedIndex] == 4
? BorderRadius.circular(2)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
), ),
); );
case 4: case 4:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 5 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 5
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF62E833), color: const Color(0xFF62E833),
border: currentSelectedRating[selectedIndex] == 5 border:
? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 5
? Border.all(
width: 2,
color: const Color(0xFF276918),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null,
borderRadius:
currentSelectedRating[selectedIndex] == 5
? BorderRadius.circular(2)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
), ),
); );
default: default:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 6 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 6
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF4DCE20), color: const Color(0xFF4DCE20),
border: currentSelectedRating[selectedIndex] == 6 border:
? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 6
? Border.all(
width: 2,
color: const Color(0xFF184D12),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 6 borderRadius:
? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8)) currentSelectedRating[selectedIndex] == 6
: const BorderRadius.horizontal(right: Radius.circular(8)), ? 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<MoodRatingWidget> {
setState(() { setState(() {
currentSelectedRating[selectedIndex] = value; currentSelectedRating[selectedIndex] = value;
}); });
EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () { EasyDebounce.debounce(
context.read<DiaryCubit>().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1); 'change-satiety',
}); const Duration(milliseconds: 250),
() {
context.read<DiaryCubit>().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,
),
], ],
), ),
], ],

View File

@@ -52,7 +52,9 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
AppRouter.navigatorKey.currentContext?.pop(); AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker(); final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera); XFile? image = await picker.pickImage(
source: ImageSource.camera,
);
if (image != null) { if (image != null) {
diaryCubit.uploadPhotos( diaryCubit.uploadPhotos(
files: [File(image.path)], files: [File(image.path)],
@@ -116,7 +118,8 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
builder: (context, state) { builder: (context, state) {
if (state is DiaryStateLoaded) { if (state is DiaryStateLoaded) {
int mealIndex = state.selectedMeal - 1; 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( return GestureDetector(
onTap: () async { onTap: () async {
showImagePicker(state.diary); showImagePicker(state.diary);
@@ -140,10 +143,11 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}', imageUrl:
'${sl<AppConfig>().baseUrl}/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}',
), ),
), ),
if (!state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) if (state.canEditDiary)
Padding( Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: IconButton.filled( child: IconButton.filled(
@@ -153,12 +157,24 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
), ),
onPressed: () async { onPressed: () async {
await diaryCubit.deletePhoto( await diaryCubit.deletePhoto(
photo: state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex], photo: state
.diary
.meals[mealIndex]
.photos[currentSelectedPhotoIndex],
diaryId: state.diary.id, diaryId: state.diary.id,
); );
setState(() { setState(() {
if (currentSelectedPhotoIndex == state.diary.meals[mealIndex].photos.length - 1 && currentSelectedPhotoIndex != 0) { if (currentSelectedPhotoIndex ==
currentSelectedPhotoIndex = state.diary.meals[mealIndex].photos.length - 2; 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<PhotoGalleryWidget> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
for (int i = 0; i < state.diary.meals[mealIndex].photos.length + 1; i++) ...[ for (
if (i == state.diary.meals[mealIndex].photos.length && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) int i = 0;
i < state.diary.meals[mealIndex].photos.length + 1;
i++
) ...[
if (i == state.diary.meals[mealIndex].photos.length &&
state.canEditDiary)
Padding( Padding(
padding: const EdgeInsets.only(left: 8), padding: const EdgeInsets.only(left: 8),
child: InkWell( child: InkWell(
@@ -212,13 +233,15 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
height: 52, height: 52,
width: 52, width: 52,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.diary.meals[mealIndex].photos[i].path}', imageUrl:
'${sl<AppConfig>().baseUrl}/storage/${state.diary.meals[mealIndex].photos[i].path}',
fit: BoxFit.fitWidth, 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),
], ],
], ],
), ),

View File

@@ -1,8 +1,6 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dotted_border/dotted_border.dart'; import 'package:dotted_border/dotted_border.dart';
import 'package:eatvio_app/core/theme/theme_helper.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/consultant_review_model.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
@@ -32,17 +30,26 @@ class _ProductWidgetState extends State<ProductWidget> {
super.initState(); super.initState();
diaryCubit = context.read<DiaryCubit>(); diaryCubit = context.read<DiaryCubit>();
portionName = widget.portion.portion == 'gr' ? 'гр' : 'шт'; portionName = widget.portion.portion == 'gr' ? 'гр' : 'шт';
isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false; isEaten = widget.portion.portionState == PortionState.eatenFromDiet
? true
: false;
} }
Widget _buildNutritionalInfo() { Widget _buildNutritionalInfo() {
if (widget.portion.productNutritionalValueId == null) { if (widget.portion.productNutritionalValueId == null) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 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( 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<ProductWidget> {
calories = (caloriesInfo.calories * widget.portion.quantity) / 100; calories = (caloriesInfo.calories * widget.portion.quantity) / 100;
proteins = (caloriesInfo.proteins * widget.portion.quantity) / 100; proteins = (caloriesInfo.proteins * widget.portion.quantity) / 100;
fats = (caloriesInfo.fats * 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 { } else {
calories = caloriesInfo.energyOnPortion; calories = caloriesInfo.energyOnPortion;
proteins = caloriesInfo.proteinsOnPortion; proteins = caloriesInfo.proteinsOnPortion;
@@ -81,10 +89,17 @@ class _ProductWidgetState extends State<ProductWidget> {
Widget _buildNutritionChip(String label, int value) { Widget _buildNutritionChip(String label, int value) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), 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( child: Text(
'$label $value', '$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<ProductWidget> {
void didUpdateWidget(ProductWidget oldWidget) { void didUpdateWidget(ProductWidget oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
portionName = widget.portion.portion == 'gr' ? 'гр' : 'шт'; 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) { Future updateProductInMeal(DiaryStateLoaded state, PortionModel portion) {
return showModalBottomSheet( return showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
context: context, context: context,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))), shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
isScrollControlled: true, 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<ProductWidget> {
if (state is DiaryStateLoadedTomorrowWithDiet) { if (state is DiaryStateLoadedTomorrowWithDiet) {
return ListTile( return ListTile(
horizontalTitleGap: 0, 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), title: Text(widget.portion.productName),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), Text(
'${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}',
),
const SizedBox(height: 4), const SizedBox(height: 4),
_buildNutritionalInfo(), _buildNutritionalInfo(),
], ],
), ),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
tileColor: const Color(0xFFF6FAFF), tileColor: const Color(0xFFF6FAFF),
); );
} }
if (state is DiaryStateLoaded) { if (state is DiaryStateLoaded) {
final isDiaryCommented = final canEditPortion = state.canEditDiary && !widget.portion.fromDiet;
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 consultantComments = state.consultantReview?.consultantComments; final consultantComments = state.consultantReview?.consultantComments;
final canMarkAsEaten = !state.isConsultantView && !isDiaryCommented && !isDiaryFinished; final canMarkAsEaten = state.canEditDiary;
PortionComment? portionComment; PortionComment? portionComment;
if (consultantComments != null) { 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) { if (widget.portion.portionState == PortionState.eatenDeleted) {
return DottedBorder( return DottedBorder(
@@ -151,27 +177,41 @@ class _ProductWidgetState extends State<ProductWidget> {
color: Color(0xFF6D6F71), color: Color(0xFF6D6F71),
), ),
child: ListTile( child: ListTile(
onTap: () => diaryCubit.restore(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id), onTap: state.canEditDiary
trailing: SvgPicture.asset('assets/icons/revert_product.svg'), ? () => 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, 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), title: Text(widget.portion.productName),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), Text(
'${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}',
),
const SizedBox(height: 4), const SizedBox(height: 4),
_buildNutritionalInfo(), _buildNutritionalInfo(),
], ],
), ),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
// ignore: use_full_hex_values_for_flutter_colors // ignore: use_full_hex_values_for_flutter_colors
tileColor: const Color(0xfff6faff03).withValues(alpha: 0.01), tileColor: const Color(0xfff6faff03).withValues(alpha: 0.01),
), ),
); );
} else { } else {
Widget? trailing; Widget? trailing;
if (state.isConsultantView && !isDiaryCommented) { if (state.isConsultantView && !state.isDiaryCommented) {
trailing = GestureDetector( trailing = GestureDetector(
onTap: () { onTap: () {
setState(() { setState(() {
@@ -180,13 +220,15 @@ class _ProductWidgetState extends State<ProductWidget> {
}, },
child: SvgPicture.asset('assets/icons/edit.svg'), child: SvgPicture.asset('assets/icons/edit.svg'),
); );
} else if (!isDiaryCommented && !widget.portion.fromDiet) { } else if (canEditPortion) {
trailing = const Icon(Icons.more_vert_rounded); trailing = const Icon(Icons.more_vert_rounded);
} }
return Column( return Column(
children: [ children: [
ListTile( ListTile(
onTap: canEditPortion ? () => updateProductInMeal(state, widget.portion) : null, onTap: canEditPortion
? () => updateProductInMeal(state, widget.portion)
: null,
trailing: trailing, trailing: trailing,
leading: widget.portion.fromDiet leading: widget.portion.fromDiet
? Transform.scale( ? Transform.scale(
@@ -199,35 +241,55 @@ class _ProductWidgetState extends State<ProductWidget> {
isEaten = value!; isEaten = value!;
}); });
if (isEaten) { 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 { } 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, : null,
activeColor: appTheme.blueA400, activeColor: appTheme.blueA400,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
side: const BorderSide(color: Color(0xFF809CB9)), side: const BorderSide(color: Color(0xFF809CB9)),
), ),
) )
: null, : null,
horizontalTitleGap: 0, 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), title: Text(widget.portion.productName),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), Text(
'${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}',
),
const SizedBox(height: 4), const SizedBox(height: 4),
_buildNutritionalInfo(), _buildNutritionalInfo(),
], ],
), ),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
tileColor: const Color(0xFFF6FAFF), tileColor: const Color(0xFFF6FAFF),
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), 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,
),
), ),
], ],
); );

View File

@@ -26,7 +26,8 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
final meals = state.diary.meals; final meals = state.diary.meals;
if (selectedIndex >= 0 && selectedIndex < meals.length) { if (selectedIndex >= 0 && selectedIndex < meals.length) {
if (isFirstFetch[selectedIndex]) { if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = meals[selectedIndex].satiety.toDouble() + 1; currentSelectedRating[selectedIndex] =
meals[selectedIndex].satiety.toDouble() + 1;
isFirstFetch[selectedIndex] = false; isFirstFetch[selectedIndex] = false;
} }
} }
@@ -36,21 +37,32 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
state.isConsultantView ? 'Сытость пользователя после приема пищи' : 'Ваша сытость после приёма пищи', state.isConsultantView
? 'Сытость пользователя после приема пищи'
: 'Ваша сытость после приёма пищи',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ 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( RatingBar.builder(
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, ignoreGestures: !state.canEditDiary,
initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length) initialRating:
? state.diary.meals[selectedIndex].satiety.toDouble() + 1 (selectedIndex >= 0 &&
selectedIndex < state.diary.meals.length)
? state.diary.meals[selectedIndex].satiety
.toDouble() +
1
: 3, : 3,
itemCount: 6, itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6), itemSize:
MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true, updateOnDrag: true,
itemBuilder: (context, index) { itemBuilder: (context, index) {
switch (index) { switch (index) {
@@ -58,82 +70,154 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 1 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 1
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFC82222), color: const Color(0xFFC82222),
border: currentSelectedRating[selectedIndex] == 1 border:
? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 1
? Border.all(
width: 2,
color: const Color(0xFFAA3333),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 1 borderRadius:
? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2)) currentSelectedRating[selectedIndex] == 1
: const BorderRadius.horizontal(left: Radius.circular(8)), ? const BorderRadius.horizontal(
left: Radius.circular(8),
right: Radius.circular(2),
)
: const BorderRadius.horizontal(
left: Radius.circular(8),
),
), ),
); );
case 1: case 1:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 2 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 2
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFF3E3E), color: const Color(0xFFFF3E3E),
border: currentSelectedRating[selectedIndex] == 2 border:
? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 2
? Border.all(
width: 2,
color: const Color(0xFF550000),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null,
borderRadius:
currentSelectedRating[selectedIndex] == 2
? BorderRadius.circular(2)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
), ),
); );
case 2: case 2:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 3 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 3
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8C926), color: const Color(0xFFF8C926),
border: currentSelectedRating[selectedIndex] == 3 border:
? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 3
? Border.all(
width: 2,
color: const Color(0xFF8C5D00),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null,
borderRadius:
currentSelectedRating[selectedIndex] == 3
? BorderRadius.circular(2)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
), ),
); );
case 3: case 3:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 4 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 4
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFF3BF), color: const Color(0xFFFFF3BF),
border: currentSelectedRating[selectedIndex] == 4 border:
? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 4
? Border.all(
width: 2,
color: const Color(0xFFB5911B),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null,
borderRadius:
currentSelectedRating[selectedIndex] == 4
? BorderRadius.circular(2)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
), ),
); );
case 4: case 4:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 5 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 5
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF62E833), color: const Color(0xFF62E833),
border: currentSelectedRating[selectedIndex] == 5 border:
? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 5
? Border.all(
width: 2,
color: const Color(0xFF276918),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null,
borderRadius:
currentSelectedRating[selectedIndex] == 5
? BorderRadius.circular(2)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
), ),
); );
default: default:
return Container( return Container(
width: MediaQuery.of(context).size.width / 6, width: MediaQuery.of(context).size.width / 6,
height: 24, height: 24,
margin: currentSelectedRating[selectedIndex] == 6 ? const EdgeInsets.symmetric(horizontal: 2) : null, margin: currentSelectedRating[selectedIndex] == 6
? const EdgeInsets.symmetric(horizontal: 2)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF4DCE20), color: const Color(0xFF4DCE20),
border: currentSelectedRating[selectedIndex] == 6 border:
? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside) currentSelectedRating[selectedIndex] == 6
? Border.all(
width: 2,
color: const Color(0xFF184D12),
strokeAlign:
BorderSide.strokeAlignOutside,
)
: null, : null,
borderRadius: currentSelectedRating[selectedIndex] == 6 borderRadius:
? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8)) currentSelectedRating[selectedIndex] == 6
: const BorderRadius.horizontal(right: Radius.circular(8)), ? 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<SatietyRatingWidget> {
setState(() { setState(() {
currentSelectedRating[selectedIndex] = value; currentSelectedRating[selectedIndex] = value;
}); });
EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () { EasyDebounce.debounce(
context.read<DiaryCubit>().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1); 'change-satiety',
}); const Duration(milliseconds: 250),
() {
context.read<DiaryCubit>().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,
),
], ],
), ),
], ],

View File

@@ -46,7 +46,8 @@ class _TimeOfMealPickerWidgetState extends State<TimeOfMealPickerWidget> {
Navigator.of(context).pop(); Navigator.of(context).pop();
diaryCubit.fillTime( diaryCubit.fillTime(
diaryId: diaryId, 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), borderRadius: BorderRadius.circular(20),
@@ -69,7 +70,7 @@ class _TimeOfMealPickerWidgetState extends State<TimeOfMealPickerWidget> {
), ),
), ),
), ),
const SizedBox(height: 30) const SizedBox(height: 30),
], ],
), ),
), ),
@@ -98,7 +99,9 @@ class _TimeOfMealPickerWidgetState extends State<TimeOfMealPickerWidget> {
), ),
), ),
InkWell( 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), borderRadius: BorderRadius.circular(100),
child: Ink( child: Ink(
height: 32, height: 32,
@@ -110,7 +113,16 @@ class _TimeOfMealPickerWidgetState extends State<TimeOfMealPickerWidget> {
alignment: Alignment.center, alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text( 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( style: const TextStyle(
color: Color(0xFF6D6F71), color: Color(0xFF6D6F71),
fontSize: 17, fontSize: 17,

View File

@@ -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);
});
}

View File

@@ -25,16 +25,25 @@ import 'package:mockito/mockito.dart';
import 'diary_screen_smoke_test.mocks.dart'; import 'diary_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<DiaryCubit>(), MockSpec<ConsultantCommentsCubit>()]) @GenerateNiceMocks([
MockSpec<DiaryCubit>(),
MockSpec<ConsultantCommentsCubit>(),
])
void main() { void main() {
group('DiaryScreen smoke', () { group('DiaryScreen smoke', () {
setUpAll(() async { 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 { setUp(() async {
await sl.reset(); await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); sl.registerLazySingleton<AppConfig>(
() =>
AppConfig(buildType: AppBuildType.tests)
..setBaseUrl('http://localhost'),
);
}); });
Widget wrap(Widget child) { 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( final diary = DiaryDayEntity(
id: 1, id: 1,
date: '2025-01-01', date: date,
wakeup: '07:00', wakeup: '07:00',
retire: '23:00', retire: '23:00',
closed: 0, closed: closed,
comment: '', comment: '',
typeOfDiary: TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: hasChosenPlan, diaryState: null), typeOfDiary: TypeOfDiaryModel(
meals: const [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], 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, chosenPlanId: hasChosenPlan ? 10 : null,
dayOfChosenDiet: null, dayOfChosenDiet: null,
diet: null, diet: null,
); );
const nutrition = NutritionDataModel( const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), dailyNutritionIntake: NutritionModel(
dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), energy: 0,
proteins: 0,
fats: 0,
carbohydrates: 0,
),
dailyNutritionRecommendation: NutritionModel(
energy: 0,
proteins: 0,
fats: 0,
carbohydrates: 0,
),
); );
return DiaryState.loaded( return DiaryState.loaded(
diary: diary, diary: diary,
@@ -75,8 +114,8 @@ void main() {
selectedDay: DateTime(2025, 1, 1), selectedDay: DateTime(2025, 1, 1),
selectedMeal: 1, selectedMeal: 1,
isConsultantView: false, isConsultantView: false,
isDiaryFinished: false, isDiaryFinished: isDiaryFinished,
isDiaryCommented: false, isDiaryCommented: isDiaryCommented,
isWakeUpAndRetireEnabled: true, isWakeUpAndRetireEnabled: true,
isMoodEnabled: true, isMoodEnabled: true,
) )
@@ -86,13 +125,26 @@ void main() {
testWidgets('renders loading state', (tester) async { testWidgets('renders loading state', (tester) async {
final diaryCubit = MockDiaryCubit(); final diaryCubit = MockDiaryCubit();
when(diaryCubit.state).thenReturn(const DiaryState.loading()); when(diaryCubit.state).thenReturn(const DiaryState.loading());
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty()); when(
diaryCubit.stream,
).thenAnswer((_) => const Stream<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value()); when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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(); final commentsCubit = MockConsultantCommentsCubit();
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); when(commentsCubit.state).thenReturn(
when(commentsCubit.stream).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty()); ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: ''),
),
);
when(
commentsCubit.stream,
).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value()); when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit); sl.registerFactory<DiaryCubit>(() => diaryCubit);
@@ -107,13 +159,27 @@ void main() {
testWidgets('renders loaded state (chosen plan)', (tester) async { testWidgets('renders loaded state (chosen plan)', (tester) async {
final diaryCubit = MockDiaryCubit(); final diaryCubit = MockDiaryCubit();
when(diaryCubit.state).thenReturn(loadedState(hasChosenPlan: true)); when(diaryCubit.state).thenReturn(loadedState(hasChosenPlan: true));
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty()); when(
diaryCubit.stream,
).thenAnswer((_) => const Stream<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value()); when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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(); final commentsCubit = MockConsultantCommentsCubit();
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false)); when(commentsCubit.state).thenReturn(
when(commentsCubit.stream).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty()); ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: ''),
dayCommentNeedsToBeFilled: false,
),
);
when(
commentsCubit.stream,
).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value()); when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit); sl.registerFactory<DiaryCubit>(() => diaryCubit);
@@ -124,19 +190,37 @@ void main() {
expect(find.byType(DiaryScreen), findsOneWidget); expect(find.byType(DiaryScreen), findsOneWidget);
expect(find.text('Дополнительная информация для консультанта'), findsOneWidget); expect(
find.text('Дополнительная информация для консультанта'),
findsOneWidget,
);
}); });
testWidgets('renders error state and reload button', (tester) async { testWidgets('renders error state and reload button', (tester) async {
final diaryCubit = MockDiaryCubit(); final diaryCubit = MockDiaryCubit();
when(diaryCubit.state).thenReturn(const DiaryState.error(errorMessage: 'err')); when(
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty()); diaryCubit.state,
).thenReturn(const DiaryState.error(errorMessage: 'err'));
when(
diaryCubit.stream,
).thenAnswer((_) => const Stream<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value()); when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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(); final commentsCubit = MockConsultantCommentsCubit();
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); when(commentsCubit.state).thenReturn(
when(commentsCubit.stream).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty()); ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: ''),
),
);
when(
commentsCubit.stream,
).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value()); when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit); sl.registerFactory<DiaryCubit>(() => diaryCubit);
@@ -153,19 +237,52 @@ void main() {
final diaryCubit = MockDiaryCubit(); final diaryCubit = MockDiaryCubit();
const futureDiary = DiaryFutureDietDayEntity( const futureDiary = DiaryFutureDietDayEntity(
date: '2025-01-02', date: '2025-01-02',
typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null), typeOfDiary: TypeOfDiaryModel(
meals: [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], hasChosenDiet: true,
hasChosenPlan: false,
diaryState: null,
),
meals: [
MealModel(
id: 1,
time: '08:00',
typeNumber: 1,
satiety: 0,
mood: 0,
portions: [],
photos: [],
),
],
diet: null, diet: null,
dayOfChosenDiet: 1, dayOfChosenDiet: 1,
); );
when(diaryCubit.state).thenReturn(DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1)); when(diaryCubit.state).thenReturn(
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty()); DiaryState.loadedTomorrowWithDiet(
diary: futureDiary,
selectedDay: DateTime(2025, 1, 2),
selectedMeal: 1,
),
);
when(
diaryCubit.stream,
).thenAnswer((_) => const Stream<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value()); when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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(); final commentsCubit = MockConsultantCommentsCubit();
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); when(commentsCubit.state).thenReturn(
when(commentsCubit.stream).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty()); ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: ''),
),
);
when(
commentsCubit.stream,
).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value()); when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit); sl.registerFactory<DiaryCubit>(() => diaryCubit);
@@ -187,17 +304,35 @@ void main() {
planName: 'Plan', planName: 'Plan',
planDuration: 30, planDuration: 30,
currentPlanDay: 1, 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.state).thenReturn(state);
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty()); when(
diaryCubit.stream,
).thenAnswer((_) => const Stream<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value()); when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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(); final commentsCubit = MockConsultantCommentsCubit();
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false)); when(commentsCubit.state).thenReturn(
when(commentsCubit.stream).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty()); ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: ''),
dayCommentNeedsToBeFilled: false,
),
);
when(
commentsCubit.stream,
).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value()); when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.submitComments(any)).thenAnswer((_) async => true); when(commentsCubit.submitComments(any)).thenAnswer((_) async => true);
@@ -215,13 +350,26 @@ void main() {
final diaryCubit = MockDiaryCubit(); final diaryCubit = MockDiaryCubit();
final state = loadedState(hasChosenPlan: false); final state = loadedState(hasChosenPlan: false);
when(diaryCubit.state).thenReturn(state); when(diaryCubit.state).thenReturn(state);
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty()); when(
diaryCubit.stream,
).thenAnswer((_) => const Stream<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value()); when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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(); final commentsCubit = MockConsultantCommentsCubit();
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); when(commentsCubit.state).thenReturn(
when(commentsCubit.stream).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty()); ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: ''),
),
);
when(
commentsCubit.stream,
).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value()); when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit); sl.registerFactory<DiaryCubit>(() => diaryCubit);
@@ -234,5 +382,96 @@ void main() {
expect(find.byType(Text), findsWidgets); 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<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit);
sl.registerFactory<ConsultantCommentsCubit>(() => 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<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit);
sl.registerFactory<ConsultantCommentsCubit>(() => 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';
}

View File

@@ -1,4 +1,3 @@
import 'package:eatvio_app/core/theme/theme_helper.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/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
@@ -27,7 +26,8 @@ class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
} }
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit { class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState>
implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState); FakeConsultantCommentsCubit(super.initialState);
@override @override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
@@ -60,9 +60,16 @@ void main() {
</svg> </svg>
'''; ''';
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( return DefaultAssetBundle(
bundle: bundle, bundle: bundle,
child: MaterialApp( child: MaterialApp(
@@ -77,7 +84,9 @@ void main() {
home: MultiBlocProvider( home: MultiBlocProvider(
providers: [ providers: [
BlocProvider<DiaryCubit>.value(value: diaryCubit), BlocProvider<DiaryCubit>.value(value: diaryCubit),
BlocProvider<ConsultantCommentsCubit>.value(value: consultantCommentsCubit), BlocProvider<ConsultantCommentsCubit>.value(
value: consultantCommentsCubit,
),
], ],
child: Scaffold(body: child), 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( final diary = DiaryDayEntity(
id: 1, id: 1,
date: '2025-01-01', date: date,
wakeup: '07:00', wakeup: '07:00',
retire: '23:00', retire: '23:00',
closed: 0, closed: closed,
comment: '', comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started), typeOfDiary: const TypeOfDiaryModel(
hasChosenDiet: false,
hasChosenPlan: true,
diaryState: DiaryStateEnum.started,
),
meals: [ 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, chosenPlanId: 1,
dayOfChosenDiet: null, dayOfChosenDiet: null,
@@ -103,8 +131,18 @@ void main() {
); );
const nutrition = NutritionDataModel( const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), dailyNutritionIntake: NutritionModel(
dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), energy: 0,
proteins: 0,
fats: 0,
carbohydrates: 0,
),
dailyNutritionRecommendation: NutritionModel(
energy: 0,
proteins: 0,
fats: 0,
carbohydrates: 0,
),
); );
return DiaryState.loaded( return DiaryState.loaded(
@@ -113,13 +151,17 @@ void main() {
selectedDay: DateTime(2025, 1, 1), selectedDay: DateTime(2025, 1, 1),
selectedMeal: 1, selectedMeal: 1,
isConsultantView: consultantView, isConsultantView: consultantView,
isDiaryFinished: false, isDiaryFinished: isDiaryFinished,
isDiaryCommented: false, isDiaryCommented: isDiaryCommented,
consultantReview: ConsultantReviewModel( consultantReview: ConsultantReviewModel(
planName: 'Plan', planName: 'Plan',
planDuration: 10, planDuration: 10,
currentPlanDay: 1, currentPlanDay: 1,
consultantComments: ConsultantComments(dayComment: null, mealsComments: const [], portionsComments: const []), consultantComments: ConsultantComments(
dayComment: null,
mealsComments: const [],
portionsComments: const [],
),
), ),
) )
as DiaryStateLoaded; as DiaryStateLoaded;
@@ -140,9 +182,21 @@ void main() {
DiaryState.loadedTomorrowWithDiet( DiaryState.loadedTomorrowWithDiet(
diary: const DiaryFutureDietDayEntity( diary: const DiaryFutureDietDayEntity(
date: '2025-01-02', date: '2025-01-02',
typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null), typeOfDiary: TypeOfDiaryModel(
hasChosenDiet: true,
hasChosenPlan: false,
diaryState: null,
),
meals: [ 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, diet: null,
dayOfChosenDiet: 1, dayOfChosenDiet: 1,
@@ -151,7 +205,11 @@ void main() {
selectedMeal: 1, selectedMeal: 1,
), ),
); );
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); final commentsCubit = FakeConsultantCommentsCubit(
ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: ''),
),
);
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
@@ -177,8 +235,14 @@ void main() {
fromDiet: false, fromDiet: false,
portionState: PortionState.eatenDeleted, portionState: PortionState.eatenDeleted,
); );
final diaryCubit = FakeDiaryCubit(loadedState(portion: portion, consultantView: false)); final diaryCubit = FakeDiaryCubit(
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); loadedState(portion: portion, consultantView: false),
);
final commentsCubit = FakeConsultantCommentsCubit(
ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: ''),
),
);
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
@@ -194,32 +258,89 @@ void main() {
expect(find.byType(ProductWidget), findsOneWidget); 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( const portion = PortionModel(
id: 1, id: 1,
productNutritionalValueId: null, productNutritionalValueId: null,
productCaloriesInfo: null, productCaloriesInfo: null,
productName: 'Portion', productName: 'Apple',
quantity: 100, quantity: 100,
portion: 'gr', portion: 'gr',
fromDiet: false, fromDiet: false,
portionState: PortionState.filledDiet, portionState: PortionState.eaten,
); );
final state = loadedState(portion: portion, consultantView: true).copyWith( final yesterday = DateTime.now().subtract(const Duration(days: 1));
consultantReview: ConsultantReviewModel( final diaryCubit = FakeDiaryCubit(
planName: 'Plan', loadedState(
planDuration: 10, portion: portion,
currentPlanDay: 1, consultantView: false,
consultantComments: ConsultantComments( date: _calendarDate(yesterday),
dayComment: null, isDiaryFinished: true,
mealsComments: const [], ),
portionsComments: [PortionComment(portionId: 1, comment: 'Looks good')], );
), final commentsCubit = FakeConsultantCommentsCubit(
ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: ''),
), ),
); );
final diaryCubit = FakeDiaryCubit(state);
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
@@ -230,15 +351,51 @@ void main() {
); );
await tester.pump(); await tester.pump();
expect(tester.widget<ListTile>(find.byType(ListTile)).onTap, isNotNull);
});
expect(find.byType(CommentContainer), findsOneWidget); testWidgets('cannot edit a portion on a commented day', (tester) async {
expect(find.text('Looks good'), findsOneWidget); 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.pumpWidget(
await tester.tap(find.byType(GestureDetector).first); wrap(
diaryCubit: diaryCubit,
consultantCommentsCubit: commentsCubit,
child: const ProductWidget(portion: portion),
),
);
await tester.pump(); await tester.pump();
expect(find.byType(PortionCommentWidget), findsOneWidget); expect(tester.widget<ListTile>(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';
}