This commit is contained in:
Phuoc Nguyen
2025-10-10 16:38:07 +07:00
parent e5b247d622
commit b94c158004
177 changed files with 25080 additions and 152 deletions

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import '../../domain/entities/cart_item.dart';
import '../../../../shared/widgets/price_display.dart';
/// Cart item card widget
class CartItemCard extends StatelessWidget {
final CartItem item;
final VoidCallback? onRemove;
final Function(int)? onQuantityChanged;
const CartItemCard({
super.key,
required this.item,
this.onRemove,
this.onQuantityChanged,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
PriceDisplay(price: item.price),
],
),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: item.quantity > 1
? () => onQuantityChanged?.call(item.quantity - 1)
: null,
),
Text(
'${item.quantity}',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () => onQuantityChanged?.call(item.quantity + 1),
),
],
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: onRemove,
color: Theme.of(context).colorScheme.error,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/cart_provider.dart';
import '../providers/cart_total_provider.dart';
import 'cart_item_card.dart';
import '../../../../shared/widgets/price_display.dart';
import '../../../../core/widgets/empty_state.dart';
/// Cart summary widget
class CartSummary extends ConsumerWidget {
const CartSummary({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final cartAsync = ref.watch(cartProvider);
final totalData = ref.watch(cartTotalProvider);
return Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Shopping Cart',
style: Theme.of(context).textTheme.titleLarge,
),
if (cartAsync.value?.isNotEmpty ?? false)
TextButton.icon(
onPressed: () {
ref.read(cartProvider.notifier).clearCart();
},
icon: const Icon(Icons.delete_sweep),
label: const Text('Clear'),
),
],
),
),
Expanded(
child: cartAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (items) {
if (items.isEmpty) {
return const EmptyState(
message: 'Cart is empty',
subMessage: 'Add products to get started',
icon: Icons.shopping_cart_outlined,
);
}
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return CartItemCard(
item: item,
onRemove: () {
ref.read(cartProvider.notifier).removeItem(item.productId);
},
onQuantityChanged: (quantity) {
ref.read(cartProvider.notifier).updateQuantity(
item.productId,
quantity,
);
},
);
},
);
},
),
),
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total:',
style: Theme.of(context).textTheme.titleLarge,
),
PriceDisplay(
price: totalData.total,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: (cartAsync.value?.isNotEmpty ?? false)
? () {
// TODO: Implement checkout
}
: null,
icon: const Icon(Icons.payment),
label: const Text('Checkout'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
),
),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../products/presentation/providers/products_provider.dart';
import '../../../products/presentation/widgets/product_card.dart';
import '../../../products/domain/entities/product.dart';
import '../../../../core/widgets/loading_indicator.dart';
import '../../../../core/widgets/error_widget.dart';
import '../../../../core/widgets/empty_state.dart';
/// Product selector widget for POS
class ProductSelector extends ConsumerWidget {
final void Function(Product)? onProductTap;
const ProductSelector({
super.key,
this.onProductTap,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productsProvider);
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select Products',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Expanded(
child: productsAsync.when(
loading: () => const LoadingIndicator(
message: 'Loading products...',
),
error: (error, stack) => ErrorDisplay(
message: error.toString(),
onRetry: () => ref.refresh(productsProvider),
),
data: (products) {
if (products.isEmpty) {
return const EmptyState(
message: 'No products available',
subMessage: 'Add products to start selling',
icon: Icons.inventory_2_outlined,
);
}
// Filter only available products for POS
final availableProducts =
products.where((p) => p.isAvailable).toList();
if (availableProducts.isEmpty) {
return const EmptyState(
message: 'No products available',
subMessage: 'All products are currently unavailable',
icon: Icons.inventory_2_outlined,
);
}
return LayoutBuilder(
builder: (context, constraints) {
// Determine grid columns based on width
int crossAxisCount = 2;
if (constraints.maxWidth > 800) {
crossAxisCount = 4;
} else if (constraints.maxWidth > 600) {
crossAxisCount = 3;
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
childAspectRatio: 0.75,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: availableProducts.length,
itemBuilder: (context, index) {
final product = availableProducts[index];
return GestureDetector(
onTap: () => onProductTap?.call(product),
child: ProductCard(product: product),
);
},
);
},
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,5 @@
// Home/Cart Feature Widgets
export 'cart_item_card.dart';
export 'cart_summary.dart';
// This file provides a central export point for all home/cart widgets