feat: add isPrivate field to Product model and implement private product creation and retrieval

This commit is contained in:
Nils-Johan Gynther
2026-05-01 02:29:38 +02:00
parent 1fd910b561
commit 9ee061d5f3
10 changed files with 215 additions and 18 deletions
@@ -142,6 +142,23 @@ class AdminRepository {
.toList();
}
/// Skapar en ny aktiv produkt (kräver admin). Returnerar `{id, name, categoryId?}`.
Future<Map<String, dynamic>> createProduct(String name, {int? categoryId}) async {
final token = await _token();
final data = await guardedApiCall(
_ref,
() => _apiClient.postJson(
ProductApiPaths.list,
body: {
'name': name.trim(),
if (categoryId != null) 'categoryId': categoryId,
},
token: token,
),
);
return data as Map<String, dynamic>;
}
Future<List<AdminProduct>> listDeletedProducts() async {
final token = await _token();
final data = await guardedApiCall(
@@ -44,12 +44,14 @@ class _EditDialog extends StatefulWidget {
final _ItemEdit current;
final List<ProductOption> products;
final List<AdminCategoryNode> categoryTree;
final Future<ProductOption?> Function(String name, int categoryId)? onCreate;
const _EditDialog({
required this.item,
required this.current,
required this.products,
required this.categoryTree,
this.onCreate,
});
@override
@@ -62,6 +64,8 @@ class _EditDialogState extends State<_EditDialog> {
int? _productId;
String? _productName;
_Destination _destination = _Destination.inventory;
// Lokal lista — utökas om nya produkter skapas under dialogen
late List<ProductOption> _localProducts;
@override
void initState() {
@@ -69,6 +73,7 @@ class _EditDialogState extends State<_EditDialog> {
_productId = widget.current.productId;
_productName = widget.current.productName;
_destination = widget.current.destination;
_localProducts = List.of(widget.products);
_quantityCtrl = TextEditingController(
text: (widget.current.quantity ?? widget.item.quantity)?.toString() ?? '',
);
@@ -95,17 +100,30 @@ class _EditDialogState extends State<_EditDialog> {
// Hjälpfunktion: välj produkt via tvåstegs-picker (kategori → produkt)
Future<void> openCategoryPicker({int? preselectedCategoryId}) async {
// onCreate-wrapper: lägg även till den nya produkten i _localProducts
Future<ProductOption?> Function(String, int)? onCreateWrapped;
if (widget.onCreate != null) {
onCreateWrapped = (name, categoryId) async {
final newProduct = await widget.onCreate!(name, categoryId);
if (newProduct != null && mounted) {
setState(() => _localProducts = [..._localProducts, newProduct]);
}
return newProduct;
};
}
final id = await CategoryThenProductPicker.show(
context,
categoryTree: widget.categoryTree,
products: widget.products,
products: _localProducts,
currentProductId: _productId,
preselectedCategoryId: preselectedCategoryId,
onCreate: onCreateWrapped,
);
if (id != null && mounted) {
setState(() {
_productId = id;
_productName = widget.products
_productName = _localProducts
.cast<ProductOption?>()
.firstWhere((p) => p?.id == id, orElse: () => null)
?.name;
@@ -178,7 +196,7 @@ class _EditDialogState extends State<_EditDialog> {
children: [
Expanded(
child: ProductPickerField(
products: widget.products,
products: _localProducts,
value: _productId,
label: 'Produkt',
onChanged: (id) {
@@ -186,7 +204,7 @@ class _EditDialogState extends State<_EditDialog> {
_productId = id;
_productName = id == null
? null
: widget.products
: _localProducts
.cast<ProductOption?>()
.firstWhere((p) => p?.id == id, orElse: () => null)
?.name;
@@ -303,17 +321,28 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
final adminRepo = ref.read(adminRepositoryProvider);
final results = await Future.wait([
api.getJson(ProductApiPaths.list, token: token),
api.getJson(ProductApiPaths.mine, token: token),
adminRepo.listCategoryTree(),
]);
final data = results[0];
final list = data is List ? data : ((data as Map<String, dynamic>?)?['items'] as List? ?? []);
final globalData = results[0];
final mineData = results[1];
final globalList = globalData is List
? globalData
: ((globalData as Map<String, dynamic>?)?['items'] as List? ?? []);
final mineList = mineData is List
? mineData
: ((mineData as Map<String, dynamic>?)?['items'] as List? ?? []);
if (mounted) {
setState(() {
_products = list
.cast<Map<String, dynamic>>()
.map((e) => (id: e['id'] as int, name: e['name'] as String, categoryId: (e['categoryId'] as num?)?.toInt()))
.toList();
_categoryTree = results[1] as List<AdminCategoryNode>;
_products = [
...globalList
.cast<Map<String, dynamic>>()
.map((e) => (id: e['id'] as int, name: (e['canonicalName'] ?? e['name']) as String, categoryId: (e['categoryId'] as num?)?.toInt())),
...mineList
.cast<Map<String, dynamic>>()
.map((e) => (id: e['id'] as int, name: (e['canonicalName'] ?? e['name']) as String, categoryId: (e['categoryId'] as num?)?.toInt())),
];
_categoryTree = results[2] as List<AdminCategoryNode>;
_loadingProducts = false;
});
}
@@ -413,7 +442,35 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
final result = await showDialog<_ItemEdit>(
context: context,
builder: (_) => _EditDialog(item: item, current: current, products: _products, categoryTree: _categoryTree),
builder: (_) => _EditDialog(
item: item,
current: current,
products: _products,
categoryTree: _categoryTree,
onCreate: (name, categoryId) async {
try {
final token = await ref.read(authStateProvider.future);
final api = ref.read(apiClientProvider);
final data = await api.postJson(
ProductApiPaths.createPrivate,
body: {
'name': name.trim(),
'categoryId': categoryId,
},
token: token,
) as Map<String, dynamic>;
final newProduct = (
id: data['id'] as int,
name: (data['canonicalName'] ?? data['name']) as String,
categoryId: categoryId,
);
if (mounted) setState(() => _products = [..._products, newProduct]);
return newProduct;
} catch (_) {
return null;
}
},
),
);
if (result != null && mounted) {
setState(() {