diff --git a/filanalys.md b/filanalys.md index 08ba5c23..2213b551 100644 --- a/filanalys.md +++ b/filanalys.md @@ -24,18 +24,29 @@ Ge en detaljerad rapport enligt följande struktur: --- ### **1. Allmän kodkvalitet** -- **Optimeringar**: - - Finns det ineffektiva algoritmer (t.ex. O(n²) istället för O(n))? - - Kan loopar, databaserfrågor (Prisma) eller API-anrop optimeras (t.ex. med caching, batch-behandling)? - - Finns det onödig kod (död kod, duplicerad logik)? - - Kan minne eller CPU-användning reduceras (t.ex. undvika djupa kopior, använda streams)? -- **Läsbarhet/underhållbarhet**: +- **Läsbarhet/underhållbarhet** (kan blockera om allvarligt): - Finns det bristande namngivning (variabler, funktioner, klasser)? - Saknas kommentarer för komplex logik? - Kan modulariseringen förbättras (t.ex. splitta stora funktioner/klasser)? - Följs TypeScript-bäst-praxis (t.ex. starka typer, interfaces, SOLID-principer)? +--- +### **1b. Performance-optimeringar** (INFORMATIONAL) + +Dessa rapporteras men blockerar inte commit. Kan adresseras i senare iteration: + +- **Algoritm-effektivitet**: + - Finns det O(n²) eller värre algoritmer som kan vara O(n)? + - Finns onödig kod (död kod, duplicerad logik)? + +- **Resurser**: + - Kan minne eller CPU-användning reduceras (t.ex. undvika djupa kopior, använda streams)? + - Kan loopar eller databaserfrågor (Prisma) optimeras (t.ex. med caching, batch-behandling)? + - Finns N+1-frågor eller ineffektiva `include/select`-mönster? + +**Severity**: `Low` eller `Medium` beroende på påverkan. Blockerar aldrig commit. + --- ### **2. Säkerhetsanalys** - **Sårbarheter**: @@ -82,13 +93,27 @@ Ge en detaljerad rapport enligt följande struktur: - **Uppskattad tid** för att implementera förslagen. - **Rekommenderade verktyg** för automatiserade kontroller (t.ex. `ESLint`, `Prisma Lint`, `OWASP Dependency-Check`). +--- +### **Klassificering av fynd (Severity)** + +**BLOCKING** (hindrar commit): +- `Critical`: Säkerhetshål, scope-brister (IDOR), SQL-injection, XSS, eller data-loss risk. +- `High`: Allvarlig korrektness-fel, felaktig autentisering/auktorisation, eller felaktig felhantering som påverkar produktion. + +**INFORMATIONAL** (rapporteras, men blockerar inte): +- `Medium`: Code-quality, läsbarhet, testluckor, eller mindre performance-optimeringar. +- `Low`: Stilfrågor, dokumentation, eller nice-to-have refactor. + +**Regel**: Gate-beslut = `PASS` om inga `Critical` eller `High` finns. `BLOCK` annars. + --- ### **Regler för analysen** - Var **specifik**: Ge **kod-exempel** för varje förslag. - Var **praktisk**: Fokusera på **realistiska förbättringar** som kan implementeras nu. - Var **kritisk**: Peka ut **allvarliga risker** (t.ex. säkerhetshål) först. -- Använd **severity** per fynd: `Critical`, `High`, `Medium`, `Low`. -- För varje fynd: ange fil, kort riskbeskrivning, varför det är ett problem, och konkret åtgärd. +- Använd **severity** per fynd enligt klassificering ovan: `Critical`, `High`, `Medium`, `Low`. +- För varje fynd: ange fil, kort riskbeskrivning, varför det är ett problem, severity, och konkret åtgärd. +- **Separa fynd efter severity**: Listet först alla `Critical`/`High` (blocking), sedan `Medium`/`Low` (informational). - Om inga allvarliga risker hittas: skriv det explicit och lyft kvarvarande risker/testluckor. - Ignorera filer som inte är relevanta (t.ex. node_modules, .git, binärfiler). - Prioritera körbarhet: föreslagna åtgärder ska kunna göras i denna kodbas utan större arkitekturprojekt. @@ -100,9 +125,11 @@ Ge en detaljerad rapport enligt följande struktur: ### **Outputformat (obligatoriskt)** 1. `Scope` 2. `Gate-beslut` (`PASS` eller `BLOCK`) -3. `1. Allmän kodkvalitet` -4. `2. Säkerhetsanalys` -5. `3. Sammanfattning` +3. `1. Allmän kodkvalitet` (blocking issues) +4. `1b. Performance-optimeringar` (informational) +5. `2. Säkerhetsanalys` (blocking issues) +6. `2b. Backend-specifik kontroll` (blocking + informational) +7. `3. Sammanfattning` (topprioriteringar, tidskattning) Om inga relevanta filer hittas: - Skriv `Inget att analysera` och varför (t.ex. tom staged + tom working tree). diff --git a/flutter/lib/core/utils/global_error_handler.dart b/flutter/lib/core/utils/global_error_handler.dart index 71b1f0af..2e191c7f 100644 --- a/flutter/lib/core/utils/global_error_handler.dart +++ b/flutter/lib/core/utils/global_error_handler.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -/// Visar en global dialogruta med ett felmeddelande och en kopieringsknapp. +/// Visar en global dialogruta med ett felmeddelande och knappar för kopiera och ta bort. void showGlobalErrorDialog(BuildContext context, String errorMessage) { showDialog( context: context, @@ -17,7 +17,7 @@ void showGlobalErrorDialog(BuildContext context, String errorMessage) { ), actions: [ TextButton( - child: const Text('Stäng'), + child: const Text('Ta bort'), onPressed: () { Navigator.of(context).pop(); }, @@ -26,6 +26,7 @@ void showGlobalErrorDialog(BuildContext context, String errorMessage) { child: const Text('Kopiera'), onPressed: () { Clipboard.setData(ClipboardData(text: errorMessage)); + Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Felmeddelande kopierat!')), ); diff --git a/flutter/lib/features/import/presentation/error_dialog.dart b/flutter/lib/features/import/presentation/error_dialog.dart index 29faf87e..deba77b1 100644 --- a/flutter/lib/features/import/presentation/error_dialog.dart +++ b/flutter/lib/features/import/presentation/error_dialog.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import '../../../core/l10n/l10n.dart'; -/// Visar en dialogruta med ett felmeddelande och en kopieringsknapp. +/// Visar en dialogruta med ett felmeddelande och knappar för kopiera och ta bort. void showErrorDialog(BuildContext context, String errorMessage) { showDialog( context: context, @@ -28,6 +28,7 @@ void showErrorDialog(BuildContext context, String errorMessage) { child: Text(context.l10n.errorDialogCopy), onPressed: () { Clipboard.setData(ClipboardData(text: errorMessage)); + Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.errorDialogCopied)), ); diff --git a/flutter/lib/features/import/presentation/receipt_import_tab.dart b/flutter/lib/features/import/presentation/receipt_import_tab.dart index 1a3ba16d..a13ba0b0 100644 --- a/flutter/lib/features/import/presentation/receipt_import_tab.dart +++ b/flutter/lib/features/import/presentation/receipt_import_tab.dart @@ -460,6 +460,44 @@ class _ReceiptImportTabState extends ConsumerState { ); } + void _deleteItem(int index) { + final items = _items; + if (items == null || index < 0 || index >= items.length) return; + + // Ta bort raden från items + final remainingItems = [...items]; + remainingItems.removeAt(index); + + // Re-index edits och selected i en enda pass + final remainingEdits = {}; + final remainingSelected = {}; + var newIndex = 0; + + for (var oldIndex = 0; oldIndex < items.length; oldIndex++) { + if (oldIndex != index) { + if (_edits.containsKey(oldIndex)) { + remainingEdits[newIndex] = _edits[oldIndex]!; + } + if (_selected[oldIndex] == true) { + remainingSelected[newIndex] = true; + } + newIndex++; + } + } + + final notifier = ref.read(receiptImportSessionProvider.notifier); + if (remainingItems.isEmpty) { + notifier.clear(); + } else { + notifier.setImportedResult( + items: remainingItems, + edits: remainingEdits, + selected: remainingSelected, + ); + } + setState(() {}); + } + Future _addSelected() async { final items = _items; if (items == null) return; @@ -572,9 +610,36 @@ class _ReceiptImportTabState extends ConsumerState { SnackBar(content: Text(parts.join(', ') + '.')), ); - // Avmarkera sparade rader och uppdatera inventariet + // Ta bort de tillagda raderna från listan + final addedIndexSet = toAdd.toSet(); + final remainingItems = []; + final remainingEdits = {}; + final remainingSelected = {}; + + var newIndex = 0; + for (var oldIndex = 0; oldIndex < items.length; oldIndex++) { + if (!addedIndexSet.contains(oldIndex)) { + remainingItems.add(items[oldIndex]); + if (_edits.containsKey(oldIndex)) { + remainingEdits[newIndex] = _edits[oldIndex]!; + } + if (_selected[oldIndex] == true) { + remainingSelected[newIndex] = true; + } + newIndex++; + } + } + final notifier = ref.read(receiptImportSessionProvider.notifier); - notifier.setSelectedForIndexes(toAdd, false); + if (remainingItems.isEmpty) { + notifier.clear(); + } else { + notifier.setImportedResult( + items: remainingItems, + edits: remainingEdits, + selected: remainingSelected, + ); + } setState(() {}); await _loadInventory(); } catch (e) { @@ -779,6 +844,7 @@ class _ReceiptImportTabState extends ConsumerState { i, initialEntryMode: ImportProductEntryMode.create, ), + onDeleteRequested: () => _deleteItem(i), matchedViaBadgeBuilder: _buildMatchedViaBadge, ); }, @@ -811,6 +877,7 @@ class _ReceiptImportResultRow extends ConsumerWidget { final VoidCallback onEditRequested; final VoidCallback onSelectExistingRequested; final VoidCallback onCreateRequested; + final VoidCallback onDeleteRequested; final Widget Function(ParsedReceiptItem item, ThemeData theme) matchedViaBadgeBuilder; @@ -824,6 +891,7 @@ class _ReceiptImportResultRow extends ConsumerWidget { required this.onEditRequested, required this.onSelectExistingRequested, required this.onCreateRequested, + required this.onDeleteRequested, required this.matchedViaBadgeBuilder, }); @@ -1019,14 +1087,29 @@ class _ReceiptImportResultRow extends ConsumerWidget { ], ], ), - trailing: Icon( - hasProduct - ? Icons.check_circle - : (isSuggested ? Icons.help_outline : Icons.error_outline), - color: hasProduct - ? Colors.green - : (isSuggested ? Colors.orange : theme.colorScheme.tertiary), - size: 20, + trailing: SizedBox( + width: 80, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + hasProduct + ? Icons.check_circle + : (isSuggested ? Icons.help_outline : Icons.error_outline), + color: hasProduct + ? Colors.green + : (isSuggested ? Colors.orange : theme.colorScheme.tertiary), + size: 20, + ), + IconButton( + icon: Icon(Icons.delete_outline, size: 18, color: theme.colorScheme.error), + onPressed: onDeleteRequested, + tooltip: 'Ta bort rad', + constraints: const BoxConstraints(minWidth: 40, minHeight: 40), + padding: const EdgeInsets.all(4), + ), + ], + ), ), onTap: onEditRequested, ),