feat: New Open Tab dialog with search and optional customer email. Drinks are now grouped per order. Payments trigger cha-ching and money rain.
This commit is contained in:
+172
-135
@@ -18,6 +18,7 @@ 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 '../dialogs/open_tab_dialog.dart';
|
||||
import '../widgets/venue_title.dart';
|
||||
import 'product_grid.dart';
|
||||
import 'tab_panel.dart';
|
||||
@@ -30,8 +31,11 @@ class BarScreenView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BarScreenViewState extends State<BarScreenView> {
|
||||
static const _idleTimeout = Duration(minutes: 5);
|
||||
|
||||
bool _showSearch = false;
|
||||
late final StreamSubscription<RemoteSyncEvent> _remoteCloseSubscription;
|
||||
Timer? _idleTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -41,6 +45,7 @@ class _BarScreenViewState extends State<BarScreenView> {
|
||||
.read<RemoteSyncViewModel>()
|
||||
.tabsClosedRemotely
|
||||
.listen(_showRemoteClose);
|
||||
_restartIdleTimer();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
UpdateChecker.check(context);
|
||||
@@ -50,9 +55,19 @@ class _BarScreenViewState extends State<BarScreenView> {
|
||||
@override
|
||||
void dispose() {
|
||||
_remoteCloseSubscription.cancel();
|
||||
_idleTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// A tab left open after the bar goes quiet is easy to add to by mistake,
|
||||
/// so it is put back in the list.
|
||||
void _restartIdleTimer() {
|
||||
_idleTimer?.cancel();
|
||||
_idleTimer = Timer(_idleTimeout, () {
|
||||
if (mounted) context.read<BarScreenViewModel>().deselectTab();
|
||||
});
|
||||
}
|
||||
|
||||
void _showRemoteClose(RemoteSyncEvent event) {
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -91,6 +106,16 @@ class _BarScreenViewState extends State<BarScreenView> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openTab() async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final name = await showOpenTabDialog(context);
|
||||
|
||||
if (name == null || !mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.tabOpened(name))));
|
||||
}
|
||||
|
||||
void _showSyncProblem({required String title, required String description}) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
showDialog<void>(
|
||||
@@ -151,150 +176,162 @@ class _BarScreenViewState extends State<BarScreenView> {
|
||||
(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),
|
||||
// Any touch counts as activity, wherever it lands
|
||||
return Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPointerDown: (_) => _restartIdleTimer(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: VenueTitle(
|
||||
text: venueName == null || venueName.trim().isEmpty
|
||||
? l10n.barTabs
|
||||
: venueName,
|
||||
),
|
||||
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',
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: l10n.openTab,
|
||||
onPressed: _openTab,
|
||||
icon: const Icon(Icons.person_add_alt_1_outlined),
|
||||
),
|
||||
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,
|
||||
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.cloud_off_rounded),
|
||||
icon: const Icon(Icons.admin_panel_settings_outlined),
|
||||
),
|
||||
],
|
||||
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),
|
||||
if (kDebugMode) ...[
|
||||
const SizedBox(width: 6),
|
||||
_DebugShortcut(
|
||||
tooltip: l10n.manageProducts,
|
||||
icon: Icons.inventory_2_outlined,
|
||||
location: '/products',
|
||||
),
|
||||
],
|
||||
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),
|
||||
),
|
||||
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,
|
||||
onTabDeselected: viewModel.deselectTab,
|
||||
onItemQuantityChanged: viewModel.changeItemQuantity,
|
||||
onCloseTabPressed: () => confirmCloseTab(context),
|
||||
onTabEdited: (tab) => showEditTabDialog(context, tab),
|
||||
onTabDeleted: (tab) => confirmDeleteTab(context, tab),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user