import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations_helpers.dart'; import '../../models/product.dart'; import '../../models/remote_command.dart'; import '../../utils/app_updater.dart'; import '../../viewmodels/admin_pin_view_model.dart'; import '../../viewmodels/bar_screen_view_model.dart'; import '../../viewmodels/pin_lock_view_model.dart'; import '../../viewmodels/product_list_view_model.dart'; import '../../viewmodels/remote_sync_view_model.dart'; import '../../viewmodels/settings_view_model.dart'; import '../dialogs/close_tab_dialog.dart'; import '../dialogs/edit_tab_dialog.dart'; import 'product_grid.dart'; import 'tab_panel.dart'; class BarScreenView extends StatefulWidget { const BarScreenView({super.key}); @override State createState() => _BarScreenViewState(); } class _BarScreenViewState extends State { bool _showSearch = false; late final StreamSubscription _remoteCloseSubscription; @override void initState() { super.initState(); _remoteCloseSubscription = context .read() .tabsClosedRemotely .listen(_showRemoteClose); WidgetsBinding.instance.addPostFrameCallback((_) { UpdateChecker.check(context); }); } @override void dispose() { _remoteCloseSubscription.cancel(); super.dispose(); } void _showRemoteClose(RemoteSyncEvent event) { if (!mounted) return; final l10n = AppLocalizations.of(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( l10n.tabClosedRemotely( event.tabCustomerName ?? '', event.requestedBy ?? '', ), ), behavior: SnackBarBehavior.floating, ), ); } Future _addProduct(Product product) async { final l10n = AppLocalizations.of(context); final viewModel = context.read(); if (viewModel.selectedTab == null) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.openOrSelectTab))); return; } try { await viewModel.addProductToSelectedTab(product); } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.couldNotAddProductToTab))); } } void _showSyncProblem({required String title, required String description}) { final l10n = AppLocalizations.of(context); showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text(title), content: SelectableText(description), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: Text(l10n.close), ), ], ), ); } void _logout() { context.read().lock(); context.read().lock(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final viewModel = context.watch(); final productsViewModel = context.watch(); final settings = context.watch().settings; return Scaffold( appBar: AppBar( title: Text(l10n.barTabs), actions: [ IconButton( tooltip: _showSearch ? l10n.closeSearch : l10n.searchTabs, isSelected: _showSearch, onPressed: () => setState(() => _showSearch = !_showSearch), icon: const Icon(Icons.search_rounded), selectedIcon: const Icon(Icons.search_off_rounded), ), const SizedBox(width: 6), IconButton( tooltip: l10n.admin, onPressed: () => context.push('/admin'), icon: const Icon(Icons.admin_panel_settings_outlined), ), if (kDebugMode) ...[ const SizedBox(width: 6), _DebugShortcut( tooltip: l10n.manageProducts, icon: Icons.inventory_2_outlined, location: '/products', ), const SizedBox(width: 6), _DebugShortcut( tooltip: l10n.tabHistory, icon: Icons.history_rounded, location: '/history', ), ], if (context.watch().wasRevoked) ...[ const SizedBox(width: 6), IconButton( tooltip: l10n.deviceAccessRevoked, color: Theme.of(context).colorScheme.error, onPressed: () => context.push('/admin'), icon: const Icon(Icons.cloud_off_rounded), ), ], // A failing sync is otherwise invisible: the bar keeps working // offline while nothing from the website arrives if (context.watch().lastError case final error?) ...[ const SizedBox(width: 6), // Offline is normal for a bar; anything else needs attention if (context.watch().isOffline) IconButton( tooltip: l10n.offline, onPressed: () => _showSyncProblem( title: l10n.offline, description: l10n.offlineDescription, ), icon: const Icon(Icons.wifi_off_rounded), ) else IconButton( tooltip: '${l10n.syncProblem}: $error', color: Theme.of(context).colorScheme.error, onPressed: () => _showSyncProblem( title: l10n.syncProblem, description: '${l10n.syncProblemDescription}\n\n$error', ), icon: const Icon(Icons.sync_problem_rounded), ), ], const SizedBox(width: 6), IconButton( tooltip: l10n.refresh, onPressed: () => context.read().syncNow(), icon: const Icon(Icons.refresh_rounded), ), const SizedBox(width: 6), IconButton( tooltip: l10n.settings, onPressed: () => context.push('/settings'), icon: const Icon(Icons.settings), ), if (context.watch().isPinSet) ...[ const SizedBox(width: 6), IconButton( tooltip: l10n.logout, onPressed: _logout, icon: const Icon(Icons.logout), ), ], ], actionsPadding: const EdgeInsets.symmetric(horizontal: 8), ), resizeToAvoidBottomInset: false, body: Builder( builder: (context) { if (viewModel.isLoading) { return const Center( child: CircularProgressIndicator(strokeWidth: 2.5), ); } if (viewModel.loadError != null) { return _LoadError(message: l10n.errorText(viewModel.loadError)); } return Row( children: [ Expanded( flex: 2, child: ProductGrid( products: productsViewModel.products, rows: settings.barGridRows, hasSelectedTab: viewModel.selectedTab != null, onProductTap: _addProduct, ), ), Container(width: 1, color: Theme.of(context).dividerColor), Expanded( child: TabPanel( tabs: viewModel.tabs, selectedTab: viewModel.selectedTab, selectedTabId: viewModel.selectedTabId, showSearch: _showSearch, onTabSelected: viewModel.selectTab, onItemQuantityChanged: viewModel.changeItemQuantity, onCloseTabPressed: () => confirmCloseTab(context), onTabEdited: (tab) => showEditTabDialog(context, tab), onTabDeleted: (tab) => confirmDeleteTab(context, tab), ), ), ], ); }, ), ); } } class _DebugShortcut extends StatelessWidget { final String tooltip; final IconData icon; final String location; const _DebugShortcut({ required this.tooltip, required this.icon, required this.location, }); @override Widget build(BuildContext context) { return IconButton( tooltip: tooltip, style: IconButton.styleFrom( backgroundColor: Colors.yellow, foregroundColor: Colors.black, ), onPressed: () => context.push(location), icon: Icon(icon), ); } } class _LoadError extends StatelessWidget { final String message; const _LoadError({required this.message}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline_rounded, size: 40, color: Theme.of(context).colorScheme.error, ), const SizedBox(height: 12), Text( message, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), ); } }