653 lines
24 KiB
Dart
653 lines
24 KiB
Dart
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)),
|
|
],
|
|
);
|
|
}
|
|
}
|