second commit

This commit is contained in:
2026-07-29 11:41:53 +05:30
parent fcccf22bac
commit d72522e737
211 changed files with 19260 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
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 '../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 = '';
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 <Customer>[];
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<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: '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(
onPressed: () {},
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(),
),
],
),
),
],
);
}
}