Files
eatvio_app/lib/features/diary/presentation/bloc/diary_cubit.dart

575 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
import 'package:eatvio_app/core/local_db/diary_local_data_source.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.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_future_diet_day_entity.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/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class DiaryCubit extends Cubit<DiaryState> {
late final DateTime today;
late DateTime selectedDay;
int selectedMeal = 1;
final DiaryRepository diaryRepository;
final GlossaryRepository glossaryRepository;
final ProfileRepository profileRepository;
final DietRepository dietRepository;
final ChatRepository chatRepository;
final SettingsRepository settingsRepository;
final DiaryLocalDataSource localDataSource;
final InternetConnectionCubit connectionCubit;
int? diaryId;
// Трек офлайн-режима
bool _lastFetchWasOffline = false;
DiaryCubit({
required this.chatRepository,
required this.diaryRepository,
required this.glossaryRepository,
required this.profileRepository,
required this.dietRepository,
required this.settingsRepository,
required this.localDataSource,
required this.connectionCubit,
}) : super(const DiaryState.loading()) {
today = DateTime.now();
selectedDay = today;
diaryRepository.forceFetchStream.listen((_) {
fetchDiaryV2(needLoading: true, diaryId: diaryId);
});
}
String comment = '';
bool commentNeedsToBeFilled = false;
void setInitialDate(DateTime? date) {
if (date == null) return;
selectedDay = date;
}
Future<bool> setSelectedDay(DateTime day) async {
final prevDay = selectedDay;
final prevMeal = selectedMeal;
selectedDay = day;
selectedMeal = 1;
final canSelect = await fetchDiaryV2(needLoading: true);
if (!canSelect) {
selectedDay = prevDay;
selectedMeal = prevMeal;
}
return canSelect;
}
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId}) async {
final currentState = state;
try {
if (needLoading) {
emit(const DiaryState.loading());
}
if (diaryId != null) {
final diaryDayById = await diaryRepository.getDiaryDay(diaryId);
final diaryEntity = diaryDayById.toEntity();
if (diaryEntity != null) {
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
return true;
}
}
final diaryDay = await getDiary();
if (diaryDay == null) {
// Офлайн и нет кэша — показываем понятное сообщение
final msg = connectionCubit.state.isInternetAvailable
? 'Дневник не найден'
: 'Нет интернета. Откройте дневник онлайн хотя бы один раз, чтобы он был доступен офлайн.';
emit(DiaryState.error(errorMessage: msg));
return true;
}
final diaryEntity = diaryDay.toEntity();
if (diaryEntity != null) {
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
return true;
}
if (diaryDay.typeOfDiary.hasChosenDiet == true && diaryDay.typeOfDiary.diaryState == null) {
setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity());
return true;
}
emit(currentState);
return false;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(DiaryState.error(errorMessage: e.toString()));
return false;
}
}
Future<void> updateCurrentDiary() async {
try {
final diaryDay = await diaryRepository.getDiaryDayV2(
day: selectedDay.toISO8601WithTimeZoneOffset(),
);
final diaryEntity = diaryDay.toEntity();
if (diaryEntity != null) {
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
}
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
}
}
Future<DiaryDayModel?> getDiary() async {
_lastFetchWasOffline = false;
// Офлайн: СРАЗУ в кэш, не трогаем сеть
if (!connectionCubit.state.isInternetAvailable) {
_lastFetchWasOffline = true;
try {
return await diaryRepository.getDiaryDayV2(
day: selectedDay.toISO8601WithTimeZoneOffset(),
);
} catch (_) {
return null; // Нет кэша — вернём null, UI покажет offline-заглушку
}
}
// Онлайн: создаём день + грузим с сервера
try {
await diaryRepository.createDiaryDay(day: selectedDay.toISO8601WithTimeZoneOffset());
return await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
if (e is DioException && e.response?.statusCode == 422) {
// День уже существует — просто загружаем
try {
return await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
} catch (_) {}
}
// Другая сетевая ошибка → пробуем кэш
_lastFetchWasOffline = true;
try {
return await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
} catch (_) {
return null;
}
}
}
void setSelectedMeal(int meal) {
selectedMeal = meal;
final currentState = state;
if (currentState is DiaryStateLoaded) {
emit(currentState.copyWith(selectedMeal: selectedMeal));
} else if (currentState is DiaryStateLoadedTomorrowWithDiet) {
emit(currentState.copyWith(selectedMeal: selectedMeal));
}
}
Future<bool> setDiaryForDietOnly(DiaryFutureDietDayEntity diaryDay) async {
if (!diaryDay.typeOfDiary.hasChosenDiet) {
return false;
}
emit(const DiaryState.loading());
try {
if (diaryDay.diet?.id != null && diaryDay.dayOfChosenDiet != null) {
final diet = await dietRepository.getDiet((diaryDay.diet?.id)!);
final meals = diet.dailySchedule[diaryDay.dayOfChosenDiet];
final firstMealPortions = meals?[1]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final secondMealPortions = meals?[2]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final thirdMealPortions = meals?[3]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final fourthMealPortions = meals?[4]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final fifthMealPortions = meals?[5]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final firstMeal = MealModel(id: 0, time: '0', typeNumber: 1, satiety: 0, mood: 0, portions: firstMealPortions ?? [], photos: []);
final secondMeal = MealModel(id: 0, time: '0', typeNumber: 2, satiety: 0, mood: 0, portions: secondMealPortions ?? [], photos: []);
final thirdMeal = MealModel(id: 0, time: '0', typeNumber: 3, satiety: 0, mood: 0, portions: thirdMealPortions ?? [], photos: []);
final fourthMeal = MealModel(id: 0, time: '0', typeNumber: 4, satiety: 0, mood: 0, portions: fourthMealPortions ?? [], photos: []);
final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []);
emit(
DiaryState.loadedTomorrowWithDiet(
diary: diaryDay.copyWith(meals: [firstMeal, secondMeal, thirdMeal, fourthMeal, fifthMeal]),
selectedDay: selectedDay,
selectedMeal: selectedMeal,
),
);
return true;
}
return false;
} catch (e, st) {
await reportFrom(e, st, this);
emit(DiaryState.error(errorMessage: e.toString()));
return false;
}
}
Future<void> setDiary({int? diaryId, required DiaryDayEntity diaryDay}) async {
this.diaryId ??= diaryId;
final isConsultantView = this.diaryId != null;
try {
bool isMoodEnabled = false;
bool isWakeUpAndRetireTimeEnabled = false;
if (diaryId == null) {
try {
final profileSettings = await settingsRepository.getProfileSettings();
isMoodEnabled = profileSettings.isMoodFillable;
isWakeUpAndRetireTimeEnabled = profileSettings.isBedtimeFillable;
} catch (_) {
// Офлайн — используем значения по умолчанию
}
}
ConsultantReviewModel? consultantReview;
int? planDuration;
int? currentPlanDay;
String? planName;
if (diaryDay.chosenPlanId != null) {
try {
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
if (!isConsultantView &&
(diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented)) {
await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id);
}
consultantReview = await diaryRepository.getDiaryComments(
planId: diaryDay.chosenPlanId!,
diaryId: diaryDay.id,
);
planDuration = chosenPlanModel.plan.duration;
planName = chosenPlanModel.plan.name;
currentPlanDay = consultantReview.currentPlanDay;
} catch (_) {
// Офлайн — данные плана недоступны
}
}
final nutrition = await getNutrition(diaryDay.id);
final isDiaryFinished =
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.finished ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished;
final isDiaryCommented =
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser;
// Offline-first: получаем кол-во операций в очереди
final pendingCount = await localDataSource.getPendingCount();
emit(
DiaryState.loaded(
diary: diaryDay,
nutrition: nutrition,
selectedDay: selectedDay,
selectedMeal: selectedMeal,
isConsultantView: isConsultantView,
consultantReview: consultantReview,
isDiaryFinished: isDiaryFinished,
isDiaryCommented: isDiaryCommented,
chosenPlanCurrentDay: currentPlanDay,
chosenPlanDuration: planDuration,
chosenPlanName: planName,
isMoodEnabled: isMoodEnabled,
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled,
pendingOpsCount: pendingCount,
isOfflineMode: _lastFetchWasOffline,
),
);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(DiaryState.error(errorMessage: e.toString()));
}
}
Future<bool> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
try {
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
await updateCurrentDiary();
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
}
Future<NutritionDataModel?> getNutrition(int index) async {
try {
return await diaryRepository.getNutritionData(index);
} catch (e, st) {
await reportFrom(e, st, this);
log('getNutrition offline fallback: $e');
return null; // Офлайн — нет данных по калориям, не критично
}
}
Future<void> addProductToMeal({
required int diaryId,
required int mealId,
required String productName,
required String portion,
required int quantity,
int? productNutritionalValueId,
}) async {
emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true));
try {
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
final s = state;
if (s is DiaryStateLoaded) {
emit(s.copyWith(isLoadingAddingProductInMeal: false));
}
}
}
Future<void> updateProductInMeal({
required int diaryId,
required int mealId,
required int portionId,
required String productName,
required String portion,
required int quantity,
int? productNutritionalValueId,
required PortionState portionState,
}) async {
try {
if (portionState == PortionState.filledDiet || portionState == PortionState.eatenFromDiet) {
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
} else {
await diaryRepository.updateProductInMeal(
diaryId: diaryId,
mealId: mealId,
portionId: portionId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
portionState: portionState,
);
}
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
try {
await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
try {
await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
try {
await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
try {
await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
try {
return await glossaryRepository.getGlossaryListByQuery(query);
} catch (e, st) {
await reportFrom(e, st, this);
return [];
}
}
Future<void> uploadPhotos({required List<File> files, required int diaryId}) async {
try {
await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async {
try {
await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<GlossaryModerationModel?> sendProductToModeration({
required String name,
required double energy,
required double proteins,
required double fats,
required double carbohydrates,
}) async {
try {
return await diaryRepository.sendProductToModeration(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates);
} catch (e, stackTrace) {
await reportFrom(e, stackTrace, this);
log(e.toString(), stackTrace: stackTrace);
}
return null;
}
Future<void> fillTime({required int diaryId, required String time}) async {
try {
await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillWakeup({required int diaryId, required String time}) async {
try {
await diaryRepository.fillWakeup(diaryId: diaryId, time: time);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillRetire({required int diaryId, required String time}) async {
try {
await diaryRepository.fillRetire(diaryId: diaryId, time: time);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
try {
await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillMood({required int diaryId, required int mood}) async {
try {
await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
void fillComment(String comment) {
this.comment = comment;
commentNeedsToBeFilled = comment.isNotEmpty && comment.length < 3;
}
}
enum DiaryFetchingErrors { canNotCreateFutureDays, dayAlreadyExists }