import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/router/app_router.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_typography.dart'; import '../../../core/utils/extensions.dart'; import '../../../core/utils/formatters.dart'; import '../../../core/widgets/empty_state.dart'; import '../../../core/widgets/primary_button.dart'; import '../../../domain/entities/cart.dart'; import '../../customer/widgets/customer_capture_sheet.dart'; import '../providers/cart_controller.dart'; import 'cart_line_tile.dart'; import 'discount_sheet.dart'; /// Always-visible bill on the right of the dashboard. class BillingPanel extends ConsumerWidget { const BillingPanel({super.key, this.inSheet = false}); final bool inSheet; @override Widget build(BuildContext context, WidgetRef ref) { final cart = ref.watch(cartControllerProvider); final controller = ref.read(cartControllerProvider.notifier); return Container( color: AppColors.surface, child: Column(children: [ _Header(cart: cart, inSheet: inSheet), const Divider(height: 1), Expanded( child: cart.isEmpty ? const EmptyState( title: 'Cart is empty', message: 'Scan a barcode or tap a product to begin.', emoji: '🛒', compact: true, ) : ListView.separated( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.lg, vertical: AppSpacing.md, ), itemCount: cart.lines.length, separatorBuilder: (_, __) => const SizedBox(height: AppSpacing.sm), itemBuilder: (_, i) { // Newest line first mirrors what the cashier just scanned. final line = cart.lines[cart.lines.length - 1 - i]; return CartLineTile( key: ValueKey(line.product.id), line: line, onIncrement: () => controller.increment(line.product.id), onDecrement: () => controller.decrement(line.product.id), onRemove: () => controller.removeLine(line.product.id), onDiscount: () => showLineDiscountSheet(context, ref, line), ); }, ), ), if (cart.isNotEmpty) _Summary(cart: cart), _Actions(cart: cart), ],), ); } } class _Header extends ConsumerWidget { const _Header({required this.cart, required this.inSheet}); final Cart cart; final bool inSheet; @override Widget build(BuildContext context, WidgetRef ref) { final controller = ref.read(cartControllerProvider.notifier); return Padding( padding: const EdgeInsets.fromLTRB( AppSpacing.lg, AppSpacing.md, AppSpacing.sm, AppSpacing.md, ), child: Row( children: [ Flexible( child: Text( 'Cart', overflow: TextOverflow.ellipsis, style: context.text.titleLarge, ), ), if (cart.isNotEmpty) ...[ const SizedBox(width: AppSpacing.sm), Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: const BoxDecoration( color: AppColors.primarySurface, borderRadius: AppRadius.brPill, ), child: Text( '${cart.lineCount}', style: const TextStyle( color: AppColors.primary, fontWeight: FontWeight.w700, fontSize: 12.5, ), ), ), ], const Spacer(), // Icon-only actions: labelled buttons overflowed the 380px panel. if (controller.canUndo) _IconAction( icon: Icons.undo_rounded, tooltip: 'Undo (F8)', color: AppColors.textSecondary, onTap: controller.undo, ), if (cart.isNotEmpty) ...[ _IconAction( icon: Icons.pause_circle_outline_rounded, tooltip: 'Park bill', color: AppColors.warning, onTap: () async { await controller.park(); ref.invalidate(parkedBillsProvider); if (context.mounted) context.showSnack('Bill parked'); }, ), _IconAction( icon: Icons.delete_outline_rounded, tooltip: 'Clear bill', color: AppColors.danger, onTap: controller.clear, ), ], if (inSheet) _IconAction( icon: Icons.close_rounded, tooltip: 'Close', color: AppColors.textSecondary, onTap: () => Navigator.of(context).pop(), ), ], ), ); } } /// Compact square action used in the bill header. class _IconAction extends StatelessWidget { const _IconAction({ required this.icon, required this.tooltip, required this.color, required this.onTap, }); final IconData icon; final String tooltip; final Color color; final VoidCallback onTap; @override Widget build(BuildContext context) { return Tooltip( message: tooltip, child: IconButton( onPressed: onTap, icon: Icon(icon, size: 19), color: color, visualDensity: VisualDensity.compact, constraints: const BoxConstraints(minWidth: 36, minHeight: 36), padding: EdgeInsets.zero, ), ); } } class _Summary extends ConsumerWidget { const _Summary({required this.cart}); final Cart cart; @override Widget build(BuildContext context, WidgetRef ref) { final controller = ref.read(cartControllerProvider.notifier); return Container( padding: const EdgeInsets.fromLTRB( AppSpacing.xl, AppSpacing.lg, AppSpacing.xl, AppSpacing.md, ), decoration: const BoxDecoration( border: Border(top: BorderSide(color: AppColors.divider)), ), child: Column(children: [ if (cart.pointsEarned > 0) Container( width: double.infinity, margin: const EdgeInsets.only(bottom: AppSpacing.md), padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm + 2, ), decoration: const BoxDecoration( color: AppColors.successSurface, borderRadius: AppRadius.brSm, ), child: Row(children: [ const Icon(Icons.stars_rounded, size: 16, color: AppColors.success,), const SizedBox(width: AppSpacing.sm), Text( 'This sale earns +${cart.pointsEarned} pts', style: const TextStyle( color: AppColors.success, fontWeight: FontWeight.w700, fontSize: 13, ), ), ],), ), _Row(label: 'Subtotal', value: Formatters.money(cart.subtotal)), if (cart.membershipDiscountAmount > 0) _Row( label: '${cart.customer!.tier.label} discount', value: '-${Formatters.money(cart.membershipDiscountAmount)}', valueColor: AppColors.success, ), _Row( label: 'GST', value: Formatters.money(cart.taxAmount), hint: cart.taxBreakdown.keys.isEmpty ? null : cart.taxBreakdown.keys .map((r) => '${(r * 100).toStringAsFixed(0)}%') .join(', '), ), InkWell( onTap: () => showBillDiscountSheet(context, ref), borderRadius: AppRadius.brXs, child: _Row( label: 'Discount', value: cart.manualBillDiscountAmount > 0 ? '-${Formatters.money(cart.manualBillDiscountAmount)}' : '-${Formatters.money(0)}', valueColor: cart.manualBillDiscountAmount > 0 ? AppColors.success : null, trailingIcon: Icons.edit_outlined, ), ), if (cart.maxRedeemablePoints > 0 || cart.pointsRedeemed > 0) InkWell( onTap: () => cart.pointsRedeemed > 0 ? controller.clearRedemption() : controller.redeemAllPoints(), borderRadius: AppRadius.brXs, child: _Row( label: cart.pointsRedeemed > 0 ? 'Points redeemed (${cart.pointsRedeemed})' : 'Redeem ${cart.maxRedeemablePoints} points', value: cart.pointsRedeemed > 0 ? '-${Formatters.money(cart.loyaltyRedemptionValue)}' : 'Apply', valueColor: AppColors.primary, trailingIcon: cart.pointsRedeemed > 0 ? Icons.close_rounded : Icons.add_rounded, ), ), if (cart.roundOff != 0) _Row( label: 'Round Off', value: '${cart.roundOff >= 0 ? '+' : ''}' '${Formatters.money(cart.roundOff)}', ), const Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.md), child: Divider(height: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Total', style: context.text.titleLarge), Text( Formatters.money(cart.grandTotal), style: AppTypography.money(26, color: AppColors.primary), ), ], ), if (cart.totalSavings > 0) Padding( padding: const EdgeInsets.only(top: AppSpacing.xs), child: Align( alignment: Alignment.centerRight, child: Text( 'You saved ${Formatters.money(cart.totalSavings)}', style: const TextStyle( color: AppColors.success, fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ), ],), ); } } class _Row extends StatelessWidget { const _Row({ required this.label, required this.value, this.valueColor, this.hint, this.trailingIcon, }); final String label; final String value; final Color? valueColor; final String? hint; final IconData? trailingIcon; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs + 2), child: Row(children: [ Flexible( child: Text( label, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 14, color: AppColors.textSecondary, ), ), ), if (hint != null) ...[ const SizedBox(width: AppSpacing.xs), Text('($hint)', style: const TextStyle( fontSize: 11.5, color: AppColors.textTertiary, ),), ], const SizedBox(width: AppSpacing.sm), const Spacer(), Text( value, style: AppTypography.money( 14.5, weight: FontWeight.w600, color: valueColor ?? AppColors.textPrimary, ), ), if (trailingIcon != null) ...[ const SizedBox(width: AppSpacing.xs), Icon(trailingIcon, size: 14, color: AppColors.textTertiary), ], ],), ); } } class _Actions extends ConsumerWidget { const _Actions({required this.cart}); final Cart cart; @override Widget build(BuildContext context, WidgetRef ref) { final enabled = cart.isNotEmpty; return Container( padding: const EdgeInsets.fromLTRB( AppSpacing.xl, AppSpacing.sm, AppSpacing.xl, AppSpacing.xl, ), child: PrimaryButton( label: 'CHARGE', large: true, onPressed: enabled ? () async { // Ask once per bill, before payment. Skipping is one tap and // leaves the sale as walk-in. if (ref.read(cartControllerProvider).customer == null) { await showCustomerCaptureSheet(context); } // Navigation result is not needed here. if (context.mounted) unawaited(context.push(AppRoutes.payment)); } : null, trailing: enabled ? Text( Formatters.money(cart.grandTotal), style: AppTypography.money(21, color: Colors.white), ) : null, ), ); } }