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 {
int id;
final int id;
String name;
String description;
// category ID
@ -563,9 +563,14 @@ class RoomProduct {
// parent product ID
int? parent;
final String server;
final String room;
RoomProduct(
{required this.id,
required this.name,
required this.server,
required this.room,
this.description = '',
this.category = -1,
this.defaultUnit = 0,
@ -573,8 +578,10 @@ class RoomProduct {
this.ean,
this.parent});
factory RoomProduct.fromJSON(dynamic json) {
factory RoomProduct.fromJSON(String server, String room, dynamic json) {
return RoomProduct(
server: server,
room: room,
id: json['listProdID'],
name: json['title'],
description: json['description'],
@ -584,6 +591,78 @@ class RoomProduct {
ean: json['ean'],
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 {

View file

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

View file

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

View file

@ -106,12 +106,25 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
}
}
void fetchCategories() {
void fetchCategories() async {
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(
credentials: user,
target: user.server,
@ -140,12 +153,20 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
});
}
void fetchProducts() {
void fetchProducts() async {
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(
credentials: user,
target: user.server,
@ -153,7 +174,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.map<RoomProduct>((raw) => RoomProduct.fromJSON(
widget.room!.serverTag, widget.room!.id, raw))
.toList();
if (mounted) {
@ -168,6 +190,34 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
void 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((_) {
fetchItems();
fetchCategories();

View file

@ -21,10 +21,20 @@ class RoomProductsPage extends StatefulWidget {
class _RoomProductsPageState extends State<RoomProductsPage> {
List<RoomProduct> products = [];
void fetchProducts() {
void fetchProducts() async {
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(
credentials: user,
target: user.server,
@ -32,10 +42,13 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.map<RoomProduct>((raw) => RoomProduct.fromJSON(
widget.room!.serverTag, widget.room!.id, raw))
.toList();
// TODO: cache products
for (RoomProduct prod in resp) {
prod.toDisk();
}
if (mounted) {
setState(() {
@ -49,6 +62,18 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
void 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());
}

View file

@ -36,12 +36,19 @@ class _EditProductPageState extends State<EditProductPage> {
List<RoomCategory> categories = [];
List<RoomProduct> products = [];
void fetchCategories() {
void fetchCategories() async {
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(
credentials: user,
target: user.server,
@ -59,12 +66,19 @@ class _EditProductPageState extends State<EditProductPage> {
});
}
void fetchProducts() {
void fetchProducts() async {
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(
credentials: user,
target: user.server,
@ -72,7 +86,8 @@ class _EditProductPageState extends State<EditProductPage> {
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList();
if (widget.product != null) {
@ -255,7 +270,22 @@ class _EditProductPageState extends State<EditProductPage> {
'ean': _ctrEAN.text,
'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();
});
} else {
@ -277,6 +307,20 @@ class _EditProductPageState extends State<EditProductPage> {
'parent': _ctrParent
}),
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();
});
}

View file

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

View file

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