diff --git a/flutter/lib/features/admin/presentation/admin_database_panel.dart b/flutter/lib/features/admin/presentation/admin_database_panel.dart new file mode 100644 index 00000000..4185f252 --- /dev/null +++ b/flutter/lib/features/admin/presentation/admin_database_panel.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../core/api/api_error_mapper.dart'; +import '../../../core/l10n/l10n.dart'; +import '../../profile/data/profile_repository.dart'; +import 'admin_products_panel.dart'; + +enum _DatabaseTab { inventory, pantry, products } + +class AdminDatabasePanel extends ConsumerStatefulWidget { + final bool embedded; + + const AdminDatabasePanel({super.key, this.embedded = false}); + + @override + ConsumerState createState() => _AdminDatabasePanelState(); +} + +class _AdminDatabasePanelState extends ConsumerState { + _DatabaseTab _activeTab = _DatabaseTab.inventory; + bool _isRefreshingCategories = false; + + Future _refreshCategories() async { + setState(() => _isRefreshingCategories = true); + try { + await ref.read(profileRepositoryProvider).refreshCategories(); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Kategorier har uppdaterats.')), + ); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(mapErrorToUserMessage(e, context))), + ); + } finally { + if (mounted) setState(() => _isRefreshingCategories = false); + } + } + + Widget _sectionCard({ + required IconData icon, + required String title, + required String description, + required VoidCallback onPressed, + required String buttonLabel, + }) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon), + const SizedBox(width: 8), + Expanded( + child: Text(title, style: Theme.of(context).textTheme.titleMedium), + ), + ], + ), + const SizedBox(height: 8), + Text(description), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: onPressed, + icon: Icon(icon), + label: Text(buttonLabel), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + String tabLabel(_DatabaseTab tab) { + switch (tab) { + case _DatabaseTab.inventory: + return context.l10n.profileInventoryTab; + case _DatabaseTab.pantry: + return context.l10n.profilePantryTab; + case _DatabaseTab.products: + return context.l10n.profileProductsTab; + } + } + + Widget activeSection; + switch (_activeTab) { + case _DatabaseTab.inventory: + activeSection = _sectionCard( + icon: Icons.inventory_2_outlined, + title: context.l10n.profileInventoryTab, + description: context.l10n.profileInventoryDescription, + onPressed: () => context.go('/inventory'), + buttonLabel: context.l10n.profileOpenInventory, + ); + case _DatabaseTab.pantry: + activeSection = _sectionCard( + icon: Icons.storefront_outlined, + title: context.l10n.profilePantryTab, + description: context.l10n.profilePantryDescription, + onPressed: () => context.go('/baslager'), + buttonLabel: context.l10n.profileOpenPantry, + ); + case _DatabaseTab.products: + activeSection = const AdminProductsPanel(embedded: true); + } + + return SingleChildScrollView( + padding: widget.embedded ? EdgeInsets.zero : const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.profileDatabaseDescription, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 12), + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Adminverktyg', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + 'Uppdatera kategorier manuellt i backend-cachen.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _isRefreshingCategories ? null : _refreshCategories, + icon: _isRefreshingCategories + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + label: const Text('Uppdatera kategorier'), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _DatabaseTab.values + .map( + (tab) => Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: Text(tabLabel(tab)), + selected: _activeTab == tab, + onSelected: (_) => setState(() => _activeTab = tab), + ), + ), + ) + .toList(), + ), + ), + const SizedBox(height: 16), + activeSection, + ], + ), + ); + } +} diff --git a/flutter/lib/features/admin/presentation/admin_screen.dart b/flutter/lib/features/admin/presentation/admin_screen.dart index 90c8d339..9a621ae7 100644 --- a/flutter/lib/features/admin/presentation/admin_screen.dart +++ b/flutter/lib/features/admin/presentation/admin_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/l10n/l10n.dart'; import 'admin_ai_panel.dart'; import 'admin_aliases_panel.dart'; +import 'admin_database_panel.dart'; import 'admin_pending_products_panel.dart'; import 'admin_products_panel.dart'; import 'admin_users_panel.dart'; @@ -18,7 +19,7 @@ class _AdminScreenState extends ConsumerState { @override Widget build(BuildContext context) { return DefaultTabController( - length: 5, + length: 6, child: Column( children: [ Material( @@ -27,6 +28,7 @@ class _AdminScreenState extends ConsumerState { isScrollable: true, tabs: [ Tab(text: context.l10n.profileUsersTab, icon: const Icon(Icons.people_outline)), + const Tab(text: 'Databas', icon: Icon(Icons.storage_outlined)), const Tab(text: 'Produkter', icon: Icon(Icons.inventory_2_outlined)), Tab(text: context.l10n.profilePendingTab, icon: const Icon(Icons.pending_actions_outlined)), const Tab(text: 'Alias', icon: Icon(Icons.link_outlined)), @@ -41,6 +43,10 @@ class _AdminScreenState extends ConsumerState { padding: EdgeInsets.fromLTRB(12, 12, 12, 8), child: AdminUsersPanel(embedded: true), ), + Padding( + padding: EdgeInsets.fromLTRB(12, 12, 12, 8), + child: AdminDatabasePanel(embedded: true), + ), Padding( padding: EdgeInsets.fromLTRB(12, 12, 12, 8), child: AdminProductsPanel(embedded: true), diff --git a/flutter/lib/features/profile/presentation/profile_screen.dart b/flutter/lib/features/profile/presentation/profile_screen.dart index 79e6d004..ebd277c9 100644 --- a/flutter/lib/features/profile/presentation/profile_screen.dart +++ b/flutter/lib/features/profile/presentation/profile_screen.dart @@ -4,17 +4,11 @@ import 'package:go_router/go_router.dart'; import '../../../core/api/api_error_mapper.dart'; import '../../../core/l10n/l10n.dart'; -import '../../admin/presentation/admin_ai_panel.dart'; -import '../../admin/presentation/admin_pending_products_panel.dart'; -import '../../admin/presentation/admin_products_panel.dart'; -import '../../admin/presentation/admin_users_panel.dart'; import '../../auth/data/auth_providers.dart'; import '../data/profile_repository.dart'; import '../domain/user_profile.dart'; -enum _ProfileTab { profile, database, users, suggestions, ai } - -enum _DatabaseTab { inventory, pantry, products } +enum _ProfileTab { profile } class ProfileScreen extends ConsumerStatefulWidget { const ProfileScreen({super.key}); @@ -27,11 +21,9 @@ class _ProfileScreenState extends ConsumerState { final _formKey = GlobalKey(); bool _isLoading = true; bool _isSaving = false; - bool _isRefreshingCategories = false; String? _error; UserProfile? _profile; _ProfileTab _activeTab = _ProfileTab.profile; - _DatabaseTab _activeDatabaseTab = _DatabaseTab.inventory; late final TextEditingController _emailCtrl; late final TextEditingController _firstNameCtrl; @@ -106,33 +98,10 @@ class _ProfileScreenState extends ConsumerState { context.go('/login'); } - Future _refreshCategories() async { - setState(() => _isRefreshingCategories = true); - try { - await ref.read(profileRepositoryProvider).refreshCategories(); - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Kategorier har uppdaterats.')), - ); - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), - ); - } finally { - if (mounted) setState(() => _isRefreshingCategories = false); - } - } List<_ProfileTab> _visibleTabs(bool isAdmin) { return [ _ProfileTab.profile, - _ProfileTab.database, - if (isAdmin) ...[ - _ProfileTab.users, - _ProfileTab.suggestions, - _ProfileTab.ai, - ], ]; } @@ -140,14 +109,6 @@ class _ProfileScreenState extends ConsumerState { switch (tab) { case _ProfileTab.profile: return context.l10n.profileMyProfileTab; - case _ProfileTab.database: - return context.l10n.profileDatabaseTab; - case _ProfileTab.users: - return context.l10n.profileUsersTab; - case _ProfileTab.suggestions: - return context.l10n.profilePendingTab; - case _ProfileTab.ai: - return context.l10n.profileAiTab; } } @@ -236,170 +197,10 @@ class _ProfileScreenState extends ConsumerState { ); } - Widget _buildDatabaseTab(BuildContext context) { - final isAdmin = _profile?.isAdmin == true; - final visibleTabs = [ - _DatabaseTab.inventory, - _DatabaseTab.pantry, - if (isAdmin) _DatabaseTab.products, - ]; - if (!visibleTabs.contains(_activeDatabaseTab)) { - _activeDatabaseTab = _DatabaseTab.inventory; - } - - String tabLabel(_DatabaseTab tab) { - switch (tab) { - case _DatabaseTab.inventory: - return context.l10n.profileInventoryTab; - case _DatabaseTab.pantry: - return context.l10n.profilePantryTab; - case _DatabaseTab.products: - return context.l10n.profileProductsTab; - } - } - - Widget sectionCard({ - required IconData icon, - required String title, - required String description, - required VoidCallback onPressed, - required String buttonLabel, - }) { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(icon), - const SizedBox(width: 8), - Expanded( - child: Text( - title, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ], - ), - const SizedBox(height: 8), - Text(description), - const SizedBox(height: 16), - FilledButton.icon( - onPressed: onPressed, - icon: Icon(icon), - label: Text(buttonLabel), - ), - ], - ), - ), - ); - } - - Widget activeSection; - switch (_activeDatabaseTab) { - case _DatabaseTab.inventory: - activeSection = sectionCard( - icon: Icons.inventory_2_outlined, - title: context.l10n.profileInventoryTab, - description: context.l10n.profileInventoryDescription, - onPressed: () => context.go('/inventory'), - buttonLabel: context.l10n.profileOpenInventory, - ); - case _DatabaseTab.pantry: - activeSection = sectionCard( - icon: Icons.storefront_outlined, - title: context.l10n.profilePantryTab, - description: context.l10n.profilePantryDescription, - onPressed: () => context.go('/baslager'), - buttonLabel: context.l10n.profileOpenPantry, - ); - case _DatabaseTab.products: - activeSection = const AdminProductsPanel(embedded: true); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.profileDatabaseDescription, - style: Theme.of(context).textTheme.bodyMedium, - ), - if (isAdmin) ...[ - const SizedBox(height: 12), - Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Adminverktyg', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Text( - 'Uppdatera kategorier manuellt i backend-cachen.', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: - _isRefreshingCategories ? null : _refreshCategories, - icon: _isRefreshingCategories - ? const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh), - label: const Text('Uppdatera kategorier'), - ), - ), - ], - ), - ), - ), - ], - const SizedBox(height: 12), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: visibleTabs - .map( - (tab) => Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(tabLabel(tab)), - selected: _activeDatabaseTab == tab, - onSelected: (_) => setState(() => _activeDatabaseTab = tab), - ), - ), - ) - .toList(), - ), - ), - const SizedBox(height: 16), - activeSection, - ], - ); - } - Widget _buildActiveTabContent(BuildContext context, ThemeData theme) { switch (_activeTab) { case _ProfileTab.profile: return _buildProfileForm(context, theme); - case _ProfileTab.database: - return _buildDatabaseTab(context); - case _ProfileTab.users: - return const AdminUsersPanel(embedded: true); - case _ProfileTab.suggestions: - return const AdminPendingProductsPanel(embedded: true); - case _ProfileTab.ai: - return const AdminAiPanel(embedded: true); } }