Files
kooltab/lib/views/bar/bar_screen_view.dart
T

358 lines
11 KiB
Dart

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 '../widgets/venue_title.dart';
import 'product_grid.dart';
import 'tab_panel.dart';
class BarScreenView extends StatefulWidget {
const BarScreenView({super.key});
@override
State<BarScreenView> createState() => _BarScreenViewState();
}
class _BarScreenViewState extends State<BarScreenView> {
bool _showSearch = false;
late final StreamSubscription<RemoteSyncEvent> _remoteCloseSubscription;
@override
void initState() {
super.initState();
_remoteCloseSubscription = context
.read<RemoteSyncViewModel>()
.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<void> _addProduct(Product product) async {
final l10n = AppLocalizations.of(context);
final viewModel = context.read<BarScreenViewModel>();
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<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(title),
content: SelectableText(description),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(l10n.close),
),
],
),
);
}
void _showUpdateRequired(String message) {
final l10n = AppLocalizations.of(context);
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(l10n.appUpdateRequired),
content: SelectableText(
message.isEmpty
? l10n.appUpdateRequiredDescription
: '${l10n.appUpdateRequiredDescription}\n\n$message',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(l10n.close),
),
FilledButton(
onPressed: () {
Navigator.of(dialogContext).pop();
context.push('/settings');
},
child: Text(l10n.update),
),
],
),
);
}
void _logout() {
context.read<PinLockViewModel>().lock();
context.read<AdminPinViewModel>().lock();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final viewModel = context.watch<BarScreenViewModel>();
final productsViewModel = context.watch<ProductListViewModel>();
final settings = context.watch<SettingsViewModel>().settings;
final venueName = context.select<RemoteSyncViewModel, String?>(
(viewModel) => viewModel.venueName,
);
return Scaffold(
appBar: AppBar(
title: VenueTitle(
text: venueName == null || venueName.trim().isEmpty
? l10n.barTabs
: venueName,
),
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<RemoteSyncViewModel>().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),
),
],
if (context.watch<RemoteSyncViewModel>().updateRequired
case final message?) ...[
const SizedBox(width: 6),
IconButton(
tooltip: l10n.appUpdateRequired,
color: Theme.of(context).colorScheme.error,
onPressed: () => _showUpdateRequired(message),
icon: const Icon(Icons.system_update_rounded),
),
],
// A failing sync is otherwise invisible: the bar keeps working
// offline while nothing from the website arrives
if (context.watch<RemoteSyncViewModel>().lastError
case final error?) ...[
const SizedBox(width: 6),
// Offline is normal for a bar; anything else needs attention
if (context.watch<RemoteSyncViewModel>().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<RemoteSyncViewModel>().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<PinLockViewModel>().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,
),
],
),
),
);
}
}