odoo-mobile-app/lib/screens/pos_selling_screen.dart

587 lines
25 KiB
Dart

import 'package:flutter/material.dart';
import '../utils/theme.dart';
class PosSellingScreen extends StatefulWidget {
final String shopName;
final int sessionId;
const PosSellingScreen({super.key, required this.shopName, required this.sessionId});
@override
State<PosSellingScreen> createState() => _PosSellingScreenState();
}
class _PosSellingScreenState extends State<PosSellingScreen> {
// Mock Data
final List<Map<String, dynamic>> _foodProducts = [
{'name': 'Bacon Burger', 'price': 12.0, 'image': 'https://plus.unsplash.com/premium_photo-1661331777080-459247d853e3?q=80&w=300&auto=format&fit=crop'},
{'name': 'Cheese Burger', 'price': 10.5, 'image': 'https://images.unsplash.com/photo-1568901346375-23c9450c58cd?q=80&w=300&auto=format&fit=crop'},
{'name': 'Pizza Margherita', 'price': 15.0, 'image': 'https://images.unsplash.com/photo-1513104890138-7c749659a591?q=80&w=300&auto=format&fit=crop'},
{'name': 'Pizza Veggie', 'price': 14.0, 'image': 'https://images.unsplash.com/photo-1604382354936-07c5d9983bd3?q=80&w=300&auto=format&fit=crop'},
{'name': 'Pasta Carbonara', 'price': 13.5, 'image': 'https://images.unsplash.com/photo-1612874742237-6526221588e3?q=80&w=300&auto=format&fit=crop'},
{'name': 'Club Sandwich', 'price': 9.0, 'image': 'https://images.unsplash.com/photo-1597255682855-667d4ccf044d?q=80&w=300&auto=format&fit=crop'},
];
final List<Map<String, dynamic>> _drinkProducts = [
{'name': 'Coca-Cola', 'price': 3.0, 'image': 'https://images.unsplash.com/photo-1622483767028-3f66f32aef97?q=80&w=300&auto=format&fit=crop'},
{'name': 'Water 500ml', 'price': 2.0, 'image': 'https://images.unsplash.com/photo-1616118132534-381148898bb6?q=80&w=300&auto=format&fit=crop'},
{'name': 'Espresso', 'price': 2.5, 'image': 'https://images.unsplash.com/photo-1514432324607-a09d9b4aefdd?q=80&w=300&auto=format&fit=crop'},
{'name': 'Green Tea', 'price': 3.5, 'image': 'https://images.unsplash.com/photo-1627435601361-ec25f5b1d0e5?q=80&w=300&auto=format&fit=crop'},
{'name': 'Milkshake', 'price': 5.0, 'image': 'https://images.unsplash.com/photo-1579954115563-e72bf1381629?q=80&w=300&auto=format&fit=crop'},
];
String _selectedCategory = 'Food'; // 'Food' or 'Drinks'
List<Map<String, dynamic>> _cartItems = [];
void _addToCart(Map<String, dynamic> product) {
setState(() {
final index = _cartItems.indexWhere((item) => item['name'] == product['name']);
if (index != -1) {
_cartItems[index]['qty'] += 1;
} else {
_cartItems.add({
'name': product['name'],
'price': product['price'],
'qty': 1,
});
}
});
}
void _removeFromCart(int index) {
setState(() {
_cartItems.removeAt(index);
});
}
void _decrementCartItem(int index) {
setState(() {
if (_cartItems[index]['qty'] > 1) {
_cartItems[index]['qty'] -= 1;
} else {
_cartItems.removeAt(index);
}
});
}
double get _totalAmount {
return _cartItems.fold(0.0, (sum, item) => sum + (item['price'] * item['qty']));
}
int get _totalItems {
return _cartItems.fold(0, (sum, item) => sum + (item['qty'] as int));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF0F4F7),
body: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 700) {
return _buildWideLayout();
} else {
return _buildMobileLayout();
}
},
),
);
}
Widget _buildWideLayout() {
return Row(
children: [
// Left: Register/Cart
Expanded(
flex: 4,
child: _buildCartPanel(isMobile: false),
),
// Right: Products
Expanded(
flex: 6,
child: _buildProductPanel(),
),
],
);
}
Widget _buildMobileLayout() {
return Stack(
children: [
// Product Panel Full Screen
Column(
children: [
Expanded(child: _buildProductPanel()),
const SizedBox(height: 80), // Space for bottom bar
],
),
// Cart Summary Bottom Bar
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildMobileBottomBar(),
),
],
);
}
Widget _buildMobileBottomBar() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF2C3E50),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 10, offset: const Offset(0, -2))],
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('$_totalItems Items', style: const TextStyle(color: Colors.white70, fontSize: 13)),
const SizedBox(height: 4),
Text('${_totalAmount.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20)),
],
),
const Spacer(),
ElevatedButton.icon(
onPressed: _showMobileCartModal,
icon: const Icon(Icons.shopping_cart_outlined),
label: const Text('View Cart'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.secondaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
],
),
);
}
void _showMobileCartModal() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.85,
decoration: const BoxDecoration(
color: Color(0xFF2C3E50),
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
children: [
// Handle
Container(width: 40, height: 4, margin: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(2))),
Expanded(child: _buildCartPanel(isMobile: true)),
],
),
),
);
}
Widget _buildCartPanel({required bool isMobile}) {
return Container(
color: const Color(0xFF2C3E50),
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.white10)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Current Order', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)),
if (isMobile)
IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.keyboard_arrow_down, color: Colors.white70))
else
IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close, color: Colors.white70))
],
),
),
// Cart Items
Expanded(
child: _cartItems.isEmpty
? const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.shopping_cart_outlined, color: Colors.white24, size: 48),
SizedBox(height: 16),
Text('Cart is empty', style: TextStyle(color: Colors.white54)),
]
)
)
: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _cartItems.length,
itemBuilder: (context, index) {
final item = _cartItems[index];
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(color: Colors.white.withOpacity(0.05), borderRadius: BorderRadius.circular(12)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
title: Text(item['name'], style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
subtitle: Text('${item['qty']} x ₹${item['price']}', style: const TextStyle(color: Colors.white70)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('${(item['price'] * item['qty']).toStringAsFixed(2)}', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.remove_circle_outline, size: 22, color: Colors.orangeAccent),
tooltip: 'Reduce Quantity',
onPressed: () => _decrementCartItem(index),
),
IconButton(
icon: const Icon(Icons.add_circle_outline, size: 22, color: Colors.greenAccent),
tooltip: 'Add Quantity',
onPressed: () => _addToCart(item),
),
],
),
),
);
},
),
),
// Totals & Actions
Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(color: Color(0xFF233140)),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Total', style: TextStyle(color: Colors.white, fontSize: 18)),
Text('${_totalAmount.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE91E63),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Customer', style: TextStyle(color: Colors.white)),
)
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: _showPaymentModal,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4CAF50),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Payment', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
)
),
],
)
],
),
)
],
),
);
}
Widget _buildProductPanel() {
return Container(
color: const Color(0xFFF0F4F7),
child: Column(
children: [
// Top Bar
Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16), // Top padding for status bar if mobile
color: Colors.white,
child: Column(
children: [
Row(
children: [
// Back button here too for convenience? Or relying on Cart back
if (MediaQuery.of(context).size.width <= 700)
IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back)),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildCategoryTab('Food', Icons.lunch_dining, const Color(0xFFF8BBD0), const Color(0xFFE91E63)),
const SizedBox(width: 12),
_buildCategoryTab('Drinks', Icons.local_drink, const Color(0xFFC8E6C9), const Color(0xFF4CAF50)),
],
),
),
),
],
),
],
),
),
// Grid
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: MediaQuery.of(context).size.width > 900 ? 5 : (MediaQuery.of(context).size.width > 600 ? 4 : 2),
childAspectRatio: 0.8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: _selectedCategory == 'Food' ? _foodProducts.length : _drinkProducts.length,
itemBuilder: (context, index) {
final product = _selectedCategory == 'Food' ? _foodProducts[index] : _drinkProducts[index];
return _buildProductCard(product);
},
),
),
),
],
),
);
}
Widget _buildCategoryTab(String title, IconData icon, Color bgColor, Color textColor) {
final bool isSelected = _selectedCategory == title;
return GestureDetector(
onTap: () => setState(() => _selectedCategory = title),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? bgColor : Colors.grey[100],
borderRadius: BorderRadius.circular(12),
border: isSelected ? Border.all(color: textColor.withOpacity(0.5), width: 1.5) : null,
),
child: Row(
children: [
Icon(icon, color: isSelected ? textColor : Colors.grey[600], size: 20),
const SizedBox(width: 8),
Text(title, style: TextStyle(color: isSelected ? textColor : Colors.grey[800], fontWeight: FontWeight.w700)),
],
),
),
);
}
Widget _buildProductCard(Map<String, dynamic> product) {
return GestureDetector(
onTap: () => _addToCart(product),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 12, offset: const Offset(0, 4)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
child: Image.network(
product['image'],
fit: BoxFit.cover,
errorBuilder: (ctx, err, stack) => Container(
color: Colors.grey[100],
child: const Icon(Icons.restaurant, color: Colors.grey, size: 30)
),
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product['name'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Color(0xFF2D3748)), maxLines: 1, overflow: TextOverflow.ellipsis),
const SizedBox(height: 6),
Text('${product['price']}', style: const TextStyle(color: Color(0xFF38A169), fontWeight: FontWeight.bold, fontSize: 15)),
],
),
)
],
),
),
);
}
void _showPaymentModal() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.9,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Payment', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close)),
],
),
),
Expanded(
child: Row(
children: [
// Payment Methods (Left)
Expanded(
flex: 4,
child: Container(
color: const Color(0xFFF8F9FA),
child: ListView(
padding: const EdgeInsets.all(20),
children: [
_buildPaymentMethod('Cash', Icons.money, true),
const SizedBox(height: 10),
_buildPaymentMethod('Bank', Icons.credit_card, false),
const SizedBox(height: 10),
_buildPaymentMethod('Customer Account', Icons.person, false),
],
),
),
),
// Payment Details & Numpad (Right)
Expanded(
flex: 6,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Total Display
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Remaining to Pay', style: TextStyle(color: Colors.grey[600], fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
Text('${_totalAmount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 42, fontWeight: FontWeight.bold, color: Color(0xFF2D3748))),
],
),
const Spacer(),
// Validate Button
SizedBox(
height: 56,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context); // Close Payment
_processOrderSuccess();
},
icon: const Icon(Icons.check_circle_outline),
label: const Text('Validate Order', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4CAF50),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 4,
),
),
),
],
),
),
),
],
),
),
],
),
),
);
}
Widget _buildPaymentMethod(String name, IconData icon, bool selected) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: selected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: selected ? Border.all(color: AppTheme.primaryColor, width: 2) : Border.all(color: Colors.grey[300]!),
boxShadow: selected ? [BoxShadow(color: AppTheme.primaryColor.withOpacity(0.1), blurRadius: 10)] : [],
),
child: Row(
children: [
Icon(icon, color: selected ? AppTheme.primaryColor : Colors.grey[600]),
const SizedBox(width: 12),
Text(name, style: TextStyle(
fontWeight: FontWeight.bold,
color: selected ? AppTheme.primaryColor : Colors.grey[800],
fontSize: 16
)),
const Spacer(),
if (selected) const Icon(Icons.check_circle, color: AppTheme.primaryColor),
],
),
);
}
void _processOrderSuccess() {
setState(() {
_cartItems.clear();
});
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 64),
const SizedBox(height: 16),
const Text('Order Validated!', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('Receipt has been printed.', style: TextStyle(color: Colors.grey)),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Next Order', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
)
],
),
);
}
}