Fixed web loading screen and setup related issues
@@ -1,4 +1,7 @@
|
|||||||
flutter_icons:
|
flutter_icons:
|
||||||
android: true
|
android: true
|
||||||
ios: true
|
ios: true
|
||||||
|
web:
|
||||||
|
generate: true
|
||||||
|
image_path: "assets/icon.png"
|
||||||
image_path: "assets/icon.png"
|
image_path: "assets/icon.png"
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class DesktopSidebar extends ConsumerWidget {
|
|||||||
final isDark = theme.brightness == Brightness.dark;
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
final isBusinessMode = ref.watch(businessModeProvider);
|
final isBusinessMode = ref.watch(businessModeProvider);
|
||||||
final businessProfile = ref.watch(businessProfileProvider).asData?.value;
|
final businessProfile = ref.watch(businessProfileProvider).asData?.value;
|
||||||
|
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
|
final isJewellery = nature == 'JEWELLERY';
|
||||||
final ratesAsync = ref.watch(commodityRatesProvider);
|
final ratesAsync = ref.watch(commodityRatesProvider);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@@ -51,22 +53,23 @@ class DesktopSidebar extends ConsumerWidget {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [Color(0xFFD4AF37), Color(0xFFAA771C)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: const Color(0xFFD4AF37).withValues(alpha: 0.3),
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
blurRadius: 10,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: ClipRRect(
|
||||||
child: Icon(LucideIcons.gem, color: Colors.white, size: 22),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/icon.png',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -276,8 +279,9 @@ class DesktopSidebar extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 4. Live Metal Rate Widget in Sidebar
|
// 4. Live Metal Rate Widget in Sidebar (Jewellery only)
|
||||||
ratesAsync.when(
|
if (isJewellery)
|
||||||
|
ratesAsync.when(
|
||||||
data: (rates) {
|
data: (rates) {
|
||||||
if (rates.isEmpty) return const SizedBox.shrink();
|
if (rates.isEmpty) return const SizedBox.shrink();
|
||||||
final goldRate = rates.firstWhere(
|
final goldRate = rates.firstWhere(
|
||||||
|
|||||||
@@ -131,19 +131,28 @@ class _AuthScreenState extends ConsumerState<AuthScreen>
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Center(
|
||||||
width: 80,
|
child: Container(
|
||||||
height: 80,
|
width: 80,
|
||||||
decoration: const BoxDecoration(
|
height: 80,
|
||||||
color: Colors.transparent,
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
boxShadow: [
|
||||||
child: Center(
|
BoxShadow(
|
||||||
child: Image.asset(
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
'assets/logo.png',
|
blurRadius: 16,
|
||||||
width: 80,
|
offset: const Offset(0, 4),
|
||||||
height: 80,
|
),
|
||||||
fit: BoxFit.contain,
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/icon.png',
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ class SetupWizardScreen extends StatefulWidget {
|
|||||||
State<SetupWizardScreen> createState() => _SetupWizardScreenState();
|
State<SetupWizardScreen> createState() => _SetupWizardScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SetupWizardScreenState extends State<SetupWizardScreen>
|
class _SetupWizardScreenState extends State<SetupWizardScreen> {
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
int _currentStep = 0;
|
int _currentStep = 0;
|
||||||
String _accountType = 'INDIVIDUAL';
|
String _accountType = 'INDIVIDUAL';
|
||||||
@@ -40,6 +39,19 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
|
|
||||||
int get _totalSteps => _accountType == 'BUSINESS' ? 4 : 3;
|
int get _totalSteps => _accountType == 'BUSINESS' ? 4 : 3;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_usernameController.dispose();
|
||||||
|
_businessNameController.dispose();
|
||||||
|
_addressController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
_gstController.dispose();
|
||||||
|
_panController.dispose();
|
||||||
|
_msmeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _checkUsername(String username) async {
|
Future<void> _checkUsername(String username) async {
|
||||||
if (username.length < 3) return;
|
if (username.length < 3) return;
|
||||||
setState(() => _checkingUsername = true);
|
setState(() => _checkingUsername = true);
|
||||||
@@ -111,6 +123,7 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _nextStep() {
|
void _nextStep() {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
if (_currentStep == 1) {
|
if (_currentStep == 1) {
|
||||||
if (_nameController.text.trim().isEmpty) {
|
if (_nameController.text.trim().isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -150,6 +163,7 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _prevStep() {
|
void _prevStep() {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
if (_currentStep > 0) {
|
if (_currentStep > 0) {
|
||||||
setState(() => _currentStep--);
|
setState(() => _currentStep--);
|
||||||
}
|
}
|
||||||
@@ -209,7 +223,6 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
|
|
||||||
Widget _buildStep0AccountType() {
|
Widget _buildStep0AccountType() {
|
||||||
return Column(
|
return Column(
|
||||||
key: const ValueKey(0),
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
@@ -324,7 +337,6 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
|
|
||||||
Widget _buildStep1Personal() {
|
Widget _buildStep1Personal() {
|
||||||
return Column(
|
return Column(
|
||||||
key: const ValueKey(1),
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
@@ -354,6 +366,7 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
child: _checkingUsername
|
child: _checkingUsername
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
|
key: ValueKey('user_check_spinner'),
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
@@ -362,7 +375,7 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
_isUsernameAvailable
|
_isUsernameAvailable
|
||||||
? LucideIcons.checkCircle2
|
? LucideIcons.checkCircle2
|
||||||
: LucideIcons.xCircle,
|
: LucideIcons.xCircle,
|
||||||
key: ValueKey(_isUsernameAvailable),
|
key: ValueKey('user_avail_${_isUsernameAvailable}'),
|
||||||
color: _usernameController.text.isEmpty
|
color: _usernameController.text.isEmpty
|
||||||
? Colors.transparent
|
? Colors.transparent
|
||||||
: (_isUsernameAvailable
|
: (_isUsernameAvailable
|
||||||
@@ -388,7 +401,6 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
|
|
||||||
Widget _buildStep2Business() {
|
Widget _buildStep2Business() {
|
||||||
return Column(
|
return Column(
|
||||||
key: const ValueKey(2),
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
@@ -410,6 +422,10 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
value: 'JEWELLERY',
|
value: 'JEWELLERY',
|
||||||
child: Text('Jewellery', style: TextStyle(color: Colors.black87)),
|
child: Text('Jewellery', style: TextStyle(color: Colors.black87)),
|
||||||
),
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'ECOMMERCE',
|
||||||
|
child: Text('E-Commerce Seller', style: TextStyle(color: Colors.black87)),
|
||||||
|
),
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: 'PROJECT_MANAGEMENT',
|
value: 'PROJECT_MANAGEMENT',
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -474,7 +490,6 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
|
|
||||||
Widget _buildStep3Consent() {
|
Widget _buildStep3Consent() {
|
||||||
return Column(
|
return Column(
|
||||||
key: const ValueKey(3),
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
@@ -556,12 +571,11 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCurrentStep() {
|
int get _stackIndex {
|
||||||
if (_currentStep == 0) return _buildStep0AccountType();
|
if (_accountType == 'INDIVIDUAL' && _currentStep == 2) {
|
||||||
if (_currentStep == 1) return _buildStep1Personal();
|
return 3;
|
||||||
if (_accountType == 'BUSINESS' && _currentStep == 2)
|
}
|
||||||
return _buildStep2Business();
|
return _currentStep;
|
||||||
return _buildStep3Consent(); // Step 2 (Individual) or Step 3 (Business)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -587,28 +601,38 @@ class _SetupWizardScreenState extends State<SetupWizardScreen>
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 600),
|
constraints: const BoxConstraints(maxWidth: 600),
|
||||||
child: AnimatedSwitcher(
|
child: IndexedStack(
|
||||||
duration: const Duration(milliseconds: 300),
|
index: _stackIndex,
|
||||||
switchInCurve: Curves.easeOutCubic,
|
children: [
|
||||||
switchOutCurve: Curves.easeInCubic,
|
SingleChildScrollView(
|
||||||
transitionBuilder: (child, animation) {
|
padding: const EdgeInsets.symmetric(
|
||||||
return SlideTransition(
|
horizontal: 24.0,
|
||||||
position: Tween<Offset>(
|
vertical: 16.0,
|
||||||
begin: const Offset(0.05, 0),
|
),
|
||||||
end: Offset.zero,
|
child: _buildStep0AccountType(),
|
||||||
).animate(animation),
|
|
||||||
child: FadeTransition(opacity: animation, child: child),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
key: ValueKey(_currentStep),
|
|
||||||
physics: const BouncingScrollPhysics(),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24.0,
|
|
||||||
vertical: 16.0,
|
|
||||||
),
|
),
|
||||||
child: _buildCurrentStep(),
|
SingleChildScrollView(
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24.0,
|
||||||
|
vertical: 16.0,
|
||||||
|
),
|
||||||
|
child: _buildStep1Personal(),
|
||||||
|
),
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24.0,
|
||||||
|
vertical: 16.0,
|
||||||
|
),
|
||||||
|
child: _buildStep2Business(),
|
||||||
|
),
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24.0,
|
||||||
|
vertical: 16.0,
|
||||||
|
),
|
||||||
|
child: _buildStep3Consent(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class BusinessProfile {
|
|||||||
final String? emailId;
|
final String? emailId;
|
||||||
final String? panNumber;
|
final String? panNumber;
|
||||||
final String? gstin;
|
final String? gstin;
|
||||||
|
final String? natureOfBusiness;
|
||||||
|
final String? msmeNumber;
|
||||||
|
|
||||||
BusinessProfile({
|
BusinessProfile({
|
||||||
this.id,
|
this.id,
|
||||||
@@ -29,13 +31,15 @@ class BusinessProfile {
|
|||||||
this.emailId,
|
this.emailId,
|
||||||
this.panNumber,
|
this.panNumber,
|
||||||
this.gstin,
|
this.gstin,
|
||||||
|
this.natureOfBusiness,
|
||||||
|
this.msmeNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory BusinessProfile.fromJson(Map<String, dynamic> json) {
|
factory BusinessProfile.fromJson(Map<String, dynamic> json) {
|
||||||
return BusinessProfile(
|
return BusinessProfile(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
userId: json['userId'],
|
userId: json['userId'],
|
||||||
businessName: json['businessName'],
|
businessName: json['businessName'] ?? '',
|
||||||
industry: json['industry'],
|
industry: json['industry'],
|
||||||
taxNumber: json['taxNumber'],
|
taxNumber: json['taxNumber'],
|
||||||
currency: json['currency'],
|
currency: json['currency'],
|
||||||
@@ -47,6 +51,8 @@ class BusinessProfile {
|
|||||||
emailId: json['emailId'],
|
emailId: json['emailId'],
|
||||||
panNumber: json['panNumber'],
|
panNumber: json['panNumber'],
|
||||||
gstin: json['gstin'],
|
gstin: json['gstin'],
|
||||||
|
natureOfBusiness: json['natureOfBusiness'] ?? json['industry'],
|
||||||
|
msmeNumber: json['msmeNumber'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +72,8 @@ class BusinessProfile {
|
|||||||
'emailId': emailId,
|
'emailId': emailId,
|
||||||
'panNumber': panNumber,
|
'panNumber': panNumber,
|
||||||
'gstin': gstin,
|
'gstin': gstin,
|
||||||
|
'natureOfBusiness': natureOfBusiness,
|
||||||
|
'msmeNumber': msmeNumber,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +90,8 @@ class BusinessProfile {
|
|||||||
String? emailId,
|
String? emailId,
|
||||||
String? panNumber,
|
String? panNumber,
|
||||||
String? gstin,
|
String? gstin,
|
||||||
|
String? natureOfBusiness,
|
||||||
|
String? msmeNumber,
|
||||||
}) {
|
}) {
|
||||||
return BusinessProfile(
|
return BusinessProfile(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -98,6 +108,8 @@ class BusinessProfile {
|
|||||||
emailId: emailId ?? this.emailId,
|
emailId: emailId ?? this.emailId,
|
||||||
panNumber: panNumber ?? this.panNumber,
|
panNumber: panNumber ?? this.panNumber,
|
||||||
gstin: gstin ?? this.gstin,
|
gstin: gstin ?? this.gstin,
|
||||||
|
natureOfBusiness: natureOfBusiness ?? this.natureOfBusiness,
|
||||||
|
msmeNumber: msmeNumber ?? this.msmeNumber,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class BusinessHubScreen extends ConsumerWidget {
|
|||||||
final bool showInventory = featureState?.inventoryManagement ?? false;
|
final bool showInventory = featureState?.inventoryManagement ?? false;
|
||||||
final bool showSales = featureState?.salesManagement ?? false;
|
final bool showSales = featureState?.salesManagement ?? false;
|
||||||
final bool showPurchase = featureState?.purchaseManagement ?? false;
|
final bool showPurchase = featureState?.purchaseManagement ?? false;
|
||||||
|
final nature = (profile?.natureOfBusiness ?? profile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
|
final bool isJewellery = nature == 'JEWELLERY';
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
@@ -200,7 +202,7 @@ class BusinessHubScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
),
|
),
|
||||||
if (showInventory)
|
if (showInventory && isJewellery)
|
||||||
_buildActionCard(
|
_buildActionCard(
|
||||||
context,
|
context,
|
||||||
'Daily Rates',
|
'Daily Rates',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../providers/product_categories_provider.dart';
|
import '../providers/product_categories_provider.dart';
|
||||||
import '../../../core/theme/nature_colors.dart';
|
import '../../../core/theme/nature_colors.dart';
|
||||||
import '../../business/providers/business_mode_provider.dart';
|
import '../../business/providers/business_mode_provider.dart';
|
||||||
|
import '../../business/providers/business_provider.dart';
|
||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
@@ -99,6 +100,18 @@ class _CategoryManagementScreenState extends ConsumerState<CategoryManagementScr
|
|||||||
final children = allCategories.where((c) => c.parentCategoryId == category.id).toList();
|
final children = allCategories.where((c) => c.parentCategoryId == category.id).toList();
|
||||||
final isLeaf = children.isEmpty && !category.hasChild;
|
final isLeaf = children.isEmpty && !category.hasChild;
|
||||||
|
|
||||||
|
final hasCommodity = category.commodityCode != null && category.commodityCode != 'NONE' && category.commodityCode!.isNotEmpty;
|
||||||
|
final hasHsn = category.defaultHsn != null && category.defaultHsn!.isNotEmpty;
|
||||||
|
|
||||||
|
String? subtitleText;
|
||||||
|
if (hasCommodity && hasHsn) {
|
||||||
|
subtitleText = '${category.commodityCode} • HSN: ${category.defaultHsn}';
|
||||||
|
} else if (hasCommodity) {
|
||||||
|
subtitleText = category.commodityCode;
|
||||||
|
} else if (hasHsn) {
|
||||||
|
subtitleText = 'HSN: ${category.defaultHsn}';
|
||||||
|
}
|
||||||
|
|
||||||
// A nice frosted glass card
|
// A nice frosted glass card
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(left: depth * 16.0, bottom: 8),
|
margin: EdgeInsets.only(left: depth * 16.0, bottom: 8),
|
||||||
@@ -136,8 +149,8 @@ class _CategoryManagementScreenState extends ConsumerState<CategoryManagementScr
|
|||||||
child: const Icon(LucideIcons.tag, size: 20, color: Colors.blue),
|
child: const Icon(LucideIcons.tag, size: 20, color: Colors.blue),
|
||||||
),
|
),
|
||||||
title: Text(category.name, style: const TextStyle(fontWeight: FontWeight.w600)),
|
title: Text(category.name, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
subtitle: category.commodityCode != null
|
subtitle: subtitleText != null
|
||||||
? Text('${category.commodityCode} • HSN: ${category.defaultHsn ?? "N/A"}',
|
? Text(subtitleText,
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600))
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600))
|
||||||
: null,
|
: null,
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
@@ -186,35 +199,77 @@ class _CategoryManagementScreenState extends ConsumerState<CategoryManagementScr
|
|||||||
|
|
||||||
Widget _buildCategoryPopupMenu(ProductCategory category, bool isLeaf) {
|
Widget _buildCategoryPopupMenu(ProductCategory category, bool isLeaf) {
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
icon: const Icon(LucideIcons.moreVertical, color: Colors.grey),
|
icon: const Icon(LucideIcons.ellipsisVertical, size: 18, color: Colors.grey),
|
||||||
onSelected: (value) {
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
if (value == 'edit') {
|
onSelected: (val) {
|
||||||
|
if (val == 'edit') {
|
||||||
_showAddCategorySheet(context, category, ref, isEdit: true);
|
_showAddCategorySheet(context, category, ref, isEdit: true);
|
||||||
} else if (value == 'add_sub') {
|
} else if (val == 'addSub') {
|
||||||
_showAddCategorySheet(context, category, ref, isEdit: false);
|
_showAddCategorySheet(context, category, ref, isEdit: false);
|
||||||
} else if (value == 'delete') {
|
} else if (val == 'delete') {
|
||||||
if (category.id != null) {
|
_confirmDelete(category);
|
||||||
ref.read(productCategoriesProvider.notifier).deleteCategory(category.id!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
const PopupMenuItem(
|
|
||||||
value: 'edit',
|
|
||||||
child: Row(children: [Icon(LucideIcons.edit, size: 18), SizedBox(width: 8), Text('Edit')]),
|
|
||||||
),
|
|
||||||
if (!isLeaf)
|
if (!isLeaf)
|
||||||
const PopupMenuItem(
|
const PopupMenuItem(
|
||||||
value: 'add_sub',
|
value: 'addSub',
|
||||||
child: Row(children: [Icon(LucideIcons.plus, size: 18), SizedBox(width: 8), Text('Add Subcategory')]),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.folderPlus, size: 18, color: Colors.blue),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text('Add Subcategory'),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'edit',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.pencil, size: 18, color: Colors.orange),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text('Edit Details'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const PopupMenuItem(
|
const PopupMenuItem(
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
child: Row(children: [Icon(LucideIcons.trash2, size: 18, color: Colors.red), SizedBox(width: 8), Text('Delete', style: TextStyle(color: Colors.red))]),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.trash2, size: 18, color: Colors.red),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text('Delete Category', style: TextStyle(color: Colors.red)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _confirmDelete(ProductCategory category) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
title: const Text('Delete Category?'),
|
||||||
|
content: Text('Are you sure you want to delete "${category.name}"? If it has subcategories or products, they will also be affected.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(productCategoriesProvider.notifier).deleteCategory(category.id!);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showAddCategorySheet(BuildContext context, ProductCategory? parentOrCategory, WidgetRef ref, {bool isEdit = false}) {
|
void _showAddCategorySheet(BuildContext context, ProductCategory? parentOrCategory, WidgetRef ref, {bool isEdit = false}) {
|
||||||
@@ -251,7 +306,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
bool _huidRequired = false;
|
bool _huidRequired = false;
|
||||||
String _commodityCode = 'XAU';
|
String _commodityCode = 'XAU';
|
||||||
String _makingChargeType = 'PER_GRAM';
|
String _makingChargeType = 'PER_GRAM';
|
||||||
String _baseUnit = 'g';
|
String _baseUnit = 'pcs';
|
||||||
|
|
||||||
final List<Map<String, String>> _commodities = [
|
final List<Map<String, String>> _commodities = [
|
||||||
{'code': 'XAU', 'label': 'XAU - Gold'},
|
{'code': 'XAU', 'label': 'XAU - Gold'},
|
||||||
@@ -264,6 +319,13 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
final profile = ref.read(businessProfileProvider).value;
|
||||||
|
final nature = (profile?.natureOfBusiness ?? profile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
|
final isJewellery = nature == 'JEWELLERY';
|
||||||
|
|
||||||
|
_baseUnit = isJewellery ? 'g' : 'pcs';
|
||||||
|
_commodityCode = isJewellery ? 'XAU' : 'NONE';
|
||||||
|
|
||||||
if (widget.categoryToEdit != null) {
|
if (widget.categoryToEdit != null) {
|
||||||
final c = widget.categoryToEdit!;
|
final c = widget.categoryToEdit!;
|
||||||
_selectedParentId = c.parentCategoryId;
|
_selectedParentId = c.parentCategoryId;
|
||||||
@@ -275,14 +337,13 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
||||||
_purityFactorController.text = formatPurity(c.purityFactor);
|
_purityFactorController.text = formatPurity(c.purityFactor);
|
||||||
_huidRequired = c.huidRequired;
|
_huidRequired = c.huidRequired;
|
||||||
_commodityCode = c.commodityCode ?? 'XAU';
|
_commodityCode = c.commodityCode ?? (isJewellery ? 'XAU' : 'NONE');
|
||||||
|
|
||||||
// Handle legacy or varying enum string values
|
// Handle legacy or varying enum string values
|
||||||
_makingChargeType = c.makingChargeType ?? 'PER_GRAM';
|
_makingChargeType = c.makingChargeType ?? 'PER_GRAM';
|
||||||
if (_makingChargeType == 'PER_GM') _makingChargeType = 'PER_GRAM';
|
if (_makingChargeType == 'PER_GM') _makingChargeType = 'PER_GRAM';
|
||||||
|
|
||||||
|
_baseUnit = c.baseUnit ?? (isJewellery ? 'g' : 'pcs');
|
||||||
_baseUnit = c.baseUnit ?? 'g';
|
|
||||||
if (_baseUnit == 'gm') _baseUnit = 'g';
|
if (_baseUnit == 'gm') _baseUnit = 'g';
|
||||||
}
|
}
|
||||||
} else if (widget.parent != null) {
|
} else if (widget.parent != null) {
|
||||||
@@ -302,18 +363,22 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
|
|
||||||
void _submit() {
|
void _submit() {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
|
final profile = ref.read(businessProfileProvider).value;
|
||||||
|
final nature = (profile?.natureOfBusiness ?? profile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
|
final isJewellery = nature == 'JEWELLERY';
|
||||||
|
|
||||||
final newCategory = ProductCategory(
|
final newCategory = ProductCategory(
|
||||||
id: widget.categoryToEdit?.id,
|
id: widget.categoryToEdit?.id,
|
||||||
name: _nameController.text.trim(),
|
name: _nameController.text.trim(),
|
||||||
parentCategoryId: _selectedParentId,
|
parentCategoryId: _selectedParentId,
|
||||||
hasChild: !_isLeaf,
|
hasChild: !_isLeaf,
|
||||||
commodityCode: _isLeaf ? _commodityCode : null,
|
commodityCode: _isLeaf ? (isJewellery ? _commodityCode : 'NONE') : null,
|
||||||
purityFactor: _isLeaf ? normalizePurity(double.tryParse(_purityFactorController.text)) : 1.0,
|
purityFactor: _isLeaf ? (isJewellery ? normalizePurity(double.tryParse(_purityFactorController.text)) : 1.0) : 1.0,
|
||||||
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
||||||
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
||||||
huidRequired: _isLeaf ? _huidRequired : false,
|
huidRequired: _isLeaf ? (isJewellery ? _huidRequired : false) : false,
|
||||||
defaultMakingCharge: _isLeaf ? double.tryParse(_makingChargeController.text) : null,
|
defaultMakingCharge: _isLeaf && isJewellery ? double.tryParse(_makingChargeController.text) : null,
|
||||||
makingChargeType: _isLeaf ? _makingChargeType : null,
|
makingChargeType: _isLeaf && isJewellery ? _makingChargeType : null,
|
||||||
baseUnit: _isLeaf ? _baseUnit : 'pcs',
|
baseUnit: _isLeaf ? _baseUnit : 'pcs',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -330,6 +395,9 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final isBusinessMode = ref.watch(businessModeProvider);
|
final isBusinessMode = ref.watch(businessModeProvider);
|
||||||
|
final businessProfile = ref.watch(businessProfileProvider).value;
|
||||||
|
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
|
final isJewellery = nature == 'JEWELLERY';
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: MediaQuery.of(context).size.height * 0.85,
|
height: MediaQuery.of(context).size.height * 0.85,
|
||||||
@@ -461,7 +529,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
const Text('Product Defaults', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
const Text('Product Defaults', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
if (isBusinessMode) ...[
|
if (isJewellery && isBusinessMode) ...[
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -516,51 +584,66 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
SwitchListTile(
|
if (isJewellery) ...[
|
||||||
title: const Text('HUID Required'),
|
SwitchListTile(
|
||||||
subtitle: const Text('Is Hallmark Unique Identification mandatory?'),
|
title: const Text('HUID Required'),
|
||||||
value: _huidRequired,
|
subtitle: const Text('Is Hallmark Unique Identification mandatory?'),
|
||||||
onChanged: (val) => setState(() => _huidRequired = val),
|
value: _huidRequired,
|
||||||
contentPadding: EdgeInsets.zero,
|
onChanged: (val) => setState(() => _huidRequired = val),
|
||||||
),
|
contentPadding: EdgeInsets.zero,
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: _makingChargeType,
|
value: _makingChargeType,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Making Charge Type',
|
labelText: 'Making Charge Type',
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')),
|
||||||
|
DropdownMenuItem(value: 'PER_PIECE', child: Text('Per Piece')),
|
||||||
|
DropdownMenuItem(value: 'PERCENTAGE', child: Text('Percentage')),
|
||||||
|
],
|
||||||
|
onChanged: (val) => setState(() => _makingChargeType = val!),
|
||||||
),
|
),
|
||||||
items: const [
|
|
||||||
DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')),
|
|
||||||
DropdownMenuItem(value: 'PER_PIECE', child: Text('Per Piece')),
|
|
||||||
DropdownMenuItem(value: 'PERCENTAGE', child: Text('Percentage')),
|
|
||||||
],
|
|
||||||
onChanged: (val) => setState(() => _makingChargeType = val!),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 16),
|
||||||
const SizedBox(width: 16),
|
Expanded(
|
||||||
Expanded(
|
child: _buildTextField(
|
||||||
child: _buildTextField(
|
controller: _makingChargeController,
|
||||||
controller: _makingChargeController,
|
label: 'Default Charge',
|
||||||
label: 'Default Charge',
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
|
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _baseUnit,
|
value: _baseUnit,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Base Unit',
|
labelText: 'Base Unit',
|
||||||
),
|
),
|
||||||
items: const [
|
items: [
|
||||||
DropdownMenuItem(value: 'g', child: Text('Grams (g)')),
|
if (isJewellery) ...const [
|
||||||
DropdownMenuItem(value: 'kg', child: Text('Kilograms (kg)')),
|
DropdownMenuItem(value: 'g', child: Text('Grams (g)')),
|
||||||
DropdownMenuItem(value: 'pcs', child: Text('Pieces (pcs)')),
|
DropdownMenuItem(value: 'kg', child: Text('Kilograms (kg)')),
|
||||||
|
DropdownMenuItem(value: 'pcs', child: Text('Pieces (pcs)')),
|
||||||
|
] else ...const [
|
||||||
|
DropdownMenuItem(value: 'pcs', child: Text('Pieces (pcs)')),
|
||||||
|
DropdownMenuItem(value: 'unit', child: Text('Units (unit)')),
|
||||||
|
DropdownMenuItem(value: 'box', child: Text('Boxes (box)')),
|
||||||
|
DropdownMenuItem(value: 'set', child: Text('Sets (set)')),
|
||||||
|
DropdownMenuItem(value: 'pair', child: Text('Pairs (pair)')),
|
||||||
|
DropdownMenuItem(value: 'g', child: Text('Grams (g)')),
|
||||||
|
DropdownMenuItem(value: 'kg', child: Text('Kilograms (kg)')),
|
||||||
|
DropdownMenuItem(value: 'm', child: Text('Meters (m)')),
|
||||||
|
DropdownMenuItem(value: 'l', child: Text('Liters (l)')),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
onChanged: (val) => setState(() => _baseUnit = val!),
|
onChanged: (val) => setState(() => _baseUnit = val!),
|
||||||
),
|
),
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 795 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 161 KiB |
@@ -5,7 +5,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta name="description" content="KIFI - Modern Financial Ledger & Jewellery Business Suite">
|
<meta name="description" content="KIFI - Modern Financial Ledger & Business ERP">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
<title>KIFI - Financial Ledger & Jewellery ERP</title>
|
<title>KIFI - Financial Ledger & Business ERP</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
@@ -85,9 +85,9 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="splash-container">
|
<div class="splash-container">
|
||||||
<div class="splash-logo">💎</div>
|
<img src="icons/Icon-192.png" alt="KIFI" style="width: 64px; height: 64px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); animation: pulse 2s infinite ease-in-out;">
|
||||||
<div class="splash-title">KIFI</div>
|
<div class="splash-title">KIFI</div>
|
||||||
<div class="splash-sub">Financial Ledger & Jewellery ERP</div>
|
<div class="splash-sub">Financial Ledger & Business ERP</div>
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
|||||||