feat: enhance receipt import functionality with category selection and PDF opening support
This commit is contained in:
@@ -6,11 +6,16 @@ import '../domain/parsed_receipt_item.dart';
|
||||
|
||||
enum ImportDestination { inventory, pantry }
|
||||
|
||||
enum CategorySelectionSource { ai, manual }
|
||||
|
||||
// ── Per-rad redigeringstillstånd ──────────────────────────────────────────────
|
||||
|
||||
class ItemEdit {
|
||||
final int? productId;
|
||||
final String? productName;
|
||||
final int? categoryId;
|
||||
final String? categoryPath;
|
||||
final CategorySelectionSource? categorySource;
|
||||
final double? quantity;
|
||||
final String? unit;
|
||||
final ImportDestination destination;
|
||||
@@ -18,6 +23,9 @@ class ItemEdit {
|
||||
const ItemEdit({
|
||||
this.productId,
|
||||
this.productName,
|
||||
this.categoryId,
|
||||
this.categoryPath,
|
||||
this.categorySource,
|
||||
this.quantity,
|
||||
this.unit,
|
||||
this.destination = ImportDestination.inventory,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/api/api_error_mapper.dart';
|
||||
import '../../../core/api/api_paths.dart';
|
||||
import '../../../core/api/api_providers.dart';
|
||||
import '../../../core/ui/category_then_product_picker.dart';
|
||||
import '../../../core/ui/product_picker_field.dart';
|
||||
import '../../../core/utils/pdf_opener.dart';
|
||||
import '../../../core/utils/global_error_handler.dart';
|
||||
import '../../admin/data/admin_repository.dart';
|
||||
import '../../admin/domain/admin_category_node.dart';
|
||||
@@ -18,11 +18,10 @@ import '../data/import_providers.dart';
|
||||
import '../data/receipt_import_session.dart';
|
||||
import '../domain/parsed_receipt_item.dart';
|
||||
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html' as html show Blob, Url, window;
|
||||
|
||||
typedef _Destination = ImportDestination;
|
||||
|
||||
enum _ProductEntryMode { existing, create }
|
||||
|
||||
// ── Redigeringstillstånd per rad ─────────────────────────────────────────────
|
||||
|
||||
typedef _ItemEdit = ItemEdit;
|
||||
@@ -35,6 +34,7 @@ class _EditDialog extends StatefulWidget {
|
||||
final List<ProductOption> products;
|
||||
final List<AdminCategoryNode> categoryTree;
|
||||
final Future<ProductOption?> Function(String name, int categoryId)? onCreate;
|
||||
final _ProductEntryMode? initialEntryMode;
|
||||
|
||||
const _EditDialog({
|
||||
required this.item,
|
||||
@@ -42,6 +42,7 @@ class _EditDialog extends StatefulWidget {
|
||||
required this.products,
|
||||
required this.categoryTree,
|
||||
this.onCreate,
|
||||
this.initialEntryMode,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -51,9 +52,18 @@ class _EditDialog extends StatefulWidget {
|
||||
class _EditDialogState extends State<_EditDialog> {
|
||||
late final TextEditingController _quantityCtrl;
|
||||
late final TextEditingController _unitCtrl;
|
||||
late final TextEditingController _newProductNameCtrl;
|
||||
int? _productId;
|
||||
String? _productName;
|
||||
int? _productCategoryId;
|
||||
String? _productCategoryPath;
|
||||
CategorySelectionSource? _productCategorySource;
|
||||
int? _newCategoryId;
|
||||
String? _newCategoryPath;
|
||||
CategorySelectionSource? _newCategorySource;
|
||||
_Destination _destination = _Destination.inventory;
|
||||
_ProductEntryMode _entryMode = _ProductEntryMode.existing;
|
||||
bool _isCreatingProduct = false;
|
||||
// Lokal lista — utökas om nya produkter skapas under dialogen
|
||||
late List<ProductOption> _localProducts;
|
||||
|
||||
@@ -63,22 +73,120 @@ class _EditDialogState extends State<_EditDialog> {
|
||||
_productId = widget.current.productId;
|
||||
_productName = widget.current.productName;
|
||||
_destination = widget.current.destination;
|
||||
_entryMode = widget.initialEntryMode ??
|
||||
(_productId == null ? _ProductEntryMode.create : _ProductEntryMode.existing);
|
||||
_localProducts = List.of(widget.products);
|
||||
_productCategoryId = widget.current.categoryId ?? _categoryIdForProduct(_productId);
|
||||
_productCategoryPath = widget.current.categoryPath ?? _categoryPathForCategoryId(_productCategoryId);
|
||||
_productCategorySource = widget.current.categorySource;
|
||||
_newCategoryId = widget.current.categoryId ?? widget.item.categorySuggestionId;
|
||||
_newCategoryPath = widget.current.categoryPath ?? widget.item.categorySuggestionPath;
|
||||
_newCategorySource = widget.current.categorySource;
|
||||
_quantityCtrl = TextEditingController(
|
||||
text: (widget.current.quantity ?? widget.item.quantity)?.toString() ?? '',
|
||||
);
|
||||
_unitCtrl = TextEditingController(
|
||||
text: widget.current.unit ?? widget.item.unit ?? '',
|
||||
);
|
||||
_newProductNameCtrl = TextEditingController(
|
||||
text: widget.current.productName ?? widget.item.rawName,
|
||||
);
|
||||
}
|
||||
|
||||
int? _categoryIdForProduct(int? productId) {
|
||||
if (productId == null) return null;
|
||||
return _localProducts
|
||||
.cast<ProductOption?>()
|
||||
.firstWhere((p) => p?.id == productId, orElse: () => null)
|
||||
?.categoryId;
|
||||
}
|
||||
|
||||
String? _categoryPathForCategoryId(int? categoryId) {
|
||||
if (categoryId == null) return null;
|
||||
|
||||
List<String>? walk(List<AdminCategoryNode> nodes, List<String> parents) {
|
||||
for (final node in nodes) {
|
||||
final path = [...parents, node.name];
|
||||
if (node.id == categoryId) return path;
|
||||
final found = walk(node.children, path);
|
||||
if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return walk(widget.categoryTree, const [])?.join(' > ');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_quantityCtrl.dispose();
|
||||
_unitCtrl.dispose();
|
||||
_newProductNameCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openCreateCategoryPicker() async {
|
||||
final selected = await CategoryThenProductPicker.showCategorySheet(
|
||||
context,
|
||||
categoryTree: widget.categoryTree,
|
||||
preselectedCategoryId: _newCategoryId ?? widget.item.categorySuggestionId,
|
||||
);
|
||||
if (selected == null || !mounted) return;
|
||||
setState(() {
|
||||
_newCategoryId = selected.id;
|
||||
_newCategoryPath = selected.path;
|
||||
_newCategorySource = CategorySelectionSource.manual;
|
||||
});
|
||||
}
|
||||
|
||||
bool get _canConfirm {
|
||||
if (_entryMode == _ProductEntryMode.create) {
|
||||
return !_isCreatingProduct &&
|
||||
_newProductNameCtrl.text.trim().isNotEmpty &&
|
||||
_newCategoryId != null;
|
||||
}
|
||||
return _productId != null;
|
||||
}
|
||||
|
||||
Future<void> _confirm() async {
|
||||
if (_entryMode == _ProductEntryMode.create) {
|
||||
if (widget.onCreate == null || _newCategoryId == null) return;
|
||||
setState(() => _isCreatingProduct = true);
|
||||
try {
|
||||
final newProduct = await widget.onCreate!(
|
||||
_newProductNameCtrl.text.trim(),
|
||||
_newCategoryId!,
|
||||
);
|
||||
if (newProduct == null || !mounted) return;
|
||||
if (!_localProducts.any((p) => p.id == newProduct.id)) {
|
||||
_localProducts = [..._localProducts, newProduct];
|
||||
}
|
||||
_productId = newProduct.id;
|
||||
_productName = newProduct.name;
|
||||
_productCategoryId = _newCategoryId;
|
||||
_productCategoryPath = _newCategoryPath;
|
||||
_productCategorySource = _newCategorySource ?? CategorySelectionSource.manual;
|
||||
} finally {
|
||||
if (mounted) setState(() => _isCreatingProduct = false);
|
||||
}
|
||||
if (!mounted || _productId == null) return;
|
||||
}
|
||||
|
||||
Navigator.pop(
|
||||
context,
|
||||
_ItemEdit(
|
||||
productId: _productId,
|
||||
productName: _productName,
|
||||
categoryId: _productCategoryId,
|
||||
categoryPath: _productCategoryPath,
|
||||
categorySource: _productCategorySource,
|
||||
quantity: double.tryParse(_quantityCtrl.text) ?? widget.item.quantity,
|
||||
unit: _unitCtrl.text.trim().isEmpty ? widget.item.unit : _unitCtrl.text.trim(),
|
||||
destination: _destination,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -118,6 +226,9 @@ class _EditDialogState extends State<_EditDialog> {
|
||||
.cast<ProductOption?>()
|
||||
.firstWhere((p) => p?.id == id, orElse: () => null)
|
||||
?.name;
|
||||
_productCategoryId = _categoryIdForProduct(id);
|
||||
_productCategoryPath = _categoryPathForCategoryId(_productCategoryId);
|
||||
_productCategorySource = CategorySelectionSource.manual;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -130,6 +241,12 @@ class _EditDialogState extends State<_EditDialog> {
|
||||
setState(() {
|
||||
_productId = sugId;
|
||||
_productName = item.suggestedProductName;
|
||||
_productCategoryId = _categoryIdForProduct(sugId) ?? item.categorySuggestionId;
|
||||
_productCategoryPath =
|
||||
_categoryPathForCategoryId(_productCategoryId) ?? item.categorySuggestionPath;
|
||||
_productCategorySource = item.categorySuggestionPath != null
|
||||
? CategorySelectionSource.ai
|
||||
: null;
|
||||
});
|
||||
} else {
|
||||
// Öppna kategori → produkt med AI-föreslagen kategori förvald
|
||||
@@ -182,41 +299,144 @@ class _EditDialogState extends State<_EditDialog> {
|
||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Produktval: sök direkt eller välj via kategoriträd
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ProductPickerField(
|
||||
products: _localProducts,
|
||||
value: _productId,
|
||||
label: 'Produkt',
|
||||
onChanged: (id) {
|
||||
setState(() {
|
||||
_productId = id;
|
||||
_productName = id == null
|
||||
? null
|
||||
: _localProducts
|
||||
.cast<ProductOption?>()
|
||||
.firstWhere((p) => p?.id == id, orElse: () => null)
|
||||
?.name;
|
||||
});
|
||||
},
|
||||
// Produktval: befintlig produkt eller skapa ny från importnamnet
|
||||
SegmentedButton<_ProductEntryMode>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: _ProductEntryMode.existing,
|
||||
icon: Icon(Icons.search, size: 16),
|
||||
label: Text('Befintlig'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _ProductEntryMode.create,
|
||||
icon: Icon(Icons.add_box_outlined, size: 16),
|
||||
label: Text('Ny produkt'),
|
||||
),
|
||||
],
|
||||
selected: {_entryMode},
|
||||
onSelectionChanged: (s) => setState(() => _entryMode = s.first),
|
||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_entryMode == _ProductEntryMode.existing)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ProductPickerField(
|
||||
products: _localProducts,
|
||||
value: _productId,
|
||||
label: 'Produkt',
|
||||
initialQuery: item.rawName,
|
||||
onChanged: (id) {
|
||||
setState(() {
|
||||
_productId = id;
|
||||
_productName = id == null
|
||||
? null
|
||||
: _localProducts
|
||||
.cast<ProductOption?>()
|
||||
.firstWhere((p) => p?.id == id, orElse: () => null)
|
||||
?.name;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: 'Välj via kategori',
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(44, 56),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () => openCategoryPicker(),
|
||||
child: const Icon(Icons.account_tree_outlined, size: 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
TextField(
|
||||
controller: _newProductNameCtrl,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Produktnamn',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _openCreateCategoryPicker,
|
||||
icon: const Icon(Icons.account_tree_outlined),
|
||||
label: Text(
|
||||
_newCategoryPath == null
|
||||
? 'Välj kategori'
|
||||
: 'Kategori: $_newCategoryPath',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: 'Välj via kategori',
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(44, 56),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () => openCategoryPicker(),
|
||||
child: const Icon(Icons.account_tree_outlined, size: 20),
|
||||
),
|
||||
if (_newCategoryPath != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Vald kategori',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_newCategoryPath!,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (item.categorySuggestionPath != null &&
|
||||
item.categorySuggestionPath!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
children: [
|
||||
ActionChip(
|
||||
avatar: Icon(
|
||||
Icons.auto_awesome,
|
||||
size: 14,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
label: Text(
|
||||
'AI-förslag: ${item.categorySuggestionPath}',
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
backgroundColor: Colors.green.shade50,
|
||||
side: BorderSide(color: Colors.green.shade300),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: item.categorySuggestionId == null
|
||||
? null
|
||||
: () => setState(() {
|
||||
_newCategoryId = item.categorySuggestionId;
|
||||
_newCategoryPath = item.categorySuggestionPath;
|
||||
_newCategorySource = CategorySelectionSource.ai;
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
if (_destination == _Destination.inventory) ...[
|
||||
Row(children: [
|
||||
@@ -247,19 +467,14 @@ class _EditDialogState extends State<_EditDialog> {
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Avbryt')),
|
||||
FilledButton(
|
||||
onPressed: _productId == null ? null : () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
_ItemEdit(
|
||||
productId: _productId,
|
||||
productName: _productName,
|
||||
quantity: double.tryParse(_quantityCtrl.text) ?? widget.item.quantity,
|
||||
unit: _unitCtrl.text.trim().isEmpty ? widget.item.unit : _unitCtrl.text.trim(),
|
||||
destination: _destination,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('OK'),
|
||||
onPressed: _canConfirm ? _confirm : null,
|
||||
child: _isCreatingProduct
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(_entryMode == _ProductEntryMode.create ? 'Skapa och välj' : 'OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -288,7 +503,6 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
|
||||
// Produktlistan för pickern
|
||||
List<ProductOption> _products = [];
|
||||
bool _loadingProducts = false;
|
||||
|
||||
// Kategoriträdet för tvåstegs-picker
|
||||
List<AdminCategoryNode> _categoryTree = [];
|
||||
@@ -306,7 +520,6 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
}
|
||||
|
||||
Future<void> _loadProducts() async {
|
||||
setState(() => _loadingProducts = true);
|
||||
try {
|
||||
final token = await ref.read(authStateProvider.future);
|
||||
final api = ref.read(apiClientProvider);
|
||||
@@ -335,12 +548,9 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
.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;
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _loadingProducts = false);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _loadInventory() async {
|
||||
@@ -409,6 +619,8 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
notifier.setEdit(i, _ItemEdit(
|
||||
productId: pid,
|
||||
productName: name,
|
||||
categoryId: it.categorySuggestionId,
|
||||
categoryPath: it.categorySuggestionPath,
|
||||
quantity: it.quantity,
|
||||
unit: it.unit,
|
||||
));
|
||||
@@ -423,12 +635,17 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openEditDialog(int index) async {
|
||||
Future<void> _openEditDialog(
|
||||
int index, {
|
||||
_ProductEntryMode? initialEntryMode,
|
||||
}) async {
|
||||
final item = _items![index];
|
||||
final current = _edits[index] ??
|
||||
_ItemEdit(
|
||||
productId: item.matchedProductId ?? item.suggestedProductId,
|
||||
productName: item.matchedProductName ?? item.suggestedProductName,
|
||||
categoryId: item.categorySuggestionId,
|
||||
categoryPath: item.categorySuggestionPath,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
);
|
||||
@@ -440,6 +657,7 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
current: current,
|
||||
products: _products,
|
||||
categoryTree: _categoryTree,
|
||||
initialEntryMode: initialEntryMode,
|
||||
onCreate: (name, categoryId) async {
|
||||
try {
|
||||
final token = await ref.read(authStateProvider.future);
|
||||
@@ -582,10 +800,14 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
icon: const Icon(Icons.open_in_new, size: 16),
|
||||
label: const Text('Öppna PDF'),
|
||||
style: OutlinedButton.styleFrom(visualDensity: VisualDensity.compact),
|
||||
onPressed: () {
|
||||
final blob = html.Blob([bytes], 'application/pdf');
|
||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||
html.window.open(url, '_blank');
|
||||
onPressed: () async {
|
||||
final opened = await openPdfBytes(bytes);
|
||||
if (!context.mounted || opened) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('PDF kan bara öppnas direkt i webbversionen just nu.'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -722,16 +944,88 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
if (hasProduct)
|
||||
Text(edit!.productName ?? '', style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: isMatched ? Colors.green.shade700 : theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
))
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text(edit!.productName ?? '', style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: isMatched ? Colors.green.shade700 : theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
)),
|
||||
if (edit.categorySource != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: edit.categorySource == CategorySelectionSource.ai
|
||||
? Colors.green.shade50
|
||||
: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(
|
||||
color: edit.categorySource == CategorySelectionSource.ai
|
||||
? Colors.green.shade300
|
||||
: theme.colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
edit.categorySource == CategorySelectionSource.ai ? 'AI' : 'Manuell',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: edit.categorySource == CategorySelectionSource.ai
|
||||
? Colors.green.shade800
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (isSuggested)
|
||||
Text('Förslag: ${item.suggestedProductName ?? ''}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: Colors.orange.shade700))
|
||||
else
|
||||
Text('Ingen match — tryck för att välja produkt',
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.error)),
|
||||
Text('Ingen matchning ännu — tryck för att välja eller skapa produkt',
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.tertiary)),
|
||||
if (hasProduct && edit?.categoryPath != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
edit!.categoryPath!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!hasProduct && !isSuggested) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _openEditDialog(
|
||||
i,
|
||||
initialEntryMode: _ProductEntryMode.existing,
|
||||
),
|
||||
icon: const Icon(Icons.search, size: 16),
|
||||
label: const Text('Välj befintlig'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _openEditDialog(
|
||||
i,
|
||||
initialEntryMode: _ProductEntryMode.create,
|
||||
),
|
||||
icon: const Icon(Icons.add_box_outlined, size: 16),
|
||||
label: const Text('Ny produkt'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (existingInv != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Row(children: [
|
||||
@@ -758,7 +1052,7 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
hasProduct ? Icons.check_circle : (isSuggested ? Icons.help_outline : Icons.error_outline),
|
||||
color: hasProduct
|
||||
? Colors.green
|
||||
: (isSuggested ? Colors.orange : theme.colorScheme.error),
|
||||
: (isSuggested ? Colors.orange : theme.colorScheme.tertiary),
|
||||
size: 20,
|
||||
),
|
||||
onTap: () => _openEditDialog(i),
|
||||
|
||||
Reference in New Issue
Block a user