Allow multiple weight entries per day

This commit is contained in:
Roland Geider
2025-09-26 12:21:50 +02:00
parent 2e5209b700
commit ccbaf0b42a
14 changed files with 87 additions and 44 deletions

View File

@@ -29,7 +29,7 @@ class WeightEntry {
@JsonKey(required: true, fromJson: stringToNum, toJson: numToString)
late num weight = 0;
@JsonKey(required: true, toJson: dateToYYYYMMDD)
@JsonKey(required: true)
late DateTime date;
WeightEntry({this.id, weight, DateTime? date}) {

View File

@@ -21,5 +21,5 @@ WeightEntry _$WeightEntryFromJson(Map<String, dynamic> json) {
Map<String, dynamic> _$WeightEntryToJson(WeightEntry instance) => <String, dynamic>{
'id': instance.id,
'weight': numToString(instance.weight),
'date': dateToYYYYMMDD(instance.date),
'date': instance.date.toIso8601String(),
};

View File

@@ -32,8 +32,6 @@ class WeightScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final lastWeightEntry = context.read<BodyWeightProvider>().getNewestEntry();
return Scaffold(
appBar: EmptyAppBar(AppLocalizations.of(context).weight),
floatingActionButton: FloatingActionButton(
@@ -44,7 +42,7 @@ class WeightScreen extends StatelessWidget {
FormScreen.routeName,
arguments: FormScreenArguments(
AppLocalizations.of(context).newEntry,
WeightForm(lastWeightEntry?.copyWith(id: null, date: DateTime.now())),
WeightForm(),
),
);
},

View File

@@ -21,38 +21,41 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/date.dart';
import 'package:wger/helpers/json.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/body_weight/weight_entry.dart';
import 'package:wger/providers/body_weight.dart';
class WeightForm extends StatelessWidget {
final _form = GlobalKey<FormState>();
final dateController = TextEditingController();
final weightController = TextEditingController();
final dateController = TextEditingController(text: '');
final timeController = TextEditingController(text: '');
final weightController = TextEditingController(text: '');
late final WeightEntry _weightEntry;
final WeightEntry _weightEntry;
WeightForm([WeightEntry? weightEntry]) {
_weightEntry = weightEntry ?? WeightEntry(date: DateTime.now());
weightController.text = '';
dateController.text = dateToYYYYMMDD(_weightEntry.date)!;
}
WeightForm([WeightEntry? weightEntry])
: _weightEntry = weightEntry ?? WeightEntry(date: DateTime.now());
@override
Widget build(BuildContext context) {
final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
final timeFormat = DateFormat.Hm(Localizations.localeOf(context).languageCode);
if (weightController.text.isEmpty && _weightEntry.weight != 0) {
weightController.text = numberFormat.format(_weightEntry.weight);
}
if (dateController.text.isEmpty) {
dateController.text = dateFormat.format(_weightEntry.date);
}
if (timeController.text.isEmpty) {
timeController.text = TimeOfDay.fromDateTime(_weightEntry.date).format(context);
}
return Form(
key: _form,
child: Column(
children: [
// Weight date
TextFormField(
key: const Key('dateInput'),
// Stop keyboard from appearing
@@ -72,24 +75,51 @@ class WeightForm extends StatelessWidget {
initialDate: _weightEntry.date,
firstDate: DateTime(DateTime.now().year - 10),
lastDate: DateTime.now(),
selectableDayPredicate: (day) {
// Always allow the current initial date
if (day.isSameDayAs(_weightEntry.date)) {
return true;
}
// if the date is known, don't allow it
return Provider.of<BodyWeightProvider>(context, listen: false).findByDate(day) ==
null;
},
);
if (pickedDate != null) {
dateController.text = dateToYYYYMMDD(pickedDate)!;
dateController.text = dateFormat.format(pickedDate);
}
},
onSaved: (newValue) {
_weightEntry.date = DateTime.parse(newValue!);
final date = dateFormat.parse(newValue!);
_weightEntry.date = _weightEntry.date.copyWith(
year: date.year,
month: date.month,
day: date.day,
);
},
),
TextFormField(
key: const Key('timeInput'),
// Stop keyboard from appearing
readOnly: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).time,
suffixIcon: const Icon(
Icons.access_time_outlined,
key: Key('clockIcon'),
),
),
enableInteractiveSelection: false,
controller: timeController,
onTap: () async {
final pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(_weightEntry.date),
);
if (pickedTime != null) {
timeController.text = pickedTime.format(context);
}
},
onSaved: (newValue) {
final time = timeFormat.parse(newValue!);
_weightEntry.date = _weightEntry.date.copyWith(
hour: time.hour,
minute: time.minute,
second: time.second,
);
},
),

View File

@@ -84,7 +84,7 @@ class WeightOverview extends StatelessWidget {
subtitle: Text(
DateFormat.yMd(
Localizations.localeOf(context).languageCode,
).format(currentEntry.date),
).add_Hm().format(currentEntry.date),
),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) {

View File

@@ -1009,7 +1009,7 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i20.NutritionPlans
@override
_i18.Future<_i13.Ingredient?> searchIngredientWithBarcode(String? barcode) => (super.noSuchMethod(
Invocation.method(
#searchIngredientWithCode,
#searchIngredientWithBarcode,
[barcode],
),
returnValue: _i18.Future<_i13.Ingredient?>.value(),

View File

@@ -414,7 +414,7 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i8.NutritionPlansP
@override
_i9.Future<_i7.Ingredient?> searchIngredientWithBarcode(String? barcode) => (super.noSuchMethod(
Invocation.method(
#searchIngredientWithCode,
#searchIngredientWithBarcode,
[barcode],
),
returnValue: _i9.Future<_i7.Ingredient?>.value(),

View File

@@ -414,7 +414,7 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i8.NutritionPlansP
@override
_i9.Future<_i7.Ingredient?> searchIngredientWithBarcode(String? barcode) => (super.noSuchMethod(
Invocation.method(
#searchIngredientWithCode,
#searchIngredientWithBarcode,
[barcode],
),
returnValue: _i9.Future<_i7.Ingredient?>.value(),

View File

@@ -34,11 +34,21 @@ void main() {
);
}
testWidgets('The form is prefilled with the data from an entry', (WidgetTester tester) async {
testWidgets('Correctly prefills and localizes the data - en', (WidgetTester tester) async {
await tester.pumpWidget(createWeightForm(weightEntry: testWeightEntry1));
await tester.pumpAndSettle();
expect(find.text('2021-01-01'), findsOneWidget);
expect(find.text('1/1/2021'), findsOneWidget);
expect(find.text('3:30 PM'), findsOneWidget);
expect(find.text('80'), findsOneWidget);
});
testWidgets('Correctly prefills and localizes the data - de', (WidgetTester tester) async {
await tester.pumpWidget(createWeightForm(weightEntry: testWeightEntry1, locale: 'de'));
await tester.pumpAndSettle();
expect(find.text('1.1.2021'), findsOneWidget);
expect(find.text('15:30'), findsOneWidget);
expect(find.text('80'), findsOneWidget);
});

View File

@@ -22,11 +22,15 @@ import 'package:wger/models/body_weight/weight_entry.dart';
void main() {
group('fetchPost', () {
test('Test that the weight entries are correctly converted to json', () {
WeightEntry weightEntry = WeightEntry(id: 1, weight: 80, date: DateTime(2020, 12, 31));
expect(weightEntry.toJson(), {'id': 1, 'weight': '80', 'date': '2020-12-31'});
expect(
WeightEntry(id: 1, weight: 80, date: DateTime(2020, 12, 31, 12, 34)).toJson(),
{'id': 1, 'weight': '80', 'date': '2020-12-31T12:34:00.000'},
);
weightEntry = WeightEntry(id: 2, weight: 70.2, date: DateTime(2020, 12, 01));
expect(weightEntry.toJson(), {'id': 2, 'weight': '70.2', 'date': '2020-12-01'});
expect(
WeightEntry(id: 2, weight: 70.2, date: DateTime(2020, 12, 01)).toJson(),
{'id': 2, 'weight': '70.2', 'date': '2020-12-01T00:00:00.000'},
);
});
test('Test that the weight entries are correctly converted from json', () {

View File

@@ -70,8 +70,10 @@ void main() {
path: 'api/v2/weightentry/',
);
when(mockBaseProvider.makeUrl(any, query: anyNamed('query'))).thenReturn(uri);
when(mockBaseProvider.post({'id': null, 'weight': '80', 'date': '2021-01-01'}, uri))
.thenAnswer((_) => Future.value({'id': 25, 'date': '2021-01-01', 'weight': '80'}));
when(mockBaseProvider.post(
{'id': null, 'weight': '80', 'date': '2021-01-01T00:00:00.000'},
uri,
)).thenAnswer((_) => Future.value({'id': 25, 'date': '2021-01-01', 'weight': '80'}));
// Act
final BodyWeightProvider provider = BodyWeightProvider(mockBaseProvider);

View File

@@ -43,7 +43,6 @@ void main() {
setUp(() {
mockWeightProvider = MockBodyWeightProvider();
when(mockWeightProvider.items).thenReturn(getWeightEntries());
when(mockWeightProvider.getNewestEntry()).thenReturn(null);
mockUserProvider = MockUserProvider();
when(mockUserProvider.profile).thenReturn(tProfile1);

View File

@@ -739,7 +739,7 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i16.NutritionPlans
@override
_i11.Future<_i9.Ingredient?> searchIngredientWithBarcode(String? barcode) => (super.noSuchMethod(
Invocation.method(
#searchIngredientWithCode,
#searchIngredientWithBarcode,
[barcode],
),
returnValue: _i11.Future<_i9.Ingredient?>.value(),

View File

@@ -18,8 +18,8 @@
import 'package:wger/models/body_weight/weight_entry.dart';
final testWeightEntry1 = WeightEntry(id: 1, weight: 80, date: DateTime(2021, 01, 01));
final testWeightEntry2 = WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10));
final testWeightEntry1 = WeightEntry(id: 1, weight: 80, date: DateTime(2021, 01, 01, 15, 30));
final testWeightEntry2 = WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10, 10, 0));
List<WeightEntry> getWeightEntries() {
return [testWeightEntry1, testWeightEntry2];