added editing entries and deleting them

This commit is contained in:
doman 2023-07-30 13:09:41 +02:00
parent fa9a0569fb
commit 955c506b92
9 changed files with 369 additions and 101 deletions

View file

@ -76,7 +76,7 @@ class ApiClient {
} }
Future<Map<String, dynamic>> delete(String path) async { Future<void> delete(String path) async {
final response = await httpClient.delete( final response = await httpClient.delete(
Uri.parse('$baseUrl$path'), Uri.parse('$baseUrl$path'),
headers: headers(), headers: headers(),
@ -85,8 +85,6 @@ class ApiClient {
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Response returned status code: ${response.statusCode}'); throw Exception('Response returned status code: ${response.statusCode}');
} }
return _jsonDecode(response);
} }
Future<Map<String, dynamic>> patch(String path, Map<String, dynamic> body) async { Future<Map<String, dynamic>> patch(String path, Map<String, dynamic> body) async {
@ -182,4 +180,22 @@ class ApiClient {
}; };
await post("/entry", entry); await post("/entry", entry);
} }
Future<void> deleteEntry(int id) async {
await delete("/entry/$id");
}
Future<void> updateEntry(int id, {
required double grams,
required int productId,
required int mealId,
}
) async {
var entry = {
"grams": grams,
"product_id": productId,
"meal_id": mealId,
};
await patch("/entry/$id", entry);
}
} }

View file

@ -65,7 +65,6 @@ class _AddEntryScreen extends State<AddEntryScreen> {
try { try {
double.parse(gramsController.text); double.parse(gramsController.text);
} catch (e) { } catch (e) {
print(e);
showError("Grams must be a number"); showError("Grams must be a number");
return; return;
} }
@ -106,10 +105,10 @@ class _AddEntryScreen extends State<AddEntryScreen> {
), ),
for (var product in products) for (var product in products)
ListTile( ListTile(
onTap: () { onTap: () {
setState(() { setState(() {
products = [product]; products = [product];
}); });
}, },
title: ProductWidget( title: ProductWidget(
product: product, product: product,

146
lib/screens/edit_entry.dart Normal file
View file

@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:fooder_web/screens/based.dart';
import 'package:fooder_web/models/product.dart';
import 'package:fooder_web/models/entry.dart';
import 'package:fooder_web/widgets/product.dart';
class EditEntryScreen extends BasedScreen {
final Entry entry;
const EditEntryScreen({super.key, required super.apiClient, required this.entry});
@override
State<EditEntryScreen> createState() => _EditEntryScreen();
}
class _EditEntryScreen extends State<EditEntryScreen> {
final gramsController = TextEditingController();
final productNameController = TextEditingController();
List<Product> products = [];
@override
void dispose() {
gramsController.dispose();
productNameController.dispose();
super.dispose();
}
void popMeDady() {
Navigator.pop(context);
}
@override
void initState () {
super.initState();
setState(() {
gramsController.text = widget.entry.grams.toString();
productNameController.text = widget.entry.product.name;
products = [widget.entry.product];
});
}
Future<void> _getProducts() async {
var productsMap = await widget.apiClient.getProducts(productNameController.text);
setState(() {
products = (productsMap['products'] as List<dynamic>).map((e) => Product.fromJson(e as Map<String, dynamic>)).toList();
});
}
void showError(String message)
{
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message, textAlign: TextAlign.center),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
Future<void> _saveEntry() async {
if (products.length != 1) {
showError("Pick product first");
return;
}
try {
double.parse(gramsController.text);
} catch (e) {
showError("Grams must be a number");
return;
}
await widget.apiClient.updateEntry(
widget.entry.id,
grams: double.parse(gramsController.text),
productId: products[0].id,
mealId: widget.entry.mealId,
);
popMeDady();
}
Future<void> _deleteEntry() async {
await widget.apiClient.deleteEntry(widget.entry.id);
popMeDady();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("🅵🅾🅾🅳🅴🆁"),
),
body: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 720),
padding: const EdgeInsets.all(10),
child: ListView(
children: <Widget>[
TextFormField(
decoration: const InputDecoration(
labelText: 'Grams',
),
controller: gramsController,
),
TextFormField(
decoration: const InputDecoration(
labelText: 'Product name',
),
controller: productNameController,
onChanged: (_) => _getProducts(),
),
for (var product in products)
ListTile(
onTap: () {
setState(() {
products = [product];
});
},
title: ProductWidget(
product: product,
),
),
]
)
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: _deleteEntry,
heroTag: null,
child: const Icon(Icons.delete),
),
FloatingActionButton(
onPressed: _saveEntry,
heroTag: null,
child: const Icon(Icons.save),
),
],
),
);
}
}

View file

@ -69,7 +69,7 @@ class _MainScreen extends State<MainScreen> {
content = Container( content = Container(
constraints: const BoxConstraints(maxWidth: 720), constraints: const BoxConstraints(maxWidth: 720),
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: DiaryWidget(diary: diary!), child: DiaryWidget(diary: diary!, apiClient: widget.apiClient, refreshParent: _asyncInitState),
); );
title = Row( title = Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View file

@ -1,55 +1,67 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fooder_web/models/diary.dart'; import 'package:fooder_web/models/diary.dart';
import 'package:fooder_web/widgets/meal.dart'; import 'package:fooder_web/widgets/meal.dart';
import 'package:fooder_web/widgets/macro.dart';
import 'package:fooder_web/client.dart';
import 'dart:core'; import 'dart:core';
class DiaryWidget extends StatelessWidget { class DiaryWidget extends StatelessWidget {
final Diary diary; final Diary diary;
final ApiClient apiClient;
final Function() refreshParent;
const DiaryWidget({super.key, required this.diary}); const DiaryWidget({super.key, required this.diary, required this.apiClient, required this.refreshParent});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: ListView( child: CustomScrollView(
padding: const EdgeInsets.all(8), slivers: <Widget>[
children: <Widget>[ SliverAppBar(
for (var meal in diary.meals) title: Row(
MealWidget( children: <Widget>[
meal: meal, const Spacer(),
), Text(
Card( "${diary.calories.toStringAsFixed(1)} kcal",
child: Padding( style: Theme.of(context).textTheme.headlineLarge!.copyWith(
padding: const EdgeInsets.all(8.0), color: Theme.of(context).colorScheme.onSecondary,
child: Column( fontWeight: FontWeight.bold,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"carb: ${diary.carb.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
Text(
"fat: ${diary.fat.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
Text(
"protein: ${diary.protein.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
Text(
"calories: ${diary.calories.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
],
), ),
], ),
]
),
expandedHeight: 128,
backgroundColor: Theme.of(context).colorScheme.secondary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
floating: true,
flexibleSpace: FlexibleSpaceBar(
title: MacroWidget(
protein: diary.protein,
carb: diary.carb,
fat: diary.fat,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSecondary,
fontWeight: FontWeight.bold,
),
), ),
), ),
) ),
SliverList(
delegate: SliverChildListDelegate(
[
for (var meal in diary.meals)
MealWidget(
meal: meal,
apiClient: apiClient,
refreshParent: refreshParent,
),
],
),
),
], ],
), ),
); );

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fooder_web/models/entry.dart'; import 'package:fooder_web/models/entry.dart';
import 'package:fooder_web/widgets/macro.dart';
import 'dart:core'; import 'dart:core';
@ -22,29 +23,17 @@ class EntryWidget extends StatelessWidget {
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
), ),
Text("${entry.calories.toStringAsFixed(2)} kcal"), Text("${entry.calories.toStringAsFixed(1)} kcal"),
], ],
), ),
Row( MacroWidget(
mainAxisAlignment: MainAxisAlignment.spaceBetween, protein: entry.protein,
children: <Widget>[ carb: entry.carb,
Text( fat: entry.fat,
"carb: ${entry.carb.toStringAsFixed(2)}", amount: entry.grams,
style: TextStyle(color: Theme.of(context).colorScheme.secondary), style: Theme.of(context).textTheme.bodyMedium!.copyWith(
), color: Theme.of(context).colorScheme.secondary,
Text( ),
"fat: ${entry.fat.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
Text(
"protein: ${entry.protein.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
Text(
"amount: ${entry.grams.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
],
), ),
], ],
), ),

102
lib/widgets/macro.dart Normal file
View file

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
class MacroWidget extends StatelessWidget {
final double? amount;
final double? calories;
final double protein;
final double carb;
final double fat;
final TextStyle style;
const MacroWidget({
Key? key,
this.calories,
this.amount,
required this.protein,
required this.carb,
required this.fat,
required this.style,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var elements = <Widget>[
Expanded(
flex: 1,
child: Text(
"C: ${carb.toStringAsFixed(1)} g",
style: style,
textAlign: TextAlign.center,
),
),
Expanded(
flex: 1,
child: Text(
"F: ${fat.toStringAsFixed(1)} g",
style: style,
textAlign: TextAlign.center,
),
),
Expanded(
flex: 1,
child: Text(
"P: ${protein.toStringAsFixed(1)} g",
style: style,
textAlign: TextAlign.center,
),
),
];
if (calories != null) {
elements.add(
Expanded(
flex: 1,
child: Text(
"${calories!.toStringAsFixed(1)} kcal",
style: style,
textAlign: TextAlign.center,
),
),
);
}
if (amount != null) {
elements.add(
Expanded(
flex: 1,
child: Text(
"${amount!.toStringAsFixed(1)} g",
style: style,
textAlign: TextAlign.center,
),
),
);
}
if (amount == null && calories == null) {
elements.add(
Expanded(
flex: 1,
child: Text(
"",
style: style,
textAlign: TextAlign.center,
),
),
);
}
return Padding(
padding: const EdgeInsets.only(
top: 4.0,
bottom: 4.0,
left: 8.0,
right: 8.0,
),
child: Row(
children: elements,
),
);
}
}

View file

@ -1,13 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fooder_web/models/meal.dart'; import 'package:fooder_web/models/meal.dart';
import 'package:fooder_web/widgets/entry.dart'; import 'package:fooder_web/widgets/entry.dart';
import 'package:fooder_web/widgets/macro.dart';
import 'package:fooder_web/screens/edit_entry.dart';
import 'package:fooder_web/client.dart';
import 'dart:core'; import 'dart:core';
class MealWidget extends StatelessWidget { class MealWidget extends StatelessWidget {
final Meal meal; final Meal meal;
final ApiClient apiClient;
final Function() refreshParent;
const MealWidget({super.key, required this.meal}); const MealWidget({super.key, required this.meal, required this.apiClient, required this.refreshParent});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -26,33 +31,39 @@ class MealWidget extends StatelessWidget {
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
), ),
Text("${meal.calories.toStringAsFixed(2)} kcal"), Text("${meal.calories.toStringAsFixed(1)} kcal"),
], ],
), ),
Row( MacroWidget(
mainAxisAlignment: MainAxisAlignment.spaceBetween, protein: meal.protein,
children: <Widget>[ carb: meal.carb,
Text( fat: meal.fat,
"carb: ${meal.carb.toStringAsFixed(2)}", style: Theme.of(context).textTheme.bodyMedium!.copyWith(
style: TextStyle(color: Theme.of(context).colorScheme.secondary), color: Theme.of(context).colorScheme.secondary,
), ),
Text(
"fat: ${meal.fat.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
Text(
"protein: ${meal.protein.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
],
), ),
], ],
), ),
children: <Widget>[ children: <Widget>[
for (var entry in meal.entries) for (var entry in meal.entries)
EntryWidget( ListTile(
entry: entry, title: EntryWidget(
entry: entry,
), ),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditEntryScreen(
apiClient: apiClient,
entry: entry,
),
),
).then((_) {
refreshParent();
});
},
)
], ],
), ),
), ),

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fooder_web/models/product.dart'; import 'package:fooder_web/models/product.dart';
import 'package:fooder_web/widgets/macro.dart';
import 'dart:core'; import 'dart:core';
@ -22,25 +23,17 @@ class ProductWidget extends StatelessWidget {
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
), ),
Text("${product.calories.toStringAsFixed(2)} kcal"), Text("${product.calories.toStringAsFixed(1)} kcal"),
], ],
), ),
Row( MacroWidget(
mainAxisAlignment: MainAxisAlignment.spaceBetween, protein: product.protein,
children: <Widget>[ carb: product.carb,
Text( fat: product.fat,
"carb: ${product.carb.toStringAsFixed(2)}", style: Theme.of(context).textTheme.bodyMedium!.copyWith(
style: TextStyle(color: Theme.of(context).colorScheme.secondary), color: Theme.of(context).colorScheme.secondary,
), fontWeight: FontWeight.bold,
Text( ),
"fat: ${product.fat.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
Text(
"protein: ${product.protein.toStringAsFixed(2)}",
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
],
), ),
], ],
), ),