import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../app/providers.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_layout.dart'; import '../../../core/utils/formatters.dart'; import '../providers/cart_controller.dart'; import '../providers/navigation_provider.dart'; /// White page header: breadcrumb, title, and the terminal's quick actions. /// /// Replaces the old purple app bar now that branding lives in the sidebar. class PageHeader extends ConsumerWidget { const PageHeader({ super.key, required this.layout, this.onMenuTap, }); final PosLayout layout; final VoidCallback? onMenuTap; @override Widget build(BuildContext context, WidgetRef ref) { final compact = layout.sidebarIsDrawer; // Measured from the header's own box, not the window. The window can be // wide while this column is narrow — the sidebar and the docked bill both // take from it — which is how the status chrome ended up overflowing. return LayoutBuilder( builder: (context, box) { final showStatus = box.maxWidth >= 720; return _bar(context, ref, compact, showStatus); }, ); } Widget _bar( BuildContext context, WidgetRef ref, bool compact, bool showStatus, ) { final module = ref.watch(activeModuleProvider); final now = ref.watch(clockProvider).value ?? DateTime.now(); return Container( constraints: const BoxConstraints(minHeight: AppSizes.headerHeight), padding: EdgeInsets.symmetric( horizontal: layout.contentPadding, vertical: AppSpacing.md, ), decoration: const BoxDecoration( color: AppColors.surface, border: Border(bottom: BorderSide(color: AppColors.border)), ), child: Row( children: [ if (compact) ...[ IconButton( onPressed: onMenuTap, icon: const Icon(Icons.menu_rounded), tooltip: 'Menu', color: AppColors.textPrimary, ), const SizedBox(width: AppSpacing.xs), ], Flexible( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( module.title, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 19, fontWeight: FontWeight.w600, letterSpacing: -0.4, color: AppColors.textPrimary, height: 1.2, ), ), if (!compact) _Breadcrumb(module: module), ], ), ), const Spacer(), if (showStatus) ...[ _LivePill(offline: ref.watch(simulateOfflineProvider)), const SizedBox(width: AppSpacing.lg), Text( Formatters.time(now), style: const TextStyle( fontSize: 13.5, fontWeight: FontWeight.w500, color: AppColors.textSecondary, fontFeatures: [FontFeature.tabularFigures()], ), ), const SizedBox(width: AppSpacing.lg), Container(width: 1, height: 26, color: AppColors.border), const SizedBox(width: AppSpacing.lg), ], _ParkedBillsButton(compact: compact), const SizedBox(width: AppSpacing.sm), _NewSaleButton(compact: compact), ], ), ); } } class _Breadcrumb extends StatelessWidget { const _Breadcrumb({required this.module}); final PosModule module; @override Widget build(BuildContext context) { const style = TextStyle(fontSize: 12, color: AppColors.textTertiary); return Row( mainAxisSize: MainAxisSize.min, children: [ const Text('Home', style: style), const Padding( padding: EdgeInsets.symmetric(horizontal: AppSpacing.xs + 2), child: Icon(Icons.chevron_right_rounded, size: 13, color: AppColors.textTertiary,), ), Text(module.section.label, style: style), const Padding( padding: EdgeInsets.symmetric(horizontal: AppSpacing.xs + 2), child: Icon(Icons.chevron_right_rounded, size: 13, color: AppColors.textTertiary,), ), Text( module.label, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.primary, ), ), ], ); } } class _LivePill extends StatefulWidget { const _LivePill({required this.offline}); final bool offline; @override State<_LivePill> createState() => _LivePillState(); } class _LivePillState extends State<_LivePill> with SingleTickerProviderStateMixin { late final AnimationController _c = AnimationController( vsync: this, duration: const Duration(milliseconds: 1400), )..repeat(reverse: true); @override void dispose() { _c.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final offline = widget.offline; final tone = offline ? AppColors.warning : AppColors.success; final surface = offline ? AppColors.warningSurface : AppColors.successSurface; return Tooltip( message: offline ? 'Simulate offline is ON in Settings — imports and syncs are being ' 'failed deliberately.' : 'Terminal is operating normally.', child: Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.xs + 2, ), decoration: BoxDecoration( color: surface, borderRadius: AppRadius.brPill, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ FadeTransition( opacity: _c, child: Container( width: 7, height: 7, decoration: BoxDecoration(color: tone, shape: BoxShape.circle), ), ), const SizedBox(width: AppSpacing.xs + 2), Text( offline ? 'OFFLINE (SIM)' : 'LIVE', style: TextStyle( color: tone, fontSize: 10.5, fontWeight: FontWeight.w700, letterSpacing: 0.8, ), ), ], ), ), ); } } class _ParkedBillsButton extends ConsumerWidget { const _ParkedBillsButton({required this.compact}); final bool compact; @override Widget build(BuildContext context, WidgetRef ref) { final parked = ref.watch(parkedBillsProvider).value ?? const []; if (compact) { return IconButton( tooltip: 'Parked bills', onPressed: () => _openParked(context, ref), icon: Badge( isLabelVisible: parked.isNotEmpty, label: Text('${parked.length}'), backgroundColor: AppColors.warning, child: const Icon(Icons.pause_circle_outline_rounded), ), ); } return OutlinedButton.icon( onPressed: () => _openParked(context, ref), icon: const Icon(Icons.pause_circle_outline_rounded, size: 17), label: Text( parked.isEmpty ? 'Parked' : 'Parked (${parked.length})', ), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 42), padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), foregroundColor: AppColors.textSecondary, ), ); } void _openParked(BuildContext context, WidgetRef ref) { final parked = ref.read(parkedBillsProvider).value ?? const []; if (parked.isEmpty) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(const SnackBar(content: Text('No parked bills.'))); return; } showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Parked bills'), content: SizedBox( width: (MediaQuery.sizeOf(context).width - 96).clamp(280.0, 380.0), child: ListView.separated( shrinkWrap: true, itemCount: parked.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (_, i) { final bill = parked[i]; return ListTile( leading: const Icon(Icons.receipt_long_rounded, color: AppColors.primary,), title: Text(bill.displayLabel), subtitle: Text( '${bill.cart.lineCount} items · ' '${Formatters.money(bill.cart.grandTotal)} · ' '${Formatters.time(bill.parkedAt)}', ), onTap: () async { await ref .read(cartControllerProvider.notifier) .resume(bill); ref.invalidate(parkedBillsProvider); if (context.mounted) Navigator.of(context).pop(); }, ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Close'), ), ], ), ); } } class _NewSaleButton extends ConsumerWidget { const _NewSaleButton({required this.compact}); final bool compact; @override Widget build(BuildContext context, WidgetRef ref) { void start() { ref.read(cartControllerProvider.notifier).reset(); ref.read(activeModuleProvider.notifier).state = PosModule.pos; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(const SnackBar(content: Text('Started a new sale.'))); } if (compact) { return IconButton.filled( tooltip: 'New sale', onPressed: start, icon: const Icon(Icons.add_rounded), style: IconButton.styleFrom(backgroundColor: AppColors.primary), ); } return FilledButton.icon( onPressed: start, icon: const Icon(Icons.add_rounded, size: 18), label: const Text('New Sale'), style: FilledButton.styleFrom( backgroundColor: AppColors.primary, minimumSize: const Size(0, 42), padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), shape: const RoundedRectangleBorder(borderRadius: AppRadius.brSm), ), ); } }