cache products for item and product list and product view screen

BUG: when deleting a product the product list won't update
This commit is contained in:
Jakob Meier 2024-02-23 20:06:49 +01:00
parent 6cdfcdf85c
commit 9ff6d97c90
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
8 changed files with 466 additions and 177 deletions

View file

@ -547,7 +547,7 @@ String colorIdFromColor(ColorSwatch<int> color) {
} }
class RoomProduct { class RoomProduct {
int id; final int id;
String name; String name;
String description; String description;
// category ID // category ID
@ -563,9 +563,14 @@ class RoomProduct {
// parent product ID // parent product ID
int? parent; int? parent;
final String server;
final String room;
RoomProduct( RoomProduct(
{required this.id, {required this.id,
required this.name, required this.name,
required this.server,
required this.room,
this.description = '', this.description = '',
this.category = -1, this.category = -1,
this.defaultUnit = 0, this.defaultUnit = 0,
@ -573,8 +578,10 @@ class RoomProduct {
this.ean, this.ean,
this.parent}); this.parent});
factory RoomProduct.fromJSON(dynamic json) { factory RoomProduct.fromJSON(String server, String room, dynamic json) {
return RoomProduct( return RoomProduct(
server: server,
room: room,
id: json['listProdID'], id: json['listProdID'],
name: json['title'], name: json['title'],
description: json['description'], description: json['description'],
@ -584,6 +591,78 @@ class RoomProduct {
ean: json['ean'], ean: json['ean'],
parent: json['parent']); parent: json['parent']);
} }
// get list of all categories in a given room
static Future<List<RoomProduct>> list(String server, String room) async {
final db = Localstore.instance;
final rooms = (await db.collection('products:$room@$server').get()) ?? {};
List<RoomProduct> builder = [];
for (MapEntry entry in rooms.entries) {
try {
builder.add(RoomProduct.fromMap(entry.value));
} catch (e) {
// skip invalid entries
// NOTE: might want to autodelete them in the future
// although keeping them might be ok,
// in case we ever get a new dataset to fix the current state
}
}
return builder;
}
// listen to room change
static listen(
String server, String room, Function(Map<String, dynamic>) cb) async {
final db = Localstore.instance;
final stream = db.collection('products:$room@$server').stream;
stream.listen(cb);
}
factory RoomProduct.fromMap(Map<String, dynamic> map) {
return RoomProduct(
server: map['server'],
room: map['room'],
id: map['id'],
name: map['name'],
description: map['description'],
category: map['category'],
defaultUnit: map['default_unit'],
defaultValue: map['default_value'],
ean: map['ean'],
parent: map['parent']);
}
Map<String, dynamic> toMap() {
return {
'server': server,
'room': room,
'id': id,
'name': name,
'description': description,
'category': category,
'default_unit': defaultUnit,
'default_value': defaultValue,
'ean': ean,
'parent': parent
};
}
Future<void> toDisk() async {
final db = Localstore.instance;
await db.collection('products:$room@$server').doc('$id').set(toMap());
}
Future<void> removeDisk() async {
final db = Localstore.instance;
await db.collection('products:$room@$server').doc('$id').delete();
}
static Future<RoomProduct> fromDisk(
{required int id, required String server, required String room}) async {
final db = Localstore.instance;
final raw = await db.collection('products:$room@$server').doc('$id').get();
return RoomProduct.fromMap(raw!);
}
} }
class RoomItem { class RoomItem {

View file

@ -77,7 +77,8 @@ class _EditItemPageState extends State<EditItemPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
setState(() { setState(() {

View file

@ -66,7 +66,8 @@ class _NewItemPageState extends State<NewItemPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
setState(() { setState(() {

View file

@ -106,12 +106,25 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
} }
} }
void fetchCategories() { void fetchCategories() async {
final user = context.read<User>(); final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached categories first // load cached categories
final cache = await RoomCategory.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
if (mounted) {
Map<int?, RoomCategory> map = {};
doNetworkRequest(ScaffoldMessenger.of(context), for (RoomCategory cat in cache) {
map[cat.id] = cat;
}
setState(() {
categories = map;
});
}
doNetworkRequest(scaffmgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -140,12 +153,20 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
}); });
} }
void fetchProducts() { void fetchProducts() async {
final user = context.read<User>(); final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached products first // load cached products first
final cache = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
if (mounted) {
setState(() {
products = cache;
});
}
doNetworkRequest(ScaffoldMessenger.of(context), doNetworkRequest(scaffmgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -153,7 +174,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) => RoomProduct.fromJSON(
widget.room!.serverTag, widget.room!.id, raw))
.toList(); .toList();
if (mounted) { if (mounted) {
@ -168,6 +190,34 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
void initState() { void initState() {
super.initState(); super.initState();
// wait for background room product changes
RoomProduct.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "",
(_) async {
try {
final updated = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
setState(() {
products = updated;
});
} catch (_) {}
});
// wait for background room category changes
RoomCategory.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "",
(_) async {
try {
final updated = await RoomCategory.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
Map<int?, RoomCategory> map = {};
for (RoomCategory cat in updated) {
map[cat.id] = cat;
}
setState(() {
categories = map;
});
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
fetchItems(); fetchItems();
fetchCategories(); fetchCategories();

View file

@ -21,10 +21,20 @@ class RoomProductsPage extends StatefulWidget {
class _RoomProductsPageState extends State<RoomProductsPage> { class _RoomProductsPageState extends State<RoomProductsPage> {
List<RoomProduct> products = []; List<RoomProduct> products = [];
void fetchProducts() { void fetchProducts() async {
final user = context.read<User>(); final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
doNetworkRequest(ScaffoldMessenger.of(context), // load cached products first
final cache = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
if (mounted) {
setState(() {
products = cache;
});
}
doNetworkRequest(scaffmgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -32,10 +42,13 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) => RoomProduct.fromJSON(
widget.room!.serverTag, widget.room!.id, raw))
.toList(); .toList();
// TODO: cache products for (RoomProduct prod in resp) {
prod.toDisk();
}
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -49,6 +62,18 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
void initState() { void initState() {
super.initState(); super.initState();
// wait for background room product changes
RoomProduct.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "",
(_) async {
try {
final updated = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
setState(() {
products = updated;
});
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) => fetchProducts()); WidgetsBinding.instance.addPostFrameCallback((_) => fetchProducts());
} }

View file

@ -36,12 +36,19 @@ class _EditProductPageState extends State<EditProductPage> {
List<RoomCategory> categories = []; List<RoomCategory> categories = [];
List<RoomProduct> products = []; List<RoomProduct> products = [];
void fetchCategories() { void fetchCategories() async {
final user = context.read<User>(); final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached categories first // load cached categories
final cache = await RoomCategory.list(widget.server, widget.room);
if (mounted) {
setState(() {
categories = cache;
});
}
doNetworkRequest(ScaffoldMessenger.of(context), doNetworkRequest(scaffmgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -59,12 +66,19 @@ class _EditProductPageState extends State<EditProductPage> {
}); });
} }
void fetchProducts() { void fetchProducts() async {
final user = context.read<User>(); final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached products first // load cached products first
final cache = await RoomProduct.list(widget.server, widget.room);
if (mounted) {
setState(() {
products = cache;
});
}
doNetworkRequest(ScaffoldMessenger.of(context), doNetworkRequest(scaffmgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -72,7 +86,8 @@ class _EditProductPageState extends State<EditProductPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
if (widget.product != null) { if (widget.product != null) {
@ -255,7 +270,22 @@ class _EditProductPageState extends State<EditProductPage> {
'ean': _ctrEAN.text, 'ean': _ctrEAN.text,
'parent': _ctrParent 'parent': _ctrParent
}), }),
onOK: (_) async { onOK: (body) async {
// cache product
final id = body["data"]["listProdID"];
final prod = RoomProduct(
id: id,
name: _ctrName.text,
server: widget.server,
room: widget.room,
description: _ctrDescription.text,
category: _ctrCategory,
defaultUnit: _ctrUnit,
defaultValue: _ctrValue,
ean: _ctrEAN.text,
parent: _ctrParent);
await prod.toDisk();
nav.pop(); nav.pop();
}); });
} else { } else {
@ -277,6 +307,20 @@ class _EditProductPageState extends State<EditProductPage> {
'parent': _ctrParent 'parent': _ctrParent
}), }),
onOK: (_) async { onOK: (_) async {
// cache product
final prod = RoomProduct(
id: widget.product!,
name: _ctrName.text,
server: widget.server,
room: widget.room,
description: _ctrDescription.text,
category: _ctrCategory,
defaultUnit: _ctrUnit,
defaultValue: _ctrValue,
ean: _ctrEAN.text,
parent: _ctrParent);
await prod.toDisk();
nav.pop(); nav.pop();
}); });
} }

View file

@ -53,12 +53,24 @@ class _ViewProductPageState extends State<ViewProductPage> {
); );
} }
void fetchCategories() { void fetchCategories() async {
final user = context.read<User>(); final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached categories first // load cached categories
final cache = await RoomCategory.list(widget.server, widget.room);
if (mounted) {
Map<int?, RoomCategory> map = {};
doNetworkRequest(ScaffoldMessenger.of(context), for (RoomCategory cat in cache) {
map[cat.id] = cat;
}
setState(() {
categories = map;
});
}
doNetworkRequest(scaffmgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -81,12 +93,19 @@ class _ViewProductPageState extends State<ViewProductPage> {
}); });
} }
void fetchProducts() { void fetchProducts() async {
final user = context.read<User>(); final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached products first // load cached products first
final cache = await RoomProduct.list(widget.server, widget.room);
if (mounted) {
setState(() {
products = cache;
});
}
doNetworkRequest(ScaffoldMessenger.of(context), doNetworkRequest(scaffmgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -94,7 +113,8 @@ class _ViewProductPageState extends State<ViewProductPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
for (RoomProduct prod in resp) { for (RoomProduct prod in resp) {
@ -116,6 +136,39 @@ class _ViewProductPageState extends State<ViewProductPage> {
void initState() { void initState() {
super.initState(); super.initState();
// wait for background room product changes
RoomProduct.listen(widget.server, widget.room, (_) async {
try {
final updated = await RoomProduct.list(widget.server, widget.room);
for (RoomProduct prod in updated) {
// load product info
// for current product
if (prod.id == widget.product) {
setState(() {
product = prod;
});
}
}
setState(() {
products = updated;
});
} catch (_) {}
});
// wait for background room category changes
RoomCategory.listen(widget.server, widget.room, (_) async {
try {
final updated = await RoomCategory.list(widget.server, widget.room);
Map<int?, RoomCategory> map = {};
for (RoomCategory cat in updated) {
map[cat.id] = cat;
}
setState(() {
categories = map;
});
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
fetchCategories(); fetchCategories();
fetchProducts(); fetchProducts();
@ -132,6 +185,11 @@ class _ViewProductPageState extends State<ViewProductPage> {
title: Text(product?.name ?? ''), title: Text(product?.name ?? ''),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(children: [ child: Column(children: [
// display product into // display product into
Center( Center(
@ -141,17 +199,20 @@ class _ViewProductPageState extends State<ViewProductPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text(product?.name ?? '', style: textTheme.headlineLarge), Text(product?.name ?? '',
style: textTheme.headlineLarge),
Text(product?.description ?? '', Text(product?.description ?? '',
style: textTheme.titleMedium), style: textTheme.titleMedium),
Text(product?.ean ?? ''), Text(product?.ean ?? ''),
CategoryChip( CategoryChip(
server: widget.server, server: widget.server,
room: widget.room, room: widget.room,
category: categories[product?.category]), category:
categories[product?.category]),
Text(product != null Text(product != null
? Unit.fromId(product!.defaultUnit) ? Unit.fromId(product!.defaultUnit)
.display(context, product!.defaultValue) .display(
context, product!.defaultValue)
: '') : '')
], ],
))), ))),
@ -161,12 +222,15 @@ class _ViewProductPageState extends State<ViewProductPage> {
...(info != null && ...(info != null &&
(info!.isAdmin || (info!.isAdmin ||
info!.isOwner || info!.isOwner ||
(info!.permissions & RoomPermission.editRoomContent != 0))) (info!.permissions &
RoomPermission.editRoomContent !=
0)))
? [ ? [
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.editProductTitle), title: Text(AppLocalizations.of(context)!
subtitle: .editProductTitle),
Text(AppLocalizations.of(context)!.editProductSubtitle), subtitle: Text(AppLocalizations.of(context)!
.editProductSubtitle),
onTap: () { onTap: () {
context.pushNamed('edit-product', params: { context.pushNamed('edit-product', params: {
'server': widget.server, 'server': widget.server,
@ -182,10 +246,10 @@ class _ViewProductPageState extends State<ViewProductPage> {
...(product?.parent != null) ...(product?.parent != null)
? [ ? [
ListTile( ListTile(
title: Text( title: Text(AppLocalizations.of(context)!
AppLocalizations.of(context)!.viewParentProductTitle), .viewParentProductTitle),
subtitle: Text( subtitle: Text(AppLocalizations.of(context)!
AppLocalizations.of(context)!.viewParentProductSubtitle), .viewParentProductSubtitle),
onTap: () { onTap: () {
context.pushNamed('view-product', params: { context.pushNamed('view-product', params: {
'server': widget.server, 'server': widget.server,
@ -199,9 +263,10 @@ class _ViewProductPageState extends State<ViewProductPage> {
: [], : [],
// show/manage children // show/manage children
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.viewProductChildrenTitle), title: Text(AppLocalizations.of(context)!
subtitle: .viewProductChildrenTitle),
Text(AppLocalizations.of(context)!.viewProductChildrenSubtitle), subtitle: Text(AppLocalizations.of(context)!
.viewProductChildrenSubtitle),
onTap: () { onTap: () {
context.pushNamed('view-product-children', params: { context.pushNamed('view-product-children', params: {
'server': widget.server, 'server': widget.server,
@ -214,14 +279,16 @@ class _ViewProductPageState extends State<ViewProductPage> {
...(info != null && ...(info != null &&
((info?.isAdmin ?? false) || ((info?.isAdmin ?? false) ||
(info?.isOwner ?? false) || (info?.isOwner ?? false) ||
((info?.permissions)! & RoomPermission.editRoomContent != ((info?.permissions)! &
RoomPermission.editRoomContent !=
0))) 0)))
? [ ? [
// delete product // delete product
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.deleteProductTitle), title: Text(AppLocalizations.of(context)!
subtitle: .deleteProductTitle),
Text(AppLocalizations.of(context)!.deleteProductSubtitle), subtitle: Text(AppLocalizations.of(context)!
.deleteProductSubtitle),
onTap: () { onTap: () {
// show popup // show popup
showDialog( showDialog(
@ -229,9 +296,12 @@ class _ViewProductPageState extends State<ViewProductPage> {
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
title: Text( title: Text(
AppLocalizations.of(context)!.deleteProduct), AppLocalizations.of(context)!
content: Text(AppLocalizations.of(context)! .deleteProduct),
.deleteProductConfirm(product?.name ?? "")), content: Text(
AppLocalizations.of(context)!
.deleteProductConfirm(
product?.name ?? "")),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -239,30 +309,47 @@ class _ViewProductPageState extends State<ViewProductPage> {
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
}, },
child: Text( child: Text(
AppLocalizations.of(context)!.cancel), AppLocalizations.of(
context)!
.cancel),
), ),
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
// send request // send request
final scaffMgr = ScaffoldMessenger.of(ctx); final scaffMgr =
ScaffoldMessenger.of(
ctx);
// popup context // popup context
final navInner = Navigator.of(ctx); final navInner =
Navigator.of(ctx);
// bottomsheet context // bottomsheet context
final nav = Navigator.of(context); final nav =
final user = context.read<User>(); Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(scaffMgr, doNetworkRequest(scaffMgr,
req: () => postWithCreadentials( req: () =>
path: 'deleteProduct', postWithCreadentials(
target: user.server, path:
'deleteProduct',
target:
user.server,
body: { body: {
'room': widget.room, 'room': widget
'server': widget.server, .room,
'listProdID': product?.id ?? "" 'server': widget
.server,
'listProdID':
product?.id ??
""
}, },
credentials: user), credentials:
user),
onOK: (_) async { onOK: (_) async {
// TODO: remove cached product // remove cached product
await product!
.removeDisk();
}, },
after: () { after: () {
// close popup // close popup
@ -271,7 +358,9 @@ class _ViewProductPageState extends State<ViewProductPage> {
nav.pop(); nav.pop();
}); });
}, },
child: Text(AppLocalizations.of(context)! child: Text(
AppLocalizations.of(
context)!
.deleteProduct), .deleteProduct),
) )
], ],
@ -281,7 +370,7 @@ class _ViewProductPageState extends State<ViewProductPage> {
), ),
] ]
: [] : []
])), ]))))),
); );
} }
} }

View file

@ -71,6 +71,6 @@ Future<void> doNetworkRequest(ScaffoldMessengerState? sm,
} }
if (after != null) { if (after != null) {
after(); await after();
} }
} }