added product import and billing integration

This commit is contained in:
2026-08-05 18:15:21 +05:30
parent 33b4337933
commit 6c0266c9c7
32 changed files with 889 additions and 457 deletions

View File

@@ -5,7 +5,6 @@ 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';
@@ -24,14 +23,6 @@ class CustomersView extends ConsumerStatefulWidget {
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) {
@@ -39,10 +30,9 @@ class _CustomersViewState extends ConsumerState<CustomersView> {
final filtered = all.where((c) {
final q = _query.trim().toLowerCase();
final matchesQuery = q.isEmpty ||
return 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);
@@ -85,26 +75,6 @@ class _CustomersViewState extends ConsumerState<CustomersView> {
),
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',
@@ -134,45 +104,11 @@ class _CustomersViewState extends ConsumerState<CustomersView> {
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),
@@ -199,7 +135,6 @@ class _CustomersViewState extends ConsumerState<CustomersView> {
],
),
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,),

View File

@@ -1,10 +1,12 @@
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/primary_button.dart';
import '../../../data/sync/sync_engine.dart';
import '../../../domain/entities/transaction.dart';
import '../../../domain/repositories/sync_repository.dart';
import '../../sync/providers/sync_controller.dart';
@@ -26,10 +28,21 @@ class EventsView extends ConsumerWidget {
final syncState = ref.watch(orderSyncProvider);
final events = ref.watch(syncEventsProvider);
// The engine may not have emitted yet on a cold start, so fall back to
// its current value rather than showing nothing. This is the same state
// that drives the header pill — it is what actually knows whether the
// automatic push right after a sale succeeded, not just what the manual
// "Sync" button on this page last did.
final engine = ref.watch(syncEngineStateProvider).value ??
ref.watch(syncEngineProvider).state;
final r = report.value;
return ModulePage(
children: [
if (engine.lastError != null && pending > 0)
_EngineWarningBanner(engine: engine),
Wrap(
spacing: AppSpacing.lg,
runSpacing: AppSpacing.lg,
@@ -310,3 +323,81 @@ class EventsView extends ConsumerWidget {
),
);
}
/// Flags a bill that could not reach the server on its own — the automatic
/// push right after checkout, not the manual "Sync" button below.
///
/// Sits above everything else on the page because a bill stuck here is the
/// one thing on this screen that needs a person to notice it, rather than
/// just waiting for the next background retry.
class _EngineWarningBanner extends StatelessWidget {
const _EngineWarningBanner({required this.engine});
final SyncEngineState engine;
@override
Widget build(BuildContext context) {
final halted = engine.isHalted;
final color = halted ? AppColors.danger : AppColors.warning;
final surface = halted ? AppColors.dangerSurface : AppColors.warningSurface;
final retry = engine.nextAttemptAt;
final retryNote = halted
? 'Retrying will not help until this is fixed — press Sync below '
'once it is sorted.'
: retry == null
? 'It will retry automatically, or press Sync below to try now.'
: 'It will retry automatically at ${Formatters.time(retry)}, or '
'press Sync below to try now.';
return Container(
margin: const EdgeInsets.only(bottom: AppSpacing.lg),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(color: surface, borderRadius: AppRadius.brLg),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.wifi_off_rounded, size: 20, color: color),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
halted
? 'Sync halted — ${engine.pending} bill(s) not sent'
: "Couldn't reach the server — "
'${engine.pending} bill(s) not sent',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: color,
),
),
const SizedBox(height: 2),
Text(
engine.lastError ?? 'The last upload attempt failed.',
style: TextStyle(
fontSize: 12.5,
color: color,
height: 1.45,
),
),
const SizedBox(height: 4),
Text(
'$retryNote Every bill is still safe on this terminal — '
'nothing is lost while it waits.',
style: const TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
height: 1.45,
),
),
],
),
),
],
),
);
}
}