Files
eatvio_app/lib/features/diary/presentation/bloc/diary_cubit.dart
2026-04-30 05:55:25 +03:00

617 lines
22 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:async';
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/internet_connection_cubit/internet_connection_state.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;
// Подписка на изменения connectivity
StreamSubscription<InternetConnectionState>? _connectivitySubscription;
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);
});
// При переходе offline→online перезагружаем дневник
_connectivitySubscription = connectionCubit.stream.listen((connState) {
if (connState.isInternetAvailable && _lastFetchWasOffline) {
log('[DiaryCubit] Internet restored, reloading diary...');
fetchDiaryV2(needLoading: false, 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) {
log('[fetchDiaryV2] Loading by diaryId=$diaryId');
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
? 'Дневник не найден'
: 'Нет интернета. Откройте дневник онлайн хотя бы один раз, чтобы он был доступен офлайн.';
log('[fetchDiaryV2] diaryDay is null, msg=$msg');
emit(DiaryState.error(errorMessage: msg));
return true;
}
log('[fetchDiaryV2] diaryDay loaded, id=${diaryDay.id}, converting to entity...');
final diaryEntity = diaryDay.toEntity();
if (diaryEntity != null) {
log('[fetchDiaryV2] entity OK, calling setDiary...');
await setDiary(diaryDay: diaryEntity, diaryId: diaryId);
log('[fetchDiaryV2] setDiary completed');
return true;
}
if (diaryDay.typeOfDiary.hasChosenDiet == true && diaryDay.typeOfDiary.diaryState == null) {
setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity());
return true;
}
log('[fetchDiaryV2] no entity, emitting previous state');
emit(currentState);
return false;
} catch (e, st) {
log('[fetchDiaryV2] CRASH: $e', stackTrace: st);
await reportFrom(e, st, this);
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;
final dayStr = selectedDay.toISO8601WithTimeZoneOffset();
final isOnline = connectionCubit.state.isInternetAvailable;
log('[getDiary] START: day=$dayStr, isOnline=$isOnline');
// Офлайн: сначала пробуем кэш
if (!isOnline) {
try {
final result = await diaryRepository.getDiaryDayV2(day: dayStr);
log('[getDiary] OFFLINE: loaded from cache OK');
_lastFetchWasOffline = true;
return result;
} catch (_) {
// Кэша нет — попробуем сеть на случай ложного "offline"
log('[getDiary] OFFLINE: no cache, trying network as fallback...');
}
}
// Онлайн (или fallback после неудачного offline): создаём день + грузим с сервера
try {
log('[getDiary] ONLINE: creating diary day...');
await diaryRepository.createDiaryDay(day: dayStr);
log('[getDiary] ONLINE: day created, loading...');
final result = await diaryRepository.getDiaryDayV2(day: dayStr);
log('[getDiary] ONLINE: loaded OK, id=${result.id}');
return result;
} catch (e, st) {
log('[getDiary] ONLINE ERROR: $e');
await reportFrom(e, st, this);
if (e is DioException && e.response?.statusCode == 422) {
// День уже существует — просто загружаем
try {
log('[getDiary] 422 fallback: loading existing day...');
return await diaryRepository.getDiaryDayV2(day: dayStr);
} catch (e2) {
log('[getDiary] 422 fallback failed: $e2');
}
}
// Другая сетевая ошибка → пробуем кэш
_lastFetchWasOffline = true;
try {
log('[getDiary] Error fallback: trying cache...');
return await diaryRepository.getDiaryDayV2(day: dayStr);
} catch (e3) {
log('[getDiary] Cache fallback failed: $e3');
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: получаем кол-во операций в очереди
int pendingCount = 0;
try {
pendingCount = await localDataSource.getPendingCount();
} catch (_) {
// Ошибка чтения pending_operations — не критично
}
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;
}
@override
Future<void> close() {
_connectivitySubscription?.cancel();
return super.close();
}
}
enum DiaryFetchingErrors { canNotCreateFutureDays, dayAlreadyExists }