Done Category, Product Catalogue, Customer, Vendor, Purchase Invoice Module
This commit is contained in:
@@ -34,7 +34,9 @@ class BusinessFeature {
|
||||
bomReductionStrategy: json['bomReductionStrategy'] ?? 'COMPONENTS_ONLY',
|
||||
stockDeductionOnInvoice: json['stockDeductionOnInvoice'] ?? true,
|
||||
barcodeSource: json['barcodeSource'] ?? 'SKU',
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,7 +73,8 @@ class BusinessFeature {
|
||||
purchaseManagement: purchaseManagement ?? this.purchaseManagement,
|
||||
multiLocation: multiLocation ?? this.multiLocation,
|
||||
bomReductionStrategy: bomReductionStrategy ?? this.bomReductionStrategy,
|
||||
stockDeductionOnInvoice: stockDeductionOnInvoice ?? this.stockDeductionOnInvoice,
|
||||
stockDeductionOnInvoice:
|
||||
stockDeductionOnInvoice ?? this.stockDeductionOnInvoice,
|
||||
barcodeSource: barcodeSource ?? this.barcodeSource,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
@@ -3,11 +3,7 @@ class IndianState {
|
||||
final String name;
|
||||
final String gstCode;
|
||||
|
||||
IndianState({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.gstCode,
|
||||
});
|
||||
IndianState({required this.id, required this.name, required this.gstCode});
|
||||
|
||||
factory IndianState.fromJson(Map<String, dynamic> json) {
|
||||
return IndianState(
|
||||
@@ -18,10 +14,6 @@ class IndianState {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'gstCode': gstCode,
|
||||
};
|
||||
return {'id': id, 'name': name, 'gstCode': gstCode};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/theme/nature_colors.dart';
|
||||
import '../../../inventory/presentation/product_list_screen.dart';
|
||||
import '../../../inventory/presentation/daily_rates_screen.dart';
|
||||
import '../../../inventory/presentation/quick_adjust_stock_screen.dart';
|
||||
import '../../../inventory/presentation/uoms_list_screen.dart';
|
||||
import '../../../inventory/presentation/category_management_screen.dart';
|
||||
import '../../../sales/presentation/customers_list_screen.dart';
|
||||
import '../../../sales/presentation/invoices_list_screen.dart';
|
||||
@@ -22,12 +22,13 @@ class BusinessHubScreen extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final businessState = ref.watch(businessProfileProvider);
|
||||
final profile = businessState.value;
|
||||
|
||||
final title = (profile?.businessName != null && profile!.businessName.isNotEmpty)
|
||||
? profile.businessName
|
||||
|
||||
final title =
|
||||
(profile?.businessName != null && profile!.businessName.isNotEmpty)
|
||||
? profile.businessName
|
||||
: 'Business Overview';
|
||||
final subtitle = (profile?.address != null && profile!.address!.isNotEmpty)
|
||||
? profile.address!
|
||||
final subtitle = (profile?.address != null && profile!.address!.isNotEmpty)
|
||||
? profile.address!
|
||||
: 'Manage your inventory and stock';
|
||||
|
||||
final featureState = ref.watch(businessFeatureProvider).value;
|
||||
@@ -35,236 +36,337 @@ class BusinessHubScreen extends ConsumerWidget {
|
||||
final bool showSales = featureState?.salesManagement ?? false;
|
||||
final bool showPurchase = featureState?.purchaseManagement ?? false;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => const BusinessProfileFormSheet(),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.2)),
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(businessProfileProvider);
|
||||
ref.invalidate(businessFeatureProvider);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => const BusinessProfileFormSheet(),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.briefcase,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.briefcase, color: Theme.of(context).colorScheme.primary, size: 32),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(subtitle, style: const TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 1.2,
|
||||
children: [
|
||||
if (showInventory) ...[
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Categories',
|
||||
'Manage catalog structure',
|
||||
LucideIcons.listTree,
|
||||
Colors.teal,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const CategoryManagementScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Products Catalog',
|
||||
'View all products',
|
||||
LucideIcons.packageSearch,
|
||||
Colors.blue,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ProductListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showSales)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Customers',
|
||||
'Manage clients',
|
||||
LucideIcons.users,
|
||||
NatureColors.getColor('RECEIVABLES'),
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const CustomersListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showPurchase)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Vendors',
|
||||
'Manage suppliers',
|
||||
LucideIcons.truck,
|
||||
Colors.orange,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const VendorsListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showPurchase)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Purchases',
|
||||
'Manage inward stock',
|
||||
LucideIcons.clipboardList,
|
||||
Colors.deepOrange,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const PurchaseOrdersListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showSales)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Sales & Invoices',
|
||||
'Create invoices',
|
||||
LucideIcons.receipt,
|
||||
Colors.green,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const InvoicesListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showInventory)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Daily Rates',
|
||||
'Sync live gold/silver rates',
|
||||
LucideIcons.refreshCw,
|
||||
Colors.orange,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const DailyRatesScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Reports',
|
||||
'Analytics & Valuation',
|
||||
LucideIcons.barChart2,
|
||||
Colors.indigo,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ReportsScreen()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 1.2,
|
||||
children: [
|
||||
if (showInventory) ...[
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Products Catalog',
|
||||
'View all products',
|
||||
LucideIcons.packageSearch,
|
||||
Colors.blue,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const ProductListScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Adjust Stock',
|
||||
'Quick Stock Edit',
|
||||
LucideIcons.arrowRightLeft,
|
||||
Colors.orange,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const QuickAdjustStockScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Units of Measure',
|
||||
'Manage measurements',
|
||||
LucideIcons.ruler,
|
||||
Colors.purple,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const UomsListScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Categories',
|
||||
'Manage catalog structure',
|
||||
LucideIcons.listTree,
|
||||
Colors.teal,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const CategoryManagementScreen())),
|
||||
),
|
||||
],
|
||||
if (showSales) ...[
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Sales & Invoices',
|
||||
'Create invoices',
|
||||
LucideIcons.receipt,
|
||||
Colors.green,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const InvoicesListScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Customers',
|
||||
'Manage clients',
|
||||
LucideIcons.users,
|
||||
NatureColors.getColor('RECEIVABLES'),
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomersListScreen())),
|
||||
),
|
||||
],
|
||||
if (showPurchase) ...[
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Vendors',
|
||||
'Manage suppliers',
|
||||
LucideIcons.truck,
|
||||
Colors.orange,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const VendorsListScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Purchase Orders',
|
||||
'Manage inward stock',
|
||||
LucideIcons.clipboardList,
|
||||
Colors.deepOrange,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const PurchaseOrdersListScreen())),
|
||||
),
|
||||
],
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Reports',
|
||||
'Analytics & Valuation',
|
||||
LucideIcons.barChart2,
|
||||
Colors.indigo,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const ReportsScreen())),
|
||||
if (showInventory) ...[
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Low Stock Alerts',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showInventory) ...[
|
||||
const SizedBox(height: 32),
|
||||
Text('Low Stock Alerts', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final productsState = ref.watch(productsProvider);
|
||||
return productsState.when(
|
||||
data: (products) {
|
||||
final lowStockProducts = products.where((p) {
|
||||
if (!(p.trackInventory ?? false)) return false;
|
||||
if (p.currentStock == null) return false;
|
||||
final minStock = p.minStock ?? 0;
|
||||
return p.currentStock! <= minStock;
|
||||
}).toList();
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final productsState = ref.watch(productsProvider);
|
||||
return productsState.when(
|
||||
data: (products) {
|
||||
final lowStockProducts = products.where((p) {
|
||||
if (p.currentStock == null) return false;
|
||||
if (p.currentStock == null) return false;
|
||||
final minStock = 0;
|
||||
return p.currentStock! <= minStock;
|
||||
}).toList();
|
||||
|
||||
if (lowStockProducts.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text('All products are adequately stocked.', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: lowStockProducts.length > 5 ? 5 : lowStockProducts.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final product = lowStockProducts[index];
|
||||
final isOutOfStock = product.currentStock! <= 0;
|
||||
if (lowStockProducts.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock ? Colors.red.withOpacity(0.05) : Colors.orange.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: isOutOfStock ? Colors.red.withOpacity(0.2) : Colors.orange.withOpacity(0.2)),
|
||||
color: Colors.grey.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock ? Colors.red.withOpacity(0.1) : Colors.orange.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.alertTriangle,
|
||||
color: isOutOfStock ? Colors.red : Colors.orange,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(product.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
if (product.sku != null && product.sku!.isNotEmpty)
|
||||
Text('SKU: ${product.sku}', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${product.currentStock} left',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOutOfStock ? Colors.red : Colors.orange.shade800,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Min: ${product.minStock ?? 0}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'All products are adequately stocked.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: lowStockProducts.length > 5
|
||||
? 5
|
||||
: lowStockProducts.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final product = lowStockProducts[index];
|
||||
final isOutOfStock = product.currentStock! <= 0;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock
|
||||
? Colors.red.withOpacity(0.05)
|
||||
: Colors.orange.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isOutOfStock
|
||||
? Colors.red.withOpacity(0.2)
|
||||
: Colors.orange.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock
|
||||
? Colors.red.withOpacity(0.1)
|
||||
: Colors.orange.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.alertTriangle,
|
||||
color: isOutOfStock
|
||||
? Colors.red
|
||||
: Colors.orange,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (product.sku != null &&
|
||||
product.sku!.isNotEmpty)
|
||||
Text(
|
||||
'SKU: ${product.sku}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${product.currentStock} left',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOutOfStock
|
||||
? Colors.red
|
||||
: Colors.orange.shade800,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Out of stock',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) =>
|
||||
Center(child: Text('Error: $error')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionCard(BuildContext context, String title, String subtitle, IconData icon, Color color, VoidCallback onTap) {
|
||||
Widget _buildActionCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String subtitle,
|
||||
IconData icon,
|
||||
Color color,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
@@ -280,9 +382,19 @@ class BusinessHubScreen extends ConsumerWidget {
|
||||
children: [
|
||||
Icon(icon, color: color, size: 28),
|
||||
const SizedBox(height: 12),
|
||||
Text(title, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(subtitle, style: TextStyle(color: color.withOpacity(0.7), fontSize: 12)),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(color: color.withOpacity(0.7), fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -13,9 +13,7 @@ class ReportsScreen extends ConsumerWidget {
|
||||
final formatCurrency = NumberFormat.currency(symbol: '₹');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Business Reports'),
|
||||
),
|
||||
appBar: AppBar(title: const Text('Business Reports')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -29,7 +27,10 @@ class ReportsScreen extends ConsumerWidget {
|
||||
content: valuationState.when(
|
||||
data: (val) => Text(
|
||||
'Total Estimated Value: ${formatCurrency.format(val)}',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, s) => Text('Error: $e'),
|
||||
@@ -41,7 +42,9 @@ class ReportsScreen extends ConsumerWidget {
|
||||
title: 'GST Report (Coming Soon)',
|
||||
icon: LucideIcons.fileText,
|
||||
color: Colors.orange,
|
||||
content: const Text('Export GSTR-1 & GSTR-3B formats based on invoices.'),
|
||||
content: const Text(
|
||||
'Export GSTR-1 & GSTR-3B formats based on invoices.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildReportCard(
|
||||
@@ -57,7 +60,8 @@ class ReportsScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReportCard(BuildContext context, {
|
||||
Widget _buildReportCard(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
@@ -81,7 +85,13 @@ class ReportsScreen extends ConsumerWidget {
|
||||
child: Icon(icon, color: color),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -7,10 +7,12 @@ class BusinessSettingsScreen extends ConsumerStatefulWidget {
|
||||
const BusinessSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<BusinessSettingsScreen> createState() => _BusinessSettingsScreenState();
|
||||
ConsumerState<BusinessSettingsScreen> createState() =>
|
||||
_BusinessSettingsScreenState();
|
||||
}
|
||||
|
||||
class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen> {
|
||||
class _BusinessSettingsScreenState
|
||||
extends ConsumerState<BusinessSettingsScreen> {
|
||||
bool _taxIncludedInPrice = false;
|
||||
bool _salesEnabled = true;
|
||||
bool _inventoryEnabled = true;
|
||||
@@ -48,11 +50,20 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: [
|
||||
const Text('Module Configuration', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
const Text(
|
||||
'Module Configuration',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Inventory Management'),
|
||||
subtitle: const Text('Track stock levels, multiple locations, and products'),
|
||||
subtitle: const Text(
|
||||
'Track stock levels, multiple locations, and products',
|
||||
),
|
||||
value: _inventoryEnabled,
|
||||
onChanged: (val) {
|
||||
setState(() => _inventoryEnabled = val);
|
||||
@@ -62,7 +73,9 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Sales & POS'),
|
||||
subtitle: const Text('Enable point of sale, invoicing, and receivables'),
|
||||
subtitle: const Text(
|
||||
'Enable point of sale, invoicing, and receivables',
|
||||
),
|
||||
value: _salesEnabled,
|
||||
onChanged: (val) {
|
||||
setState(() => _salesEnabled = val);
|
||||
@@ -72,7 +85,9 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Vendor & Purchases'),
|
||||
subtitle: const Text('Manage vendors, purchase orders, and inward stock'),
|
||||
subtitle: const Text(
|
||||
'Manage vendors, purchase invoices, and inward stock',
|
||||
),
|
||||
value: _purchaseEnabled,
|
||||
onChanged: (val) {
|
||||
setState(() => _purchaseEnabled = val);
|
||||
@@ -81,64 +96,105 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
secondary: const Icon(LucideIcons.truck),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const Text('Inventory Strategy', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
const Text(
|
||||
'Inventory Strategy',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final featureState = ref.watch(businessFeatureProvider);
|
||||
final currentStrategy = featureState.value?.bomReductionStrategy ?? 'COMPONENTS_ONLY';
|
||||
final deductParentStock = currentStrategy == 'PARENT_AND_COMPONENTS';
|
||||
final stockDeductionOnInvoice = featureState.value?.stockDeductionOnInvoice ?? true;
|
||||
|
||||
final currentStrategy =
|
||||
featureState.value?.bomReductionStrategy ?? 'COMPONENTS_ONLY';
|
||||
final deductParentStock =
|
||||
currentStrategy == 'PARENT_AND_COMPONENTS';
|
||||
final stockDeductionOnInvoice =
|
||||
featureState.value?.stockDeductionOnInvoice ?? true;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Deduct Parent Stock on BOM Sale'),
|
||||
subtitle: const Text('If enabled, selling an assembled product reduces both parent and component stock.'),
|
||||
subtitle: const Text(
|
||||
'If enabled, selling an assembled product reduces both parent and component stock.',
|
||||
),
|
||||
value: deductParentStock,
|
||||
secondary: const Icon(LucideIcons.gitMerge),
|
||||
onChanged: (val) {
|
||||
if (featureState.value != null) {
|
||||
final newStrategy = val ? 'PARENT_AND_COMPONENTS' : 'COMPONENTS_ONLY';
|
||||
final updated = featureState.value!.copyWith(bomReductionStrategy: newStrategy);
|
||||
ref.read(businessFeatureProvider.notifier).updateFeatures(updated);
|
||||
final newStrategy = val
|
||||
? 'PARENT_AND_COMPONENTS'
|
||||
: 'COMPONENTS_ONLY';
|
||||
final updated = featureState.value!.copyWith(
|
||||
bomReductionStrategy: newStrategy,
|
||||
);
|
||||
ref
|
||||
.read(businessFeatureProvider.notifier)
|
||||
.updateFeatures(updated);
|
||||
}
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Stock Deduction on Invoice'),
|
||||
subtitle: const Text('If enabled, finalizing an invoice automatically deducts product stock. Turn off for a two-step fulfillment process.'),
|
||||
subtitle: const Text(
|
||||
'If enabled, finalizing an invoice automatically deducts product stock. Turn off for a two-step fulfillment process.',
|
||||
),
|
||||
value: stockDeductionOnInvoice,
|
||||
secondary: const Icon(LucideIcons.boxes),
|
||||
onChanged: (val) {
|
||||
if (featureState.value != null) {
|
||||
final updated = featureState.value!.copyWith(stockDeductionOnInvoice: val);
|
||||
ref.read(businessFeatureProvider.notifier).updateFeatures(updated);
|
||||
final updated = featureState.value!.copyWith(
|
||||
stockDeductionOnInvoice: val,
|
||||
);
|
||||
ref
|
||||
.read(businessFeatureProvider.notifier)
|
||||
.updateFeatures(updated);
|
||||
}
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Use EAN/Barcode for Scanner'),
|
||||
subtitle: const Text('If disabled, the barcode scanner will search using the SKU field instead.'),
|
||||
value: (featureState.value?.barcodeSource ?? 'SKU') == 'BARCODE',
|
||||
subtitle: const Text(
|
||||
'If disabled, the barcode scanner will search using the SKU field instead.',
|
||||
),
|
||||
value:
|
||||
(featureState.value?.barcodeSource ?? 'SKU') ==
|
||||
'BARCODE',
|
||||
secondary: const Icon(LucideIcons.scanLine),
|
||||
onChanged: (val) {
|
||||
if (featureState.value != null) {
|
||||
final updated = featureState.value!.copyWith(barcodeSource: val ? 'BARCODE' : 'SKU');
|
||||
ref.read(businessFeatureProvider.notifier).updateFeatures(updated);
|
||||
final updated = featureState.value!.copyWith(
|
||||
barcodeSource: val ? 'BARCODE' : 'SKU',
|
||||
);
|
||||
ref
|
||||
.read(businessFeatureProvider.notifier)
|
||||
.updateFeatures(updated);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const Text('Pricing & Taxation', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
const Text(
|
||||
'Pricing & Taxation',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Selling Price includes Tax'),
|
||||
subtitle: const Text('If enabled, GST is assumed to be inclusive in the entered selling price.'),
|
||||
subtitle: const Text(
|
||||
'If enabled, GST is assumed to be inclusive in the entered selling price.',
|
||||
),
|
||||
value: _taxIncludedInPrice,
|
||||
onChanged: (val) {
|
||||
setState(() => _taxIncludedInPrice = val);
|
||||
@@ -154,7 +210,9 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
void _saveSettings() {
|
||||
final profileState = ref.read(businessProfileProvider).value;
|
||||
if (profileState != null) {
|
||||
final updated = profileState.copyWith(taxIncludedInPrice: _taxIncludedInPrice);
|
||||
final updated = profileState.copyWith(
|
||||
taxIncludedInPrice: _taxIncludedInPrice,
|
||||
);
|
||||
ref.read(businessProfileProvider.notifier).updateProfile(updated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class BusinessProfileNotifier extends AsyncNotifier<BusinessProfile?> {
|
||||
'/business/profile',
|
||||
data: profile.toJson(),
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
state = AsyncValue.data(BusinessProfile.fromJson(response.data));
|
||||
}
|
||||
@@ -35,9 +35,10 @@ class BusinessProfileNotifier extends AsyncNotifier<BusinessProfile?> {
|
||||
}
|
||||
}
|
||||
|
||||
final businessProfileProvider = AsyncNotifierProvider<BusinessProfileNotifier, BusinessProfile?>(() {
|
||||
return BusinessProfileNotifier();
|
||||
});
|
||||
final businessProfileProvider =
|
||||
AsyncNotifierProvider<BusinessProfileNotifier, BusinessProfile?>(() {
|
||||
return BusinessProfileNotifier();
|
||||
});
|
||||
|
||||
class BusinessFeatureNotifier extends AsyncNotifier<BusinessFeature?> {
|
||||
@override
|
||||
@@ -64,7 +65,7 @@ class BusinessFeatureNotifier extends AsyncNotifier<BusinessFeature?> {
|
||||
'/business/features',
|
||||
data: feature.toJson(),
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
state = AsyncValue.data(BusinessFeature.fromJson(response.data));
|
||||
}
|
||||
@@ -74,6 +75,7 @@ class BusinessFeatureNotifier extends AsyncNotifier<BusinessFeature?> {
|
||||
}
|
||||
}
|
||||
|
||||
final businessFeatureProvider = AsyncNotifierProvider<BusinessFeatureNotifier, BusinessFeature?>(() {
|
||||
return BusinessFeatureNotifier();
|
||||
});
|
||||
final businessFeatureProvider =
|
||||
AsyncNotifierProvider<BusinessFeatureNotifier, BusinessFeature?>(() {
|
||||
return BusinessFeatureNotifier();
|
||||
});
|
||||
|
||||
@@ -9,17 +9,27 @@ class IndianStatesNotifier extends AsyncNotifier<List<IndianState>> {
|
||||
}
|
||||
|
||||
Future<List<IndianState>> _fetchStates() async {
|
||||
final response = await DioClient().dio.get('/master/states').timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw Exception('Connection timed out'),
|
||||
);
|
||||
final response = await DioClient().dio
|
||||
.get('/master/states')
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw Exception('Connection timed out'),
|
||||
);
|
||||
if (response.data == null || response.data.toString().isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return (response.data as List).map((e) => IndianState.fromJson(e)).toList();
|
||||
final list = (response.data as List).map((e) => IndianState.fromJson(e)).toList();
|
||||
final Map<String, IndianState> uniqueStates = {};
|
||||
for (var state in list) {
|
||||
uniqueStates[state.gstCode] = state;
|
||||
}
|
||||
final result = uniqueStates.values.toList();
|
||||
result.sort((a, b) => a.name.compareTo(b.name));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
final indianStatesProvider = AsyncNotifierProvider<IndianStatesNotifier, List<IndianState>>(() {
|
||||
return IndianStatesNotifier();
|
||||
});
|
||||
final indianStatesProvider =
|
||||
AsyncNotifierProvider<IndianStatesNotifier, List<IndianState>>(() {
|
||||
return IndianStatesNotifier();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user