feat(ai): add AI trace tracking and admin panel
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 12m45s
Test Suite / flutter-quality (push) Failing after 7m24s

- Add AiTrace model to Prisma schema with relations to User
- Implement AiTraceService with CRUD operations for AI traces
- Add new admin panel for AI traces with filtering and detail views
- Integrate trace persistence in receipt import flow
- Add API endpoints for listing and retrieving AI traces
- Update Flutter admin UI with new AI tab and navigation
- Add new domain models for AI traces and details
- Add migration for AiTrace table creation

BREAKING CHANGE: None
This commit is contained in:
Nils-Johan Gynther
2026-05-21 17:33:21 +02:00
parent c3520b5ad4
commit 67a7590525
21 changed files with 2477 additions and 509 deletions
+40 -32
View File
@@ -49,19 +49,19 @@ class AppShell extends ConsumerWidget {
icon: Icons.storefront_outlined,
label: 'Baslager',
),
_AppDestination(
path: '/import',
title: 'Importera',
icon: Icons.upload_file_outlined,
label: 'Importera',
),
_AppDestination(
path: '/inkopslista',
title: 'Inköpslista',
icon: Icons.shopping_cart_outlined,
label: 'Inköpslista',
),
];
_AppDestination(
path: '/import',
title: 'Importera',
icon: Icons.upload_file_outlined,
label: 'Importera',
),
_AppDestination(
path: '/inkopslista',
title: 'Inköpslista',
icon: Icons.shopping_cart_outlined,
label: 'Inköpslista',
),
];
List<_AppDestination> _destinations() => _baseDestinations;
@@ -101,8 +101,8 @@ class AppShell extends ConsumerWidget {
}
}
final isRecipesRoute = location.startsWith('/recipes') &&
!location.startsWith('/recipes/');
final isRecipesRoute =
location.startsWith('/recipes') && !location.startsWith('/recipes/');
final isImportRoute = location == '/import';
final isAdminRoute = location.startsWith('/admin');
final adminTab = AdminViewTabX.fromQuery(
@@ -133,6 +133,12 @@ class AppShell extends ConsumerWidget {
selected: adminTab == AdminViewTab.database,
onSelected: (_) => navigateToAdminTab(AdminViewTab.database),
),
const SizedBox(width: 8),
ChoiceChip(
label: const Text('AI'),
selected: adminTab == AdminViewTab.ai,
onSelected: (_) => navigateToAdminTab(AdminViewTab.ai),
),
],
),
);
@@ -140,7 +146,8 @@ class AppShell extends ConsumerWidget {
Widget shell = Scaffold(
appBar: AppBar(
title: isAdminRoute ? buildAdminTitle() : Text(selectedDestination.title),
title:
isAdminRoute ? buildAdminTitle() : Text(selectedDestination.title),
bottom: isImportRoute
? const TabBar(
tabs: [
@@ -148,17 +155,17 @@ class AppShell extends ConsumerWidget {
icon: Icon(Icons.restaurant_menu_outlined),
text: 'Recept',
),
Tab(
icon: Icon(Icons.receipt_long_outlined),
text: 'Kvitto',
),
Tab(
icon: Icon(Icons.local_offer_outlined),
text: 'Flyer',
),
],
)
: null,
Tab(
icon: Icon(Icons.receipt_long_outlined),
text: 'Kvitto',
),
Tab(
icon: Icon(Icons.local_offer_outlined),
text: 'Flyer',
),
],
)
: null,
actions: [
if (isRecipesRoute)
Consumer(
@@ -184,8 +191,9 @@ class AppShell extends ConsumerWidget {
PopupMenuButton<int>(
icon: const Icon(Icons.grid_view),
tooltip: 'Välj antal kolumner',
onSelected: (columns) =>
ref.read(recipesViewProvider.notifier).setColumns(columns),
onSelected: (columns) => ref
.read(recipesViewProvider.notifier)
.setColumns(columns),
itemBuilder: (context) => const [
PopupMenuItem(value: 2, child: Text('2 kolumner')),
PopupMenuItem(value: 4, child: Text('4 kolumner')),
@@ -288,9 +296,9 @@ class AppShell extends ConsumerWidget {
),
);
if (isImportRoute) {
shell = DefaultTabController(length: 3, child: shell);
}
if (isImportRoute) {
shell = DefaultTabController(length: 3, child: shell);
}
return shell;
}