320 lines
9.7 KiB
Dart
320 lines
9.7 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 _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),
|
|
),
|
|
],
|
|
// 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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|