Added Combined new/edit and view product screen
Similarily to categories and rooms, the edit product screen is reused as a new-room screen, which is especially easy, because the user is unable to select the product id themselves. NOTE: the dynamic value-unit input is still missing some "subunits" The view product screen has links to the edit product page, the view parent page (if available) and a not yet functional view children screen. NOTE: The parent product display should be restricted in width, and the screen is missing value/unit information.
This commit is contained in:
parent
47387bb395
commit
5cd21c8adf
11 changed files with 1101 additions and 185 deletions
|
@ -127,32 +127,32 @@ class RoomIcon {
|
||||||
String get text {
|
String get text {
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case 'love':
|
case 'love':
|
||||||
return 'Friends';
|
return 'Friends';
|
||||||
case 'sports':
|
case 'sports':
|
||||||
return 'Sports';
|
return 'Sports';
|
||||||
case 'pets':
|
case 'pets':
|
||||||
return 'Pets';
|
return 'Pets';
|
||||||
case 'vacation':
|
case 'vacation':
|
||||||
return 'Vacation';
|
return 'Vacation';
|
||||||
case 'gifts':
|
case 'gifts':
|
||||||
return 'Gifts';
|
return 'Gifts';
|
||||||
case 'groceries':
|
case 'groceries':
|
||||||
return 'Groceries';
|
return 'Groceries';
|
||||||
case 'fashion':
|
case 'fashion':
|
||||||
return 'Clothing';
|
return 'Clothing';
|
||||||
case 'art':
|
case 'art':
|
||||||
return 'Arts & Crafts';
|
return 'Arts & Crafts';
|
||||||
case 'tech':
|
case 'tech':
|
||||||
return 'Electronics';
|
return 'Electronics';
|
||||||
case 'home':
|
case 'home':
|
||||||
return 'Home supplies';
|
return 'Home supplies';
|
||||||
case 'family':
|
case 'family':
|
||||||
return 'Family';
|
return 'Family';
|
||||||
case 'social':
|
case 'social':
|
||||||
return 'Social';
|
return 'Social';
|
||||||
case 'other':
|
case 'other':
|
||||||
default:
|
default:
|
||||||
return 'Other';
|
return 'Other';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,44 +161,44 @@ class RoomIcon {
|
||||||
String path = "";
|
String path = "";
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case 'love':
|
case 'love':
|
||||||
path = 'undraw/undraw_couple.svg';
|
path = 'undraw/undraw_couple.svg';
|
||||||
break;
|
break;
|
||||||
case 'sports':
|
case 'sports':
|
||||||
path = 'undraw/undraw_greek_freak.svg';
|
path = 'undraw/undraw_greek_freak.svg';
|
||||||
break;
|
break;
|
||||||
case 'pets':
|
case 'pets':
|
||||||
path = 'undraw/undraw_dog.svg';
|
path = 'undraw/undraw_dog.svg';
|
||||||
break;
|
break;
|
||||||
case 'vacation':
|
case 'vacation':
|
||||||
path = 'undraw/undraw_trip.svg';
|
path = 'undraw/undraw_trip.svg';
|
||||||
break;
|
break;
|
||||||
case 'gifts':
|
case 'gifts':
|
||||||
path = 'undraw/undraw_gifts.svg';
|
path = 'undraw/undraw_gifts.svg';
|
||||||
break;
|
break;
|
||||||
case 'groceries':
|
case 'groceries':
|
||||||
path = 'undraw/undraw_gone_shopping.svg';
|
path = 'undraw/undraw_gone_shopping.svg';
|
||||||
break;
|
break;
|
||||||
case 'fashion':
|
case 'fashion':
|
||||||
path = 'undraw/undraw_jewelry.svg';
|
path = 'undraw/undraw_jewelry.svg';
|
||||||
break;
|
break;
|
||||||
case 'art':
|
case 'art':
|
||||||
path = 'undraw/undraw_sculpting.svg';
|
path = 'undraw/undraw_sculpting.svg';
|
||||||
break;
|
break;
|
||||||
case 'tech':
|
case 'tech':
|
||||||
path = 'undraw/undraw_progressive_app.svg';
|
path = 'undraw/undraw_progressive_app.svg';
|
||||||
break;
|
break;
|
||||||
case 'home':
|
case 'home':
|
||||||
path = 'undraw/undraw_under_construction.svg';
|
path = 'undraw/undraw_under_construction.svg';
|
||||||
break;
|
break;
|
||||||
case 'family':
|
case 'family':
|
||||||
path = 'undraw/undraw_family.svg';
|
path = 'undraw/undraw_family.svg';
|
||||||
break;
|
break;
|
||||||
case 'social':
|
case 'social':
|
||||||
path = 'undraw/undraw_pizza_sharing.svg';
|
path = 'undraw/undraw_pizza_sharing.svg';
|
||||||
break;
|
break;
|
||||||
case 'other':
|
case 'other':
|
||||||
default:
|
default:
|
||||||
path = 'undraw/undraw_file_manager.svg';
|
path = 'undraw/undraw_file_manager.svg';
|
||||||
}
|
}
|
||||||
|
|
||||||
return asset(path);
|
return asset(path);
|
||||||
|
@ -214,7 +214,7 @@ class Room {
|
||||||
RoomVisibility? visibility = RoomVisibility.private;
|
RoomVisibility? visibility = RoomVisibility.private;
|
||||||
|
|
||||||
Room(
|
Room(
|
||||||
{required this.id,
|
{required this.id,
|
||||||
required this.serverTag,
|
required this.serverTag,
|
||||||
this.name = "",
|
this.name = "",
|
||||||
this.description = "",
|
this.description = "",
|
||||||
|
@ -263,12 +263,12 @@ class Room {
|
||||||
|
|
||||||
factory Room.fromMap(Map<String, dynamic> map) {
|
factory Room.fromMap(Map<String, dynamic> map) {
|
||||||
return Room(
|
return Room(
|
||||||
id: map['id'],
|
id: map['id'],
|
||||||
serverTag: map['server'],
|
serverTag: map['server'],
|
||||||
name: map['name'],
|
name: map['name'],
|
||||||
description: map['description'] ?? '',
|
description: map['description'] ?? '',
|
||||||
icon: RoomIcon(type: map['icon'] ?? 'Other'),
|
icon: RoomIcon(type: map['icon'] ?? 'Other'),
|
||||||
visibility: RoomVisibility(map['visibility'] ?? 0));
|
visibility: RoomVisibility(map['visibility'] ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
|
@ -284,12 +284,12 @@ class Room {
|
||||||
|
|
||||||
factory Room.fromJSON(dynamic json) {
|
factory Room.fromJSON(dynamic json) {
|
||||||
return Room(
|
return Room(
|
||||||
id: json['name'],
|
id: json['name'],
|
||||||
serverTag: json['server'],
|
serverTag: json['server'],
|
||||||
name: json['title'],
|
name: json['title'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
icon: RoomIcon(type: json['icon']),
|
icon: RoomIcon(type: json['icon']),
|
||||||
visibility: RoomVisibility(json['visibility']));
|
visibility: RoomVisibility(json['visibility']));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toDisk() async {
|
Future<void> toDisk() async {
|
||||||
|
@ -308,7 +308,7 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Room> fromDisk(
|
static Future<Room> fromDisk(
|
||||||
{required String id, required String serverTag}) async {
|
{required String id, required String serverTag}) async {
|
||||||
final db = Localstore.instance;
|
final db = Localstore.instance;
|
||||||
final raw = await db.collection('rooms').doc('$id@$serverTag').get();
|
final raw = await db.collection('rooms').doc('$id@$serverTag').get();
|
||||||
return Room.fromMap(raw!);
|
return Room.fromMap(raw!);
|
||||||
|
@ -321,13 +321,13 @@ class RoomMember {
|
||||||
final bool isAdmin;
|
final bool isAdmin;
|
||||||
|
|
||||||
const RoomMember(
|
const RoomMember(
|
||||||
{required this.id, required this.serverTag, required this.isAdmin});
|
{required this.id, required this.serverTag, required this.isAdmin});
|
||||||
|
|
||||||
factory RoomMember.fromJSON(dynamic json) {
|
factory RoomMember.fromJSON(dynamic json) {
|
||||||
return RoomMember(
|
return RoomMember(
|
||||||
id: json['name'],
|
id: json['name'],
|
||||||
serverTag: json['server'],
|
serverTag: json['server'],
|
||||||
isAdmin: json['admin'] == 1);
|
isAdmin: json['admin'] == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get humanReadableName {
|
String get humanReadableName {
|
||||||
|
@ -342,40 +342,39 @@ class RoomInfo {
|
||||||
final int permissions;
|
final int permissions;
|
||||||
|
|
||||||
const RoomInfo(
|
const RoomInfo(
|
||||||
{required this.permissions,
|
{required this.permissions,
|
||||||
required this.owner,
|
required this.owner,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
required this.isOwner});
|
required this.isOwner});
|
||||||
|
|
||||||
factory RoomInfo.fromJSON(dynamic json) {
|
factory RoomInfo.fromJSON(dynamic json) {
|
||||||
return RoomInfo(
|
return RoomInfo(
|
||||||
permissions: json['rights'],
|
permissions: json['rights'],
|
||||||
owner: json['owner'],
|
owner: json['owner'],
|
||||||
isAdmin: json['isAdmin'],
|
isAdmin: json['isAdmin'],
|
||||||
isOwner: json['isOwner']);
|
isOwner: json['isOwner']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoomCategory {
|
class RoomCategory {
|
||||||
final int id;
|
final int? id;
|
||||||
final String name;
|
final String name;
|
||||||
final ColorSwatch<int> color;
|
final ColorSwatch<int> color;
|
||||||
|
|
||||||
const RoomCategory(
|
const RoomCategory(
|
||||||
{required this.id, required this.name, required this.color});
|
{required this.id, required this.name, required this.color});
|
||||||
|
|
||||||
factory RoomCategory.fromJSON(dynamic json) {
|
factory RoomCategory.fromJSON(dynamic json) {
|
||||||
return RoomCategory(
|
return RoomCategory(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
name: json['title'],
|
name: json['title'],
|
||||||
color: colorFromString(json['color']));
|
color: colorFromString(json['color']));
|
||||||
}
|
}
|
||||||
factory RoomCategory.other(BuildContext context) {
|
factory RoomCategory.other(BuildContext context) {
|
||||||
return RoomCategory(
|
return RoomCategory(
|
||||||
id: -1,
|
id: null,
|
||||||
name: AppLocalizations.of(context)!.categoryNameOther,
|
name: AppLocalizations.of(context)!.categoryNameOther,
|
||||||
color: Colors.grey
|
color: Colors.grey);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<ColorSwatch<int>> listColors() {
|
static List<ColorSwatch<int>> listColors() {
|
||||||
|
@ -399,31 +398,31 @@ class RoomCategory {
|
||||||
ColorSwatch<int> colorFromString(String text) {
|
ColorSwatch<int> colorFromString(String text) {
|
||||||
switch (text.toLowerCase()) {
|
switch (text.toLowerCase()) {
|
||||||
case 'red-acc':
|
case 'red-acc':
|
||||||
return Colors.redAccent;
|
return Colors.redAccent;
|
||||||
case 'green-acc':
|
case 'green-acc':
|
||||||
return Colors.greenAccent;
|
return Colors.greenAccent;
|
||||||
case 'yellow-acc':
|
case 'yellow-acc':
|
||||||
return Colors.yellowAccent;
|
return Colors.yellowAccent;
|
||||||
case 'blue-acc':
|
case 'blue-acc':
|
||||||
return Colors.blueAccent;
|
return Colors.blueAccent;
|
||||||
case 'aqua-acc':
|
case 'aqua-acc':
|
||||||
return Colors.tealAccent;
|
return Colors.tealAccent;
|
||||||
case 'purple-acc':
|
case 'purple-acc':
|
||||||
return Colors.purpleAccent;
|
return Colors.purpleAccent;
|
||||||
|
|
||||||
case 'red':
|
case 'red':
|
||||||
return Colors.red;
|
return Colors.red;
|
||||||
case 'green':
|
case 'green':
|
||||||
return Colors.green;
|
return Colors.green;
|
||||||
case 'yellow':
|
case 'yellow':
|
||||||
return Colors.yellow;
|
return Colors.yellow;
|
||||||
case 'blue':
|
case 'blue':
|
||||||
return Colors.blue;
|
return Colors.blue;
|
||||||
case 'aqua':
|
case 'aqua':
|
||||||
return Colors.teal;
|
return Colors.teal;
|
||||||
case 'purple':
|
case 'purple':
|
||||||
default:
|
default:
|
||||||
return Colors.purple;
|
return Colors.purple;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -487,7 +486,7 @@ class RoomProduct {
|
||||||
int? parent;
|
int? parent;
|
||||||
|
|
||||||
RoomProduct(
|
RoomProduct(
|
||||||
{required this.id,
|
{required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.description = '',
|
this.description = '',
|
||||||
this.category = -1,
|
this.category = -1,
|
||||||
|
@ -498,14 +497,14 @@ class RoomProduct {
|
||||||
|
|
||||||
factory RoomProduct.fromJSON(dynamic json) {
|
factory RoomProduct.fromJSON(dynamic json) {
|
||||||
return RoomProduct(
|
return RoomProduct(
|
||||||
id: json['listProdID'],
|
id: json['listProdID'],
|
||||||
name: json['title'],
|
name: json['title'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
category: json['category'],
|
category: json['category'],
|
||||||
defaultUnit: json['defUnit'],
|
defaultUnit: json['defUnit'],
|
||||||
defaultValue: json['defValue'],
|
defaultValue: json['defValue'],
|
||||||
ean: json['ean'],
|
ean: json['ean'],
|
||||||
parent: json['parent']);
|
parent: json['parent']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -524,7 +523,7 @@ class RoomItem {
|
||||||
int? link;
|
int? link;
|
||||||
|
|
||||||
RoomItem(
|
RoomItem(
|
||||||
{required this.id,
|
{required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.description = '',
|
this.description = '',
|
||||||
this.state = 0,
|
this.state = 0,
|
||||||
|
@ -535,24 +534,24 @@ class RoomItem {
|
||||||
|
|
||||||
factory RoomItem.fromJSON(dynamic json) {
|
factory RoomItem.fromJSON(dynamic json) {
|
||||||
return RoomItem(
|
return RoomItem(
|
||||||
id: json['listItemID'],
|
id: json['listItemID'],
|
||||||
name: json['title'],
|
name: json['title'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
category: json['listCatID'],
|
category: json['listCatID'],
|
||||||
state: json['state'],
|
state: json['state'],
|
||||||
unit: json['unit'],
|
unit: json['unit'],
|
||||||
value: json['value'],
|
value: json['value'],
|
||||||
link: json['listProdID']);
|
link: json['listProdID']);
|
||||||
}
|
}
|
||||||
|
|
||||||
RoomItem clone() {
|
RoomItem clone() {
|
||||||
return RoomItem(
|
return RoomItem(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
category: category,
|
category: category,
|
||||||
unit: unit,
|
unit: unit,
|
||||||
value: value,
|
value: value,
|
||||||
link: link);
|
link: link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
lib/components/category_chip.dart
Normal file
16
lib/components/category_chip.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:outbag_app/backend/room.dart';
|
||||||
|
|
||||||
|
class CategoryChip extends StatelessWidget {
|
||||||
|
final RoomCategory? category;
|
||||||
|
|
||||||
|
const CategoryChip({super.key, this.category});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ActionChip(
|
||||||
|
avatar: Icon(Icons.square_rounded, color: category?.color ?? RoomCategory.other(context).color),
|
||||||
|
label: Text(category?.name ?? RoomCategory.other(context).name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,19 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:outbag_app/backend/room.dart';
|
import 'package:outbag_app/backend/room.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
class CategoryPicker extends StatelessWidget {
|
class CategoryPicker extends StatelessWidget {
|
||||||
List<RoomCategory> categories = [];
|
final List<RoomCategory> categories;
|
||||||
int? selected;
|
final int? selected;
|
||||||
bool enabled = true;
|
final bool enabled;
|
||||||
Function(int?)? onSelect;
|
final Function(int?)? onSelect;
|
||||||
|
|
||||||
// hint and label may differ depending on the screen
|
// hint and label may differ depending on the screen
|
||||||
String? hint;
|
final String? hint;
|
||||||
String? label;
|
final String? label;
|
||||||
|
|
||||||
CategoryPicker(
|
const CategoryPicker(
|
||||||
{required this.categories,
|
{super.key,
|
||||||
|
required this.categories,
|
||||||
this.selected,
|
this.selected,
|
||||||
this.onSelect,
|
this.onSelect,
|
||||||
this.hint,
|
this.hint,
|
||||||
|
@ -24,34 +24,35 @@ class CategoryPicker extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: DropdownMenu<int?>(
|
child: DropdownButtonFormField<int?>(
|
||||||
initialSelection: selected,
|
hint: hint==null?null:Text(hint!),
|
||||||
enabled: enabled,
|
decoration: InputDecoration(
|
||||||
hintText: hint,
|
label: label==null?null:Text(label!),
|
||||||
label: (label!=null)?Text(label!):null,
|
border: const OutlineInputBorder(),
|
||||||
onSelected: ((id) {
|
prefixIcon: const Icon(Icons.category)
|
||||||
if (onSelect != null) {
|
),
|
||||||
onSelect!(id);
|
value: selected,
|
||||||
}
|
items: [
|
||||||
}),
|
...categories,
|
||||||
dropdownMenuEntries: [
|
RoomCategory.other(context)
|
||||||
// entry for every categry
|
].map((category)=>DropdownMenuItem<int?>(
|
||||||
...categories.map((category) => DropdownMenuEntry(
|
value: category.id,
|
||||||
value: category.id,
|
child: Row(
|
||||||
label: category.name,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
trailingIcon: Icon(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Icons.square_rounded,
|
children: [
|
||||||
color: category.color,
|
Text(category.name),
|
||||||
))),
|
Icon(Icons.square_rounded,
|
||||||
// entry for default ("other") category
|
color:category.color,
|
||||||
DropdownMenuEntry(
|
size: 32)
|
||||||
value: null,
|
]
|
||||||
label: AppLocalizations.of(context)!.categoryNameOther,
|
),
|
||||||
trailingIcon: Icon(
|
)).toList(),
|
||||||
Icons.square_rounded,
|
onChanged: enabled?(cid) {
|
||||||
color: RoomCategory.other(context).color,
|
if (onSelect != null) {
|
||||||
))
|
onSelect!(cid);
|
||||||
],
|
}
|
||||||
|
}:null,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,20 +3,23 @@ import 'package:outbag_app/backend/room.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
class ProductPicker extends StatelessWidget {
|
class ProductPicker extends StatelessWidget {
|
||||||
List<RoomProduct> products = [];
|
final List<RoomProduct> products;
|
||||||
int? selected;
|
final int? selected;
|
||||||
bool enabled = true;
|
final bool enabled;
|
||||||
Function(int?)? onSelect;
|
final Function(int?)? onSelect;
|
||||||
|
|
||||||
// hint and label may differ depending on the screen
|
// hint and label may differ depending on the screen
|
||||||
String? hint;
|
final String? hint;
|
||||||
String? label;
|
final String? label;
|
||||||
|
final String? help;
|
||||||
|
|
||||||
ProductPicker(
|
const ProductPicker(
|
||||||
{required this.products,
|
{super.key,
|
||||||
|
required this.products,
|
||||||
this.selected,
|
this.selected,
|
||||||
this.onSelect,
|
this.onSelect,
|
||||||
this.hint,
|
this.hint,
|
||||||
|
this.help,
|
||||||
this.label,
|
this.label,
|
||||||
this.enabled = true});
|
this.enabled = true});
|
||||||
|
|
||||||
|
@ -24,28 +27,31 @@ class ProductPicker extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: DropdownMenu<int?>(
|
child: DropdownButtonFormField<int?>(
|
||||||
initialSelection: selected,
|
hint: hint == null ? null : Text(hint!),
|
||||||
label: (label!=null)?Text(label!):null,
|
decoration: InputDecoration(
|
||||||
hintText: hint,
|
label: label == null ? null : Text(label!),
|
||||||
enabled: enabled,
|
border: const OutlineInputBorder(),
|
||||||
onSelected: ((id) {
|
prefixIcon: const Icon(Icons.inventory_2),
|
||||||
if (onSelect != null) {
|
helperText: help),
|
||||||
onSelect!(id);
|
value: selected,
|
||||||
}
|
items: [
|
||||||
}),
|
// "no product" entry
|
||||||
dropdownMenuEntries: [
|
DropdownMenuItem<int?>(
|
||||||
// entry for no product
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: null,
|
value: null,
|
||||||
label: AppLocalizations.of(context)!.productNameNone,
|
child: Text(AppLocalizations.of(context)!.productNameNone),
|
||||||
),
|
),
|
||||||
// entry for every product
|
// other products
|
||||||
...products.map((product) => DropdownMenuEntry(
|
...products.map((product) => DropdownMenuItem<int?>(
|
||||||
value: product.id,
|
value: product.id, child: Text(product.name)))
|
||||||
label: product.name,
|
|
||||||
)),
|
|
||||||
],
|
],
|
||||||
|
onChanged: enabled
|
||||||
|
? (pid) {
|
||||||
|
if (onSelect != null) {
|
||||||
|
onSelect!(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
331
lib/components/value_unit_input.dart
Normal file
331
lib/components/value_unit_input.dart
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
enum UnitType { text, amount, mass, volume, length, area }
|
||||||
|
|
||||||
|
class Unit {
|
||||||
|
UnitType type = UnitType.text;
|
||||||
|
|
||||||
|
Unit(this.type);
|
||||||
|
|
||||||
|
factory Unit.fromId(int id) {
|
||||||
|
switch (id) {
|
||||||
|
case 1:
|
||||||
|
return Unit(UnitType.amount);
|
||||||
|
case 2:
|
||||||
|
return Unit(UnitType.mass);
|
||||||
|
case 3:
|
||||||
|
return Unit(UnitType.volume);
|
||||||
|
case 4:
|
||||||
|
return Unit(UnitType.length);
|
||||||
|
case 5:
|
||||||
|
return Unit(UnitType.area);
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
return Unit(UnitType.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String name(BuildContext context) {
|
||||||
|
final trans = AppLocalizations.of(context);
|
||||||
|
|
||||||
|
if (type == UnitType.text) {
|
||||||
|
return trans!.unitText;
|
||||||
|
} else if (type == UnitType.amount) {
|
||||||
|
return trans!.unitAmount;
|
||||||
|
} else if (type == UnitType.mass) {
|
||||||
|
return trans!.unitMass;
|
||||||
|
} else if (type == UnitType.volume) {
|
||||||
|
return trans!.unitVolume;
|
||||||
|
} else if (type == UnitType.length) {
|
||||||
|
return trans!.unitLength;
|
||||||
|
} else if (type == UnitType.area) {
|
||||||
|
return trans!.unitArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trans!.unitText;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get id {
|
||||||
|
if (type == UnitType.text) {
|
||||||
|
return 0;
|
||||||
|
} else if (type == UnitType.amount) {
|
||||||
|
return 1;
|
||||||
|
} else if (type == UnitType.mass) {
|
||||||
|
return 2;
|
||||||
|
} else if (type == UnitType.volume) {
|
||||||
|
return 3;
|
||||||
|
} else if (type == UnitType.length) {
|
||||||
|
return 4;
|
||||||
|
} else if (type == UnitType.area) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Unit> list() {
|
||||||
|
return [
|
||||||
|
Unit.fromId(0),
|
||||||
|
Unit.fromId(1),
|
||||||
|
Unit.fromId(2),
|
||||||
|
Unit.fromId(3),
|
||||||
|
Unit.fromId(4),
|
||||||
|
Unit.fromId(5),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SubUnit> get subUnits {
|
||||||
|
if (type == UnitType.text) {
|
||||||
|
// NOTE: plain text does not need subunits
|
||||||
|
return [];
|
||||||
|
} else if (type == UnitType.amount) {
|
||||||
|
// NOTE: amount does not need subunits
|
||||||
|
return [];
|
||||||
|
} else if (type == UnitType.mass) {
|
||||||
|
// TODO: add subunits (with conversion)
|
||||||
|
return [SubUnit.def('kg'), SubUnit.nth('g', 1 / 1000)];
|
||||||
|
} else if (type == UnitType.volume) {
|
||||||
|
// TODO: add subunits (with conversion)
|
||||||
|
return [SubUnit.def('l')];
|
||||||
|
} else if (type == UnitType.length) {
|
||||||
|
// TODO: add subunits (with conversion)
|
||||||
|
return [SubUnit.def('m')];
|
||||||
|
} else if (type == UnitType.area) {
|
||||||
|
// TODO: add subunits (with conversion)
|
||||||
|
return [SubUnit.def('m²')];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
int? get defaultSubUnit {
|
||||||
|
if (type == UnitType.text) {
|
||||||
|
// NOTE: plain text does not need subunits
|
||||||
|
return null;
|
||||||
|
} else if (type == UnitType.amount) {
|
||||||
|
// NOTE: amount does not need subunits
|
||||||
|
return null;
|
||||||
|
} else if (type == UnitType.mass) {
|
||||||
|
return 0;
|
||||||
|
} else if (type == UnitType.volume) {
|
||||||
|
return 0;
|
||||||
|
} else if (type == UnitType.length) {
|
||||||
|
return 0;
|
||||||
|
} else if (type == UnitType.area) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInputType get recommendedType {
|
||||||
|
if (type == UnitType.text) {
|
||||||
|
return TextInputType.text;
|
||||||
|
} else {
|
||||||
|
return TextInputType.number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubUnit {
|
||||||
|
final String name;
|
||||||
|
// function to convert from default subunit to this subunit
|
||||||
|
final String Function(String value) convertTo;
|
||||||
|
// function to convert from this subunit to default subunit
|
||||||
|
final String Function(String value) convertFrom;
|
||||||
|
|
||||||
|
const SubUnit({
|
||||||
|
required this.name,
|
||||||
|
required this.convertTo,
|
||||||
|
required this.convertFrom,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (runtimeType != other.runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.hashCode == other.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return name.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SubUnit.def(String name) {
|
||||||
|
return SubUnit(name: name, convertTo: (v) => v, convertFrom: (v) => v);
|
||||||
|
}
|
||||||
|
factory SubUnit.nth(String name, double fact) {
|
||||||
|
return SubUnit(
|
||||||
|
name: name,
|
||||||
|
convertTo: (v) {
|
||||||
|
try {
|
||||||
|
final double number = double.parse(v);
|
||||||
|
return (number * fact).toString();
|
||||||
|
} catch (_) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
convertFrom: (v) {
|
||||||
|
try {
|
||||||
|
final double number = double.parse(v);
|
||||||
|
return (number / fact).toString();
|
||||||
|
} catch (_) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DynamicValueUnitInput extends StatefulWidget {
|
||||||
|
final int initialUnit;
|
||||||
|
final String initialValue;
|
||||||
|
final Function(String)? onValueChange;
|
||||||
|
final Function(int)? onUnitChange;
|
||||||
|
|
||||||
|
const DynamicValueUnitInput(
|
||||||
|
{super.key,
|
||||||
|
required this.initialUnit,
|
||||||
|
required this.initialValue,
|
||||||
|
this.onValueChange,
|
||||||
|
this.onUnitChange});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _DynamicValueUnitInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DynamicValueUnitInputState extends State<DynamicValueUnitInput> {
|
||||||
|
Unit unit = Unit.fromId(0);
|
||||||
|
bool enabled = true;
|
||||||
|
TextEditingController controller = TextEditingController();
|
||||||
|
SubUnit? sub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
controller = TextEditingController(text: widget.initialValue);
|
||||||
|
unit = Unit.fromId(widget.initialUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Wrap(children: [
|
||||||
|
// unit type picker
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: DropdownMenu<int>(
|
||||||
|
label: Text(AppLocalizations.of(context)!.selectUnitTypeLabel),
|
||||||
|
enabled: enabled,
|
||||||
|
initialSelection: unit.id,
|
||||||
|
onSelected: (unit) {
|
||||||
|
if (unit == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final u = Unit.fromId(unit);
|
||||||
|
SubUnit? s;
|
||||||
|
if (u.defaultSubUnit != null) {
|
||||||
|
s = u.subUnits[u.defaultSubUnit!];
|
||||||
|
}
|
||||||
|
// NOTE: we could run this here as well,
|
||||||
|
// but at least to me it seems more natural without it
|
||||||
|
// convertSubunit(s);
|
||||||
|
|
||||||
|
if (widget.onUnitChange != null) {
|
||||||
|
widget.onUnitChange!(unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
this.unit = u;
|
||||||
|
sub = s;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
dropdownMenuEntries: Unit.list()
|
||||||
|
.map((unit) => DropdownMenuEntry(
|
||||||
|
value: unit.id, label: unit.name(context)))
|
||||||
|
.toList(),
|
||||||
|
)),
|
||||||
|
|
||||||
|
// (optional) subunit selector
|
||||||
|
...(unit.defaultSubUnit != null)
|
||||||
|
? [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: DropdownButtonFormField<SubUnit>(
|
||||||
|
hint: Text(AppLocalizations.of(context)!.selectUnitLabel),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label:
|
||||||
|
Text(AppLocalizations.of(context)!.selectUnitLabel),
|
||||||
|
border: const OutlineInputBorder()),
|
||||||
|
value: sub,
|
||||||
|
items: unit.subUnits
|
||||||
|
.map((sub) =>
|
||||||
|
DropdownMenuItem(value: sub, child: Text(sub.name)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (sub) {
|
||||||
|
// there is no way to select nothing
|
||||||
|
if (sub == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub == this.sub) {
|
||||||
|
// no new subunit selected
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
convertSubunit(sub);
|
||||||
|
setState(() {
|
||||||
|
this.sub = sub;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
))
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
|
||||||
|
// value input field
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: unit.recommendedType,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context)!.inputUnitValueLabel,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
final String intermediate =
|
||||||
|
(sub != null) ? sub!.convertFrom(value) : value;
|
||||||
|
|
||||||
|
if (widget.onValueChange != null) {
|
||||||
|
widget.onValueChange!(intermediate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void convertSubunit(SubUnit? newest) {
|
||||||
|
final String old2intermediate =
|
||||||
|
(sub != null) ? sub!.convertFrom(value) : value;
|
||||||
|
final String intermediate2new = (newest != null)
|
||||||
|
? newest.convertTo(old2intermediate)
|
||||||
|
: old2intermediate;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
value = intermediate2new;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String get value {
|
||||||
|
return controller.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(String txt) {
|
||||||
|
controller.text = txt;
|
||||||
|
}
|
||||||
|
}
|
|
@ -352,8 +352,46 @@
|
||||||
"productNameNone": "None",
|
"productNameNone": "None",
|
||||||
"selectCategoryLabel": "Category",
|
"selectCategoryLabel": "Category",
|
||||||
"selectCategoryHint": "Select a category",
|
"selectCategoryHint": "Select a category",
|
||||||
|
"selectCategoryHelp": "Categories determine your shopping list order",
|
||||||
"selectLinkedProductLabel": "Linked Product",
|
"selectLinkedProductLabel": "Linked Product",
|
||||||
"selectLinkedProductHint": "Link a product to your item",
|
"selectLinkedProductHint": "Select a linked Product",
|
||||||
|
"selectLinkedProductHelp": "Link a product to your item, to provide more information",
|
||||||
"selectParentProductLabel": "Parent Product",
|
"selectParentProductLabel": "Parent Product",
|
||||||
"selectParentProductHint": "Nest products by choosing a parent product"
|
"selectParentProductHint": "Select a parent Product",
|
||||||
|
"selectParentProductHelp": "Nest products by choosing a parent product",
|
||||||
|
|
||||||
|
"unitText": "Plain text",
|
||||||
|
"unitAmount": "Amount",
|
||||||
|
"unitMass": "Mass",
|
||||||
|
"unitVolume": "Volume",
|
||||||
|
"unitLength": "Length",
|
||||||
|
"unitArea": "Area",
|
||||||
|
|
||||||
|
"inputUnitValueLabel": "Value",
|
||||||
|
"selectUnitTypeLabel": "Unit Type",
|
||||||
|
"selectUnitLabel": "Unit",
|
||||||
|
|
||||||
|
"inputProductNameLabel":"Name",
|
||||||
|
"inputProductNameHint":"Product name",
|
||||||
|
"inputProductNameHelp":"Give the product a name",
|
||||||
|
"inputProductDescriptionLabel":"Description",
|
||||||
|
"inputProductDescriptionHint":"Product Description",
|
||||||
|
"inputProductDescriptionHelp":"Give a brief description of this product",
|
||||||
|
"inputProductEANLabel":"EAN",
|
||||||
|
"inputProductEANHint":"Product EAN",
|
||||||
|
"inputProductEANHelp":"Easily identify products in the shelf by looking at their ean",
|
||||||
|
|
||||||
|
"newItemShort": "Add",
|
||||||
|
"newItemLong": "Add new Shopping list entry",
|
||||||
|
"newProductShort": "New",
|
||||||
|
"newProductLong": "Create a new product",
|
||||||
|
|
||||||
|
"errorProductNameShouldNotBeEmpty": "Product name shouldn't be empty",
|
||||||
|
|
||||||
|
"viewParentProductTitle": "View parent Product",
|
||||||
|
"viewParentProductSubtitle": "This product is the child of a different product, have a look at it",
|
||||||
|
"viewProductChildrenTitle": "View children",
|
||||||
|
"viewProductChildrenSubtitle": "If other products specify this product as their parent, they are listed here",
|
||||||
|
"editProductTitle": "Edit Product",
|
||||||
|
"editProductSubtitle": "Change product metadata"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import 'package:outbag_app/backend/themes.dart';
|
||||||
import 'package:outbag_app/backend/user.dart';
|
import 'package:outbag_app/backend/user.dart';
|
||||||
import 'package:outbag_app/backend/request.dart';
|
import 'package:outbag_app/backend/request.dart';
|
||||||
import 'package:outbag_app/screens/room/categories/edit.dart';
|
import 'package:outbag_app/screens/room/categories/edit.dart';
|
||||||
|
import 'package:outbag_app/screens/room/products/edit.dart';
|
||||||
|
import 'package:outbag_app/screens/room/products/view.dart';
|
||||||
|
|
||||||
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||||
|
|
||||||
|
@ -254,11 +256,34 @@ class _OutbagAppState extends State {
|
||||||
state.params['id'] ?? '')),
|
state.params['id'] ?? '')),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'edit-category',
|
name: 'edit-category',
|
||||||
path: 'edit-category/:cid',
|
path: 'edit-category/:category',
|
||||||
builder: (context, state)=>EditCategoryPage(
|
builder: (context, state)=>EditCategoryPage(
|
||||||
state.params['server'] ?? '',
|
state.params['server'] ?? '',
|
||||||
state.params['id'] ?? '',
|
state.params['id'] ?? '',
|
||||||
id: int.tryParse(state.params['cid'] ?? ''))),
|
id: int.tryParse(state.params['category'] ?? ''))),
|
||||||
|
GoRoute(
|
||||||
|
name: 'new-product',
|
||||||
|
path: 'new-product',
|
||||||
|
builder: (context, state)=>EditProductPage(
|
||||||
|
server: state.params['server'] ?? '',
|
||||||
|
room: state.params['id'] ?? '',)),
|
||||||
|
GoRoute(
|
||||||
|
name: 'view-product',
|
||||||
|
path: 'p/:product',
|
||||||
|
builder: (context, state)=>ViewProductPage(
|
||||||
|
server: state.params['server'] ?? '',
|
||||||
|
room: state.params['id'] ?? '',
|
||||||
|
product: int.tryParse(state.params['product'] ?? '') ?? 0),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
name: 'edit-product',
|
||||||
|
path: 'edit',
|
||||||
|
builder: (context, state)=>EditProductPage(
|
||||||
|
server: state.params['server'] ?? '',
|
||||||
|
room: state.params['id'] ?? '',
|
||||||
|
product: int.tryParse(state.params['product'] ?? ''))),
|
||||||
|
]
|
||||||
|
),
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:outbag_app/backend/permissions.dart';
|
||||||
import 'package:outbag_app/backend/request.dart';
|
import 'package:outbag_app/backend/request.dart';
|
||||||
import 'package:outbag_app/backend/room.dart';
|
import 'package:outbag_app/backend/room.dart';
|
||||||
import 'package:outbag_app/backend/user.dart';
|
import 'package:outbag_app/backend/user.dart';
|
||||||
|
import 'package:outbag_app/components/category_chip.dart';
|
||||||
import 'package:outbag_app/components/category_picker.dart';
|
import 'package:outbag_app/components/category_picker.dart';
|
||||||
import 'package:outbag_app/components/labeled_divider.dart';
|
import 'package:outbag_app/components/labeled_divider.dart';
|
||||||
import 'package:outbag_app/components/product_picker.dart';
|
import 'package:outbag_app/components/product_picker.dart';
|
||||||
|
@ -292,8 +293,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
|
||||||
0))
|
0))
|
||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: Text(AppLocalizations.of(context)!.newCategoryShort),
|
label: Text(AppLocalizations.of(context)!.newItemShort),
|
||||||
tooltip: AppLocalizations.of(context)!.newCategoryLong,
|
tooltip: AppLocalizations.of(context)!.newItemLong,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// show new category popup
|
// show new category popup
|
||||||
context.pushNamed('new-item', params: {
|
context.pushNamed('new-item', params: {
|
||||||
|
@ -351,10 +352,7 @@ class ShoppingListItem extends StatelessWidget {
|
||||||
enabled: !inCart,
|
enabled: !inCart,
|
||||||
title: Text(name),
|
title: Text(name),
|
||||||
subtitle: Text(description),
|
subtitle: Text(description),
|
||||||
trailing: ActionChip(
|
trailing: CategoryChip(category: category,),
|
||||||
avatar: Icon(Icons.square_rounded, color: category.color),
|
|
||||||
label: Text(category.name),
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (onTap != null) {
|
if (onTap != null) {
|
||||||
onTap!();
|
onTap!();
|
||||||
|
@ -398,8 +396,8 @@ class ShoppingListItemInfo extends StatelessWidget {
|
||||||
selected: item.category,
|
selected: item.category,
|
||||||
enabled: false),
|
enabled: false),
|
||||||
ProductPicker(
|
ProductPicker(
|
||||||
label:
|
label:AppLocalizations.of(context)!.selectLinkedProductLabel,
|
||||||
AppLocalizations.of(context)!.selectLinkedProductLabel,
|
help:AppLocalizations.of(context)!.selectLinkedProductHelp,
|
||||||
products: products,
|
products: products,
|
||||||
selected: item.link,
|
selected: item.link,
|
||||||
enabled: false)
|
enabled: false)
|
||||||
|
|
|
@ -85,8 +85,8 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
|
||||||
0))
|
0))
|
||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: Text(AppLocalizations.of(context)!.newCategoryShort),
|
label: Text(AppLocalizations.of(context)!.newProductShort),
|
||||||
tooltip: AppLocalizations.of(context)!.newCategoryLong,
|
tooltip: AppLocalizations.of(context)!.newProductLong,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// show new category popup
|
// show new category popup
|
||||||
context.pushNamed('new-product', params: {
|
context.pushNamed('new-product', params: {
|
||||||
|
|
287
lib/screens/room/products/edit.dart
Normal file
287
lib/screens/room/products/edit.dart
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:outbag_app/backend/request.dart';
|
||||||
|
import 'package:outbag_app/backend/room.dart';
|
||||||
|
import 'package:outbag_app/backend/user.dart';
|
||||||
|
import 'package:outbag_app/components/category_picker.dart';
|
||||||
|
import 'package:outbag_app/components/product_picker.dart';
|
||||||
|
import 'package:outbag_app/components/value_unit_input.dart';
|
||||||
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||||
|
import 'package:outbag_app/tools/snackbar.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class EditProductPage extends StatefulWidget {
|
||||||
|
final int? product;
|
||||||
|
final String server;
|
||||||
|
final String room;
|
||||||
|
const EditProductPage(
|
||||||
|
{required this.server, required this.room, this.product, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _EditProductPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditProductPageState extends State<EditProductPage> {
|
||||||
|
// input controllers
|
||||||
|
final _ctrName = TextEditingController();
|
||||||
|
final _ctrDescription = TextEditingController();
|
||||||
|
final _ctrEAN = TextEditingController();
|
||||||
|
int? _ctrCategory;
|
||||||
|
int _ctrUnit = 0;
|
||||||
|
String _ctrValue = '';
|
||||||
|
int? _ctrParent;
|
||||||
|
|
||||||
|
// data cache
|
||||||
|
List<RoomCategory> categories = [];
|
||||||
|
List<RoomProduct> products = [];
|
||||||
|
|
||||||
|
void fetchCategories() {
|
||||||
|
final user = context.read<User>();
|
||||||
|
|
||||||
|
// TODO: load cached categories first
|
||||||
|
|
||||||
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'getCategories',
|
||||||
|
body: {'room': widget.room, 'server': widget.server}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final resp = body['data']
|
||||||
|
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
categories = resp;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetchProducts() {
|
||||||
|
final user = context.read<User>();
|
||||||
|
|
||||||
|
// TODO: load cached products first
|
||||||
|
|
||||||
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'getProducts',
|
||||||
|
body: {'room': widget.room, 'server': widget.server}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final resp = body['data']
|
||||||
|
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (widget.product != null) {
|
||||||
|
for (RoomProduct prod in resp) {
|
||||||
|
// load product info
|
||||||
|
// for current product
|
||||||
|
if (prod.id == widget.product) {
|
||||||
|
setState(() {
|
||||||
|
_ctrName.text = prod.name;
|
||||||
|
_ctrDescription.text = prod.description;
|
||||||
|
_ctrEAN.text = prod.ean ?? '';
|
||||||
|
_ctrUnit = prod.defaultUnit;
|
||||||
|
_ctrValue = prod.defaultValue;
|
||||||
|
_ctrParent = prod.parent;
|
||||||
|
_ctrCategory = prod.category;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
products = resp;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
fetchCategories();
|
||||||
|
fetchProducts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text((widget.product == null)
|
||||||
|
? AppLocalizations.of(context)!.createProduct
|
||||||
|
: AppLocalizations.of(context)!.editProduct),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _ctrName,
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon: const Icon(Icons.badge),
|
||||||
|
labelText: AppLocalizations.of(context)!
|
||||||
|
.inputProductNameLabel,
|
||||||
|
hintText: AppLocalizations.of(context)!
|
||||||
|
.inputProductNameHint,
|
||||||
|
helperText: AppLocalizations.of(context)!
|
||||||
|
.inputProductNameHelp,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _ctrDescription,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context)!
|
||||||
|
.inputProductDescriptionLabel,
|
||||||
|
hintText: AppLocalizations.of(context)!
|
||||||
|
.inputProductDescriptionHint,
|
||||||
|
helperText: AppLocalizations.of(context)!
|
||||||
|
.inputProductDescriptionHelp,
|
||||||
|
prefixIcon: const Icon(Icons.dns),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _ctrEAN,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context)!
|
||||||
|
.inputProductEANLabel,
|
||||||
|
hintText: AppLocalizations.of(context)!
|
||||||
|
.inputProductEANHint,
|
||||||
|
helperText: AppLocalizations.of(context)!
|
||||||
|
.inputProductEANHelp,
|
||||||
|
prefixIcon: const Icon(Icons.qr_code_rounded),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DynamicValueUnitInput(
|
||||||
|
initialUnit: _ctrUnit,
|
||||||
|
initialValue: _ctrValue,
|
||||||
|
onUnitChange: (unit) {
|
||||||
|
setState(() {
|
||||||
|
_ctrUnit = unit;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onValueChange: (value) {
|
||||||
|
setState(() {
|
||||||
|
_ctrValue = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CategoryPicker(
|
||||||
|
label: AppLocalizations.of(context)!
|
||||||
|
.selectCategoryLabel,
|
||||||
|
hint: AppLocalizations.of(context)!
|
||||||
|
.selectCategoryHint,
|
||||||
|
categories: categories,
|
||||||
|
selected: _ctrCategory,
|
||||||
|
onSelect: (cid) {
|
||||||
|
setState(() {
|
||||||
|
_ctrCategory = cid;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ProductPicker(
|
||||||
|
label: AppLocalizations.of(context)!
|
||||||
|
.selectParentProductLabel,
|
||||||
|
hint: AppLocalizations.of(context)!
|
||||||
|
.selectParentProductHint,
|
||||||
|
products: products,
|
||||||
|
selected: _ctrParent,
|
||||||
|
onSelect: (pid) {
|
||||||
|
setState(() {
|
||||||
|
_ctrParent = pid;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
))))),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () async {
|
||||||
|
final scaffMgr = ScaffoldMessenger.of(context);
|
||||||
|
final router = GoRouter.of(context);
|
||||||
|
final trans = AppLocalizations.of(context);
|
||||||
|
final nav = Navigator.of(context);
|
||||||
|
|
||||||
|
if (_ctrName.text.isEmpty) {
|
||||||
|
showSimpleSnackbar(scaffMgr,
|
||||||
|
text: trans!.errorProductNameShouldNotBeEmpty,
|
||||||
|
action: trans.ok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = context.read<User>();
|
||||||
|
|
||||||
|
if (widget.product == null) {
|
||||||
|
doNetworkRequest(scaffMgr,
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'addProduct',
|
||||||
|
body: {
|
||||||
|
'room': widget.room,
|
||||||
|
'server': widget.server,
|
||||||
|
'title': _ctrName.text,
|
||||||
|
'description': _ctrDescription.text,
|
||||||
|
'listCatID': _ctrCategory,
|
||||||
|
'defUnit': _ctrUnit,
|
||||||
|
'defValue': _ctrValue,
|
||||||
|
'ean': _ctrEAN.text,
|
||||||
|
'parent': _ctrParent
|
||||||
|
}),
|
||||||
|
onOK: (_) async {
|
||||||
|
nav.pop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
doNetworkRequest(scaffMgr,
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'changeProduct',
|
||||||
|
body: {
|
||||||
|
'listProdID': widget.product,
|
||||||
|
'room': widget.room,
|
||||||
|
'server': widget.server,
|
||||||
|
'title': _ctrName.text,
|
||||||
|
'description': _ctrDescription.text,
|
||||||
|
'listCatID': _ctrCategory,
|
||||||
|
'defUnit': _ctrUnit,
|
||||||
|
'defValue': _ctrValue,
|
||||||
|
'ean': _ctrEAN.text,
|
||||||
|
'parent': _ctrParent
|
||||||
|
}),
|
||||||
|
onOK: (_) async {
|
||||||
|
nav.pop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: Text(widget.product != null
|
||||||
|
? AppLocalizations.of(context)!.editProductShort
|
||||||
|
: AppLocalizations.of(context)!.createProductShort),
|
||||||
|
icon: Icon(widget.product != null ? Icons.edit : Icons.add)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
215
lib/screens/room/products/view.dart
Normal file
215
lib/screens/room/products/view.dart
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:outbag_app/backend/permissions.dart';
|
||||||
|
import 'package:outbag_app/backend/request.dart';
|
||||||
|
import 'package:outbag_app/backend/room.dart';
|
||||||
|
import 'package:outbag_app/backend/user.dart';
|
||||||
|
import 'package:outbag_app/components/category_chip.dart';
|
||||||
|
import 'package:outbag_app/components/product_picker.dart';
|
||||||
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class ViewProductPage extends StatefulWidget {
|
||||||
|
final int product;
|
||||||
|
final String server;
|
||||||
|
final String room;
|
||||||
|
const ViewProductPage(
|
||||||
|
{required this.server,
|
||||||
|
required this.room,
|
||||||
|
required this.product,
|
||||||
|
super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ViewProductPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ViewProductPageState extends State<ViewProductPage> {
|
||||||
|
RoomProduct? product;
|
||||||
|
// data cache
|
||||||
|
List<RoomProduct> products = [];
|
||||||
|
Map<int?, RoomCategory> categories = {};
|
||||||
|
|
||||||
|
RoomInfo? info;
|
||||||
|
|
||||||
|
void fetchInfo() async {
|
||||||
|
final sm = ScaffoldMessenger.of(context);
|
||||||
|
final user = context.read<User>();
|
||||||
|
|
||||||
|
doNetworkRequest(
|
||||||
|
sm,
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
path: 'getRoomInfo',
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
body: {'room': widget.room, 'server': widget.server}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final info = RoomInfo.fromJSON(body['data']);
|
||||||
|
setState(() {
|
||||||
|
this.info = info;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetchCategories() {
|
||||||
|
final user = context.read<User>();
|
||||||
|
|
||||||
|
// TODO: load cached categories first
|
||||||
|
|
||||||
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'getCategories',
|
||||||
|
body: {'room': widget.room, 'server': widget.server}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final resp = body['data']
|
||||||
|
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Map<int?, RoomCategory> map = {};
|
||||||
|
|
||||||
|
for (RoomCategory cat in resp) {
|
||||||
|
map[cat.id] = cat;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
categories = map;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetchProducts() {
|
||||||
|
final user = context.read<User>();
|
||||||
|
|
||||||
|
// TODO: load cached products first
|
||||||
|
|
||||||
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'getProducts',
|
||||||
|
body: {'room': widget.room, 'server': widget.server}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final resp = body['data']
|
||||||
|
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (RoomProduct prod in resp) {
|
||||||
|
// load product info
|
||||||
|
// for current product
|
||||||
|
if (prod.id == widget.product) {
|
||||||
|
setState(() {
|
||||||
|
product = prod;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
products = resp;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
fetchCategories();
|
||||||
|
fetchProducts();
|
||||||
|
fetchInfo();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(product?.name ?? ''),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(children: [
|
||||||
|
// display product into
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(product?.name ?? '', style: textTheme.headlineLarge),
|
||||||
|
Text(product?.description ?? '',
|
||||||
|
style: textTheme.titleMedium),
|
||||||
|
Text(product?.ean ?? ''),
|
||||||
|
CategoryChip(category: categories[product?.category]),
|
||||||
|
ProductPicker(
|
||||||
|
label: AppLocalizations.of(context)!
|
||||||
|
.selectParentProductLabel,
|
||||||
|
products: products,
|
||||||
|
selected: product?.parent,
|
||||||
|
enabled: false)
|
||||||
|
],
|
||||||
|
))),
|
||||||
|
|
||||||
|
// show actions (if allowed / available
|
||||||
|
// edit product button
|
||||||
|
...(info != null &&
|
||||||
|
(info!.isAdmin ||
|
||||||
|
info!.isOwner ||
|
||||||
|
(info!.permissions & RoomPermission.editRoomContent != 0)))
|
||||||
|
? [
|
||||||
|
ListTile(
|
||||||
|
title: Text(AppLocalizations.of(context)!.editProductTitle),
|
||||||
|
subtitle:
|
||||||
|
Text(AppLocalizations.of(context)!.editProductSubtitle),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed('edit-product', params: {
|
||||||
|
'server': widget.server,
|
||||||
|
'id': widget.room,
|
||||||
|
'product': widget.product.toString()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
// show parent?
|
||||||
|
...(product?.parent != null)
|
||||||
|
? [
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
AppLocalizations.of(context)!.viewParentProductTitle),
|
||||||
|
subtitle: Text(
|
||||||
|
AppLocalizations.of(context)!.viewParentProductSubtitle),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed('view-product', params: {
|
||||||
|
'server': widget.server,
|
||||||
|
'id': widget.room,
|
||||||
|
'product': product!.parent.toString()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
// show/manage children
|
||||||
|
ListTile(
|
||||||
|
title: Text(AppLocalizations.of(context)!.viewProductChildrenTitle),
|
||||||
|
subtitle:
|
||||||
|
Text(AppLocalizations.of(context)!.viewProductChildrenSubtitle),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed('view-product-children', params: {
|
||||||
|
'server': widget.server,
|
||||||
|
'id': widget.room,
|
||||||
|
'product': widget.product.toString()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue