Individual and Jwellery Account Setup done
This commit is contained in:
@@ -15,8 +15,8 @@ class DioClient {
|
||||
|
||||
DioClient._internal()
|
||||
: dio = Dio(BaseOptions(
|
||||
//baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
|
||||
baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing
|
||||
baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
|
||||
//baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
)),
|
||||
|
||||
@@ -7,32 +7,12 @@ class ThemeNotifier extends Notifier<ThemeMode> {
|
||||
|
||||
@override
|
||||
ThemeMode build() {
|
||||
_loadTheme();
|
||||
return ThemeMode.system;
|
||||
}
|
||||
|
||||
Future<void> _loadTheme() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final themeString = prefs.getString(_themePrefKey);
|
||||
if (themeString != null) {
|
||||
if (themeString == 'light') {
|
||||
state = ThemeMode.light;
|
||||
} else if (themeString == 'dark') {
|
||||
state = ThemeMode.dark;
|
||||
}
|
||||
}
|
||||
return ThemeMode.light;
|
||||
}
|
||||
|
||||
Future<void> setTheme(ThemeMode mode) async {
|
||||
state = mode;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mode == ThemeMode.light) {
|
||||
await prefs.setString(_themePrefKey, 'light');
|
||||
} else if (mode == ThemeMode.dark) {
|
||||
await prefs.setString(_themePrefKey, 'dark');
|
||||
} else {
|
||||
await prefs.setString(_themePrefKey, 'system');
|
||||
}
|
||||
// Always force light mode since the app UI is not optimized for dark mode yet
|
||||
state = ThemeMode.light;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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) ...[
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
79
kifi-app/lib/features/inventory/domain/inventory_item.dart
Normal file
79
kifi-app/lib/features/inventory/domain/inventory_item.dart
Normal 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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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: [
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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>>(() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user