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/utils/formatters.dart'; import '../../../core/widgets/status_pill.dart'; import '../../../domain/entities/customer.dart'; import '../../customer/widgets/customer_capture_sheet.dart'; import '../widgets/module_widgets.dart'; /// Full customer book, independent of the six shown during billing. final allCustomersProvider = FutureProvider>( (ref) => ref.watch(customerRepositoryProvider).recent(limit: 200), ); class CustomersView extends ConsumerStatefulWidget { const CustomersView({super.key}); @override ConsumerState createState() => _CustomersViewState(); } class _CustomersViewState extends ConsumerState { String _query = ''; MembershipTier? _tier; Color _tierColor(MembershipTier t) => switch (t) { MembershipTier.bronze => AppColors.tierBronze, MembershipTier.silver => AppColors.tierSilver, MembershipTier.gold => AppColors.tierGold, MembershipTier.platinum => AppColors.tierPlatinum, }; @override Widget build(BuildContext context) { final all = ref.watch(allCustomersProvider).value ?? const []; final filtered = all.where((c) { final q = _query.trim().toLowerCase(); final matchesQuery = q.isEmpty || c.name.toLowerCase().contains(q) || c.mobile.contains(q); return matchesQuery && (_tier == null || c.tier == _tier); }).toList(); final lifetime = all.fold(0, (s, c) => s + c.lifetimeSpend); final points = all.fold(0, (s, c) => s + c.loyaltyPoints); return ModulePage( children: [ Wrap( spacing: AppSpacing.lg, runSpacing: AppSpacing.lg, children: [ StatTile( label: 'Total Customers', value: '${all.length}', icon: Icons.people_alt_rounded, caption: 'registered', ), StatTile( label: 'Lifetime Value', value: Formatters.moneyCompact(lifetime), icon: Icons.payments_rounded, color: AppColors.success, caption: 'all customers', ), StatTile( label: 'Points Outstanding', value: '$points', icon: Icons.stars_rounded, color: AppColors.tierGold, caption: 'worth ${Formatters.money(points * 0.25)}', ), StatTile( label: 'Avg Spend', value: Formatters.money(all.isEmpty ? 0 : lifetime / all.length), icon: Icons.trending_up_rounded, color: AppColors.info, caption: 'per customer', ), ], ), const SizedBox(height: AppSpacing.lg), PanelCard( title: 'Tier distribution', child: Column( mainAxisSize: MainAxisSize.min, children: [ for (final t in MembershipTier.values) ProgressRow( label: '${t.label} · ' '${(t.discountRate * 100).toStringAsFixed(0)}% off', value: '${all.where((c) => c.tier == t).length}', fraction: all.isEmpty ? 0 : all.where((c) => c.tier == t).length / all.length, color: _tierColor(t), ), ], ), ), const SizedBox(height: AppSpacing.lg), PanelCard( title: 'Customer book', subtitle: '${filtered.length} shown', action: FilledButton.icon( // Reuses the sheet the till already opens at checkout, so a // customer added here is validated the same way — including the // duplicate-mobile guard. onPressed: () async { await showCustomerCaptureSheet(context); ref.invalidate(allCustomersProvider); }, icon: const Icon(Icons.person_add_alt_1_rounded, size: 17), label: const Text('Add customer'), style: FilledButton.styleFrom( backgroundColor: AppColors.primary, minimumSize: const Size(0, 40), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( onChanged: (v) => setState(() => _query = v), decoration: const InputDecoration( hintText: 'Search by name or mobile number…', prefixIcon: Icon(Icons.search_rounded), isDense: true, ), ), const SizedBox(height: AppSpacing.md), Wrap( spacing: AppSpacing.sm, runSpacing: AppSpacing.sm, children: [ ChoiceChip( label: const Text('All tiers'), selected: _tier == null, onSelected: (_) => setState(() => _tier = null), labelStyle: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w600, color: _tier == null ? Colors.white : AppColors.textSecondary, ), ), for (final t in MembershipTier.values) ChoiceChip( label: Text(t.label), selected: _tier == t, onSelected: (_) => setState(() => _tier = _tier == t ? null : t), labelStyle: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w600, color: _tier == t ? Colors.white : AppColors.textSecondary, ), ), ], ), const SizedBox(height: AppSpacing.lg), ResponsiveTable( columns: const [ TableCol('Customer', flex: 4), TableCol('Mobile', flex: 3, priority: 1), TableCol('Tier', flex: 2), TableCol('Points', flex: 2, numeric: true, priority: 1), TableCol('Lifetime', flex: 2, numeric: true), TableCol('Visits', flex: 2, numeric: true, priority: 1), ], rows: filtered .map((c) => [ Row( mainAxisSize: MainAxisSize.min, children: [ CircleAvatar( radius: 14, backgroundColor: AppColors.primarySurface, child: Text( Formatters.initials(c.name), style: const TextStyle( fontSize: 10.5, fontWeight: FontWeight.w700, color: AppColors.primary, ), ), ), const SizedBox(width: AppSpacing.sm), Flexible(child: Cell(c.name, bold: true)), ], ), Cell(Formatters.mobile(c.mobile), mono: true), StatusPill.tier(c.tier, dense: true), Cell('${c.loyaltyPoints}', mono: true), Cell(Formatters.moneyCompact(c.lifetimeSpend), mono: true, bold: true,), Cell('${c.visitCount}', mono: true), ],) .toList(), ), ], ), ), ], ); } }