Files
nearle_pos/lib/presentation/pos/widgets/scan_toast.dart
2026-07-29 11:41:53 +05:30

122 lines
3.6 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../providers/cart_controller.dart';
/// Brief floating confirmation after a scan.
///
/// Deliberately not a dialog: the cashier must never have to dismiss anything
/// between items.
class ScanToast extends ConsumerStatefulWidget {
const ScanToast({super.key});
@override
ConsumerState<ScanToast> createState() => _ScanToastState();
}
class _ScanToastState extends ConsumerState<ScanToast> {
Timer? _timer;
ScanFeedback? _visible;
void _show(ScanFeedback feedback) {
_timer?.cancel();
setState(() => _visible = feedback);
_timer = Timer(
const Duration(milliseconds: 1600),
() {
if (mounted) setState(() => _visible = null);
},
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
ref.listen<ScanFeedback?>(scanFeedbackProvider, (prev, next) {
if (next != null) _show(next);
});
final feedback = _visible;
if (feedback == null) return const SizedBox.shrink();
final success = feedback.isSuccess;
final color = success ? AppColors.success : AppColors.danger;
final product = feedback.product;
final message = feedback.message ??
switch (feedback.outcome) {
ScanOutcome.added => 'Added to bill',
ScanOutcome.incremented => 'Quantity updated',
ScanOutcome.notFound => 'Product not found',
ScanOutcome.outOfStock => 'Out of stock',
};
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.xxl),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl,
vertical: AppSpacing.md,
),
decoration: BoxDecoration(
color: AppColors.textPrimary,
borderRadius: AppRadius.brPill,
boxShadow: AppColors.shadowLg,
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Container(
width: 26,
height: 26,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
child: Icon(
success ? Icons.check_rounded : Icons.priority_high_rounded,
size: 17,
color: Colors.white,
),
),
const SizedBox(width: AppSpacing.md),
if (product != null) ...[
Text(product.emoji, style: const TextStyle(fontSize: 17)),
const SizedBox(width: AppSpacing.sm),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 220),
child: Text(
product.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 14.5,
),
),
),
const SizedBox(width: AppSpacing.sm),
Container(width: 1, height: 16, color: Colors.white24),
const SizedBox(width: AppSpacing.sm),
],
Text(
message,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.85),
fontSize: 13.5,
),
),
]),
)
.animate(key: ValueKey(feedback.stamp))
.fadeIn(duration: 140.ms)
.slideY(begin: 0.4, end: 0, curve: Curves.easeOutBack)
.then(delay: 1200.ms)
.fadeOut(duration: 250.ms);
}
}