Files
recipe-app/flutter/lib/features/admin/presentation/admin_database_panel.dart
T
Nils-Johan Gynther 98ee8a3ad6
Test Suite / backend-pr-quick (24.15.0) (push) Has been cancelled
Test Suite / backend-full (24.15.0) (push) Has been cancelled
Test Suite / flutter-quality (push) Has been cancelled
feat: implement real-time database synchronization with SSE and update backend modules
2026-05-12 16:57:05 +02:00

196 lines
6.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';
import '../../../core/api/api_error_mapper.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/realtime/realtime_sync.dart';
import 'admin_ai_panel.dart';
import 'admin_aliases_panel.dart';
import 'admin_inventory_panel.dart';
import 'admin_pantry_panel.dart';
import 'admin_private_products_panel.dart';
import 'admin_pending_products_panel.dart';
import 'admin_products_panel.dart';
import '../../profile/data/profile_repository.dart';
enum _DatabaseTab { inventory, pantry, products, privateProducts, pending, aliases, ai }
class _DatabaseTabConfig {
final _DatabaseTab tab;
final String title;
final Widget panel;
const _DatabaseTabConfig({
required this.tab,
required this.title,
required this.panel,
});
}
class AdminDatabasePanel extends ConsumerStatefulWidget {
final bool embedded;
const AdminDatabasePanel({super.key, this.embedded = false});
@override
ConsumerState<AdminDatabasePanel> createState() => _AdminDatabasePanelState();
}
class _AdminDatabasePanelState extends ConsumerState<AdminDatabasePanel> {
_DatabaseTab _activeTab = _DatabaseTab.inventory;
bool _isRefreshingCategories = false;
int _panelRefreshVersion = 0;
ProviderSubscription<int>? _realtimeTickSubscription;
Timer? _realtimeDebounce;
@override
void initState() {
super.initState();
_realtimeTickSubscription = ref.listenManual<int>(
realtimeRefreshTickProvider,
(_, __) {
if (!mounted) return;
_realtimeDebounce?.cancel();
_realtimeDebounce = Timer(const Duration(milliseconds: 600), () {
if (!mounted) return;
setState(() => _panelRefreshVersion++);
});
},
);
}
@override
void dispose() {
_realtimeDebounce?.cancel();
_realtimeTickSubscription?.close();
super.dispose();
}
List<_DatabaseTabConfig> get _tabConfigs => [
_DatabaseTabConfig(
tab: _DatabaseTab.inventory,
title: context.l10n.profileInventoryTab,
panel: const AdminInventoryPanel(embedded: true),
),
_DatabaseTabConfig(
tab: _DatabaseTab.pantry,
title: context.l10n.profilePantryTab,
panel: const AdminPantryPanel(embedded: true),
),
_DatabaseTabConfig(
tab: _DatabaseTab.products,
title: 'Globala produkter',
panel: const AdminProductsPanel(embedded: true),
),
_DatabaseTabConfig(
tab: _DatabaseTab.privateProducts,
title: 'Privata produkter',
panel: const AdminPrivateProductsPanel(embedded: true),
),
_DatabaseTabConfig(
tab: _DatabaseTab.pending,
title: context.l10n.profilePendingTab,
panel: const AdminPendingProductsPanel(embedded: true),
),
_DatabaseTabConfig(
tab: _DatabaseTab.aliases,
title: 'Alias',
panel: const AdminAliasesPanel(embedded: true),
),
_DatabaseTabConfig(
tab: _DatabaseTab.ai,
title: 'AI',
panel: const AdminAiPanel(embedded: true),
),
];
Future<void> _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(
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
);
} finally {
if (mounted) setState(() => _isRefreshingCategories = false);
}
}
@override
Widget build(BuildContext context) {
final currentTab = _tabConfigs.firstWhere((config) => config.tab == _activeTab);
final header = Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _tabConfigs
.map(
(config) => Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(config.title),
selected: _activeTab == config.tab,
onSelected: (_) => setState(() => _activeTab = config.tab),
),
),
)
.toList(),
),
),
),
const SizedBox(width: 8),
IconButton(
tooltip: 'Uppdatera kategorier',
onPressed: _isRefreshingCategories ? null : _refreshCategories,
icon: _isRefreshingCategories
? const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
),
],
),
const SizedBox(height: 8),
],
),
),
);
return Padding(
padding: widget.embedded ? EdgeInsets.zero : const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
header,
const SizedBox(height: 12),
Expanded(
child: KeyedSubtree(
key: ValueKey('admin-db-${_activeTab.name}-$_panelRefreshVersion'),
child: currentTab.panel,
),
),
],
),
);
}
}