Files
nearle_pos/lib/presentation/modules/screens/customers_view.dart

152 lines
5.6 KiB
Dart

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 '../../../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<List<Customer>>(
(ref) => ref.watch(customerRepositoryProvider).recent(limit: 200),
);
class CustomersView extends ConsumerStatefulWidget {
const CustomersView({super.key});
@override
ConsumerState<CustomersView> createState() => _CustomersViewState();
}
class _CustomersViewState extends ConsumerState<CustomersView> {
String _query = '';
@override
Widget build(BuildContext context) {
final all = ref.watch(allCustomersProvider).value ?? const <Customer>[];
final filtered = all.where((c) {
final q = _query.trim().toLowerCase();
return q.isEmpty ||
c.name.toLowerCase().contains(q) ||
c.mobile.contains(q);
}).toList();
final lifetime = all.fold<double>(0, (s, c) => s + c.lifetimeSpend);
final points = all.fold<int>(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: '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.lg),
ResponsiveTable(
columns: const [
TableCol('Customer', flex: 4),
TableCol('Mobile', flex: 3, priority: 1),
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),
Cell('${c.loyaltyPoints}', mono: true),
Cell(Formatters.moneyCompact(c.lifetimeSpend),
mono: true, bold: true,),
Cell('${c.visitCount}', mono: true),
],)
.toList(),
),
],
),
),
],
);
}
}