import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/services/barcode_service.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_layout.dart'; import '../../modules/screens/customers_view.dart'; import '../../modules/screens/events_view.dart'; import '../../modules/screens/product_import_view.dart'; import '../../modules/screens/promos_view.dart'; import '../../modules/screens/settings_view.dart'; import '../../sync/providers/sync_controller.dart'; import '../providers/cart_controller.dart'; import '../providers/navigation_provider.dart'; import '../widgets/app_sidebar.dart'; import '../widgets/billing_panel.dart'; import '../widgets/cart_fab.dart'; import '../widgets/page_header.dart'; import 'pos_view.dart'; /// Application shell. /// /// Owns the sidebar, page header and the body for whichever module is active. /// Keeping one shell means navigation never rebuilds the chrome, and the /// scanner stays live across modules. /// /// Layout collapses in a fixed order as width shrinks: /// /// * `>= 1300` sidebar with labels, docked bill /// * `1120–1300` sidebar as an icon rail, docked bill /// * `920–1120` icon rail, bill becomes a bottom sheet /// * `< 920` sidebar goes off-canvas behind a menu button class PosDashboardScreen extends ConsumerStatefulWidget { const PosDashboardScreen({super.key}); @override ConsumerState createState() => _PosDashboardScreenState(); } class _PosDashboardScreenState extends ConsumerState { final GlobalKey _scaffoldKey = GlobalKey(); final FocusNode _searchFocus = FocusNode(); late final BarcodeService _barcode; @override void initState() { super.initState(); // The scanner behaves like a keyboard, so listen globally rather than // depending on any one field holding focus. A scan from another module // jumps back to billing, which is what a cashier expects. _barcode = BarcodeService( onScan: (code) { // Without an imported catalogue there is nothing to resolve against. if (!ref.read(catalogueReadyProvider)) return; ref.read(activeModuleProvider.notifier).state = PosModule.pos; ref.read(cartControllerProvider.notifier).scanBarcode(code); }, )..attach(); } @override void dispose() { _barcode.dispose(); _searchFocus.dispose(); super.dispose(); } void _openBillingSheet() { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => FractionallySizedBox( heightFactor: 0.92, child: ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(AppRadius.xxl), ), child: const BillingPanel(inSheet: true), ), ), ); } Widget _body(PosModule module, PosLayout layout) => switch (module) { PosModule.pos => PosView(layout: layout, searchFocus: _searchFocus), PosModule.customers => const CustomersView(), PosModule.productImport => const ProductImportView(), PosModule.promos => const PromosView(), PosModule.events => const EventsView(), PosModule.settings => const SettingsView(), }; @override Widget build(BuildContext context) { final layout = PosLayout.of(context); final module = ref.watch(activeModuleProvider); final ready = ref.watch(catalogueReadyProvider); final isPos = module == PosModule.pos && ready; // Only the terminal itself needs the bill docked beside it. final showDockedBill = isPos && !layout.billingIsSheet; final showCartFab = isPos && layout.billingIsSheet; return Scaffold( key: _scaffoldKey, backgroundColor: AppColors.background, drawer: layout.sidebarIsDrawer ? Drawer( width: PosLayout.expandedWidth, backgroundColor: AppColors.surface, child: AppSidebar( mode: SidebarMode.expanded, onDestinationTap: () => Navigator.of(context).maybePop(), ), ) : null, floatingActionButton: showCartFab ? CartFab(onTap: _openBillingSheet) : null, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, body: CallbackShortcuts( bindings: { const SingleActivator(LogicalKeyboardKey.f2): _searchFocus.requestFocus, const SingleActivator(LogicalKeyboardKey.f8): () => ref.read(cartControllerProvider.notifier).undo(), const SingleActivator(LogicalKeyboardKey.escape): _searchFocus.unfocus, }, child: Focus( autofocus: true, child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (!layout.sidebarIsDrawer) AppSidebar(mode: layout.sidebar), Expanded( child: Column( children: [ PageHeader( layout: layout, onMenuTap: () => _scaffoldKey.currentState?.openDrawer(), ), Expanded( child: AnimatedSwitcher( duration: AppMotion.fast, child: KeyedSubtree( key: ValueKey(module), child: _body(module, layout), ), ), ), ], ), ), if (showDockedBill) ...[ const VerticalDivider(width: 1), SizedBox( width: layout.billingWidth, child: const BillingPanel(), ), ], ], ), ), ), ); } }