import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/router/app_router.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/utils/extensions.dart'; import '../../../core/utils/formatters.dart'; import '../../../core/widgets/empty_state.dart'; import '../../../core/widgets/glass_card.dart'; import '../../../core/widgets/numeric_keypad.dart'; import '../../../core/widgets/primary_button.dart'; import '../../../core/widgets/status_pill.dart'; import '../../../domain/entities/customer.dart'; import '../../pos/providers/cart_controller.dart'; import '../providers/customer_providers.dart'; /// Screen 3 — mobile lookup that auto-searches on the tenth digit. class ExistingCustomerScreen extends ConsumerStatefulWidget { const ExistingCustomerScreen({super.key}); @override ConsumerState createState() => _ExistingCustomerScreenState(); } class _ExistingCustomerScreenState extends ConsumerState { String _digits = ''; void _append(String d) { if (_digits.length >= AppConstants.mobileNumberLength) return; setState(() => _digits += d); if (_digits.length == AppConstants.mobileNumberLength) _search(); } void _backspace() { if (_digits.isEmpty) return; setState(() => _digits = _digits.substring(0, _digits.length - 1)); ref.read(customerLookupProvider.notifier).reset(); } void _clear() { setState(() => _digits = ''); ref.read(customerLookupProvider.notifier).reset(); } void _search() => ref.read(customerLookupProvider.notifier).search(_digits); void _continueWith(Customer customer) { ref.read(cartControllerProvider.notifier).attachCustomer(customer); context.go(AppRoutes.pos); } @override Widget build(BuildContext context) { final lookup = ref.watch(customerLookupProvider); return Scaffold( backgroundColor: AppColors.background, appBar: AppBar( title: const Text('Find Customer'), leading: IconButton( icon: const Icon(Icons.arrow_back_rounded), onPressed: () => context.pop(), ), actions: [ TextButton.icon( onPressed: () { ref.read(cartControllerProvider.notifier).attachCustomer(null); context.go(AppRoutes.pos); }, icon: const Icon(Icons.directions_walk_rounded, color: Colors.white, size: 18), label: const Text('Continue as Walk-in', style: TextStyle(color: Colors.white)), ), const SizedBox(width: AppSpacing.lg), ], ), body: Padding( padding: const EdgeInsets.all(AppSpacing.xxl), child: context.isCompact ? SingleChildScrollView( child: Column(children: [ _entryPanel(), const SizedBox(height: AppSpacing.xxl), // Bound the height: the panel uses Expanded/Spacer // internally, which a scroll view cannot supply. SizedBox(height: 480, child: _resultPanel(lookup)), ]), ) : Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded(flex: 4, child: _entryPanel()), const SizedBox(width: AppSpacing.xxl), Expanded(flex: 5, child: _resultPanel(lookup)), ], ), ), ); } Widget _entryPanel() { return GlassCard( padding: const EdgeInsets.all(AppSpacing.xxl), radius: AppRadius.xl, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('Mobile number', style: context.text.labelMedium), const SizedBox(height: AppSpacing.md), _display(), const SizedBox(height: AppSpacing.xxl), Center( child: NumericKeypad( onKey: _append, onBackspace: _backspace, onClear: _clear, onSubmit: _digits.length == AppConstants.mobileNumberLength ? _search : null, submitLabel: 'Search', ), ), ], ), ); } /// Ten slots so the cashier can see progress at a glance. Widget _display() { return Container( height: 72, padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), decoration: BoxDecoration( color: AppColors.primarySurface, borderRadius: AppRadius.brLg, border: Border.all(color: AppColors.primaryBorder), ), child: Row(children: [ const Text('+91', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, color: AppColors.textSecondary, )), const SizedBox(width: AppSpacing.md), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate( AppConstants.mobileNumberLength, (i) { final filled = i < _digits.length; return AnimatedContainer( duration: AppMotion.fast, width: 22, alignment: Alignment.center, child: Text( filled ? _digits[i] : '–', style: TextStyle( fontSize: 24, fontWeight: FontWeight.w700, color: filled ? AppColors.textPrimary : AppColors.textTertiary.withValues(alpha: 0.5), ), ), ); }, ), ), ), if (_digits.isNotEmpty) IconButton( onPressed: _clear, icon: const Icon(Icons.close_rounded, size: 20), color: AppColors.textTertiary, tooltip: 'Clear', ), ]), ); } Widget _resultPanel(CustomerLookupState state) { return GlassCard( padding: const EdgeInsets.all(AppSpacing.xxl), radius: AppRadius.xl, child: switch (state) { LookupIdle() => _idle(), LookupSearching() => const Center( child: CircularProgressIndicator(color: AppColors.primary), ), LookupFound(:final customer) => _found(customer), LookupNotFound(:final mobile) => _notFound(mobile), LookupError(:final message) => EmptyState( title: 'Something went wrong', message: message, emoji: '⚠️', ), }, ); } Widget _idle() { final recent = ref.watch(recentCustomersProvider); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('Recent customers', style: context.text.titleMedium), const SizedBox(height: AppSpacing.xs), Text('Tap to select, or key in a mobile number.', style: context.text.bodySmall), const SizedBox(height: AppSpacing.lg), Expanded( child: recent.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => EmptyState( title: 'Could not load customers', message: '$e', emoji: '⚠️', compact: true, ), data: (customers) => customers.isEmpty ? const EmptyState( title: 'No customers yet', message: 'Register the first one from the welcome screen.', emoji: '👤', compact: true, ) : ListView.separated( itemCount: customers.length, separatorBuilder: (_, __) => const SizedBox(height: AppSpacing.sm), itemBuilder: (_, i) => _RecentTile( customer: customers[i], onTap: () => _continueWith(customers[i]), ), ), ), ), ], ); } Widget _found(Customer c) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row(children: [ CircleAvatar( radius: 30, backgroundColor: AppColors.primarySurface, child: Text( Formatters.initials(c.name), style: const TextStyle( color: AppColors.primary, fontSize: 22, fontWeight: FontWeight.w700, ), ), ), const SizedBox(width: AppSpacing.lg), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Flexible( child: Text(c.name, style: context.text.headlineSmall, overflow: TextOverflow.ellipsis), ), const SizedBox(width: AppSpacing.sm), StatusPill.tier(c.tier), ]), const SizedBox(height: 2), Text('+91 ${Formatters.mobile(c.mobile)}', style: context.text.bodyMedium ?.copyWith(color: AppColors.textSecondary)), ], ), ), ]), if (c.isBirthdayToday) ...[ const SizedBox(height: AppSpacing.lg), Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.warningSurface, borderRadius: AppRadius.brSm, ), child: const Row(children: [ Text('🎂', style: TextStyle(fontSize: 18)), SizedBox(width: AppSpacing.sm), Text("It's their birthday today — wish them!", style: TextStyle( color: AppColors.warning, fontWeight: FontWeight.w600, )), ]), ), ], const SizedBox(height: AppSpacing.xxl), Row(children: [ Expanded( child: _Stat( label: 'Loyalty Points', value: '${c.loyaltyPoints}', caption: 'Worth ${Formatters.money(c.redeemableValue)}', icon: Icons.stars_rounded, color: AppColors.tierGold, ), ), const SizedBox(width: AppSpacing.md), Expanded( child: _Stat( label: 'Lifetime Spend', value: Formatters.moneyCompact(c.lifetimeSpend), caption: '${c.visitCount} visits', icon: Icons.receipt_long_rounded, color: AppColors.primary, ), ), ]), if (c.tier.discountRate > 0) ...[ const SizedBox(height: AppSpacing.md), Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.successSurface, borderRadius: AppRadius.brSm, ), child: Row(children: [ const Icon(Icons.local_offer_rounded, color: AppColors.success, size: 18), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( '${c.tier.label} members get ' '${Formatters.percent(c.tier.discountRate)} off ' 'automatically on every bill.', style: const TextStyle( color: AppColors.success, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), ]), ), ], const Spacer(), PrimaryButton( label: 'Continue to Billing', icon: Icons.point_of_sale_rounded, large: true, onPressed: () => _continueWith(c), ), ], ).animate().fadeIn(duration: 200.ms); } Widget _notFound(String mobile) { return Column( children: [ Expanded( child: EmptyState( title: 'No customer found', message: 'Nobody is registered against ' '+91 ${Formatters.mobile(mobile)}.', emoji: '🔍', ), ), PrimaryButton( label: 'Register Customer', icon: Icons.person_add_alt_1_rounded, onPressed: () => context.push( '${AppRoutes.registerCustomer}?mobile=$mobile', ), ), const SizedBox(height: AppSpacing.md), PrimaryButton( label: 'Continue as Walk-in', icon: Icons.directions_walk_rounded, tone: ButtonTone.neutral, onPressed: () { ref.read(cartControllerProvider.notifier).attachCustomer(null); context.go(AppRoutes.pos); }, ), ], ).animate().fadeIn(duration: 200.ms); } } class _RecentTile extends StatelessWidget { const _RecentTile({required this.customer, required this.onTap}); final Customer customer; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( color: AppColors.surfaceAlt, borderRadius: AppRadius.brMd, child: InkWell( onTap: onTap, borderRadius: AppRadius.brMd, child: Padding( padding: const EdgeInsets.all(AppSpacing.md), child: Row(children: [ CircleAvatar( radius: 18, backgroundColor: AppColors.primarySurface, child: Text( Formatters.initials(customer.name), style: const TextStyle( color: AppColors.primary, fontSize: 12, fontWeight: FontWeight.w700, ), ), ), const SizedBox(width: AppSpacing.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(customer.name, style: context.text.titleSmall, overflow: TextOverflow.ellipsis), Text(Formatters.maskedMobile(customer.mobile), style: context.text.bodySmall), ], ), ), StatusPill.tier(customer.tier, dense: true), const SizedBox(width: AppSpacing.sm), const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary), ]), ), ), ); } } class _Stat extends StatelessWidget { const _Stat({ required this.label, required this.value, required this.caption, required this.icon, required this.color, }); final String label; final String value; final String caption; final IconData icon; final Color color; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: AppColors.surfaceAlt, borderRadius: AppRadius.brMd, border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(icon, size: 16, color: color), const SizedBox(width: AppSpacing.xs), Text(label, style: context.text.labelSmall ?.copyWith(color: AppColors.textSecondary)), ]), const SizedBox(height: AppSpacing.sm), Text(value, style: context.text.headlineSmall?.copyWith(color: color)), Text(caption, style: context.text.bodySmall), ], ), ); } }