2026-06-10 13:29:04 +05:30
|
|
|
|
import 'dart:math';
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
2026-06-10 14:05:58 +05:30
|
|
|
|
import '../route_transitions.dart';
|
2026-06-10 13:29:04 +05:30
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-06-10 14:05:58 +05:30
|
|
|
|
showTopAlert(context, message, isError: true);
|
2026-06-10 13:29:04 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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: () {
|
2026-06-10 14:05:58 +05:30
|
|
|
|
showTopAlert(
|
|
|
|
|
|
context,
|
|
|
|
|
|
'Thermal Printed: $_invoiceNumber successfully!',
|
|
|
|
|
|
icon: Icons.print,
|
2026-06-10 13:29:04 +05:30
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|