OnboardingPozoAppv1/flutter_app/lib/screens/billing_dashboard_screen.dart

1009 lines
41 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:math';
import 'package:flutter/material.dart';
class ProductItem {
final String id;
final String name;
final double price;
final int stock;
final String code;
ProductItem({
required this.id,
required this.name,
required this.price,
required this.stock,
required this.code,
});
}
class BillingItem {
final ProductItem product;
int quantity;
BillingItem({
required this.product,
required this.quantity,
});
}
class BillingDashboardScreen extends StatefulWidget {
final String businessName;
final String businessType;
final String gstin;
final bool barcodeScanner;
final bool kotManagement;
final bool inventoryAlerts;
const BillingDashboardScreen({
super.key,
required this.businessName,
required this.businessType,
required this.gstin,
required this.barcodeScanner,
required this.kotManagement,
required this.inventoryAlerts,
});
@override
State<BillingDashboardScreen> createState() => _BillingDashboardScreenState();
}
class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
late List<ProductItem> _products;
final List<BillingItem> _billingList = [];
bool _printingInvoice = false;
String _invoiceNumber = 'POZO-26402';
String _scanMessage = '';
final Map<String, List<ProductItem>> _inventoryPresets = {
'supermarket': [
ProductItem(id: 'sm1', name: 'Premium Basmati Rice (1kg)', price: 120.0, stock: 45, code: '89012300052'),
ProductItem(id: 'sm2', name: 'Fresh Farm Milk (1L)', price: 65.0, stock: 2, code: '89045300010'),
ProductItem(id: 'sm3', name: 'Refined Cooking Oil (1L)', price: 165.0, stock: 18, code: '89010320491'),
ProductItem(id: 'sm4', name: 'Refined White Sugar (1kg)', price: 48.0, stock: 3, code: '89034200421'),
ProductItem(id: 'sm5', name: 'Organic Green T-Tea (250g)', price: 110.0, stock: 24, code: '89512300011'),
ProductItem(id: 'sm6', name: 'Whole Wheat Atta (5kg)', price: 230.0, stock: 7, code: '89011210042'),
],
'restaurant': [
ProductItem(id: 'rt1', name: 'Fresh Paneer Butter Masala', price: 240.0, stock: 19, code: '9901230'),
ProductItem(id: 'rt2', name: 'Butter Tandoori Naan', price: 45.0, stock: 15, code: '9904530'),
ProductItem(id: 'rt3', name: 'Dum Veg Biryani Deluxe', price: 190.0, stock: 2, code: '9901032'),
ProductItem(id: 'rt4', name: 'Crispy Butter Masala Dosa', price: 90.0, stock: 22, code: '9903420'),
ProductItem(id: 'rt5', name: 'Rich Chocolate Milkshake', price: 120.0, stock: 3, code: '9951230'),
ProductItem(id: 'rt6', name: 'Hot Roasted Filter Coffee', price: 50.0, stock: 30, code: '9901121'),
],
'clothing': [
ProductItem(id: 'cl1', name: 'Premium Oxford Cotton Shirt', price: 899.0, stock: 8, code: '77012301'),
ProductItem(id: 'cl2', name: 'Slim Fit Dark Jeans', price: 1599.0, stock: 2, code: '77045302'),
ProductItem(id: 'cl3', name: 'Designer Summer Slip Dress', price: 1299.0, stock: 5, code: '77010323'),
ProductItem(id: 'cl4', name: 'Graphic Printed Tee-Shirt', price: 450.0, stock: 40, code: '77034204'),
ProductItem(id: 'cl5', name: 'Breathable Sports Sneakers', price: 1899.0, stock: 3, code: '77512305'),
ProductItem(id: 'cl6', name: 'Stitch Leather Belt Brown', price: 349.0, stock: 12, code: '77011216'),
],
'kirana': [
ProductItem(id: 'kr1', name: 'Premium Toor Dal (1kg)', price: 145.0, stock: 15, code: '4401230'),
ProductItem(id: 'kr2', name: 'Iodized Powdered Salt (1kg)', price: 22.0, stock: 50, code: '4404530'),
ProductItem(id: 'kr3', name: 'Herbal Moisturizer Soap', price: 42.0, stock: 2, code: '4401032'),
ProductItem(id: 'kr4', name: 'Anti-Germ Toothpaste (200g)', price: 85.0, stock: 16, code: '4403420'),
ProductItem(id: 'kr5', name: 'Dishwash Cleaning Soap', price: 25.0, stock: 3, code: '4451230'),
ProductItem(id: 'kr6', name: 'Red Chili Powder (200g)', price: 65.0, stock: 18, code: '4401121'),
],
'others': [
ProductItem(id: 'ot1', name: 'Utility Household Scrubber', price: 35.0, stock: 33, code: '5501230'),
ProductItem(id: 'ot2', name: 'AAA Rechargeable Batteries', price: 120.0, stock: 4, code: '5504530'),
ProductItem(id: 'ot3', name: 'LED High Bright Focus Bulb', price: 140.0, stock: 2, code: '5501032'),
ProductItem(id: 'ot4', name: 'Water Bottle Stainless (1L)', price: 320.0, stock: 11, code: '5503420'),
ProductItem(id: 'ot5', name: 'Microfiber Floor Duster', price: 75.0, stock: 24, code: '5551230'),
ProductItem(id: 'ot6', name: 'Multi-plug Protection Adapter', price: 240.0, stock: 9, code: '5501121'),
]
};
@override
void initState() {
super.initState();
_products = _inventoryPresets[widget.businessType] ?? _inventoryPresets['supermarket']!;
}
void _handleAddItem(ProductItem product) {
setState(() {
final index = _billingList.indexWhere((element) => element.product.id == product.id);
if (index != -1) {
final existingItem = _billingList[index];
if (existingItem.quantity >= product.stock && widget.inventoryAlerts) {
_showErrorSnackBar('Cannot add more. Inventory limit reached for ${product.name}!');
return;
}
existingItem.quantity += 1;
} else {
_billingList.add(BillingItem(product: product, quantity: 1));
}
});
}
void _handleRemoveOne(String productId) {
setState(() {
final index = _billingList.indexWhere((element) => element.product.id == productId);
if (index != -1) {
if (_billingList[index].quantity > 1) {
_billingList[index].quantity -= 1;
} else {
_billingList.removeAt(index);
}
}
});
}
void _simulateBarcodeScan() {
if (!widget.barcodeScanner) return;
final random = Random();
final randomProduct = _products[random.nextInt(_products.length)];
_handleAddItem(randomProduct);
setState(() {
_scanMessage = '🎯 Scanned EAN Barcode [${randomProduct.code}] - Added ${randomProduct.name}!';
});
Future.delayed(const Duration(milliseconds: 3000), () {
if (mounted) {
setState(() {
_scanMessage = '';
});
}
});
}
void _clearRegister() {
setState(() {
_billingList.clear();
});
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red[800],
behavior: SnackBarBehavior.floating,
),
);
}
double get _subtotal {
return _billingList.fold(0.0, (sum, item) => sum + (item.product.price * item.quantity));
}
bool get _hasGstin {
return widget.gstin.trim().isNotEmpty;
}
double get _cgst => _hasGstin ? _subtotal * 0.09 : 0.0;
double get _sgst => _hasGstin ? _subtotal * 0.09 : 0.0;
double get _total => _subtotal + _cgst + _sgst;
void _handleReviewAndGenerate() {
final random = Random();
final invoiceVal = 'POZO-${random.nextInt(90000) + 10000}';
setState(() {
_invoiceNumber = invoiceVal;
_printingInvoice = true;
});
}
void _closeReceiptAndReset() {
setState(() {
_printingInvoice = false;
_billingList.clear();
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDesktop = MediaQuery.of(context).size.width >= 600;
return Stack(
children: [
Scaffold(
backgroundColor: const Color(0xFF1E293B), // Slate-900 like
appBar: AppBar(
backgroundColor: const Color(0xFF0F172A), // Slate-950 like
elevation: 4,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.businessName.isNotEmpty ? widget.businessName : 'Sharma General Store',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
Row(
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Color(0xFF10B981),
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'Live Register',
style: TextStyle(
color: Colors.grey,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
actions: [
if (widget.barcodeScanner)
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Directionality(
textDirection: TextDirection.rtl,
child: ElevatedButton.icon(
onPressed: _simulateBarcodeScan,
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 32),
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: const Icon(Icons.qr_code_scanner, size: 16),
label: const Text(
'Scan Item',
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
),
),
),
),
if (widget.kotManagement)
Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.cyan.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.cyan.withOpacity(0.3)),
),
child: const Center(
child: Text(
'KOT',
style: TextStyle(
color: Colors.cyanAccent,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 8),
],
),
body: SafeArea(
child: Column(
children: [
// Scan Banner Message
if (_scanMessage.isNotEmpty)
Container(
width: double.infinity,
color: const Color(0xFF10B981),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.stars, color: Colors.white, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
_scanMessage,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
],
),
),
// Main Layout Split
Expanded(
child: isDesktop
? Row(
children: [
Expanded(child: _buildCatalogPart(theme)),
_buildLedgerTrayPart(theme, isDesktop),
],
)
: Column(
children: [
Expanded(child: _buildCatalogPart(theme)),
const Divider(color: Color(0xFF334155), height: 1),
SizedBox(
height: 240,
child: _buildLedgerTrayPart(theme, isDesktop),
),
],
),
),
// Bottom Checkout sum action for Mobile ONLY
if (!isDesktop) _buildBottomComputationBar(theme),
],
),
),
),
// Custom Overlay for Printable Invoice
if (_printingInvoice) _buildThermalReceiptOverlay(theme),
],
);
}
Widget _buildCatalogPart(ThemeData theme) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'STORE CATALOG',
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
fontSize: 11,
letterSpacing: 1.0,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF334155),
borderRadius: BorderRadius.circular(10),
),
child: Text(
widget.businessType.toUpperCase(),
style: const TextStyle(
color: Colors.white70,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 10),
Expanded(
child: GridView.builder(
physics: const BouncingScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1.4,
),
itemCount: _products.length,
itemBuilder: (context, index) {
final prod = _products[index];
final billingIndex = _billingList.indexWhere((element) => element.product.id == prod.id);
final countInCart = billingIndex != -1 ? _billingList[billingIndex].quantity : 0;
final remainingStock = prod.stock - countInCart;
final isLowStock = widget.inventoryAlerts && remainingStock <= 3;
final isOutOfStock = widget.inventoryAlerts && remainingStock <= 0;
return InkWell(
onTap: isOutOfStock ? null : () => _handleAddItem(prod),
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: countInCart > 0
? const Color(0xFF1E1B4B) // Deep indigo accent
: const Color(0xFF1E293B).withOpacity(0.8),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: countInCart > 0 ? theme.colorScheme.primary : const Color(0xFF334155),
width: countInCart > 0 ? 1.5 : 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
prod.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
const SizedBox(height: 4),
Text(
'${prod.price.toStringAsFixed(0)}',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w800,
fontSize: 13,
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.inventoryAlerts)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isLowStock
? Colors.red.withOpacity(0.15)
: const Color(0xFF334155),
borderRadius: BorderRadius.circular(6),
),
child: Text(
isOutOfStock ? 'Out of stock' : '$remainingStock Stock',
style: TextStyle(
color: isLowStock ? Colors.redAccent : Colors.grey,
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
)
else
const SizedBox(),
if (countInCart > 0)
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
'$countInCart',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
)
],
)
],
),
),
);
},
),
)
],
),
);
}
Widget _buildLedgerTrayPart(ThemeData theme, bool isDesktop) {
return Container(
width: isDesktop ? 300 : double.infinity,
color: const Color(0xFF0F172A).withOpacity(0.3),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'BILLING TRAY (${_billingList.length})',
style: const TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
fontSize: 11,
letterSpacing: 1.0,
),
),
if (_billingList.isNotEmpty)
GestureDetector(
onTap: _clearRegister,
child: const Row(
children: [
Icon(Icons.refresh, color: Colors.grey, size: 12),
SizedBox(width: 4),
Text(
'Clear',
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
fontSize: 10,
),
)
],
),
)
],
),
const SizedBox(height: 10),
Expanded(
child: _billingList.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shopping_cart_outlined, color: Colors.grey[600], size: 30),
const SizedBox(height: 8),
const Text(
'No products selected.',
style: TextStyle(color: Colors.grey, fontSize: 11),
),
const Text(
'Tap items in catalog above.',
style: TextStyle(color: Colors.white30, fontSize: 10),
),
],
),
)
: ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: _billingList.length,
itemBuilder: (context, index) {
final item = _billingList[index];
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF1E293B),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF334155), width: 0.5),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.product.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
const SizedBox(height: 2),
Text(
'${item.product.price.toStringAsFixed(0)} × ${item.quantity}',
style: const TextStyle(
color: Colors.grey,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
),
Row(
children: [
GestureDetector(
onTap: () => _handleRemoveOne(item.product.id),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFF334155),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(Icons.remove, size: 14, color: Colors.white),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'${item.quantity}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
GestureDetector(
onTap: () => _handleAddItem(item.product),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFF334155),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(Icons.add, size: 14, color: Colors.white),
),
),
],
),
],
),
);
},
),
),
if (isDesktop) ...[
const Divider(color: Color(0xFF334155), height: 16),
_buildComputationCalculations(theme),
const SizedBox(height: 10),
ElevatedButton(
onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
disabledBackgroundColor: const Color(0xFF1E293B),
minimumSize: const Size.fromHeight(48),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt, size: 16),
SizedBox(width: 8),
Text('Generate Invoice'),
],
),
)
]
],
),
);
}
Widget _buildComputationCalculations(ThemeData theme) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Subtotal', style: TextStyle(color: Colors.grey, fontSize: 11)),
Text('${_subtotal.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)),
],
),
if (_hasGstin) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('CGST (9%)', style: TextStyle(color: Colors.grey, fontSize: 11)),
Text('${_cgst.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('SGST (9%)', style: TextStyle(color: Colors.grey, fontSize: 11)),
Text('${_sgst.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
],
),
],
const Padding(
padding: EdgeInsets.symmetric(vertical: 6.0),
child: Divider(color: Color(0xFF334155), height: 1),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Grand Total', style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)),
Text(
'${_total.toStringAsFixed(2)}',
style: TextStyle(
color: theme.colorScheme.primary,
fontSize: 14,
fontWeight: FontWeight.w900,
),
),
],
),
],
);
}
Widget _buildBottomComputationBar(ThemeData theme) {
return Container(
color: const Color(0xFF0F172A),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('GRAND TOTAL', style: TextStyle(color: Colors.grey, fontSize: 9, fontWeight: FontWeight.bold)),
Text(
'${_total.toStringAsFixed(2)}',
style: TextStyle(
color: theme.colorScheme.primary,
fontSize: 18,
fontWeight: FontWeight.w900,
),
)
],
),
ElevatedButton(
onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
minimumSize: const Size(180, 44),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.print, size: 16),
SizedBox(width: 8),
Text('Review & Invoice', style: TextStyle(fontSize: 12)),
],
),
)
],
)
],
),
);
}
Widget _buildThermalReceiptOverlay(ThemeData theme) {
return Scaffold(
backgroundColor: Colors.black87,
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 30),
child: Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 340),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Thermal dotted-dash receipt border box
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[400]!, width: 2, style: BorderStyle.solid),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Column(
children: [
Text(
widget.businessName.isNotEmpty ? widget.businessName.toUpperCase() : 'SHARMA GENERAL STORE',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w900,
fontSize: 16,
fontFamily: 'monospace',
),
),
const SizedBox(height: 2),
Text(
'${widget.businessType.toUpperCase()} COUNTER',
style: const TextStyle(
color: Colors.grey,
fontSize: 9,
fontFamily: 'monospace',
),
),
if (_hasGstin) ...[
const SizedBox(height: 4),
Text(
'GSTIN: ${widget.gstin}',
style: const TextStyle(
color: Colors.black,
fontSize: 8,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
),
]
],
),
),
const SizedBox(height: 12),
const Divider(color: Colors.black38, thickness: 1),
const SizedBox(height: 4),
// Metadata Info
_buildReceiptRow('Invoice No', _invoiceNumber),
_buildReceiptRow('Date Track', '${DateTime.now().day}/${DateTime.now().month}/${DateTime.now().year}'),
_buildReceiptRow('Terminal', 'Pozo Counter'),
const SizedBox(height: 4),
const Divider(color: Colors.black38, thickness: 1),
const SizedBox(height: 4),
// Headers
const Row(
children: [
Expanded(
flex: 6,
child: Text('Item Description', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black, fontSize: 8, fontFamily: 'monospace')),
),
Expanded(
flex: 2,
child: Text('Qty', textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black, fontSize: 8, fontFamily: 'monospace')),
),
Expanded(
flex: 4,
child: Text('Price', textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black, fontSize: 8, fontFamily: 'monospace')),
),
],
),
const SizedBox(height: 6),
// Products
..._billingList.map((item) => Padding(
padding: const EdgeInsets.only(bottom: 6.0),
child: Row(
children: [
Expanded(
flex: 6,
child: Text(
item.product.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.black87, fontSize: 9, fontFamily: 'monospace'),
),
),
Expanded(
flex: 2,
child: Text(
'x${item.quantity}',
textAlign: TextAlign.right,
style: const TextStyle(color: Colors.black87, fontSize: 9, fontFamily: 'monospace'),
),
),
Expanded(
flex: 4,
child: Text(
'${(item.product.price * item.quantity).toStringAsFixed(0)}',
textAlign: TextAlign.right,
style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 9, fontFamily: 'monospace'),
),
),
],
),
)),
const Divider(color: Colors.black38, thickness: 1),
const SizedBox(height: 4),
// Ledger math sum
_buildReceiptRow('Cart Total:', '${_subtotal.toStringAsFixed(2)}'),
if (_hasGstin) ...[
_buildReceiptRow('CGST (9%):', '${_cgst.toStringAsFixed(2)}'),
_buildReceiptRow('SGST (9%):', '${_sgst.toStringAsFixed(2)}'),
],
const SizedBox(height: 4),
_buildReceiptRow('NET TOTAL:', '${_total.toStringAsFixed(2)}', isBold: true),
const SizedBox(height: 8),
const Divider(color: Colors.black38, thickness: 1),
const SizedBox(height: 8),
const Center(
child: Column(
children: [
Text(
'THANK YOU FOR YOUR PATRONAGE!',
style: TextStyle(color: Colors.black54, fontSize: 8, fontWeight: FontWeight.bold, fontFamily: 'monospace'),
),
SizedBox(height: 4),
Text(
'Powered by PozoApp',
style: TextStyle(color: Colors.red, fontSize: 8, fontWeight: FontWeight.bold, fontFamily: 'monospace'),
),
],
),
),
],
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('🧾 Thermal Printed: $_invoiceNumber successfully!'),
backgroundColor: const Color(0xFF10B981),
behavior: SnackBarBehavior.floating,
),
);
},
icon: const Icon(Icons.print, size: 16),
label: const Text('Simulate Print Receipt', style: TextStyle(fontSize: 12)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(42),
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _closeReceiptAndReset,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
minimumSize: const Size.fromHeight(42),
),
child: const Text('New Ledger Sale', style: TextStyle(fontSize: 12)),
),
],
),
),
),
),
);
}
Widget _buildReceiptRow(String label, String val, {bool isBold = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 9,
fontFamily: 'monospace',
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
color: isBold ? Colors.black : Colors.black87,
),
),
Text(
val,
style: TextStyle(
fontSize: 9,
fontFamily: 'monospace',
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
color: isBold ? Colors.black : Colors.black87,
),
),
],
),
);
}
}