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

@@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
import '../../../core/router/app_router.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/theme/app_layout.dart';
import '../../../core/theme/app_typography.dart';
import '../../../core/utils/extensions.dart';
import '../../../core/utils/formatters.dart';
@@ -15,6 +16,7 @@ import '../../../core/widgets/primary_button.dart';
import '../../../domain/entities/cart.dart';
import '../../customer/widgets/customer_capture_sheet.dart';
import '../providers/cart_controller.dart';
import 'admin_pin_dialog.dart';
import 'cart_line_tile.dart';
import 'discount_sheet.dart';
@@ -58,7 +60,12 @@ class BillingPanel extends ConsumerWidget {
line: line,
onIncrement: () => controller.increment(line.product.id),
onDecrement: () => controller.decrement(line.product.id),
onRemove: () => controller.removeLine(line.product.id),
onRemove: () => _removeLine(
context,
ref,
controller,
line.product.id,
),
onDiscount: () =>
showLineDiscountSheet(context, ref, line),
);
@@ -72,6 +79,37 @@ class BillingPanel extends ConsumerWidget {
}
}
/// Once an item is on the bill, taking it back off needs an admin's
/// approval — a cashier can always start an entirely new sale instead. This
/// is the one gate both removal paths (a single line, or the whole cart) go
/// through, so they can never drift out of sync with each other.
Future<void> _removeLine(
BuildContext context,
WidgetRef ref,
CartController controller,
String productId,
) async {
final ok = await requireAdminPin(
context,
ref,
reason: 'Removing a scanned item from the bill needs admin approval.',
);
if (ok) controller.removeLine(productId);
}
Future<void> _clearCart(
BuildContext context,
WidgetRef ref,
CartController controller,
) async {
final ok = await requireAdminPin(
context,
ref,
reason: 'Clearing the whole bill needs admin approval.',
);
if (ok) controller.clear();
}
class _Header extends ConsumerWidget {
const _Header({required this.cart, required this.inSheet});
@@ -81,15 +119,15 @@ class _Header extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = ref.read(cartControllerProvider.notifier);
// Same fixed height as the page header on the left, so the two bars
// line up on one visual line instead of the cart title floating lower.
final contentPadding = PosLayout.of(context).contentPadding;
return Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.lg,
AppSpacing.md,
AppSpacing.sm,
AppSpacing.md,
),
return Container(
constraints: const BoxConstraints(minHeight: AppSizes.headerHeight),
padding: EdgeInsets.symmetric(horizontal: contentPadding),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
@@ -99,9 +137,9 @@ class _Header extends ConsumerWidget {
),
),
if (cart.isNotEmpty) ...[
const SizedBox(width: AppSpacing.sm),
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: const BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppRadius.brPill,
@@ -116,40 +154,52 @@ class _Header extends ConsumerWidget {
),
),
],
const Spacer(),
SizedBox(),
SizedBox(),
Spacer(),
// Icon-only actions: labelled buttons overflowed the 380px panel.
if (controller.canUndo)
_IconAction(
icon: Icons.undo_rounded,
tooltip: 'Undo (F8)',
color: AppColors.textSecondary,
onTap: controller.undo,
),
if (cart.isNotEmpty) ...[
_IconAction(
icon: Icons.pause_circle_outline_rounded,
tooltip: 'Park bill',
color: AppColors.warning,
onTap: () async {
await controller.park();
ref.invalidate(parkedBillsProvider);
if (context.mounted) context.showSnack('Bill parked');
},
),
_IconAction(
icon: Icons.delete_outline_rounded,
tooltip: 'Clear bill',
color: AppColors.danger,
onTap: controller.clear,
),
],
if (inSheet)
_IconAction(
icon: Icons.close_rounded,
tooltip: 'Close',
color: AppColors.textSecondary,
onTap: () => Navigator.of(context).pop(),
),
// Grouped tight with even spacing, flush against the same right
// edge the header buttons on the left use.
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (controller.canUndo)
_IconAction(
icon: Icons.undo_rounded,
tooltip: 'Undo (F8)',
color: AppColors.textSecondary,
onTap: controller.undo,
),
if (cart.isNotEmpty) ...[
_IconAction(
icon: Icons.pause_circle_outline_rounded,
tooltip: 'Park bill',
color: AppColors.warning,
onTap: () async {
await controller.park();
ref.invalidate(parkedBillsProvider);
if (context.mounted) context.showSnack('Bill parked');
},
),
_IconAction(
icon: Icons.delete_outline_rounded,
tooltip: 'Clear bill',
color: AppColors.danger,
onTap: () => _clearCart(context, ref, controller),
),
],
if (inSheet)
_IconAction(
icon: Icons.close_rounded,
tooltip: 'Close',
color: AppColors.textSecondary,
onTap: () => Navigator.of(context).pop(),
),
],
),
],
),
);
@@ -360,7 +410,9 @@ class _Row extends StatelessWidget {
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs + 2),
child: Row(children: [
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
label,
@@ -390,7 +442,7 @@ class _Row extends StatelessWidget {
),
),
if (trailingIcon != null) ...[
const SizedBox(width: AppSpacing.xs),
Icon(trailingIcon, size: 14, color: AppColors.textTertiary),
],
],),