pos changes

This commit is contained in:
2026-08-06 19:26:53 +05:30
parent cb065a0f69
commit eebd10da6d
42 changed files with 3451 additions and 2531 deletions

View File

@@ -18,7 +18,6 @@ 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';
/// Always-visible bill on the right of the dashboard.
class BillingPanel extends ConsumerWidget {
@@ -66,8 +65,6 @@ class BillingPanel extends ConsumerWidget {
controller,
line.product.id,
),
onDiscount: () =>
showLineDiscountSheet(context, ref, line),
);
},
),
@@ -79,37 +76,24 @@ 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.
/// Once an item is on the bill, taking it back off needs the removal PIN an
/// admin sets in Settings — a cashier can always start an entirely new sale
/// instead. This is the one gate every removal path goes through, so the
/// close button and the swipe can never drift apart.
Future<void> _removeLine(
BuildContext context,
WidgetRef ref,
CartController controller,
String productId,
) async {
final ok = await requireAdminPin(
final ok = await requireVoidPin(
context,
ref,
reason: 'Removing a scanned item from the bill needs admin approval.',
reason: 'Removing a scanned item from the bill needs the removal PIN.',
);
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});
@@ -118,7 +102,6 @@ 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;
@@ -127,8 +110,9 @@ class _Header extends ConsumerWidget {
constraints: const BoxConstraints(minHeight: AppSizes.headerHeight),
padding: EdgeInsets.symmetric(horizontal: contentPadding),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Title and count read as one label, so they sit together rather
// than being pushed to opposite ends by a space-between row.
Flexible(
child: Text(
'Cart',
@@ -137,9 +121,12 @@ class _Header extends ConsumerWidget {
),
),
if (cart.isNotEmpty) ...[
const SizedBox(width: AppSpacing.sm),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 2,
),
decoration: const BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppRadius.brPill,
@@ -154,52 +141,21 @@ class _Header extends ConsumerWidget {
),
),
],
SizedBox(),
SizedBox(),
Spacer(),
const Spacer(),
// Icon-only actions: labelled buttons overflowed the 380px panel.
// 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(),
),
],
),
// Undo, Park and Clear used to sit here as three icon buttons. They
// are the least-pressed controls on the panel and they were the
// first thing the eye landed on, above the bill itself. Undo is on
// F8; Park and Clear moved down beside the total, next to the button
// a cashier is already reaching for.
if (inSheet)
_IconAction(
icon: Icons.close_rounded,
tooltip: 'Close',
color: AppColors.textSecondary,
onTap: () => Navigator.of(context).pop(),
),
],
),
);
@@ -313,21 +269,6 @@ class _Summary extends ConsumerWidget {
.join(', '),
),
InkWell(
onTap: () => showBillDiscountSheet(context, ref),
borderRadius: AppRadius.brXs,
child: _Row(
label: 'Discount',
value: cart.manualBillDiscountAmount > 0
? '-${Formatters.money(cart.manualBillDiscountAmount)}'
: '-${Formatters.money(0)}',
valueColor: cart.manualBillDiscountAmount > 0
? AppColors.success
: null,
trailingIcon: Icons.edit_outlined,
),
),
if (cart.maxRedeemablePoints > 0 || cart.pointsRedeemed > 0)
InkWell(
onTap: () => cart.pointsRedeemed > 0
@@ -466,26 +407,33 @@ class _Actions extends ConsumerWidget {
AppSpacing.xl,
AppSpacing.xl,
),
child: PrimaryButton(
label: 'CHARGE',
large: true,
onPressed: enabled
? () async {
// Ask once per bill, before payment. Skipping is one tap and
// leaves the sale as walk-in.
if (ref.read(cartControllerProvider).customer == null) {
await showCustomerCaptureSheet(context);
}
// Navigation result is not needed here.
if (context.mounted) unawaited(context.push(AppRoutes.payment));
}
: null,
trailing: enabled
? Text(
Formatters.money(cart.grandTotal),
style: AppTypography.money(21, color: Colors.white),
)
: null,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
PrimaryButton(
label: 'CHARGE',
large: true,
onPressed: enabled
? () async {
// Ask once per bill, before payment. Skipping is one tap
// and leaves the sale as walk-in.
if (ref.read(cartControllerProvider).customer == null) {
await showCustomerCaptureSheet(context);
}
// Navigation result is not needed here.
if (context.mounted) {
unawaited(context.push(AppRoutes.payment));
}
}
: null,
trailing: enabled
? Text(
Formatters.money(cart.grandTotal),
style: AppTypography.money(21, color: Colors.white),
)
: null,
),
],
),
);
}