Individual and Jwellery Account Setup done

This commit is contained in:
2026-08-25 22:19:10 +05:30
parent 53c1f62373
commit 0d13833679
41 changed files with 1395 additions and 610 deletions

View File

@@ -61,6 +61,7 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
InputDecoration _buildInputDecoration(String label, IconData icon) {
return InputDecoration(
labelText: label,
labelStyle: TextStyle(color: Colors.grey.shade700),
prefixIcon: Icon(icon, color: Colors.grey.shade500),
filled: true,
fillColor: Colors.white,
@@ -107,11 +108,11 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
decoration: const BoxDecoration(
color: Colors.transparent,
shape: BoxShape.circle,
),
child: Center(child: Image.asset('assets/logo.png', width: 50, height: 50, fit: BoxFit.contain)),
child: Center(child: Image.asset('assets/logo.png', width: 80, height: 80, fit: BoxFit.contain)),
),
const SizedBox(height: 32),
Text(
@@ -180,7 +181,7 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
style: const TextStyle(fontWeight: FontWeight.w500),
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black87),
textInputAction: TextInputAction.next,
decoration: _buildInputDecoration(isLogin ? 'Email or Username' : 'Email Address', LucideIcons.mail),
),
@@ -188,7 +189,7 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
TextField(
controller: passwordController,
obscureText: true,
style: const TextStyle(fontWeight: FontWeight.w500),
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black87),
textInputAction: TextInputAction.done,
onSubmitted: (_) => submit(),
decoration: _buildInputDecoration('Password', LucideIcons.lock),

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -10,7 +11,6 @@ import 'auth_screen.dart';
import '../../../core/network/dio_client.dart';
import '../providers/auth_provider.dart';
import '../../transactions/providers/providers.dart';
import '../../../core/theme/theme_provider.dart';
import '../../business/providers/business_mode_provider.dart';
import '../../projects/providers/project_mode_provider.dart';
@@ -23,18 +23,29 @@ class ProfileScreen extends ConsumerStatefulWidget {
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends ConsumerState<ProfileScreen> {
class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTickerProviderStateMixin {
bool _isExporting = false;
String? _profileType;
String _userName = 'Loading...';
String _userEmail = 'Loading...';
late AnimationController _animController;
late Animation<double> _fadeAnim;
@override
void initState() {
super.initState();
_animController = AnimationController(vsync: this, duration: const Duration(milliseconds: 800));
_fadeAnim = CurvedAnimation(parent: _animController, curve: Curves.easeOutCubic);
_animController.forward();
_fetchProfileType();
}
@override
void dispose() {
_animController.dispose();
super.dispose();
}
Future<void> _fetchProfileType() async {
try {
final res = await DioClient().dio.get('/account/setup/status');
@@ -47,12 +58,17 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
});
}
} catch (e) {
// ignore
if (mounted) {
setState(() {
_userName = 'Kifi User';
_userEmail = '';
});
}
}
}
String _getInitials(String name) {
if (name.isEmpty || name == 'Kifi User' || name == 'Loading...') return 'U';
if (name.isEmpty || name == 'Kifi User' || name == 'Loading...') return 'KU';
final parts = name.trim().split(' ');
if (parts.length > 1 && parts[1].isNotEmpty) {
return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
@@ -77,234 +93,314 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
setState(() => _isExporting = true);
try {
final bytes = await ref.read(apiRepositoryProvider).exportTransactions();
final tempDir = await getTemporaryDirectory();
final file = File('${tempDir.path}/transactions_export.csv');
await file.writeAsBytes(bytes);
final xFile = XFile(file.path);
await Share.shareXFiles([xFile], text: 'Here is my Kifi transactions export.');
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Export failed: $e')));
}
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Export failed: $e')));
} finally {
if (mounted) {
setState(() => _isExporting = false);
}
if (mounted) setState(() => _isExporting = false);
}
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final primaryColor = Theme.of(context).primaryColor;
final themeMode = ref.watch(themeProvider);
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
extendBodyBehindAppBar: true,
appBar: AppBar(
title: Text('Profile', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
title: const Text('Profile', style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2)),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 20),
Center(
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(
child: Text(
_getInitials(_userName),
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
),
),
const SizedBox(height: 24),
Text(
_userName,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Theme.of(context).textTheme.bodyLarge?.color),
textAlign: TextAlign.center,
),
if (_userEmail.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
_userEmail,
style: TextStyle(color: isDark ? Colors.grey.shade400 : Colors.grey.shade600, fontSize: 16),
textAlign: TextAlign.center,
),
],
const SizedBox(height: 48),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(isDark ? 0.2 : 0.02), blurRadius: 10, offset: const Offset(0, 4)),
],
border: Border.all(color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2)),
),
child: Column(
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.downloadCloud, color: Colors.blue.shade600, size: 22),
),
title: const Text('Export Data to CSV', style: TextStyle(fontWeight: FontWeight.w600)),
trailing: _isExporting
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(LucideIcons.chevronRight, size: 20),
onTap: _isExporting ? null : _exportData,
),
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.purple.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.moon, color: Colors.purple.shade600, size: 22),
),
title: const Text('Theme', style: TextStyle(fontWeight: FontWeight.w600)),
trailing: SegmentedButton<ThemeMode>(
segments: const [
ButtonSegment(value: ThemeMode.light, icon: Icon(LucideIcons.sun, size: 18)),
ButtonSegment(value: ThemeMode.system, icon: Icon(LucideIcons.monitor, size: 18)),
ButtonSegment(value: ThemeMode.dark, icon: Icon(LucideIcons.moon, size: 18)),
],
selected: {ref.watch(themeProvider)},
onSelectionChanged: (Set<ThemeMode> newSelection) {
ref.read(themeProvider.notifier).setTheme(newSelection.first);
},
showSelectedIcon: false,
style: ButtonStyle(
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
),
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
Consumer(
builder: (context, ref, child) {
final isBusinessMode = ref.watch(businessModeProvider);
return Column(
children: [
if (_profileType == 'BUSINESS') ...[
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
secondary: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.orange.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.briefcase, color: Colors.orange.shade600, size: 22),
),
title: const Text('Business Mode', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text('Inventory and sales', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
value: isBusinessMode,
activeColor: Theme.of(context).primaryColor,
onChanged: (val) {
ref.read(businessModeProvider.notifier).toggleMode();
},
),
if (isBusinessMode) ...[
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.settings, color: Colors.grey.shade700, size: 22),
),
title: const Text('Business Settings', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text('Tax, Modules, POS', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const BusinessSettingsScreen()));
},
),
],
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
Consumer(
builder: (context, ref, child) {
final isProjectMode = ref.watch(projectModeProvider);
return SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
secondary: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.teal.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.trello, color: Colors.teal.shade600, size: 22),
),
title: const Text('Project Management', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text('Task boards and tracking', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
value: isProjectMode,
activeColor: Theme.of(context).primaryColor,
onChanged: (val) {
ref.read(projectModeProvider.notifier).toggleMode();
},
);
},
),
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
],
],
);
}
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.green.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.helpCircle, color: Colors.green.shade600, size: 22),
),
title: const Text('Help & Support', style: TextStyle(fontWeight: FontWeight.w600)),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () {},
),
],
),
),
const SizedBox(height: 48),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _logout(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.withOpacity(0.08),
foregroundColor: Colors.red.shade700,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
icon: const Icon(LucideIcons.logOut, size: 22),
label: const Text('Log Out', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
),
),
const SizedBox(height: 20),
],
),
),
flexibleSpace: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5)),
),
),
),
body: Stack(
children: [
// Background Decorative Elements
Positioned(
top: -50, right: -50,
child: Container(
width: 200, height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: primaryColor.withOpacity(isDark ? 0.2 : 0.1),
),
),
),
Positioned(
bottom: -100, left: -50,
child: Container(
width: 300, height: 300,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.purple.withOpacity(isDark ? 0.15 : 0.08),
),
),
),
// Main Content
SafeArea(
child: FadeTransition(
opacity: _fadeAnim,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 24.0),
children: [
// Avatar Section
Center(
child: Hero(
tag: 'profile_avatar',
child: Container(
width: 120, height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
primaryColor.withOpacity(0.8),
primaryColor.withOpacity(0.5)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(color: primaryColor.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10)),
],
),
child: Center(
child: Text(
_getInitials(_userName),
style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 2),
),
),
),
),
),
const SizedBox(height: 24),
Text(
_userName,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
if (_userEmail.isNotEmpty && _userEmail != 'Loading...') ...[
const SizedBox(height: 8),
Text(
_userEmail,
style: TextStyle(color: isDark ? Colors.grey.shade400 : Colors.grey.shade600, fontSize: 16),
textAlign: TextAlign.center,
),
],
const SizedBox(height: 40),
// Glassmorphism Card for Settings
ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor.withOpacity(isDark ? 0.3 : 0.6),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withOpacity(isDark ? 0.05 : 0.2), width: 1.5),
),
child: Column(
children: [
_buildSettingsTile(
context: context,
icon: LucideIcons.downloadCloud,
iconColor: Colors.blue,
title: 'Export Data to CSV',
trailing: _isExporting
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(LucideIcons.chevronRight, size: 20, color: Colors.grey),
onTap: _isExporting ? null : _exportData,
),
_buildDivider(context, isDark),
_buildSettingsTile(
context: context,
icon: LucideIcons.palette,
iconColor: Colors.purple,
title: 'Appearance',
trailing: SegmentedButton<ThemeMode>(
segments: const [
ButtonSegment(value: ThemeMode.light, icon: Icon(LucideIcons.sun, size: 16)),
ButtonSegment(value: ThemeMode.system, icon: Icon(LucideIcons.monitor, size: 16)),
ButtonSegment(value: ThemeMode.dark, icon: Icon(LucideIcons.moon, size: 16)),
],
selected: {themeMode},
onSelectionChanged: (Set<ThemeMode> newSelection) {
ref.read(themeProvider.notifier).setTheme(newSelection.first);
},
showSelectedIcon: false,
style: ButtonStyle(
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
if (states.contains(WidgetState.selected)) {
return primaryColor.withOpacity(0.2);
}
return null;
}),
),
),
),
_buildDivider(context, isDark),
Consumer(
builder: (context, ref, child) {
final isBusinessMode = ref.watch(businessModeProvider);
return Column(
children: [
if (_profileType == 'BUSINESS') ...[
_buildSwitchTile(
context: context,
icon: LucideIcons.briefcase,
iconColor: Colors.orange,
title: 'Business Mode',
subtitle: 'Inventory and Sales Hub',
value: isBusinessMode,
onChanged: (val) => ref.read(businessModeProvider.notifier).toggleMode(),
),
if (isBusinessMode) ...[
_buildDivider(context, isDark),
_buildSettingsTile(
context: context,
icon: LucideIcons.settings2,
iconColor: Colors.grey.shade600,
title: 'Business Settings',
subtitle: 'Configure Taxes, Barcodes, etc.',
trailing: const Icon(LucideIcons.chevronRight, size: 20, color: Colors.grey),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BusinessSettingsScreen())),
),
],
_buildDivider(context, isDark),
],
],
);
}
),
_buildDivider(context, isDark),
_buildSettingsTile(
context: context,
icon: LucideIcons.helpCircle,
iconColor: Colors.green,
title: 'Help & Support',
trailing: const Icon(LucideIcons.chevronRight, size: 20, color: Colors.grey),
onTap: () {},
),
],
),
),
),
),
const SizedBox(height: 48),
// Logout Button
Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [Colors.red.shade400, Colors.red.shade600],
),
boxShadow: [
BoxShadow(color: Colors.red.withOpacity(0.3), blurRadius: 15, offset: const Offset(0, 5)),
],
),
child: ElevatedButton.icon(
onPressed: () => _logout(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
icon: const Icon(LucideIcons.logOut, size: 22),
label: const Text('Log Out', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 1.0)),
),
),
const SizedBox(height: 40),
],
),
),
),
),
),
],
),
);
}
Widget _buildSettingsTile({
required BuildContext context,
required IconData icon,
required Color iconColor,
required String title,
String? subtitle,
required Widget trailing,
VoidCallback? onTap,
}) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
leading: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: iconColor.withOpacity(isDark ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: iconColor, size: 24),
),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
subtitle: subtitle != null ? Text(subtitle, style: TextStyle(fontSize: 13, color: isDark ? Colors.grey.shade400 : Colors.grey.shade600)) : null,
trailing: trailing,
onTap: onTap,
);
}
Widget _buildSwitchTile({
required BuildContext context,
required IconData icon,
required Color iconColor,
required String title,
required String subtitle,
required bool value,
required ValueChanged<bool> onChanged,
}) {
return _buildSettingsTile(
context: context,
icon: icon,
iconColor: iconColor,
title: title,
subtitle: subtitle,
trailing: Switch(
value: value,
onChanged: onChanged,
activeColor: iconColor,
),
onTap: () => onChanged(!value),
);
}
Widget _buildDivider(BuildContext context, bool isDark) {
return Divider(
height: 1,
indent: 76,
endIndent: 20,
color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05),
);
}
}

View File

@@ -305,11 +305,12 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
const SizedBox(height: 32),
DropdownButtonFormField<String>(
decoration: _buildInputDecoration('Nature of Business *'),
dropdownColor: Colors.white,
value: _natureOfBusiness,
items: const [
DropdownMenuItem(value: 'JEWELLERY', child: Text('Jewellery')),
DropdownMenuItem(value: 'PROJECT_MANAGEMENT', child: Text('Project Management')),
DropdownMenuItem(value: 'INVENTORY_MANAGEMENT', child: Text('Inventory Management')),
DropdownMenuItem(value: 'JEWELLERY', child: Text('Jewellery', style: TextStyle(color: Colors.black87))),
DropdownMenuItem(value: 'PROJECT_MANAGEMENT', child: Text('Project Management', style: TextStyle(color: Colors.black87))),
DropdownMenuItem(value: 'INVENTORY_MANAGEMENT', child: Text('Inventory Management', style: TextStyle(color: Colors.black87))),
],
onChanged: (val) => setState(() => _natureOfBusiness = val),
),

View File

@@ -5,8 +5,10 @@ import '../../../../core/theme/nature_colors.dart';
import '../../../inventory/presentation/product_list_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';
import '../reports/reports_screen.dart';
import '../../providers/business_provider.dart';
import '../widgets/business_profile_form_sheet.dart';
import '../../../inventory/providers/products_provider.dart';
@@ -106,6 +108,14 @@ class BusinessHubScreen extends ConsumerWidget {
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(
@@ -143,6 +153,14 @@ class BusinessHubScreen extends ConsumerWidget {
() => 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) ...[

View File

@@ -0,0 +1,94 @@
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 '../../../inventory/providers/inventory_valuation_provider.dart';
class ReportsScreen extends ConsumerWidget {
const ReportsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final valuationState = ref.watch(inventoryValuationProvider);
final formatCurrency = NumberFormat.currency(symbol: '');
return Scaffold(
appBar: AppBar(
title: const Text('Business Reports'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildReportCard(
context,
title: 'Inventory Valuation Report',
icon: LucideIcons.boxes,
color: Colors.blue,
content: valuationState.when(
data: (val) => Text(
'Total Estimated Value: ${formatCurrency.format(val)}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
loading: () => const CircularProgressIndicator(),
error: (e, s) => Text('Error: $e'),
),
),
const SizedBox(height: 16),
_buildReportCard(
context,
title: 'GST Report (Coming Soon)',
icon: LucideIcons.fileText,
color: Colors.orange,
content: const Text('Export GSTR-1 & GSTR-3B formats based on invoices.'),
),
const SizedBox(height: 16),
_buildReportCard(
context,
title: 'Sales & Revenue (Coming Soon)',
icon: LucideIcons.trendingUp,
color: Colors.green,
content: const Text('Daily and monthly sales analytics.'),
),
],
),
),
);
}
Widget _buildReportCard(BuildContext context, {
required String title,
required IconData icon,
required Color color,
required Widget content,
}) {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color),
),
const SizedBox(width: 12),
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 16),
content,
],
),
),
);
}
}

View File

@@ -22,7 +22,7 @@ import '../../sales/domain/invoice.dart';
import '../../sales/presentation/customers_list_screen.dart';
import 'widgets/swipeable_account_card.dart';
import 'widgets/budget_status_card.dart';
import '../../inventory/presentation/daily_rates_screen.dart';
import 'widgets/upcoming_dues_widget.dart';
import 'widgets/statistics_tab.dart';
import '../../../core/widgets/shimmer_loading.dart';
@@ -185,17 +185,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
backgroundColor: Colors.transparent,
elevation: 0,
actions: [
if (isBusinessMode)
IconButton(
icon: const Icon(LucideIcons.trendingUp),
tooltip: 'Daily Commodity Rates',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DailyRatesScreen()),
);
},
),
Consumer(
builder: (context, ref, child) {
final invitationsState = ref.watch(invitationProvider);
@@ -632,9 +621,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
));
if (isProjectMode) {
screens.add(const ProjectHubScreen());
}
if (isBusinessMode) {
screens.add(const BusinessHubScreen());
@@ -668,11 +654,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
bottomNavigationBar: Builder(
builder: (context) {
final isProjectMode = ref.watch(projectModeProvider);
final isBusinessMode = ref.watch(businessModeProvider);
final List<BottomNavigationBarItem> navItems = [];
navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.home), label: 'Home'));
if (isProjectMode) navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.trello), label: 'Projects'));
if (isBusinessMode) navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.briefcase), label: 'Business'));
navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.pieChart), label: 'Stats'));
navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.plusCircle), label: 'Add'));
@@ -682,7 +666,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
int safeIndex = _currentIndex;
// Adjust _currentIndex for the display of bottom nav bar because of 'Add' button
int addIdx = 1;
if (isProjectMode) addIdx++;
if (isBusinessMode) addIdx++;
addIdx++; // For Stats

View File

@@ -0,0 +1,79 @@
class InventoryItem {
final int? id;
final int? userId;
final int? productId;
final String? tagNumber;
final String? sku;
final String? huid;
final String? purity;
final double? grossWeight;
final double? netWeight;
final double? stoneWeight;
final double? diamondWeight;
final double? fineWeight;
final double? makingCharges;
final String? makingChargeType;
final double? purchaseCost;
final double? metalCost;
final double? stoneCost;
final double? certificationCost;
final double? tax;
final int? vendorId;
final int? branchId;
final String? purchaseRef;
final String? status;
InventoryItem({
this.id,
this.userId,
this.productId,
this.tagNumber,
this.sku,
this.huid,
this.purity,
this.grossWeight,
this.netWeight,
this.stoneWeight,
this.diamondWeight,
this.fineWeight,
this.makingCharges,
this.makingChargeType,
this.purchaseCost,
this.metalCost,
this.stoneCost,
this.certificationCost,
this.tax,
this.vendorId,
this.branchId,
this.purchaseRef,
this.status,
});
factory InventoryItem.fromJson(Map<String, dynamic> json) {
return InventoryItem(
id: json['id'],
userId: json['userId'],
productId: json['productId'],
tagNumber: json['tagNumber'],
sku: json['sku'],
huid: json['huid'],
purity: json['purity'],
grossWeight: json['grossWeight']?.toDouble(),
netWeight: json['netWeight']?.toDouble(),
stoneWeight: json['stoneWeight']?.toDouble(),
diamondWeight: json['diamondWeight']?.toDouble(),
fineWeight: json['fineWeight']?.toDouble(),
makingCharges: json['makingCharges']?.toDouble(),
makingChargeType: json['makingChargeType'],
purchaseCost: json['purchaseCost']?.toDouble(),
metalCost: json['metalCost']?.toDouble(),
stoneCost: json['stoneCost']?.toDouble(),
certificationCost: json['certificationCost']?.toDouble(),
tax: json['tax']?.toDouble(),
vendorId: json['vendorId'],
branchId: json['branchId'],
purchaseRef: json['purchaseRef'],
status: json['status'],
);
}
}

View File

@@ -167,95 +167,6 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
);
}
void _showAddCategoryDialog() {
String newCatName = '';
bool newCatCommodity = false;
double newCatRate = 0;
String newCalcMethod = 'UNIT'; // WEIGHT, UNIT, VOLUME
String newBaseUnit = 'pcs';
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
title: const Text('New Category', style: TextStyle(fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildPremiumTextField(
label: 'Category Name',
onChanged: (val) => newCatName = val,
),
const SizedBox(height: 16),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Is this a Commodity?'),
subtitle: const Text('Enable for daily rate pricing (e.g. Gold)'),
value: newCatCommodity,
onChanged: (val) => setDialogState(() => newCatCommodity = val),
),
if (newCatCommodity) ...[
// Replace DropdownButtonFormField with SmartSearchDropdown
SmartSearchDropdown<String>(
hintText: 'Calculation Method',
value: newCalcMethod,
items: const ['UNIT', 'WEIGHT', 'VOLUME'],
itemAsString: (val) => val,
onChanged: (val) => setDialogState(() {
newCalcMethod = val!;
if (val == 'WEIGHT') newBaseUnit = 'gm';
else if (val == 'VOLUME') newBaseUnit = 'liter';
else newBaseUnit = 'pcs';
})
),
const SizedBox(height: 16),
_buildPremiumTextField(
label: 'Base Unit (e.g. gm, kg, pcs)',
initialValue: newBaseUnit,
onChanged: (val) => newBaseUnit = val,
),
const SizedBox(height: 16),
_buildPremiumTextField(
label: 'Daily Rate',
prefixText: '',
keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: (val) => newCatRate = double.tryParse(val) ?? 0,
),
]
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
onPressed: () async {
if (newCatName.trim().isEmpty) return;
final newCategory = ProductCategory(
name: newCatName.trim(),
isCommodity: newCatCommodity,
calculationMethod: newCalcMethod,
baseUnit: newBaseUnit,
dailyRate: newCatCommodity ? newCatRate : null,
);
await ref.read(productCategoriesProvider.notifier).createCategory(newCategory);
if (context.mounted) Navigator.pop(context);
},
child: const Text('Create'),
),
],
);
},
);
},
);
}
Future<void> _save() async {
if (!_formKey.currentState!.validate()) return;
@@ -305,21 +216,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
}
double _calculateLivePrice() {
if (_selectedCategory == null || !_selectedCategory!.isCommodity) return _sellingPrice;
double rate = _selectedCategory!.dailyRate ?? 0;
double baseVal = (_weight > 0) ? _weight : 1.0;
// Base Material Cost = (Base Quantity + Wastage) * Rate * Purity
double materialWeight = baseVal + (baseVal * (_wastagePercentage / 100));
double materialCost = materialWeight * rate * _purityFactor;
// Making Charges
double making = 0;
if (_makingChargesType == 'FLAT') making = _makingCharges;
else if (_makingChargesType == 'PER_UNIT') making = _makingCharges * baseVal;
else if (_makingChargesType == 'PERCENTAGE') making = materialCost * (_makingCharges / 100);
return materialCost + making;
return _sellingPrice;
}
@override
@@ -475,37 +372,44 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
children: [
Icon(LucideIcons.alertCircle, color: Colors.orange),
SizedBox(width: 12),
Expanded(child: Text('No categories found. Create one to organize your inventory.', style: TextStyle(color: Colors.orange))),
Expanded(child: Text('No categories found. Manage categories in Business Hub.', style: TextStyle(color: Colors.orange))),
],
),
)
else
// Replace DropdownButtonFormField with SmartSearchDropdown
SmartSearchDropdown<ProductCategory>(
hintText: 'Select Category*',
value: _selectedCategory,
items: leafCategories,
itemAsString: (c) {
String displayName = c.name;
if (c.parentCategoryId != null) {
final parent = categories.firstWhere((p) => p.id == c.parentCategoryId, orElse: () => c);
displayName = '${parent.name} > ${c.name}';
itemAsString: (c) => c.name,
itemBuilder: (context, item) {
// Build full path
List<String> path = [];
ProductCategory? current = item;
while (current != null) {
path.insert(0, current.name);
if (current.parentCategoryId != null) {
current = categories.where((p) => p.id == current!.parentCategoryId).firstOrNull;
} else {
current = null;
}
}
return displayName;
final pathString = path.join(' -> ');
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 2),
Text(pathString, style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
],
),
);
},
onChanged: (val) => setState(() => _selectedCategory = val),
),
const SizedBox(height: 12),
GestureDetector(
onTap: _showAddCategoryDialog,
child: Row(
children: [
Icon(LucideIcons.plusCircle, size: 18, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 8),
Text('Create New Category', style: TextStyle(color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w600)),
],
),
),
],
);
},
@@ -552,7 +456,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
const SizedBox(height: 32),
_buildPremiumTextField(
label: _selectedCategory?.isCommodity == true ? 'Volume / Weight' : 'Weight',
label: 'Weight',
initialValue: _weight == 0 ? '' : _weight.toString(),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: (val) => setState(() => _weight = double.tryParse(val) ?? 0),
@@ -583,7 +487,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
Widget _buildPricingStep() {
final taxInclusive = ref.watch(businessProfileProvider).value?.taxIncludedInPrice ?? false;
final isCommodity = _selectedCategory?.isCommodity ?? false;
final isCommodity = false;
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
@@ -619,8 +523,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
),
],
),
const SizedBox(height: 8),
Text('Daily Rate: ₹${_selectedCategory!.dailyRate ?? 0} / ${_selectedCategory!.baseUnit}', style: const TextStyle(color: Colors.white70)),
if (_autoCalculatePrice) ...[
const Divider(color: Colors.white24, height: 32),
_buildPremiumTextField(

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/product_categories_provider.dart';
class CategoryManagementScreen extends ConsumerStatefulWidget {
const CategoryManagementScreen({super.key});
@override
ConsumerState<CategoryManagementScreen> createState() => _CategoryManagementScreenState();
}
class _CategoryManagementScreenState extends ConsumerState<CategoryManagementScreen> {
@override
Widget build(BuildContext context) {
final categoriesAsync = ref.watch(productCategoriesProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Category Management'),
),
body: categoriesAsync.when(
data: (categories) {
// Build category tree
final rootCategories = categories.where((c) => c.parentCategoryId == null).toList();
return ListView.builder(
itemCount: rootCategories.length,
itemBuilder: (context, index) {
return _buildCategoryTile(rootCategories[index], categories, 0);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddCategoryDialog(null),
child: const Icon(Icons.add),
),
);
}
Widget _buildCategoryTile(ProductCategory category, List<ProductCategory> allCategories, int depth) {
final children = allCategories.where((c) => c.parentCategoryId == category.id).toList();
if (children.isEmpty) {
return ListTile(
contentPadding: EdgeInsets.only(left: 16.0 + (depth * 24.0), right: 16.0),
title: Text(category.name),
trailing: IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () => _showAddCategoryDialog(category),
),
);
}
return ExpansionTile(
tilePadding: EdgeInsets.only(left: 16.0 + (depth * 24.0), right: 16.0),
title: Text(category.name),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () => _showAddCategoryDialog(category),
),
const Icon(Icons.expand_more),
],
),
children: children.map((child) => _buildCategoryTile(child, allCategories, depth + 1)).toList(),
);
}
void _showAddCategoryDialog(ProductCategory? parent) {
final nameController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(parent == null ? 'Add Root Category' : 'Add Subcategory to ${parent.name}'),
content: TextField(
controller: nameController,
decoration: const InputDecoration(labelText: 'Category Name'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
if (nameController.text.isNotEmpty) {
final newCategory = ProductCategory(
name: nameController.text,
parentCategoryId: parent?.id,
);
ref.read(productCategoriesProvider.notifier).createCategory(newCategory);
Navigator.pop(context);
}
},
child: const Text('Save'),
),
],
),
);
}
}

View File

@@ -6,7 +6,6 @@ import '../providers/products_provider.dart';
import '../providers/product_categories_provider.dart';
import 'add_product_screen.dart';
import 'product_detail_screen.dart';
import 'daily_rates_screen.dart';
import '../../../core/network/dio_client.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -58,18 +57,6 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
appBar: AppBar(
title: const Text('Products Catalog'),
elevation: 0,
actions: [
IconButton(
icon: const Icon(LucideIcons.trendingUp),
tooltip: 'Daily Commodity Rates',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DailyRatesScreen()),
);
},
),
],
),
body: Column(
children: [

View File

@@ -0,0 +1,16 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/dio_client.dart';
import '../domain/inventory_item.dart';
final inventoryItemsProvider = FutureProvider<List<InventoryItem>>((ref) async {
try {
final response = await DioClient().dio.get('/inventory/items');
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => InventoryItem.fromJson(json)).toList();
}
return [];
} catch (e) {
return [];
}
});

View File

@@ -0,0 +1,17 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/dio_client.dart';
final inventoryValuationProvider = FutureProvider<double>((ref) async {
try {
final response = await DioClient().dio.get('/inventory/items/total-value');
if (response.statusCode == 200) {
if (response.data is num) {
return (response.data as num).toDouble();
}
return double.tryParse(response.data.toString()) ?? 0.0;
}
return 0.0;
} catch (e) {
return 0.0;
}
});

View File

@@ -7,20 +7,32 @@ class ProductCategory {
final int? userId;
final String name;
final int? parentCategoryId;
final bool isCommodity;
final bool hasChild;
final String? defaultHsn;
final double? defaultGst;
final bool huidRequired;
final double? defaultMakingCharge;
final String? makingChargeType;
final String calculationMethod;
final String baseUnit;
final double? dailyRate;
final bool isActive;
final int sortOrder;
ProductCategory({
this.id,
this.userId,
required this.name,
this.parentCategoryId,
this.isCommodity = false,
this.hasChild = false,
this.defaultHsn,
this.defaultGst,
this.huidRequired = false,
this.defaultMakingCharge,
this.makingChargeType,
this.calculationMethod = 'UNIT',
this.baseUnit = 'pcs',
this.dailyRate,
this.isActive = true,
this.sortOrder = 0,
});
factory ProductCategory.fromJson(Map<String, dynamic> json) {
@@ -29,10 +41,16 @@ class ProductCategory {
userId: json['userId'],
name: json['name'],
parentCategoryId: json['parentCategoryId'],
isCommodity: json['isCommodity'] ?? false,
hasChild: json['hasChild'] ?? false,
defaultHsn: json['defaultHsn'],
defaultGst: (json['defaultGst'] as num?)?.toDouble(),
huidRequired: json['huidRequired'] ?? false,
defaultMakingCharge: (json['defaultMakingCharge'] as num?)?.toDouble(),
makingChargeType: json['makingChargeType'],
calculationMethod: json['calculationMethod'] ?? 'UNIT',
baseUnit: json['baseUnit'] ?? 'pcs',
dailyRate: (json['dailyRate'] as num?)?.toDouble(),
isActive: json['isActive'] ?? true,
sortOrder: json['sortOrder'] ?? 0,
);
}
@@ -42,10 +60,16 @@ class ProductCategory {
'userId': userId,
'name': name,
'parentCategoryId': parentCategoryId,
'isCommodity': isCommodity,
'hasChild': hasChild,
'defaultHsn': defaultHsn,
'defaultGst': defaultGst,
'huidRequired': huidRequired,
'defaultMakingCharge': defaultMakingCharge,
'makingChargeType': makingChargeType,
'calculationMethod': calculationMethod,
'baseUnit': baseUnit,
'dailyRate': dailyRate,
'isActive': isActive,
'sortOrder': sortOrder,
};
}
}
@@ -77,55 +101,7 @@ class ProductCategoriesNotifier extends AsyncNotifier<List<ProductCategory>> {
}
}
Future<void> updateCategoryRate(int categoryId, double rate) async {
try {
final category = state.value?.firstWhere((c) => c.id == categoryId);
if (category == null) return;
final updatedCategory = ProductCategory(
id: category.id,
userId: category.userId,
name: category.name,
parentCategoryId: category.parentCategoryId,
isCommodity: category.isCommodity,
calculationMethod: category.calculationMethod,
baseUnit: category.baseUnit,
dailyRate: rate,
);
await DioClient().dio.put(
'/inventory/categories/$categoryId',
data: updatedCategory.toJson(),
);
state = AsyncValue.data(await _fetchCategories());
} catch (e) {
rethrow;
}
}
Future<int> syncRates(int categoryId) async {
try {
final response = await DioClient().dio.post('/inventory/categories/$categoryId/sync-rates');
if (response.statusCode == 200 && response.data != null) {
return response.data['syncedCount'] ?? 0;
}
return 0;
} catch (e) {
rethrow;
}
}
Future<List<dynamic>> fetchRateHistory(int categoryId) async {
try {
final response = await DioClient().dio.get('/inventory/categories/$categoryId/rate-history');
if (response.statusCode == 200) {
return response.data as List<dynamic>;
}
return [];
} catch (e) {
return [];
}
}
}
final productCategoriesProvider = AsyncNotifierProvider<ProductCategoriesNotifier, List<ProductCategory>>(() {

View File

@@ -2,6 +2,7 @@ class InvoiceItem {
final int? id;
final int? invoiceId;
final int? productId;
final int? inventoryItemId;
final String? sku;
final String? hsnCode;
@@ -18,6 +19,7 @@ class InvoiceItem {
this.id,
this.invoiceId,
this.productId,
this.inventoryItemId,
this.hsnCode,
this.sku,
this.description,
@@ -34,6 +36,7 @@ class InvoiceItem {
int? id,
int? invoiceId,
int? productId,
int? inventoryItemId,
String? hsnCode,
String? sku,
String? description,
@@ -49,6 +52,7 @@ class InvoiceItem {
id: id ?? this.id,
invoiceId: invoiceId ?? this.invoiceId,
productId: productId ?? this.productId,
inventoryItemId: inventoryItemId ?? this.inventoryItemId,
hsnCode: hsnCode ?? this.hsnCode,
sku: sku ?? this.sku,
description: description ?? this.description,
@@ -67,6 +71,7 @@ class InvoiceItem {
id: json['id'],
invoiceId: json['invoiceId'],
productId: json['productId'],
inventoryItemId: json['inventoryItemId'],
hsnCode: json['hsnCode'] ?? json['hsn_code'],
sku: json['sku'],
description: json['description'],
@@ -85,6 +90,7 @@ class InvoiceItem {
if (id != null) data['id'] = id;
if (invoiceId != null) data['invoiceId'] = invoiceId;
if (productId != null) data['productId'] = productId;
if (inventoryItemId != null) data['inventoryItemId'] = inventoryItemId;
if (hsnCode != null) data['hsnCode'] = hsnCode;
if (sku != null) data['sku'] = sku;
if (description != null) data['description'] = description;

View File

@@ -11,8 +11,10 @@ import '../domain/invoice.dart';
import '../providers/customers_provider.dart';
import '../domain/customer.dart';
import '../../inventory/providers/products_provider.dart';
import '../../inventory/providers/inventory_items_provider.dart';
import '../../projects/providers/project_mode_provider.dart';
import '../../inventory/domain/product.dart';
import '../../inventory/domain/inventory_item.dart';
import 'add_customer_sheet.dart';
import '../../transactions/providers/providers.dart';
@@ -309,9 +311,49 @@ class _InvoiceBuilderScreenState extends ConsumerState<InvoiceBuilderScreen> {
);
if (barcode != null && barcode.isNotEmpty) {
final products = ref.read(productsProvider).value ?? [];
final inventoryItems = ref.read(inventoryItemsProvider).value ?? [];
final businessFeature = ref.read(businessFeatureProvider).value;
final bool useBarcodeField = businessFeature?.barcodeSource == 'BARCODE';
// 1. Try to match by HUID first
final invItem = inventoryItems.where((i) => i.huid?.toLowerCase() == barcode.toLowerCase()).firstOrNull;
if (invItem != null) {
final p = products.where((p) => p.id == invItem.productId).firstOrNull;
setState(() {
final existingIndex = _items.indexWhere((item) => item.inventoryItemId == invItem.id);
if (existingIndex >= 0) {
// HUIDs are unique, so this shouldn't normally increment qty, but for safety:
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Item with this HUID is already in the invoice')));
}
} else {
final price = invItem.purchaseCost ?? p?.sellingPrice ?? 0; // Ideally use selling price logic, but keeping simple
final tax = invItem.tax ?? p?.gstRate ?? 0;
final making = invItem.makingCharges ?? p?.makingCharges ?? 0;
final total = price + (price * (tax / 100)) + making;
_items.add(InvoiceItem(
productId: p?.id,
inventoryItemId: invItem.id,
sku: invItem.huid ?? invItem.sku ?? p?.sku,
description: p?.name ?? 'Inventory Item',
quantity: 1,
unitPrice: price,
taxRate: tax,
makingCharge: making,
otherCharges: 0,
discount: 0,
total: total,
));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added HUID item to invoice')));
}
}
});
return;
}
// 2. Fallback to matching by Product SKU or Barcode
final p = products.where((p) {
if (useBarcodeField) {
return p.barcode?.toLowerCase() == barcode.toLowerCase();
@@ -322,7 +364,7 @@ class _InvoiceBuilderScreenState extends ConsumerState<InvoiceBuilderScreen> {
if (p != null) {
setState(() {
final existingIndex = _items.indexWhere((item) => item.productId == p.id);
final existingIndex = _items.indexWhere((item) => item.productId == p.id && item.inventoryItemId == null);
if (existingIndex >= 0) {
// Increment qty
final item = _items[existingIndex];
@@ -354,7 +396,7 @@ class _InvoiceBuilderScreenState extends ConsumerState<InvoiceBuilderScreen> {
}
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Product not found for barcode: $barcode')));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Product/Item not found for barcode: $barcode')));
}
}
}