From bd78b1de8195a9a5826746a10a5623eb4d121e80 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Fri, 8 May 2026 16:56:03 +0200 Subject: [PATCH] feat: add "See receipt" button and preview modal in receipt import flow --- .../IMPLEMENTATION_PLAN_RECEIPT_PREVIEW.md | 220 ++++++++++++++++++ .../presentation/receipt_import_tab.dart | 130 ++++++++++- flutter/next_steps_flutter.md | 22 +- 3 files changed, 361 insertions(+), 11 deletions(-) create mode 100644 flutter/IMPLEMENTATION_PLAN_RECEIPT_PREVIEW.md diff --git a/flutter/IMPLEMENTATION_PLAN_RECEIPT_PREVIEW.md b/flutter/IMPLEMENTATION_PLAN_RECEIPT_PREVIEW.md new file mode 100644 index 00000000..78da4486 --- /dev/null +++ b/flutter/IMPLEMENTATION_PLAN_RECEIPT_PREVIEW.md @@ -0,0 +1,220 @@ +# Implementeringsplan: "Se kvitto"-Modal för Kvittoimporten + +**Mål**: MVP-vägen för split-view UX – lägg till modal som visar OCR-text från parsade kvittoraderna. +**Scope**: 2-3 timmar +**Status**: Planering + +--- + +## 1. Ändringar i `receipt_import_tab.dart` + +### 1.1 Lägg till knapp "Se kvitto" i header-raden (rad ~745-752) + +**Plats**: Höger om "Välj alla/Avmarkera alla"-knappen + +```dart +// Innan: Row med bara "Välj alla"-knapp +Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${items.length} rader — tryck för att redigera', style: theme.textTheme.titleSmall), + TextButton(...), // "Välj alla/Avmarkera alla" + ], +) + +// Efter: Lägg till "Se kvitto"-knapp +Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${items.length} rader — tryck för att redigera', style: theme.textTheme.titleSmall), + Row( + children: [ + TextButton.icon( + onPressed: items.isEmpty ? null : () => _showReceiptPreview(context, items), + icon: const Icon(Icons.description_outlined), + label: const Text('Se kvitto'), + ), + const SizedBox(width: 8), + TextButton( + onPressed: () => setState(...), // Befintlig "Välj alla" + child: Text(...), + ), + ], + ), + ], +) +``` + +### 1.2 Implementera `_showReceiptPreview`-metod + +Lägg till denna metod i `_ReceiptImportTabState`: + +```dart +Future _showReceiptPreview(BuildContext context, List items) async { + if (!context.mounted) return; + await showDialog( + context: context, + builder: (ctx) => _ReceiptPreviewDialog(items: items), + ); +} +``` + +--- + +## 2. Ny widget: `_ReceiptPreviewDialog` + +Lägg till denna widget **i samma fil** (`receipt_import_tab.dart`), efter `_ReceiptImportResultRow`-klassen: + +```dart +class _ReceiptPreviewDialog extends StatelessWidget { + final List items; + + const _ReceiptPreviewDialog({required this.items}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AlertDialog( + title: const Text('Kvittotexten i sin helhet'), + content: SizedBox( + width: 600, // Responsiv bredd på desktop + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Här visas all OCR-parsad text från kvittot. En rad per artikel:', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerLowest, + border: Border.all(color: theme.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(12), + child: SelectableText.rich( + TextSpan( + children: items.isEmpty + ? [TextSpan(text: '(Inga rader)', style: theme.textTheme.bodySmall)] + : items + .asMap() + .entries + .map((entry) { + final item = entry.value; + final lineNumber = entry.key + 1; + final lineText = _formatReceiptLine(item); + return TextSpan( + children: [ + TextSpan( + text: '$lineNumber. ', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.outlineVariant, + ), + ), + TextSpan( + text: lineText, + style: theme.textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + ), + ), + const TextSpan(text: '\n'), + ], + ); + }) + .toList(), + ), + style: theme.textTheme.bodySmall, + ), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Stäng'), + ), + ], + ); + } + + String _formatReceiptLine(ParsedReceiptItem item) { + final parts = []; + + if (item.quantity != null) { + parts.add('${item.quantity}'); + } + + if (item.unit != null) { + parts.add(item.unit!); + } + + parts.add(item.rawName); + + if (item.price != null) { + parts.add('— ${item.price} kr'); + } + + return parts.join(' '); + } +} +``` + +--- + +## 3. Implementeringssteg (steg-för-steg) + +1. **Läs receipt_import_tab.dart** och identifiera raden med "Välj alla/Avmarkera alla"-knappen +2. **Refaktorera Row**: Lägg "Se kvitto"-knapp bredvid befintliga knapp +3. **Lägg till `_showReceiptPreview()`-metod** i `_ReceiptImportTabState` +4. **Implementera `_ReceiptPreviewDialog`-widget** på slutet av filen +5. **Testa**: + - Ladda ett kvitto + - Klicka "Se kvitto"-knappen + - Verifiera att texten är lesbar och formaterad + - Testa responsive bredd (dialog behöver minska på mobil) + +--- + +## 4. Responsiv förbättring (optional) + +Om dialogen behöver anpassas för mobil: + +```dart +// I _ReceiptPreviewDialog.build(): +final isWide = MediaQuery.of(context).size.width > 600; + +return Dialog( + insetPadding: const EdgeInsets.all(16), + child: SizedBox( + width: isWide ? 600 : double.maxFinite, // Full bredd på mobil + // ... + ), +); +``` + +--- + +## 5. Långsiktiga förbättringar (Phase 2) + +Se `next_steps_flutter.md` för split-view roadmap: +- Horisontell split-view på desktop +- Scroll-synkronisering +- Tab-fallback på mobil +- AI-guiding labels + +--- + +## Ärendemal + +**Titel**: "Se kvitto"-modal för kvittoimporten +**Branch**: `feat/receipt-preview-modal` +**Labels**: `enhancement`, `import-ux`, `phase-1-mvp` +**Estimate**: 2-3h diff --git a/flutter/lib/features/import/presentation/receipt_import_tab.dart b/flutter/lib/features/import/presentation/receipt_import_tab.dart index 64aa2756..78df31a4 100644 --- a/flutter/lib/features/import/presentation/receipt_import_tab.dart +++ b/flutter/lib/features/import/presentation/receipt_import_tab.dart @@ -449,6 +449,14 @@ class _ReceiptImportTabState extends ConsumerState { } } + Future _showReceiptPreview(BuildContext context, List items) async { + if (!context.mounted) return; + await showDialog( + context: context, + builder: (ctx) => _ReceiptPreviewDialog(items: items), + ); + } + Future _addSelected() async { final items = _items; if (items == null) return; @@ -742,12 +750,22 @@ class _ReceiptImportTabState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('${items.length} rader — tryck för att redigera', style: theme.textTheme.titleSmall), - TextButton( - onPressed: () => setState(() { - final notifier = ref.read(receiptImportSessionProvider.notifier); - notifier.setSelectedForAll(items.length, _selectedCount < items.length); - }), - child: Text(_selectedCount < items.length ? 'Välj alla' : 'Avmarkera alla'), + Row( + children: [ + TextButton.icon( + onPressed: items.isEmpty ? null : () => _showReceiptPreview(context, items), + icon: const Icon(Icons.description_outlined), + label: const Text('Se kvitto'), + ), + const SizedBox(width: 8), + TextButton( + onPressed: () => setState(() { + final notifier = ref.read(receiptImportSessionProvider.notifier); + notifier.setSelectedForAll(items.length, _selectedCount < items.length); + }), + child: Text(_selectedCount < items.length ? 'Välj alla' : 'Avmarkera alla'), + ), + ], ), ], ), @@ -1031,3 +1049,103 @@ class _ReceiptImportResultRow extends ConsumerWidget { } } +class _ReceiptPreviewDialog extends StatelessWidget { + final List items; + + const _ReceiptPreviewDialog({required this.items}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AlertDialog( + title: const Text('Kvittotexten i sin helhet'), + content: SizedBox( + width: 600, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Här visas all OCR-parsad text från kvittot. En rad per artikel:', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerLowest, + border: Border.all(color: theme.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(12), + child: SelectableText.rich( + TextSpan( + children: items.isEmpty + ? [TextSpan(text: '(Inga rader)', style: theme.textTheme.bodySmall)] + : items + .asMap() + .entries + .map((entry) { + final item = entry.value; + final lineNumber = entry.key + 1; + final lineText = _formatReceiptLine(item); + return TextSpan( + children: [ + TextSpan( + text: '$lineNumber. ', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.outlineVariant, + ), + ), + TextSpan( + text: lineText, + style: theme.textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + ), + ), + const TextSpan(text: '\n'), + ], + ); + }) + .toList(), + ), + style: theme.textTheme.bodySmall, + ), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Stäng'), + ), + ], + ); + } + + String _formatReceiptLine(ParsedReceiptItem item) { + final parts = []; + + if (item.quantity != null) { + parts.add('${item.quantity}'); + } + + if (item.unit != null) { + parts.add(item.unit!); + } + + parts.add(item.rawName); + + if (item.price != null) { + parts.add('— ${item.price} kr'); + } + + return parts.join(' '); + } +} + diff --git a/flutter/next_steps_flutter.md b/flutter/next_steps_flutter.md index 92d5f89c..c1691547 100644 --- a/flutter/next_steps_flutter.md +++ b/flutter/next_steps_flutter.md @@ -22,11 +22,23 @@ All historik och implementationdetaljer finns i `teknisk_beskrivning_flutter.md` ## Prioriterade nasta steg -1. Verifiera bildimport och felhantering end-to-end i testmiljo. -2. Implementera alias-inlarning vid manuell korrigering i importflodet. -3. Forbattra UI/UX i granskningsfloden for kvittoimport. -4. Fortsatt migrering av kvarvarande adminfloden. -5. Lokalisera kvarvarande delar i import- och inventarievyer. +1. **Kvitto-import UX förbättring (Split-view långsiktigt)** + - MVP (kort sikt): Lägg till "Se kvitto"-modal som visar full OCR-text från parsade rader + - Knapp i radlist-header, öppnar dialog med ScrollableText + - Enkelt UI, höga UX-vinster + - Implementering: ~2h + - Långsiktigt (Phase 2): Split-view med scroll-synkronisering + - Desktop: Horisontell split (kvitto-text vänster, radlista höger) + - Tablet/Mobil: Tab-based fallback (radlista standard, "Se kvitto"-tab för kontext) + - Scroll-sync mellan text och rader (om rad 3 är synlig, visa motsvarande text) + - AI-guiding labels ("Denna rad matchade mejeri automatiskt") + - Implementering: ~8h + +2. Verifiera bildimport och felhantering end-to-end i testmiljo. +3. Implementera alias-inlarning vid manuell korrigering i importflodet. +4. Forbattra UI/UX i granskningsfloden for kvittoimport. +5. Fortsatt migrering av kvarvarande adminfloden. +6. Lokalisera kvarvarande delar i import- och inventarievyer. ## Viktiga beslut