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 createState() => _PosListScreenState(); } class _PosListScreenState extends State { List _posConfigs = []; bool _isLoading = true; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _fetchPosConfigs()); } Map _sessionDetails = {}; Future _fetchPosConfigs() async { final odoo = Provider.of(context, listen: false); try { // 1. Fetch Configs final List 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 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 details = {}; if (sessionIds.isNotEmpty) { final List 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 _openSession(int configId) async { final odoo = Provider.of(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 _createPos(String name) async { final odoo = Provider.of(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 _deletePos(int id, String name) async { final odoo = Provider.of(context, listen: false); // Show confirmation dialog before deleting final bool? confirm = await showDialog( 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 _closeSession(int sessionId) async { final odoo = Provider.of(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 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? 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)), ], ); } }