Files
eatvio_app/lib/features/insights/domain/insights_text_builder.dart
2026-11-09 15:35:10 +03:00

223 lines
7.1 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 'package:eatvio_app/features/insights/models/insights_payload_model.dart';
import 'package:eatvio_app/features/insights/models/insights_period.dart';
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
/// Собирает [InsightsPayloadModel] из сырых точек мобильного API.
///
/// Зеркало веб-`UserInsightsService`: порог изменения [deltaEpsilon], минимум две
/// точки для дельты, те же категории ИМТ и формат подписей.
class InsightsTextBuilder {
InsightsTextBuilder(this.s);
final S s;
static const double deltaEpsilon = 0.05;
static const String _emptyHeadline = '—';
InsightsPayloadModel build({
required List<WeightModel> weights,
required List<CircumferenceModel> circumferences,
required BodyPeriod period,
int? heightCm,
}) {
final sortedWeights = [...weights]
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
final sortedCircumferences = [...circumferences]
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
return InsightsPayloadModel(
weight: buildWeight(sortedWeights, period),
bmi: buildBmi(sortedWeights, period, heightCm),
measure: buildMeasure(sortedCircumferences, period),
);
}
WeightChartModel buildWeight(List<WeightModel> weights, BodyPeriod period) {
final values = weights.map((e) => e.weight).toList();
final last = values.isEmpty ? null : values.last;
final delta = values.length < 2 ? null : _round(values.last - values.first);
return WeightChartModel(
labels: weights.map((e) => formatLabel(e.createdAt)).toList(),
values: values,
last: last,
delta: delta,
headline: last == null ? _emptyHeadline : last.toStringAsFixed(1),
unit: s.insights_unit_kg,
deltaText: _weightDeltaText(values, period),
);
}
BmiChartModel buildBmi(
List<WeightModel> weights,
BodyPeriod period,
int? heightCm,
) {
final hasHeight = heightCm != null && heightCm > 0;
if (!hasHeight) {
return BmiChartModel(
headline: _emptyHeadline,
deltaText: s.insights_need_height,
hint: s.insights_need_height,
);
}
final points = weights.where((e) => e.bmi != null).toList();
final values = points.map((e) => e.bmi!).toList();
final last = values.isEmpty ? null : values.last;
final delta = values.length < 2 ? null : _round(values.last - values.first);
final category = last == null ? null : bmiCategory(last);
return BmiChartModel(
labels: points.map((e) => formatLabel(e.createdAt)).toList(),
values: values,
last: last,
delta: delta,
headline: last == null ? _emptyHeadline : last.toStringAsFixed(1),
category: category,
deltaText: _bmiDeltaText(values, period, category),
hint: s.insights_bmi_hint(heightCm.toString()),
);
}
MeasureChartModel buildMeasure(
List<CircumferenceModel> circumferences,
BodyPeriod period,
) {
final waist = circumferences.map((e) => e.waistCircumference).toList();
final chest = circumferences.map((e) => e.chestCircumference).toList();
final hips = circumferences.map((e) => e.hipCircumference).toList();
return MeasureChartModel(
labels: circumferences.map((e) => formatLabel(e.createdAt)).toList(),
waist: waist,
chest: chest,
hips: hips,
headline: waist.isEmpty ? _emptyHeadline : waist.last.round().toString(),
deltaText: _measureDeltaText(
waist: waist,
chest: chest,
hips: hips,
period: period,
),
);
}
/// Категория по таблице ВОЗ из постановки #2120.
String bmiCategory(double bmi) {
if (bmi < 16) {
return s.insights_bmi_severe_underweight;
}
if (bmi < 18.5) {
return s.insights_bmi_underweight;
}
if (bmi < 25) {
return s.insights_bmi_normal;
}
if (bmi < 30) {
return s.insights_bmi_overweight;
}
if (bmi < 35) {
return s.insights_bmi_obesity_1;
}
if (bmi < 40) {
return s.insights_bmi_obesity_2;
}
return s.insights_bmi_obesity_3;
}
String periodLabel(BodyPeriod period) {
switch (period) {
case BodyPeriod.month:
return s.insights_period_label_1;
case BodyPeriod.threeMonths:
return s.insights_period_label_3;
case BodyPeriod.sixMonths:
return s.insights_period_label_6;
case BodyPeriod.year:
return s.insights_period_label_12;
case BodyPeriod.all:
return s.insights_period_label_all;
}
}
String formatLabel(DateTime date) {
final local = date.toLocal();
final day = local.day.toString().padLeft(2, '0');
final month = local.month.toString().padLeft(2, '0');
return '$day.$month';
}
/// Одна цифра после запятой без хвостового нуля: `3`, `3.5`.
String formatNumber(double value) {
final text = value.toStringAsFixed(1);
return text.endsWith('.0') ? text.substring(0, text.length - 2) : text;
}
String _weightDeltaText(List<double> values, BodyPeriod period) {
if (values.length < 2) {
return s.insights_not_enough;
}
final diff = values.last - values.first;
final periodText = periodLabel(period);
if (diff.abs() < deltaEpsilon) {
return s.insights_weight_same(periodText);
}
final value = formatNumber(diff.abs());
return diff < 0
? s.insights_weight_lost(value, periodText)
: s.insights_weight_gained(value, periodText);
}
String _bmiDeltaText(
List<double> values,
BodyPeriod period,
String? category,
) {
if (values.length < 2 || category == null) {
return s.insights_not_enough;
}
final diff = values.last - values.first;
final periodText = periodLabel(period);
if (diff.abs() < deltaEpsilon) {
return s.insights_bmi_same(periodText, category);
}
final value = formatNumber(diff.abs());
return diff < 0
? s.insights_bmi_lower(value, periodText, category)
: s.insights_bmi_higher(value, periodText, category);
}
String _measureDeltaText({
required List<double> waist,
required List<double> chest,
required List<double> hips,
required BodyPeriod period,
}) {
if (waist.length < 2) {
return s.insights_not_enough;
}
final parts = [
_measurePart(s.insights_series_waist, waist),
_measurePart(s.insights_series_chest, chest),
_measurePart(s.insights_series_hips, hips),
];
return s.insights_measures_summary(parts.join(', '), periodLabel(period));
}
String _measurePart(String name, List<double> values) {
final diff = values.last - values.first;
if (diff.abs() < deltaEpsilon) {
return s.insights_measure_same(name);
}
final value = formatNumber(diff.abs());
return diff < 0
? s.insights_measure_less(name, value)
: s.insights_measure_more(name, value);
}
double _round(double value) => (value * 10).round() / 10;
}