Revamp Done - Support for Individual and Jwellery module added
This commit is contained in:
@@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../../dashboard/presentation/dashboard_screen.dart';
|
||||
import 'setup_wizard_screen.dart';
|
||||
import '../../../../core/network/dio_client.dart';
|
||||
|
||||
class OtpScreen extends ConsumerStatefulWidget {
|
||||
final String email;
|
||||
@@ -23,11 +25,32 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
||||
.read(authControllerProvider.notifier)
|
||||
.verifyOtp(widget.email, otp);
|
||||
if (success && mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const DashboardScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
final res = await DioClient().dio.get('/account/setup/status');
|
||||
final status = res.data['status'];
|
||||
if (status == 'COMPLETED' && mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const DashboardScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
} else if (mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const SetupWizardScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const SetupWizardScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,23 @@ import 'auth_screen.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../../transactions/providers/providers.dart';
|
||||
import '../../transactions/providers/paginated_transaction_provider.dart';
|
||||
import '../../../core/theme/theme_provider.dart';
|
||||
import '../../business/providers/business_mode_provider.dart';
|
||||
import '../../business/providers/business_provider.dart';
|
||||
import '../../projects/providers/project_mode_provider.dart';
|
||||
import '../../projects/providers/project_provider.dart';
|
||||
import '../../inventory/providers/products_provider.dart';
|
||||
import '../../inventory/providers/product_categories_provider.dart';
|
||||
import '../../inventory/providers/inventory_items_provider.dart';
|
||||
import '../../inventory/providers/inventory_valuation_provider.dart';
|
||||
import '../../inventory/providers/commodity_rates_provider.dart';
|
||||
import '../../vendor/providers/vendors_provider.dart';
|
||||
import '../../vendor/providers/purchase_orders_provider.dart';
|
||||
import '../../sales/providers/customers_provider.dart';
|
||||
import '../../sales/providers/invoices_provider.dart';
|
||||
import '../../business/presentation/settings/business_settings_screen.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ProfileScreen extends ConsumerStatefulWidget {
|
||||
const ProfileScreen({super.key});
|
||||
@@ -86,6 +99,35 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen>
|
||||
|
||||
Future<void> _logout() async {
|
||||
await DioClient().clearToken();
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('is_business_mode');
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
ref.invalidate(categoryProvider);
|
||||
ref.invalidate(transactionProvider);
|
||||
ref.invalidate(paginatedTransactionProvider);
|
||||
ref.invalidate(budgetProvider);
|
||||
ref.invalidate(recurringTransactionProvider);
|
||||
ref.invalidate(walletProvider);
|
||||
ref.invalidate(invitationProvider);
|
||||
ref.invalidate(businessProfileProvider);
|
||||
ref.invalidate(businessFeatureProvider);
|
||||
ref.invalidate(businessModeProvider);
|
||||
ref.invalidate(productsProvider);
|
||||
ref.invalidate(productCategoriesProvider);
|
||||
ref.invalidate(inventoryItemsProvider);
|
||||
ref.invalidate(inventoryValuationProvider);
|
||||
ref.invalidate(commodityRatesProvider);
|
||||
ref.invalidate(vendorsProvider);
|
||||
ref.invalidate(purchaseOrdersProvider);
|
||||
ref.invalidate(customersProvider);
|
||||
ref.invalidate(invoicesProvider);
|
||||
ref.invalidate(projectsProvider);
|
||||
ref.invalidate(authControllerProvider);
|
||||
} catch (_) {}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
|
||||
@@ -78,6 +78,12 @@ class AuthController extends AsyncNotifier<void> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await DioClient().clearToken();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
final authControllerProvider = AsyncNotifierProvider<AuthController, void>(() {
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'business_provider.dart';
|
||||
|
||||
class BusinessModeNotifier extends Notifier<bool> {
|
||||
@override
|
||||
bool build() {
|
||||
_loadState();
|
||||
return false; // Default until loaded
|
||||
final businessProfile = ref.watch(businessProfileProvider).asData?.value;
|
||||
if (businessProfile == null) {
|
||||
return false;
|
||||
}
|
||||
_loadPreference();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _loadState() async {
|
||||
Future<void> _loadPreference() async {
|
||||
final businessProfile = ref.read(businessProfileProvider).asData?.value;
|
||||
if (businessProfile == null) {
|
||||
state = false;
|
||||
return;
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
state = prefs.getBool('is_business_mode') ?? false;
|
||||
final saved = prefs.getBool('is_business_mode');
|
||||
if (saved != null) {
|
||||
state = saved;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleMode() async {
|
||||
final businessProfile = ref.read(businessProfileProvider).asData?.value;
|
||||
if (businessProfile == null) {
|
||||
state = false;
|
||||
return;
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
state = !state;
|
||||
await prefs.setBool('is_business_mode', state);
|
||||
|
||||
@@ -41,7 +41,7 @@ class Product {
|
||||
this.size,
|
||||
this.priceCalcRule = 'MANUAL',
|
||||
this.autoCalculatePrice = false,
|
||||
this.purityFactor = 1.0,
|
||||
this.purityFactor,
|
||||
this.makingCharges = 0.0,
|
||||
this.makingChargesType = 'FLAT',
|
||||
this.wastagePercentage = 0.0,
|
||||
@@ -72,8 +72,7 @@ class Product {
|
||||
autoCalculatePrice:
|
||||
json['autoCalculatePrice'] ?? json['auto_calculate_price'] ?? false,
|
||||
purityFactor:
|
||||
(json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble() ??
|
||||
1.0,
|
||||
(json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble(),
|
||||
makingCharges:
|
||||
(json['makingCharges'] ?? json['making_charges'] as num?)
|
||||
?.toDouble() ??
|
||||
|
||||
@@ -24,6 +24,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _hsnController = TextEditingController();
|
||||
final _gstController = TextEditingController();
|
||||
final _makingChargesController = TextEditingController();
|
||||
String _makingChargesType = 'PER_GRAM';
|
||||
|
||||
// Basic
|
||||
String _name = '';
|
||||
@@ -55,6 +57,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
_uomId = p.uomId;
|
||||
_color = p.color ?? '';
|
||||
_gstController.text = p.gstRate != null && p.gstRate! > 0 ? p.gstRate.toString() : '';
|
||||
_makingChargesController.text = p.makingCharges != null && p.makingCharges! > 0 ? p.makingCharges!.toString() : '';
|
||||
_makingChargesType = p.makingChargesType ?? 'PER_GRAM';
|
||||
|
||||
if (p.imageIds.isNotEmpty) {
|
||||
_existingImageIds.addAll(p.imageIds);
|
||||
@@ -75,6 +79,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
void dispose() {
|
||||
_hsnController.dispose();
|
||||
_gstController.dispose();
|
||||
_makingChargesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -142,6 +147,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
uomId: _uomId,
|
||||
color: _color.isNotEmpty ? _color : null,
|
||||
gstRate: double.tryParse(_gstController.text) ?? 0,
|
||||
makingCharges: double.tryParse(_makingChargesController.text) ?? 0.0,
|
||||
makingChargesType: _makingChargesType,
|
||||
priceCalcRule: 'MANUAL',
|
||||
autoCalculatePrice: false,
|
||||
);
|
||||
@@ -245,6 +252,12 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
if (val.defaultGst != null && val.defaultGst! > 0) {
|
||||
_gstController.text = val.defaultGst.toString();
|
||||
}
|
||||
if (_makingChargesController.text.isEmpty && val.defaultMakingCharge != null && val.defaultMakingCharge! > 0) {
|
||||
_makingChargesController.text = val.defaultMakingCharge!.toString();
|
||||
}
|
||||
if (val.makingChargeType != null && val.makingChargeType!.isNotEmpty) {
|
||||
_makingChargesType = val.makingChargeType!;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -284,6 +297,36 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _makingChargesController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Making Charges',
|
||||
prefixIcon: Icon(LucideIcons.hammer),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _makingChargesType,
|
||||
decoration: const InputDecoration(labelText: 'Charge Type'),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')),
|
||||
DropdownMenuItem(value: 'PER_PIECE', child: Text('Per Piece')),
|
||||
DropdownMenuItem(value: 'PERCENTAGE', child: Text('Percentage %')),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null) setState(() => _makingChargesType = val);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
const Text('Product Images', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../business/providers/business_mode_provider.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'dart:ui';
|
||||
import '../../../core/utils/purity_utils.dart';
|
||||
|
||||
class CategoryManagementScreen extends ConsumerStatefulWidget {
|
||||
const CategoryManagementScreen({super.key});
|
||||
@@ -272,7 +273,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
_hsnController.text = c.defaultHsn ?? '';
|
||||
_gstController.text = c.defaultGst?.toString() ?? '';
|
||||
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
||||
_purityFactorController.text = (c.purityFactor ?? 1.0).toString();
|
||||
_purityFactorController.text = formatPurity(c.purityFactor);
|
||||
_huidRequired = c.huidRequired;
|
||||
_commodityCode = c.commodityCode ?? 'XAU';
|
||||
|
||||
@@ -307,7 +308,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
parentCategoryId: _selectedParentId,
|
||||
hasChild: !_isLeaf,
|
||||
commodityCode: _isLeaf ? _commodityCode : null,
|
||||
purityFactor: _isLeaf ? (double.tryParse(_purityFactorController.text) ?? 1.0) : 1.0,
|
||||
purityFactor: _isLeaf ? normalizePurity(double.tryParse(_purityFactorController.text)) : 1.0,
|
||||
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
||||
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
||||
huidRequired: _isLeaf ? _huidRequired : false,
|
||||
@@ -483,7 +484,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
controller: _purityFactorController,
|
||||
label: 'Purity (e.g. 0.916)',
|
||||
label: 'Purity (0 to 1, e.g. 0.916)',
|
||||
icon: LucideIcons.gem,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../providers/commodity_rates_provider.dart';
|
||||
import '../domain/commodity_rate.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/utils/snackbar_service.dart';
|
||||
import '../../../core/utils/purity_utils.dart';
|
||||
|
||||
class DailyRatesScreen extends ConsumerStatefulWidget {
|
||||
const DailyRatesScreen({super.key});
|
||||
@@ -699,7 +700,7 @@ class _DailyRatesScreenState extends ConsumerState<DailyRatesScreen> {
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: linkedCategories.map((cat) {
|
||||
final purity = cat.purityFactor ?? 1.0;
|
||||
final purity = normalizePurity(cat.purityFactor);
|
||||
final effectiveUnitRate = currentRate * purity;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
@@ -725,7 +726,7 @@ class _DailyRatesScreenState extends ConsumerState<DailyRatesScreen> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'${(purity * 100).toStringAsFixed(1)}% (₹${effectiveUnitRate.toStringAsFixed(0)}/g)',
|
||||
'${formatPurity(purity)} (₹${effectiveUnitRate.toStringAsFixed(0)}/g)',
|
||||
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: color),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -133,6 +133,15 @@ class ProductDetailScreen extends ConsumerWidget {
|
||||
"GST Rate",
|
||||
"${p.gstRate?.toStringAsFixed(1) ?? '0'}%",
|
||||
),
|
||||
if (p.makingCharges != null && p.makingCharges! > 0)
|
||||
_buildDetailRow(
|
||||
"Making Charges",
|
||||
p.makingChargesType == 'PERCENTAGE'
|
||||
? "${p.makingCharges}%"
|
||||
: (p.makingChargesType == 'PER_PIECE'
|
||||
? "₹${p.makingCharges!.toStringAsFixed(2)} / pc"
|
||||
: "₹${p.makingCharges!.toStringAsFixed(2)} / g"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:kifi_app/features/vendor/presentation/purchase_order_details_screen.dart';
|
||||
import 'package:kifi_app/features/vendor/providers/purchase_orders_provider.dart';
|
||||
import 'package:kifi_app/core/utils/purity_utils.dart';
|
||||
|
||||
class StockLedgerTab extends ConsumerStatefulWidget {
|
||||
final Product product;
|
||||
@@ -71,7 +72,7 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
).firstOrNull;
|
||||
|
||||
final String unit = category?.baseUnit ?? 'g';
|
||||
final double purityFactor = category?.purityFactor ?? (widget.product.purityFactor ?? 1.0);
|
||||
final double purityFactor = resolvePurity(categoryPurity: category?.purityFactor, productPurity: widget.product.purityFactor);
|
||||
|
||||
double commodityRate = 0.0;
|
||||
if (category?.commodityCode != null && commodityRatesState.value != null) {
|
||||
@@ -221,6 +222,23 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.amber.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
'Purity: ${formatPurity(double.tryParse(item.purity ?? '') ?? purityFactor)}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,16 +3,26 @@ class InvoiceItem {
|
||||
final int? invoiceId;
|
||||
final int? productId;
|
||||
final int? inventoryItemId;
|
||||
final String? productName;
|
||||
final String? categoryName;
|
||||
final String? commodityCode;
|
||||
final String? sku;
|
||||
final String? huid;
|
||||
final String? hsnCode;
|
||||
|
||||
final String? description;
|
||||
final double quantity;
|
||||
final double unitPrice;
|
||||
final double taxRate;
|
||||
final double discount;
|
||||
final double? weight;
|
||||
final double unitPrice; // metal rate
|
||||
final double makingCharge;
|
||||
final String makingChargesType; // PER_GRAM, PER_PIECE, PERCENTAGE
|
||||
final double taxRate;
|
||||
final double cgst;
|
||||
final double sgst;
|
||||
final double igst;
|
||||
final double discount;
|
||||
final double otherCharges;
|
||||
final String? photoUrl;
|
||||
final String? localPhotoPath;
|
||||
final double total;
|
||||
|
||||
InvoiceItem({
|
||||
@@ -20,15 +30,26 @@ class InvoiceItem {
|
||||
this.invoiceId,
|
||||
this.productId,
|
||||
this.inventoryItemId,
|
||||
this.productName,
|
||||
this.categoryName,
|
||||
this.commodityCode,
|
||||
this.hsnCode,
|
||||
this.sku,
|
||||
this.huid,
|
||||
this.description,
|
||||
required this.quantity,
|
||||
this.weight,
|
||||
required this.unitPrice,
|
||||
this.taxRate = 0.0,
|
||||
this.discount = 0.0,
|
||||
this.makingCharge = 0.0,
|
||||
this.makingChargesType = 'PER_GRAM',
|
||||
this.taxRate = 0.0,
|
||||
this.cgst = 0.0,
|
||||
this.sgst = 0.0,
|
||||
this.igst = 0.0,
|
||||
this.discount = 0.0,
|
||||
this.otherCharges = 0.0,
|
||||
this.photoUrl,
|
||||
this.localPhotoPath,
|
||||
required this.total,
|
||||
});
|
||||
|
||||
@@ -37,15 +58,26 @@ class InvoiceItem {
|
||||
int? invoiceId,
|
||||
int? productId,
|
||||
int? inventoryItemId,
|
||||
String? productName,
|
||||
String? categoryName,
|
||||
String? commodityCode,
|
||||
String? hsnCode,
|
||||
String? sku,
|
||||
String? huid,
|
||||
String? description,
|
||||
double? quantity,
|
||||
double? weight,
|
||||
double? unitPrice,
|
||||
double? taxRate,
|
||||
double? discount,
|
||||
double? makingCharge,
|
||||
String? makingChargesType,
|
||||
double? taxRate,
|
||||
double? cgst,
|
||||
double? sgst,
|
||||
double? igst,
|
||||
double? discount,
|
||||
double? otherCharges,
|
||||
String? photoUrl,
|
||||
String? localPhotoPath,
|
||||
double? total,
|
||||
}) {
|
||||
return InvoiceItem(
|
||||
@@ -53,15 +85,26 @@ class InvoiceItem {
|
||||
invoiceId: invoiceId ?? this.invoiceId,
|
||||
productId: productId ?? this.productId,
|
||||
inventoryItemId: inventoryItemId ?? this.inventoryItemId,
|
||||
productName: productName ?? this.productName,
|
||||
categoryName: categoryName ?? this.categoryName,
|
||||
commodityCode: commodityCode ?? this.commodityCode,
|
||||
hsnCode: hsnCode ?? this.hsnCode,
|
||||
sku: sku ?? this.sku,
|
||||
huid: huid ?? this.huid,
|
||||
description: description ?? this.description,
|
||||
quantity: quantity ?? this.quantity,
|
||||
weight: weight ?? this.weight,
|
||||
unitPrice: unitPrice ?? this.unitPrice,
|
||||
taxRate: taxRate ?? this.taxRate,
|
||||
discount: discount ?? this.discount,
|
||||
makingCharge: makingCharge ?? this.makingCharge,
|
||||
makingChargesType: makingChargesType ?? this.makingChargesType,
|
||||
taxRate: taxRate ?? this.taxRate,
|
||||
cgst: cgst ?? this.cgst,
|
||||
sgst: sgst ?? this.sgst,
|
||||
igst: igst ?? this.igst,
|
||||
discount: discount ?? this.discount,
|
||||
otherCharges: otherCharges ?? this.otherCharges,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
localPhotoPath: localPhotoPath ?? this.localPhotoPath,
|
||||
total: total ?? this.total,
|
||||
);
|
||||
}
|
||||
@@ -69,19 +112,29 @@ class InvoiceItem {
|
||||
factory InvoiceItem.fromJson(Map<String, dynamic> json) {
|
||||
return InvoiceItem(
|
||||
id: json['id'],
|
||||
invoiceId: json['invoiceId'],
|
||||
productId: json['productId'],
|
||||
inventoryItemId: json['inventoryItemId'],
|
||||
invoiceId: json['invoiceId'] ?? json['invoice_id'],
|
||||
productId: json['productId'] ?? json['product_id'],
|
||||
inventoryItemId: json['inventoryItemId'] ?? json['inventory_item_id'],
|
||||
productName: json['productName'] ?? json['product_name'],
|
||||
categoryName: json['categoryName'] ?? json['category_name'],
|
||||
commodityCode: json['commodityCode'] ?? json['commodity_code'],
|
||||
hsnCode: json['hsnCode'] ?? json['hsn_code'],
|
||||
sku: json['sku'],
|
||||
huid: json['huid'],
|
||||
description: json['description'],
|
||||
quantity: json['quantity'].toDouble(),
|
||||
unitPrice: json['unitPrice'].toDouble(),
|
||||
taxRate: json['taxRate']?.toDouble() ?? 0.0,
|
||||
discount: json['discount']?.toDouble() ?? 0.0,
|
||||
makingCharge: json['makingCharge']?.toDouble() ?? 0.0,
|
||||
otherCharges: json['otherCharges']?.toDouble() ?? 0.0,
|
||||
total: json['total'].toDouble(),
|
||||
quantity: (json['quantity'] as num?)?.toDouble() ?? 1.0,
|
||||
weight: (json['weight'] as num?)?.toDouble(),
|
||||
unitPrice: (json['unitPrice'] ?? json['unit_price'] as num?)?.toDouble() ?? 0.0,
|
||||
makingCharge: (json['makingCharge'] ?? json['making_charge'] as num?)?.toDouble() ?? 0.0,
|
||||
makingChargesType: json['makingChargesType'] ?? json['making_charges_type'] ?? 'PER_GRAM',
|
||||
taxRate: (json['taxRate'] ?? json['tax_rate'] as num?)?.toDouble() ?? 0.0,
|
||||
cgst: (json['cgst'] as num?)?.toDouble() ?? 0.0,
|
||||
sgst: (json['sgst'] as num?)?.toDouble() ?? 0.0,
|
||||
igst: (json['igst'] as num?)?.toDouble() ?? 0.0,
|
||||
discount: (json['discount'] as num?)?.toDouble() ?? 0.0,
|
||||
otherCharges: (json['otherCharges'] ?? json['other_charges'] as num?)?.toDouble() ?? 0.0,
|
||||
photoUrl: json['photoUrl'] ?? json['photo_url'],
|
||||
total: (json['total'] as num?)?.toDouble() ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,15 +144,25 @@ class InvoiceItem {
|
||||
if (invoiceId != null) data['invoiceId'] = invoiceId;
|
||||
if (productId != null) data['productId'] = productId;
|
||||
if (inventoryItemId != null) data['inventoryItemId'] = inventoryItemId;
|
||||
if (productName != null) data['productName'] = productName;
|
||||
if (categoryName != null) data['categoryName'] = categoryName;
|
||||
if (commodityCode != null) data['commodityCode'] = commodityCode;
|
||||
if (hsnCode != null) data['hsnCode'] = hsnCode;
|
||||
if (sku != null) data['sku'] = sku;
|
||||
if (huid != null) data['huid'] = huid;
|
||||
if (description != null) data['description'] = description;
|
||||
data['quantity'] = quantity;
|
||||
if (weight != null) data['weight'] = weight;
|
||||
data['unitPrice'] = unitPrice;
|
||||
data['taxRate'] = taxRate;
|
||||
data['discount'] = discount;
|
||||
data['makingCharge'] = makingCharge;
|
||||
data['makingChargesType'] = makingChargesType;
|
||||
data['taxRate'] = taxRate;
|
||||
data['cgst'] = cgst;
|
||||
data['sgst'] = sgst;
|
||||
data['igst'] = igst;
|
||||
data['discount'] = discount;
|
||||
data['otherCharges'] = otherCharges;
|
||||
if (photoUrl != null) data['photoUrl'] = photoUrl;
|
||||
data['total'] = total;
|
||||
return data;
|
||||
}
|
||||
@@ -113,6 +176,9 @@ class Invoice {
|
||||
final DateTime? dueDate;
|
||||
final double subtotal;
|
||||
final double taxTotal;
|
||||
final double cgstTotal;
|
||||
final double sgstTotal;
|
||||
final double igstTotal;
|
||||
final double discountTotal;
|
||||
final double totalAmount;
|
||||
final double amountPaid;
|
||||
@@ -121,6 +187,7 @@ class Invoice {
|
||||
final int? paymentWalletId;
|
||||
final String status;
|
||||
final String? notes;
|
||||
final String? invoiceUrl;
|
||||
final bool isEmi;
|
||||
final double? emiAmount;
|
||||
final String? emiCycle;
|
||||
@@ -136,6 +203,9 @@ class Invoice {
|
||||
this.dueDate,
|
||||
required this.subtotal,
|
||||
this.taxTotal = 0.0,
|
||||
this.cgstTotal = 0.0,
|
||||
this.sgstTotal = 0.0,
|
||||
this.igstTotal = 0.0,
|
||||
this.discountTotal = 0.0,
|
||||
required this.totalAmount,
|
||||
this.amountPaid = 0.0,
|
||||
@@ -144,6 +214,7 @@ class Invoice {
|
||||
this.paymentWalletId,
|
||||
this.status = 'DRAFT',
|
||||
this.notes,
|
||||
this.invoiceUrl,
|
||||
this.isEmi = false,
|
||||
this.emiAmount,
|
||||
this.emiCycle,
|
||||
@@ -155,28 +226,34 @@ class Invoice {
|
||||
factory Invoice.fromJson(Map<String, dynamic> json) {
|
||||
return Invoice(
|
||||
id: json['id'],
|
||||
customerId: json['customerId'],
|
||||
invoiceNumber: json['invoiceNumber'],
|
||||
issueDate: DateTime.parse(json['issueDate']),
|
||||
dueDate: json['dueDate'] != null ? DateTime.parse(json['dueDate']) : null,
|
||||
subtotal: json['subtotal'].toDouble(),
|
||||
taxTotal: json['taxTotal']?.toDouble() ?? 0.0,
|
||||
discountTotal: json['discountTotal']?.toDouble() ?? 0.0,
|
||||
totalAmount: json['totalAmount'].toDouble(),
|
||||
amountPaid: json['amountPaid']?.toDouble() ?? 0.0,
|
||||
customerId: json['customerId'] ?? json['customer_id'],
|
||||
invoiceNumber: json['invoiceNumber'] ?? json['invoice_number'] ?? '',
|
||||
issueDate: json['issueDate'] != null
|
||||
? DateTime.parse(json['issueDate'])
|
||||
: (json['issue_date'] != null ? DateTime.parse(json['issue_date']) : DateTime.now()),
|
||||
dueDate: json['dueDate'] != null ? DateTime.parse(json['dueDate']) : (json['due_date'] != null ? DateTime.parse(json['due_date']) : null),
|
||||
subtotal: (json['subtotal'] as num?)?.toDouble() ?? 0.0,
|
||||
taxTotal: (json['taxTotal'] ?? json['tax_total'] as num?)?.toDouble() ?? 0.0,
|
||||
cgstTotal: (json['cgstTotal'] ?? json['cgst_total'] as num?)?.toDouble() ?? 0.0,
|
||||
sgstTotal: (json['sgstTotal'] ?? json['sgst_total'] as num?)?.toDouble() ?? 0.0,
|
||||
igstTotal: (json['igstTotal'] ?? json['igst_total'] as num?)?.toDouble() ?? 0.0,
|
||||
discountTotal: (json['discountTotal'] ?? json['discount_total'] as num?)?.toDouble() ?? 0.0,
|
||||
totalAmount: (json['totalAmount'] ?? json['total_amount'] as num?)?.toDouble() ?? 0.0,
|
||||
amountPaid: (json['amountPaid'] ?? json['amount_paid'] as num?)?.toDouble() ?? 0.0,
|
||||
nextPaymentDate: json['nextPaymentDate'] != null
|
||||
? DateTime.parse(json['nextPaymentDate'])
|
||||
: null,
|
||||
paymentMethod: json['paymentMethod'],
|
||||
paymentWalletId: json['paymentWalletId'],
|
||||
: (json['next_payment_date'] != null ? DateTime.parse(json['next_payment_date']) : null),
|
||||
paymentMethod: json['paymentMethod'] ?? json['payment_method'],
|
||||
paymentWalletId: json['paymentWalletId'] ?? json['payment_wallet_id'],
|
||||
status: json['status'] ?? 'DRAFT',
|
||||
notes: json['notes'],
|
||||
isEmi: json['isEmi'] ?? false,
|
||||
emiAmount: json['emiAmount']?.toDouble(),
|
||||
emiCycle: json['emiCycle'],
|
||||
invoiceUrl: json['invoiceUrl'] ?? json['invoice_url'],
|
||||
isEmi: json['isEmi'] ?? json['is_emi'] ?? false,
|
||||
emiAmount: (json['emiAmount'] ?? json['emi_amount'] as num?)?.toDouble(),
|
||||
emiCycle: json['emiCycle'] ?? json['emi_cycle'],
|
||||
emiStartDate: json['emiStartDate'] != null
|
||||
? DateTime.parse(json['emiStartDate'])
|
||||
: null,
|
||||
: (json['emi_start_date'] != null ? DateTime.parse(json['emi_start_date']) : null),
|
||||
items: json['items'] != null
|
||||
? (json['items'] as List).map((i) => InvoiceItem.fromJson(i)).toList()
|
||||
: [],
|
||||
@@ -194,29 +271,35 @@ class Invoice {
|
||||
if (customerId != null) data['customerId'] = customerId;
|
||||
data['invoiceNumber'] = invoiceNumber;
|
||||
data['issueDate'] = issueDate.toIso8601String().split('T')[0];
|
||||
if (dueDate != null)
|
||||
if (dueDate != null) {
|
||||
data['dueDate'] = dueDate!.toIso8601String().split('T')[0];
|
||||
}
|
||||
data['subtotal'] = subtotal;
|
||||
data['taxTotal'] = taxTotal;
|
||||
data['cgstTotal'] = cgstTotal;
|
||||
data['sgstTotal'] = sgstTotal;
|
||||
data['igstTotal'] = igstTotal;
|
||||
data['discountTotal'] = discountTotal;
|
||||
data['totalAmount'] = totalAmount;
|
||||
if (amountPaid > 0) data['amountPaid'] = amountPaid;
|
||||
if (nextPaymentDate != null)
|
||||
data['nextPaymentDate'] = nextPaymentDate!.toIso8601String().split(
|
||||
'T',
|
||||
)[0];
|
||||
if (nextPaymentDate != null) {
|
||||
data['nextPaymentDate'] = nextPaymentDate!.toIso8601String().split('T')[0];
|
||||
}
|
||||
if (paymentMethod != null) data['paymentMethod'] = paymentMethod;
|
||||
if (paymentWalletId != null) data['paymentWalletId'] = paymentWalletId;
|
||||
data['status'] = status;
|
||||
if (notes != null) data['notes'] = notes;
|
||||
if (invoiceUrl != null) data['invoiceUrl'] = invoiceUrl;
|
||||
data['isEmi'] = isEmi;
|
||||
if (emiAmount != null) data['emiAmount'] = emiAmount;
|
||||
if (emiCycle != null) data['emiCycle'] = emiCycle;
|
||||
if (emiStartDate != null)
|
||||
if (emiStartDate != null) {
|
||||
data['emiStartDate'] = emiStartDate!.toIso8601String().split('T')[0];
|
||||
}
|
||||
data['items'] = items.map((i) => i.toJson()).toList();
|
||||
if (payments != null)
|
||||
if (payments != null) {
|
||||
data['payments'] = payments!.map((i) => i.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -243,14 +326,14 @@ class InvoicePayment {
|
||||
factory InvoicePayment.fromJson(Map<String, dynamic> json) {
|
||||
return InvoicePayment(
|
||||
id: json['id'],
|
||||
invoiceId: json['invoiceId'],
|
||||
amount: json['amount'].toDouble(),
|
||||
invoiceId: json['invoiceId'] ?? json['invoice_id'],
|
||||
amount: (json['amount'] as num?)?.toDouble() ?? 0.0,
|
||||
paymentDate: json['paymentDate'] != null
|
||||
? DateTime.parse(json['paymentDate'])
|
||||
: null,
|
||||
paymentMethod: json['paymentMethod'] ?? 'Cash',
|
||||
emiInstallmentNumber: json['emiInstallmentNumber'],
|
||||
walletId: json['walletId'],
|
||||
: (json['payment_date'] != null ? DateTime.parse(json['payment_date']) : null),
|
||||
paymentMethod: json['paymentMethod'] ?? json['payment_method'] ?? 'Cash',
|
||||
emiInstallmentNumber: json['emiInstallmentNumber'] ?? json['emi_installment_number'],
|
||||
walletId: json['walletId'] ?? json['wallet_id'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -259,11 +342,13 @@ class InvoicePayment {
|
||||
if (id != null) data['id'] = id;
|
||||
if (invoiceId != null) data['invoiceId'] = invoiceId;
|
||||
data['amount'] = amount;
|
||||
if (paymentDate != null)
|
||||
if (paymentDate != null) {
|
||||
data['paymentDate'] = paymentDate!.toIso8601String().split('T')[0];
|
||||
}
|
||||
data['paymentMethod'] = paymentMethod;
|
||||
if (emiInstallmentNumber != null)
|
||||
if (emiInstallmentNumber != null) {
|
||||
data['emiInstallmentNumber'] = emiInstallmentNumber;
|
||||
}
|
||||
if (walletId != null) data['walletId'] = walletId;
|
||||
return data;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,14 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../core/widgets/shimmer_loading.dart';
|
||||
import '../providers/invoices_provider.dart';
|
||||
import '../domain/invoice.dart';
|
||||
import '../providers/customers_provider.dart';
|
||||
import '../domain/customer.dart';
|
||||
import 'invoice_builder_screen.dart';
|
||||
import 'invoice_details_screen.dart';
|
||||
import '../../transactions/providers/providers.dart';
|
||||
import 'widgets/receive_payment_sheet.dart';
|
||||
import '../providers/customers_provider.dart';
|
||||
|
||||
class InvoicesListScreen extends ConsumerStatefulWidget {
|
||||
const InvoicesListScreen({super.key});
|
||||
@@ -24,277 +22,115 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
|
||||
String _getStatusLabel(Invoice invoice) {
|
||||
if (invoice.isEmi && invoice.status != 'PAID') return 'EMI';
|
||||
if (invoice.status == 'PAID') return 'Fully Paid';
|
||||
if (invoice.status == 'PARTIAL') return 'Partially Paid';
|
||||
if (invoice.status == 'PAID' || invoice.amountPaid >= invoice.totalAmount && invoice.totalAmount > 0) return 'PAID';
|
||||
if (invoice.amountPaid > 0) return 'PARTIAL';
|
||||
return invoice.status;
|
||||
}
|
||||
|
||||
Color _getStatusColor(Invoice invoice) {
|
||||
if (invoice.isEmi && invoice.status != 'PAID') return Colors.purple;
|
||||
switch (invoice.status) {
|
||||
case 'DRAFT':
|
||||
return Colors.grey;
|
||||
case 'FINALIZED':
|
||||
return Colors.orange;
|
||||
final label = _getStatusLabel(invoice);
|
||||
switch (label) {
|
||||
case 'PAID':
|
||||
return Colors.green;
|
||||
case 'PARTIAL':
|
||||
return Colors.blue;
|
||||
case 'FINALIZED':
|
||||
return Colors.orange;
|
||||
case 'OVERDUE':
|
||||
return Colors.red;
|
||||
case 'CANCELLED':
|
||||
return Colors.black;
|
||||
case 'DRAFT':
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
void _showPaymentHistory(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Invoice invoice,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Payment History: ${invoice.invoiceNumber}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FutureBuilder<List<InvoicePayment>>(
|
||||
future: ref
|
||||
.read(invoicesProvider.notifier)
|
||||
.fetchPaymentsForInvoice(invoice.id!),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('Error: ${snapshot.error}'));
|
||||
}
|
||||
final payments = snapshot.data;
|
||||
if (payments == null || payments.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Text('No payments recorded yet.'),
|
||||
);
|
||||
}
|
||||
final wallets = ref.read(walletProvider).value ?? [];
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.resolveWith(
|
||||
(states) => Colors.grey.shade100,
|
||||
),
|
||||
columnSpacing: 24,
|
||||
columns: const [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Date',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Wallet',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Method',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Amount',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: payments.map((p) {
|
||||
final walletName = wallets
|
||||
.firstWhere(
|
||||
(w) => w.id == p.walletId,
|
||||
orElse: () => wallets.first,
|
||||
)
|
||||
.name;
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text(
|
||||
p.paymentDate != null
|
||||
? DateFormat(
|
||||
'dd MMM yyyy',
|
||||
).format(p.paymentDate!)
|
||||
: '-',
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(p.walletId != null ? walletName : '-'),
|
||||
),
|
||||
DataCell(Text(p.paymentMethod ?? '-')),
|
||||
DataCell(
|
||||
Text(
|
||||
'₹${p.amount.toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
color: Colors.green.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showReceivePaymentSheet(BuildContext context, Invoice invoice) async {
|
||||
final result = await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ReceivePaymentSheet(invoice: invoice),
|
||||
);
|
||||
if (result == true) {
|
||||
ref.read(invoicesProvider.notifier).refresh();
|
||||
}
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final invoicesState = ref.watch(invoicesProvider);
|
||||
final customersState = ref.watch(customersProvider);
|
||||
final customers = customersState.value ?? [];
|
||||
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final formatCurrency = NumberFormat.currency(symbol: '₹');
|
||||
final formatDate = DateFormat('MMM dd, yyyy');
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
backgroundColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFC),
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Invoices',
|
||||
'Sales Invoices',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
backgroundColor: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||
foregroundColor: isDark ? Colors.white : Colors.black,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Unified Search Header
|
||||
Container(
|
||||
color: Colors.white,
|
||||
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search by Invoice # or Customer',
|
||||
hintText: 'Search Invoice # or Customer...',
|
||||
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: isDark ? Colors.white12 : Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: isDark ? Colors.white12 : Colors.grey.shade300),
|
||||
),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(LucideIcons.x, size: 20),
|
||||
icon: const Icon(LucideIcons.xCircle, size: 20),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
setState(() {
|
||||
_searchQuery = '';
|
||||
});
|
||||
setState(() => _searchQuery = '');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value.toLowerCase();
|
||||
});
|
||||
},
|
||||
onChanged: (val) => setState(() => _searchQuery = val.trim()),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: invoicesState.when(
|
||||
loading: () => ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) => const Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: ShimmerCard(),
|
||||
),
|
||||
),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
data: (allInvoices) {
|
||||
final invoices = allInvoices.where((invoice) {
|
||||
final matchesInvoice = invoice.invoiceNumber
|
||||
.toLowerCase()
|
||||
.contains(_searchQuery);
|
||||
final customer = customers.firstWhere(
|
||||
(c) => c.id == invoice.customerId,
|
||||
orElse: () => customers.first,
|
||||
);
|
||||
final customerName = invoice.customerId != null
|
||||
? customer.name.toLowerCase()
|
||||
: '';
|
||||
final matchesCustomer = customerName.contains(_searchQuery);
|
||||
return matchesInvoice || matchesCustomer;
|
||||
data: (invoices) {
|
||||
final customers = customersState.value ?? [];
|
||||
final filtered = invoices.where((inv) {
|
||||
final matchesInv = inv.invoiceNumber.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
final customer = customers.where((c) => c.id == inv.customerId).firstOrNull;
|
||||
final matchesCust = customer != null && customer.name.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
return matchesInv || matchesCust;
|
||||
}).toList();
|
||||
|
||||
if (invoices.isEmpty) {
|
||||
if (filtered.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(invoicesProvider.notifier).refresh();
|
||||
},
|
||||
onRefresh: () => ref.refresh(invoicesProvider.future),
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SizedBox(height: 100),
|
||||
SizedBox(height: 120),
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.receipt,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Icon(LucideIcons.fileText, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'No invoices found.',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
'No sales invoices found.',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -305,32 +141,25 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(invoicesProvider.notifier).refresh();
|
||||
},
|
||||
onRefresh: () => ref.refresh(invoicesProvider.future),
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: invoices.length,
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invoice = invoices[index];
|
||||
double remaining =
|
||||
invoice.totalAmount - invoice.amountPaid;
|
||||
final customer = invoice.customerId != null
|
||||
? customers.firstWhere(
|
||||
(c) => c.id == invoice.customerId,
|
||||
orElse: () => customers.first,
|
||||
)
|
||||
: null;
|
||||
final invoice = filtered[index];
|
||||
final customer = customers.where((c) => c.id == invoice.customerId).firstOrNull;
|
||||
final statusColor = _getStatusColor(invoice);
|
||||
final statusLabel = _getStatusLabel(invoice);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -341,13 +170,21 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
InvoiceDetailsScreen(invoice: invoice),
|
||||
),
|
||||
);
|
||||
if (invoice.status == 'DRAFT') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceBuilderScreen(existingInvoice: invoice),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceDetailsScreen(invoice: invoice),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
@@ -355,49 +192,28 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Row 1: Invoice Number + Status Pill
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
invoice.invoiceNumber,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (customer != null)
|
||||
Text(
|
||||
customer.name,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
invoice.invoiceNumber,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(
|
||||
invoice,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
color: statusColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: statusColor.withValues(alpha: 0.3), width: 0.8),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusLabel(invoice),
|
||||
statusLabel,
|
||||
style: TextStyle(
|
||||
color: _getStatusColor(invoice),
|
||||
color: statusColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -406,174 +222,50 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Row 2: Customer Name
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Issued: ${formatDate.format(invoice.issueDate)}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
if (invoice.dueDate != null)
|
||||
Text(
|
||||
'Due: ${formatDate.format(invoice.dueDate!)}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
if (invoice.nextPaymentDate != null &&
|
||||
remaining > 0)
|
||||
Text(
|
||||
'Next Pmt: ${formatDate.format(invoice.nextPaymentDate!)}',
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade700,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
formatCurrency.format(
|
||||
invoice.totalAmount,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
if (invoice.amountPaid > 0)
|
||||
Text(
|
||||
'Paid: ${formatCurrency.format(invoice.amountPaid)}',
|
||||
style: TextStyle(
|
||||
color: Colors.green.shade700,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (remaining > 0 &&
|
||||
invoice.status != 'DRAFT')
|
||||
Text(
|
||||
'Bal: ${formatCurrency.format(remaining)}',
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Icon(LucideIcons.user, size: 16, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
customer?.name ?? 'Walk-in Customer',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (invoice.items.isNotEmpty)
|
||||
Text(
|
||||
'${invoice.items.length} ${invoice.items.length == 1 ? "item" : "items"}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (invoice.isEmi &&
|
||||
invoice.emiAmount != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.purple.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.calendarClock,
|
||||
size: 16,
|
||||
color: Colors.purple,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'EMI: ${formatCurrency.format(invoice.emiAmount)} / ${invoice.emiCycle}',
|
||||
style: const TextStyle(
|
||||
color: Colors.purple,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const Divider(height: 24),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Row 3: Date & Total Amount
|
||||
Row(
|
||||
children: [
|
||||
if (invoice.amountPaid > 0)
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
_showPaymentHistory(
|
||||
context,
|
||||
ref,
|
||||
invoice,
|
||||
),
|
||||
icon: const Icon(
|
||||
LucideIcons.history,
|
||||
size: 14,
|
||||
),
|
||||
label: const Text(
|
||||
'History',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor:
|
||||
Colors.blue.shade700,
|
||||
side: BorderSide(
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(LucideIcons.calendar, size: 16, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
formatDate.format(invoice.issueDate),
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 13,
|
||||
),
|
||||
if (invoice.amountPaid > 0 &&
|
||||
remaining > 0)
|
||||
const SizedBox(width: 8),
|
||||
if (remaining > 0)
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () =>
|
||||
_showReceivePaymentSheet(
|
||||
context,
|
||||
invoice,
|
||||
),
|
||||
icon: const Icon(
|
||||
LucideIcons.indianRupee,
|
||||
size: 14,
|
||||
),
|
||||
label: const Text(
|
||||
'Receive',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Colors.blue.shade50,
|
||||
foregroundColor:
|
||||
Colors.blue.shade700,
|
||||
elevation: 0,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
formatCurrency.format(invoice.totalAmount),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -586,6 +278,8 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, stack) => Center(child: Text('Error: $e')),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -598,8 +292,9 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
);
|
||||
},
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: const Text('Create Invoice'),
|
||||
label: const Text('Create Invoice', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/widgets/premium_text_field.dart';
|
||||
import '../../../../core/widgets/smart_search_dropdown.dart';
|
||||
import '../../providers/customers_provider.dart';
|
||||
import '../../domain/customer.dart';
|
||||
import '../../../business/providers/indian_states_provider.dart';
|
||||
|
||||
class QuickAddCustomerSheet extends ConsumerStatefulWidget {
|
||||
const QuickAddCustomerSheet({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<QuickAddCustomerSheet> createState() => _QuickAddCustomerSheetState();
|
||||
}
|
||||
|
||||
class _QuickAddCustomerSheetState extends ConsumerState<QuickAddCustomerSheet> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameCtrl = TextEditingController();
|
||||
final _phoneCtrl = TextEditingController();
|
||||
final _addressCtrl = TextEditingController();
|
||||
final _gstinCtrl = TextEditingController();
|
||||
int? _selectedStateId;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_phoneCtrl.dispose();
|
||||
_addressCtrl.dispose();
|
||||
_gstinCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final customer = Customer(
|
||||
name: _nameCtrl.text.trim(),
|
||||
phone: _phoneCtrl.text.trim().isEmpty ? null : _phoneCtrl.text.trim(),
|
||||
address: _addressCtrl.text.trim().isEmpty ? null : _addressCtrl.text.trim(),
|
||||
gstin: _gstinCtrl.text.trim().isEmpty ? null : _gstinCtrl.text.trim(),
|
||||
stateId: _selectedStateId,
|
||||
);
|
||||
|
||||
final created = await ref.read(customersProvider.notifier).addCustomer(customer);
|
||||
if (mounted) Navigator.pop(context, created ?? true);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.chevronLeft),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Quick Add Customer',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
PremiumTextField(
|
||||
controller: _nameCtrl,
|
||||
labelText: 'Customer Name *',
|
||||
prefixIcon: const Icon(LucideIcons.user),
|
||||
textCapitalization: TextCapitalization.words,
|
||||
validator: (val) => val == null || val.isEmpty ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PremiumTextField(
|
||||
controller: _phoneCtrl,
|
||||
labelText: 'Phone Number',
|
||||
prefixIcon: const Icon(LucideIcons.phone),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final statesState = ref.watch(indianStatesProvider);
|
||||
return statesState.when(
|
||||
data: (states) => SmartSearchDropdown<int>(
|
||||
labelText: 'State',
|
||||
hintText: 'Select State',
|
||||
value: _selectedStateId,
|
||||
items: states.map((s) => s.id).toList(),
|
||||
itemAsString: (id) => states.firstWhere((s) => s.id == id).name,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_selectedStateId = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Text('Error loading states: $e'),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PremiumTextField(
|
||||
controller: _gstinCtrl,
|
||||
labelText: 'GSTIN (Optional)',
|
||||
prefixIcon: const Icon(LucideIcons.fileText),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PremiumTextField(
|
||||
controller: _addressCtrl,
|
||||
labelText: 'Address',
|
||||
prefixIcon: const Icon(LucideIcons.home),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _save,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: const Text('Save Customer', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addCustomer(Customer customer, {XFile? photo}) async {
|
||||
Future<Customer?> addCustomer(Customer customer, {XFile? photo}) async {
|
||||
try {
|
||||
final response = await DioClient().dio.post(
|
||||
'/customers',
|
||||
@@ -55,14 +55,18 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
||||
}
|
||||
|
||||
await refresh();
|
||||
if (response.data != null) {
|
||||
return Customer.fromJson(response.data);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException) {
|
||||
throw Exception(
|
||||
'Failed to add customer: ${e.response?.statusCode} - ${e.response?.data}',
|
||||
e.response?.data?['message'] ?? 'Failed to add customer',
|
||||
);
|
||||
}
|
||||
throw Exception('Failed to add customer: $e');
|
||||
throw Exception('Error adding customer: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> updateCustomer(int id, Customer customer, {XFile? photo}) async {
|
||||
|
||||
@@ -34,10 +34,14 @@ class InvoicesNotifier extends AsyncNotifier<List<Invoice>> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createInvoice(Invoice invoice) async {
|
||||
Future<Invoice?> createInvoice(Invoice invoice) async {
|
||||
try {
|
||||
await DioClient().dio.post('/invoices', data: invoice.toJson());
|
||||
final response = await DioClient().dio.post('/invoices', data: invoice.toJson());
|
||||
await refresh();
|
||||
if (response.data != null) {
|
||||
return Invoice.fromJson(response.data);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
if (e is DioException) {
|
||||
throw Exception(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../data/repository.dart';
|
||||
import '../data/models.dart';
|
||||
import 'providers.dart';
|
||||
|
||||
@@ -98,9 +97,8 @@ class PaginatedTransactionNotifier extends Notifier<PaginatedTransactionState> {
|
||||
isLoading: false,
|
||||
hasMore: _currentPage < totalPages,
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
state = state.copyWith(isLoading: false);
|
||||
print("Error loading paginated transactions: $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
@@ -1143,11 +1142,11 @@ class _PurchaseOrderDetailsScreenState
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50.withOpacity(0.5),
|
||||
color: Colors.blue.shade50.withValues(alpha: 0.5),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
|
||||
@@ -56,34 +56,44 @@ class _PurchaseOrdersListScreenState
|
||||
Widget build(BuildContext context) {
|
||||
final poState = ref.watch(purchaseOrdersProvider);
|
||||
final vendorsState = ref.watch(vendorsProvider);
|
||||
final darkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final formatCurrency = NumberFormat.currency(symbol: '₹');
|
||||
final formatDate = DateFormat('MMM dd, yyyy');
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
backgroundColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFC),
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Purchase Invoices',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
backgroundColor: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||
foregroundColor: isDark ? Colors.white : Colors.black,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search PO Number...',
|
||||
hintText: 'Search PO Number or Vendor...',
|
||||
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: isDark ? Colors.white12 : Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: isDark ? Colors.white12 : Colors.grey.shade300),
|
||||
),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(LucideIcons.xCircle),
|
||||
icon: const Icon(LucideIcons.xCircle, size: 20),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
setState(() => _searchQuery = '');
|
||||
@@ -91,32 +101,43 @@ class _PurchaseOrdersListScreenState
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (val) => setState(() => _searchQuery = val),
|
||||
onChanged: (val) => setState(() => _searchQuery = val.trim()),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: poState.when(
|
||||
data: (pos) {
|
||||
final vendors = vendorsState.value ?? [];
|
||||
final filtered = pos.where((p) {
|
||||
return p.poNumber.toLowerCase().contains(
|
||||
_searchQuery.toLowerCase(),
|
||||
);
|
||||
final matchesPo = p.poNumber.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
final vendor = vendors.where((v) => v.id == p.vendorId).firstOrNull;
|
||||
final matchesVendor = vendor != null && vendor.name.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
return matchesPo || matchesVendor;
|
||||
}).toList();
|
||||
|
||||
if (filtered.isEmpty) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.clipboardList,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'No purchase invoices found.',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 16),
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(purchaseOrdersProvider.future),
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SizedBox(height: 120),
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.clipboardList,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'No purchase invoices found.',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -130,20 +151,19 @@ class _PurchaseOrdersListScreenState
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
final po = filtered[index];
|
||||
final vendors = vendorsState.value ?? [];
|
||||
final vendor = vendors.firstWhere(
|
||||
(v) => v.id == po.vendorId,
|
||||
orElse: () => po.vendor!,
|
||||
);
|
||||
final vendor = vendors.where((v) => v.id == po.vendorId).firstOrNull;
|
||||
final statusColor = _getStatusColor(po);
|
||||
final statusLabel = _getStatusLabel(po);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: isDark ? const Color(0xFF1E293B) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -193,20 +213,17 @@ class _PurchaseOrdersListScreenState
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(
|
||||
po,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
20,
|
||||
),
|
||||
color: statusColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: statusColor.withValues(alpha: 0.3), width: 0.8),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusLabel(po),
|
||||
statusLabel,
|
||||
style: TextStyle(
|
||||
color: _getStatusColor(po),
|
||||
color: statusColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -218,24 +235,30 @@ class _PurchaseOrdersListScreenState
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.user,
|
||||
LucideIcons.truck,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
vendor.name,
|
||||
vendor?.name ?? po.vendor?.name ?? 'Vendor',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (po.items.isNotEmpty)
|
||||
Text(
|
||||
'${po.items.length} ${po.items.length == 1 ? "item" : "items"}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
@@ -245,20 +268,19 @@ class _PurchaseOrdersListScreenState
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
DateFormat(
|
||||
'MMM dd, yyyy',
|
||||
).format(po.issueDate),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
formatDate.format(po.issueDate),
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'₹${po.totalAmount.toStringAsFixed(2)}',
|
||||
formatCurrency.format(po.totalAmount),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -289,7 +311,9 @@ class _PurchaseOrdersListScreenState
|
||||
);
|
||||
},
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: const Text('New PO'),
|
||||
label: const Text('Create PO', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ class _VendorsListScreenState extends ConsumerState<VendorsListScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vendorsState = ref.watch(vendorsProvider);
|
||||
final darkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
@@ -82,7 +81,7 @@ class _VendorsListScreenState extends ConsumerState<VendorsListScreen> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
color: Colors.grey.withValues(alpha: 0.08),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -107,8 +106,8 @@ class _VendorsListScreenState extends ConsumerState<VendorsListScreen> {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Colors.orange.withOpacity(
|
||||
0.1,
|
||||
backgroundColor: Colors.orange.withValues(
|
||||
alpha: 0.1,
|
||||
),
|
||||
child: vendor.photoUrl != null
|
||||
? ClipOval(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../domain/purchase_order.dart';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../domain/vendor.dart';
|
||||
|
||||
Reference in New Issue
Block a user