import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_typography.dart'; import '../../../core/utils/formatters.dart'; import '../../../domain/entities/cart.dart'; /// One row of the bill, with inline quantity stepper. class CartLineTile extends StatelessWidget { const CartLineTile({ super.key, required this.line, required this.onIncrement, required this.onDecrement, required this.onRemove, }); final CartLine line; final VoidCallback onIncrement; final VoidCallback onDecrement; final VoidCallback onRemove; @override Widget build(BuildContext context) { final p = line.product; return Dismissible( key: ValueKey('dismiss_${p.id}'), direction: DismissDirection.endToStart, // Confirm rather than dismiss: [onRemove] opens the PIN dialog, and a // refused PIN must leave the line exactly where it was. Dismissing first // and asking after left the row gone from the screen but still in the // cart — and Flutter asserting about a dismissed widget still in the // tree. Returning false always is correct: when the PIN is accepted the // line disappears because the cart changed, not because of the swipe. confirmDismiss: (_) async { onRemove(); return false; }, background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: AppSpacing.xl), decoration: const BoxDecoration( color: AppColors.dangerSurface, borderRadius: AppRadius.brMd, ), child: const Icon(Icons.delete_outline_rounded, color: AppColors.danger,), ), child: Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.surfaceAlt, borderRadius: AppRadius.brMd, border: Border.all( color: line.exceedsStock ? AppColors.danger : AppColors.border, ), ), child: Column(children: [ Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: AppColors.surface, borderRadius: AppRadius.brSm, border: Border.all(color: AppColors.border), ), alignment: Alignment.center, child: Text(p.emoji, style: const TextStyle(fontSize: 21)), ), const SizedBox(width: AppSpacing.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( p.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 14.5, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Row(children: [ Text( Formatters.money(p.price), style: AppTypography.money(13.5, weight: FontWeight.w600, color: AppColors.textSecondary,), ), Text(' / ${p.unit.symbol}', style: const TextStyle( fontSize: 11.5, color: AppColors.textTertiary, ),), if (line.discount.isActive) ...[ const SizedBox(width: AppSpacing.sm), Container( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 1,), decoration: BoxDecoration( color: AppColors.successSurface, borderRadius: BorderRadius.circular(4), ), child: Text( line.discount.label, style: const TextStyle( fontSize: 10, color: AppColors.success, fontWeight: FontWeight.w700, ), ), ), ], ],), ], ), ), IconButton( onPressed: onRemove, icon: const Icon(Icons.close_rounded, size: 18), color: AppColors.textTertiary, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), padding: EdgeInsets.zero, tooltip: 'Remove from bill (needs the removal PIN)', ), ],), const SizedBox(height: AppSpacing.sm), Row(children: [ _Stepper( quantity: line.quantity, unit: p.unit.symbol, onIncrement: onIncrement, onDecrement: onDecrement, ), const Spacer(), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ if (line.discountAmount > 0) Text( Formatters.money(line.grossAmount), style: const TextStyle( fontSize: 11.5, color: AppColors.textTertiary, decoration: TextDecoration.lineThrough, ), ), Text( Formatters.money(line.payable), style: AppTypography.money(16), ), ], ), ],), if (line.exceedsStock) Padding( padding: const EdgeInsets.only(top: AppSpacing.sm), child: Row(children: [ const Icon(Icons.error_outline_rounded, size: 14, color: AppColors.danger,), const SizedBox(width: AppSpacing.xs), Text( 'Only ${p.stock.toStringAsFixed(0)} ${p.unit.symbol} ' 'available', style: const TextStyle( fontSize: 11.5, color: AppColors.danger, fontWeight: FontWeight.w600, ), ), ],), ), ],), ), ); } } class _Stepper extends StatelessWidget { const _Stepper({ required this.quantity, required this.unit, required this.onIncrement, required this.onDecrement, }); final double quantity; final String unit; final VoidCallback onIncrement; final VoidCallback onDecrement; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: AppRadius.brSm, border: Border.all(color: AppColors.border), ), // Minus stops at one rather than emptying the line. Dropping to zero // was a silent removal that skipped the PIN the close button asks for — // two taps of a stepper should not be a way around the till's only // theft control. child: Row(mainAxisSize: MainAxisSize.min, children: [ _btn(Icons.remove_rounded, quantity > 1 ? onDecrement : null), Container( constraints: const BoxConstraints(minWidth: 42), alignment: Alignment.center, child: Text( quantity % 1 == 0 ? quantity.toStringAsFixed(0) : quantity.toStringAsFixed(2), style: AppTypography.money(15.5), ), ), _btn(Icons.add_rounded, onIncrement), ],), ); } Widget _btn(IconData icon, VoidCallback? onTap) => Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: AppRadius.brSm, child: SizedBox( width: 34, height: 34, child: Icon( icon, size: 17, color: onTap == null ? AppColors.textTertiary.withValues(alpha: 0.5) : AppColors.primary, ), ), ), ); }