feat(insights): added kbju fetching for analitics

This commit is contained in:
misha
2026-11-10 12:21:53 +03:00
parent 45d072cff4
commit 8c33f033ff
30 changed files with 1910 additions and 118 deletions

View File

@@ -17,6 +17,9 @@ abstract class DiaryDayEntity with _$DiaryDayEntity {
@JsonKey(name: "chosen_plan_id") required int? chosenPlanId,
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
@JsonKey(name: "chosen_diet", fromJson: parseDiet) required DiaryDietModel? diet,
/// Slug владельца дневника; `null`, если сервер его не вернул.
String? ownerSlug,
}) = _DiaryDayEntity;
const DiaryDayEntity._();

View File

@@ -48,6 +48,10 @@ abstract class DiaryDayModel with _$DiaryDayModel {
@JsonKey(name: "chosen_plan_id") required int? chosenPlanId,
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
@JsonKey(name: "chosen_diet", fromJson: parseDiet) required DiaryDietModel? diet,
/// Владелец дневника (`user` в `DiaryResource`): по `slug` консультант
/// запрашивает `/api/profile/insights/{slug}`.
@JsonKey(name: "user") DiaryOwnerModel? owner,
}) = _DiaryDayModel;
const DiaryDayModel._();
factory DiaryDayModel.fromJson(Map<String, dynamic> json) => _$DiaryDayModelFromJson(json);
@@ -68,6 +72,7 @@ abstract class DiaryDayModel with _$DiaryDayModel {
wakeup: wakeup,
retire: retire,
diet: diet,
ownerSlug: owner?.slug,
);
}
@@ -81,6 +86,13 @@ DiaryDietModel? parseDiet(dynamic json) {
return DiaryDietModel.fromJson(json['diet']);
}
@freezed
abstract class DiaryOwnerModel with _$DiaryOwnerModel {
const factory DiaryOwnerModel({int? id, String? slug}) = _DiaryOwnerModel;
factory DiaryOwnerModel.fromJson(Map<String, dynamic> json) => _$DiaryOwnerModelFromJson(json);
}
@freezed
abstract class DiaryDietModel with _$DiaryDietModel {
const factory DiaryDietModel({required int id, required int duration, required String name}) = _DiaryDietModel;

View File

@@ -22,6 +22,7 @@ import 'package:eatvio_app/features/diary/presentation/widgets/meal_widget.dart'
import 'package:eatvio_app/features/diary/presentation/widgets/wakeup_and_retire_widget.dart';
import 'package:eatvio_app/features/insights/presentation/widgets/insights_card.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -49,7 +50,12 @@ class UserDiaryScreen extends StatelessWidget {
}
class DiaryScreenContent extends StatefulWidget {
const DiaryScreenContent({super.key, this.diaryId, this.date, this.ownerSlug});
const DiaryScreenContent({
super.key,
this.diaryId,
this.date,
this.ownerSlug,
});
final int? diaryId;
final DateTime? date;
final String? ownerSlug;
@@ -60,9 +66,14 @@ class DiaryScreenContent extends StatefulWidget {
class _DiaryScreenContentState extends State<DiaryScreenContent> {
bool _isDiaryFinishing = false;
/// Slug владельца: из `user.slug` дневника, иначе из параметра экрана.
String? _ownerSlug(DiaryStateLoaded state) {
return state.diary.ownerSlug ?? widget.ownerSlug;
}
/// Консультант видит аналитику клиента только при известном slug владельца.
bool _canShowInsights(DiaryStateLoaded state) {
return !state.isConsultantView || widget.ownerSlug != null;
return !state.isConsultantView || _ownerSlug(state) != null;
}
@override
@@ -122,9 +133,9 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
],
if (state is DiaryStateLoaded && _canShowInsights(state))
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: InsightsCard(
slug: state.isConsultantView ? widget.ownerSlug : null,
padding: const EdgeInsets.fromLTRB(4, 12, 4, 12),
child: _DiaryInsights(
slug: state.isConsultantView ? _ownerSlug(state) : null,
canAdd: !state.isConsultantView,
),
),
@@ -267,3 +278,45 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
);
}
}
/// Блок «Аналитика» в дневнике. Для своего дневника перезапрашивает данные,
/// когда в том же дне меняются приёмы пищи или время подъёма/отбоя: от них
/// зависят КБЖУ-графики. Чужой дневник консультант не редактирует.
class _DiaryInsights extends StatefulWidget {
const _DiaryInsights({required this.slug, required this.canAdd});
final String? slug;
final bool canAdd;
@override
State<_DiaryInsights> createState() => _DiaryInsightsState();
}
class _DiaryInsightsState extends State<_DiaryInsights> {
int _refreshToken = 0;
bool _isOwnDiaryMutation(DiaryState previous, DiaryState current) {
if (!widget.canAdd ||
previous is! DiaryStateLoaded ||
current is! DiaryStateLoaded ||
previous.diary.id != current.diary.id) {
return false;
}
return !listEquals(previous.diary.meals, current.diary.meals) ||
previous.diary.wakeup != current.diary.wakeup ||
previous.diary.retire != current.diary.retire;
}
@override
Widget build(BuildContext context) {
return BlocListener<DiaryCubit, DiaryState>(
listenWhen: _isOwnDiaryMutation,
listener: (context, state) => setState(() => _refreshToken++),
child: InsightsCard(
slug: widget.slug,
canAdd: widget.canAdd,
refreshToken: _refreshToken,
),
);
}
}