Odoo login issue fixed & POS list ui implemented.
This commit is contained in:
parent
8ad535e3f2
commit
a6f1c6a8b8
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/odoo_service.dart';
|
import '../services/odoo_service.dart';
|
||||||
import '../utils/theme.dart';
|
import '../utils/theme.dart';
|
||||||
|
import 'pos_list_screen.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
@ -118,9 +119,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (name.toLowerCase().contains('point of sale')) {
|
||||||
SnackBar(content: Text('Opening $name... (Not implemented in demo)')),
|
Navigator.push(
|
||||||
);
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => const PosListScreen()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Opening $name... (Not implemented in demo)')),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@ -15,8 +15,8 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _urlController = TextEditingController(text: AppConstants.defaultOdooUrl);
|
final _urlController = TextEditingController(text: AppConstants.defaultOdooUrl);
|
||||||
final _dbController = TextEditingController(text: AppConstants.dbName);
|
final _dbController = TextEditingController(text: AppConstants.dbName);
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController(text: 'alaguraj0361@gmail.com');
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController(text: 'Alaguraj@123');
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
void _login() async {
|
void _login() async {
|
||||||
|
|||||||
652
lib/screens/pos_list_screen.dart
Normal file
652
lib/screens/pos_list_screen.dart
Normal file
@ -0,0 +1,652 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../services/odoo_service.dart';
|
||||||
|
import '../utils/theme.dart';
|
||||||
|
import 'pos_selling_screen.dart';
|
||||||
|
|
||||||
|
class PosListScreen extends StatefulWidget {
|
||||||
|
const PosListScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PosListScreen> createState() => _PosListScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PosListScreenState extends State<PosListScreen> {
|
||||||
|
List<dynamic> _posConfigs = [];
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _fetchPosConfigs());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<int, dynamic> _sessionDetails = {};
|
||||||
|
|
||||||
|
Future<void> _fetchPosConfigs() async {
|
||||||
|
final odoo = Provider.of<OdooService>(context, listen: false);
|
||||||
|
try {
|
||||||
|
// 1. Fetch Configs
|
||||||
|
final List<dynamic> configs = await odoo.callKw(
|
||||||
|
model: 'pos.config',
|
||||||
|
method: 'search_read',
|
||||||
|
args: [],
|
||||||
|
kwargs: {
|
||||||
|
'fields': [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'current_session_id',
|
||||||
|
'current_session_state',
|
||||||
|
'pos_session_username'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Extract Session IDs
|
||||||
|
List<int> sessionIds = [];
|
||||||
|
for (var config in configs) {
|
||||||
|
if (config['current_session_id'] != null && config['current_session_id'] is List) {
|
||||||
|
sessionIds.add(config['current_session_id'][0]); // [id, name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch Session Details (if any valid sessions)
|
||||||
|
Map<int, dynamic> details = {};
|
||||||
|
if (sessionIds.isNotEmpty) {
|
||||||
|
final List<dynamic> sessions = await odoo.callKw(
|
||||||
|
model: 'pos.session',
|
||||||
|
method: 'read',
|
||||||
|
args: [sessionIds],
|
||||||
|
kwargs: {
|
||||||
|
'fields': ['start_at', 'cash_register_balance_start'] // Basic info. 'total_payments_amount' often requires computation, keeping it simple for now.
|
||||||
|
// Note: 'total_payments_amount' isn't always stored directly on session in some versions, or is 'cash_register_total_entry_encoding'.
|
||||||
|
// Let's rely on valid fields we can definitely verify.
|
||||||
|
// For now, we will show Date and Opening. "Sold" might require order calculation which is heavier.
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for (var session in sessions) {
|
||||||
|
details[session['id']] = session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_posConfigs = configs;
|
||||||
|
_sessionDetails = details;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
print('Error fetching POS configs: $e');
|
||||||
|
print(stackTrace);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() { _isLoading = false; });
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error fetching POS data: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openSession(int configId) async {
|
||||||
|
final odoo = Provider.of<OdooService>(context, listen: false);
|
||||||
|
setState(() { _isLoading = true; });
|
||||||
|
try {
|
||||||
|
// Use 'create' on pos.session directly instead of open_session_cb
|
||||||
|
// This is more robust for API calls as open_session_cb returns a web action.
|
||||||
|
await odoo.callKw(
|
||||||
|
model: 'pos.session',
|
||||||
|
method: 'create',
|
||||||
|
args: [
|
||||||
|
{'config_id': configId}
|
||||||
|
],
|
||||||
|
kwargs: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Session Opened Successfully!')),
|
||||||
|
);
|
||||||
|
_fetchPosConfigs();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() { _isLoading = false; });
|
||||||
|
// Clean up error message for display
|
||||||
|
String errorMsg = e.toString();
|
||||||
|
if (errorMsg.contains('OdooException')) {
|
||||||
|
errorMsg = errorMsg.replaceAll('OdooException:', '').trim();
|
||||||
|
}
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Error Opening Session: $errorMsg'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_fetchPosConfigs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createPos(String name) async {
|
||||||
|
final odoo = Provider.of<OdooService>(context, listen: false);
|
||||||
|
setState(() { _isLoading = true; });
|
||||||
|
try {
|
||||||
|
await odoo.callKw(
|
||||||
|
model: 'pos.config',
|
||||||
|
method: 'create',
|
||||||
|
args: [
|
||||||
|
{'name': name}
|
||||||
|
],
|
||||||
|
kwargs: {},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('POS Created Successfully')),
|
||||||
|
);
|
||||||
|
_fetchPosConfigs();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() { _isLoading = false; });
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error creating POS: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deletePos(int id, String name) async {
|
||||||
|
final odoo = Provider.of<OdooService>(context, listen: false);
|
||||||
|
|
||||||
|
// Show confirmation dialog before deleting
|
||||||
|
final bool? confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(color: Colors.red[50], borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: const Icon(Icons.warning_rounded, color: Colors.red),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Text('Delete Shop', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
'Are you sure you want to delete "$name"?\nThis action cannot be undone.',
|
||||||
|
style: TextStyle(color: Colors.grey[700], fontSize: 15),
|
||||||
|
),
|
||||||
|
actionsPadding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.grey[600]),
|
||||||
|
child: const Text('Cancel', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
),
|
||||||
|
child: const Text('Delete')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirm != true) return;
|
||||||
|
|
||||||
|
setState(() { _isLoading = true; });
|
||||||
|
try {
|
||||||
|
// Try to hard delete first
|
||||||
|
await odoo.callKw(
|
||||||
|
model: 'pos.config',
|
||||||
|
method: 'unlink',
|
||||||
|
args: [[id]],
|
||||||
|
kwargs: {},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('POS Deleted Successfully')),
|
||||||
|
);
|
||||||
|
_fetchPosConfigs();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If hard delete fails (likely due to existing data), try to archive
|
||||||
|
try {
|
||||||
|
await odoo.callKw(
|
||||||
|
model: 'pos.config',
|
||||||
|
method: 'write',
|
||||||
|
args: [[id], {'active': false, 'name': '$name (Archived)'}],
|
||||||
|
kwargs: {},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('POS Archived Successfully (Hard delete restricted)')),
|
||||||
|
);
|
||||||
|
_fetchPosConfigs();
|
||||||
|
}
|
||||||
|
} catch (e2) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() { _isLoading = false; });
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Cannot Delete or Archive: $e2')),
|
||||||
|
);
|
||||||
|
_fetchPosConfigs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _closeSession(int sessionId) async {
|
||||||
|
final odoo = Provider.of<OdooService>(context, listen: false);
|
||||||
|
setState(() { _isLoading = true; });
|
||||||
|
try {
|
||||||
|
// 1. Try to validate the closing directly
|
||||||
|
await odoo.callKw(
|
||||||
|
model: 'pos.session',
|
||||||
|
method: 'action_pos_session_validate',
|
||||||
|
args: [[sessionId]],
|
||||||
|
kwargs: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Session Closed Successfully')));
|
||||||
|
_fetchPosConfigs();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback or Error Display
|
||||||
|
if (mounted) {
|
||||||
|
setState(() { _isLoading = false; });
|
||||||
|
String errorMsg = e.toString();
|
||||||
|
if (errorMsg.contains('OdooException')) {
|
||||||
|
errorMsg = errorMsg.replaceAll('OdooException:', '').trim();
|
||||||
|
}
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Closing Failed: $errorMsg'), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
_fetchPosConfigs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCreateDialog() {
|
||||||
|
final TextEditingController nameController = TextEditingController();
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(color: Colors.green[50], borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: const Icon(Icons.add_business_rounded, color: Colors.green, size: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Expanded(child: Text('New Point of Sale', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Enter a unique name for your new shop to identify it easily.', style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Shop Name',
|
||||||
|
hintText: 'e.g. Main Bar',
|
||||||
|
prefixIcon: const Icon(Icons.store, color: Colors.green),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.green.shade200)),
|
||||||
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.green, width: 2)),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.green.withOpacity(0.05),
|
||||||
|
),
|
||||||
|
autofocus: true,
|
||||||
|
cursorColor: Colors.green,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actionsPadding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
|
||||||
|
actions: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: const BorderSide(color: Colors.red),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
child: const Text('Cancel', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
if (nameController.text.isNotEmpty) {
|
||||||
|
_createPos(nameController.text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 4,
|
||||||
|
shadowColor: Colors.green.withOpacity(0.4),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
child: const Text('Create Shop', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
actionsAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update build to use extendBodyBehindAppBar: true
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Point of Sale', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24)),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: _showCreateDialog,
|
||||||
|
backgroundColor: AppTheme.primaryColor,
|
||||||
|
icon: const Icon(Icons.add, color: Colors.white),
|
||||||
|
label: const Text('New Shop', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [Color(0xFF2bb1a5), Color(0xFF1a4d46)], // Rich Teal Gradient
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator(color: Colors.white))
|
||||||
|
: SafeArea(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
color: AppTheme.secondaryColor,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
onRefresh: _fetchPosConfigs,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 10, 20, 80),
|
||||||
|
itemCount: _posConfigs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildPosCard(_posConfigs[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPosCard(Map<String, dynamic> config) {
|
||||||
|
final int id = config['id'];
|
||||||
|
final String name = config['name'] is String ? config['name'] : 'Shop';
|
||||||
|
final String status = config['current_session_state'] is String ? config['current_session_state'] : 'closed';
|
||||||
|
final dynamic rawUsername = config['pos_session_username'];
|
||||||
|
final String? username = rawUsername is String ? rawUsername : null;
|
||||||
|
|
||||||
|
// Get Session Details
|
||||||
|
Map<String, dynamic>? sessionData;
|
||||||
|
if (config['current_session_id'] != null && config['current_session_id'] is List) {
|
||||||
|
int sessionId = config['current_session_id'][0];
|
||||||
|
sessionData = _sessionDetails[sessionId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format Data
|
||||||
|
String dateStr = '---';
|
||||||
|
String openingStr = '₹ 0.00';
|
||||||
|
// String soldStr = '₹ 0.00'; // Need complex calculation or field, omitting for simplicity/safety
|
||||||
|
|
||||||
|
if (sessionData != null) {
|
||||||
|
if (sessionData['start_at'] != null) {
|
||||||
|
// Simple date parsing or just display string
|
||||||
|
dateStr = sessionData['start_at'].toString().split(' ')[0]; // yyyy-mm-dd
|
||||||
|
}
|
||||||
|
if (sessionData['cash_register_balance_start'] != null) {
|
||||||
|
openingStr = '₹ ${sessionData['cash_register_balance_start']}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color statusColor;
|
||||||
|
String statusText;
|
||||||
|
IconData statusIcon;
|
||||||
|
Color cardAccentColor;
|
||||||
|
String mainButtonText;
|
||||||
|
VoidCallback mainButtonAction;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'opened':
|
||||||
|
statusColor = const Color(0xFF4CAF50); // Bright Green
|
||||||
|
statusText = 'Active Session';
|
||||||
|
statusIcon = Icons.check_circle_rounded;
|
||||||
|
cardAccentColor = const Color(0xFFE8F5E9);
|
||||||
|
mainButtonText = 'Continue Selling';
|
||||||
|
mainButtonAction = () {
|
||||||
|
int sessionId = (config['current_session_id'] != null && config['current_session_id'] is List) ? config['current_session_id'][0] : 0;
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) => PosSellingScreen(shopName: name, sessionId: sessionId)));
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'opening_control':
|
||||||
|
statusColor = const Color(0xFF2196F3); // Blue
|
||||||
|
statusText = 'Opening';
|
||||||
|
statusIcon = Icons.sync;
|
||||||
|
cardAccentColor = const Color(0xFFE3F2FD);
|
||||||
|
mainButtonText = 'Continue Selling';
|
||||||
|
mainButtonAction = () {
|
||||||
|
int sessionId = (config['current_session_id'] != null && config['current_session_id'] is List) ? config['current_session_id'][0] : 0;
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) => PosSellingScreen(shopName: name, sessionId: sessionId)));
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'closing_control':
|
||||||
|
statusColor = const Color(0xFFFF9800); // Orange
|
||||||
|
statusText = 'Closing Control';
|
||||||
|
statusIcon = Icons.history_toggle_off;
|
||||||
|
cardAccentColor = const Color(0xFFFFF3E0);
|
||||||
|
mainButtonText = 'Close Session';
|
||||||
|
mainButtonAction = () {
|
||||||
|
int sessionId = (config['current_session_id'] != null && config['current_session_id'] is List) ? config['current_session_id'][0] : 0;
|
||||||
|
if (sessionId != 0) {
|
||||||
|
_closeSession(sessionId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'closed':
|
||||||
|
default:
|
||||||
|
statusColor = const Color(0xFF757575); // Grey
|
||||||
|
statusText = 'Closed';
|
||||||
|
statusIcon = Icons.storefront;
|
||||||
|
cardAccentColor = const Color(0xFFF5F5F5);
|
||||||
|
mainButtonText = 'Open Register';
|
||||||
|
mainButtonAction = () => _openSession(id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Decorative background circle
|
||||||
|
Positioned(
|
||||||
|
right: -20,
|
||||||
|
top: -20,
|
||||||
|
child: Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cardAccentColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header Row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cardAccentColor,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.store_mall_directory_rounded, color: statusColor, size: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF2D3748),
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(statusIcon, size: 14, color: statusColor),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
statusText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: statusColor,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _deletePos(id, name),
|
||||||
|
icon: Icon(Icons.delete_outline_rounded, color: Colors.grey[400]),
|
||||||
|
tooltip: 'Delete',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Info Stats Row (Only if not closed or has session info)
|
||||||
|
if (status != 'closed' || sessionData != null) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.grey[200]!),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildStatItem('Date', dateStr),
|
||||||
|
_buildStatItem('Opening', openingStr),
|
||||||
|
// _buildStatItem('Sold', soldStr), // Placeholder for future
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (username != null && status != 'closed') ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 10,
|
||||||
|
backgroundColor: Colors.grey[300],
|
||||||
|
child: const Icon(Icons.person, size: 14, color: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Session by $username',
|
||||||
|
style: TextStyle(color: Colors.grey[600], fontWeight: FontWeight.w500, fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Action Button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: mainButtonAction,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: status != 'closed' ? statusColor : AppTheme.secondaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
elevation: 4,
|
||||||
|
shadowColor: statusColor.withOpacity(0.4),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
mainButtonText,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatItem(String label, String value) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label, style: TextStyle(color: Colors.grey[500], fontSize: 12, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(value, style: const TextStyle(color: Color(0xFF2D3748), fontSize: 14, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
586
lib/screens/pos_selling_screen.dart
Normal file
586
lib/screens/pos_selling_screen.dart
Normal file
@ -0,0 +1,586 @@
|
|||||||
|
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)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -68,6 +68,8 @@ class OdooService with ChangeNotifier {
|
|||||||
}) async {
|
}) async {
|
||||||
if (_client == null) throw Exception("Client not initialized");
|
if (_client == null) throw Exception("Client not initialized");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// In odoo_rpc 0.4.x, callKw expects a Map of params
|
// In odoo_rpc 0.4.x, callKw expects a Map of params
|
||||||
return _client!.callKw({
|
return _client!.callKw({
|
||||||
'model': model,
|
'model': model,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
class AppConstants {
|
class AppConstants {
|
||||||
// Use 10.0.2.2 for Android emulator to access localhost
|
// Use 10.0.2.2 for Android emulator to access localhost
|
||||||
static const String defaultOdooUrl = 'http://192.168.1.2:10001';
|
static const String defaultOdooUrl = 'https://food2.odoo.com';
|
||||||
static const String dbName = 'FOODIES-DELIGHT'; // Found in your local database
|
static const String dbName = 'food2'; // Found in your local database
|
||||||
}
|
}
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@ -108,10 +108,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.6"
|
version: "1.6.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -188,10 +188,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: odoo_rpc
|
name: odoo_rpc
|
||||||
sha256: "735cad4a40dcba251f9a2441223d511d9f23fcc79c2c1e0faa773cb2996019cb"
|
sha256: c243639c4d33efe74c3227d9f8d10fa41599aee922795d909c6a9da497b18481
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.5"
|
version: "0.7.1"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -224,14 +224,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
pedantic:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pedantic
|
|
||||||
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.11.1"
|
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -34,7 +34,7 @@ dependencies:
|
|||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
odoo_rpc: ^0.4.1
|
odoo_rpc: ^0.7.1
|
||||||
provider: ^6.0.0
|
provider: ^6.0.0
|
||||||
shared_preferences: ^2.2.0
|
shared_preferences: ^2.2.0
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user