Done Category, Product Catalogue, Customer, Vendor, Purchase Invoice Module
This commit is contained in:
579
analyze_output.txt
Normal file
579
analyze_output.txt
Normal file
@@ -0,0 +1,579 @@
|
||||
Analyzing Kifi...
|
||||
|
||||
warning • Unused import: 'dart:convert' • kifi-api/scratch.dart:1:8 • unused_import
|
||||
error • Target of URI doesn't exist: 'package:http/http.dart' • kifi-api/scratch.dart:2:8 • uri_does_not_exist
|
||||
warning • The value of the local variable 'token' isn't used • kifi-api/scratch.dart:5:9 • unused_local_variable
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/core/theme/app_theme.dart:42:53 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/core/theme/app_theme.dart:46:53 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/core/theme/app_theme.dart:98:53 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/core/theme/app_theme.dart:102:53 • deprecated_member_use
|
||||
warning • Unused import: 'package:shared_preferences/shared_preferences.dart' • kifi-app/lib/core/theme/theme_provider.dart:3:8 • unused_import
|
||||
warning • The value of the field '_themePrefKey' isn't used • kifi-app/lib/core/theme/theme_provider.dart:6:16 • unused_field
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/core/widgets/smart_search_dropdown.dart:196:81 • deprecated_member_use
|
||||
warning • Dead code • kifi-app/lib/core/widgets/smart_search_dropdown.dart:228:65 • dead_code
|
||||
warning • The left operand can't be null, so the right operand is never executed • kifi-app/lib/core/widgets/smart_search_dropdown.dart:228:68 • dead_null_aware_expression
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/auth/presentation/auth_screen.dart:46:11 • avoid_print
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/auth/presentation/auth_screen.dart:48:11 • avoid_print
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/auth/presentation/auth_screen.dart:61:11 • avoid_print
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/auth/presentation/auth_screen.dart:63:13 • curly_braces_in_flow_control_structures
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/auth_screen.dart:187:63 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/auth_screen.dart:223:63 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/forgot_password_screen.dart:111:63 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/otp_screen.dart:76:63 • deprecated_member_use
|
||||
info • The import of 'package:cross_file/cross_file.dart' is unnecessary because all of the used elements are also provided by the import of 'package:share_plus/share_plus.dart' • kifi-app/lib/features/auth/presentation/profile_screen.dart:9:8 • unnecessary_import
|
||||
info • The imported package 'cross_file' isn't a dependency of the importing package • kifi-app/lib/features/auth/presentation/profile_screen.dart:9:8 • depend_on_referenced_packages
|
||||
warning • Unused import: '../providers/auth_provider.dart' • kifi-app/lib/features/auth/presentation/profile_screen.dart:12:8 • unused_import
|
||||
warning • Unused import: '../../projects/providers/project_mode_provider.dart' • kifi-app/lib/features/auth/presentation/profile_screen.dart:16:8 • unused_import
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/auth/presentation/profile_screen.dart:79:7 • curly_braces_in_flow_control_structures
|
||||
info • 'Share' is deprecated and shouldn't be used. Use SharePlus instead • kifi-app/lib/features/auth/presentation/profile_screen.dart:108:13 • deprecated_member_use
|
||||
info • 'shareXFiles' is deprecated and shouldn't be used. Use SharePlus.instance.share() instead • kifi-app/lib/features/auth/presentation/profile_screen.dart:108:19 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/auth/presentation/profile_screen.dart:113:9 • curly_braces_in_flow_control_structures
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:142:64 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:158:37 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:170:38 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:199:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:200:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:207:55 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:259:43 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:262:53 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:333:67 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:423:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:478:28 • deprecated_member_use
|
||||
info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • kifi-app/lib/features/auth/presentation/profile_screen.dart:519:9 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/profile_screen.dart:530:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/reset_password_screen.dart:143:63 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart:199:63 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart:257:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart:270:41 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart:407:11 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart:497:37 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart:563:7 • curly_braces_in_flow_control_structures
|
||||
info • The private field _isSearching could be 'final' • kifi-app/lib/features/budget/presentation/budget_screen.dart:18:8 • prefer_final_fields
|
||||
warning • The value of the field '_isSearching' isn't used • kifi-app/lib/features/budget/presentation/budget_screen.dart:18:8 • unused_field
|
||||
info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • kifi-app/lib/features/budget/presentation/budget_screen.dart:81:25 • deprecated_member_use
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/budget/presentation/budget_screen.dart:110:60 • use_build_context_synchronously
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/budget/presentation/budget_screen.dart:168:23 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/budget/presentation/budget_screen.dart:362:35 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/budget/presentation/budget_screen.dart:363:40 • curly_braces_in_flow_control_structures
|
||||
warning • Unused import: '../../../inventory/presentation/quick_adjust_stock_screen.dart' • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:7:8 • unused_import
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:63:64 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:68:43 • deprecated_member_use
|
||||
warning • Dead code • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:233:48 • dead_code
|
||||
warning • The left operand can't be null, so the right operand is never executed • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:233:51 • dead_null_aware_expression
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:243:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:270:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:271:51 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:275:50 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:276:53 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:285:54 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:286:57 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:375:24 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:377:43 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/hub/business_hub_screen.dart:396:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/business/presentation/reports/reports_screen.dart:82:34 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/business/presentation/widgets/business_profile_form_sheet.dart:161:29 • deprecated_member_use
|
||||
info • The private field _isSearching could be 'final' • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:21:8 • prefer_final_fields
|
||||
warning • The value of the field '_isSearching' isn't used • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:21:8 • unused_field
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:118:25 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:129:44 • curly_braces_in_flow_control_structures
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:148:27 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:205:33 • deprecated_member_use
|
||||
warning • The value of the local variable 'newWallet' isn't used • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:312:39 • unused_local_variable
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:382:23 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:637:45 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:646:64 • curly_braces_in_flow_control_structures
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:717:47 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:766:53 • deprecated_member_use
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:818:53 • avoid_print
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:819:53 • avoid_print
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:820:53 • avoid_print
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:821:53 • avoid_print
|
||||
info • The private field _autoCompleteCtrl could be 'final' • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:955:25 • prefer_final_fields
|
||||
warning • The receiver can't be null, so the null-aware operator '?.' is unnecessary • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:1068:38 • invalid_null_aware_operator
|
||||
warning • The receiver can't be null, so the null-aware operator '?.' is unnecessary • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:1068:92 • invalid_null_aware_operator
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:1082:69 • deprecated_member_use
|
||||
info • Unnecessary use of multiple underscores • kifi-app/lib/features/dashboard/presentation/accounts_screen.dart:1088:49 • unnecessary_underscores
|
||||
warning • Unused import: '../../projects/presentation/project_hub_screen.dart' • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:3:8 • unused_import
|
||||
warning • Unused import: '../../projects/presentation/projects_screen.dart' • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:4:8 • unused_import
|
||||
warning • Unused import: '../../sales/domain/invoice.dart' • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:21:8 • unused_import
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:167:28 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:177:30 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:178:35 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:179:35 • curly_braces_in_flow_control_structures
|
||||
warning • The value of the local variable 'totalIncome' isn't used • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:291:17 • unused_local_variable
|
||||
warning • The value of the local variable 'totalExpense' isn't used • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:292:17 • unused_local_variable
|
||||
warning • The value of the local variable 'totalInvestment' isn't used • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:293:17 • unused_local_variable
|
||||
warning • The value of the local variable 'totalPayablesPeriod' isn't used • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:295:18 • unused_local_variable
|
||||
warning • The value of the local variable 'totalReceivablesPeriod' isn't used • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:296:18 • unused_local_variable
|
||||
warning • Dead code • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:348:52 • dead_code
|
||||
warning • The left operand can't be null, so the right operand is never executed • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:348:55 • dead_null_aware_expression
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:590:64 • deprecated_member_use
|
||||
warning • The value of the local variable 'safeIndex' isn't used • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:666:15 • unused_local_variable
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:705:24 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart:707:43 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/dashboard/presentation/maturity_dialog.dart:222:17 • deprecated_member_use
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/dashboard/presentation/maturity_dialog.dart:274:43 • use_build_context_synchronously
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/dashboard/presentation/maturity_dialog.dart:275:50 • use_build_context_synchronously
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/dashboard/presentation/maturity_dialog.dart:283:50 • use_build_context_synchronously
|
||||
warning • Unused import: '../../../core/theme/nature_colors.dart' • kifi-app/lib/features/dashboard/presentation/wallet_ledger_screen.dart:6:8 • unused_import
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/budget_status_card.dart:47:7 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/budget_status_card.dart:49:7 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/category_spending_chart.dart:32:9 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/category_spending_chart.dart:36:9 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/category_spending_chart.dart:40:9 • curly_braces_in_flow_control_structures
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/category_spending_chart.dart:114:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/category_spending_chart.dart:126:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/category_spending_chart.dart:246:42 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/day_wise_spending_chart.dart:49:11 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/day_wise_spending_chart.dart:53:11 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/day_wise_spending_chart.dart:57:11 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/day_wise_spending_chart.dart:66:17 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/day_wise_spending_chart.dart:69:17 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/day_wise_spending_chart.dart:130:31 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/day_wise_spending_chart.dart:136:31 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/dashboard/presentation/widgets/day_wise_spending_chart.dart:138:31 • curly_braces_in_flow_control_structures
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/statistics_tab.dart:53:49 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/statistics_tab.dart:65:42 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/statistics_tab.dart:207:43 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/statistics_tab.dart:257:65 • deprecated_member_use
|
||||
warning • Unused import: 'package:lucide_icons/lucide_icons.dart' • kifi-app/lib/features/dashboard/presentation/widgets/swipeable_account_card.dart:2:8 • unused_import
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/swipeable_account_card.dart:82:38 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/swipeable_account_card.dart:86:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/swipeable_account_card.dart:97:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/swipeable_account_card.dart:161:37 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/upcoming_dues_widget.dart:142:42 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/upcoming_dues_widget.dart:148:40 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/upcoming_dues_widget.dart:152:38 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/upcoming_dues_widget.dart:157:41 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/dashboard/presentation/widgets/upcoming_dues_widget.dart:172:30 • deprecated_member_use
|
||||
info • The import of 'package:flutter_image_compress/flutter_image_compress.dart' is unnecessary because all of the used elements are also provided by the import of 'package:image_picker/image_picker.dart' • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:6:8 • unnecessary_import
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:273:87 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:310:70 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:369:70 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:422:13 • deprecated_member_use
|
||||
warning • Dead code • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:490:28 • dead_code
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:496:58 • deprecated_member_use
|
||||
info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:509:25 • deprecated_member_use
|
||||
info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:623:52 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:623:67 • deprecated_member_use
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:676:59 • use_build_context_synchronously
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:701:64 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:702:83 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:729:53 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:730:57 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/add_product_screen.dart:759:61 • deprecated_member_use
|
||||
warning • Unused import: '../../../core/theme/nature_colors.dart' • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:4:8 • unused_import
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:88:63 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:110:50 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:113:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:113:77 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:118:39 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:132:46 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:164:56 • deprecated_member_use
|
||||
warning • Dead code • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:282:50 • dead_code
|
||||
warning • The left operand can't be null, so the right operand is never executed • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:282:53 • dead_null_aware_expression
|
||||
warning • The left operand can't be null, so the right operand is never executed • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:284:35 • dead_null_aware_expression
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:338:41 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:350:36 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:391:25 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:423:61 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:430:58 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:430:99 • deprecated_member_use
|
||||
info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:451:27 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:464:25 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:513:29 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:540:29 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:554:29 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/category_management_screen.dart:587:54 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart:167:50 • deprecated_member_use
|
||||
info • Unnecessary use of multiple underscores • kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart:299:55 • unnecessary_underscores
|
||||
warning • Unused import: 'package:kifi_app/features/inventory/presentation/widgets/adjust_stock_sheet.dart' • kifi-app/lib/features/inventory/presentation/product_detail_screen.dart:8:8 • unused_import
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/product_list_screen.dart:157:50 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/product_list_screen.dart:207:68 • deprecated_member_use
|
||||
info • Missing type annotation • kifi-app/lib/features/inventory/presentation/product_list_screen.dart:302:29 • strict_top_level_inference
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/product_list_screen.dart:308:30 • deprecated_member_use
|
||||
info • Missing type annotation • kifi-app/lib/features/inventory/presentation/quick_adjust_stock_screen.dart:175:29 • strict_top_level_inference
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/quick_adjust_stock_screen.dart:181:30 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart:87:38 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart:88:36 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/inventory/presentation/widgets/adjust_stock_sheet.dart:100:13 • deprecated_member_use
|
||||
warning • Unused import: '../../../../core/theme/nature_colors.dart' • kifi-app/lib/features/inventory/presentation/widgets/quick_rate_update_sheet.dart:3:8 • unused_import
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/widgets/quick_rate_update_sheet.dart:79:36 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/widgets/quick_rate_update_sheet.dart:90:40 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/widgets/quick_rate_update_sheet.dart:112:36 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/inventory/presentation/widgets/quick_rate_update_sheet.dart:114:55 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/inventory/presentation/widgets/quick_rate_update_sheet.dart:126:15 • deprecated_member_use
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/inventory/providers/product_categories_provider.dart:128:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/inventory/providers/product_categories_provider.dart:160:9 • avoid_print
|
||||
info • The import of 'package:image_picker/image_picker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter_image_compress/flutter_image_compress.dart' • kifi-app/lib/features/inventory/providers/products_provider.dart:10:8 • unnecessary_import
|
||||
warning • The stack trace variable 'stack' isn't used and can be removed • kifi-app/lib/features/inventory/providers/products_provider.dart:69:17 • unused_catch_stack
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:37:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:38:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:39:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:56:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:57:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:58:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:59:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:60:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:88:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:89:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:90:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:91:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:92:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:112:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:113:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:114:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:115:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:116:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:117:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:118:9 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/projects/data/project_repository.dart:119:9 • use_null_aware_elements
|
||||
warning • Dead code • kifi-app/lib/features/projects/presentation/widgets/add_project_sheet.dart:32:40 • dead_code
|
||||
warning • The left operand can't be null, so the right operand is never executed • kifi-app/lib/features/projects/presentation/widgets/add_project_sheet.dart:32:43 • dead_null_aware_expression
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/projects/presentation/widgets/add_project_sheet.dart:90:27 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/projects/presentation/widgets/add_project_sheet.dart:117:72 • deprecated_member_use
|
||||
info • Don't use 'BuildContext's across async gaps • kifi-app/lib/features/projects/presentation/widgets/add_project_sheet.dart:189:28 • use_build_context_synchronously
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/projects/presentation/widgets/add_task_sheet.dart:171:31 • curly_braces_in_flow_control_structures
|
||||
info • Parameter 'key' could be a super parameter • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:18:9 • use_super_parameters
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:67:11 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:69:11 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:71:11 • curly_braces_in_flow_control_structures
|
||||
info • 'Share' is deprecated and shouldn't be used. Use SharePlus instead • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:141:15 • deprecated_member_use
|
||||
info • 'shareXFiles' is deprecated and shouldn't be used. Use SharePlus.instance.share() instead • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:141:21 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:175:19 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:250:49 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:264:49 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/projects/presentation/widgets/task_details_sheet.dart:270:49 • curly_braces_in_flow_control_structures
|
||||
warning • Unused import: '../../auth/providers/auth_provider.dart' • kifi-app/lib/features/projects/providers/project_provider.dart:3:8 • unused_import
|
||||
warning • Unused import: 'dart:async' • kifi-app/lib/features/projects/providers/user_provider.dart:4:8 • unused_import
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/domain/invoice.dart:198:7 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/domain/invoice.dart:205:7 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/domain/invoice.dart:216:7 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/domain/invoice.dart:219:7 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/domain/invoice.dart:263:7 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/domain/invoice.dart:266:7 • curly_braces_in_flow_control_structures
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/customer_ledger_screen.dart:183:36 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/customers_list_screen.dart:146:54 • deprecated_member_use
|
||||
warning • Unused import: '../../inventory/domain/inventory_item.dart' • kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart:17:8 • unused_import
|
||||
warning • The '!' will have no effect because the receiver can't be null • kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart:205:65 • unnecessary_non_null_assertion
|
||||
warning • The '!' will have no effect because the receiver can't be null • kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart:206:63 • unnecessary_non_null_assertion
|
||||
warning • The '!' will have no effect because the receiver can't be null • kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart:207:66 • unnecessary_non_null_assertion
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart:395:32 • use_build_context_synchronously
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart:399:32 • use_build_context_synchronously
|
||||
info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart:539:43 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:115:44 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:144:51 • deprecated_member_use
|
||||
warning • Dead code • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:413:41 • dead_code
|
||||
warning • The left operand can't be null, so the right operand is never executed • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:413:44 • dead_null_aware_expression
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:431:46 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:661:44 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:747:23 • curly_braces_in_flow_control_structures
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:752:52 • deprecated_member_use
|
||||
info • 'Share' is deprecated and shouldn't be used. Use SharePlus instead • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:940:13 • deprecated_member_use
|
||||
info • 'shareXFiles' is deprecated and shouldn't be used. Use SharePlus.instance.share() instead • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:940:19 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:945:9 • curly_braces_in_flow_control_structures
|
||||
warning • Dead code • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1152:43 • dead_code
|
||||
warning • The left operand can't be null, so the right operand is never executed • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1152:46 • dead_null_aware_expression
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1164:23 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1166:23 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1168:23 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1171:23 • curly_braces_in_flow_control_structures
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1174:23 • curly_braces_in_flow_control_structures
|
||||
info • 'Share' is deprecated and shouldn't be used. Use SharePlus instead • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1387:13 • deprecated_member_use
|
||||
info • 'shareXFiles' is deprecated and shouldn't be used. Use SharePlus.instance.share() instead • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1387:19 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/sales/presentation/invoice_details_screen.dart:1392:9 • curly_braces_in_flow_control_structures
|
||||
warning • Dead code • kifi-app/lib/features/sales/presentation/invoices_list_screen.dart:151:59 • dead_code
|
||||
warning • The left operand can't be null, so the right operand is never executed • kifi-app/lib/features/sales/presentation/invoices_list_screen.dart:151:62 • dead_null_aware_expression
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/invoices_list_screen.dart:333:50 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/invoices_list_screen.dart:392:45 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/invoices_list_screen.dart:485:62 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/sales/presentation/invoices_list_screen.dart:488:64 • deprecated_member_use
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/sales/presentation/widgets/receive_payment_sheet.dart:173:29 • use_build_context_synchronously
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/sales/presentation/widgets/receive_payment_sheet.dart:176:48 • use_build_context_synchronously
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/sales/presentation/widgets/receive_payment_sheet.dart:185:29 • use_build_context_synchronously
|
||||
warning • Unused import: 'dart:io' • kifi-app/lib/features/sales/providers/customers_provider.dart:2:8 • unused_import
|
||||
info • The import of 'package:image_picker/image_picker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter_image_compress/flutter_image_compress.dart' • kifi-app/lib/features/sales/providers/customers_provider.dart:5:8 • unnecessary_import
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/sales/providers/customers_provider.dart:30:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/sales/providers/invoices_provider.dart:22:7 • avoid_print
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/sales/providers/invoices_provider.dart:88:7 • avoid_print
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:35:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:36:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:37:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:38:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:39:7 • use_null_aware_elements
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/transactions/data/repository.dart:106:7 • avoid_print
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:164:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:168:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:169:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:170:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:171:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:172:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:191:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:192:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:193:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:194:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:195:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:196:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:197:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:198:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:199:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:200:7 • use_null_aware_elements
|
||||
info • Use the null-aware marker '?' rather than a null check via an 'if' • kifi-app/lib/features/transactions/data/repository.dart:201:7 • use_null_aware_elements
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart:297:25 • deprecated_member_use
|
||||
info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart:524:54 • use_build_context_synchronously
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart:836:21 • deprecated_member_use
|
||||
info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart:1048:19 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart:1053:19 • deprecated_member_use
|
||||
info • 'translate' is deprecated and shouldn't be used. Use translateByVector3, translateByVector4, or translateByDouble instead • kifi-app/lib/features/transactions/presentation/widgets/attachment_gallery_screen.dart:110:11 • deprecated_member_use
|
||||
info • 'scale' is deprecated and shouldn't be used. Use scaleByVector3, scaleByVector4, or scaleByDouble instead • kifi-app/lib/features/transactions/presentation/widgets/attachment_gallery_screen.dart:111:11 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/transactions/presentation/widgets/transaction_filter_sheet.dart:90:17 • deprecated_member_use
|
||||
info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • kifi-app/lib/features/transactions/presentation/widgets/transaction_filter_sheet.dart:115:17 • deprecated_member_use
|
||||
warning • Unused import: '../data/repository.dart' • kifi-app/lib/features/transactions/providers/paginated_transaction_provider.dart:3:8 • unused_import
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/features/transactions/providers/paginated_transaction_provider.dart:103:7 • avoid_print
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/vendor/presentation/add_vendor_sheet.dart:210:58 • deprecated_member_use
|
||||
warning • Unused import: 'dart:io' • kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart:5:8 • unused_import
|
||||
warning • Unused import: '../domain/vendor.dart' • kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart:16:8 • unused_import
|
||||
info • Don't use 'BuildContext's across async gaps • kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart:239:25 • use_build_context_synchronously
|
||||
warning • Unused import: 'dart:typed_data' • kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart:2:8 • unused_import
|
||||
info • 'Share' is deprecated and shouldn't be used. Use SharePlus instead • kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart:542:13 • deprecated_member_use
|
||||
info • 'shareXFiles' is deprecated and shouldn't be used. Use SharePlus.instance.share() instead • kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart:542:19 • deprecated_member_use
|
||||
info • Statements in an if should be enclosed in a block • kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart:547:9 • curly_braces_in_flow_control_structures
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart:663:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/vendor/presentation/purchase_order_details_screen.dart:692:49 • deprecated_member_use
|
||||
warning • The value of the local variable 'darkTheme' isn't used • kifi-app/lib/features/vendor/presentation/purchase_orders_list_screen.dart:59:11 • unused_local_variable
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/vendor/presentation/purchase_orders_list_screen.dart:146:50 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/vendor/presentation/purchase_orders_list_screen.dart:201:45 • deprecated_member_use
|
||||
warning • The value of the local variable 'darkTheme' isn't used • kifi-app/lib/features/vendor/presentation/vendors_list_screen.dart:20:11 • unused_local_variable
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/vendor/presentation/vendors_list_screen.dart:85:48 • deprecated_member_use
|
||||
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • kifi-app/lib/features/vendor/presentation/vendors_list_screen.dart:105:66 • deprecated_member_use
|
||||
warning • Unused import: 'package:dio/dio.dart' • kifi-app/lib/features/vendor/providers/purchase_orders_provider.dart:1:8 • unused_import
|
||||
warning • Unused import: 'package:dio/dio.dart' • kifi-app/lib/features/vendor/providers/vendors_provider.dart:1:8 • unused_import
|
||||
warning • Unused import: 'core/network/dio_client.dart' • kifi-app/lib/main.dart:11:8 • unused_import
|
||||
warning • The stack trace variable 'stacktrace' isn't used and can be removed • kifi-app/lib/main.dart:94:17 • unused_catch_stack
|
||||
info • Don't invoke 'print' in production code • kifi-app/lib/test_dio.dart:5:3 • avoid_print
|
||||
error • Target of URI doesn't exist: 'package:flutter/material.dart' • scratch/UpdateTaskDetailsSheet.dart:3:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: 'package:flutter_riverpod/flutter_riverpod.dart' • scratch/UpdateTaskDetailsSheet.dart:4:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: 'package:lucide_icons/lucide_icons.dart' • scratch/UpdateTaskDetailsSheet.dart:5:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: 'package:image_picker/image_picker.dart' • scratch/UpdateTaskDetailsSheet.dart:6:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: 'package:file_picker/file_picker.dart' • scratch/UpdateTaskDetailsSheet.dart:7:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: 'package:path_provider/path_provider.dart' • scratch/UpdateTaskDetailsSheet.dart:8:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: 'package:dio/dio.dart' • scratch/UpdateTaskDetailsSheet.dart:9:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: 'package:share_plus/share_plus.dart' • scratch/UpdateTaskDetailsSheet.dart:10:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: 'package:flutter_secure_storage/flutter_secure_storage.dart' • scratch/UpdateTaskDetailsSheet.dart:11:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: '../../domain/project_task.dart' • scratch/UpdateTaskDetailsSheet.dart:12:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: '../../providers/project_provider.dart' • scratch/UpdateTaskDetailsSheet.dart:13:8 • uri_does_not_exist
|
||||
error • Target of URI doesn't exist: '../../../transactions/presentation/widgets/attachment_gallery_screen.dart' • scratch/UpdateTaskDetailsSheet.dart:14:8 • uri_does_not_exist
|
||||
error • Classes can only extend other classes • scratch/UpdateTaskDetailsSheet.dart:16:32 • extends_non_class
|
||||
error • Undefined class 'ProjectTask' • scratch/UpdateTaskDetailsSheet.dart:17:9 • undefined_class
|
||||
error • Undefined class 'Key' • scratch/UpdateTaskDetailsSheet.dart:18:27 • undefined_class
|
||||
error • The named parameter 'key' isn't defined • scratch/UpdateTaskDetailsSheet.dart:18:66 • undefined_named_parameter
|
||||
error • Undefined class 'ConsumerState' • scratch/UpdateTaskDetailsSheet.dart:21:3 • undefined_class
|
||||
warning • The method doesn't override an inherited method • scratch/UpdateTaskDetailsSheet.dart:21:35 • override_on_non_overriding_member
|
||||
error • Classes can only extend other classes • scratch/UpdateTaskDetailsSheet.dart:24:38 • extends_non_class
|
||||
error • Undefined class 'TextEditingController' • scratch/UpdateTaskDetailsSheet.dart:25:9 • undefined_class
|
||||
error • The method 'TextEditingController' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:25:52 • undefined_method
|
||||
error • Undefined class 'ImagePicker' • scratch/UpdateTaskDetailsSheet.dart:26:9 • undefined_class
|
||||
error • The method 'ImagePicker' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:26:31 • undefined_method
|
||||
warning • The method doesn't override an inherited method • scratch/UpdateTaskDetailsSheet.dart:33:8 • override_on_non_overriding_member
|
||||
error • The method 'initState' isn't defined in a superclass of '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:34:11 • undefined_super_member
|
||||
error • Const variables must be initialized with a constant value • scratch/UpdateTaskDetailsSheet.dart:39:21 • const_initialized_with_non_constant_value
|
||||
error • The method 'FlutterSecureStorage' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:39:21 • undefined_method
|
||||
error • Undefined name 'mounted' • scratch/UpdateTaskDetailsSheet.dart:41:9 • undefined_identifier
|
||||
error • The method 'setState' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:41:18 • undefined_method
|
||||
error • Undefined class 'XFile' • scratch/UpdateTaskDetailsSheet.dart:45:11 • undefined_class
|
||||
error • Undefined name 'ImageSource' • scratch/UpdateTaskDetailsSheet.dart:45:58 • undefined_identifier
|
||||
error • The method 'setState' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:48:7 • undefined_method
|
||||
error • Undefined class 'FilePickerResult' • scratch/UpdateTaskDetailsSheet.dart:59:5 • undefined_class
|
||||
error • Undefined name 'FilePicker' • scratch/UpdateTaskDetailsSheet.dart:59:38 • undefined_identifier
|
||||
error • The method 'setState' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:69:7 • undefined_method
|
||||
error • The method 'setState' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:82:5 • undefined_method
|
||||
error • Undefined name 'ref' • scratch/UpdateTaskDetailsSheet.dart:84:20 • undefined_identifier
|
||||
error • Undefined name 'projectRepositoryProvider' • scratch/UpdateTaskDetailsSheet.dart:84:29 • undefined_identifier
|
||||
error • Undefined name 'widget' • scratch/UpdateTaskDetailsSheet.dart:86:17 • undefined_identifier
|
||||
error • The method 'setState' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:91:7 • undefined_method
|
||||
error • Undefined name 'ref' • scratch/UpdateTaskDetailsSheet.dart:94:7 • undefined_identifier
|
||||
error • The method 'projectTaskCommentsProvider' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:94:22 • undefined_method
|
||||
error • Undefined name 'widget' • scratch/UpdateTaskDetailsSheet.dart:94:50 • undefined_identifier
|
||||
error • Undefined name 'mounted' • scratch/UpdateTaskDetailsSheet.dart:96:11 • undefined_identifier
|
||||
error • Undefined name 'ScaffoldMessenger' • scratch/UpdateTaskDetailsSheet.dart:97:9 • undefined_identifier
|
||||
error • Undefined name 'context' • scratch/UpdateTaskDetailsSheet.dart:97:30 • undefined_identifier
|
||||
error • The method 'SnackBar' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:97:52 • undefined_method
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:97:70 • undefined_method
|
||||
error • Undefined name 'mounted' • scratch/UpdateTaskDetailsSheet.dart:100:11 • undefined_identifier
|
||||
error • The method 'setState' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:100:20 • undefined_method
|
||||
error • The method 'setState' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:111:5 • undefined_method
|
||||
error • The method 'getTemporaryDirectory' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:114:25 • undefined_method
|
||||
error • The method 'Dio' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:116:19 • undefined_method
|
||||
error • Undefined name 'mounted' • scratch/UpdateTaskDetailsSheet.dart:123:11 • undefined_identifier
|
||||
error • Undefined name 'ScaffoldMessenger' • scratch/UpdateTaskDetailsSheet.dart:124:9 • undefined_identifier
|
||||
error • Undefined name 'context' • scratch/UpdateTaskDetailsSheet.dart:124:30 • undefined_identifier
|
||||
error • The method 'SnackBar' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:124:52 • undefined_method
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:124:70 • undefined_method
|
||||
error • Undefined name 'Share' • scratch/UpdateTaskDetailsSheet.dart:125:15 • undefined_identifier
|
||||
error • The method 'XFile' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:125:34 • undefined_method
|
||||
error • Undefined name 'mounted' • scratch/UpdateTaskDetailsSheet.dart:128:11 • undefined_identifier
|
||||
error • Undefined name 'ScaffoldMessenger' • scratch/UpdateTaskDetailsSheet.dart:129:9 • undefined_identifier
|
||||
error • Undefined name 'context' • scratch/UpdateTaskDetailsSheet.dart:129:30 • undefined_identifier
|
||||
error • The method 'SnackBar' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:129:52 • undefined_method
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:129:70 • undefined_method
|
||||
error • Undefined name 'mounted' • scratch/UpdateTaskDetailsSheet.dart:132:11 • undefined_identifier
|
||||
error • The method 'setState' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:133:9 • undefined_method
|
||||
error • Undefined class 'Widget' • scratch/UpdateTaskDetailsSheet.dart:139:3 • undefined_class
|
||||
warning • The method doesn't override an inherited method • scratch/UpdateTaskDetailsSheet.dart:139:10 • override_on_non_overriding_member
|
||||
error • Undefined class 'BuildContext' • scratch/UpdateTaskDetailsSheet.dart:139:16 • undefined_class
|
||||
error • Undefined name 'ref' • scratch/UpdateTaskDetailsSheet.dart:140:27 • undefined_identifier
|
||||
error • The method 'projectTaskCommentsProvider' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:140:37 • undefined_method
|
||||
error • Undefined name 'widget' • scratch/UpdateTaskDetailsSheet.dart:140:65 • undefined_identifier
|
||||
error • The method 'Container' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:142:12 • undefined_method
|
||||
error • Undefined name 'MediaQuery' • scratch/UpdateTaskDetailsSheet.dart:143:15 • undefined_identifier
|
||||
error • The name 'BoxDecoration' isn't a class • scratch/UpdateTaskDetailsSheet.dart:144:25 • creation_with_non_type
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:145:16 • undefined_identifier
|
||||
error • Undefined name 'BorderRadius' • scratch/UpdateTaskDetailsSheet.dart:146:23 • undefined_identifier
|
||||
error • Undefined name 'Radius' • scratch/UpdateTaskDetailsSheet.dart:146:50 • undefined_identifier
|
||||
error • The method 'Column' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:148:14 • undefined_method
|
||||
error • The method 'Expanded' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:151:11 • undefined_method
|
||||
error • The name 'Center' isn't a class • scratch/UpdateTaskDetailsSheet.dart:154:52 • creation_with_non_type
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:154:66 • undefined_method
|
||||
error • Undefined name 'ListView' • scratch/UpdateTaskDetailsSheet.dart:155:24 • undefined_identifier
|
||||
error • Undefined name 'EdgeInsets' • scratch/UpdateTaskDetailsSheet.dart:156:34 • undefined_identifier
|
||||
error • The method 'Card' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:160:28 • undefined_method
|
||||
error • Undefined name 'EdgeInsets' • scratch/UpdateTaskDetailsSheet.dart:161:37 • undefined_identifier
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:162:30 • undefined_identifier
|
||||
error • The method 'Padding' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:164:30 • undefined_method
|
||||
error • Undefined name 'EdgeInsets' • scratch/UpdateTaskDetailsSheet.dart:165:40 • undefined_identifier
|
||||
error • The method 'Column' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:166:32 • undefined_method
|
||||
error • Undefined name 'CrossAxisAlignment' • scratch/UpdateTaskDetailsSheet.dart:167:47 • undefined_identifier
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:169:29 • undefined_method
|
||||
error • The name 'TextStyle' isn't a class • scratch/UpdateTaskDetailsSheet.dart:169:58 • creation_with_non_type
|
||||
error • The name 'SizedBox' isn't a class • scratch/UpdateTaskDetailsSheet.dart:171:37 • creation_with_non_type
|
||||
error • The method 'Wrap' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:172:31 • undefined_method
|
||||
error • Undefined class 'ImageProvider' • scratch/UpdateTaskDetailsSheet.dart:191:37 • undefined_class
|
||||
error • The method 'MemoryImage' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:194:51 • undefined_method
|
||||
error • The method 'NetworkImage' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:198:45 • undefined_method
|
||||
error • The name 'AssetImage' isn't a class • scratch/UpdateTaskDetailsSheet.dart:199:51 • creation_with_non_type
|
||||
error • The name 'ImageProvider' isn't a type, so it can't be used in an 'as' expression • scratch/UpdateTaskDetailsSheet.dart:199:98 • cast_to_non_type
|
||||
error • The method 'GestureDetector' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:202:44 • undefined_method
|
||||
error • The method 'MemoryImage' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:214:76 • undefined_method
|
||||
error • The name 'ImageProvider' isn't a type, so it can't be used in an 'as' expression • scratch/UpdateTaskDetailsSheet.dart:214:108 • cast_to_non_type
|
||||
error • The method 'MemoryImage' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:216:86 • undefined_method
|
||||
error • The name 'ImageProvider' isn't a type, so it can't be used in an 'as' expression • scratch/UpdateTaskDetailsSheet.dart:216:137 • cast_to_non_type
|
||||
error • The method 'NetworkImage' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:218:52 • undefined_method
|
||||
error • The name 'ImageProvider' isn't a type, so it can't be used in an 'as' expression • scratch/UpdateTaskDetailsSheet.dart:218:122 • cast_to_non_type
|
||||
error • Undefined name 'Navigator' • scratch/UpdateTaskDetailsSheet.dart:235:41 • undefined_identifier
|
||||
error • The method 'MaterialPageRoute' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:237:43 • undefined_method
|
||||
error • The method 'AttachmentGalleryScreen' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:238:67 • undefined_method
|
||||
error • The method 'ClipRRect' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:245:46 • undefined_method
|
||||
error • Undefined name 'BorderRadius' • scratch/UpdateTaskDetailsSheet.dart:246:55 • undefined_identifier
|
||||
error • The method 'Image' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:247:48 • undefined_method
|
||||
error • Undefined name 'BoxFit' • scratch/UpdateTaskDetailsSheet.dart:251:48 • undefined_identifier
|
||||
error • The method 'GestureDetector' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:261:44 • undefined_method
|
||||
error • The method 'Container' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:267:46 • undefined_method
|
||||
error • The method 'BoxDecoration' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:270:53 • undefined_method
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:271:50 • undefined_identifier
|
||||
error • Undefined name 'BorderRadius' • scratch/UpdateTaskDetailsSheet.dart:272:57 • undefined_identifier
|
||||
error • Undefined name 'Border' • scratch/UpdateTaskDetailsSheet.dart:273:51 • undefined_identifier
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:273:69 • undefined_identifier
|
||||
error • The method 'Column' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:275:48 • undefined_method
|
||||
error • Undefined name 'MainAxisAlignment' • scratch/UpdateTaskDetailsSheet.dart:276:62 • undefined_identifier
|
||||
error • The name 'SizedBox' isn't a class • scratch/UpdateTaskDetailsSheet.dart:279:57 • creation_with_non_type
|
||||
error • The method 'CircularProgressIndicator' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:279:96 • undefined_method
|
||||
error • The name 'Icon' isn't a class • scratch/UpdateTaskDetailsSheet.dart:280:57 • creation_with_non_type
|
||||
error • Undefined name 'LucideIcons' • scratch/UpdateTaskDetailsSheet.dart:280:62 • undefined_identifier
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:280:91 • undefined_identifier
|
||||
error • The name 'SizedBox' isn't a class • scratch/UpdateTaskDetailsSheet.dart:281:51 • creation_with_non_type
|
||||
error • The method 'Padding' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:282:45 • undefined_method
|
||||
error • Undefined name 'EdgeInsets' • scratch/UpdateTaskDetailsSheet.dart:283:62 • undefined_identifier
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:284:54 • undefined_method
|
||||
error • The name 'TextStyle' isn't a class • scratch/UpdateTaskDetailsSheet.dart:286:62 • creation_with_non_type
|
||||
error • Undefined name 'TextOverflow' • scratch/UpdateTaskDetailsSheet.dart:288:59 • undefined_identifier
|
||||
error • The name 'Center' isn't a class • scratch/UpdateTaskDetailsSheet.dart:306:36 • creation_with_non_type
|
||||
error • The method 'CircularProgressIndicator' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:306:50 • undefined_method
|
||||
error • The method 'Center' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:307:38 • undefined_method
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:307:52 • undefined_method
|
||||
error • Undefined class 'Widget' • scratch/UpdateTaskDetailsSheet.dart:316:3 • undefined_class
|
||||
error • Undefined class 'BuildContext' • scratch/UpdateTaskDetailsSheet.dart:316:23 • undefined_class
|
||||
error • The method 'Container' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:317:12 • undefined_method
|
||||
error • Undefined name 'EdgeInsets' • scratch/UpdateTaskDetailsSheet.dart:318:22 • undefined_identifier
|
||||
error • The method 'BoxDecoration' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:319:19 • undefined_method
|
||||
error • The method 'Border' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:319:41 • undefined_method
|
||||
error • The method 'BorderSide' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:319:56 • undefined_method
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:319:74 • undefined_identifier
|
||||
error • The method 'Row' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:320:14 • undefined_method
|
||||
error • Undefined name 'MainAxisAlignment' • scratch/UpdateTaskDetailsSheet.dart:321:28 • undefined_identifier
|
||||
error • The method 'Expanded' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:323:11 • undefined_method
|
||||
error • The method 'Column' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:324:20 • undefined_method
|
||||
error • Undefined name 'CrossAxisAlignment' • scratch/UpdateTaskDetailsSheet.dart:325:35 • undefined_identifier
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:327:17 • undefined_method
|
||||
error • Undefined name 'widget' • scratch/UpdateTaskDetailsSheet.dart:327:22 • undefined_identifier
|
||||
error • The name 'TextStyle' isn't a class • scratch/UpdateTaskDetailsSheet.dart:327:54 • creation_with_non_type
|
||||
error • Undefined name 'FontWeight' • scratch/UpdateTaskDetailsSheet.dart:327:90 • undefined_identifier
|
||||
error • Undefined name 'widget' • scratch/UpdateTaskDetailsSheet.dart:328:21 • undefined_identifier
|
||||
error • Undefined name 'widget' • scratch/UpdateTaskDetailsSheet.dart:328:56 • undefined_identifier
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:329:19 • undefined_method
|
||||
error • Undefined name 'widget' • scratch/UpdateTaskDetailsSheet.dart:329:24 • undefined_identifier
|
||||
error • The name 'TextStyle' isn't a class • scratch/UpdateTaskDetailsSheet.dart:329:63 • creation_with_non_type
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:329:80 • undefined_identifier
|
||||
error • The method 'IconButton' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:333:11 • undefined_method
|
||||
error • The name 'Icon' isn't a class • scratch/UpdateTaskDetailsSheet.dart:333:34 • creation_with_non_type
|
||||
error • Undefined name 'LucideIcons' • scratch/UpdateTaskDetailsSheet.dart:333:39 • undefined_identifier
|
||||
error • Undefined name 'Navigator' • scratch/UpdateTaskDetailsSheet.dart:333:72 • undefined_identifier
|
||||
error • Undefined class 'Widget' • scratch/UpdateTaskDetailsSheet.dart:339:3 • undefined_class
|
||||
error • The method 'Container' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:340:12 • undefined_method
|
||||
error • Undefined name 'EdgeInsets' • scratch/UpdateTaskDetailsSheet.dart:341:16 • undefined_identifier
|
||||
error • Undefined name 'MediaQuery' • scratch/UpdateTaskDetailsSheet.dart:342:17 • undefined_identifier
|
||||
error • Undefined name 'context' • scratch/UpdateTaskDetailsSheet.dart:342:31 • undefined_identifier
|
||||
error • The method 'BoxDecoration' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:347:19 • undefined_method
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:348:16 • undefined_identifier
|
||||
error • The method 'Border' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:349:17 • undefined_method
|
||||
error • The method 'BorderSide' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:349:29 • undefined_method
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:349:47 • undefined_identifier
|
||||
error • The method 'Column' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:351:14 • undefined_method
|
||||
error • Undefined name 'CrossAxisAlignment' • scratch/UpdateTaskDetailsSheet.dart:352:29 • undefined_identifier
|
||||
error • The method 'Padding' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:355:13 • undefined_method
|
||||
error • Undefined name 'EdgeInsets' • scratch/UpdateTaskDetailsSheet.dart:356:30 • undefined_identifier
|
||||
error • The method 'Wrap' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:357:22 • undefined_method
|
||||
error • The method 'Stack' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:363:26 • undefined_method
|
||||
error • The method 'Container' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:365:23 • undefined_method
|
||||
error • The method 'BoxDecoration' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:368:37 • undefined_method
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:369:34 • undefined_identifier
|
||||
error • Undefined name 'BorderRadius' • scratch/UpdateTaskDetailsSheet.dart:370:41 • undefined_identifier
|
||||
error • The method 'ClipRRect' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:373:29 • undefined_method
|
||||
error • Undefined name 'BorderRadius' • scratch/UpdateTaskDetailsSheet.dart:374:45 • undefined_identifier
|
||||
error • Undefined name 'Image' • scratch/UpdateTaskDetailsSheet.dart:375:38 • undefined_identifier
|
||||
error • Undefined name 'BoxFit' • scratch/UpdateTaskDetailsSheet.dart:377:38 • undefined_identifier
|
||||
error • The method 'Column' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:380:29 • undefined_method
|
||||
error • Undefined name 'MainAxisAlignment' • scratch/UpdateTaskDetailsSheet.dart:381:50 • undefined_identifier
|
||||
error • The name 'Icon' isn't a class • scratch/UpdateTaskDetailsSheet.dart:383:39 • creation_with_non_type
|
||||
error • Undefined name 'LucideIcons' • scratch/UpdateTaskDetailsSheet.dart:383:44 • undefined_identifier
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:383:83 • undefined_identifier
|
||||
error • The method 'Text' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:384:33 • undefined_method
|
||||
error • The name 'TextStyle' isn't a class • scratch/UpdateTaskDetailsSheet.dart:384:74 • creation_with_non_type
|
||||
error • Undefined name 'TextOverflow' • scratch/UpdateTaskDetailsSheet.dart:384:121 • undefined_identifier
|
||||
error • The method 'Positioned' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:388:23 • undefined_method
|
||||
error • The method 'GestureDetector' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:391:32 • undefined_method
|
||||
error • The method 'setState' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:393:29 • undefined_method
|
||||
error • The method 'Container' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:397:34 • undefined_method
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:398:36 • undefined_identifier
|
||||
error • The name 'Icon' isn't a class • scratch/UpdateTaskDetailsSheet.dart:399:42 • creation_with_non_type
|
||||
error • Undefined name 'LucideIcons' • scratch/UpdateTaskDetailsSheet.dart:399:47 • undefined_identifier
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:399:79 • undefined_identifier
|
||||
error • The method 'Row' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:408:11 • undefined_method
|
||||
error • The method 'IconButton' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:410:15 • undefined_method
|
||||
error • The name 'Icon' isn't a class • scratch/UpdateTaskDetailsSheet.dart:411:29 • creation_with_non_type
|
||||
error • Undefined name 'LucideIcons' • scratch/UpdateTaskDetailsSheet.dart:411:34 • undefined_identifier
|
||||
error • The method 'IconButton' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:414:15 • undefined_method
|
||||
error • The name 'Icon' isn't a class • scratch/UpdateTaskDetailsSheet.dart:415:29 • creation_with_non_type
|
||||
error • Undefined name 'LucideIcons' • scratch/UpdateTaskDetailsSheet.dart:415:34 • undefined_identifier
|
||||
error • The method 'Expanded' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:418:15 • undefined_method
|
||||
error • The method 'TextField' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:419:24 • undefined_method
|
||||
error • The name 'InputDecoration' isn't a class • scratch/UpdateTaskDetailsSheet.dart:421:37 • creation_with_non_type
|
||||
error • Undefined name 'InputBorder' • scratch/UpdateTaskDetailsSheet.dart:423:29 • undefined_identifier
|
||||
error • The name 'Padding' isn't a class • scratch/UpdateTaskDetailsSheet.dart:428:27 • creation_with_non_type
|
||||
error • Undefined name 'EdgeInsets' • scratch/UpdateTaskDetailsSheet.dart:428:44 • undefined_identifier
|
||||
error • The method 'SizedBox' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:428:73 • undefined_method
|
||||
error • The method 'CircularProgressIndicator' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:428:112 • undefined_method
|
||||
error • The method 'IconButton' isn't defined for the type '_TaskDetailsSheetState' • scratch/UpdateTaskDetailsSheet.dart:429:21 • undefined_method
|
||||
error • The name 'Icon' isn't a class • scratch/UpdateTaskDetailsSheet.dart:430:35 • creation_with_non_type
|
||||
error • Undefined name 'LucideIcons' • scratch/UpdateTaskDetailsSheet.dart:430:40 • undefined_identifier
|
||||
error • Undefined name 'Colors' • scratch/UpdateTaskDetailsSheet.dart:430:65 • undefined_identifier
|
||||
|
||||
17
kifi-api/TestJwt.java
Normal file
17
kifi-api/TestJwt.java
Normal file
@@ -0,0 +1,17 @@
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import java.util.HashMap;
|
||||
public class TestJwt {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
byte[] key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".getBytes();
|
||||
HashMap<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", 1L);
|
||||
String token = Jwts.builder().claims(claims).signWith(Keys.hmacShaKeyFor(key)).compact();
|
||||
Long parsed = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(key)).build().parseSignedClaims(token).getPayload().get("userId", Long.class);
|
||||
System.out.println("Parsed: " + parsed);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ USERNAME="technobee_admin"
|
||||
PASSWORD='M@tr!x#149@dm!N'
|
||||
IMAGE_NAME="kifi-api"
|
||||
TAG="latest"
|
||||
TAG_ONE="kifi-v2"
|
||||
FULL_IMAGE_NAME="$REGISTRY/$IMAGE_NAME:$TAG"
|
||||
FULL_IMAGE_NAME_ONE="$REGISTRY/$IMAGE_NAME:$TAG_ONE"
|
||||
|
||||
# Stop on any error
|
||||
set -e
|
||||
@@ -14,7 +16,7 @@ set -e
|
||||
echo "Logging into Docker registry: $REGISTRY..."
|
||||
echo "$PASSWORD" | docker login "$REGISTRY" -u "$USERNAME" --password-stdin
|
||||
|
||||
echo "Building and pushing Docker image for linux/amd64: $FULL_IMAGE_NAME..."
|
||||
docker buildx build --platform linux/amd64 -t "$FULL_IMAGE_NAME" --push .
|
||||
echo "Building and pushing Docker image for linux/amd64: $FULL_IMAGE_NAME and $FULL_IMAGE_NAME_ONE..."
|
||||
docker buildx build --no-cache --platform linux/amd64 -t "$FULL_IMAGE_NAME" -t "$FULL_IMAGE_NAME_ONE" --push .
|
||||
|
||||
echo "Done! Image built and pushed successfully."
|
||||
|
||||
@@ -45,10 +45,11 @@ public class SecurityConfig {
|
||||
.securityContextRepository(securityContextRepository)
|
||||
.authorizeExchange(exchange -> exchange
|
||||
.pathMatchers(HttpMethod.OPTIONS).permitAll()
|
||||
.pathMatchers("/api/kifi-v2/auth/**").permitAll()
|
||||
.pathMatchers("/api/kifi-v2/health/**").permitAll()
|
||||
.pathMatchers("/api/kifi-v2/auth/**", "/api/kifi-v2/categories/**", "/api/kifi-v2/purchase-orders/test/**").permitAll()
|
||||
.pathMatchers("/api/kifi-v2/health/**", "/api/kifi-v2/upload/view**").permitAll()
|
||||
.pathMatchers("/api/kifi-v2/wallets/test/**").permitAll()
|
||||
.pathMatchers("/public/legal/**").permitAll()
|
||||
.pathMatchers("/test/**").permitAll()
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.build();
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.kifi.api.controller;
|
||||
|
||||
import com.kifi.api.service.MinioServiceClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/kifi-v2/upload")
|
||||
@RequiredArgsConstructor
|
||||
public class UploadController {
|
||||
|
||||
private final MinioServiceClient minioServiceClient;
|
||||
|
||||
@PostMapping
|
||||
public Mono<Map<String, String>> upload(@RequestPart("file") Mono<FilePart> filePartMono) {
|
||||
return filePartMono.flatMap(filePart ->
|
||||
DataBufferUtils.join(filePart.content())
|
||||
.flatMap(dataBuffer -> {
|
||||
byte[] bytes = new byte[dataBuffer.readableByteCount()];
|
||||
dataBuffer.read(bytes);
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
|
||||
String base64 = Base64.getEncoder().encodeToString(bytes);
|
||||
String fileName = UUID.randomUUID().toString() + "-" + filePart.filename();
|
||||
|
||||
return minioServiceClient.uploadFile(
|
||||
"general",
|
||||
filePart.headers().getContentType() != null ? filePart.headers().getContentType().toString() : "application/octet-stream",
|
||||
fileName,
|
||||
base64
|
||||
).map(response -> Map.of("url", response.getFilePath()));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/view")
|
||||
public Mono<ResponseEntity<byte[]>> viewFile(@RequestParam("path") String path) {
|
||||
String contentType = "image/jpeg";
|
||||
if (path.endsWith(".png")) contentType = "image/png";
|
||||
else if (path.endsWith(".pdf")) contentType = "application/pdf";
|
||||
else if (path.endsWith(".webp")) contentType = "image/webp";
|
||||
|
||||
final String finalContentType = contentType;
|
||||
return minioServiceClient.downloadFile(contentType, path)
|
||||
.map(response -> {
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(response.getBase64Content());
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_TYPE, finalContentType)
|
||||
.header(HttpHeaders.CACHE_CONTROL, "max-age=86400")
|
||||
.body(decodedBytes);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ public class InventoryItemController {
|
||||
return inventoryItemService.getItemsByUserId(userId);
|
||||
}
|
||||
|
||||
@GetMapping("/product/{productId}")
|
||||
public Flux<InventoryItem> getItemsByProductId(@PathVariable Long productId, Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return inventoryItemService.getItemsByProductId(userId, productId);
|
||||
}
|
||||
|
||||
@GetMapping("/total-value")
|
||||
public Mono<ResponseEntity<BigDecimal>> getTotalInventoryValue(Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
|
||||
@@ -46,12 +46,12 @@ public class ProductCategoryController {
|
||||
existing.setName(category.getName());
|
||||
existing.setParentCategoryId(category.getParentCategoryId());
|
||||
existing.setHasChild(category.getHasChild());
|
||||
existing.setCommodityCode(category.getCommodityCode());
|
||||
existing.setDefaultHsn(category.getDefaultHsn());
|
||||
existing.setDefaultGst(category.getDefaultGst());
|
||||
existing.setHuidRequired(category.getHuidRequired());
|
||||
existing.setDefaultMakingCharge(category.getDefaultMakingCharge());
|
||||
existing.setMakingChargeType(category.getMakingChargeType());
|
||||
existing.setCalculationMethod(category.getCalculationMethod());
|
||||
existing.setBaseUnit(category.getBaseUnit());
|
||||
existing.setIsActive(category.getIsActive());
|
||||
existing.setSortOrder(category.getSortOrder());
|
||||
|
||||
@@ -24,6 +24,17 @@ public class PurchaseOrderController {
|
||||
return Mono.just(ResponseEntity.ok(purchaseOrderService.getPurchaseOrders(userId)));
|
||||
}
|
||||
|
||||
@GetMapping("/test/{id}")
|
||||
public Mono<ResponseEntity<PurchaseOrder>> testGet(@PathVariable Long id) {
|
||||
return purchaseOrderService.getPurchaseOrder(1L, id)
|
||||
.flatMap(po -> purchaseOrderService.getPurchaseOrderItems(id)
|
||||
.collectList()
|
||||
.map(items -> {
|
||||
po.setItems(items);
|
||||
return ResponseEntity.ok(po);
|
||||
}));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<ResponseEntity<PurchaseOrder>> getPurchaseOrder(@PathVariable Long id, Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
@@ -31,6 +42,7 @@ public class PurchaseOrderController {
|
||||
.flatMap(po -> purchaseOrderService.getPurchaseOrderItems(id)
|
||||
.collectList()
|
||||
.map(items -> {
|
||||
System.out.println("Fetched PO ID " + id + ", Items count: " + items.size());
|
||||
po.setItems(items);
|
||||
return ResponseEntity.ok(po);
|
||||
}))
|
||||
@@ -40,6 +52,8 @@ public class PurchaseOrderController {
|
||||
@PostMapping
|
||||
public Mono<ResponseEntity<PurchaseOrder>> createPurchaseOrder(Authentication authentication, @RequestBody PurchaseOrder po) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
System.out.println("---- CREATE PO RECEIVED ITEMS ----");
|
||||
System.out.println(po.getItems());
|
||||
return purchaseOrderService.createPurchaseOrder(userId, po, po.getItems())
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@@ -23,4 +23,8 @@ public class InventoryMovementItem {
|
||||
private BigDecimal quantity;
|
||||
private BigDecimal unitPrice;
|
||||
private LocalDateTime createdAt;
|
||||
private String huid;
|
||||
private BigDecimal weight;
|
||||
private String photoUrl;
|
||||
private BigDecimal purchaseAmount;
|
||||
}
|
||||
|
||||
@@ -28,11 +28,9 @@ public class Product {
|
||||
private String barcode;
|
||||
private String hsnCode;
|
||||
private String description;
|
||||
private BigDecimal purchasePrice;
|
||||
private BigDecimal sellingPrice;
|
||||
private BigDecimal gstRate;
|
||||
private String dimensions;
|
||||
private BigDecimal weight;
|
||||
private String color;
|
||||
private String size;
|
||||
private String priceCalcRule;
|
||||
@@ -40,9 +38,6 @@ public class Product {
|
||||
private Double makingCharges;
|
||||
private String makingChargesType;
|
||||
private Double wastagePercentage;
|
||||
private BigDecimal minStock;
|
||||
private BigDecimal reorderLevel;
|
||||
private Boolean trackInventory;
|
||||
private Boolean isActive;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -22,6 +23,7 @@ public class ProductCategory {
|
||||
private Long parentCategoryId;
|
||||
@Builder.Default
|
||||
private Boolean hasChild = false;
|
||||
private String commodityCode;
|
||||
private String defaultHsn;
|
||||
private java.math.BigDecimal defaultGst;
|
||||
@Builder.Default
|
||||
@@ -29,7 +31,6 @@ public class ProductCategory {
|
||||
private Double defaultMakingCharge;
|
||||
private String makingChargeType;
|
||||
|
||||
private String calculationMethod;
|
||||
private String baseUnit;
|
||||
|
||||
@Builder.Default
|
||||
|
||||
@@ -29,6 +29,9 @@ public class PurchaseOrder {
|
||||
private LocalDate dueDate;
|
||||
private BigDecimal subtotal;
|
||||
private BigDecimal taxTotal;
|
||||
private BigDecimal cgstTotal;
|
||||
private BigDecimal sgstTotal;
|
||||
private BigDecimal igstTotal;
|
||||
private BigDecimal discountTotal;
|
||||
private BigDecimal totalAmount;
|
||||
private String status; // DRAFT, RECEIVED, CANCELLED
|
||||
@@ -41,5 +44,16 @@ public class PurchaseOrder {
|
||||
private String vendorInvoiceUrl;
|
||||
|
||||
@Transient
|
||||
@com.fasterxml.jackson.annotation.JsonProperty(value = "items", access = com.fasterxml.jackson.annotation.JsonProperty.Access.READ_WRITE)
|
||||
private List<PurchaseOrderItem> items;
|
||||
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("items")
|
||||
public List<PurchaseOrderItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("items")
|
||||
public void setItems(List<PurchaseOrderItem> items) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,16 @@ public class PurchaseOrderItem {
|
||||
private BigDecimal quantity;
|
||||
private BigDecimal unitPrice;
|
||||
private BigDecimal taxRate;
|
||||
private BigDecimal cgstRate;
|
||||
private BigDecimal sgstRate;
|
||||
private BigDecimal igstRate;
|
||||
private BigDecimal discount;
|
||||
private BigDecimal total;
|
||||
private String description;
|
||||
private BigDecimal makingCharge;
|
||||
private BigDecimal otherCharges;
|
||||
private String sku;
|
||||
private String huid;
|
||||
private BigDecimal weight;
|
||||
private String photoUrl;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
public interface InventoryItemRepository extends ReactiveCrudRepository<InventoryItem, Long> {
|
||||
Flux<InventoryItem> findByUserId(Long userId);
|
||||
Flux<InventoryItem> findByUserIdAndProductId(Long userId, Long productId);
|
||||
Flux<InventoryItem> findByProductId(Long productId);
|
||||
Mono<InventoryItem> findByTagNumber(String tagNumber);
|
||||
Mono<InventoryItem> findByHuid(String huid);
|
||||
|
||||
@@ -25,7 +25,8 @@ public class AuthenticationManager implements ReactiveAuthenticationManager {
|
||||
if (jwtUtil.validateToken(authToken)) {
|
||||
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
|
||||
String email = claims.getSubject();
|
||||
Long userId = claims.get("userId", Long.class);
|
||||
Number userIdNumber = claims.get("userId", Number.class);
|
||||
Long userId = userIdNumber != null ? userIdNumber.longValue() : null;
|
||||
|
||||
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
|
||||
email,
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.kifi.api.entity.business.BusinessProfile;
|
||||
import com.kifi.api.entity.business.BusinessFeature;
|
||||
import com.kifi.api.repository.business.BusinessProfileRepository;
|
||||
import com.kifi.api.repository.business.BusinessFeatureRepository;
|
||||
import com.kifi.api.util.CryptoUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -15,9 +16,15 @@ import java.time.LocalDateTime;
|
||||
public class BusinessService {
|
||||
private final BusinessProfileRepository businessProfileRepository;
|
||||
private final BusinessFeatureRepository businessFeatureRepository;
|
||||
private final CryptoUtils cryptoUtils;
|
||||
|
||||
public Mono<BusinessProfile> getProfileByUserId(Long userId) {
|
||||
return businessProfileRepository.findByUserId(userId);
|
||||
return businessProfileRepository.findByUserId(userId)
|
||||
.map(profile -> {
|
||||
profile.setPanNumber(cryptoUtils.decrypt(profile.getPanNumber()));
|
||||
profile.setGstin(cryptoUtils.decrypt(profile.getGstin()));
|
||||
return profile;
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<BusinessFeature> getFeaturesByUserId(Long userId) {
|
||||
@@ -76,6 +83,8 @@ public class BusinessService {
|
||||
profile.setUserId(userId);
|
||||
profile.setCreatedAt(LocalDateTime.now());
|
||||
profile.setUpdatedAt(LocalDateTime.now());
|
||||
profile.setPanNumber(profile.getPanNumber());
|
||||
profile.setGstin(profile.getGstin());
|
||||
return businessProfileRepository.save(profile);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ public class InventoryItemService {
|
||||
return inventoryItemRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
public Flux<InventoryItem> getItemsByProductId(Long userId, Long productId) {
|
||||
return inventoryItemRepository.findByUserIdAndProductId(userId, productId);
|
||||
}
|
||||
|
||||
public Mono<InventoryItem> createItem(Long userId, InventoryItem item) {
|
||||
item.setUserId(userId);
|
||||
item.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
@@ -67,7 +67,6 @@ public class ProductService {
|
||||
product.setCreatedAt(LocalDateTime.now());
|
||||
product.setUpdatedAt(LocalDateTime.now());
|
||||
if (product.getIsActive() == null) product.setIsActive(true);
|
||||
if (product.getTrackInventory() == null) product.setTrackInventory(true);
|
||||
if (product.getPriceCalcRule() == null) product.setPriceCalcRule("MANUAL");
|
||||
if (product.getPurityFactor() == null) product.setPurityFactor(1.0);
|
||||
return productRepository.save(product);
|
||||
@@ -81,10 +80,9 @@ public class ProductService {
|
||||
existingProduct.setSku(updatedProduct.getSku());
|
||||
existingProduct.setCategoryId(updatedProduct.getCategoryId());
|
||||
existingProduct.setUomId(updatedProduct.getUomId());
|
||||
existingProduct.setPurchasePrice(updatedProduct.getPurchasePrice());
|
||||
existingProduct.setSellingPrice(updatedProduct.getSellingPrice());
|
||||
existingProduct.setGstRate(updatedProduct.getGstRate());
|
||||
existingProduct.setWeight(updatedProduct.getWeight());
|
||||
existingProduct.setHsnCode(updatedProduct.getHsnCode());
|
||||
existingProduct.setColor(updatedProduct.getColor());
|
||||
existingProduct.setSize(updatedProduct.getSize());
|
||||
existingProduct.setDimensions(updatedProduct.getDimensions());
|
||||
@@ -93,9 +91,6 @@ public class ProductService {
|
||||
existingProduct.setMakingCharges(updatedProduct.getMakingCharges());
|
||||
existingProduct.setMakingChargesType(updatedProduct.getMakingChargesType());
|
||||
existingProduct.setWastagePercentage(updatedProduct.getWastagePercentage());
|
||||
existingProduct.setMinStock(updatedProduct.getMinStock());
|
||||
existingProduct.setReorderLevel(updatedProduct.getReorderLevel());
|
||||
existingProduct.setTrackInventory(updatedProduct.getTrackInventory());
|
||||
existingProduct.setIsActive(updatedProduct.getIsActive());
|
||||
existingProduct.setUpdatedAt(LocalDateTime.now());
|
||||
return productRepository.save(existingProduct);
|
||||
|
||||
@@ -35,14 +35,27 @@ public class PurchaseOrderService {
|
||||
private final LedgerService ledgerService;
|
||||
private final TransactionService transactionService;
|
||||
private final VendorRepository vendorRepository;
|
||||
private final com.kifi.api.repository.inventory.InventoryBalanceRepository inventoryBalanceRepository;
|
||||
|
||||
public Flux<PurchaseOrder> getPurchaseOrders(Long userId) {
|
||||
return purchaseOrderRepository.findByUserId(userId)
|
||||
.flatMap(po -> purchaseOrderItemRepository.findByPoId(po.getId())
|
||||
.collectList()
|
||||
.map(items -> {
|
||||
po.setItems(items);
|
||||
return po;
|
||||
}))
|
||||
.sort((a, b) -> b.getIssueDate().compareTo(a.getIssueDate()));
|
||||
}
|
||||
|
||||
public Mono<PurchaseOrder> getPurchaseOrder(Long userId, Long id) {
|
||||
return purchaseOrderRepository.findByUserIdAndId(userId, id);
|
||||
return purchaseOrderRepository.findByUserIdAndId(userId, id)
|
||||
.flatMap(po -> purchaseOrderItemRepository.findByPoId(po.getId())
|
||||
.collectList()
|
||||
.map(items -> {
|
||||
po.setItems(items);
|
||||
return po;
|
||||
}));
|
||||
}
|
||||
|
||||
public Flux<PurchaseOrderItem> getPurchaseOrderItems(Long poId) {
|
||||
@@ -64,13 +77,21 @@ public class PurchaseOrderService {
|
||||
|
||||
return purchaseOrderRepository.save(po)
|
||||
.flatMap(savedPo -> {
|
||||
if (items == null || items.isEmpty()) {
|
||||
savedPo.setItems(List.of());
|
||||
return Mono.just(savedPo);
|
||||
}
|
||||
return Flux.fromIterable(items)
|
||||
.map(item -> {
|
||||
item.setPoId(savedPo.getId());
|
||||
return item;
|
||||
})
|
||||
.flatMap(purchaseOrderItemRepository::save)
|
||||
.then(Mono.just(savedPo));
|
||||
.collectList()
|
||||
.map(savedItems -> {
|
||||
savedPo.setItems(savedItems);
|
||||
return savedPo;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,6 +99,9 @@ public class PurchaseOrderService {
|
||||
public Mono<PurchaseOrder> updatePurchaseOrder(Long userId, Long id, PurchaseOrder po, List<PurchaseOrderItem> items) {
|
||||
return purchaseOrderRepository.findByUserIdAndId(userId, id)
|
||||
.flatMap(existingPo -> {
|
||||
if (items == null) {
|
||||
return Mono.error(new IllegalArgumentException("Items cannot be null"));
|
||||
}
|
||||
existingPo.setVendorId(po.getVendorId());
|
||||
existingPo.setPoNumber(po.getPoNumber());
|
||||
existingPo.setIssueDate(po.getIssueDate());
|
||||
@@ -94,11 +118,16 @@ public class PurchaseOrderService {
|
||||
.flatMap(savedPo -> purchaseOrderItemRepository.deleteByPoId(id)
|
||||
.thenMany(Flux.fromIterable(items)
|
||||
.map(item -> {
|
||||
item.setId(null);
|
||||
item.setPoId(id);
|
||||
return item;
|
||||
})
|
||||
.flatMap(purchaseOrderItemRepository::save))
|
||||
.then(Mono.just(savedPo)));
|
||||
.collectList()
|
||||
.map(savedItems -> {
|
||||
savedPo.setItems(savedItems);
|
||||
return savedPo;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,6 +146,7 @@ public class PurchaseOrderService {
|
||||
.flatMap(savedPo -> purchaseOrderItemRepository.deleteByPoId(id)
|
||||
.thenMany(Flux.fromIterable(receivedItems)
|
||||
.map(item -> {
|
||||
item.setId(null);
|
||||
item.setPoId(id);
|
||||
return item;
|
||||
})
|
||||
@@ -126,21 +156,34 @@ public class PurchaseOrderService {
|
||||
// Update inventory for received items
|
||||
return Flux.fromIterable(savedItems)
|
||||
.flatMap(item -> {
|
||||
if (item.getProductId() != null && item.getQuantity() != null && item.getQuantity().compareTo(BigDecimal.ZERO) > 0) {
|
||||
int quantity = item.getQuantity().intValue();
|
||||
return Flux.range(0, quantity)
|
||||
.flatMap(i -> {
|
||||
InventoryItem invItem = InventoryItem.builder()
|
||||
.userId(userId)
|
||||
.productId(item.getProductId())
|
||||
.vendorId(savedPo.getVendorId())
|
||||
.purchaseRef(savedPo.getPoNumber())
|
||||
.purchaseCost(item.getUnitPrice())
|
||||
.makingCharges(item.getMakingCharge())
|
||||
.sku(item.getSku() != null ? item.getSku() + "-" + (i + 1) : null)
|
||||
.build();
|
||||
return inventoryItemService.createItem(userId, invItem);
|
||||
})
|
||||
if (item.getProductId() != null) {
|
||||
InventoryItem invItem = InventoryItem.builder()
|
||||
.userId(userId)
|
||||
.productId(item.getProductId())
|
||||
.vendorId(savedPo.getVendorId())
|
||||
.purchaseRef(savedPo.getPoNumber())
|
||||
.purchaseCost(item.getUnitPrice())
|
||||
.makingCharges(item.getMakingCharge())
|
||||
.sku(item.getSku())
|
||||
.huid(item.getHuid())
|
||||
.grossWeight(item.getWeight())
|
||||
.netWeight(item.getWeight())
|
||||
.build();
|
||||
return inventoryItemService.createItem(userId, invItem)
|
||||
.then(inventoryBalanceRepository.findByProductIdAndLocationId(item.getProductId(), 2L) // Default location 2L
|
||||
.flatMap(balance -> {
|
||||
balance.setQuantity(balance.getQuantity().add(item.getWeight() != null ? item.getWeight() : BigDecimal.ONE));
|
||||
balance.setLastUpdated(LocalDateTime.now());
|
||||
return inventoryBalanceRepository.save(balance);
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
com.kifi.api.entity.inventory.InventoryBalance newBalance = new com.kifi.api.entity.inventory.InventoryBalance();
|
||||
newBalance.setProductId(item.getProductId());
|
||||
newBalance.setLocationId(2L);
|
||||
newBalance.setQuantity(item.getWeight() != null ? item.getWeight() : BigDecimal.ONE);
|
||||
newBalance.setLastUpdated(LocalDateTime.now());
|
||||
return inventoryBalanceRepository.save(newBalance);
|
||||
})))
|
||||
.then(Mono.just(item));
|
||||
}
|
||||
return Mono.just(item);
|
||||
@@ -162,7 +205,11 @@ public class PurchaseOrderService {
|
||||
.build();
|
||||
return transactionService.addTransaction(userId, tx);
|
||||
})
|
||||
.thenReturn(savedPo);
|
||||
.thenReturn(savedPo)
|
||||
.map(poResult -> {
|
||||
poResult.setItems(savedItems);
|
||||
return poResult;
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,8 +202,7 @@ CREATE TABLE IF NOT EXISTS product_categories (
|
||||
huid_required BOOLEAN DEFAULT FALSE,
|
||||
default_making_charge DECIMAL(15, 2),
|
||||
making_charge_type VARCHAR(50),
|
||||
calculation_method VARCHAR(50) DEFAULT 'UNIT',
|
||||
base_unit VARCHAR(50) DEFAULT 'pcs',
|
||||
base_unit VARCHAR(20) DEFAULT 'pcs',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
|
||||
26
kifi-api/src/test/java/RunSchema.java
Normal file
26
kifi-api/src/test/java/RunSchema.java
Normal file
@@ -0,0 +1,26 @@
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.Statement;
|
||||
|
||||
public class RunSchema {
|
||||
public static void main(String[] args) {
|
||||
String url = "jdbc:postgresql://localhost:5432/kifi_db";
|
||||
String user = "postgres";
|
||||
String password = "postgres";
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url, user, password);
|
||||
Statement stmt = conn.createStatement()) {
|
||||
|
||||
stmt.execute("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS cgst_total NUMERIC(10,2)");
|
||||
stmt.execute("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS sgst_total NUMERIC(10,2)");
|
||||
stmt.execute("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS igst_total NUMERIC(10,2)");
|
||||
stmt.execute("ALTER TABLE purchase_order_items ADD COLUMN IF NOT EXISTS cgst_rate NUMERIC(10,2)");
|
||||
stmt.execute("ALTER TABLE purchase_order_items ADD COLUMN IF NOT EXISTS sgst_rate NUMERIC(10,2)");
|
||||
stmt.execute("ALTER TABLE purchase_order_items ADD COLUMN IF NOT EXISTS igst_rate NUMERIC(10,2)");
|
||||
|
||||
System.out.println("Schema updated successfully!");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.kifi.api;
|
||||
import com.kifi.api.entity.vendor.PurchaseOrder;
|
||||
import com.kifi.api.entity.vendor.PurchaseOrderItem;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
public class PurchaseOrderJacksonTest {
|
||||
@Test
|
||||
public void testJackson() throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String json = "{\"poNumber\": \"TEST-123\", \"items\": [{\"productId\": 1}]}";
|
||||
PurchaseOrder po = mapper.readValue(json, PurchaseOrder.class);
|
||||
System.out.println("Parsed items: " + po.getItems());
|
||||
if (po.getItems() == null || po.getItems().isEmpty()) {
|
||||
throw new RuntimeException("ITEMS NOT PARSED");
|
||||
}
|
||||
}
|
||||
}
|
||||
18
kifi-api/src/test/java/com/kifi/api/TestJackson.java
Normal file
18
kifi-api/src/test/java/com/kifi/api/TestJackson.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.kifi.api;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kifi.api.entity.vendor.PurchaseOrder;
|
||||
import com.kifi.api.entity.vendor.PurchaseOrderItem;
|
||||
import java.util.List;
|
||||
|
||||
public class TestJackson {
|
||||
public static void main(String[] args) throws Exception {
|
||||
PurchaseOrder po = new PurchaseOrder();
|
||||
po.setId(5L);
|
||||
PurchaseOrderItem item = new PurchaseOrderItem();
|
||||
item.setId(6L);
|
||||
po.setItems(List.of(item));
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
System.out.println(mapper.writeValueAsString(po));
|
||||
}
|
||||
}
|
||||
19
kifi-api/src/test/java/com/kifi/api/TestService.java
Normal file
19
kifi-api/src/test/java/com/kifi/api/TestService.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.kifi.api;
|
||||
|
||||
import com.kifi.api.entity.vendor.PurchaseOrder;
|
||||
import com.kifi.api.service.vendor.PurchaseOrderService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
public class TestService {
|
||||
@Autowired
|
||||
private PurchaseOrderService service;
|
||||
|
||||
@Test
|
||||
public void testGetPO() {
|
||||
PurchaseOrder po = service.getPurchaseOrder(6L, 5L).block();
|
||||
System.out.println("ITEMS COUNT: " + po.getItems().size());
|
||||
}
|
||||
}
|
||||
15
kifi-api/src/test/java/com/kifi/api/TestUserMapping.java
Normal file
15
kifi-api/src/test/java/com/kifi/api/TestUserMapping.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.kifi.api;
|
||||
import com.kifi.api.repository.UserRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@SpringBootTest
|
||||
public class TestUserMapping {
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
@Test
|
||||
public void test() {
|
||||
var user = userRepository.findById(6L).block();
|
||||
System.out.println("STATUS_IS: " + user.getSetupStatus());
|
||||
}
|
||||
}
|
||||
11
kifi-api/test_flux.java
Normal file
11
kifi-api/test_flux.java
Normal file
@@ -0,0 +1,11 @@
|
||||
import reactor.core.publisher.Flux;
|
||||
public class test_flux {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
Flux.fromIterable(null).subscribe();
|
||||
System.out.println("Did not throw");
|
||||
} catch(Exception e) {
|
||||
System.out.println("Threw: " + e.getClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
kifi-api/test_flux2.class
Normal file
BIN
kifi-api/test_flux2.class
Normal file
Binary file not shown.
10
kifi-api/test_flux2.java
Normal file
10
kifi-api/test_flux2.java
Normal file
@@ -0,0 +1,10 @@
|
||||
public class test_flux2 {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
reactor.core.publisher.Flux.fromIterable(null).subscribe();
|
||||
System.out.println("Did not throw");
|
||||
} catch(Exception e) {
|
||||
System.out.println("Threw: " + e.getClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,22 @@ class DioClient {
|
||||
static final DioClient _instance = DioClient._internal();
|
||||
final Dio dio;
|
||||
final FlutterSecureStorage storage;
|
||||
String? _cachedToken;
|
||||
|
||||
factory DioClient() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
Future<void> saveToken(String token) async {
|
||||
_cachedToken = token;
|
||||
await storage.write(key: 'jwt_token', value: token);
|
||||
}
|
||||
|
||||
Future<void> clearToken() async {
|
||||
_cachedToken = null;
|
||||
await storage.delete(key: 'jwt_token');
|
||||
}
|
||||
|
||||
DioClient._internal()
|
||||
: dio = Dio(BaseOptions(
|
||||
baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
|
||||
@@ -23,18 +34,26 @@ class DioClient {
|
||||
storage = const FlutterSecureStorage() {
|
||||
dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
final token = await storage.read(key: 'jwt_token');
|
||||
String? token = _cachedToken;
|
||||
if (token == null) {
|
||||
token = await storage.read(key: 'jwt_token');
|
||||
_cachedToken = token;
|
||||
}
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
print('DioClient sending token: ${token.substring(0, 10)}...');
|
||||
} else {
|
||||
print('DioClient sending request WITHOUT token!');
|
||||
}
|
||||
return handler.next(options);
|
||||
},
|
||||
onError: (DioException error, handler) async {
|
||||
if (error.response?.statusCode == 401) {
|
||||
// Token is invalid or expired
|
||||
_cachedToken = null;
|
||||
await storage.delete(key: 'jwt_token');
|
||||
if (navigatorKey.currentContext != null) {
|
||||
Navigator.of(navigatorKey.currentContext!).pushAndRemoveUntil(
|
||||
if (navigatorKey.currentState != null) {
|
||||
navigatorKey.currentState!.pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const AuthScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
|
||||
@@ -38,15 +38,15 @@ class AppTheme {
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: primaryColor, width: 2),
|
||||
),
|
||||
hintStyle: const TextStyle(color: textSecondaryColor),
|
||||
@@ -94,15 +94,15 @@ class AppTheme {
|
||||
fillColor: darkSurface,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: primaryColor, width: 2),
|
||||
),
|
||||
hintStyle: const TextStyle(color: darkTextSecondary),
|
||||
|
||||
@@ -12,6 +12,7 @@ class PremiumTextField extends StatelessWidget {
|
||||
final TextCapitalization textCapitalization;
|
||||
final int maxLines;
|
||||
final bool obscureText;
|
||||
final bool readOnly;
|
||||
final String? Function(String?)? validator;
|
||||
final void Function(String)? onChanged;
|
||||
final void Function(String?)? onSaved;
|
||||
@@ -30,6 +31,7 @@ class PremiumTextField extends StatelessWidget {
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.maxLines = 1,
|
||||
this.obscureText = false,
|
||||
this.readOnly = false,
|
||||
this.validator,
|
||||
this.onChanged,
|
||||
this.onSaved,
|
||||
@@ -49,20 +51,12 @@ class PremiumTextField extends StatelessWidget {
|
||||
suffixIcon: suffixIcon,
|
||||
suffixText: suffixText,
|
||||
suffixStyle: TextStyle(color: darkTheme ? Colors.white70 : Colors.grey),
|
||||
filled: true,
|
||||
fillColor: darkTheme ? Colors.black26 : Colors.grey[100],
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: darkTheme ? Colors.white : Theme.of(context).colorScheme.primary, width: 2)
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
),
|
||||
keyboardType: keyboardType,
|
||||
textCapitalization: textCapitalization,
|
||||
maxLines: maxLines,
|
||||
obscureText: obscureText,
|
||||
readOnly: readOnly,
|
||||
onChanged: onChanged,
|
||||
onSaved: onSaved,
|
||||
validator: validator,
|
||||
|
||||
@@ -10,6 +10,8 @@ class SmartSearchDropdown<T> extends StatefulWidget {
|
||||
final bool Function(T, String)? filterFn;
|
||||
final Color? fillColor;
|
||||
final String? labelText;
|
||||
final String? emptyActionText;
|
||||
final VoidCallback? onEmptyActionPressed;
|
||||
|
||||
const SmartSearchDropdown({
|
||||
super.key,
|
||||
@@ -22,6 +24,8 @@ class SmartSearchDropdown<T> extends StatefulWidget {
|
||||
this.filterFn,
|
||||
this.fillColor,
|
||||
this.labelText,
|
||||
this.emptyActionText,
|
||||
this.onEmptyActionPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -135,9 +139,24 @@ class _SmartSearchDropdownState<T> extends State<SmartSearchDropdown<T>> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (displayItems.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text('No matches found'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('No matches found'),
|
||||
if (widget.emptyActionText != null && widget.onEmptyActionPressed != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
_focusNode.unfocus();
|
||||
widget.onEmptyActionPressed!();
|
||||
},
|
||||
child: Text(widget.emptyActionText!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
|
||||
@@ -15,10 +15,13 @@ class AuthRepository {
|
||||
Future<void> signup(String email, String password) async {
|
||||
try {
|
||||
await _ensureCryptoReady();
|
||||
await dio.post('/auth/signup', data: {
|
||||
'email': _crypto.encrypt(email),
|
||||
'password': _crypto.encrypt(password),
|
||||
});
|
||||
await dio.post(
|
||||
'/auth/signup',
|
||||
data: {
|
||||
'email': _crypto.encrypt(email),
|
||||
'password': _crypto.encrypt(password),
|
||||
},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
final data = e.response?.data;
|
||||
final errorMsg = data is Map ? data['error'] : data;
|
||||
@@ -30,7 +33,9 @@ class AuthRepository {
|
||||
try {
|
||||
return await _attemptLogin(email, password);
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 500 || e.response?.statusCode == 400 || e.response?.statusCode == 401) {
|
||||
if (e.response?.statusCode == 500 ||
|
||||
e.response?.statusCode == 400 ||
|
||||
e.response?.statusCode == 401) {
|
||||
_crypto.clearKey();
|
||||
try {
|
||||
return await _attemptLogin(email, password);
|
||||
@@ -46,21 +51,27 @@ class AuthRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _attemptLogin(String email, String password) async {
|
||||
Future<Map<String, dynamic>> _attemptLogin(
|
||||
String email,
|
||||
String password,
|
||||
) async {
|
||||
await _ensureCryptoReady();
|
||||
final response = await dio.post('/auth/login', data: {
|
||||
'email': _crypto.encrypt(email),
|
||||
'password': _crypto.encrypt(password),
|
||||
});
|
||||
final response = await dio.post(
|
||||
'/auth/login',
|
||||
data: {
|
||||
'email': _crypto.encrypt(email),
|
||||
'password': _crypto.encrypt(password),
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> verifyOtp(String email, String otp) async {
|
||||
try {
|
||||
final response = await dio.post('/auth/verify-otp', data: {
|
||||
'email': email,
|
||||
'otp': otp,
|
||||
});
|
||||
final response = await dio.post(
|
||||
'/auth/verify-otp',
|
||||
data: {'email': email, 'otp': otp},
|
||||
);
|
||||
return response.data;
|
||||
} on DioException catch (e) {
|
||||
final data = e.response?.data;
|
||||
@@ -71,9 +82,7 @@ class AuthRepository {
|
||||
|
||||
Future<void> forgotPassword(String email) async {
|
||||
try {
|
||||
await dio.post('/auth/forgot-password', data: {
|
||||
'email': email,
|
||||
});
|
||||
await dio.post('/auth/forgot-password', data: {'email': email});
|
||||
} on DioException catch (e) {
|
||||
final data = e.response?.data;
|
||||
final errorMsg = data is Map ? data['error'] : data;
|
||||
@@ -81,14 +90,21 @@ class AuthRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetPassword(String email, String otp, String newPassword) async {
|
||||
Future<void> resetPassword(
|
||||
String email,
|
||||
String otp,
|
||||
String newPassword,
|
||||
) async {
|
||||
try {
|
||||
await _ensureCryptoReady();
|
||||
await dio.post('/auth/reset-password', data: {
|
||||
'email': email,
|
||||
'otp': otp,
|
||||
'newPassword': _crypto.encrypt(newPassword),
|
||||
});
|
||||
await dio.post(
|
||||
'/auth/reset-password',
|
||||
data: {
|
||||
'email': email,
|
||||
'otp': otp,
|
||||
'newPassword': _crypto.encrypt(newPassword),
|
||||
},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
final data = e.response?.data;
|
||||
final errorMsg = data is Map ? data['error'] : data;
|
||||
|
||||
@@ -16,9 +16,10 @@ class AuthScreen extends ConsumerStatefulWidget {
|
||||
ConsumerState<AuthScreen> createState() => _AuthScreenState();
|
||||
}
|
||||
|
||||
class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProviderStateMixin {
|
||||
class _AuthScreenState extends ConsumerState<AuthScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool isLogin = true;
|
||||
final emailController = TextEditingController(text: 'maddy23285@gmail.com');
|
||||
final emailController = TextEditingController(text: 'nmadaswamy@gmail.com');
|
||||
final passwordController = TextEditingController(text: 'Algorithm@123');
|
||||
|
||||
void toggleMode(bool login) {
|
||||
@@ -35,25 +36,49 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
|
||||
if (email.isEmpty || password.isEmpty) return;
|
||||
|
||||
if (isLogin) {
|
||||
final success = await ref.read(authControllerProvider.notifier).login(email, password);
|
||||
final success = await ref
|
||||
.read(authControllerProvider.notifier)
|
||||
.login(email, password);
|
||||
if (success && mounted) {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 500)); // Allow secure storage to settle
|
||||
final dio = DioClient().dio;
|
||||
final res = await dio.get('/account/setup/status');
|
||||
print('Setup status response: ${res.data}');
|
||||
final status = res.data['status'];
|
||||
print('Status is: $status');
|
||||
if (status == 'COMPLETED' && mounted) {
|
||||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardScreen()));
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const DashboardScreen()),
|
||||
);
|
||||
} else if (mounted) {
|
||||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const SetupWizardScreen()));
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const SetupWizardScreen()),
|
||||
);
|
||||
}
|
||||
} catch (e, st) {
|
||||
print('Error getting setup status: $e\n$st');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error fetching setup status: $e')));
|
||||
// Fallback to Dashboard if login was successful but status check failed (e.g. network glitch)
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const DashboardScreen()),
|
||||
);
|
||||
}
|
||||
} catch(e) {
|
||||
if (mounted) Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const SetupWizardScreen()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final success = await ref.read(authControllerProvider.notifier).signup(email, password);
|
||||
final success = await ref
|
||||
.read(authControllerProvider.notifier)
|
||||
.signup(email, password);
|
||||
if (success && mounted) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => OtpScreen(email: email)));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => OtpScreen(email: email)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,9 +110,9 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen<AsyncValue<void>>(authControllerProvider, (previous, next) {
|
||||
if (next.hasError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${next.error}')),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('${next.error}')));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,23 +132,36 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
width: 80, height: 80,
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(child: Image.asset('assets/logo.png', width: 80, height: 80, fit: BoxFit.contain)),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/logo.png',
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Welcome to Kifi',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Manage your inventory and expenses securely.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
@@ -143,11 +181,31 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isLogin ? Colors.white : Colors.transparent,
|
||||
color: isLogin
|
||||
? Colors.white
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: isLogin ? [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2))] : [],
|
||||
boxShadow: isLogin
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Login',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isLogin
|
||||
? Colors.black87
|
||||
: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(child: Text('Login', style: TextStyle(fontWeight: FontWeight.w600, color: isLogin ? Colors.black87 : Colors.grey.shade600))),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -159,11 +217,31 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: !isLogin ? Colors.white : Colors.transparent,
|
||||
color: !isLogin
|
||||
? Colors.white
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: !isLogin ? [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2))] : [],
|
||||
boxShadow: !isLogin
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Sign Up',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: !isLogin
|
||||
? Colors.black87
|
||||
: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(child: Text('Sign Up', style: TextStyle(fontWeight: FontWeight.w600, color: !isLogin ? Colors.black87 : Colors.grey.shade600))),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -181,18 +259,30 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
|
||||
TextField(
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black87),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: _buildInputDecoration(isLogin ? 'Email or Username' : 'Email Address', LucideIcons.mail),
|
||||
decoration: _buildInputDecoration(
|
||||
isLogin ? 'Email or Username' : 'Email Address',
|
||||
LucideIcons.mail,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black87),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => submit(),
|
||||
decoration: _buildInputDecoration('Password', LucideIcons.lock),
|
||||
decoration: _buildInputDecoration(
|
||||
'Password',
|
||||
LucideIcons.lock,
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -202,14 +292,32 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ForgotPasswordScreen())),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).primaryColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
const ForgotPasswordScreen(),
|
||||
),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(
|
||||
context,
|
||||
).primaryColor,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: const Text(
|
||||
'Forgot Password?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
child: const Text('Forgot Password?', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -227,12 +335,28 @@ class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProvid
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: Text(isLogin ? 'Log In' : 'Create Account', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
isLogin ? 'Log In' : 'Create Account',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -8,7 +8,8 @@ class ForgotPasswordScreen extends ConsumerStatefulWidget {
|
||||
const ForgotPasswordScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
|
||||
ConsumerState<ForgotPasswordScreen> createState() =>
|
||||
_ForgotPasswordScreenState();
|
||||
}
|
||||
|
||||
class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
@@ -34,7 +35,9 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final success = await ref.read(authControllerProvider.notifier).forgotPassword(email);
|
||||
final success = await ref
|
||||
.read(authControllerProvider.notifier)
|
||||
.forgotPassword(email);
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
@@ -44,7 +47,10 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
MaterialPageRoute(builder: (_) => ResetPasswordScreen(email: email)),
|
||||
);
|
||||
} else {
|
||||
setState(() => _errorMessage = 'Failed to send reset code. Please check your email.');
|
||||
setState(
|
||||
() => _errorMessage =
|
||||
'Failed to send reset code. Please check your email.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,24 +105,38 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
width: 80, height: 80,
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(child: Icon(LucideIcons.keyRound, size: 40, color: Theme.of(context).primaryColor)),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
LucideIcons.keyRound,
|
||||
size: 40,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Forgot Password?',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
style: Theme.of(context).textTheme.headlineMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Enter your email address and we\'ll send you\na verification code to reset your password.',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600, height: 1.5),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
TextField(
|
||||
@@ -125,7 +145,10 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _sendOtp(),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
decoration: _buildInputDecoration('Email Address', LucideIcons.mail),
|
||||
decoration: _buildInputDecoration(
|
||||
'Email Address',
|
||||
LucideIcons.mail,
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -135,17 +158,35 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
border: Border.all(
|
||||
color: Colors.red.shade200,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.alertCircle, color: Colors.red.shade400, size: 20),
|
||||
Icon(
|
||||
LucideIcons.alertCircle,
|
||||
color: Colors.red.shade400,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(_errorMessage!, style: TextStyle(color: Colors.red.shade700, fontSize: 14, fontWeight: FontWeight.w500))),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -161,12 +202,28 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Text('Send Reset Code', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Send Reset Code',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -19,10 +19,15 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
||||
final otp = otpController.text.trim();
|
||||
if (otp.isEmpty) return;
|
||||
|
||||
final success = await ref.read(authControllerProvider.notifier).verifyOtp(widget.email, otp);
|
||||
final success = await ref
|
||||
.read(authControllerProvider.notifier)
|
||||
.verifyOtp(widget.email, otp);
|
||||
if (success && mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context, MaterialPageRoute(builder: (_) => const DashboardScreen()), (route) => false);
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const DashboardScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +35,9 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen<AsyncValue<void>>(authControllerProvider, (previous, next) {
|
||||
if (next.hasError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${next.error}')),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('${next.error}')));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -65,23 +70,37 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
width: 80, height: 80,
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(child: Icon(LucideIcons.mailCheck, size: 40, color: Theme.of(context).primaryColor)),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
LucideIcons.mailCheck,
|
||||
size: 40,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Verify Email',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
style: Theme.of(context).textTheme.headlineMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Enter the 6-digit OTP sent to\n${widget.email}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600, height: 1.5),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
@@ -90,17 +109,39 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
maxLength: 6,
|
||||
style: const TextStyle(fontSize: 24, letterSpacing: 16, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
letterSpacing: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: '000000',
|
||||
hintStyle: TextStyle(letterSpacing: 16, color: Colors.grey.shade300),
|
||||
hintStyle: TextStyle(
|
||||
letterSpacing: 16,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 20,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
@@ -112,12 +153,28 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: const Text('Verify Account', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Verify Account',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -23,7 +23,8 @@ class ProfileScreen extends ConsumerStatefulWidget {
|
||||
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
|
||||
}
|
||||
|
||||
class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTickerProviderStateMixin {
|
||||
class _ProfileScreenState extends ConsumerState<ProfileScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool _isExporting = false;
|
||||
String? _profileType;
|
||||
String _userName = 'Loading...';
|
||||
@@ -34,8 +35,14 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animController = AnimationController(vsync: this, duration: const Duration(milliseconds: 800));
|
||||
_fadeAnim = CurvedAnimation(parent: _animController, curve: Curves.easeOutCubic);
|
||||
_animController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
);
|
||||
_fadeAnim = CurvedAnimation(
|
||||
parent: _animController,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
_animController.forward();
|
||||
_fetchProfileType();
|
||||
}
|
||||
@@ -68,7 +75,8 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
}
|
||||
|
||||
String _getInitials(String name) {
|
||||
if (name.isEmpty || name == 'Kifi User' || name == 'Loading...') return 'KU';
|
||||
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();
|
||||
@@ -76,11 +84,9 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
return name.substring(0, name.length >= 2 ? 2 : 1).toUpperCase();
|
||||
}
|
||||
|
||||
Future<void> _logout(BuildContext context) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.delete(key: 'jwt_token');
|
||||
|
||||
if (context.mounted) {
|
||||
Future<void> _logout() async {
|
||||
await DioClient().clearToken();
|
||||
if (mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const AuthScreen()),
|
||||
@@ -97,9 +103,14 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
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.');
|
||||
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);
|
||||
}
|
||||
@@ -115,14 +126,19 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
title: const Text('Profile', style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2)),
|
||||
title: const Text(
|
||||
'Profile',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
flexibleSpace: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5)),
|
||||
child: Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -130,9 +146,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
children: [
|
||||
// Background Decorative Elements
|
||||
Positioned(
|
||||
top: -50, right: -50,
|
||||
top: -50,
|
||||
right: -50,
|
||||
child: Container(
|
||||
width: 200, height: 200,
|
||||
width: 200,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: primaryColor.withOpacity(isDark ? 0.2 : 0.1),
|
||||
@@ -140,9 +158,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -100, left: -50,
|
||||
bottom: -100,
|
||||
left: -50,
|
||||
child: Container(
|
||||
width: 300, height: 300,
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.purple.withOpacity(isDark ? 0.15 : 0.08),
|
||||
@@ -158,32 +178,45 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 24.0),
|
||||
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,
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
primaryColor.withOpacity(0.8),
|
||||
primaryColor.withOpacity(0.5)
|
||||
primaryColor.withOpacity(0.5),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(color: primaryColor.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10)),
|
||||
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),
|
||||
style: const TextStyle(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -192,14 +225,21 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
_userName,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (_userEmail.isNotEmpty && _userEmail != 'Loading...') ...[
|
||||
if (_userEmail.isNotEmpty &&
|
||||
_userEmail != 'Loading...') ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_userEmail,
|
||||
style: TextStyle(color: isDark ? Colors.grey.shade400 : Colors.grey.shade600, fontSize: 16),
|
||||
style: TextStyle(
|
||||
color: isDark
|
||||
? Colors.grey.shade400
|
||||
: Colors.grey.shade600,
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -212,9 +252,16 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor.withOpacity(isDark ? 0.3 : 0.6),
|
||||
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),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(
|
||||
isDark ? 0.05 : 0.2,
|
||||
),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -224,8 +271,18 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
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),
|
||||
? 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),
|
||||
@@ -236,31 +293,56 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
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)),
|
||||
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);
|
||||
},
|
||||
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;
|
||||
}),
|
||||
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);
|
||||
final isBusinessMode = ref.watch(
|
||||
businessModeProvider,
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
if (_profileType == 'BUSINESS') ...[
|
||||
@@ -271,7 +353,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
title: 'Business Mode',
|
||||
subtitle: 'Inventory and Sales Hub',
|
||||
value: isBusinessMode,
|
||||
onChanged: (val) => ref.read(businessModeProvider.notifier).toggleMode(),
|
||||
onChanged: (val) => ref
|
||||
.read(
|
||||
businessModeProvider.notifier,
|
||||
)
|
||||
.toggleMode(),
|
||||
),
|
||||
if (isBusinessMode) ...[
|
||||
_buildDivider(context, isDark),
|
||||
@@ -280,16 +366,27 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
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())),
|
||||
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(
|
||||
@@ -297,7 +394,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
icon: LucideIcons.helpCircle,
|
||||
iconColor: Colors.green,
|
||||
title: 'Help & Support',
|
||||
trailing: const Icon(LucideIcons.chevronRight, size: 20, color: Colors.grey),
|
||||
trailing: const Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 20,
|
||||
color: Colors.grey,
|
||||
),
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
@@ -316,20 +417,33 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
colors: [Colors.red.shade400, Colors.red.shade600],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.red.withOpacity(0.3), blurRadius: 15, offset: const Offset(0, 5)),
|
||||
BoxShadow(
|
||||
color: Colors.red.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _logout(context),
|
||||
onPressed: () => _logout(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
shadowColor: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
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)),
|
||||
label: const Text(
|
||||
'Log Out',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
@@ -364,8 +478,19 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTicker
|
||||
),
|
||||
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,
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -9,7 +9,8 @@ class ResetPasswordScreen extends ConsumerStatefulWidget {
|
||||
const ResetPasswordScreen({super.key, required this.email});
|
||||
|
||||
@override
|
||||
ConsumerState<ResetPasswordScreen> createState() => _ResetPasswordScreenState();
|
||||
ConsumerState<ResetPasswordScreen> createState() =>
|
||||
_ResetPasswordScreenState();
|
||||
}
|
||||
|
||||
class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
|
||||
@@ -52,7 +53,9 @@ class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final success = await ref.read(authControllerProvider.notifier).resetPassword(widget.email, otp, newPassword);
|
||||
final success = await ref
|
||||
.read(authControllerProvider.notifier)
|
||||
.resetPassword(widget.email, otp, newPassword);
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
@@ -69,15 +72,24 @@ class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
|
||||
(route) => false,
|
||||
);
|
||||
} else {
|
||||
setState(() => _errorMessage = 'Invalid OTP or reset failed. Please try again.');
|
||||
setState(
|
||||
() =>
|
||||
_errorMessage = 'Invalid OTP or reset failed. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InputDecoration _buildInputDecoration(String hint, {IconData? prefixIcon, Widget? suffixIcon}) {
|
||||
InputDecoration _buildInputDecoration(
|
||||
String hint, {
|
||||
IconData? prefixIcon,
|
||||
Widget? suffixIcon,
|
||||
}) {
|
||||
return InputDecoration(
|
||||
hintText: hint,
|
||||
prefixIcon: prefixIcon != null ? Icon(prefixIcon, color: Colors.grey.shade500) : null,
|
||||
prefixIcon: prefixIcon != null
|
||||
? Icon(prefixIcon, color: Colors.grey.shade500)
|
||||
: null,
|
||||
suffixIcon: suffixIcon,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
@@ -125,24 +137,38 @@ class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
width: 80, height: 80,
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(child: Icon(LucideIcons.shieldCheck, size: 40, color: Theme.of(context).primaryColor)),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
LucideIcons.shieldCheck,
|
||||
size: 40,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Reset Password',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
style: Theme.of(context).textTheme.headlineMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'We\'ve sent a 6-digit code to\n${widget.email}',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600, height: 1.5),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
@@ -152,18 +178,40 @@ class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
maxLength: 6,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 24, letterSpacing: 16),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 24,
|
||||
letterSpacing: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
decoration: InputDecoration(
|
||||
hintText: '000000',
|
||||
hintStyle: TextStyle(color: Colors.grey.shade300, letterSpacing: 16),
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey.shade300,
|
||||
letterSpacing: 16,
|
||||
),
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 20,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -178,8 +226,15 @@ class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
|
||||
'New Password',
|
||||
prefixIcon: LucideIcons.lock,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_obscurePassword ? LucideIcons.eyeOff : LucideIcons.eye, color: Colors.grey.shade500),
|
||||
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? LucideIcons.eyeOff
|
||||
: LucideIcons.eye,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
onPressed: () => setState(
|
||||
() => _obscurePassword = !_obscurePassword,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -196,8 +251,15 @@ class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
|
||||
'Confirm Password',
|
||||
prefixIcon: LucideIcons.lock,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_obscureConfirm ? LucideIcons.eyeOff : LucideIcons.eye, color: Colors.grey.shade500),
|
||||
onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
|
||||
icon: Icon(
|
||||
_obscureConfirm
|
||||
? LucideIcons.eyeOff
|
||||
: LucideIcons.eye,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
onPressed: () => setState(
|
||||
() => _obscureConfirm = !_obscureConfirm,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -210,17 +272,35 @@ class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
border: Border.all(
|
||||
color: Colors.red.shade200,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.alertCircle, color: Colors.red.shade400, size: 20),
|
||||
Icon(
|
||||
LucideIcons.alertCircle,
|
||||
color: Colors.red.shade400,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(_errorMessage!, style: TextStyle(color: Colors.red.shade700, fontSize: 14, fontWeight: FontWeight.w500))),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -237,12 +317,28 @@ class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Text('Reset Password', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Reset Password',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -12,7 +12,8 @@ class SetupWizardScreen extends StatefulWidget {
|
||||
State<SetupWizardScreen> createState() => _SetupWizardScreenState();
|
||||
}
|
||||
|
||||
class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTickerProviderStateMixin {
|
||||
class _SetupWizardScreenState extends State<SetupWizardScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool _isLoading = false;
|
||||
int _currentStep = 0;
|
||||
String _accountType = 'INDIVIDUAL';
|
||||
@@ -43,7 +44,10 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
if (username.length < 3) return;
|
||||
setState(() => _checkingUsername = true);
|
||||
try {
|
||||
final res = await DioClient().dio.get('/account/username/availability', queryParameters: {'username': username});
|
||||
final res = await DioClient().dio.get(
|
||||
'/account/username/availability',
|
||||
queryParameters: {'username': username},
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isUsernameAvailable = res.data['available'] == true;
|
||||
@@ -70,7 +74,9 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
|
||||
if (_accountType == 'BUSINESS') {
|
||||
data.addAll({
|
||||
'businessName': _businessNameController.text.isNotEmpty ? _businessNameController.text : _nameController.text,
|
||||
'businessName': _businessNameController.text.isNotEmpty
|
||||
? _businessNameController.text
|
||||
: _nameController.text,
|
||||
'natureOfBusiness': _natureOfBusiness!,
|
||||
'address': _addressController.text,
|
||||
'emailId': _emailController.text,
|
||||
@@ -82,12 +88,17 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
|
||||
await DioClient().dio.post('/account/setup', data: data);
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardScreen()));
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const DashboardScreen()),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Setup failed: $e')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Setup failed: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,21 +113,31 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
void _nextStep() {
|
||||
if (_currentStep == 1) {
|
||||
if (_nameController.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter your Full Name')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please enter your Full Name')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_usernameController.text.trim().length < 3) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Username must be at least 3 characters')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Username must be at least 3 characters'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!_isUsernameAvailable) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Username is unavailable or invalid')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Username is unavailable or invalid')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_accountType == 'BUSINESS' && _currentStep == 2) {
|
||||
if (_natureOfBusiness == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select Nature of Business')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please select Nature of Business')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -163,7 +184,11 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
children: [
|
||||
Text(
|
||||
'Step ${_currentStep + 1} of $_totalSteps',
|
||||
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold, fontSize: 14),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
@@ -172,7 +197,9 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
value: (_currentStep + 1) / _totalSteps,
|
||||
minHeight: 6,
|
||||
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -185,18 +212,39 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
key: const ValueKey(0),
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text('Welcome to Kifi!', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'Welcome to Kifi!',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('How will you be using the application?', style: TextStyle(fontSize: 16, color: Colors.grey)),
|
||||
const Text(
|
||||
'How will you be using the application?',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_buildAccountTypeCard('INDIVIDUAL', 'Individual', 'For personal use and expenses', LucideIcons.user),
|
||||
_buildAccountTypeCard(
|
||||
'INDIVIDUAL',
|
||||
'Individual',
|
||||
'For personal use and expenses',
|
||||
LucideIcons.user,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildAccountTypeCard('BUSINESS', 'Business', 'For managing business and inventory', LucideIcons.building2),
|
||||
_buildAccountTypeCard(
|
||||
'BUSINESS',
|
||||
'Business',
|
||||
'For managing business and inventory',
|
||||
LucideIcons.building2,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAccountTypeCard(String type, String title, String subtitle, IconData icon) {
|
||||
Widget _buildAccountTypeCard(
|
||||
String type,
|
||||
String title,
|
||||
String subtitle,
|
||||
IconData icon,
|
||||
) {
|
||||
final isSelected = _accountType == type;
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _accountType = type),
|
||||
@@ -205,40 +253,67 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Theme.of(context).primaryColor.withOpacity(0.08) : Colors.white,
|
||||
color: isSelected
|
||||
? Theme.of(context).primaryColor.withOpacity(0.08)
|
||||
: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected ? Theme.of(context).primaryColor : Colors.grey.shade300,
|
||||
color: isSelected
|
||||
? Theme.of(context).primaryColor
|
||||
: Colors.grey.shade300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: isSelected ? [] : [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4)),
|
||||
],
|
||||
boxShadow: isSelected
|
||||
? []
|
||||
: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Theme.of(context).primaryColor : Colors.grey.shade100,
|
||||
color: isSelected
|
||||
? Theme.of(context).primaryColor
|
||||
: Colors.grey.shade100,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: isSelected ? Colors.white : Colors.grey[600], size: 28),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: isSelected ? Colors.white : Colors.grey[600],
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(subtitle, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isSelected ? LucideIcons.checkCircle2 : LucideIcons.circle,
|
||||
color: isSelected ? Theme.of(context).primaryColor : Colors.grey[300],
|
||||
color: isSelected
|
||||
? Theme.of(context).primaryColor
|
||||
: Colors.grey[300],
|
||||
size: 28,
|
||||
),
|
||||
],
|
||||
@@ -252,9 +327,15 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
key: const ValueKey(1),
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text('Personal Details', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'Personal Details',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Tell us a bit about yourself.', style: TextStyle(fontSize: 16, color: Colors.grey)),
|
||||
const Text(
|
||||
'Tell us a bit about yourself.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
@@ -266,17 +347,28 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
controller: _usernameController,
|
||||
decoration: _buildInputDecoration('Username *').copyWith(
|
||||
suffixIcon: SizedBox(
|
||||
width: 48, height: 48,
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: Center(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: _checkingUsername
|
||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: Icon(
|
||||
_isUsernameAvailable ? LucideIcons.checkCircle2 : LucideIcons.xCircle,
|
||||
key: ValueKey(_isUsernameAvailable),
|
||||
color: _usernameController.text.isEmpty ? Colors.transparent : (_isUsernameAvailable ? Colors.green : Colors.red),
|
||||
),
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Icon(
|
||||
_isUsernameAvailable
|
||||
? LucideIcons.checkCircle2
|
||||
: LucideIcons.xCircle,
|
||||
key: ValueKey(_isUsernameAvailable),
|
||||
color: _usernameController.text.isEmpty
|
||||
? Colors.transparent
|
||||
: (_isUsernameAvailable
|
||||
? Colors.green
|
||||
: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -299,18 +391,39 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
key: const ValueKey(2),
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text('Business Profile', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'Business Profile',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Set up your company details for invoicing and tracking.', style: TextStyle(fontSize: 16, color: Colors.grey)),
|
||||
const Text(
|
||||
'Set up your company details for invoicing and tracking.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: _buildInputDecoration('Nature of Business *'),
|
||||
dropdownColor: Colors.white,
|
||||
value: _natureOfBusiness,
|
||||
items: const [
|
||||
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))),
|
||||
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),
|
||||
),
|
||||
@@ -364,9 +477,15 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
key: const ValueKey(3),
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text('Final Step', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'Final Step',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Please review and accept our policies to continue.', style: TextStyle(fontSize: 16, color: Colors.grey)),
|
||||
const Text(
|
||||
'Please review and accept our policies to continue.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -374,7 +493,11 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 10, offset: const Offset(0, 4)),
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.02),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
@@ -382,25 +505,49 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
CheckboxListTile(
|
||||
title: InkWell(
|
||||
onTap: () => _launchUrl('terms'),
|
||||
child: const Text('I accept the Terms & Conditions', style: TextStyle(color: Colors.blue, decoration: TextDecoration.underline, fontSize: 15, fontWeight: FontWeight.w500)),
|
||||
child: const Text(
|
||||
'I accept the Terms & Conditions',
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
value: _termsAccepted,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
onChanged: (val) => setState(() => _termsAccepted = val ?? false),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
onChanged: (val) =>
|
||||
setState(() => _termsAccepted = val ?? false),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
Divider(height: 1, indent: 56, color: Colors.grey.shade200),
|
||||
CheckboxListTile(
|
||||
title: InkWell(
|
||||
onTap: () => _launchUrl('privacy'),
|
||||
child: const Text('I accept the Privacy Policy', style: TextStyle(color: Colors.blue, decoration: TextDecoration.underline, fontSize: 15, fontWeight: FontWeight.w500)),
|
||||
child: const Text(
|
||||
'I accept the Privacy Policy',
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
value: _privacyAccepted,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
onChanged: (val) => setState(() => _privacyAccepted = val ?? false),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
onChanged: (val) =>
|
||||
setState(() => _privacyAccepted = val ?? false),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -412,7 +559,8 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
Widget _buildCurrentStep() {
|
||||
if (_currentStep == 0) return _buildStep0AccountType();
|
||||
if (_currentStep == 1) return _buildStep1Personal();
|
||||
if (_accountType == 'BUSINESS' && _currentStep == 2) return _buildStep2Business();
|
||||
if (_accountType == 'BUSINESS' && _currentStep == 2)
|
||||
return _buildStep2Business();
|
||||
return _buildStep3Consent(); // Step 2 (Individual) or Step 3 (Business)
|
||||
}
|
||||
|
||||
@@ -424,7 +572,10 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
leading: _currentStep > 0
|
||||
? IconButton(icon: const Icon(LucideIcons.chevronLeft), onPressed: _prevStep)
|
||||
? IconButton(
|
||||
icon: const Icon(LucideIcons.chevronLeft),
|
||||
onPressed: _prevStep,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: SafeArea(
|
||||
@@ -446,16 +597,16 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
begin: const Offset(0.05, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
child: FadeTransition(opacity: animation, child: child),
|
||||
);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
key: ValueKey(_currentStep),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24.0,
|
||||
vertical: 16.0,
|
||||
),
|
||||
child: _buildCurrentStep(),
|
||||
),
|
||||
),
|
||||
@@ -464,7 +615,10 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 24.0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24.0,
|
||||
vertical: 24.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey.shade200)),
|
||||
@@ -481,14 +635,29 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTicker
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: Text(
|
||||
_currentStep == _totalSteps - 1 ? 'Complete Setup' : 'Continue',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5),
|
||||
),
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_currentStep == _totalSteps - 1
|
||||
? 'Complete Setup'
|
||||
: 'Continue',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@ class AuthController extends AsyncNotifier<void> {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final data = await _repository.login(email, password);
|
||||
await DioClient().storage.write(key: 'jwt_token', value: data['token']);
|
||||
await DioClient().saveToken(data['token']);
|
||||
state = const AsyncValue.data(null);
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
@@ -42,7 +42,7 @@ class AuthController extends AsyncNotifier<void> {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final data = await _repository.verifyOtp(email, otp);
|
||||
await DioClient().storage.write(key: 'jwt_token', value: data['token']);
|
||||
await DioClient().saveToken(data['token']);
|
||||
state = const AsyncValue.data(null);
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
@@ -63,7 +63,11 @@ class AuthController extends AsyncNotifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> resetPassword(String email, String otp, String newPassword) async {
|
||||
Future<bool> resetPassword(
|
||||
String email,
|
||||
String otp,
|
||||
String newPassword,
|
||||
) async {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
await _repository.resetPassword(email, otp, newPassword);
|
||||
|
||||
@@ -34,7 +34,9 @@ class BusinessFeature {
|
||||
bomReductionStrategy: json['bomReductionStrategy'] ?? 'COMPONENTS_ONLY',
|
||||
stockDeductionOnInvoice: json['stockDeductionOnInvoice'] ?? true,
|
||||
barcodeSource: json['barcodeSource'] ?? 'SKU',
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,7 +73,8 @@ class BusinessFeature {
|
||||
purchaseManagement: purchaseManagement ?? this.purchaseManagement,
|
||||
multiLocation: multiLocation ?? this.multiLocation,
|
||||
bomReductionStrategy: bomReductionStrategy ?? this.bomReductionStrategy,
|
||||
stockDeductionOnInvoice: stockDeductionOnInvoice ?? this.stockDeductionOnInvoice,
|
||||
stockDeductionOnInvoice:
|
||||
stockDeductionOnInvoice ?? this.stockDeductionOnInvoice,
|
||||
barcodeSource: barcodeSource ?? this.barcodeSource,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
@@ -3,11 +3,7 @@ class IndianState {
|
||||
final String name;
|
||||
final String gstCode;
|
||||
|
||||
IndianState({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.gstCode,
|
||||
});
|
||||
IndianState({required this.id, required this.name, required this.gstCode});
|
||||
|
||||
factory IndianState.fromJson(Map<String, dynamic> json) {
|
||||
return IndianState(
|
||||
@@ -18,10 +14,6 @@ class IndianState {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'gstCode': gstCode,
|
||||
};
|
||||
return {'id': id, 'name': name, 'gstCode': gstCode};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/theme/nature_colors.dart';
|
||||
import '../../../inventory/presentation/product_list_screen.dart';
|
||||
import '../../../inventory/presentation/daily_rates_screen.dart';
|
||||
import '../../../inventory/presentation/quick_adjust_stock_screen.dart';
|
||||
import '../../../inventory/presentation/uoms_list_screen.dart';
|
||||
import '../../../inventory/presentation/category_management_screen.dart';
|
||||
import '../../../sales/presentation/customers_list_screen.dart';
|
||||
import '../../../sales/presentation/invoices_list_screen.dart';
|
||||
@@ -23,7 +23,8 @@ class BusinessHubScreen extends ConsumerWidget {
|
||||
final businessState = ref.watch(businessProfileProvider);
|
||||
final profile = businessState.value;
|
||||
|
||||
final title = (profile?.businessName != null && profile!.businessName.isNotEmpty)
|
||||
final title =
|
||||
(profile?.businessName != null && profile!.businessName.isNotEmpty)
|
||||
? profile.businessName
|
||||
: 'Business Overview';
|
||||
final subtitle = (profile?.address != null && profile!.address!.isNotEmpty)
|
||||
@@ -35,236 +36,337 @@ class BusinessHubScreen extends ConsumerWidget {
|
||||
final bool showSales = featureState?.salesManagement ?? false;
|
||||
final bool showPurchase = featureState?.purchaseManagement ?? false;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => const BusinessProfileFormSheet(),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.2)),
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(businessProfileProvider);
|
||||
ref.invalidate(businessFeatureProvider);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => const BusinessProfileFormSheet(),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.briefcase,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.briefcase, color: Theme.of(context).colorScheme.primary, size: 32),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(subtitle, style: const TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 1.2,
|
||||
children: [
|
||||
if (showInventory) ...[
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Categories',
|
||||
'Manage catalog structure',
|
||||
LucideIcons.listTree,
|
||||
Colors.teal,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const CategoryManagementScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Products Catalog',
|
||||
'View all products',
|
||||
LucideIcons.packageSearch,
|
||||
Colors.blue,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ProductListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showSales)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Customers',
|
||||
'Manage clients',
|
||||
LucideIcons.users,
|
||||
NatureColors.getColor('RECEIVABLES'),
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const CustomersListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showPurchase)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Vendors',
|
||||
'Manage suppliers',
|
||||
LucideIcons.truck,
|
||||
Colors.orange,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const VendorsListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showPurchase)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Purchases',
|
||||
'Manage inward stock',
|
||||
LucideIcons.clipboardList,
|
||||
Colors.deepOrange,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const PurchaseOrdersListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showSales)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Sales & Invoices',
|
||||
'Create invoices',
|
||||
LucideIcons.receipt,
|
||||
Colors.green,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const InvoicesListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showInventory)
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Daily Rates',
|
||||
'Sync live gold/silver rates',
|
||||
LucideIcons.refreshCw,
|
||||
Colors.orange,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const DailyRatesScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Reports',
|
||||
'Analytics & Valuation',
|
||||
LucideIcons.barChart2,
|
||||
Colors.indigo,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ReportsScreen()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 1.2,
|
||||
children: [
|
||||
if (showInventory) ...[
|
||||
_buildActionCard(
|
||||
if (showInventory) ...[
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Low Stock Alerts',
|
||||
style: Theme.of(
|
||||
context,
|
||||
'Products Catalog',
|
||||
'View all products',
|
||||
LucideIcons.packageSearch,
|
||||
Colors.blue,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const ProductListScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Adjust Stock',
|
||||
'Quick Stock Edit',
|
||||
LucideIcons.arrowRightLeft,
|
||||
Colors.orange,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const QuickAdjustStockScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Units of Measure',
|
||||
'Manage measurements',
|
||||
LucideIcons.ruler,
|
||||
Colors.purple,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const UomsListScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Categories',
|
||||
'Manage catalog structure',
|
||||
LucideIcons.listTree,
|
||||
Colors.teal,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const CategoryManagementScreen())),
|
||||
),
|
||||
],
|
||||
if (showSales) ...[
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Sales & Invoices',
|
||||
'Create invoices',
|
||||
LucideIcons.receipt,
|
||||
Colors.green,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const InvoicesListScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Customers',
|
||||
'Manage clients',
|
||||
LucideIcons.users,
|
||||
NatureColors.getColor('RECEIVABLES'),
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomersListScreen())),
|
||||
),
|
||||
],
|
||||
if (showPurchase) ...[
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Vendors',
|
||||
'Manage suppliers',
|
||||
LucideIcons.truck,
|
||||
Colors.orange,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const VendorsListScreen())),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Purchase Orders',
|
||||
'Manage inward stock',
|
||||
LucideIcons.clipboardList,
|
||||
Colors.deepOrange,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const PurchaseOrdersListScreen())),
|
||||
),
|
||||
],
|
||||
_buildActionCard(
|
||||
context,
|
||||
'Reports',
|
||||
'Analytics & Valuation',
|
||||
LucideIcons.barChart2,
|
||||
Colors.indigo,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const ReportsScreen())),
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showInventory) ...[
|
||||
const SizedBox(height: 32),
|
||||
Text('Low Stock Alerts', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final productsState = ref.watch(productsProvider);
|
||||
return productsState.when(
|
||||
data: (products) {
|
||||
final lowStockProducts = products.where((p) {
|
||||
if (!(p.trackInventory ?? false)) return false;
|
||||
if (p.currentStock == null) return false;
|
||||
final minStock = p.minStock ?? 0;
|
||||
return p.currentStock! <= minStock;
|
||||
}).toList();
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final productsState = ref.watch(productsProvider);
|
||||
return productsState.when(
|
||||
data: (products) {
|
||||
final lowStockProducts = products.where((p) {
|
||||
if (p.currentStock == null) return false;
|
||||
if (p.currentStock == null) return false;
|
||||
final minStock = 0;
|
||||
return p.currentStock! <= minStock;
|
||||
}).toList();
|
||||
|
||||
if (lowStockProducts.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text('All products are adequately stocked.', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: lowStockProducts.length > 5 ? 5 : lowStockProducts.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final product = lowStockProducts[index];
|
||||
final isOutOfStock = product.currentStock! <= 0;
|
||||
if (lowStockProducts.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock ? Colors.red.withOpacity(0.05) : Colors.orange.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: isOutOfStock ? Colors.red.withOpacity(0.2) : Colors.orange.withOpacity(0.2)),
|
||||
color: Colors.grey.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock ? Colors.red.withOpacity(0.1) : Colors.orange.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.alertTriangle,
|
||||
color: isOutOfStock ? Colors.red : Colors.orange,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(product.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
if (product.sku != null && product.sku!.isNotEmpty)
|
||||
Text('SKU: ${product.sku}', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${product.currentStock} left',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOutOfStock ? Colors.red : Colors.orange.shade800,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Min: ${product.minStock ?? 0}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'All products are adequately stocked.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: lowStockProducts.length > 5
|
||||
? 5
|
||||
: lowStockProducts.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final product = lowStockProducts[index];
|
||||
final isOutOfStock = product.currentStock! <= 0;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock
|
||||
? Colors.red.withOpacity(0.05)
|
||||
: Colors.orange.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isOutOfStock
|
||||
? Colors.red.withOpacity(0.2)
|
||||
: Colors.orange.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock
|
||||
? Colors.red.withOpacity(0.1)
|
||||
: Colors.orange.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.alertTriangle,
|
||||
color: isOutOfStock
|
||||
? Colors.red
|
||||
: Colors.orange,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (product.sku != null &&
|
||||
product.sku!.isNotEmpty)
|
||||
Text(
|
||||
'SKU: ${product.sku}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${product.currentStock} left',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOutOfStock
|
||||
? Colors.red
|
||||
: Colors.orange.shade800,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Out of stock',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) =>
|
||||
Center(child: Text('Error: $error')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionCard(BuildContext context, String title, String subtitle, IconData icon, Color color, VoidCallback onTap) {
|
||||
Widget _buildActionCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String subtitle,
|
||||
IconData icon,
|
||||
Color color,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
@@ -280,9 +382,19 @@ class BusinessHubScreen extends ConsumerWidget {
|
||||
children: [
|
||||
Icon(icon, color: color, size: 28),
|
||||
const SizedBox(height: 12),
|
||||
Text(title, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(subtitle, style: TextStyle(color: color.withOpacity(0.7), fontSize: 12)),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(color: color.withOpacity(0.7), fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -13,9 +13,7 @@ class ReportsScreen extends ConsumerWidget {
|
||||
final formatCurrency = NumberFormat.currency(symbol: '₹');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Business Reports'),
|
||||
),
|
||||
appBar: AppBar(title: const Text('Business Reports')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -29,7 +27,10 @@ class ReportsScreen extends ConsumerWidget {
|
||||
content: valuationState.when(
|
||||
data: (val) => Text(
|
||||
'Total Estimated Value: ${formatCurrency.format(val)}',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, s) => Text('Error: $e'),
|
||||
@@ -41,7 +42,9 @@ class ReportsScreen extends ConsumerWidget {
|
||||
title: 'GST Report (Coming Soon)',
|
||||
icon: LucideIcons.fileText,
|
||||
color: Colors.orange,
|
||||
content: const Text('Export GSTR-1 & GSTR-3B formats based on invoices.'),
|
||||
content: const Text(
|
||||
'Export GSTR-1 & GSTR-3B formats based on invoices.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildReportCard(
|
||||
@@ -57,7 +60,8 @@ class ReportsScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReportCard(BuildContext context, {
|
||||
Widget _buildReportCard(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
@@ -81,7 +85,13 @@ class ReportsScreen extends ConsumerWidget {
|
||||
child: Icon(icon, color: color),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -7,10 +7,12 @@ class BusinessSettingsScreen extends ConsumerStatefulWidget {
|
||||
const BusinessSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<BusinessSettingsScreen> createState() => _BusinessSettingsScreenState();
|
||||
ConsumerState<BusinessSettingsScreen> createState() =>
|
||||
_BusinessSettingsScreenState();
|
||||
}
|
||||
|
||||
class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen> {
|
||||
class _BusinessSettingsScreenState
|
||||
extends ConsumerState<BusinessSettingsScreen> {
|
||||
bool _taxIncludedInPrice = false;
|
||||
bool _salesEnabled = true;
|
||||
bool _inventoryEnabled = true;
|
||||
@@ -48,11 +50,20 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: [
|
||||
const Text('Module Configuration', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
const Text(
|
||||
'Module Configuration',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Inventory Management'),
|
||||
subtitle: const Text('Track stock levels, multiple locations, and products'),
|
||||
subtitle: const Text(
|
||||
'Track stock levels, multiple locations, and products',
|
||||
),
|
||||
value: _inventoryEnabled,
|
||||
onChanged: (val) {
|
||||
setState(() => _inventoryEnabled = val);
|
||||
@@ -62,7 +73,9 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Sales & POS'),
|
||||
subtitle: const Text('Enable point of sale, invoicing, and receivables'),
|
||||
subtitle: const Text(
|
||||
'Enable point of sale, invoicing, and receivables',
|
||||
),
|
||||
value: _salesEnabled,
|
||||
onChanged: (val) {
|
||||
setState(() => _salesEnabled = val);
|
||||
@@ -72,7 +85,9 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Vendor & Purchases'),
|
||||
subtitle: const Text('Manage vendors, purchase orders, and inward stock'),
|
||||
subtitle: const Text(
|
||||
'Manage vendors, purchase invoices, and inward stock',
|
||||
),
|
||||
value: _purchaseEnabled,
|
||||
onChanged: (val) {
|
||||
setState(() => _purchaseEnabled = val);
|
||||
@@ -81,64 +96,105 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
secondary: const Icon(LucideIcons.truck),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const Text('Inventory Strategy', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
const Text(
|
||||
'Inventory Strategy',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final featureState = ref.watch(businessFeatureProvider);
|
||||
final currentStrategy = featureState.value?.bomReductionStrategy ?? 'COMPONENTS_ONLY';
|
||||
final deductParentStock = currentStrategy == 'PARENT_AND_COMPONENTS';
|
||||
final stockDeductionOnInvoice = featureState.value?.stockDeductionOnInvoice ?? true;
|
||||
final currentStrategy =
|
||||
featureState.value?.bomReductionStrategy ?? 'COMPONENTS_ONLY';
|
||||
final deductParentStock =
|
||||
currentStrategy == 'PARENT_AND_COMPONENTS';
|
||||
final stockDeductionOnInvoice =
|
||||
featureState.value?.stockDeductionOnInvoice ?? true;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Deduct Parent Stock on BOM Sale'),
|
||||
subtitle: const Text('If enabled, selling an assembled product reduces both parent and component stock.'),
|
||||
subtitle: const Text(
|
||||
'If enabled, selling an assembled product reduces both parent and component stock.',
|
||||
),
|
||||
value: deductParentStock,
|
||||
secondary: const Icon(LucideIcons.gitMerge),
|
||||
onChanged: (val) {
|
||||
if (featureState.value != null) {
|
||||
final newStrategy = val ? 'PARENT_AND_COMPONENTS' : 'COMPONENTS_ONLY';
|
||||
final updated = featureState.value!.copyWith(bomReductionStrategy: newStrategy);
|
||||
ref.read(businessFeatureProvider.notifier).updateFeatures(updated);
|
||||
final newStrategy = val
|
||||
? 'PARENT_AND_COMPONENTS'
|
||||
: 'COMPONENTS_ONLY';
|
||||
final updated = featureState.value!.copyWith(
|
||||
bomReductionStrategy: newStrategy,
|
||||
);
|
||||
ref
|
||||
.read(businessFeatureProvider.notifier)
|
||||
.updateFeatures(updated);
|
||||
}
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Stock Deduction on Invoice'),
|
||||
subtitle: const Text('If enabled, finalizing an invoice automatically deducts product stock. Turn off for a two-step fulfillment process.'),
|
||||
subtitle: const Text(
|
||||
'If enabled, finalizing an invoice automatically deducts product stock. Turn off for a two-step fulfillment process.',
|
||||
),
|
||||
value: stockDeductionOnInvoice,
|
||||
secondary: const Icon(LucideIcons.boxes),
|
||||
onChanged: (val) {
|
||||
if (featureState.value != null) {
|
||||
final updated = featureState.value!.copyWith(stockDeductionOnInvoice: val);
|
||||
ref.read(businessFeatureProvider.notifier).updateFeatures(updated);
|
||||
final updated = featureState.value!.copyWith(
|
||||
stockDeductionOnInvoice: val,
|
||||
);
|
||||
ref
|
||||
.read(businessFeatureProvider.notifier)
|
||||
.updateFeatures(updated);
|
||||
}
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Use EAN/Barcode for Scanner'),
|
||||
subtitle: const Text('If disabled, the barcode scanner will search using the SKU field instead.'),
|
||||
value: (featureState.value?.barcodeSource ?? 'SKU') == 'BARCODE',
|
||||
subtitle: const Text(
|
||||
'If disabled, the barcode scanner will search using the SKU field instead.',
|
||||
),
|
||||
value:
|
||||
(featureState.value?.barcodeSource ?? 'SKU') ==
|
||||
'BARCODE',
|
||||
secondary: const Icon(LucideIcons.scanLine),
|
||||
onChanged: (val) {
|
||||
if (featureState.value != null) {
|
||||
final updated = featureState.value!.copyWith(barcodeSource: val ? 'BARCODE' : 'SKU');
|
||||
ref.read(businessFeatureProvider.notifier).updateFeatures(updated);
|
||||
final updated = featureState.value!.copyWith(
|
||||
barcodeSource: val ? 'BARCODE' : 'SKU',
|
||||
);
|
||||
ref
|
||||
.read(businessFeatureProvider.notifier)
|
||||
.updateFeatures(updated);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const Text('Pricing & Taxation', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
const Text(
|
||||
'Pricing & Taxation',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Selling Price includes Tax'),
|
||||
subtitle: const Text('If enabled, GST is assumed to be inclusive in the entered selling price.'),
|
||||
subtitle: const Text(
|
||||
'If enabled, GST is assumed to be inclusive in the entered selling price.',
|
||||
),
|
||||
value: _taxIncludedInPrice,
|
||||
onChanged: (val) {
|
||||
setState(() => _taxIncludedInPrice = val);
|
||||
@@ -154,7 +210,9 @@ class _BusinessSettingsScreenState extends ConsumerState<BusinessSettingsScreen>
|
||||
void _saveSettings() {
|
||||
final profileState = ref.read(businessProfileProvider).value;
|
||||
if (profileState != null) {
|
||||
final updated = profileState.copyWith(taxIncludedInPrice: _taxIncludedInPrice);
|
||||
final updated = profileState.copyWith(
|
||||
taxIncludedInPrice: _taxIncludedInPrice,
|
||||
);
|
||||
ref.read(businessProfileProvider.notifier).updateProfile(updated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,10 @@ class BusinessProfileNotifier extends AsyncNotifier<BusinessProfile?> {
|
||||
}
|
||||
}
|
||||
|
||||
final businessProfileProvider = AsyncNotifierProvider<BusinessProfileNotifier, BusinessProfile?>(() {
|
||||
return BusinessProfileNotifier();
|
||||
});
|
||||
final businessProfileProvider =
|
||||
AsyncNotifierProvider<BusinessProfileNotifier, BusinessProfile?>(() {
|
||||
return BusinessProfileNotifier();
|
||||
});
|
||||
|
||||
class BusinessFeatureNotifier extends AsyncNotifier<BusinessFeature?> {
|
||||
@override
|
||||
@@ -74,6 +75,7 @@ class BusinessFeatureNotifier extends AsyncNotifier<BusinessFeature?> {
|
||||
}
|
||||
}
|
||||
|
||||
final businessFeatureProvider = AsyncNotifierProvider<BusinessFeatureNotifier, BusinessFeature?>(() {
|
||||
return BusinessFeatureNotifier();
|
||||
});
|
||||
final businessFeatureProvider =
|
||||
AsyncNotifierProvider<BusinessFeatureNotifier, BusinessFeature?>(() {
|
||||
return BusinessFeatureNotifier();
|
||||
});
|
||||
|
||||
@@ -9,17 +9,27 @@ class IndianStatesNotifier extends AsyncNotifier<List<IndianState>> {
|
||||
}
|
||||
|
||||
Future<List<IndianState>> _fetchStates() async {
|
||||
final response = await DioClient().dio.get('/master/states').timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw Exception('Connection timed out'),
|
||||
);
|
||||
final response = await DioClient().dio
|
||||
.get('/master/states')
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw Exception('Connection timed out'),
|
||||
);
|
||||
if (response.data == null || response.data.toString().isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return (response.data as List).map((e) => IndianState.fromJson(e)).toList();
|
||||
final list = (response.data as List).map((e) => IndianState.fromJson(e)).toList();
|
||||
final Map<String, IndianState> uniqueStates = {};
|
||||
for (var state in list) {
|
||||
uniqueStates[state.gstCode] = state;
|
||||
}
|
||||
final result = uniqueStates.values.toList();
|
||||
result.sort((a, b) => a.name.compareTo(b.name));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
final indianStatesProvider = AsyncNotifierProvider<IndianStatesNotifier, List<IndianState>>(() {
|
||||
return IndianStatesNotifier();
|
||||
});
|
||||
final indianStatesProvider =
|
||||
AsyncNotifierProvider<IndianStatesNotifier, List<IndianState>>(() {
|
||||
return IndianStatesNotifier();
|
||||
});
|
||||
|
||||
@@ -21,14 +21,19 @@ class _WalletLedgerScreenState extends ConsumerState<WalletLedgerScreen> {
|
||||
void _showFilterSheet() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Filter Ledger', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'Filter Ledger',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: const Text('Today'),
|
||||
@@ -95,7 +100,13 @@ class _WalletLedgerScreenState extends ConsumerState<WalletLedgerScreen> {
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
data: (transactions) {
|
||||
// 1. Filter all transactions related to this wallet
|
||||
final walletTxs = transactions.where((t) => t.fromWalletId == widget.wallet.id || t.toWalletId == widget.wallet.id).toList();
|
||||
final walletTxs = transactions
|
||||
.where(
|
||||
(t) =>
|
||||
t.fromWalletId == widget.wallet.id ||
|
||||
t.toWalletId == widget.wallet.id,
|
||||
)
|
||||
.toList();
|
||||
|
||||
// Sort chronologically (oldest first) to compute running balance
|
||||
walletTxs.sort((a, b) => a.date.compareTo(b.date));
|
||||
@@ -110,9 +121,17 @@ class _WalletLedgerScreenState extends ConsumerState<WalletLedgerScreen> {
|
||||
} else if (_selectedFilter == 'Current Month') {
|
||||
startDate = DateTime(now.year, now.month, 1);
|
||||
endDate = DateTime(now.year, now.month + 1, 0, 23, 59, 59);
|
||||
} else if (_selectedFilter == 'Custom Range' && _customDateRange != null) {
|
||||
} else if (_selectedFilter == 'Custom Range' &&
|
||||
_customDateRange != null) {
|
||||
startDate = _customDateRange!.start;
|
||||
endDate = DateTime(_customDateRange!.end.year, _customDateRange!.end.month, _customDateRange!.end.day, 23, 59, 59);
|
||||
endDate = DateTime(
|
||||
_customDateRange!.end.year,
|
||||
_customDateRange!.end.month,
|
||||
_customDateRange!.end.day,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Calculate opening balance (sum of all transactions BEFORE start date)
|
||||
@@ -142,25 +161,41 @@ class _WalletLedgerScreenState extends ConsumerState<WalletLedgerScreen> {
|
||||
|
||||
// Add Opening Balance Row if we have a date filter
|
||||
if (startDate != null) {
|
||||
rows.add(DataRow(
|
||||
cells: [
|
||||
const DataCell(Text('-')),
|
||||
DataCell(Text(DateFormat('dd MMM yyyy').format(startDate))),
|
||||
DataCell(Text('Rs. ${openingBalance.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
const DataCell(Text('-')),
|
||||
const DataCell(Text('-')),
|
||||
DataCell(Text('Rs. ${runningBalance.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
],
|
||||
));
|
||||
rows.add(
|
||||
DataRow(
|
||||
cells: [
|
||||
const DataCell(Text('-')),
|
||||
DataCell(Text(DateFormat('dd MMM yyyy').format(startDate))),
|
||||
DataCell(
|
||||
Text(
|
||||
'Rs. ${openingBalance.toStringAsFixed(2)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const DataCell(Text('-')),
|
||||
const DataCell(Text('-')),
|
||||
DataCell(
|
||||
Text(
|
||||
'Rs. ${runningBalance.toStringAsFixed(2)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (int i = 0; i < visibleTxs.length; i++) {
|
||||
final t = visibleTxs[i];
|
||||
|
||||
bool isIncome = t.toWalletId == widget.wallet.id && t.fromWalletId == null;
|
||||
bool isExpense = t.fromWalletId == widget.wallet.id && t.toWalletId == null;
|
||||
bool isTransferIn = t.toWalletId == widget.wallet.id && t.fromWalletId != null;
|
||||
bool isTransferOut = t.fromWalletId == widget.wallet.id && t.toWalletId != null;
|
||||
bool isIncome =
|
||||
t.toWalletId == widget.wallet.id && t.fromWalletId == null;
|
||||
bool isExpense =
|
||||
t.fromWalletId == widget.wallet.id && t.toWalletId == null;
|
||||
bool isTransferIn =
|
||||
t.toWalletId == widget.wallet.id && t.fromWalletId != null;
|
||||
bool isTransferOut =
|
||||
t.fromWalletId == widget.wallet.id && t.toWalletId != null;
|
||||
|
||||
double amount = t.amount;
|
||||
if (isExpense || isTransferOut) {
|
||||
@@ -174,7 +209,12 @@ class _WalletLedgerScreenState extends ConsumerState<WalletLedgerScreen> {
|
||||
|
||||
if (isIncome || isTransferIn) {
|
||||
if (t.fromWalletId != null && walletsState.hasValue) {
|
||||
fromName = walletsState.value!.where((w) => w.id == t.fromWalletId).firstOrNull?.name ?? 'Unknown';
|
||||
fromName =
|
||||
walletsState.value!
|
||||
.where((w) => w.id == t.fromWalletId)
|
||||
.firstOrNull
|
||||
?.name ??
|
||||
'Unknown';
|
||||
} else {
|
||||
fromName = 'External';
|
||||
}
|
||||
@@ -182,29 +222,53 @@ class _WalletLedgerScreenState extends ConsumerState<WalletLedgerScreen> {
|
||||
} else if (isExpense || isTransferOut) {
|
||||
fromName = widget.wallet.name;
|
||||
if (t.toWalletId != null && walletsState.hasValue) {
|
||||
toName = walletsState.value!.where((w) => w.id == t.toWalletId).firstOrNull?.name ?? 'Unknown';
|
||||
toName =
|
||||
walletsState.value!
|
||||
.where((w) => w.id == t.toWalletId)
|
||||
.firstOrNull
|
||||
?.name ??
|
||||
'Unknown';
|
||||
} else {
|
||||
toName = 'External';
|
||||
}
|
||||
}
|
||||
|
||||
Color amountColor = (isExpense || isTransferOut) ? Colors.red : Colors.green.shade700;
|
||||
Color amountColor = (isExpense || isTransferOut)
|
||||
? Colors.red
|
||||
: Colors.green.shade700;
|
||||
String amountPrefix = (isExpense || isTransferOut) ? '-' : '+';
|
||||
|
||||
rows.add(DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${startDate != null ? i + 1 : i + 1}')),
|
||||
DataCell(Text(DateFormat('dd MMM yyyy').format(t.date))),
|
||||
DataCell(Text('$amountPrefix Rs. ${amount.toStringAsFixed(2)}', style: TextStyle(color: amountColor, fontWeight: FontWeight.w600))),
|
||||
DataCell(Text(fromName)),
|
||||
DataCell(Text(toName)),
|
||||
DataCell(Text('Rs. ${runningBalance.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
],
|
||||
));
|
||||
rows.add(
|
||||
DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${startDate != null ? i + 1 : i + 1}')),
|
||||
DataCell(Text(DateFormat('dd MMM yyyy').format(t.date))),
|
||||
DataCell(
|
||||
Text(
|
||||
'$amountPrefix Rs. ${amount.toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
color: amountColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(Text(fromName)),
|
||||
DataCell(Text(toName)),
|
||||
DataCell(
|
||||
Text(
|
||||
'Rs. ${runningBalance.toStringAsFixed(2)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (rows.isEmpty) {
|
||||
return const Center(child: Text('No transactions found for this period.'));
|
||||
return const Center(
|
||||
child: Text('No transactions found for this period.'),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
@@ -214,8 +278,17 @@ class _WalletLedgerScreenState extends ConsumerState<WalletLedgerScreen> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Filter: $_selectedFilter', style: TextStyle(color: Colors.grey.shade600)),
|
||||
Text('Closing Balance: Rs. ${runningBalance.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text(
|
||||
'Filter: $_selectedFilter',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
Text(
|
||||
'Closing Balance: Rs. ${runningBalance.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -225,15 +298,47 @@ class _WalletLedgerScreenState extends ConsumerState<WalletLedgerScreen> {
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.resolveWith((states) => Colors.grey.shade100),
|
||||
headingRowColor: WidgetStateProperty.resolveWith(
|
||||
(states) => Colors.grey.shade100,
|
||||
),
|
||||
columnSpacing: 24,
|
||||
columns: const [
|
||||
DataColumn(label: Text('S.No', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Date', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Amount', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('From', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('To', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Balance', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'S.No',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Date',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Amount',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'From',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'To',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Balance',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: rows,
|
||||
),
|
||||
|
||||
@@ -23,22 +23,30 @@ class BudgetStatusCard extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final currentMonthTxs = transactions.where((t) => t.date.month == now.month && t.date.year == now.year);
|
||||
final currentMonthTxs = transactions.where(
|
||||
(t) => t.date.month == now.month && t.date.year == now.year,
|
||||
);
|
||||
|
||||
double totalSpent = 0;
|
||||
for (var b in budgets) {
|
||||
if (b.walletId != null) {
|
||||
final spentForWallet = currentMonthTxs.where((t) => t.toWalletId == b.walletId).fold(0.0, (s, t) => s + t.amount);
|
||||
final spentForWallet = currentMonthTxs
|
||||
.where((t) => t.toWalletId == b.walletId)
|
||||
.fold(0.0, (s, t) => s + t.amount);
|
||||
totalSpent += spentForWallet;
|
||||
}
|
||||
}
|
||||
|
||||
final totalLimit = budgets.fold(0.0, (s, b) => s + b.monthlyLimit);
|
||||
final progress = totalLimit > 0 ? (totalSpent / totalLimit).clamp(0.0, 1.0) : 0.0;
|
||||
final progress = totalLimit > 0
|
||||
? (totalSpent / totalLimit).clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
|
||||
Color progressColor = Colors.green;
|
||||
if (progress > 0.9) progressColor = Colors.red;
|
||||
else if (progress > 0.7) progressColor = Colors.orange;
|
||||
if (progress > 0.9)
|
||||
progressColor = Colors.red;
|
||||
else if (progress > 0.7)
|
||||
progressColor = Colors.orange;
|
||||
|
||||
return Card(
|
||||
child: InkWell(
|
||||
@@ -55,7 +63,10 @@ class BudgetStatusCard extends ConsumerWidget {
|
||||
children: [
|
||||
Icon(LucideIcons.target, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Overall Budget Status', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text(
|
||||
'Overall Budget Status',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -70,10 +81,16 @@ class BudgetStatusCard extends ConsumerWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Spent: Rs. ${totalSpent.toStringAsFixed(0)}', style: TextStyle(color: Colors.grey.shade700)),
|
||||
Text('Limit: Rs. ${totalLimit.toStringAsFixed(0)}', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
'Spent: Rs. ${totalSpent.toStringAsFixed(0)}',
|
||||
style: TextStyle(color: Colors.grey.shade700),
|
||||
),
|
||||
Text(
|
||||
'Limit: Rs. ${totalLimit.toStringAsFixed(0)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -28,9 +28,16 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
|
||||
final Map<int, double> categoryTotals = {};
|
||||
for (var t in widget.transactions) {
|
||||
bool matches = false;
|
||||
if (widget.selectedNature == 'INCOME') matches = t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null);
|
||||
else if (widget.selectedNature == 'EXPENSE') matches = t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null);
|
||||
else if (widget.selectedNature == 'INVESTMENTS') matches = t.type == 'INVESTMENT';
|
||||
if (widget.selectedNature == 'INCOME')
|
||||
matches =
|
||||
t.type == 'INCOME' ||
|
||||
(t.type == 'TRANSFER' && t.fromWalletId == null);
|
||||
else if (widget.selectedNature == 'EXPENSE')
|
||||
matches =
|
||||
t.type == 'EXPENSE' ||
|
||||
(t.type == 'TRANSFER' && t.toWalletId == null);
|
||||
else if (widget.selectedNature == 'INVESTMENTS')
|
||||
matches = t.type == 'INVESTMENT';
|
||||
|
||||
if (matches) {
|
||||
final catId = t.categoryId ?? -1;
|
||||
@@ -42,8 +49,9 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final List<MapEntry<int, double>> sortedTotals = categoryTotals.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
final List<MapEntry<int, double>> sortedTotals =
|
||||
categoryTotals.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
|
||||
final List<Color> colors = NatureColors.getPalette(widget.selectedNature);
|
||||
|
||||
@@ -77,9 +85,20 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.1), blurRadius: 4)],
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(
|
||||
alpha: isDark ? 0.3 : 0.1,
|
||||
),
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.touch_app,
|
||||
size: 16,
|
||||
color: colors[i % colors.length],
|
||||
),
|
||||
child: Icon(Icons.touch_app, size: 16, color: colors[i % colors.length]),
|
||||
)
|
||||
: null,
|
||||
badgePositionPercentageOffset: 1.1,
|
||||
@@ -89,21 +108,39 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(32), side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2))),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isDark ? [Theme.of(context).cardColor, Theme.of(context).cardColor.withOpacity(0.8)] : [Colors.white, Colors.grey.shade50],
|
||||
colors: isDark
|
||||
? [
|
||||
Theme.of(context).cardColor,
|
||||
Theme.of(context).cardColor.withOpacity(0.8),
|
||||
]
|
||||
: [Colors.white, Colors.grey.shade50],
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(28.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Category Breakdown', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w800, letterSpacing: -0.5, color: Theme.of(context).textTheme.bodyLarge?.color)),
|
||||
Text(
|
||||
'Category Breakdown',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.5,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
SizedBox(
|
||||
@@ -116,11 +153,15 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
|
||||
pieTouchData: PieTouchData(
|
||||
touchCallback: (FlTouchEvent event, pieTouchResponse) {
|
||||
setState(() {
|
||||
if (!event.isInterestedForInteractions || pieTouchResponse == null || pieTouchResponse.touchedSection == null) {
|
||||
if (!event.isInterestedForInteractions ||
|
||||
pieTouchResponse == null ||
|
||||
pieTouchResponse.touchedSection == null) {
|
||||
touchedIndex = -1;
|
||||
return;
|
||||
}
|
||||
touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex;
|
||||
touchedIndex = pieTouchResponse
|
||||
.touchedSection!
|
||||
.touchedSectionIndex;
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -134,8 +175,22 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Total', style: TextStyle(fontSize: 14, color: Colors.grey.shade500, fontWeight: FontWeight.w600)),
|
||||
Text('Rs. ${totalAmount.toStringAsFixed(0)}', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Theme.of(context).textTheme.bodyLarge?.color)),
|
||||
Text(
|
||||
'Total',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade500,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Rs. ${totalAmount.toStringAsFixed(0)}',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -143,38 +198,62 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
Text('Details', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Theme.of(context).textTheme.bodyLarge?.color)),
|
||||
Text(
|
||||
'Details',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Modern List for Legends
|
||||
...List.generate(sortedTotals.length, (i) {
|
||||
String categoryName = 'Uncategorized';
|
||||
if (sortedTotals[i].key != -1) {
|
||||
final match = widget.categories.where((c) => c.id == sortedTotals[i].key);
|
||||
final match = widget.categories.where(
|
||||
(c) => c.id == sortedTotals[i].key,
|
||||
);
|
||||
if (match.isNotEmpty) categoryName = match.first.name;
|
||||
}
|
||||
|
||||
final amount = sortedTotals[i].value;
|
||||
final percentage = (amount / totalAmount * 100).toStringAsFixed(1);
|
||||
final percentage = (amount / totalAmount * 100).toStringAsFixed(
|
||||
1,
|
||||
);
|
||||
final isTouched = i == touchedIndex;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 20,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isTouched ? colors[i % colors.length].withValues(alpha: 0.05) : (isDark ? Theme.of(context).scaffoldBackgroundColor : Colors.white),
|
||||
color: isTouched
|
||||
? colors[i % colors.length].withValues(alpha: 0.05)
|
||||
: (isDark
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: Colors.white),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isTouched ? colors[i % colors.length].withValues(alpha: 0.3) : Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05),
|
||||
color: isTouched
|
||||
? colors[i % colors.length].withValues(alpha: 0.3)
|
||||
: Theme.of(
|
||||
context,
|
||||
).dividerColor.withOpacity(isDark ? 0.1 : 0.05),
|
||||
width: isTouched ? 1.5 : 1.0,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: isTouched ? 0.05 : 0.02),
|
||||
color: Colors.black.withValues(
|
||||
alpha: isTouched ? 0.05 : 0.02,
|
||||
),
|
||||
blurRadius: isTouched ? 12 : 6,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
@@ -184,10 +263,16 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: colors[i % colors.length].withValues(alpha: 0.15),
|
||||
color: colors[i % colors.length].withValues(
|
||||
alpha: 0.15,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.category, color: colors[i % colors.length], size: 20),
|
||||
child: Icon(
|
||||
Icons.category,
|
||||
color: colors[i % colors.length],
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@@ -196,19 +281,45 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(categoryName, style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15, color: Theme.of(context).textTheme.bodyLarge?.color)),
|
||||
Text(
|
||||
categoryName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 15,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('Rs. ${amount.toStringAsFixed(0)}', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: Colors.grey.shade500)),
|
||||
Text(
|
||||
'Rs. ${amount.toStringAsFixed(0)}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colors[i % colors.length].withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text('$percentage%', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 13, color: colors[i % colors.length])),
|
||||
child: Text(
|
||||
'$percentage%',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 13,
|
||||
color: colors[i % colors.length],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -23,31 +23,50 @@ class DayWiseSpendingChart extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
// 1. Determine date range length
|
||||
final int daysCount = endDate.difference(startDate).inDays + 1;
|
||||
final int displayDays = daysCount > 0 && daysCount < 1000 ? daysCount : 30; // Guard against 'All Time' returning thousands
|
||||
final int displayDays = daysCount > 0 && daysCount < 1000
|
||||
? daysCount
|
||||
: 30; // Guard against 'All Time' returning thousands
|
||||
|
||||
final days = List.generate(displayDays, (i) => startDate.add(Duration(days: i)));
|
||||
final days = List.generate(
|
||||
displayDays,
|
||||
(i) => startDate.add(Duration(days: i)),
|
||||
);
|
||||
|
||||
// 2. Initialize daily totals
|
||||
final Map<String, double> dailyTotals = {};
|
||||
for (var d in days) {
|
||||
dailyTotals["${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}"] = 0.0;
|
||||
dailyTotals["${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}"] =
|
||||
0.0;
|
||||
}
|
||||
|
||||
// 3. Populate totals
|
||||
for (var t in transactions) {
|
||||
final tDate = "${t.date.year}-${t.date.month.toString().padLeft(2, '0')}-${t.date.day.toString().padLeft(2, '0')}";
|
||||
final tDate =
|
||||
"${t.date.year}-${t.date.month.toString().padLeft(2, '0')}-${t.date.day.toString().padLeft(2, '0')}";
|
||||
if (dailyTotals.containsKey(tDate)) {
|
||||
bool matches = false;
|
||||
if (selectedNature == 'INCOME') matches = t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null);
|
||||
else if (selectedNature == 'EXPENSE') matches = t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null);
|
||||
else if (selectedNature == 'INVESTMENTS') matches = t.type == 'INVESTMENT';
|
||||
else if (selectedNature == 'PAYABLES' || selectedNature == 'RECEIVABLES') {
|
||||
if (selectedNature == 'INCOME')
|
||||
matches =
|
||||
t.type == 'INCOME' ||
|
||||
(t.type == 'TRANSFER' && t.fromWalletId == null);
|
||||
else if (selectedNature == 'EXPENSE')
|
||||
matches =
|
||||
t.type == 'EXPENSE' ||
|
||||
(t.type == 'TRANSFER' && t.toWalletId == null);
|
||||
else if (selectedNature == 'INVESTMENTS')
|
||||
matches = t.type == 'INVESTMENT';
|
||||
else if (selectedNature == 'PAYABLES' ||
|
||||
selectedNature == 'RECEIVABLES') {
|
||||
if (t.type == 'TRANSFER' && t.toWalletId != null) {
|
||||
final w = wallets.where((w) => w.id == t.toWalletId);
|
||||
if (w.isNotEmpty) {
|
||||
final nature = w.first.nature;
|
||||
if (selectedNature == 'PAYABLES' && (nature == 'PAYABLES' || nature == 'LOAN')) matches = true;
|
||||
if (selectedNature == 'RECEIVABLES' && (nature == 'RECEIVABLES' || nature == 'LENDING')) matches = true;
|
||||
if (selectedNature == 'PAYABLES' &&
|
||||
(nature == 'PAYABLES' || nature == 'LOAN'))
|
||||
matches = true;
|
||||
if (selectedNature == 'RECEIVABLES' &&
|
||||
(nature == 'RECEIVABLES' || nature == 'LENDING'))
|
||||
matches = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,19 +88,26 @@ class DayWiseSpendingChart extends StatelessWidget {
|
||||
// Make chart horizontally scrollable if there are many days
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
double chartWidth = (screenWidth / 7) * displayDays;
|
||||
if (chartWidth < screenWidth - 80) { // minimum width
|
||||
if (chartWidth < screenWidth - 80) {
|
||||
// minimum width
|
||||
chartWidth = screenWidth - 80;
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24), side: BorderSide(color: Colors.grey.shade200)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Spending Trends', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'Spending Trends',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
@@ -100,51 +126,73 @@ class DayWiseSpendingChart extends StatelessWidget {
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final index = value.toInt();
|
||||
if (index < 0 || index >= days.length) return const SizedBox();
|
||||
if (index < 0 || index >= days.length)
|
||||
return const SizedBox();
|
||||
final date = days[index];
|
||||
final text = "${date.day} ${_monthStr(date.month)}";
|
||||
|
||||
// To prevent overcrowding on large ranges
|
||||
if (displayDays > 14 && index % 2 != 0) return const SizedBox();
|
||||
if (displayDays > 30 && index % 5 != 0) return const SizedBox();
|
||||
if (displayDays > 14 && index % 2 != 0)
|
||||
return const SizedBox();
|
||||
if (displayDays > 30 && index % 5 != 0)
|
||||
return const SizedBox();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(text, style: const TextStyle(color: Colors.grey, fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
reservedSize: 28,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: maxY / 4 == 0 ? 1 : maxY / 4,
|
||||
getDrawingHorizontalLine: (value) => FlLine(color: Colors.grey.shade200, strokeWidth: 1, dashArray: [4, 4]),
|
||||
getDrawingHorizontalLine: (value) => FlLine(
|
||||
color: Colors.grey.shade200,
|
||||
strokeWidth: 1,
|
||||
dashArray: [4, 4],
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barGroups: dailyTotals.entries.toList().asMap().entries.map((entry) {
|
||||
return BarChartGroupData(
|
||||
x: entry.key,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: entry.value.value,
|
||||
color: barColor,
|
||||
width: 16,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
backDrawRodData: BackgroundBarChartRodData(
|
||||
show: true,
|
||||
toY: maxY * 1.2,
|
||||
color: Colors.grey.shade100,
|
||||
barGroups: dailyTotals.entries.toList().asMap().entries.map(
|
||||
(entry) {
|
||||
return BarChartGroupData(
|
||||
x: entry.key,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: entry.value.value,
|
||||
color: barColor,
|
||||
width: 16,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
backDrawRodData: BackgroundBarChartRodData(
|
||||
show: true,
|
||||
toY: maxY * 1.2,
|
||||
color: Colors.grey.shade100,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -156,7 +204,20 @@ class DayWiseSpendingChart extends StatelessWidget {
|
||||
}
|
||||
|
||||
String _monthStr(int m) {
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
return months[m - 1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,21 @@ class StatisticsTab extends StatefulWidget {
|
||||
class _StatisticsTabState extends State<StatisticsTab> {
|
||||
String _selectedNature = 'EXPENSE';
|
||||
|
||||
Widget _buildTotalCard(String title, double amount, Color color, IconData icon) {
|
||||
Widget _buildTotalCard(
|
||||
String title,
|
||||
double amount,
|
||||
Color color,
|
||||
IconData icon,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2))),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -51,13 +61,35 @@ class _StatisticsTabState extends State<StatisticsTab> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(backgroundColor: color.withOpacity(0.1), radius: 16, child: Icon(icon, color: color, size: 16)),
|
||||
CircleAvatar(
|
||||
backgroundColor: color.withOpacity(0.1),
|
||||
radius: 16,
|
||||
child: Icon(icon, color: color, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(title, style: const TextStyle(fontSize: 12, color: Colors.grey, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('Rs. ${amount.toStringAsFixed(0)}', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: color)),
|
||||
Text(
|
||||
'Rs. ${amount.toStringAsFixed(0)}',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -67,9 +99,23 @@ class _StatisticsTabState extends State<StatisticsTab> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Compute totals
|
||||
final totalIncome = widget.transactions.where((t) => t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null)).fold(0.0, (s, t) => s + t.amount);
|
||||
final totalExpense = widget.transactions.where((t) => t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null)).fold(0.0, (s, t) => s + t.amount);
|
||||
final totalInvestment = widget.transactions.where((t) => t.type == 'INVESTMENT').fold(0.0, (s, t) => s + t.amount);
|
||||
final totalIncome = widget.transactions
|
||||
.where(
|
||||
(t) =>
|
||||
t.type == 'INCOME' ||
|
||||
(t.type == 'TRANSFER' && t.fromWalletId == null),
|
||||
)
|
||||
.fold(0.0, (s, t) => s + t.amount);
|
||||
final totalExpense = widget.transactions
|
||||
.where(
|
||||
(t) =>
|
||||
t.type == 'EXPENSE' ||
|
||||
(t.type == 'TRANSFER' && t.toWalletId == null),
|
||||
)
|
||||
.fold(0.0, (s, t) => s + t.amount);
|
||||
final totalInvestment = widget.transactions
|
||||
.where((t) => t.type == 'INVESTMENT')
|
||||
.fold(0.0, (s, t) => s + t.amount);
|
||||
|
||||
double totalPayablesPeriod = 0.0;
|
||||
double totalReceivablesPeriod = 0.0;
|
||||
@@ -124,10 +170,16 @@ class _StatisticsTabState extends State<StatisticsTab> {
|
||||
),
|
||||
if (widget.selectedFilter == 'Custom')
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
'${widget.startDate.day} ${widget.startDate.month} - ${widget.endDate.day} ${widget.endDate.month}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -139,9 +191,18 @@ class _StatisticsTabState extends State<StatisticsTab> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Analytics', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'Analytics',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: barColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -150,13 +211,31 @@ class _StatisticsTabState extends State<StatisticsTab> {
|
||||
value: _selectedNature,
|
||||
underline: const SizedBox(),
|
||||
icon: Icon(Icons.arrow_drop_down, color: barColor),
|
||||
style: TextStyle(color: barColor, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
color: barColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'EXPENSE', child: Text('Expense')),
|
||||
DropdownMenuItem(value: 'INCOME', child: Text('Income')),
|
||||
DropdownMenuItem(value: 'PAYABLES', child: Text('Payables')),
|
||||
DropdownMenuItem(value: 'RECEIVABLES', child: Text('Receivables')),
|
||||
DropdownMenuItem(value: 'INVESTMENTS', child: Text('Investments')),
|
||||
DropdownMenuItem(
|
||||
value: 'EXPENSE',
|
||||
child: Text('Expense'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'INCOME',
|
||||
child: Text('Income'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'PAYABLES',
|
||||
child: Text('Payables'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'RECEIVABLES',
|
||||
child: Text('Receivables'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'INVESTMENTS',
|
||||
child: Text('Investments'),
|
||||
),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
@@ -172,10 +251,24 @@ class _StatisticsTabState extends State<StatisticsTab> {
|
||||
if (widget.transactions.isEmpty)
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24), side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(Theme.of(context).brightness == Brightness.dark ? 0.1 : 0.2))),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor.withOpacity(
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? 0.1
|
||||
: 0.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: Center(child: Text('No transactions in this period.', style: TextStyle(color: Colors.grey))),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'No transactions in this period.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
@@ -195,7 +288,10 @@ class _StatisticsTabState extends State<StatisticsTab> {
|
||||
],
|
||||
|
||||
const SizedBox(height: 32),
|
||||
const Text('Summary Totals', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'Summary Totals',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
GridView.count(
|
||||
@@ -206,11 +302,36 @@ class _StatisticsTabState extends State<StatisticsTab> {
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 1.5,
|
||||
children: [
|
||||
_buildTotalCard('Income', totalIncome, NatureColors.getColor('INCOME'), LucideIcons.arrowDown),
|
||||
_buildTotalCard('Expense', totalExpense, NatureColors.getColor('EXPENSE'), LucideIcons.arrowUp),
|
||||
_buildTotalCard('Investment', totalInvestment, NatureColors.getColor('INVESTMENTS'), LucideIcons.trendingUp),
|
||||
_buildTotalCard('Payables', totalPayablesPeriod, NatureColors.getColor('PAYABLES'), LucideIcons.alertCircle),
|
||||
_buildTotalCard('Receivables', totalReceivablesPeriod, NatureColors.getColor('RECEIVABLES'), LucideIcons.arrowDownLeft),
|
||||
_buildTotalCard(
|
||||
'Income',
|
||||
totalIncome,
|
||||
NatureColors.getColor('INCOME'),
|
||||
LucideIcons.arrowDown,
|
||||
),
|
||||
_buildTotalCard(
|
||||
'Expense',
|
||||
totalExpense,
|
||||
NatureColors.getColor('EXPENSE'),
|
||||
LucideIcons.arrowUp,
|
||||
),
|
||||
_buildTotalCard(
|
||||
'Investment',
|
||||
totalInvestment,
|
||||
NatureColors.getColor('INVESTMENTS'),
|
||||
LucideIcons.trendingUp,
|
||||
),
|
||||
_buildTotalCard(
|
||||
'Payables',
|
||||
totalPayablesPeriod,
|
||||
NatureColors.getColor('PAYABLES'),
|
||||
LucideIcons.alertCircle,
|
||||
),
|
||||
_buildTotalCard(
|
||||
'Receivables',
|
||||
totalReceivablesPeriod,
|
||||
NatureColors.getColor('RECEIVABLES'),
|
||||
LucideIcons.arrowDownLeft,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
@@ -22,10 +22,7 @@ class CarouselItemData {
|
||||
class SwipeableAccountCard extends StatefulWidget {
|
||||
final List<CarouselItemData> items;
|
||||
|
||||
const SwipeableAccountCard({
|
||||
super.key,
|
||||
required this.items,
|
||||
});
|
||||
const SwipeableAccountCard({super.key, required this.items});
|
||||
|
||||
@override
|
||||
State<SwipeableAccountCard> createState() => _SwipeableAccountCardState();
|
||||
@@ -71,14 +68,25 @@ class _SwipeableAccountCardState extends State<SwipeableAccountCard> {
|
||||
return GestureDetector(
|
||||
onTap: item.onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2)),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).dividerColor.withOpacity(isDark ? 0.1 : 0.2),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(isDark ? 0.2 : 0.02), blurRadius: 8, offset: const Offset(0, 2))
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(isDark ? 0.2 : 0.02),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
@@ -89,11 +97,7 @@ class _SwipeableAccountCardState extends State<SwipeableAccountCard> {
|
||||
color: item.color.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
item.icon,
|
||||
color: item.color,
|
||||
size: 20,
|
||||
),
|
||||
child: Icon(item.icon, color: item.color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@@ -103,7 +107,10 @@ class _SwipeableAccountCardState extends State<SwipeableAccountCard> {
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -111,17 +118,24 @@ class _SwipeableAccountCardState extends State<SwipeableAccountCard> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item.subtitle,
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 12,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
]
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Rs. ${item.amount.toStringAsFixed(0)}',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Theme.of(context).textTheme.bodyLarge?.color),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -63,7 +63,7 @@ class UpcomingDuesWidget extends ConsumerWidget {
|
||||
} else if (w.paymentCycle == 'YEARLY') {
|
||||
// Assume cycleDate is day of current month, this is simplistic
|
||||
if (now.day > w.cycleDate!) {
|
||||
year++;
|
||||
year++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@ class UpcomingDuesWidget extends ConsumerWidget {
|
||||
|
||||
dueDate = DateTime(year, month, day);
|
||||
|
||||
int daysLeft = dueDate.difference(DateTime(now.year, now.month, now.day)).inDays;
|
||||
int daysLeft = dueDate
|
||||
.difference(DateTime(now.year, now.month, now.day))
|
||||
.inDays;
|
||||
|
||||
if (daysLeft == 0) {
|
||||
dueLabel = 'Due Today';
|
||||
@@ -108,9 +110,18 @@ class UpcomingDuesWidget extends ConsumerWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.calendarClock, color: Color(0xFF6C63FF), size: 20),
|
||||
const Icon(
|
||||
LucideIcons.calendarClock,
|
||||
color: Color(0xFF6C63FF),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('Upcoming Dues', style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
'Upcoming Dues',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -127,11 +138,26 @@ class UpcomingDuesWidget extends ConsumerWidget {
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isUrgent ? (isDark ? Colors.red.withOpacity(0.2) : Colors.red.shade50) : Theme.of(context).cardColor,
|
||||
color: isUrgent
|
||||
? (isDark ? Colors.red.withOpacity(0.2) : Colors.red.shade50)
|
||||
: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: isUrgent ? (isDark ? Colors.red.withOpacity(0.5) : Colors.red.shade200) : Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2)),
|
||||
border: Border.all(
|
||||
color: isUrgent
|
||||
? (isDark
|
||||
? Colors.red.withOpacity(0.5)
|
||||
: Colors.red.shade200)
|
||||
: Theme.of(
|
||||
context,
|
||||
).dividerColor.withOpacity(isDark ? 0.1 : 0.2),
|
||||
),
|
||||
boxShadow: [
|
||||
if (!isUrgent) BoxShadow(color: Colors.black.withOpacity(isDark ? 0.2 : 0.02), blurRadius: 8, offset: const Offset(0, 2))
|
||||
if (!isUrgent)
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(isDark ? 0.2 : 0.02),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
@@ -139,13 +165,22 @@ class UpcomingDuesWidget extends ConsumerWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: (isUrgent ? Colors.red : NatureColors.getColor('PAYABLES')).withOpacity(0.1),
|
||||
color:
|
||||
(isUrgent
|
||||
? Colors.red
|
||||
: NatureColors.getColor('PAYABLES'))
|
||||
.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
w.subNature == 'CREDIT_CARD' ? LucideIcons.creditCard :
|
||||
(w.subNature == 'LOAN_EMI' ? LucideIcons.home : LucideIcons.fileText),
|
||||
color: isUrgent ? Colors.red : NatureColors.getColor('PAYABLES'),
|
||||
w.subNature == 'CREDIT_CARD'
|
||||
? LucideIcons.creditCard
|
||||
: (w.subNature == 'LOAN_EMI'
|
||||
? LucideIcons.home
|
||||
: LucideIcons.fileText),
|
||||
color: isUrgent
|
||||
? Colors.red
|
||||
: NatureColors.getColor('PAYABLES'),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
@@ -154,14 +189,25 @@ class UpcomingDuesWidget extends ConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(w.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: Theme.of(context).textTheme.bodyLarge?.color)),
|
||||
Text(
|
||||
w.name,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$label • ${DateFormat('MMM dd').format(dueDate)}',
|
||||
style: TextStyle(
|
||||
color: isUrgent ? Colors.red.shade700 : Colors.grey.shade600,
|
||||
color: isUrgent
|
||||
? Colors.red.shade700
|
||||
: Colors.grey.shade600,
|
||||
fontSize: 12,
|
||||
fontWeight: isUrgent ? FontWeight.bold : FontWeight.normal
|
||||
fontWeight: isUrgent
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -172,7 +218,9 @@ class UpcomingDuesWidget extends ConsumerWidget {
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: isUrgent ? (isDark ? Colors.redAccent : Colors.red.shade700) : Theme.of(context).textTheme.bodyLarge?.color
|
||||
color: isUrgent
|
||||
? (isDark ? Colors.redAccent : Colors.red.shade700)
|
||||
: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -15,7 +15,9 @@ final insightProvider = Provider<Insight?>((ref) {
|
||||
final budgetsState = ref.watch(budgetProvider);
|
||||
final categoriesState = ref.watch(categoryProvider);
|
||||
|
||||
if (!transState.hasValue || !budgetsState.hasValue || !categoriesState.hasValue) {
|
||||
if (!transState.hasValue ||
|
||||
!budgetsState.hasValue ||
|
||||
!categoriesState.hasValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -24,17 +26,23 @@ final insightProvider = Provider<Insight?>((ref) {
|
||||
final categories = categoriesState.value!;
|
||||
final now = DateTime.now();
|
||||
|
||||
final currentMonthTxs = transactions.where((t) => t.date.month == now.month && t.date.year == now.year).toList();
|
||||
final currentMonthTxs = transactions
|
||||
.where((t) => t.date.month == now.month && t.date.year == now.year)
|
||||
.toList();
|
||||
|
||||
final totalIncome = currentMonthTxs.where((t) => t.type == 'INCOME').fold(0.0, (s, t) => s + t.amount);
|
||||
final totalExpense = currentMonthTxs.where((t) => t.type == 'EXPENSE').fold(0.0, (s, t) => s + t.amount);
|
||||
final totalIncome = currentMonthTxs
|
||||
.where((t) => t.type == 'INCOME')
|
||||
.fold(0.0, (s, t) => s + t.amount);
|
||||
final totalExpense = currentMonthTxs
|
||||
.where((t) => t.type == 'EXPENSE')
|
||||
.fold(0.0, (s, t) => s + t.amount);
|
||||
|
||||
// Rule 1: Expenses > Income
|
||||
if (totalExpense > totalIncome && totalIncome > 0) {
|
||||
return Insight(
|
||||
'Spending Alert',
|
||||
'You have spent more than you earned this month! (Rs. ${totalExpense.toStringAsFixed(0)} vs Rs. ${totalIncome.toStringAsFixed(0)})',
|
||||
'WARNING'
|
||||
'WARNING',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,18 +53,28 @@ final insightProvider = Provider<Insight?>((ref) {
|
||||
.fold(0.0, (s, t) => s + t.amount);
|
||||
|
||||
if (spent > budget.monthlyLimit) {
|
||||
final categoryName = categories.firstWhere((c) => c.id == budget.categoryId, orElse: () => Category(id: 0, name: 'Unknown')).name;
|
||||
final categoryName = categories
|
||||
.firstWhere(
|
||||
(c) => c.id == budget.categoryId,
|
||||
orElse: () => Category(id: 0, name: 'Unknown'),
|
||||
)
|
||||
.name;
|
||||
return Insight(
|
||||
'Budget Exceeded',
|
||||
'You have exceeded your $categoryName budget by Rs. ${(spent - budget.monthlyLimit).toStringAsFixed(0)}.',
|
||||
'WARNING'
|
||||
'WARNING',
|
||||
);
|
||||
} else if (spent > budget.monthlyLimit * 0.9) {
|
||||
final categoryName = categories.firstWhere((c) => c.id == budget.categoryId, orElse: () => Category(id: 0, name: 'Unknown')).name;
|
||||
final categoryName = categories
|
||||
.firstWhere(
|
||||
(c) => c.id == budget.categoryId,
|
||||
orElse: () => Category(id: 0, name: 'Unknown'),
|
||||
)
|
||||
.name;
|
||||
return Insight(
|
||||
'Nearing Budget Limit',
|
||||
'Careful! You have used ${(spent / budget.monthlyLimit * 100).toStringAsFixed(0)}% of your $categoryName budget.',
|
||||
'WARNING'
|
||||
'WARNING',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,18 +84,26 @@ final insightProvider = Provider<Insight?>((ref) {
|
||||
final Map<int, double> spentByCategory = {};
|
||||
for (var t in currentMonthTxs.where((t) => t.type == 'EXPENSE')) {
|
||||
if (t.categoryId != null) {
|
||||
spentByCategory[t.categoryId!] = (spentByCategory[t.categoryId!] ?? 0) + t.amount;
|
||||
spentByCategory[t.categoryId!] =
|
||||
(spentByCategory[t.categoryId!] ?? 0) + t.amount;
|
||||
}
|
||||
}
|
||||
|
||||
if (spentByCategory.isNotEmpty) {
|
||||
final maxEntry = spentByCategory.entries.reduce((a, b) => a.value > b.value ? a : b);
|
||||
final maxEntry = spentByCategory.entries.reduce(
|
||||
(a, b) => a.value > b.value ? a : b,
|
||||
);
|
||||
if (maxEntry.value > totalExpense * 0.4) {
|
||||
final categoryName = categories.firstWhere((c) => c.id == maxEntry.key, orElse: () => Category(id: 0, name: 'Unknown')).name;
|
||||
final categoryName = categories
|
||||
.firstWhere(
|
||||
(c) => c.id == maxEntry.key,
|
||||
orElse: () => Category(id: 0, name: 'Unknown'),
|
||||
)
|
||||
.name;
|
||||
return Insight(
|
||||
'High Concentration',
|
||||
'${(maxEntry.value / totalExpense * 100).toStringAsFixed(0)}% of your expenses this month went to $categoryName.',
|
||||
'INFO'
|
||||
'INFO',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -88,13 +114,13 @@ final insightProvider = Provider<Insight?>((ref) {
|
||||
return Insight(
|
||||
'Great Job!',
|
||||
'You have saved over 50% of your income this month. Keep it up!',
|
||||
'SUCCESS'
|
||||
'SUCCESS',
|
||||
);
|
||||
}
|
||||
|
||||
return Insight(
|
||||
'On Track',
|
||||
'Your finances are looking stable this month.',
|
||||
'SUCCESS'
|
||||
'SUCCESS',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -31,7 +31,8 @@ class CategoryRateHistory {
|
||||
'id': id,
|
||||
'categoryId': categoryId,
|
||||
'rate': rate,
|
||||
'date': "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}",
|
||||
'date':
|
||||
"${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}",
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ class InventoryItem {
|
||||
final int? branchId;
|
||||
final String? purchaseRef;
|
||||
final String? status;
|
||||
final DateTime? createdAt;
|
||||
|
||||
InventoryItem({
|
||||
this.id,
|
||||
@@ -47,6 +48,7 @@ class InventoryItem {
|
||||
this.branchId,
|
||||
this.purchaseRef,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
factory InventoryItem.fromJson(Map<String, dynamic> json) {
|
||||
@@ -74,6 +76,7 @@ class InventoryItem {
|
||||
branchId: json['branchId'],
|
||||
purchaseRef: json['purchaseRef'],
|
||||
status: json['status'],
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,9 @@ class Product {
|
||||
final String? sku;
|
||||
final String? barcode;
|
||||
final String? description;
|
||||
final double? purchasePrice;
|
||||
final double? sellingPrice;
|
||||
final double? minStock;
|
||||
final double? reorderLevel;
|
||||
final double? gstRate;
|
||||
final String? dimensions;
|
||||
final double? weight;
|
||||
final String? color;
|
||||
final String? size;
|
||||
final String priceCalcRule;
|
||||
@@ -24,7 +20,6 @@ class Product {
|
||||
final double? makingCharges;
|
||||
final String? makingChargesType;
|
||||
final double? wastagePercentage;
|
||||
final bool trackInventory;
|
||||
final bool isActive;
|
||||
final List<int> imageIds;
|
||||
final double? currentStock;
|
||||
@@ -39,13 +34,9 @@ class Product {
|
||||
this.sku,
|
||||
this.barcode,
|
||||
this.description,
|
||||
this.purchasePrice,
|
||||
this.sellingPrice,
|
||||
this.minStock,
|
||||
this.reorderLevel,
|
||||
this.gstRate,
|
||||
this.dimensions,
|
||||
this.weight,
|
||||
this.color,
|
||||
this.size,
|
||||
this.priceCalcRule = 'MANUAL',
|
||||
@@ -54,7 +45,6 @@ class Product {
|
||||
this.makingCharges = 0.0,
|
||||
this.makingChargesType = 'FLAT',
|
||||
this.wastagePercentage = 0.0,
|
||||
this.trackInventory = true,
|
||||
this.isActive = true,
|
||||
this.imageIds = const [],
|
||||
this.currentStock = 0.0,
|
||||
@@ -71,27 +61,36 @@ class Product {
|
||||
sku: json['sku'],
|
||||
barcode: json['barcode'],
|
||||
description: json['description'],
|
||||
purchasePrice: (json['purchasePrice'] ?? json['purchase_price'] as num?)?.toDouble(),
|
||||
sellingPrice: (json['sellingPrice'] ?? json['selling_price'] as num?)?.toDouble(),
|
||||
minStock: (json['minStock'] ?? json['min_stock'] as num?)?.toDouble(),
|
||||
reorderLevel: (json['reorderLevel'] ?? json['reorder_level'] as num?)?.toDouble(),
|
||||
sellingPrice: (json['sellingPrice'] ?? json['selling_price'] as num?)
|
||||
?.toDouble(),
|
||||
gstRate: (json['gstRate'] ?? json['gst_rate'] as num?)?.toDouble(),
|
||||
dimensions: json['dimensions'],
|
||||
weight: (json['weight'] as num?)?.toDouble(),
|
||||
color: json['color'],
|
||||
size: json['size'],
|
||||
priceCalcRule: json['priceCalcRule'] ?? json['price_calc_rule'] ?? 'MANUAL',
|
||||
autoCalculatePrice: json['autoCalculatePrice'] ?? json['auto_calculate_price'] ?? false,
|
||||
purityFactor: (json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble() ?? 1.0,
|
||||
makingCharges: (json['makingCharges'] ?? json['making_charges'] as num?)?.toDouble() ?? 0.0,
|
||||
makingChargesType: json['makingChargesType'] ?? json['making_charges_type'] ?? 'FLAT',
|
||||
wastagePercentage: (json['wastagePercentage'] ?? json['wastage_percentage'] as num?)?.toDouble() ?? 0.0,
|
||||
trackInventory: json['trackInventory'] ?? json['track_inventory'] ?? true,
|
||||
priceCalcRule:
|
||||
json['priceCalcRule'] ?? json['price_calc_rule'] ?? 'MANUAL',
|
||||
autoCalculatePrice:
|
||||
json['autoCalculatePrice'] ?? json['auto_calculate_price'] ?? false,
|
||||
purityFactor:
|
||||
(json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble() ??
|
||||
1.0,
|
||||
makingCharges:
|
||||
(json['makingCharges'] ?? json['making_charges'] as num?)
|
||||
?.toDouble() ??
|
||||
0.0,
|
||||
makingChargesType:
|
||||
json['makingChargesType'] ?? json['making_charges_type'] ?? 'FLAT',
|
||||
wastagePercentage:
|
||||
(json['wastagePercentage'] ?? json['wastage_percentage'] as num?)
|
||||
?.toDouble() ??
|
||||
0.0,
|
||||
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
||||
imageIds: json['images'] != null
|
||||
? (json['images'] as List).map((i) => i['id'] as int).toList()
|
||||
: [],
|
||||
currentStock: (json['currentStock'] ?? json['current_stock'] as num?)?.toDouble() ?? 0.0,
|
||||
currentStock:
|
||||
(json['currentStock'] ?? json['current_stock'] as num?)?.toDouble() ??
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,13 +105,9 @@ class Product {
|
||||
'sku': sku,
|
||||
'barcode': barcode,
|
||||
'description': description,
|
||||
'purchasePrice': purchasePrice,
|
||||
'sellingPrice': sellingPrice,
|
||||
'minStock': minStock,
|
||||
'reorderLevel': reorderLevel,
|
||||
'gstRate': gstRate,
|
||||
'dimensions': dimensions,
|
||||
'weight': weight,
|
||||
'color': color,
|
||||
'size': size,
|
||||
'priceCalcRule': priceCalcRule,
|
||||
@@ -121,7 +116,6 @@ class Product {
|
||||
'makingCharges': makingCharges,
|
||||
'makingChargesType': makingChargesType,
|
||||
'wastagePercentage': wastagePercentage,
|
||||
'trackInventory': trackInventory,
|
||||
'isActive': isActive,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ class ProductBom {
|
||||
parentProductId: json['parentProductId'],
|
||||
componentProductId: json['componentProductId'],
|
||||
quantity: (json['quantity'] as num).toDouble(),
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
: null,
|
||||
componentProduct: json['componentProduct'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,22 @@ class StockMovementItem {
|
||||
final double quantity;
|
||||
final double? unitPrice;
|
||||
|
||||
final String? huid;
|
||||
final double? weight;
|
||||
final String? photoUrl;
|
||||
final double? purchaseAmount;
|
||||
|
||||
StockMovementItem({
|
||||
this.id,
|
||||
this.movementId,
|
||||
this.productId,
|
||||
required this.quantity,
|
||||
this.unitPrice,
|
||||
|
||||
this.huid,
|
||||
this.weight,
|
||||
this.photoUrl,
|
||||
this.purchaseAmount,
|
||||
});
|
||||
|
||||
factory StockMovementItem.fromJson(Map<String, dynamic> json) {
|
||||
@@ -20,6 +30,11 @@ class StockMovementItem {
|
||||
productId: json['productId'],
|
||||
quantity: (json['quantity'] as num).toDouble(),
|
||||
unitPrice: (json['unitPrice'] as num?)?.toDouble(),
|
||||
|
||||
huid: json['huid'],
|
||||
weight: json['weight'] != null ? (json['weight'] as num).toDouble() : null,
|
||||
photoUrl: json['photoUrl'],
|
||||
purchaseAmount: json['purchaseAmount'] != null ? (json['purchaseAmount'] as num).toDouble() : null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +45,11 @@ class StockMovementItem {
|
||||
'productId': productId,
|
||||
'quantity': quantity,
|
||||
'unitPrice': unitPrice,
|
||||
|
||||
'huid': huid,
|
||||
'weight': weight,
|
||||
'photoUrl': photoUrl,
|
||||
'purchaseAmount': purchaseAmount,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -38,7 +58,8 @@ class StockMovement {
|
||||
final int? id;
|
||||
final int? userId;
|
||||
final int? locationId;
|
||||
final String type; // OPENING, ADDITION, REDUCTION, ADJUSTMENT, DAMAGE, SALE, RETURN
|
||||
final String
|
||||
type; // OPENING, ADDITION, REDUCTION, ADJUSTMENT, DAMAGE, SALE, RETURN
|
||||
final int? referenceTransactionId;
|
||||
final String? notes;
|
||||
final DateTime? createdAt;
|
||||
@@ -63,9 +84,13 @@ class StockMovement {
|
||||
type: json['type'],
|
||||
referenceTransactionId: json['referenceTransactionId'],
|
||||
notes: json['notes'],
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
: null,
|
||||
items: json['items'] != null
|
||||
? (json['items'] as List).map((i) => StockMovementItem.fromJson(i)).toList()
|
||||
? (json['items'] as List)
|
||||
.map((i) => StockMovementItem.fromJson(i))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
class UnitOfMeasure {
|
||||
final int? id;
|
||||
final String name;
|
||||
final String? abbreviation;
|
||||
|
||||
UnitOfMeasure({
|
||||
this.id,
|
||||
required this.name,
|
||||
this.abbreviation,
|
||||
});
|
||||
|
||||
factory UnitOfMeasure.fromJson(Map<String, dynamic> json) {
|
||||
return UnitOfMeasure(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
abbreviation: json['abbreviation'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'abbreviation': abbreviation,
|
||||
};
|
||||
}
|
||||
|
||||
UnitOfMeasure copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
String? abbreviation,
|
||||
}) {
|
||||
return UnitOfMeasure(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
abbreviation: abbreviation ?? this.abbreviation,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as import_storage;
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../../../../core/widgets/smart_search_dropdown.dart';
|
||||
import '../domain/product.dart';
|
||||
import '../providers/products_provider.dart';
|
||||
import '../providers/product_categories_provider.dart';
|
||||
import '../providers/uoms_provider.dart';
|
||||
import '../domain/uom.dart';
|
||||
import '../../business/providers/business_provider.dart';
|
||||
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
||||
|
||||
@@ -24,36 +21,18 @@ class AddProductScreen extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
final int _totalPages = 4;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _hsnController = TextEditingController();
|
||||
final _gstController = TextEditingController();
|
||||
|
||||
// Basic
|
||||
String _name = '';
|
||||
String _sku = '';
|
||||
String _hsnCode = '';
|
||||
ProductCategory? _selectedCategory;
|
||||
int? _uomId;
|
||||
|
||||
// Properties
|
||||
String _color = '';
|
||||
String _size = '';
|
||||
String _dimensions = '';
|
||||
double _weight = 0;
|
||||
|
||||
// Pricing & Inventory
|
||||
double _purchasePrice = 0;
|
||||
double _sellingPrice = 0;
|
||||
double _gstRate = 0;
|
||||
bool _trackInventory = true;
|
||||
bool _autoCalculatePrice = false;
|
||||
|
||||
// Advanced Commodity Fields
|
||||
double _purityFactor = 1.0;
|
||||
double _makingCharges = 0;
|
||||
String _makingChargesType = 'FLAT'; // FLAT, PER_UNIT, PERCENTAGE
|
||||
double _wastagePercentage = 0;
|
||||
|
||||
// Media
|
||||
final List<XFile> _images = [];
|
||||
@@ -72,21 +51,10 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
final p = widget.product!;
|
||||
_name = p.name;
|
||||
_sku = p.sku ?? '';
|
||||
_hsnCode = p.hsnCode ?? '';
|
||||
_hsnController.text = p.hsnCode ?? '';
|
||||
_uomId = p.uomId;
|
||||
_color = p.color ?? '';
|
||||
_size = p.size ?? '';
|
||||
_dimensions = p.dimensions ?? '';
|
||||
_weight = p.weight ?? 0;
|
||||
_purchasePrice = p.purchasePrice ?? 0;
|
||||
_sellingPrice = p.sellingPrice ?? 0;
|
||||
_gstRate = p.gstRate ?? 0;
|
||||
_trackInventory = p.trackInventory;
|
||||
_autoCalculatePrice = p.autoCalculatePrice;
|
||||
_purityFactor = p.purityFactor ?? 1.0;
|
||||
_makingCharges = p.makingCharges ?? 0.0;
|
||||
_makingChargesType = p.makingChargesType ?? 'FLAT';
|
||||
_wastagePercentage = p.wastagePercentage ?? 0.0;
|
||||
_gstController.text = p.gstRate != null && p.gstRate! > 0 ? p.gstRate.toString() : '';
|
||||
|
||||
if (p.imageIds.isNotEmpty) {
|
||||
_existingImageIds.addAll(p.imageIds);
|
||||
@@ -103,24 +71,11 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _nextPage() {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (_currentPage < _totalPages - 1) {
|
||||
if (_currentPage == 0 && _selectedCategory == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select a category.')));
|
||||
return;
|
||||
}
|
||||
_pageController.animateToPage(_currentPage + 1, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||
} else {
|
||||
_save();
|
||||
}
|
||||
}
|
||||
|
||||
void _prevPage() {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (_currentPage > 0) {
|
||||
_pageController.animateToPage(_currentPage - 1, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||
}
|
||||
@override
|
||||
void dispose() {
|
||||
_hsnController.dispose();
|
||||
_gstController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _pickImages() async {
|
||||
@@ -167,7 +122,6 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
_formKey.currentState!.save();
|
||||
@@ -182,24 +136,14 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
final product = Product(
|
||||
id: widget.product?.id,
|
||||
name: _name,
|
||||
hsnCode: _hsnCode.isNotEmpty ? _hsnCode : null,
|
||||
hsnCode: _hsnController.text.isNotEmpty ? _hsnController.text : null,
|
||||
sku: _sku.isNotEmpty ? _sku : null,
|
||||
categoryId: _selectedCategory?.id,
|
||||
uomId: _uomId,
|
||||
color: _color.isNotEmpty ? _color : null,
|
||||
size: _size.isNotEmpty ? _size : null,
|
||||
dimensions: _dimensions.isNotEmpty ? _dimensions : null,
|
||||
purchasePrice: _purchasePrice,
|
||||
sellingPrice: _autoCalculatePrice ? _calculateLivePrice() : _sellingPrice,
|
||||
gstRate: _gstRate,
|
||||
weight: _weight,
|
||||
priceCalcRule: _autoCalculatePrice ? 'COMMODITY' : 'MANUAL',
|
||||
autoCalculatePrice: _autoCalculatePrice,
|
||||
purityFactor: _purityFactor,
|
||||
makingCharges: _makingCharges,
|
||||
makingChargesType: _makingChargesType,
|
||||
wastagePercentage: _wastagePercentage,
|
||||
trackInventory: _trackInventory,
|
||||
gstRate: double.tryParse(_gstController.text) ?? 0,
|
||||
priceCalcRule: 'MANUAL',
|
||||
autoCalculatePrice: false,
|
||||
);
|
||||
|
||||
if (widget.product != null) {
|
||||
@@ -215,12 +159,10 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
double _calculateLivePrice() {
|
||||
return _sellingPrice;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categoriesState = ref.watch(productCategoriesProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
appBar: AppBar(
|
||||
@@ -232,23 +174,185 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProgressPills(),
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
onPageChanged: (idx) {
|
||||
FocusScope.of(context).unfocus();
|
||||
setState(() => _currentPage = idx);
|
||||
},
|
||||
children: [
|
||||
_buildBasicStep(),
|
||||
_buildPropertiesStep(),
|
||||
_buildPricingStep(),
|
||||
_buildMediaStep(),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Basic Information', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildPremiumTextField(
|
||||
label: 'Product Name*',
|
||||
initialValue: _name,
|
||||
validator: (val) => val == null || val.isEmpty ? 'Required' : null,
|
||||
onChanged: (val) => _name = val,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPremiumTextField(
|
||||
label: 'SKU / Barcode',
|
||||
initialValue: _sku,
|
||||
onChanged: (val) => _sku = val,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
const Text('Category & Unit', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 12),
|
||||
categoriesState.when(
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (err, stack) => Text('Error loading categories: $err'),
|
||||
data: (categories) {
|
||||
final leafCategories = categories.where((c) => !categories.any((child) => child.parentCategoryId == c.id)).toList();
|
||||
return leafCategories.isEmpty
|
||||
? const Text('No categories found. Manage categories in Business Hub.', style: TextStyle(color: Colors.orange))
|
||||
: SmartSearchDropdown<ProductCategory>(
|
||||
hintText: 'Select Category*',
|
||||
value: _selectedCategory,
|
||||
items: leafCategories,
|
||||
itemAsString: (c) => c.name,
|
||||
itemBuilder: (context, item) {
|
||||
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 Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(path.join(' -> '), style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_selectedCategory = val;
|
||||
if (val != null) {
|
||||
if (val.defaultHsn != null && val.defaultHsn!.isNotEmpty) {
|
||||
_hsnController.text = val.defaultHsn!;
|
||||
}
|
||||
if (val.defaultGst != null && val.defaultGst! > 0) {
|
||||
_gstController.text = val.defaultGst.toString();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
decoration: const InputDecoration(labelText: 'Unit of Measure (Optional)'),
|
||||
value: _uomId,
|
||||
items: const [
|
||||
DropdownMenuItem(value: 1, child: Text('Grams (g)')),
|
||||
DropdownMenuItem(value: 2, child: Text('Kilograms (kg)')),
|
||||
DropdownMenuItem(value: 3, child: Text('Pieces (pcs)')),
|
||||
],
|
||||
onChanged: (val) => setState(() => _uomId = val),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
const Text('Pricing & Taxes', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _hsnController,
|
||||
decoration: const InputDecoration(labelText: 'HSN Code'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _gstController,
|
||||
decoration: const InputDecoration(labelText: 'GST Rate (%)', suffixText: '%'),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
const Text('Product Images', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
if (_images.isNotEmpty || _existingImageIds.isNotEmpty)
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2, crossAxisSpacing: 16, mainAxisSpacing: 16, childAspectRatio: 1,
|
||||
),
|
||||
itemCount: _images.length + _existingImageIds.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < _existingImageIds.length) {
|
||||
final imageId = _existingImageIds[index];
|
||||
final imageUrl = '${DioClient().dio.options.baseUrl}/inventory/products/${widget.product!.id}/images/$imageId/content';
|
||||
return _buildImageThumbnail(
|
||||
isNetwork: true,
|
||||
url: imageUrl,
|
||||
onTap: () => _previewImage(index),
|
||||
onDelete: () async {
|
||||
try {
|
||||
setState(() => _isSaving = true);
|
||||
await DioClient().dio.delete('/inventory/products/images/$imageId');
|
||||
setState(() => _existingImageIds.removeAt(index));
|
||||
ref.invalidate(productsProvider);
|
||||
} catch (e) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to delete: $e')));
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
final localIndex = index - _existingImageIds.length;
|
||||
return _buildImageThumbnail(
|
||||
isNetwork: false,
|
||||
file: File(_images[localIndex].path),
|
||||
onTap: () => _previewImage(index),
|
||||
onDelete: () => _removeImage(localIndex),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if ((_images.length + _existingImageIds.length) < 4) ...[
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: _pickImages,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(LucideIcons.uploadCloud, size: 32, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Tap to Upload Images', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('${4 - (_images.length + _existingImageIds.length)} slots remaining', style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -259,476 +363,28 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressPills() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
children: List.generate(_totalPages, (index) {
|
||||
bool isActive = index <= _currentPage;
|
||||
return Expanded(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: EdgeInsets.only(right: index == _totalPages - 1 ? 0 : 8),
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Theme.of(context).colorScheme.primary : Colors.grey.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (_currentPage > 0)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: OutlinedButton(
|
||||
onPressed: _isSaving ? null : _prevPage,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
child: const Icon(LucideIcons.chevronLeft),
|
||||
),
|
||||
),
|
||||
if (_currentPage > 0) const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isSaving ? null : _nextPage,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 4,
|
||||
shadowColor: Theme.of(context).colorScheme.primary.withOpacity(0.4),
|
||||
),
|
||||
child: _isSaving
|
||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: Text(
|
||||
_currentPage == _totalPages - 1
|
||||
? (widget.product != null ? 'Update Product' : 'Publish Product')
|
||||
: 'Continue',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isSaving ? null : _save,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 4,
|
||||
),
|
||||
],
|
||||
child: _isSaving
|
||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: Text(widget.product != null ? 'Update Product' : 'Publish Product', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ==== STEPS ====
|
||||
|
||||
Widget _buildBasicStep() {
|
||||
final categoriesState = ref.watch(productCategoriesProvider);
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Basic Information', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Let\'s start with the core details of your product.', style: TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_buildPremiumTextField(
|
||||
label: 'Product Name*',
|
||||
initialValue: _name,
|
||||
validator: (val) => val == null || val.isEmpty ? 'Required' : null,
|
||||
onChanged: (val) => _name = val,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildPremiumTextField(
|
||||
label: 'SKU / Barcode',
|
||||
initialValue: _sku,
|
||||
onChanged: (val) => _sku = val,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
const Text('Category', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 12),
|
||||
categoriesState.when(
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (err, stack) => Text('Error loading categories: $err'),
|
||||
data: (categories) {
|
||||
final leafCategories = categories.where((c) => !categories.any((child) => child.parentCategoryId == c.id)).toList();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (leafCategories.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(16)),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(LucideIcons.alertCircle, color: Colors.orange),
|
||||
SizedBox(width: 12),
|
||||
Expanded(child: Text('No categories found. Manage categories in Business Hub.', style: TextStyle(color: Colors.orange))),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
SmartSearchDropdown<ProductCategory>(
|
||||
hintText: 'Select Category*',
|
||||
value: _selectedCategory,
|
||||
items: leafCategories,
|
||||
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;
|
||||
}
|
||||
}
|
||||
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: 32),
|
||||
|
||||
const Text('Unit of Measure', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 12),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final uomsState = ref.watch(uomsProvider);
|
||||
return uomsState.when(
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (err, stack) => Text('Error loading UOMs: $err'),
|
||||
data: (uoms) {
|
||||
return SmartSearchDropdown<int>(
|
||||
hintText: 'Select Unit of Measure (Optional)',
|
||||
value: _uomId,
|
||||
items: uoms.map((u) => u.id!).toList(),
|
||||
itemAsString: (id) {
|
||||
final uom = uoms.firstWhere((u) => u.id == id);
|
||||
return '${uom.name} (${uom.abbreviation})';
|
||||
},
|
||||
onChanged: (val) => setState(() => _uomId = val),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPropertiesStep() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Properties & Attributes', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Define the physical characteristics.', style: TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_buildPremiumTextField(
|
||||
label: 'Weight',
|
||||
initialValue: _weight == 0 ? '' : _weight.toString(),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (val) => setState(() => _weight = double.tryParse(val) ?? 0),
|
||||
suffixText: _getUomAbbreviation() ?? _selectedCategory?.baseUnit ?? 'unit',
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildPremiumTextField(
|
||||
label: 'Color',
|
||||
initialValue: _color,
|
||||
onChanged: (val) => _color = val,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildPremiumTextField(
|
||||
label: 'Size',
|
||||
initialValue: _size,
|
||||
onChanged: (val) => _size = val,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildPremiumTextField(
|
||||
label: 'Dimensions',
|
||||
initialValue: _dimensions,
|
||||
onChanged: (val) => _dimensions = val,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPricingStep() {
|
||||
final taxInclusive = ref.watch(businessProfileProvider).value?.taxIncludedInPrice ?? false;
|
||||
final isCommodity = false;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Pricing & Inventory', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Set up costs, pricing rules, and tracking.', style: TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
if (isCommodity) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: [Colors.blue.shade800, Colors.blue.shade500]),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [BoxShadow(color: Colors.blue.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4))],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.activity, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Commodity Pricing', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: _autoCalculatePrice,
|
||||
activeColor: Colors.white,
|
||||
onChanged: (val) => setState(() => _autoCalculatePrice = val),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (_autoCalculatePrice) ...[
|
||||
const Divider(color: Colors.white24, height: 32),
|
||||
_buildPremiumTextField(
|
||||
label: 'Purity Factor (e.g. 0.916 for 22K)',
|
||||
initialValue: _purityFactor.toString(),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (val) => setState(() => _purityFactor = double.tryParse(val) ?? 1.0),
|
||||
darkTheme: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPremiumTextField(
|
||||
label: 'Wastage Percentage (%)',
|
||||
initialValue: _wastagePercentage.toString(),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (val) => setState(() => _wastagePercentage = double.tryParse(val) ?? 0.0),
|
||||
darkTheme: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: _buildPremiumTextField(
|
||||
label: 'Making Charges',
|
||||
initialValue: _makingCharges.toString(),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (val) => setState(() => _makingCharges = double.tryParse(val) ?? 0.0),
|
||||
darkTheme: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: _buildPremiumDropdown<String>(
|
||||
label: 'Type',
|
||||
value: _makingChargesType,
|
||||
items: const ['FLAT', 'PER_UNIT', 'PERCENTAGE'],
|
||||
itemLabel: (t) => t == 'PER_UNIT' ? 'Per ${_selectedCategory?.baseUnit ?? "Unit"}' : t == 'PERCENTAGE' ? 'Percentage' : 'Flat',
|
||||
onChanged: (val) => setState(() => _makingChargesType = val!),
|
||||
darkTheme: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(12)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Live Selling Price:', style: TextStyle(color: Colors.white70)),
|
||||
Text('₹${_calculateLivePrice().toStringAsFixed(2)}', style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
|
||||
if (!_autoCalculatePrice) ...[
|
||||
_buildPremiumTextField(
|
||||
label: 'Purchase Price',
|
||||
initialValue: _purchasePrice == 0 ? '' : _purchasePrice.toString(),
|
||||
prefixText: '₹ ',
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (val) => _purchasePrice = double.tryParse(val) ?? 0,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildPremiumTextField(
|
||||
label: taxInclusive ? 'Selling Price (Inc. Tax)*' : 'Selling Price (Exc. Tax)*',
|
||||
initialValue: _sellingPrice == 0 ? '' : _sellingPrice.toString(),
|
||||
prefixText: '₹ ',
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (val) => val == null || val.isEmpty ? 'Required' : null,
|
||||
onChanged: (val) => _sellingPrice = double.tryParse(val) ?? 0,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildPremiumTextField(
|
||||
label: 'HSN Code',
|
||||
initialValue: _hsnCode,
|
||||
onChanged: (val) => _hsnCode = val,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildPremiumTextField(
|
||||
label: 'GST Rate (%)',
|
||||
initialValue: _gstRate == 0 ? '' : _gstRate.toString(),
|
||||
suffixText: '%',
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (val) => _gstRate = double.tryParse(val) ?? 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: SwitchListTile(
|
||||
title: const Text('Track Inventory', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: const Text('Monitor stock levels automatically'),
|
||||
value: _trackInventory,
|
||||
onChanged: (val) => setState(() => _trackInventory = val),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaStep() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Product Images', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Upload up to 4 high-quality images.', style: TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
if (_images.isNotEmpty || _existingImageIds.isNotEmpty)
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: _images.length + _existingImageIds.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < _existingImageIds.length) {
|
||||
final imageId = _existingImageIds[index];
|
||||
final imageUrl = '${DioClient().dio.options.baseUrl}/inventory/products/${widget.product!.id}/images/$imageId/content';
|
||||
return _buildImageThumbnail(
|
||||
isNetwork: true,
|
||||
url: imageUrl,
|
||||
onTap: () => _previewImage(index),
|
||||
onDelete: () async {
|
||||
try {
|
||||
setState(() => _isSaving = true);
|
||||
await DioClient().dio.delete('/inventory/products/images/$imageId');
|
||||
setState(() => _existingImageIds.removeAt(index));
|
||||
// Update product list in background
|
||||
ref.read(productsProvider.notifier).refresh();
|
||||
} catch (e) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to delete image: $e')));
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
final localIndex = index - _existingImageIds.length;
|
||||
return _buildImageThumbnail(
|
||||
isNetwork: false,
|
||||
file: File(_images[localIndex].path),
|
||||
onTap: () => _previewImage(index),
|
||||
onDelete: () => _removeImage(localIndex)
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if ((_images.length + _existingImageIds.length) < 4)
|
||||
GestureDetector(
|
||||
onTap: _pickImages,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3), style: BorderStyle.solid),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(LucideIcons.uploadCloud, size: 48, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Tap to Upload', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
Text('${4 - (_images.length + _existingImageIds.length)} slots remaining', style: const TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageThumbnail({required bool isNetwork, String? url, File? file, required VoidCallback onDelete, required VoidCallback onTap}) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
@@ -737,33 +393,29 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4))],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
color: Colors.grey[200],
|
||||
child: isNetwork
|
||||
? (_token == null
|
||||
? const Icon(LucideIcons.image, color: Colors.grey)
|
||||
: Image.network(
|
||||
url!,
|
||||
fit: BoxFit.cover,
|
||||
headers: {'Authorization': 'Bearer $_token'},
|
||||
errorBuilder: (ctx, err, stack) => const Icon(LucideIcons.imageOff, color: Colors.grey),
|
||||
))
|
||||
: Image.file(
|
||||
file!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
child: isNetwork
|
||||
? (_token == null
|
||||
? const Icon(LucideIcons.image, color: Colors.grey)
|
||||
: Image.network(
|
||||
url!,
|
||||
fit: BoxFit.cover,
|
||||
headers: {'Authorization': 'Bearer $_token'},
|
||||
errorBuilder: (ctx, err, stack) => const Icon(LucideIcons.imageOff, color: Colors.grey),
|
||||
))
|
||||
: Image.file(file!, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: GestureDetector(
|
||||
onTap: onDelete,
|
||||
@@ -778,8 +430,6 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// ==== WIDGET HELPERS ====
|
||||
|
||||
Widget _buildPremiumTextField({
|
||||
required String label,
|
||||
String? initialValue,
|
||||
@@ -789,25 +439,13 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
void Function(String)? onChanged,
|
||||
void Function(String?)? onSaved,
|
||||
String? Function(String?)? validator,
|
||||
bool darkTheme = false,
|
||||
}) {
|
||||
return TextFormField(
|
||||
initialValue: initialValue,
|
||||
style: TextStyle(color: darkTheme ? Colors.white : null),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
labelStyle: TextStyle(color: darkTheme ? Colors.white70 : Colors.grey[600]),
|
||||
prefixText: prefixText,
|
||||
prefixStyle: TextStyle(color: darkTheme ? Colors.white : Colors.black, fontWeight: FontWeight.bold),
|
||||
suffixText: suffixText,
|
||||
suffixStyle: TextStyle(color: darkTheme ? Colors.white70 : Colors.grey),
|
||||
filled: true,
|
||||
fillColor: darkTheme ? Colors.black26 : Colors.grey[100],
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: darkTheme ? Colors.white : Theme.of(context).colorScheme.primary, width: 2)
|
||||
),
|
||||
),
|
||||
keyboardType: keyboardType,
|
||||
onChanged: onChanged,
|
||||
@@ -815,59 +453,4 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPremiumDropdown<T>({
|
||||
required String label,
|
||||
required T? value,
|
||||
required List<T> items,
|
||||
String Function(T)? itemLabel,
|
||||
required void Function(T?) onChanged,
|
||||
bool darkTheme = false,
|
||||
}) {
|
||||
return Theme(
|
||||
data: darkTheme ? Theme.of(context).copyWith(
|
||||
textTheme: Theme.of(context).textTheme.apply(bodyColor: Colors.white, displayColor: Colors.white),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
labelStyle: const TextStyle(color: Colors.white70),
|
||||
filled: true,
|
||||
fillColor: Colors.black26,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2)
|
||||
),
|
||||
)
|
||||
) : Theme.of(context).copyWith(
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
labelStyle: TextStyle(color: Colors.grey[600]),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)
|
||||
),
|
||||
)
|
||||
),
|
||||
child: SmartSearchDropdown<T>(
|
||||
hintText: label,
|
||||
value: value,
|
||||
items: items,
|
||||
itemAsString: (e) => itemLabel != null ? itemLabel(e) : e.toString(),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _getUomAbbreviation() {
|
||||
if (_uomId == null) return null;
|
||||
final uomsState = ref.read(uomsProvider).value;
|
||||
if (uomsState == null) return null;
|
||||
try {
|
||||
final uom = uomsState.firstWhere((u) => u.id == _uomId);
|
||||
return uom.abbreviation;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,17 @@ class _BomTabState extends ConsumerState<BomTab> {
|
||||
Future<void> _loadBom() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final items = await ref.read(productsProvider.notifier).fetchProductBom(widget.productId);
|
||||
final items = await ref
|
||||
.read(productsProvider.notifier)
|
||||
.fetchProductBom(widget.productId);
|
||||
setState(() {
|
||||
_bomItems = items;
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading BOM: $e')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error loading BOM: $e')));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
@@ -42,7 +46,9 @@ class _BomTabState extends ConsumerState<BomTab> {
|
||||
|
||||
void _showAddComponentSheet() {
|
||||
final productsState = ref.read(productsProvider);
|
||||
final availableProducts = (productsState.value ?? []).where((p) => p.id != widget.productId).toList();
|
||||
final availableProducts = (productsState.value ?? [])
|
||||
.where((p) => p.id != widget.productId)
|
||||
.toList();
|
||||
|
||||
Product? selectedProduct;
|
||||
final qtyCtrl = TextEditingController(text: '1');
|
||||
@@ -63,7 +69,10 @@ class _BomTabState extends ConsumerState<BomTab> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Add Component", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
"Add Component",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SmartSearchDropdown<Product>(
|
||||
hintText: 'Select Product',
|
||||
@@ -79,10 +88,11 @@ class _BomTabState extends ConsumerState<BomTab> {
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: qtyCtrl,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Quantity Required',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -100,12 +110,18 @@ class _BomTabState extends ConsumerState<BomTab> {
|
||||
);
|
||||
|
||||
try {
|
||||
await ref.read(productsProvider.notifier).addBomItem(widget.productId, bomItem);
|
||||
await ref
|
||||
.read(productsProvider.notifier)
|
||||
.addBomItem(widget.productId, bomItem);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
_loadBom();
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error adding component: $e')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error adding component: $e'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -121,7 +137,7 @@ class _BomTabState extends ConsumerState<BomTab> {
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -138,7 +154,12 @@ class _BomTabState extends ConsumerState<BomTab> {
|
||||
return Stack(
|
||||
children: [
|
||||
if (_bomItems.isEmpty)
|
||||
const Center(child: Text("No components added yet.", style: TextStyle(color: Colors.grey)))
|
||||
const Center(
|
||||
child: Text(
|
||||
"No components added yet.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.builder(
|
||||
padding: const EdgeInsets.all(16).copyWith(bottom: 80),
|
||||
@@ -147,24 +168,36 @@ class _BomTabState extends ConsumerState<BomTab> {
|
||||
final item = _bomItems[index];
|
||||
final component = allProducts.firstWhere(
|
||||
(p) => p.id == item.componentProductId,
|
||||
orElse: () => Product(name: 'Unknown Product', priceCalcRule: 'MANUAL'),
|
||||
orElse: () =>
|
||||
Product(name: 'Unknown Product', priceCalcRule: 'MANUAL'),
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(component.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
title: Text(
|
||||
component.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text('Quantity Required: ${item.quantity}'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await ref.read(productsProvider.notifier).deleteBomItem(item.id!);
|
||||
await ref
|
||||
.read(productsProvider.notifier)
|
||||
.deleteBomItem(item.id!);
|
||||
_loadBom();
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error deleting component: $e')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error deleting component: $e'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/product_categories_provider.dart';
|
||||
import '../../../core/theme/nature_colors.dart';
|
||||
import '../../business/providers/business_mode_provider.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'dart:ui';
|
||||
|
||||
class CategoryManagementScreen extends ConsumerStatefulWidget {
|
||||
const CategoryManagementScreen({super.key});
|
||||
@@ -13,93 +18,575 @@ class _CategoryManagementScreenState extends ConsumerState<CategoryManagementScr
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categoriesAsync = ref.watch(productCategoriesProvider);
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
appBar: AppBar(
|
||||
title: const Text('Category Management'),
|
||||
title: const Text('Category Management', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
centerTitle: true,
|
||||
),
|
||||
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,
|
||||
body: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
onPressed: () => _showAddCategoryDialog(category),
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search categories...',
|
||||
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: categoriesAsync.when(
|
||||
data: (categories) {
|
||||
Widget content;
|
||||
if (categories.isEmpty) {
|
||||
content = _buildEmptyState();
|
||||
} else {
|
||||
final rootCategories = categories.where((c) => c.parentCategoryId == null).toList();
|
||||
content = ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: rootCategories.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildCategoryNode(rootCategories[index], categories, 0, isDark);
|
||||
},
|
||||
);
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => ref.invalidate(productCategoriesProvider),
|
||||
child: content,
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.expand_more),
|
||||
],
|
||||
),
|
||||
children: children.map((child) => _buildCategoryTile(child, allCategories, depth + 1)).toList(),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _showAddCategorySheet(context, null, ref),
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: const Text('Add Category'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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'),
|
||||
Widget _buildEmptyState() {
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
const SizedBox(height: 150),
|
||||
Icon(LucideIcons.layers, size: 64, color: Colors.grey.withOpacity(0.5)),
|
||||
const SizedBox(height: 16),
|
||||
const Center(child: Text('No categories yet', style: TextStyle(fontSize: 18, color: Colors.grey))),
|
||||
const SizedBox(height: 8),
|
||||
const Center(child: Text('Create your first product category to get started', style: TextStyle(color: Colors.grey))),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryNode(ProductCategory category, List<ProductCategory> allCategories, int depth, bool isDark) {
|
||||
final children = allCategories.where((c) => c.parentCategoryId == category.id).toList();
|
||||
final isLeaf = children.isEmpty && !category.hasChild;
|
||||
|
||||
// A nice frosted glass card
|
||||
return Container(
|
||||
margin: EdgeInsets.only(left: depth * 16.0, bottom: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor.withOpacity(isDark ? 0.4 : 0.8),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isLeaf ? Colors.blue.withOpacity(0.3) : Colors.white.withOpacity(isDark ? 0.05 : 0.5),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(isDark ? 0.2 : 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||
child: isLeaf
|
||||
? ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(LucideIcons.tag, size: 20, color: Colors.blue),
|
||||
),
|
||||
title: Text(category.name, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: category.commodityCode != null
|
||||
? Text('${category.commodityCode} • HSN: ${category.defaultHsn ?? "N/A"}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600))
|
||||
: null,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (category.defaultGst != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text('GST ${category.defaultGst}%',
|
||||
style: const TextStyle(color: Colors.green, fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
_buildCategoryPopupMenu(category, isLeaf),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(LucideIcons.folder, size: 20, color: AppTheme.primaryColor),
|
||||
),
|
||||
title: Text(category.name, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildCategoryPopupMenu(category, isLeaf),
|
||||
const Icon(LucideIcons.chevronDown, size: 20, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
children: children.map((child) => _buildCategoryNode(child, allCategories, depth + 1, isDark)).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryPopupMenu(ProductCategory category, bool isLeaf) {
|
||||
return PopupMenuButton<String>(
|
||||
icon: const Icon(LucideIcons.moreVertical, color: Colors.grey),
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
_showAddCategorySheet(context, category, ref, isEdit: true);
|
||||
} else if (value == 'add_sub') {
|
||||
_showAddCategorySheet(context, category, ref, isEdit: false);
|
||||
} else if (value == 'delete') {
|
||||
if (category.id != null) {
|
||||
ref.read(productCategoriesProvider.notifier).deleteCategory(category.id!);
|
||||
}
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(children: [Icon(LucideIcons.edit, size: 18), SizedBox(width: 8), Text('Edit')]),
|
||||
),
|
||||
if (!isLeaf)
|
||||
const PopupMenuItem(
|
||||
value: 'add_sub',
|
||||
child: Row(children: [Icon(LucideIcons.plus, size: 18), SizedBox(width: 8), Text('Add Subcategory')]),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(children: [Icon(LucideIcons.trash2, size: 18, color: Colors.red), SizedBox(width: 8), Text('Delete', style: TextStyle(color: Colors.red))]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showAddCategorySheet(BuildContext context, ProductCategory? parentOrCategory, WidgetRef ref, {bool isEdit = false}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => CategoryFormSheet(
|
||||
parent: isEdit ? null : parentOrCategory,
|
||||
categoryToEdit: isEdit ? parentOrCategory : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class CategoryFormSheet extends ConsumerStatefulWidget {
|
||||
final ProductCategory? parent;
|
||||
final ProductCategory? categoryToEdit;
|
||||
const CategoryFormSheet({super.key, this.parent, this.categoryToEdit});
|
||||
|
||||
@override
|
||||
ConsumerState<CategoryFormSheet> createState() => _CategoryFormSheetState();
|
||||
}
|
||||
|
||||
class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _hsnController = TextEditingController();
|
||||
final _gstController = TextEditingController();
|
||||
final _makingChargeController = TextEditingController();
|
||||
|
||||
int? _selectedParentId;
|
||||
bool _isLeaf = false;
|
||||
bool _huidRequired = false;
|
||||
String _commodityCode = 'XAU';
|
||||
String _makingChargeType = 'PER_GRAM';
|
||||
String _baseUnit = 'g';
|
||||
|
||||
final List<Map<String, String>> _commodities = [
|
||||
{'code': 'XAU', 'label': 'XAU - Gold'},
|
||||
{'code': 'XAG', 'label': 'XAG - Silver'},
|
||||
{'code': 'XPT', 'label': 'XPT - Platinum'},
|
||||
{'code': 'XPD', 'label': 'XPD - Palladium'},
|
||||
{'code': 'OTH', 'label': 'OTH - Other'},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.categoryToEdit != null) {
|
||||
final c = widget.categoryToEdit!;
|
||||
_selectedParentId = c.parentCategoryId;
|
||||
_nameController.text = c.name;
|
||||
_isLeaf = !c.hasChild;
|
||||
if (_isLeaf) {
|
||||
_hsnController.text = c.defaultHsn ?? '';
|
||||
_gstController.text = c.defaultGst?.toString() ?? '';
|
||||
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
||||
_huidRequired = c.huidRequired;
|
||||
_commodityCode = c.commodityCode ?? 'XAU';
|
||||
|
||||
// Handle legacy or varying enum string values
|
||||
_makingChargeType = c.makingChargeType ?? 'PER_GRAM';
|
||||
if (_makingChargeType == 'PER_GM') _makingChargeType = 'PER_GRAM';
|
||||
|
||||
|
||||
_baseUnit = c.baseUnit ?? 'g';
|
||||
if (_baseUnit == 'gm') _baseUnit = 'g';
|
||||
}
|
||||
} else if (widget.parent != null) {
|
||||
_selectedParentId = widget.parent!.id;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_hsnController.dispose();
|
||||
_gstController.dispose();
|
||||
_makingChargeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final newCategory = ProductCategory(
|
||||
id: widget.categoryToEdit?.id,
|
||||
name: _nameController.text.trim(),
|
||||
parentCategoryId: _selectedParentId,
|
||||
hasChild: !_isLeaf,
|
||||
commodityCode: _isLeaf ? _commodityCode : null,
|
||||
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
||||
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
||||
huidRequired: _isLeaf ? _huidRequired : false,
|
||||
defaultMakingCharge: _isLeaf ? double.tryParse(_makingChargeController.text) : null,
|
||||
makingChargeType: _isLeaf ? _makingChargeType : null,
|
||||
baseUnit: _isLeaf ? _baseUnit : 'pcs',
|
||||
);
|
||||
|
||||
if (widget.categoryToEdit != null && widget.categoryToEdit!.id != null) {
|
||||
ref.read(productCategoriesProvider.notifier).updateCategory(widget.categoryToEdit!.id!, newCategory);
|
||||
} else {
|
||||
ref.read(productCategoriesProvider.notifier).createCategory(newCategory);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final isBusinessMode = ref.watch(businessModeProvider);
|
||||
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.85,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 20, offset: const Offset(0, -5)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Drag handle
|
||||
Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 16),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.categoryToEdit != null
|
||||
? 'Edit Category'
|
||||
: (widget.parent == null ? 'New Root Category' : 'Subcategory of ${widget.parent!.name}'),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.x),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
// Form
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final categoriesAsync = ref.watch(productCategoriesProvider);
|
||||
final allCategories = categoriesAsync.value ?? [];
|
||||
final parentOptions = allCategories.where((c) =>
|
||||
c.hasChild && c.id != widget.categoryToEdit?.id
|
||||
).toList();
|
||||
|
||||
return DropdownButtonFormField<int?>(
|
||||
value: _selectedParentId,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Parent Category',
|
||||
prefixIcon: const Icon(LucideIcons.folder),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem(value: null, child: Text('None (Root Category)')),
|
||||
...parentOptions.map((c) => DropdownMenuItem(
|
||||
value: c.id,
|
||||
child: Text(c.name),
|
||||
)),
|
||||
],
|
||||
onChanged: (val) => setState(() => _selectedParentId = val),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
_buildTextField(
|
||||
controller: _nameController,
|
||||
label: 'Category Name',
|
||||
icon: LucideIcons.tag,
|
||||
validator: (val) => val == null || val.isEmpty ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Leaf Category Toggle
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.grey.shade900 : Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _isLeaf ? Colors.blue.withOpacity(0.1) : AppTheme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(_isLeaf ? LucideIcons.checkCircle : LucideIcons.folder,
|
||||
color: _isLeaf ? Colors.blue : AppTheme.primaryColor),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Final Product Category (Leaf)', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
|
||||
const SizedBox(height: 4),
|
||||
Text('Enable this if actual products will be created under this category. Subcategories cannot be added to a leaf.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: _isLeaf,
|
||||
onChanged: (val) => setState(() => _isLeaf = val),
|
||||
activeColor: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (_isLeaf) ...[
|
||||
const SizedBox(height: 24),
|
||||
const Text('Product Defaults', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (isBusinessMode) ...[
|
||||
DropdownButtonFormField<String>(
|
||||
value: _commodityCode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Commodity Code',
|
||||
prefixIcon: const Icon(LucideIcons.barChart2),
|
||||
),
|
||||
items: _commodities.map((c) => DropdownMenuItem(
|
||||
value: c['code'],
|
||||
child: Text(c['label']!),
|
||||
)).toList(),
|
||||
onChanged: (val) => setState(() => _commodityCode = val!),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
controller: _hsnController,
|
||||
label: 'HSN Code',
|
||||
icon: LucideIcons.hash,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
controller: _gstController,
|
||||
label: 'Default GST %',
|
||||
icon: LucideIcons.percent,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('HUID Required'),
|
||||
subtitle: const Text('Is Hallmark Unique Identification mandatory?'),
|
||||
value: _huidRequired,
|
||||
onChanged: (val) => setState(() => _huidRequired = val),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _makingChargeType,
|
||||
decoration: InputDecoration(
|
||||
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!),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
controller: _makingChargeController,
|
||||
label: 'Default Charge',
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _baseUnit,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Base Unit',
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'g', child: Text('Grams (g)')),
|
||||
DropdownMenuItem(value: 'kg', child: Text('Kilograms (kg)')),
|
||||
DropdownMenuItem(value: 'pcs', child: Text('Pieces (pcs)')),
|
||||
],
|
||||
onChanged: (val) => setState(() => _baseUnit = val!),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Footer
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 5,
|
||||
shadowColor: AppTheme.primaryColor.withOpacity(0.5),
|
||||
),
|
||||
child: const Text('Save Category', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
IconData? icon,
|
||||
TextInputType? keyboardType,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
prefixIcon: icon != null ? Icon(icon) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +108,9 @@ class _DailyRatesScreenState extends ConsumerState<DailyRatesScreen> {
|
||||
final categoriesState = ref.watch(productCategoriesProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
backgroundColor: Colors.grey[100],
|
||||
appBar: AppBar(
|
||||
title: const Text('Daily Commodity Rates', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
title: const Text('Daily Commodity Rates', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
@@ -118,7 +118,7 @@ class _DailyRatesScreenState extends ConsumerState<DailyRatesScreen> {
|
||||
),
|
||||
body: categoriesState.when(
|
||||
data: (categories) {
|
||||
final commodities = categories.where((c) => c.isCommodity).toList();
|
||||
final commodities = categories.where((c) => c.commodityCode != null && c.commodityCode!.isNotEmpty).toList();
|
||||
|
||||
if (commodities.isEmpty) {
|
||||
return Center(
|
||||
@@ -230,7 +230,6 @@ class _DailyRatesScreenState extends ConsumerState<DailyRatesScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Today\'s Rate (per ${category.baseUnit})',
|
||||
prefixText: '₹ ',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:kifi_app/core/network/dio_client.dart';
|
||||
import 'package:kifi_app/features/inventory/domain/product.dart';
|
||||
import 'package:kifi_app/features/inventory/presentation/add_product_screen.dart';
|
||||
import 'package:kifi_app/features/inventory/presentation/stock_ledger_tab.dart';
|
||||
import 'package:kifi_app/features/inventory/presentation/bom_tab.dart';
|
||||
import 'package:kifi_app/features/inventory/presentation/widgets/adjust_stock_sheet.dart';
|
||||
|
||||
class ProductDetailScreen extends ConsumerWidget {
|
||||
@@ -17,17 +16,25 @@ class ProductDetailScreen extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Watch products to get latest updates (like stock changes)
|
||||
final productsState = ref.watch(productsProvider);
|
||||
final currentProduct = productsState.value?.firstWhere((p) => p.id == product.id, orElse: () => product) ?? product;
|
||||
final currentProduct =
|
||||
productsState.value?.firstWhere(
|
||||
(p) => p.id == product.id,
|
||||
orElse: () => product,
|
||||
) ??
|
||||
product;
|
||||
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
title: Text(currentProduct.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
title: Text(
|
||||
currentProduct.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, color: Colors.black),
|
||||
@@ -35,7 +42,8 @@ class ProductDetailScreen extends ConsumerWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddProductScreen(product: currentProduct),
|
||||
builder: (context) =>
|
||||
AddProductScreen(product: currentProduct),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -47,7 +55,6 @@ class ProductDetailScreen extends ConsumerWidget {
|
||||
indicatorColor: Colors.blue,
|
||||
tabs: [
|
||||
Tab(text: "Overview"),
|
||||
Tab(text: "BOM"),
|
||||
Tab(text: "Stock Ledger"),
|
||||
],
|
||||
),
|
||||
@@ -55,23 +62,9 @@ class ProductDetailScreen extends ConsumerWidget {
|
||||
body: TabBarView(
|
||||
children: [
|
||||
_buildOverviewTab(context, currentProduct, ref),
|
||||
BomTab(productId: currentProduct.id!),
|
||||
StockLedgerTab(productId: currentProduct.id!),
|
||||
StockLedgerTab(product: currentProduct),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => AdjustStockSheet(productId: currentProduct.id!),
|
||||
);
|
||||
},
|
||||
backgroundColor: Colors.blue,
|
||||
icon: const Icon(Icons.inventory),
|
||||
label: const Text("Adjust Stock"),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -100,68 +93,46 @@ class ProductDetailScreen extends ConsumerWidget {
|
||||
}
|
||||
final token = snapshot.data;
|
||||
if (token == null) {
|
||||
return const Icon(Icons.image, size: 50, color: Colors.grey);
|
||||
return const Icon(
|
||||
Icons.image,
|
||||
size: 50,
|
||||
color: Colors.grey,
|
||||
);
|
||||
}
|
||||
return Image.network(
|
||||
'${DioClient().dio.options.baseUrl}/inventory/products/${p.id}/images/${p.imageIds.first}/content',
|
||||
fit: BoxFit.cover,
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(Icons.image, size: 50, color: Colors.grey),
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
const Icon(Icons.image, size: 50, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Stock Summary Card
|
||||
// Product Details Card
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.inventory_2, color: Colors.blue),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Current Stock", style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
||||
Text(
|
||||
"${p.currentStock ?? 0}",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Details Card
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Product Details", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
const Text(
|
||||
"Product Details",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Divider(),
|
||||
_buildDetailRow("SKU", p.sku ?? "-"),
|
||||
_buildDetailRow("Purchase Price", "₹${p.purchasePrice?.toStringAsFixed(2) ?? '0.00'}"),
|
||||
_buildDetailRow("Selling Price", "₹${p.sellingPrice?.toStringAsFixed(2) ?? '0.00'}"),
|
||||
_buildDetailRow("GST Rate", "${p.gstRate?.toStringAsFixed(1) ?? '0'}%"),
|
||||
_buildDetailRow("Reorder Level", "${p.reorderLevel ?? 0}"),
|
||||
_buildDetailRow("SKU / Barcode", p.sku ?? "-"),
|
||||
_buildDetailRow("HSN Code", p.hsnCode ?? "-"),
|
||||
_buildDetailRow(
|
||||
"GST Rate",
|
||||
"${p.gstRate?.toStringAsFixed(1) ?? '0'}%",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -43,7 +43,8 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
ref.read(productsProvider.notifier).fetchNextPage();
|
||||
}
|
||||
}
|
||||
@@ -54,124 +55,206 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
||||
final categoriesState = ref.watch(productCategoriesProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
appBar: AppBar(
|
||||
title: const Text('Products Catalog'),
|
||||
title: const Text(
|
||||
'Products Catalog',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search products by name or SKU...',
|
||||
prefixIcon: const Icon(LucideIcons.search),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
onChanged: (val) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () {
|
||||
ref.read(productsProvider.notifier).search(val);
|
||||
});
|
||||
},
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search products by name or SKU...',
|
||||
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
onChanged: (val) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () {
|
||||
ref.read(productsProvider.notifier).search(val);
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: productsState.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
data: (products) {
|
||||
if (products.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(productsProvider.notifier).refresh(),
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SizedBox(height: 100),
|
||||
Center(child: Text('No products found.', style: TextStyle(color: Colors.grey)))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
),
|
||||
Expanded(
|
||||
child: productsState.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
data: (products) {
|
||||
if (products.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(productsProvider.notifier).refresh(),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
onRefresh: () =>
|
||||
ref.read(productsProvider.notifier).refresh(),
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: products.length + 1, // +1 for loading indicator
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemBuilder: (context, index) {
|
||||
children: const [
|
||||
SizedBox(height: 100),
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.packageSearch,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'No products found.',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () =>
|
||||
ref.read(productsProvider.notifier).refresh(),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: products.length + 1, // +1 for loading indicator
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == products.length) {
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
|
||||
final p = products[index];
|
||||
final catName = categoriesState.value?.firstWhere((c) => c.id == p.categoryId, orElse: () => categoriesState.value!.first).name ?? 'No Category';
|
||||
final catName =
|
||||
categoriesState.value
|
||||
?.firstWhere(
|
||||
(c) => c.id == p.categoryId,
|
||||
orElse: () => categoriesState.value!.first,
|
||||
)
|
||||
.name ??
|
||||
'No Category';
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 2,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => ProductDetailScreen(product: p)));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildProductImage(p),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(p.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(catName, style: const TextStyle(color: Colors.blue, fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
ProductDetailScreen(product: p),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildProductImage(p),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
p.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
if (p.sku != null && p.sku!.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(p.sku!, style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
]
|
||||
],
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(
|
||||
0.1,
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
catName,
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (p.sku != null &&
|
||||
p.sku!.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
p.sku!,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
(p.currentStock != null)
|
||||
? 'Stock: ${p.currentStock ?? 0}'
|
||||
: 'Untracked',
|
||||
style: TextStyle(
|
||||
color: (p.currentStock != null)
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'₹${(p.sellingPrice ?? 0).toStringAsFixed(2)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
p.trackInventory ? 'Stock: ${p.currentStock ?? 0}' : 'Untracked',
|
||||
style: TextStyle(
|
||||
color: p.trackInventory ? Colors.orange : Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -181,12 +264,15 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const AddProductScreen()));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const AddProductScreen()),
|
||||
);
|
||||
},
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: const Icon(LucideIcons.plus, color: Colors.white),
|
||||
@@ -207,7 +293,8 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
final imageUrl = '${DioClient().dio.options.baseUrl}/inventory/products/${product.id}/images/${product.imageIds.first}/content';
|
||||
final imageUrl =
|
||||
'${DioClient().dio.options.baseUrl}/inventory/products/${product.id}/images/${product.imageIds.first}/content';
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -216,14 +303,16 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
|
||||
height: 60,
|
||||
color: Colors.grey[200],
|
||||
child: _token == null
|
||||
? const Icon(LucideIcons.image, color: Colors.grey)
|
||||
: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
headers: {'Authorization': 'Bearer $_token'},
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(LucideIcons.imageOff, color: Colors.grey),
|
||||
),
|
||||
? const Icon(LucideIcons.image, color: Colors.grey)
|
||||
: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
headers: {'Authorization': 'Bearer $_token'},
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
const Icon(LucideIcons.imageOff, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ class QuickAdjustStockScreen extends ConsumerStatefulWidget {
|
||||
const QuickAdjustStockScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<QuickAdjustStockScreen> createState() => _QuickAdjustStockScreenState();
|
||||
ConsumerState<QuickAdjustStockScreen> createState() =>
|
||||
_QuickAdjustStockScreenState();
|
||||
}
|
||||
|
||||
class _QuickAdjustStockScreenState extends ConsumerState<QuickAdjustStockScreen> {
|
||||
class _QuickAdjustStockScreenState
|
||||
extends ConsumerState<QuickAdjustStockScreen> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
Timer? _debounce;
|
||||
@@ -28,7 +30,8 @@ class _QuickAdjustStockScreenState extends ConsumerState<QuickAdjustStockScreen>
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
ref.read(productsProvider.notifier).fetchNextPage();
|
||||
}
|
||||
}
|
||||
@@ -53,10 +56,7 @@ class _QuickAdjustStockScreenState extends ConsumerState<QuickAdjustStockScreen>
|
||||
final productsState = ref.watch(productsProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Quick Adjust Stock'),
|
||||
elevation: 0,
|
||||
),
|
||||
appBar: AppBar(title: const Text('Quick Adjust Stock'), elevation: 0),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
@@ -66,13 +66,10 @@ class _QuickAdjustStockScreenState extends ConsumerState<QuickAdjustStockScreen>
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search product to adjust...',
|
||||
prefixIcon: const Icon(LucideIcons.search),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
onChanged: (val) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
@@ -89,7 +86,8 @@ class _QuickAdjustStockScreenState extends ConsumerState<QuickAdjustStockScreen>
|
||||
data: (products) {
|
||||
if (products.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(productsProvider.notifier).refresh(),
|
||||
onRefresh: () =>
|
||||
ref.read(productsProvider.notifier).refresh(),
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
@@ -101,7 +99,8 @@ class _QuickAdjustStockScreenState extends ConsumerState<QuickAdjustStockScreen>
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(productsProvider.notifier).refresh(),
|
||||
onRefresh: () =>
|
||||
ref.read(productsProvider.notifier).refresh(),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
@@ -112,43 +111,60 @@ class _QuickAdjustStockScreenState extends ConsumerState<QuickAdjustStockScreen>
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
final p = products[index];
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
leading: _buildProductImage(p),
|
||||
title: Text(p.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(p.sku ?? 'No SKU', style: TextStyle(color: Colors.grey.shade600)),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${p.currentStock ?? 0}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.blue),
|
||||
),
|
||||
const Text('in stock', style: TextStyle(fontSize: 10, color: Colors.grey)),
|
||||
],
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => AdjustStockSheet(productId: p.id!),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
leading: _buildProductImage(p),
|
||||
title: Text(
|
||||
p.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
p.sku ?? 'No SKU',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${p.currentStock ?? 0}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'in stock',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) =>
|
||||
AdjustStockSheet(productId: p.id!),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -169,7 +185,8 @@ class _QuickAdjustStockScreenState extends ConsumerState<QuickAdjustStockScreen>
|
||||
);
|
||||
}
|
||||
|
||||
final imageUrl = '${DioClient().dio.options.baseUrl}/inventory/products/${product.id}/images/${product.imageIds.first}/content';
|
||||
final imageUrl =
|
||||
'${DioClient().dio.options.baseUrl}/inventory/products/${product.id}/images/${product.imageIds.first}/content';
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -178,13 +195,14 @@ class _QuickAdjustStockScreenState extends ConsumerState<QuickAdjustStockScreen>
|
||||
height: 50,
|
||||
color: Colors.grey[200],
|
||||
child: _token == null
|
||||
? const Icon(LucideIcons.image, color: Colors.grey)
|
||||
: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
headers: {'Authorization': 'Bearer $_token'},
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(LucideIcons.imageOff, color: Colors.grey),
|
||||
),
|
||||
? const Icon(LucideIcons.image, color: Colors.grey)
|
||||
: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
headers: {'Authorization': 'Bearer $_token'},
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
const Icon(LucideIcons.imageOff, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:kifi_app/features/inventory/domain/stock_movement.dart';
|
||||
import 'package:kifi_app/features/inventory/domain/inventory_item.dart';
|
||||
import 'package:kifi_app/features/inventory/domain/product.dart';
|
||||
import 'package:kifi_app/features/inventory/providers/products_provider.dart';
|
||||
import 'package:kifi_app/features/inventory/providers/product_categories_provider.dart';
|
||||
import 'package:kifi_app/core/network/dio_client.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:kifi_app/features/vendor/presentation/purchase_order_details_screen.dart';
|
||||
import 'package:kifi_app/features/vendor/providers/purchase_orders_provider.dart';
|
||||
|
||||
class StockLedgerTab extends ConsumerStatefulWidget {
|
||||
final int productId;
|
||||
final Product product;
|
||||
|
||||
const StockLedgerTab({super.key, required this.productId});
|
||||
const StockLedgerTab({super.key, required this.product});
|
||||
|
||||
@override
|
||||
ConsumerState<StockLedgerTab> createState() => _StockLedgerTabState();
|
||||
}
|
||||
|
||||
class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
List<StockMovement>? _ledger;
|
||||
List<InventoryItem>? _items;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
@@ -25,10 +31,12 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
|
||||
Future<void> _fetchLedger() async {
|
||||
try {
|
||||
final ledger = await ref.read(productsProvider.notifier).fetchStockLedger(widget.productId);
|
||||
final items = await ref
|
||||
.read(productsProvider.notifier)
|
||||
.fetchInventoryItems(widget.product.id!);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_ledger = ledger;
|
||||
_items = items;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -37,7 +45,9 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,59 +58,225 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (_ledger == null || _ledger!.isEmpty) {
|
||||
return const Center(child: Text("No stock movements recorded."));
|
||||
if (_items == null || _items!.isEmpty) {
|
||||
return const Center(child: Text("No stock available."));
|
||||
}
|
||||
|
||||
final categoriesState = ref.watch(productCategoriesProvider);
|
||||
final category = categoriesState.value?.firstWhere(
|
||||
(c) => c.id == widget.product.categoryId,
|
||||
);
|
||||
|
||||
final String unit = category?.baseUnit ?? 'g';
|
||||
final double currentRate = category?.dailyRate ?? 0.0;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _fetchLedger,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: _ledger!.length,
|
||||
itemCount: _items!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final movement = _ledger![index];
|
||||
final isAddition = movement.type == 'ADDITION' || movement.type == 'OPENING';
|
||||
final qty = movement.items?.isNotEmpty == true ? movement.items!.first.quantity : 0.0;
|
||||
final item = _items![index];
|
||||
final weight = item.grossWeight ?? 0.0;
|
||||
final purchaseRate = item.purchaseCost ?? 0.0;
|
||||
final purchasePrice = weight * purchaseRate;
|
||||
final currentPrice = weight * currentRate;
|
||||
final gainLoss = currentPrice - purchasePrice;
|
||||
final isGain = gainLoss >= 0;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: isAddition ? Colors.green.withOpacity(0.1) : Colors.red.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isAddition ? Icons.arrow_downward : Icons.arrow_upward,
|
||||
color: isAddition ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
title: Text(movement.type, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(
|
||||
movement.createdAt != null ? DateFormat('dd MMM yyyy, hh:mm a').format(movement.createdAt!) : '',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.grey.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${isAddition ? '+' : '-'}${qty.toStringAsFixed(1)}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: isAddition ? Colors.green : Colors.red,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: FutureBuilder<String?>(
|
||||
future: DioClient().storage.read(key: 'jwt_token'),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && widget.product.imageIds.isNotEmpty) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
'${DioClient().dio.options.baseUrl}/inventory/products/${widget.product.id}/images/${widget.product.imageIds.first}/content',
|
||||
fit: BoxFit.cover,
|
||||
headers: {'Authorization': 'Bearer ${snapshot.data}'},
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Icon(LucideIcons.image, color: Colors.grey, size: 20);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.createdAt != null
|
||||
? DateFormat('dd MMM yyyy').format(item.createdAt!)
|
||||
: '',
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (item.purchaseRef != null)
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
try {
|
||||
var pos = ref.read(purchaseOrdersProvider).value;
|
||||
if (pos == null || pos.isEmpty) {
|
||||
pos = await ref.read(purchaseOrdersProvider.future);
|
||||
}
|
||||
final po = pos!.firstWhere(
|
||||
(p) => p.poNumber == item.purchaseRef,
|
||||
orElse: () => throw Exception('Purchase invoice not found'),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PurchaseOrderDetailsScreen(po: po),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Could not open purchase invoice: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'INV: ${item.purchaseRef}',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (item.huid != null && item.huid!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text('HUID: ${item.huid}', style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'IN STOCK',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${weight.toStringAsFixed(3)} $unit',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Purchase Rate', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||
Text('₹${purchaseRate.toStringAsFixed(2)}/$unit', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('Purchase Amt', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||
Text('₹${purchasePrice.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Current Rate', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||
Text('₹${currentRate.toStringAsFixed(2)}/$unit', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('Current Amt', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||
Text('₹${currentPrice.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isGain ? Colors.green.withValues(alpha: 0.1) : Colors.red.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isGain ? LucideIcons.trendingUp : LucideIcons.trendingDown,
|
||||
color: isGain ? Colors.green : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${isGain ? 'Gain' : 'Loss'}: ₹${gainLoss.abs().toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
color: isGain ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (movement.notes != null && movement.notes!.isNotEmpty)
|
||||
Text(
|
||||
movement.notes!,
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../providers/uoms_provider.dart';
|
||||
import 'widgets/add_uom_sheet.dart';
|
||||
|
||||
class UomsListScreen extends ConsumerStatefulWidget {
|
||||
const UomsListScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<UomsListScreen> createState() => _UomsListScreenState();
|
||||
}
|
||||
|
||||
class _UomsListScreenState extends ConsumerState<UomsListScreen> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
Timer? _debounce;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_debounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final uomsState = ref.watch(uomsProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Units of Measure'),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search units of measure...',
|
||||
prefixIcon: const Icon(LucideIcons.search),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
onChanged: (val) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () {
|
||||
ref.read(uomsProvider.notifier).search(val);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: uomsState.when(
|
||||
data: (uoms) {
|
||||
if (uoms.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(uomsProvider.notifier).fetchUoms(),
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
const SizedBox(height: 100),
|
||||
Icon(LucideIcons.ruler, size: 64, color: Colors.grey[300]),
|
||||
const SizedBox(height: 16),
|
||||
Center(child: Text('No Units of Measure found', style: TextStyle(color: Colors.grey[600], fontSize: 16))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(uomsProvider.notifier).fetchUoms(),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: uoms.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final uom = uoms[index];
|
||||
return Dismissible(
|
||||
key: ValueKey(uom.id),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: const Icon(LucideIcons.trash2, color: Colors.white),
|
||||
),
|
||||
confirmDismiss: (direction) async {
|
||||
return await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete UOM'),
|
||||
content: Text('Are you sure you want to delete ${uom.name}?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete', style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
onDismissed: (direction) {
|
||||
ref.read(uomsProvider.notifier).deleteUom(uom.id!);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
title: Text(uom.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text('Abbreviation: ${uom.abbreviation}'),
|
||||
),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(LucideIcons.chevronRight, size: 20),
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => AddUomSheet(uom: uom),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Error: $e', style: const TextStyle(color: Colors.red)),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.read(uomsProvider.notifier).fetchUoms(),
|
||||
child: const Text('Retry'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => const AddUomSheet(),
|
||||
);
|
||||
},
|
||||
child: const Icon(LucideIcons.plus),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../domain/uom.dart';
|
||||
import '../../providers/uoms_provider.dart';
|
||||
|
||||
class AddUomSheet extends ConsumerStatefulWidget {
|
||||
final UnitOfMeasure? uom;
|
||||
|
||||
const AddUomSheet({super.key, this.uom});
|
||||
|
||||
@override
|
||||
ConsumerState<AddUomSheet> createState() => _AddUomSheetState();
|
||||
}
|
||||
|
||||
class _AddUomSheetState extends ConsumerState<AddUomSheet> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _nameController;
|
||||
late TextEditingController _abbrevController;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.uom?.name ?? '');
|
||||
_abbrevController = TextEditingController(text: widget.uom?.abbreviation ?? '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_abbrevController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final uom = UnitOfMeasure(
|
||||
id: widget.uom?.id,
|
||||
name: _nameController.text.trim(),
|
||||
abbreviation: _abbrevController.text.trim(),
|
||||
);
|
||||
|
||||
if (widget.uom == null) {
|
||||
await ref.read(uomsProvider.notifier).createUom(uom);
|
||||
} else {
|
||||
await ref.read(uomsProvider.notifier).updateUom(widget.uom!.id!, uom);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(widget.uom == null ? 'Unit of Measure created' : 'Unit of Measure updated')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
top: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(widget.uom == null ? "Add Unit of Measure" : "Edit Unit of Measure", style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Name (e.g., Kilogram)",
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Colors.blue, width: 2)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100]
|
||||
),
|
||||
validator: (val) => val == null || val.isEmpty ? 'Please enter a name' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _abbrevController,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Abbreviation (e.g., kg)",
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Colors.blue, width: 2)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100]
|
||||
),
|
||||
validator: (val) => val == null || val.isEmpty ? 'Please enter an abbreviation' : null,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: Text(widget.uom == null ? "Save UOM" : "Update UOM", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/theme/nature_colors.dart';
|
||||
import '../../providers/product_categories_provider.dart';
|
||||
|
||||
class QuickRateUpdateSheet extends ConsumerStatefulWidget {
|
||||
const QuickRateUpdateSheet({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<QuickRateUpdateSheet> createState() => _QuickRateUpdateSheetState();
|
||||
}
|
||||
|
||||
class _QuickRateUpdateSheetState extends ConsumerState<QuickRateUpdateSheet> {
|
||||
String? _selectedCommodityCode;
|
||||
final _rateController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_rateController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveRate() async {
|
||||
if (_selectedCommodityCode == null || _rateController.text.isEmpty) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final rate = double.parse(_rateController.text);
|
||||
await ref.read(productCategoriesProvider.notifier).updateCommodityRate(_selectedCommodityCode!, rate);
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Rate updated successfully')));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error updating rate: $e')));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categoriesState = ref.watch(productCategoriesProvider);
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
final commodityCodes = categoriesState.value
|
||||
?.where((c) => c.commodityCode != null && c.commodityCode!.isNotEmpty)
|
||||
.map((c) => c.commodityCode!)
|
||||
.toSet()
|
||||
.toList() ?? [];
|
||||
|
||||
if (_selectedCommodityCode == null && commodityCodes.isNotEmpty) {
|
||||
_selectedCommodityCode = commodityCodes.first;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(32)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(isDark ? 0.2 : 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Icon(Icons.bolt, color: Colors.orange, size: 28),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Quick Rate Update', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, letterSpacing: -0.5)),
|
||||
Text('Update live commodity prices', style: TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (commodityCodes.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.grey),
|
||||
SizedBox(width: 12),
|
||||
Expanded(child: Text('No commodities found. Create a leaf category with a commodity code to update rates.', style: TextStyle(color: Colors.grey))),
|
||||
],
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedCommodityCode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Select Commodity',
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w500),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
),
|
||||
icon: const Icon(Icons.unfold_more, color: Colors.grey),
|
||||
items: commodityCodes.map((c) => DropdownMenuItem(value: c, child: Text(c, style: const TextStyle(fontWeight: FontWeight.w600)))).toList(),
|
||||
onChanged: (val) => setState(() => _selectedCommodityCode = val),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _rateController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Base Rate (24k / Purity 1)',
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w500),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
prefixIcon: const Icon(Icons.currency_rupee, color: Colors.orange),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _saveRate,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5))
|
||||
: const Text('Update Rate', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, letterSpacing: 0.5)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,16 @@ class ProductCategory {
|
||||
final String name;
|
||||
final int? parentCategoryId;
|
||||
final bool hasChild;
|
||||
final String? commodityCode;
|
||||
final String? defaultHsn;
|
||||
final double? defaultGst;
|
||||
final bool huidRequired;
|
||||
final double? defaultMakingCharge;
|
||||
final String? makingChargeType;
|
||||
final String calculationMethod;
|
||||
final String baseUnit;
|
||||
final bool isActive;
|
||||
final int sortOrder;
|
||||
final double? dailyRate;
|
||||
|
||||
ProductCategory({
|
||||
this.id,
|
||||
@@ -24,15 +25,16 @@ class ProductCategory {
|
||||
required this.name,
|
||||
this.parentCategoryId,
|
||||
this.hasChild = false,
|
||||
this.commodityCode,
|
||||
this.defaultHsn,
|
||||
this.defaultGst,
|
||||
this.huidRequired = false,
|
||||
this.defaultMakingCharge,
|
||||
this.makingChargeType,
|
||||
this.calculationMethod = 'UNIT',
|
||||
this.baseUnit = 'pcs',
|
||||
this.isActive = true,
|
||||
this.sortOrder = 0,
|
||||
this.dailyRate,
|
||||
});
|
||||
|
||||
factory ProductCategory.fromJson(Map<String, dynamic> json) {
|
||||
@@ -42,15 +44,16 @@ class ProductCategory {
|
||||
name: json['name'],
|
||||
parentCategoryId: json['parentCategoryId'],
|
||||
hasChild: json['hasChild'] ?? false,
|
||||
commodityCode: json['commodityCode'],
|
||||
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',
|
||||
isActive: json['isActive'] ?? true,
|
||||
sortOrder: json['sortOrder'] ?? 0,
|
||||
dailyRate: (json['dailyRate'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,12 +64,12 @@ class ProductCategory {
|
||||
'name': name,
|
||||
'parentCategoryId': parentCategoryId,
|
||||
'hasChild': hasChild,
|
||||
'commodityCode': commodityCode,
|
||||
'defaultHsn': defaultHsn,
|
||||
'defaultGst': defaultGst,
|
||||
'huidRequired': huidRequired,
|
||||
'defaultMakingCharge': defaultMakingCharge,
|
||||
'makingChargeType': makingChargeType,
|
||||
'calculationMethod': calculationMethod,
|
||||
'baseUnit': baseUnit,
|
||||
'isActive': isActive,
|
||||
'sortOrder': sortOrder,
|
||||
@@ -101,9 +104,71 @@ class ProductCategoriesNotifier extends AsyncNotifier<List<ProductCategory>> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateCategory(int id, ProductCategory category) async {
|
||||
try {
|
||||
await DioClient().dio.put(
|
||||
'/inventory/categories/$id',
|
||||
data: category.toJson(),
|
||||
);
|
||||
state = AsyncValue.data(await _fetchCategories());
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteCategory(int id) async {
|
||||
try {
|
||||
await DioClient().dio.delete('/inventory/categories/$id');
|
||||
state = await AsyncValue.guard(() => _fetchCategories());
|
||||
} catch (e) {
|
||||
print('Error deleting category: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> fetchRateHistory(int categoryId) async {
|
||||
final response = await DioClient().dio.get(
|
||||
'/inventory/categories/$categoryId/rates/history',
|
||||
);
|
||||
return response.data as List<dynamic>;
|
||||
}
|
||||
|
||||
Future<void> updateCategoryRate(int categoryId, double rate) async {
|
||||
await DioClient().dio.post(
|
||||
'/inventory/categories/$categoryId/rates',
|
||||
data: {'rate': rate},
|
||||
);
|
||||
state = await AsyncValue.guard(() => _fetchCategories());
|
||||
}
|
||||
|
||||
Future<void> updateCommodityRate(String commodityCode, double rate) async {
|
||||
final categories = state.value ?? [];
|
||||
final matchingCategories = categories.where(
|
||||
(c) => c.commodityCode == commodityCode,
|
||||
);
|
||||
for (var cat in matchingCategories) {
|
||||
try {
|
||||
await DioClient().dio.post(
|
||||
'/inventory/categories/${cat.id}/rates',
|
||||
data: {'rate': rate},
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error updating rate for category ${cat.id}: $e');
|
||||
}
|
||||
}
|
||||
state = await AsyncValue.guard(() => _fetchCategories());
|
||||
}
|
||||
|
||||
Future<int> syncRates(int categoryId) async {
|
||||
final response = await DioClient().dio.post(
|
||||
'/inventory/categories/$categoryId/rates/sync',
|
||||
);
|
||||
state = await AsyncValue.guard(() => _fetchCategories());
|
||||
return response.data['updatedCount'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
final productCategoriesProvider = AsyncNotifierProvider<ProductCategoriesNotifier, List<ProductCategory>>(() {
|
||||
return ProductCategoriesNotifier();
|
||||
});
|
||||
final productCategoriesProvider =
|
||||
AsyncNotifierProvider<ProductCategoriesNotifier, List<ProductCategory>>(() {
|
||||
return ProductCategoriesNotifier();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../../core/network/dio_client.dart';
|
||||
import '../domain/product.dart';
|
||||
import '../domain/stock_movement.dart';
|
||||
import '../domain/product_bom.dart';
|
||||
import '../domain/inventory_item.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
@@ -24,20 +25,25 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
return _fetchProducts(page: _currentPage, size: _pageSize);
|
||||
}
|
||||
|
||||
Future<List<Product>> _fetchProducts({required int page, required int size}) async {
|
||||
Future<List<Product>> _fetchProducts({
|
||||
required int page,
|
||||
required int size,
|
||||
}) async {
|
||||
final response = await DioClient().dio.get(
|
||||
'/inventory/products',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'size': size,
|
||||
if (_currentSearchQuery.isNotEmpty) 'search': _currentSearchQuery,
|
||||
}
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data;
|
||||
if (data.isNotEmpty) {
|
||||
try {
|
||||
final file = File('/Users/maddy/Projects/Kifi/kifi-app/debug_product.json');
|
||||
final file = File(
|
||||
'/Users/maddy/Projects/Kifi/kifi-app/debug_product.json',
|
||||
);
|
||||
await file.writeAsString(jsonEncode(data.first));
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -73,7 +79,9 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
_currentPage = 0;
|
||||
_hasMore = true;
|
||||
try {
|
||||
state = AsyncValue.data(await _fetchProducts(page: _currentPage, size: _pageSize));
|
||||
state = AsyncValue.data(
|
||||
await _fetchProducts(page: _currentPage, size: _pageSize),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
state = AsyncValue.error(e, stack);
|
||||
}
|
||||
@@ -102,7 +110,10 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
);
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'file': MultipartFile.fromBytes(compressedBytes, filename: image.name),
|
||||
'file': MultipartFile.fromBytes(
|
||||
compressedBytes,
|
||||
filename: image.name,
|
||||
),
|
||||
});
|
||||
await DioClient().dio.post(
|
||||
'/inventory/products/$productId/images',
|
||||
@@ -118,7 +129,11 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateProduct(int id, Product product, {List<XFile>? newImages}) async {
|
||||
Future<void> updateProduct(
|
||||
int id,
|
||||
Product product, {
|
||||
List<XFile>? newImages,
|
||||
}) async {
|
||||
try {
|
||||
await DioClient().dio.put(
|
||||
'/inventory/products/$id',
|
||||
@@ -135,7 +150,10 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
);
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'file': MultipartFile.fromBytes(compressedBytes, filename: image.name),
|
||||
'file': MultipartFile.fromBytes(
|
||||
compressedBytes,
|
||||
filename: image.name,
|
||||
),
|
||||
});
|
||||
await DioClient().dio.post(
|
||||
'/inventory/products/$id/images',
|
||||
@@ -166,7 +184,9 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
|
||||
Future<List<StockMovement>> fetchStockLedger(int productId) async {
|
||||
try {
|
||||
final response = await DioClient().dio.get('/inventory/products/$productId/movements');
|
||||
final response = await DioClient().dio.get(
|
||||
'/inventory/products/$productId/movements',
|
||||
);
|
||||
if (response.data != null) {
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((e) => StockMovement.fromJson(e)).toList();
|
||||
@@ -177,9 +197,26 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<InventoryItem>> fetchInventoryItems(int productId) async {
|
||||
try {
|
||||
final response = await DioClient().dio.get(
|
||||
'/inventory/items/product/$productId',
|
||||
);
|
||||
if (response.data != null) {
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((e) => InventoryItem.fromJson(e)).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to fetch inventory items: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ProductBom>> fetchProductBom(int productId) async {
|
||||
try {
|
||||
final response = await DioClient().dio.get('/inventory/products/$productId/bom');
|
||||
final response = await DioClient().dio.get(
|
||||
'/inventory/products/$productId/bom',
|
||||
);
|
||||
if (response.data != null) {
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((e) => ProductBom.fromJson(e)).toList();
|
||||
@@ -210,6 +247,8 @@ class ProductsNotifier extends AsyncNotifier<List<Product>> {
|
||||
}
|
||||
}
|
||||
|
||||
final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>(() {
|
||||
return ProductsNotifier();
|
||||
});
|
||||
final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>(
|
||||
() {
|
||||
return ProductsNotifier();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../domain/uom.dart';
|
||||
|
||||
class UomsNotifier extends AsyncNotifier<List<UnitOfMeasure>> {
|
||||
List<UnitOfMeasure> _allUoms = [];
|
||||
|
||||
@override
|
||||
Future<List<UnitOfMeasure>> build() async {
|
||||
return _fetchUoms();
|
||||
}
|
||||
|
||||
Future<List<UnitOfMeasure>> _fetchUoms() async {
|
||||
final response = await DioClient().dio.get('/inventory/uom').timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw Exception('Connection timed out'),
|
||||
);
|
||||
if (response.data == null || response.data.toString().isEmpty) {
|
||||
_allUoms = [];
|
||||
return [];
|
||||
}
|
||||
final list = (response.data as List).map((e) => UnitOfMeasure.fromJson(e)).toList();
|
||||
_allUoms = list;
|
||||
return list;
|
||||
}
|
||||
|
||||
void search(String query) {
|
||||
if (query.isEmpty) {
|
||||
state = AsyncValue.data(_allUoms);
|
||||
return;
|
||||
}
|
||||
final lowerQuery = query.toLowerCase();
|
||||
final filtered = _allUoms.where((uom) =>
|
||||
uom.name.toLowerCase().contains(lowerQuery) ||
|
||||
(uom.abbreviation?.toLowerCase().contains(lowerQuery) ?? false)
|
||||
).toList();
|
||||
state = AsyncValue.data(filtered);
|
||||
}
|
||||
|
||||
Future<void> fetchUoms() async {
|
||||
try {
|
||||
final data = await _fetchUoms();
|
||||
state = AsyncValue.data(data);
|
||||
} catch (e, stack) {
|
||||
state = AsyncValue.error(e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createUom(UnitOfMeasure uom) async {
|
||||
try {
|
||||
final response = await DioClient().dio.post('/inventory/uom', data: uom.toJson());
|
||||
final newUom = UnitOfMeasure.fromJson(response.data);
|
||||
if (state is AsyncData) {
|
||||
_allUoms = [..._allUoms, newUom];
|
||||
state = AsyncValue.data([...state.value!, newUom]);
|
||||
} else {
|
||||
await fetchUoms();
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to create UOM: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateUom(int id, UnitOfMeasure uom) async {
|
||||
try {
|
||||
final response = await DioClient().dio.put('/inventory/uom/$id', data: uom.toJson());
|
||||
final updatedUom = UnitOfMeasure.fromJson(response.data);
|
||||
if (state is AsyncData) {
|
||||
_allUoms = _allUoms.map((e) => e.id == id ? updatedUom : e).toList();
|
||||
state = AsyncValue.data(
|
||||
state.value!.map((e) => e.id == id ? updatedUom : e).toList(),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to update UOM: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteUom(int id) async {
|
||||
try {
|
||||
await DioClient().dio.delete('/inventory/uom/$id');
|
||||
if (state is AsyncData) {
|
||||
_allUoms = _allUoms.where((e) => e.id != id).toList();
|
||||
state = AsyncValue.data(
|
||||
state.value!.where((e) => e.id != id).toList(),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to delete UOM: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final uomsProvider = AsyncNotifierProvider<UomsNotifier, List<UnitOfMeasure>>(() {
|
||||
return UomsNotifier();
|
||||
});
|
||||
@@ -17,22 +17,26 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
final List<Map<String, dynamic>> _pages = [
|
||||
{
|
||||
'title': 'Welcome to Kifi',
|
||||
'description': 'A beautiful, intelligent way to track your personal finances.',
|
||||
'description':
|
||||
'A beautiful, intelligent way to track your personal finances.',
|
||||
'icon': LucideIcons.wallet,
|
||||
},
|
||||
{
|
||||
'title': 'Double-Entry, Simplified',
|
||||
'description': 'We track money moving from one place to another. Every expense comes from a Wallet (like Cash) and goes to a Category (like Food).',
|
||||
'description':
|
||||
'We track money moving from one place to another. Every expense comes from a Wallet (like Cash) and goes to a Category (like Food).',
|
||||
'icon': LucideIcons.arrowRightLeft,
|
||||
},
|
||||
{
|
||||
'title': 'Intelligent Automation',
|
||||
'description': 'Set up recurring transactions and let Kifi handle your monthly subscriptions and salary deposits automatically.',
|
||||
'description':
|
||||
'Set up recurring transactions and let Kifi handle your monthly subscriptions and salary deposits automatically.',
|
||||
'icon': LucideIcons.bot,
|
||||
},
|
||||
{
|
||||
'title': 'Powerful Insights',
|
||||
'description': 'View day-by-day spending trends and strict monthly budgets to take control of your financial future.',
|
||||
'description':
|
||||
'View day-by-day spending trends and strict monthly budgets to take control of your financial future.',
|
||||
'icon': LucideIcons.barChart2,
|
||||
},
|
||||
];
|
||||
@@ -76,16 +80,21 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
const SizedBox(height: 60),
|
||||
Text(
|
||||
page['title'],
|
||||
style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 28),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.displayLarge?.copyWith(fontSize: 28),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
page['description'],
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
height: 1.5,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -109,7 +118,9 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
decoration: BoxDecoration(
|
||||
color: _currentPage == index
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.primary.withValues(alpha: 0.2),
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
@@ -126,7 +137,11 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(_currentPage == _pages.length - 1 ? 'Get Started' : 'Next'),
|
||||
child: Text(
|
||||
_currentPage == _pages.length - 1
|
||||
? 'Get Started'
|
||||
: 'Next',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -16,7 +16,9 @@ class ProjectRepository {
|
||||
return (response.data as List).map((p) => Project.fromJson(p)).toList();
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception("Projects API endpoint not found. Please ensure the backend server is updated to the latest Docker image.");
|
||||
throw Exception(
|
||||
"Projects API endpoint not found. Please ensure the backend server is updated to the latest Docker image.",
|
||||
);
|
||||
}
|
||||
throw Exception(e.message ?? 'Unknown error fetching projects');
|
||||
}
|
||||
@@ -28,12 +30,15 @@ class ProjectRepository {
|
||||
int? customerId,
|
||||
double? budget,
|
||||
}) async {
|
||||
final response = await _dio.post('/projects', data: {
|
||||
'name': name,
|
||||
if (description != null) 'description': description,
|
||||
if (customerId != null) 'customerId': customerId,
|
||||
if (budget != null) 'budget': budget,
|
||||
});
|
||||
final response = await _dio.post(
|
||||
'/projects',
|
||||
data: {
|
||||
'name': name,
|
||||
if (description != null) 'description': description,
|
||||
if (customerId != null) 'customerId': customerId,
|
||||
if (budget != null) 'budget': budget,
|
||||
},
|
||||
);
|
||||
return Project.fromJson(response.data);
|
||||
}
|
||||
|
||||
@@ -45,13 +50,16 @@ class ProjectRepository {
|
||||
double? budget,
|
||||
String? status,
|
||||
}) async {
|
||||
final response = await _dio.put('/projects/$projectId', data: {
|
||||
if (name != null) 'name': name,
|
||||
if (description != null) 'description': description,
|
||||
if (customerId != null) 'customerId': customerId,
|
||||
if (budget != null) 'budget': budget,
|
||||
if (status != null) 'status': status,
|
||||
});
|
||||
final response = await _dio.put(
|
||||
'/projects/$projectId',
|
||||
data: {
|
||||
if (name != null) 'name': name,
|
||||
if (description != null) 'description': description,
|
||||
if (customerId != null) 'customerId': customerId,
|
||||
if (budget != null) 'budget': budget,
|
||||
if (status != null) 'status': status,
|
||||
},
|
||||
);
|
||||
return Project.fromJson(response.data);
|
||||
}
|
||||
|
||||
@@ -73,14 +81,17 @@ class ProjectRepository {
|
||||
bool? isBillable,
|
||||
double? hourlyRate,
|
||||
}) async {
|
||||
final response = await _dio.post('/projects/$projectId/tasks', data: {
|
||||
'title': title,
|
||||
if (description != null) 'description': description,
|
||||
if (assigneeName != null) 'assigneeName': assigneeName,
|
||||
if (assigneeUserId != null) 'assigneeUserId': assigneeUserId,
|
||||
if (isBillable != null) 'isBillable': isBillable,
|
||||
if (hourlyRate != null) 'hourlyRate': hourlyRate,
|
||||
});
|
||||
final response = await _dio.post(
|
||||
'/projects/$projectId/tasks',
|
||||
data: {
|
||||
'title': title,
|
||||
if (description != null) 'description': description,
|
||||
if (assigneeName != null) 'assigneeName': assigneeName,
|
||||
if (assigneeUserId != null) 'assigneeUserId': assigneeUserId,
|
||||
if (isBillable != null) 'isBillable': isBillable,
|
||||
if (hourlyRate != null) 'hourlyRate': hourlyRate,
|
||||
},
|
||||
);
|
||||
return ProjectTask.fromJson(response.data);
|
||||
}
|
||||
|
||||
@@ -95,25 +106,31 @@ class ProjectRepository {
|
||||
double? hourlyRate,
|
||||
double? hoursLogged,
|
||||
}) async {
|
||||
final response = await _dio.put('/projects/tasks/$taskId', data: {
|
||||
if (title != null) 'title': title,
|
||||
if (description != null) 'description': description,
|
||||
if (assigneeName != null) 'assigneeName': assigneeName,
|
||||
if (assigneeUserId != null) 'assigneeUserId': assigneeUserId,
|
||||
if (status != null) 'status': status,
|
||||
if (isBillable != null) 'isBillable': isBillable,
|
||||
if (hourlyRate != null) 'hourlyRate': hourlyRate,
|
||||
if (hoursLogged != null) 'hoursLogged': hoursLogged,
|
||||
});
|
||||
final response = await _dio.put(
|
||||
'/projects/tasks/$taskId',
|
||||
data: {
|
||||
if (title != null) 'title': title,
|
||||
if (description != null) 'description': description,
|
||||
if (assigneeName != null) 'assigneeName': assigneeName,
|
||||
if (assigneeUserId != null) 'assigneeUserId': assigneeUserId,
|
||||
if (status != null) 'status': status,
|
||||
if (isBillable != null) 'isBillable': isBillable,
|
||||
if (hourlyRate != null) 'hourlyRate': hourlyRate,
|
||||
if (hoursLogged != null) 'hoursLogged': hoursLogged,
|
||||
},
|
||||
);
|
||||
return ProjectTask.fromJson(response.data);
|
||||
}
|
||||
|
||||
Future<void> deleteTask(int taskId) async {
|
||||
await _dio.delete('/projects/tasks/$taskId');
|
||||
}
|
||||
Future<List<ProjectTaskComment>> getComments(int taskId) async {
|
||||
|
||||
Future<List<ProjectTaskComment>> getComments(int taskId) async {
|
||||
final response = await _dio.get('/projects/tasks/$taskId/comments');
|
||||
return (response.data as List).map((c) => ProjectTaskComment.fromJson(c)).toList();
|
||||
return (response.data as List)
|
||||
.map((c) => ProjectTaskComment.fromJson(c))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<ProjectTaskComment> addComment({
|
||||
@@ -121,11 +138,14 @@ Future<List<ProjectTaskComment>> getComments(int taskId) async {
|
||||
required String content,
|
||||
List<Map<String, String>>? attachments,
|
||||
}) async {
|
||||
final response = await _dio.post('/projects/tasks/$taskId/comments', data: {
|
||||
'content': content,
|
||||
if (attachments != null && attachments.isNotEmpty)
|
||||
'imagesJson': jsonEncode(attachments),
|
||||
});
|
||||
final response = await _dio.post(
|
||||
'/projects/tasks/$taskId/comments',
|
||||
data: {
|
||||
'content': content,
|
||||
if (attachments != null && attachments.isNotEmpty)
|
||||
'imagesJson': jsonEncode(attachments),
|
||||
},
|
||||
);
|
||||
return ProjectTaskComment.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ class Project {
|
||||
userId: json['userId'] as int?,
|
||||
budget: (json['budget'] as num?)?.toDouble() ?? 0.0,
|
||||
status: json['status'] as String? ?? 'ACTIVE',
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ class ProjectTask {
|
||||
isBillable: json['isBillable'] as bool? ?? false,
|
||||
hourlyRate: (json['hourlyRate'] as num?)?.toDouble() ?? 0.0,
|
||||
hoursLogged: (json['hoursLogged'] as num?)?.toDouble() ?? 0.0,
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ class _ProjectBoardScreenState extends ConsumerState<ProjectBoardScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
appBar: AppBar(
|
||||
title: Text(widget.project.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
title: Text(
|
||||
widget.project.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
@@ -49,7 +52,12 @@ class _ProjectBoardScreenState extends ConsumerState<ProjectBoardScreen> {
|
||||
data: (tasks) {
|
||||
final columns = [
|
||||
_ColumnDef('To Do', 'TODO', Colors.blueGrey, LucideIcons.circle),
|
||||
_ColumnDef('In Progress', 'IN_PROGRESS', Colors.blue, LucideIcons.loader),
|
||||
_ColumnDef(
|
||||
'In Progress',
|
||||
'IN_PROGRESS',
|
||||
Colors.blue,
|
||||
LucideIcons.loader,
|
||||
),
|
||||
_ColumnDef('Review', 'REVIEW', Colors.orange, LucideIcons.eye),
|
||||
_ColumnDef('Done', 'DONE', Colors.green, LucideIcons.checkCircle),
|
||||
];
|
||||
@@ -60,8 +68,14 @@ class _ProjectBoardScreenState extends ConsumerState<ProjectBoardScreen> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: columns.map((col) {
|
||||
final columnTasks = tasks.where((t) => t.status == col.status).toList();
|
||||
return _buildColumn(col, columnTasks, showAdd: col.status == 'TODO');
|
||||
final columnTasks = tasks
|
||||
.where((t) => t.status == col.status)
|
||||
.toList();
|
||||
return _buildColumn(
|
||||
col,
|
||||
columnTasks,
|
||||
showAdd: col.status == 'TODO',
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
@@ -90,7 +104,9 @@ class _ProjectBoardScreenState extends ConsumerState<ProjectBoardScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: col.color.withValues(alpha: 0.08),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -106,14 +122,21 @@ class _ProjectBoardScreenState extends ConsumerState<ProjectBoardScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: col.color.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${tasks.length}',
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: col.color),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: col.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -121,41 +144,41 @@ class _ProjectBoardScreenState extends ConsumerState<ProjectBoardScreen> {
|
||||
),
|
||||
|
||||
// Task cards
|
||||
...tasks.map((t) => TaskCard(
|
||||
task: t,
|
||||
onEdit: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => AddTaskSheet(
|
||||
projectId: widget.project.id,
|
||||
task: t,
|
||||
),
|
||||
);
|
||||
},
|
||||
onGenerateInvoice: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceBuilderScreen(
|
||||
initialCustomerId: widget.project.customerId,
|
||||
initialItems: [
|
||||
InvoiceItem(
|
||||
id: 0,
|
||||
invoiceId: 0,
|
||||
productId: null,
|
||||
description: t.title,
|
||||
quantity: t.hoursLogged,
|
||||
unitPrice: t.hourlyRate,
|
||||
total: t.hoursLogged * t.hourlyRate,
|
||||
)
|
||||
],
|
||||
...tasks.map(
|
||||
(t) => TaskCard(
|
||||
task: t,
|
||||
onEdit: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) =>
|
||||
AddTaskSheet(projectId: widget.project.id, task: t),
|
||||
);
|
||||
},
|
||||
onGenerateInvoice: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => InvoiceBuilderScreen(
|
||||
initialCustomerId: widget.project.customerId,
|
||||
initialItems: [
|
||||
InvoiceItem(
|
||||
id: 0,
|
||||
invoiceId: 0,
|
||||
productId: null,
|
||||
description: t.title,
|
||||
quantity: t.hoursLogged,
|
||||
unitPrice: t.hourlyRate,
|
||||
total: t.hoursLogged * t.hourlyRate,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
if (showAdd)
|
||||
Padding(
|
||||
@@ -166,17 +189,23 @@ class _ProjectBoardScreenState extends ConsumerState<ProjectBoardScreen> {
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: col.color,
|
||||
side: BorderSide(color: col.color.withValues(alpha: 0.3)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
icon: const Icon(LucideIcons.plus, size: 16),
|
||||
label: const Text('Add Task', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
label: const Text(
|
||||
'Add Task',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => AddTaskSheet(projectId: widget.project.id),
|
||||
builder: (_) =>
|
||||
AddTaskSheet(projectId: widget.project.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -31,20 +31,37 @@ class ProjectHubScreen extends ConsumerWidget {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.2)),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.layoutDashboard, color: Theme.of(context).colorScheme.primary, size: 32),
|
||||
Icon(
|
||||
LucideIcons.layoutDashboard,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Agency OS', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const Text('Manage projects, tasks, and client billing', style: TextStyle(color: Colors.grey)),
|
||||
Text(
|
||||
'Agency OS',
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Text(
|
||||
'Manage projects, tasks, and client billing',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -67,7 +84,10 @@ class ProjectHubScreen extends ConsumerWidget {
|
||||
'Manage all projects',
|
||||
LucideIcons.folderKanban,
|
||||
Colors.blue,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const ProjectsScreen())),
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ProjectsScreen()),
|
||||
),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
@@ -75,7 +95,10 @@ class ProjectHubScreen extends ConsumerWidget {
|
||||
'Client billing',
|
||||
LucideIcons.receipt,
|
||||
Colors.green,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const InvoicesListScreen())),
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const InvoicesListScreen()),
|
||||
),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
@@ -83,7 +106,12 @@ class ProjectHubScreen extends ConsumerWidget {
|
||||
'Manage clients',
|
||||
LucideIcons.users,
|
||||
NatureColors.getColor('RECEIVABLES'),
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomersListScreen())),
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const CustomersListScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildActionCard(
|
||||
context,
|
||||
@@ -91,7 +119,10 @@ class ProjectHubScreen extends ConsumerWidget {
|
||||
'Manage vendors',
|
||||
LucideIcons.truck,
|
||||
Colors.orange,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (_) => const VendorsListScreen())),
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const VendorsListScreen()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -100,7 +131,14 @@ class ProjectHubScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionCard(BuildContext context, String title, String subtitle, IconData icon, Color color, VoidCallback onTap) {
|
||||
Widget _buildActionCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String subtitle,
|
||||
IconData icon,
|
||||
Color color,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
@@ -116,9 +154,22 @@ class ProjectHubScreen extends ConsumerWidget {
|
||||
children: [
|
||||
Icon(icon, color: color, size: 28),
|
||||
const SizedBox(height: 12),
|
||||
Text(title, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(subtitle, style: TextStyle(color: color.withValues(alpha: 0.7), fontSize: 12)),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
color: color.withValues(alpha: 0.7),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -22,7 +22,10 @@ class _ProjectsScreenState extends ConsumerState<ProjectsScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
appBar: AppBar(
|
||||
title: const Text('Projects', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
title: const Text(
|
||||
'Projects',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
@@ -37,12 +40,6 @@ class _ProjectsScreenState extends ConsumerState<ProjectsScreen> {
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search projects...',
|
||||
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
onChanged: (val) {
|
||||
@@ -53,15 +50,28 @@ class _ProjectsScreenState extends ConsumerState<ProjectsScreen> {
|
||||
Expanded(
|
||||
child: projectsAsync.when(
|
||||
data: (projects) {
|
||||
final filteredProjects = projects.where((p) => p.name.toLowerCase().contains(_searchQuery.toLowerCase())).toList();
|
||||
final filteredProjects = projects
|
||||
.where(
|
||||
(p) => p.name.toLowerCase().contains(
|
||||
_searchQuery.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
if (filteredProjects.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(LucideIcons.folderKanban, size: 48, color: Colors.grey.shade400),
|
||||
Icon(
|
||||
LucideIcons.folderKanban,
|
||||
size: 48,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('No projects found.', style: TextStyle(color: Colors.grey.shade500)),
|
||||
Text(
|
||||
'No projects found.',
|
||||
style: TextStyle(color: Colors.grey.shade500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -75,33 +85,56 @@ class _ProjectsScreenState extends ConsumerState<ProjectsScreen> {
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
title: Text(p.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(p.description ?? 'No description', style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
title: Text(
|
||||
p.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
p.description ?? 'No description',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(LucideIcons.edit, size: 18, color: Colors.grey.shade600),
|
||||
icon: Icon(
|
||||
LucideIcons.edit,
|
||||
size: 18,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => AddProjectSheet(project: p),
|
||||
builder: (context) =>
|
||||
AddProjectSheet(project: p),
|
||||
);
|
||||
},
|
||||
),
|
||||
Icon(Icons.chevron_right, color: Colors.grey.shade400),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ProjectBoardScreen(project: p),
|
||||
builder: (context) =>
|
||||
ProjectBoardScreen(project: p),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -56,131 +56,185 @@ class _AddTaskSheetState extends ConsumerState<AddTaskSheet> {
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(_isEditing ? 'Edit Task' : 'New Task', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
IconButton(icon: const Icon(LucideIcons.x), onPressed: () => Navigator.pop(context)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PremiumTextField(
|
||||
labelText: 'Task Title',
|
||||
initialValue: _isEditing ? _title : null,
|
||||
prefixIcon: const Icon(LucideIcons.checkSquare),
|
||||
validator: (val) => val == null || val.isEmpty ? 'Required' : null,
|
||||
onSaved: (val) => _title = val!,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PremiumTextField(
|
||||
maxLines: 3,
|
||||
labelText: 'Description (Optional)',
|
||||
initialValue: _isEditing ? _description : null,
|
||||
prefixIcon: const Icon(LucideIcons.alignLeft),
|
||||
onSaved: (val) => _description = val ?? '',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Autocomplete<User>(
|
||||
initialValue: _isEditing && _assigneeName.isNotEmpty
|
||||
? TextEditingValue(text: _assigneeName)
|
||||
: null,
|
||||
optionsBuilder: (TextEditingValue textEditingValue) async {
|
||||
if (textEditingValue.text.isEmpty) {
|
||||
return const Iterable<User>.empty();
|
||||
}
|
||||
try {
|
||||
return await ref.read(userSearchProvider(textEditingValue.text).future);
|
||||
} catch (e) {
|
||||
return const Iterable<User>.empty();
|
||||
}
|
||||
},
|
||||
displayStringForOption: (User option) => '${option.name} (${option.email})',
|
||||
onSelected: (User selection) {
|
||||
_assigneeName = selection.name;
|
||||
},
|
||||
optionsViewBuilder: (context, onSelected, options) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final User option = options.elementAt(index);
|
||||
return ListTile(
|
||||
title: Text(option.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(option.email, style: const TextStyle(color: Colors.grey)),
|
||||
onTap: () => onSelected(option),
|
||||
);
|
||||
},
|
||||
),
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_isEditing ? 'Edit Task' : 'New Task',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) {
|
||||
return PremiumTextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
labelText: 'Assignee Name (Optional)',
|
||||
prefixIcon: const Icon(LucideIcons.user),
|
||||
onSaved: (val) {
|
||||
if (_assigneeName.isEmpty) _assigneeName = val ?? '';
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Is Billable?'),
|
||||
subtitle: const Text('Task time will be invoiced'),
|
||||
value: _isBillable,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_isBillable = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_isBillable) ...[
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.x),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PremiumTextField(
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
labelText: 'Hourly Rate',
|
||||
initialValue: _isEditing && _hourlyRate > 0 ? _hourlyRate.toString() : null,
|
||||
prefixIcon: const Icon(LucideIcons.indianRupee),
|
||||
validator: (val) => _isBillable && (val == null || val.isEmpty) ? 'Required for billable tasks' : null,
|
||||
onSaved: (val) => _hourlyRate = double.tryParse(val ?? '0') ?? 0,
|
||||
labelText: 'Task Title',
|
||||
initialValue: _isEditing ? _title : null,
|
||||
prefixIcon: const Icon(LucideIcons.checkSquare),
|
||||
validator: (val) =>
|
||||
val == null || val.isEmpty ? 'Required' : null,
|
||||
onSaved: (val) => _title = val!,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PremiumTextField(
|
||||
maxLines: 3,
|
||||
labelText: 'Description (Optional)',
|
||||
initialValue: _isEditing ? _description : null,
|
||||
prefixIcon: const Icon(LucideIcons.alignLeft),
|
||||
onSaved: (val) => _description = val ?? '',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Autocomplete<User>(
|
||||
initialValue: _isEditing && _assigneeName.isNotEmpty
|
||||
? TextEditingValue(text: _assigneeName)
|
||||
: null,
|
||||
optionsBuilder: (TextEditingValue textEditingValue) async {
|
||||
if (textEditingValue.text.isEmpty) {
|
||||
return const Iterable<User>.empty();
|
||||
}
|
||||
try {
|
||||
return await ref.read(
|
||||
userSearchProvider(textEditingValue.text).future,
|
||||
);
|
||||
} catch (e) {
|
||||
return const Iterable<User>.empty();
|
||||
}
|
||||
},
|
||||
displayStringForOption: (User option) =>
|
||||
'${option.name} (${option.email})',
|
||||
onSelected: (User selection) {
|
||||
_assigneeName = selection.name;
|
||||
},
|
||||
optionsViewBuilder: (context, onSelected, options) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 200,
|
||||
maxWidth: 300,
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final User option = options.elementAt(index);
|
||||
return ListTile(
|
||||
title: Text(
|
||||
option.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
option.email,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
onTap: () => onSelected(option),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
fieldViewBuilder:
|
||||
(
|
||||
context,
|
||||
textEditingController,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
return PremiumTextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
labelText: 'Assignee Name (Optional)',
|
||||
prefixIcon: const Icon(LucideIcons.user),
|
||||
onSaved: (val) {
|
||||
if (_assigneeName.isEmpty)
|
||||
_assigneeName = val ?? '';
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Is Billable?'),
|
||||
subtitle: const Text('Task time will be invoiced'),
|
||||
value: _isBillable,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_isBillable = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_isBillable) ...[
|
||||
const SizedBox(height: 16),
|
||||
PremiumTextField(
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
labelText: 'Hourly Rate',
|
||||
initialValue: _isEditing && _hourlyRate > 0
|
||||
? _hourlyRate.toString()
|
||||
: null,
|
||||
prefixIcon: const Icon(LucideIcons.indianRupee),
|
||||
validator: (val) =>
|
||||
_isBillable && (val == null || val.isEmpty)
|
||||
? 'Required for billable tasks'
|
||||
: null,
|
||||
onSaved: (val) =>
|
||||
_hourlyRate = double.tryParse(val ?? '0') ?? 0,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: _isLoading ? null : _submit,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_isEditing ? 'Save Changes' : 'Create Task',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: _isLoading ? null : _submit,
|
||||
child: _isLoading
|
||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: Text(_isEditing ? 'Save Changes' : 'Create Task', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,7 +270,9 @@ class _AddTaskSheetState extends ConsumerState<AddTaskSheet> {
|
||||
if (mounted) Navigator.pop(context);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
|
||||
@@ -85,7 +85,10 @@ class TaskCard extends ConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -121,7 +124,11 @@ class TaskCard extends ConsumerWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
task.description!,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600, height: 1.4),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
height: 1.4,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -132,9 +139,13 @@ class TaskCard extends ConsumerWidget {
|
||||
// Bottom row: assignee + billable badge + invoice button
|
||||
Row(
|
||||
children: [
|
||||
if (task.assigneeName != null && task.assigneeName!.isNotEmpty) ...[
|
||||
if (task.assigneeName != null &&
|
||||
task.assigneeName!.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -142,11 +153,19 @@ class TaskCard extends ConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(LucideIcons.user, size: 12, color: Colors.indigo.shade400),
|
||||
Icon(
|
||||
LucideIcons.user,
|
||||
size: 12,
|
||||
color: Colors.indigo.shade400,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
task.assigneeName!,
|
||||
style: TextStyle(fontSize: 11, color: Colors.indigo.shade600, fontWeight: FontWeight.w500),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.indigo.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -155,7 +174,10 @@ class TaskCard extends ConsumerWidget {
|
||||
],
|
||||
if (task.isBillable)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -163,11 +185,19 @@ class TaskCard extends ConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(LucideIcons.indianRupee, size: 11, color: Colors.green.shade600),
|
||||
Icon(
|
||||
LucideIcons.indianRupee,
|
||||
size: 11,
|
||||
color: Colors.green.shade600,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'Billable',
|
||||
style: TextStyle(color: Colors.green.shade700, fontSize: 11, fontWeight: FontWeight.w500),
|
||||
style: TextStyle(
|
||||
color: Colors.green.shade700,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -180,10 +210,23 @@ class TaskCard extends ConsumerWidget {
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
backgroundColor: Colors.green.withValues(alpha: 0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
LucideIcons.receipt,
|
||||
size: 13,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
label: Text(
|
||||
'Invoice',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.green.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
icon: Icon(LucideIcons.receipt, size: 13, color: Colors.green.shade700),
|
||||
label: Text('Invoice', style: TextStyle(fontSize: 11, color: Colors.green.shade700, fontWeight: FontWeight.w600)),
|
||||
onPressed: onGenerateInvoice,
|
||||
),
|
||||
),
|
||||
@@ -219,7 +262,13 @@ class TaskCard extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(task.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text(
|
||||
task.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.eye),
|
||||
@@ -245,30 +294,45 @@ class TaskCard extends ConsumerWidget {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.trash2, color: Colors.red),
|
||||
title: const Text('Delete Task', style: TextStyle(color: Colors.red)),
|
||||
title: const Text(
|
||||
'Delete Task',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(ctx);
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Delete Task'),
|
||||
content: Text('Are you sure you want to delete "${task.title}"?'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete "${task.title}"?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(c, false), child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, true),
|
||||
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
||||
child: const Text(
|
||||
'Delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirm == true) {
|
||||
try {
|
||||
await ref.read(projectRepositoryProvider).deleteTask(task.id);
|
||||
await ref
|
||||
.read(projectRepositoryProvider)
|
||||
.deleteTask(task.id);
|
||||
ref.invalidate(projectTasksProvider(task.projectId));
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,18 +352,23 @@ class TaskCard extends ConsumerWidget {
|
||||
tooltip: 'Change Status',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
child: Icon(LucideIcons.moreVertical, size: 18, color: Colors.grey.shade500),
|
||||
child: Icon(
|
||||
LucideIcons.moreVertical,
|
||||
size: 18,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
onSelected: (newStatus) async {
|
||||
if (newStatus != task.status) {
|
||||
try {
|
||||
await ref.read(projectRepositoryProvider).updateTask(
|
||||
taskId: task.id,
|
||||
status: newStatus,
|
||||
);
|
||||
await ref
|
||||
.read(projectRepositoryProvider)
|
||||
.updateTask(taskId: task.id, status: newStatus);
|
||||
ref.invalidate(projectTasksProvider(task.projectId));
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to update status: $e')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to update status: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,7 +382,11 @@ class TaskCard extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<String> _buildStatusMenuItem(String value, String label, Color color) {
|
||||
PopupMenuItem<String> _buildStatusMenuItem(
|
||||
String value,
|
||||
String label,
|
||||
Color color,
|
||||
) {
|
||||
return PopupMenuItem<String>(
|
||||
value: value,
|
||||
child: Row(
|
||||
|
||||
@@ -63,9 +63,12 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
final bytes = await f.readAsBytes();
|
||||
final extension = file.path!.split('.').last.toLowerCase();
|
||||
String contentType = 'application/octet-stream';
|
||||
if (['pdf'].contains(extension)) contentType = 'application/pdf';
|
||||
else if (['doc', 'docx'].contains(extension)) contentType = 'application/msword';
|
||||
else if (['png', 'jpg', 'jpeg'].contains(extension)) contentType = 'image/jpeg';
|
||||
if (['pdf'].contains(extension))
|
||||
contentType = 'application/pdf';
|
||||
else if (['doc', 'docx'].contains(extension))
|
||||
contentType = 'application/msword';
|
||||
else if (['png', 'jpg', 'jpeg'].contains(extension))
|
||||
contentType = 'image/jpeg';
|
||||
|
||||
setState(() {
|
||||
_attachments.add({
|
||||
@@ -98,14 +101,20 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
ref.invalidate(projectTaskCommentsProvider(widget.task.id));
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error adding comment: $e')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error adding comment: $e')));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadAttachment(Map<String, dynamic> attMap, int commentId, int attIndex) async {
|
||||
Future<void> _downloadAttachment(
|
||||
Map<String, dynamic> attMap,
|
||||
int commentId,
|
||||
int attIndex,
|
||||
) async {
|
||||
final filePath = attMap['filePath'];
|
||||
final fileName = attMap['fileName'] ?? 'attachment';
|
||||
final contentType = attMap['contentType'] ?? 'application/octet-stream';
|
||||
@@ -121,16 +130,21 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
if (_jwtToken != null) {
|
||||
dio.options.headers['Authorization'] = 'Bearer $_jwtToken';
|
||||
}
|
||||
final url = 'https://app.technobeesolutions.in/api/kifi/projects/tasks/attachments/download?filePath=$filePath&contentType=$contentType';
|
||||
final url =
|
||||
'https://app.technobeesolutions.in/api/kifi-v2/projects/tasks/attachments/download?filePath=$filePath&contentType=$contentType';
|
||||
await dio.download(url, savePath);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Downloaded $fileName')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Downloaded $fileName')));
|
||||
await Share.shareXFiles([XFile(savePath)]);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error downloading: $e')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error downloading: $e')));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -141,7 +155,9 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final commentsAsync = ref.watch(projectTaskCommentsProvider(widget.task.id));
|
||||
final commentsAsync = ref.watch(
|
||||
projectTaskCommentsProvider(widget.task.id),
|
||||
);
|
||||
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.9,
|
||||
@@ -155,7 +171,8 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
Expanded(
|
||||
child: commentsAsync.when(
|
||||
data: (comments) {
|
||||
if (comments.isEmpty) return const Center(child: Text('No comments yet.'));
|
||||
if (comments.isEmpty)
|
||||
return const Center(child: Text('No comments yet.'));
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: comments.length,
|
||||
@@ -170,7 +187,10 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(c.content, style: const TextStyle(fontSize: 14)),
|
||||
Text(
|
||||
c.content,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
if (c.images.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
@@ -189,60 +209,111 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
final isImage = isOldBase64 || (parsedData?['contentType']?.startsWith('image/') ?? false);
|
||||
final isImage =
|
||||
isOldBase64 ||
|
||||
(parsedData?['contentType']?.startsWith(
|
||||
'image/',
|
||||
) ??
|
||||
false);
|
||||
|
||||
if (isImage) {
|
||||
ImageProvider provider;
|
||||
if (isOldBase64 || parsedData?['base64Content'] != null) {
|
||||
final b64 = isOldBase64 ? imgData : parsedData!['base64Content'];
|
||||
provider = MemoryImage(base64Decode(b64));
|
||||
if (isOldBase64 ||
|
||||
parsedData?['base64Content'] != null) {
|
||||
final b64 = isOldBase64
|
||||
? imgData
|
||||
: parsedData!['base64Content'];
|
||||
provider = MemoryImage(base64Decode(b64));
|
||||
} else {
|
||||
final url = 'https://app.technobeesolutions.in/api/kifi/projects/tasks/attachments/download?filePath=${parsedData!['filePath']}&contentType=${parsedData['contentType']}';
|
||||
provider = _jwtToken != null
|
||||
? NetworkImage(url, headers: {'Authorization': 'Bearer $_jwtToken'})
|
||||
: const AssetImage('assets/images/placeholder.png') as ImageProvider;
|
||||
final url =
|
||||
'https://app.technobeesolutions.in/api/kifi-v2/projects/tasks/attachments/download?filePath=${parsedData!['filePath']}&contentType=${parsedData['contentType']}';
|
||||
provider = _jwtToken != null
|
||||
? NetworkImage(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization':
|
||||
'Bearer $_jwtToken',
|
||||
},
|
||||
)
|
||||
: const AssetImage(
|
||||
'assets/images/placeholder.png',
|
||||
)
|
||||
as ImageProvider;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// Collect all images for the gallery
|
||||
final allImages = c.images.where((i) {
|
||||
if (!i.startsWith('{')) return true;
|
||||
try {
|
||||
final map = jsonDecode(i);
|
||||
return map['contentType']?.startsWith('image/') ?? false;
|
||||
} catch (_) {
|
||||
final allImages = c.images
|
||||
.where((i) {
|
||||
if (!i.startsWith('{'))
|
||||
return true;
|
||||
}
|
||||
}).map((i) {
|
||||
if (!i.startsWith('{')) return MemoryImage(base64Decode(i)) as ImageProvider;
|
||||
final map = jsonDecode(i);
|
||||
if (map['base64Content'] != null) return MemoryImage(base64Decode(map['base64Content'])) as ImageProvider;
|
||||
final url = 'https://app.technobeesolutions.in/api/kifi/projects/tasks/attachments/download?filePath=${map['filePath']}&contentType=${map['contentType']}';
|
||||
return NetworkImage(url, headers: {'Authorization': 'Bearer $_jwtToken'}) as ImageProvider;
|
||||
}).toList();
|
||||
try {
|
||||
final map = jsonDecode(i);
|
||||
return map['contentType']
|
||||
?.startsWith(
|
||||
'image/',
|
||||
) ??
|
||||
false;
|
||||
} catch (_) {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.map((i) {
|
||||
if (!i.startsWith('{'))
|
||||
return MemoryImage(
|
||||
base64Decode(i),
|
||||
)
|
||||
as ImageProvider;
|
||||
final map = jsonDecode(i);
|
||||
if (map['base64Content'] != null)
|
||||
return MemoryImage(
|
||||
base64Decode(
|
||||
map['base64Content'],
|
||||
),
|
||||
)
|
||||
as ImageProvider;
|
||||
final url =
|
||||
'https://app.technobeesolutions.in/api/kifi-v2/projects/tasks/attachments/download?filePath=${map['filePath']}&contentType=${map['contentType']}';
|
||||
return NetworkImage(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization':
|
||||
'Bearer $_jwtToken',
|
||||
},
|
||||
)
|
||||
as ImageProvider;
|
||||
})
|
||||
.toList();
|
||||
|
||||
// Find index of clicked image among images
|
||||
int realIndex = 0;
|
||||
for (int i=0; i<=imgIndex; i++) {
|
||||
final ii = c.images[i];
|
||||
bool img = !ii.startsWith('{');
|
||||
if (!img) {
|
||||
try {
|
||||
final m = jsonDecode(ii);
|
||||
img = m['contentType']?.startsWith('image/') ?? false;
|
||||
} catch(_) { img = true; }
|
||||
for (int i = 0; i <= imgIndex; i++) {
|
||||
final ii = c.images[i];
|
||||
bool img = !ii.startsWith('{');
|
||||
if (!img) {
|
||||
try {
|
||||
final m = jsonDecode(ii);
|
||||
img =
|
||||
m['contentType']?.startsWith(
|
||||
'image/',
|
||||
) ??
|
||||
false;
|
||||
} catch (_) {
|
||||
img = true;
|
||||
}
|
||||
if (img && i < imgIndex) realIndex++;
|
||||
}
|
||||
if (img && i < imgIndex) realIndex++;
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AttachmentGalleryScreen(
|
||||
images: allImages,
|
||||
initialIndex: realIndex,
|
||||
),
|
||||
builder: (context) =>
|
||||
AttachmentGalleryScreen(
|
||||
images: allImages,
|
||||
initialIndex: realIndex,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -258,14 +329,21 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
);
|
||||
} else {
|
||||
// Non-image file
|
||||
final fileName = parsedData?['fileName'] ?? 'Document';
|
||||
final fileName =
|
||||
parsedData?['fileName'] ?? 'Document';
|
||||
final uniqueId = c.id * 1000 + imgIndex;
|
||||
final isDownloading = _downloadingIds.contains(uniqueId);
|
||||
final isDownloading = _downloadingIds
|
||||
.contains(uniqueId);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (!isDownloading && parsedData != null) {
|
||||
_downloadAttachment(parsedData, c.id, imgIndex);
|
||||
if (!isDownloading &&
|
||||
parsedData != null) {
|
||||
_downloadAttachment(
|
||||
parsedData,
|
||||
c.id,
|
||||
imgIndex,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
@@ -273,21 +351,41 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
isDownloading
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Icon(LucideIcons.fileText, color: Colors.blue),
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
LucideIcons.fileText,
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
child: Text(
|
||||
fileName,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -298,8 +396,8 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
)
|
||||
]
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -320,7 +418,9 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.grey.shade300))),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -328,13 +428,26 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.task.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
if (widget.task.description != null && widget.task.description!.isNotEmpty)
|
||||
Text(widget.task.description!, style: const TextStyle(color: Colors.grey)),
|
||||
Text(
|
||||
widget.task.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (widget.task.description != null &&
|
||||
widget.task.description!.isNotEmpty)
|
||||
Text(
|
||||
widget.task.description!,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(icon: const Icon(LucideIcons.x), onPressed: () => Navigator.pop(context)),
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.x),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -362,7 +475,8 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
spacing: 8,
|
||||
children: _attachments.asMap().entries.map((entry) {
|
||||
final att = entry.value;
|
||||
final isImg = att['contentType']?.startsWith('image/') ?? false;
|
||||
final isImg =
|
||||
att['contentType']?.startsWith('image/') ?? false;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
@@ -374,20 +488,29 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: isImg
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.memory(
|
||||
base64Decode(att['base64Content']!),
|
||||
fit: BoxFit.cover,
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.memory(
|
||||
base64Decode(att['base64Content']!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.fileText,
|
||||
size: 24,
|
||||
color: Colors.blue,
|
||||
),
|
||||
Text(
|
||||
att['fileName'] ?? '',
|
||||
style: const TextStyle(fontSize: 8),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(LucideIcons.fileText, size: 24, color: Colors.blue),
|
||||
Text(att['fileName'] ?? '', style: const TextStyle(fontSize: 8), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
@@ -400,7 +523,11 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.black54,
|
||||
child: const Icon(LucideIcons.x, size: 16, color: Colors.white),
|
||||
child: const Icon(
|
||||
LucideIcons.x,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -429,7 +556,14 @@ class _TaskDetailsSheetState extends ConsumerState<TaskDetailsSheet> {
|
||||
),
|
||||
),
|
||||
_isSubmitting
|
||||
? const Padding(padding: EdgeInsets.all(12.0), child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(LucideIcons.send, color: Colors.blue),
|
||||
onPressed: _submitComment,
|
||||
|
||||
@@ -15,12 +15,16 @@ final projectsProvider = FutureProvider<List<Project>>((ref) async {
|
||||
return repo.getProjects();
|
||||
});
|
||||
|
||||
final projectTasksProvider = FutureProvider.family<List<ProjectTask>, int>((ref, projectId) async {
|
||||
final projectTasksProvider = FutureProvider.family<List<ProjectTask>, int>((
|
||||
ref,
|
||||
projectId,
|
||||
) async {
|
||||
final repo = ref.watch(projectRepositoryProvider);
|
||||
return repo.getTasks(projectId);
|
||||
});
|
||||
|
||||
final projectTaskCommentsProvider = FutureProvider.family<List<ProjectTaskComment>, int>((ref, taskId) async {
|
||||
final repo = ref.watch(projectRepositoryProvider);
|
||||
return repo.getComments(taskId);
|
||||
});
|
||||
final projectTaskCommentsProvider =
|
||||
FutureProvider.family<List<ProjectTaskComment>, int>((ref, taskId) async {
|
||||
final repo = ref.watch(projectRepositoryProvider);
|
||||
return repo.getComments(taskId);
|
||||
});
|
||||
|
||||
@@ -3,11 +3,17 @@ import '../../../../core/network/dio_client.dart';
|
||||
import '../../../../core/domain/user.dart';
|
||||
import 'dart:async';
|
||||
|
||||
final userSearchProvider = FutureProvider.family<List<User>, String>((ref, query) async {
|
||||
final userSearchProvider = FutureProvider.family<List<User>, String>((
|
||||
ref,
|
||||
query,
|
||||
) async {
|
||||
if (query.isEmpty) return [];
|
||||
|
||||
final dio = DioClient().dio;
|
||||
final response = await dio.get('/users/search', queryParameters: {'q': query});
|
||||
final response = await dio.get(
|
||||
'/users/search',
|
||||
queryParameters: {'q': query},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List data = response.data;
|
||||
|
||||
@@ -48,7 +48,9 @@ class Customer {
|
||||
fatherName: json['fatherName'],
|
||||
gender: json['gender'],
|
||||
age: json['age'],
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,9 @@ class Invoice {
|
||||
discountTotal: json['discountTotal']?.toDouble() ?? 0.0,
|
||||
totalAmount: json['totalAmount'].toDouble(),
|
||||
amountPaid: json['amountPaid']?.toDouble() ?? 0.0,
|
||||
nextPaymentDate: json['nextPaymentDate'] != null ? DateTime.parse(json['nextPaymentDate']) : null,
|
||||
nextPaymentDate: json['nextPaymentDate'] != null
|
||||
? DateTime.parse(json['nextPaymentDate'])
|
||||
: null,
|
||||
paymentMethod: json['paymentMethod'],
|
||||
paymentWalletId: json['paymentWalletId'],
|
||||
status: json['status'] ?? 'DRAFT',
|
||||
@@ -172,9 +174,17 @@ class Invoice {
|
||||
isEmi: json['isEmi'] ?? false,
|
||||
emiAmount: json['emiAmount']?.toDouble(),
|
||||
emiCycle: json['emiCycle'],
|
||||
emiStartDate: json['emiStartDate'] != null ? DateTime.parse(json['emiStartDate']) : null,
|
||||
items: json['items'] != null ? (json['items'] as List).map((i) => InvoiceItem.fromJson(i)).toList() : [],
|
||||
payments: json['payments'] != null ? (json['payments'] as List).map((i) => InvoicePayment.fromJson(i)).toList() : null,
|
||||
emiStartDate: json['emiStartDate'] != null
|
||||
? DateTime.parse(json['emiStartDate'])
|
||||
: null,
|
||||
items: json['items'] != null
|
||||
? (json['items'] as List).map((i) => InvoiceItem.fromJson(i)).toList()
|
||||
: [],
|
||||
payments: json['payments'] != null
|
||||
? (json['payments'] as List)
|
||||
.map((i) => InvoicePayment.fromJson(i))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,13 +194,17 @@ class Invoice {
|
||||
if (customerId != null) data['customerId'] = customerId;
|
||||
data['invoiceNumber'] = invoiceNumber;
|
||||
data['issueDate'] = issueDate.toIso8601String().split('T')[0];
|
||||
if (dueDate != null) data['dueDate'] = dueDate!.toIso8601String().split('T')[0];
|
||||
if (dueDate != null)
|
||||
data['dueDate'] = dueDate!.toIso8601String().split('T')[0];
|
||||
data['subtotal'] = subtotal;
|
||||
data['taxTotal'] = taxTotal;
|
||||
data['discountTotal'] = discountTotal;
|
||||
data['totalAmount'] = totalAmount;
|
||||
if (amountPaid > 0) data['amountPaid'] = amountPaid;
|
||||
if (nextPaymentDate != null) data['nextPaymentDate'] = nextPaymentDate!.toIso8601String().split('T')[0];
|
||||
if (nextPaymentDate != null)
|
||||
data['nextPaymentDate'] = nextPaymentDate!.toIso8601String().split(
|
||||
'T',
|
||||
)[0];
|
||||
if (paymentMethod != null) data['paymentMethod'] = paymentMethod;
|
||||
if (paymentWalletId != null) data['paymentWalletId'] = paymentWalletId;
|
||||
data['status'] = status;
|
||||
@@ -198,9 +212,11 @@ class Invoice {
|
||||
data['isEmi'] = isEmi;
|
||||
if (emiAmount != null) data['emiAmount'] = emiAmount;
|
||||
if (emiCycle != null) data['emiCycle'] = emiCycle;
|
||||
if (emiStartDate != null) data['emiStartDate'] = emiStartDate!.toIso8601String().split('T')[0];
|
||||
if (emiStartDate != null)
|
||||
data['emiStartDate'] = emiStartDate!.toIso8601String().split('T')[0];
|
||||
data['items'] = items.map((i) => i.toJson()).toList();
|
||||
if (payments != null) data['payments'] = payments!.map((i) => i.toJson()).toList();
|
||||
if (payments != null)
|
||||
data['payments'] = payments!.map((i) => i.toJson()).toList();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -229,7 +245,9 @@ class InvoicePayment {
|
||||
id: json['id'],
|
||||
invoiceId: json['invoiceId'],
|
||||
amount: json['amount'].toDouble(),
|
||||
paymentDate: json['paymentDate'] != null ? DateTime.parse(json['paymentDate']) : null,
|
||||
paymentDate: json['paymentDate'] != null
|
||||
? DateTime.parse(json['paymentDate'])
|
||||
: null,
|
||||
paymentMethod: json['paymentMethod'] ?? 'Cash',
|
||||
emiInstallmentNumber: json['emiInstallmentNumber'],
|
||||
walletId: json['walletId'],
|
||||
@@ -241,9 +259,11 @@ class InvoicePayment {
|
||||
if (id != null) data['id'] = id;
|
||||
if (invoiceId != null) data['invoiceId'] = invoiceId;
|
||||
data['amount'] = amount;
|
||||
if (paymentDate != null) data['paymentDate'] = paymentDate!.toIso8601String().split('T')[0];
|
||||
if (paymentDate != null)
|
||||
data['paymentDate'] = paymentDate!.toIso8601String().split('T')[0];
|
||||
data['paymentMethod'] = paymentMethod;
|
||||
if (emiInstallmentNumber != null) data['emiInstallmentNumber'] = emiInstallmentNumber;
|
||||
if (emiInstallmentNumber != null)
|
||||
data['emiInstallmentNumber'] = emiInstallmentNumber;
|
||||
if (walletId != null) data['walletId'] = walletId;
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as import_storage;
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart'
|
||||
as import_storage;
|
||||
import '../../../core/widgets/premium_text_field.dart';
|
||||
import '../../../core/widgets/smart_search_dropdown.dart';
|
||||
import '../../business/providers/indian_states_provider.dart';
|
||||
@@ -45,9 +46,15 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
_emailCtrl = TextEditingController(text: widget.customer?.email ?? '');
|
||||
_addressCtrl = TextEditingController(text: widget.customer?.address ?? '');
|
||||
_gstinCtrl = TextEditingController(text: widget.customer?.gstin ?? '');
|
||||
_idNumberCtrl = TextEditingController(text: widget.customer?.idNumber ?? '');
|
||||
_fatherNameCtrl = TextEditingController(text: widget.customer?.fatherName ?? '');
|
||||
_ageCtrl = TextEditingController(text: widget.customer?.age?.toString() ?? '');
|
||||
_idNumberCtrl = TextEditingController(
|
||||
text: widget.customer?.idNumber ?? '',
|
||||
);
|
||||
_fatherNameCtrl = TextEditingController(
|
||||
text: widget.customer?.fatherName ?? '',
|
||||
);
|
||||
_ageCtrl = TextEditingController(
|
||||
text: widget.customer?.age?.toString() ?? '',
|
||||
);
|
||||
_selectedGender = widget.customer?.gender;
|
||||
_selectedStateId = widget.customer?.stateId;
|
||||
import_storage.FlutterSecureStorage().read(key: 'jwt_token').then((val) {
|
||||
@@ -79,7 +86,9 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
void _showImagePickerModal() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Wrap(
|
||||
children: [
|
||||
@@ -126,13 +135,23 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
|
||||
try {
|
||||
if (widget.customer == null) {
|
||||
await ref.read(customersProvider.notifier).addCustomer(customer, photo: _photo);
|
||||
await ref
|
||||
.read(customersProvider.notifier)
|
||||
.addCustomer(customer, photo: _photo);
|
||||
} else {
|
||||
await ref.read(customersProvider.notifier).updateCustomer(widget.customer!.id!, customer, photo: _photo);
|
||||
await ref
|
||||
.read(customersProvider.notifier)
|
||||
.updateCustomer(widget.customer!.id!, customer, photo: _photo);
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(widget.customer == null ? 'Customer added' : 'Customer updated')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
widget.customer == null ? 'Customer added' : 'Customer updated',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@@ -142,9 +161,12 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
title: const Text('Error'),
|
||||
content: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('OK'))
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -181,7 +203,13 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(widget.customer == null ? "New Customer" : "Edit Customer", style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
widget.customer == null ? "New Customer" : "Edit Customer",
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -195,14 +223,22 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundImage: _photo != null
|
||||
? FileImage(File(_photo!.path)) as ImageProvider
|
||||
: (widget.customer?.photoUrl != null && _token != null
|
||||
? NetworkImage(
|
||||
'${DioClient().dio.options.baseUrl}/customers/${widget.customer!.id}/photo/content',
|
||||
headers: {'Authorization': 'Bearer $_token'},
|
||||
)
|
||||
: null),
|
||||
child: _photo == null && widget.customer?.photoUrl == null
|
||||
? const Icon(LucideIcons.user, size: 50, color: Colors.grey)
|
||||
: (widget.customer?.photoUrl != null &&
|
||||
_token != null
|
||||
? NetworkImage(
|
||||
'${DioClient().dio.options.baseUrl}/customers/${widget.customer!.id}/photo/content',
|
||||
headers: {
|
||||
'Authorization': 'Bearer $_token',
|
||||
},
|
||||
)
|
||||
: null),
|
||||
child:
|
||||
_photo == null && widget.customer?.photoUrl == null
|
||||
? const Icon(
|
||||
LucideIcons.user,
|
||||
size: 50,
|
||||
color: Colors.grey,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
Positioned(
|
||||
@@ -214,7 +250,11 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
color: Colors.blue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(LucideIcons.camera, color: Colors.white, size: 16),
|
||||
child: const Icon(
|
||||
LucideIcons.camera,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -226,7 +266,8 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
controller: _nameCtrl,
|
||||
labelText: 'Customer Name *',
|
||||
prefixIcon: const Icon(LucideIcons.user),
|
||||
validator: (val) => val == null || val.isEmpty ? 'Required' : null,
|
||||
validator: (val) =>
|
||||
val == null || val.isEmpty ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PremiumTextField(
|
||||
@@ -255,7 +296,10 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -266,9 +310,13 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
isExpanded: true,
|
||||
hint: const Text('Gender'),
|
||||
items: ['Male', 'Female', 'Other']
|
||||
.map((g) => DropdownMenuItem(value: g, child: Text(g)))
|
||||
.map(
|
||||
(g) =>
|
||||
DropdownMenuItem(value: g, child: Text(g)),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (val) => setState(() => _selectedGender = val),
|
||||
onChanged: (val) =>
|
||||
setState(() => _selectedGender = val),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -290,7 +338,9 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) return null; // Optional
|
||||
final RegExp gstRegExp = RegExp(r'^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$');
|
||||
final RegExp gstRegExp = RegExp(
|
||||
r'^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$',
|
||||
);
|
||||
if (!gstRegExp.hasMatch(val.toUpperCase())) {
|
||||
return 'Invalid GSTIN format';
|
||||
}
|
||||
@@ -314,13 +364,14 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
labelText: 'State',
|
||||
hintText: 'Select State',
|
||||
value: _selectedStateId,
|
||||
fillColor: Colors.grey[100],
|
||||
items: states.map((s) => s.id).toList(),
|
||||
itemAsString: (id) {
|
||||
final s = states.firstWhere((st) => st.id == id);
|
||||
final s = states.where((st) => st.id == id).firstOrNull;
|
||||
if (s == null) return 'Unknown';
|
||||
return '${s.name} (${s.gstCode})';
|
||||
},
|
||||
onChanged: (val) => setState(() => _selectedStateId = val),
|
||||
onChanged: (val) =>
|
||||
setState(() => _selectedStateId = val),
|
||||
);
|
||||
},
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
@@ -344,11 +395,24 @@ class _AddCustomerSheetState extends ConsumerState<AddCustomerSheet> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: Text(widget.customer == null ? 'Save Customer' : 'Update Customer'),
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
widget.customer == null
|
||||
? 'Save Customer'
|
||||
: 'Update Customer',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -11,7 +11,8 @@ class CustomerLedgerScreen extends ConsumerStatefulWidget {
|
||||
const CustomerLedgerScreen({super.key, required this.customer});
|
||||
|
||||
@override
|
||||
ConsumerState<CustomerLedgerScreen> createState() => _CustomerLedgerScreenState();
|
||||
ConsumerState<CustomerLedgerScreen> createState() =>
|
||||
_CustomerLedgerScreenState();
|
||||
}
|
||||
|
||||
class _CustomerLedgerScreenState extends ConsumerState<CustomerLedgerScreen> {
|
||||
@@ -31,10 +32,14 @@ class _CustomerLedgerScreenState extends ConsumerState<CustomerLedgerScreen> {
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
data: (invoices) {
|
||||
final customerInvoices = invoices.where((i) => i.customerId == widget.customer.id).toList();
|
||||
final customerInvoices = invoices
|
||||
.where((i) => i.customerId == widget.customer.id)
|
||||
.toList();
|
||||
|
||||
if (customerInvoices.isEmpty) {
|
||||
return const Center(child: Text('No invoices found for this customer.'));
|
||||
return const Center(
|
||||
child: Text('No invoices found for this customer.'),
|
||||
);
|
||||
}
|
||||
|
||||
// Sort chronological
|
||||
@@ -48,51 +53,126 @@ class _CustomerLedgerScreenState extends ConsumerState<CustomerLedgerScreen> {
|
||||
for (var inv in customerInvoices) {
|
||||
// Add row for Invoice (Debit)
|
||||
runningBalance += inv.totalAmount;
|
||||
rows.add(DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${index++}')),
|
||||
DataCell(Text(DateFormat('dd MMM yyyy').format(inv.issueDate))),
|
||||
DataCell(Text(inv.invoiceNumber, style: const TextStyle(fontWeight: FontWeight.w600))),
|
||||
const DataCell(Text('Invoice')),
|
||||
const DataCell(Text('-')),
|
||||
const DataCell(Text('-')),
|
||||
DataCell(Text(formatCurrency.format(inv.totalAmount), style: TextStyle(color: Colors.red.shade700, fontWeight: FontWeight.bold))),
|
||||
DataCell(Text(formatCurrency.format(runningBalance), style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
],
|
||||
));
|
||||
rows.add(
|
||||
DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${index++}')),
|
||||
DataCell(
|
||||
Text(DateFormat('dd MMM yyyy').format(inv.issueDate)),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
inv.invoiceNumber,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const DataCell(Text('Invoice')),
|
||||
const DataCell(Text('-')),
|
||||
const DataCell(Text('-')),
|
||||
DataCell(
|
||||
Text(
|
||||
formatCurrency.format(inv.totalAmount),
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
formatCurrency.format(runningBalance),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Add row for Payment (Credit)
|
||||
if (inv.payments != null && inv.payments!.isNotEmpty) {
|
||||
for (var p in inv.payments!) {
|
||||
runningBalance -= p.amount;
|
||||
final walletName = wallets.firstWhere((w) => w.id == p.walletId, orElse: () => wallets.first).name;
|
||||
rows.add(DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${index++}')),
|
||||
DataCell(Text(p.paymentDate != null ? DateFormat('dd MMM yyyy').format(p.paymentDate!) : DateFormat('dd MMM yyyy').format(inv.issueDate))),
|
||||
DataCell(Text(inv.invoiceNumber, style: const TextStyle(fontWeight: FontWeight.w600))),
|
||||
const DataCell(Text('Payment')),
|
||||
DataCell(Text(p.paymentMethod)),
|
||||
DataCell(Text(p.walletId != null ? walletName : '-')),
|
||||
DataCell(Text(formatCurrency.format(p.amount), style: TextStyle(color: Colors.green.shade700, fontWeight: FontWeight.bold))),
|
||||
DataCell(Text(formatCurrency.format(runningBalance), style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
],
|
||||
));
|
||||
final walletName = wallets
|
||||
.firstWhere(
|
||||
(w) => w.id == p.walletId,
|
||||
orElse: () => wallets.first,
|
||||
)
|
||||
.name;
|
||||
rows.add(
|
||||
DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${index++}')),
|
||||
DataCell(
|
||||
Text(
|
||||
p.paymentDate != null
|
||||
? DateFormat('dd MMM yyyy').format(p.paymentDate!)
|
||||
: DateFormat('dd MMM yyyy').format(inv.issueDate),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
inv.invoiceNumber,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const DataCell(Text('Payment')),
|
||||
DataCell(Text(p.paymentMethod)),
|
||||
DataCell(Text(p.walletId != null ? walletName : '-')),
|
||||
DataCell(
|
||||
Text(
|
||||
formatCurrency.format(p.amount),
|
||||
style: TextStyle(
|
||||
color: Colors.green.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
formatCurrency.format(runningBalance),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (inv.amountPaid > 0) {
|
||||
runningBalance -= inv.amountPaid;
|
||||
rows.add(DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${index++}')),
|
||||
DataCell(Text(DateFormat('dd MMM yyyy').format(inv.issueDate))),
|
||||
DataCell(Text(inv.invoiceNumber, style: const TextStyle(fontWeight: FontWeight.w600))),
|
||||
const DataCell(Text('Payment')),
|
||||
const DataCell(Text('-')),
|
||||
const DataCell(Text('-')),
|
||||
DataCell(Text(formatCurrency.format(inv.amountPaid), style: TextStyle(color: Colors.green.shade700, fontWeight: FontWeight.bold))),
|
||||
DataCell(Text(formatCurrency.format(runningBalance), style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
],
|
||||
));
|
||||
rows.add(
|
||||
DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${index++}')),
|
||||
DataCell(
|
||||
Text(DateFormat('dd MMM yyyy').format(inv.issueDate)),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
inv.invoiceNumber,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const DataCell(Text('Payment')),
|
||||
const DataCell(Text('-')),
|
||||
const DataCell(Text('-')),
|
||||
DataCell(
|
||||
Text(
|
||||
formatCurrency.format(inv.amountPaid),
|
||||
style: TextStyle(
|
||||
color: Colors.green.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
formatCurrency.format(runningBalance),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,14 +184,19 @@ class _CustomerLedgerScreenState extends ConsumerState<CustomerLedgerScreen> {
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Outstanding Balance', style: TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
const Text(
|
||||
'Outstanding Balance',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
formatCurrency.format(runningBalance),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: runningBalance > 0 ? Colors.red.shade700 : Colors.green.shade700,
|
||||
color: runningBalance > 0
|
||||
? Colors.red.shade700
|
||||
: Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -123,17 +208,59 @@ class _CustomerLedgerScreenState extends ConsumerState<CustomerLedgerScreen> {
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.resolveWith((states) => Colors.grey.shade100),
|
||||
headingRowColor: WidgetStateProperty.resolveWith(
|
||||
(states) => Colors.grey.shade100,
|
||||
),
|
||||
columnSpacing: 24,
|
||||
columns: const [
|
||||
DataColumn(label: Text('S.No', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Date', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Invoice #', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Type', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Mode', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Wallet', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Amount', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Balance', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'S.No',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Date',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Invoice #',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Type',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Mode',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Wallet',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Amount',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Balance',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: rows,
|
||||
),
|
||||
|
||||
@@ -7,7 +7,8 @@ import '../../../core/widgets/shimmer_loading.dart';
|
||||
import 'add_customer_sheet.dart';
|
||||
import 'customer_ledger_screen.dart';
|
||||
import '../providers/invoices_provider.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as import_storage;
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart'
|
||||
as import_storage;
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@@ -15,7 +16,8 @@ class CustomersListScreen extends ConsumerStatefulWidget {
|
||||
const CustomersListScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CustomersListScreen> createState() => _CustomersListScreenState();
|
||||
ConsumerState<CustomersListScreen> createState() =>
|
||||
_CustomersListScreenState();
|
||||
}
|
||||
|
||||
class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
@@ -53,7 +55,10 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
appBar: AppBar(
|
||||
title: const Text('Customers', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
title: const Text(
|
||||
'Customers',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
@@ -69,12 +74,6 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search by name or phone...',
|
||||
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
@@ -99,7 +98,10 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
children: [
|
||||
Icon(LucideIcons.users, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text('No customers found.', style: TextStyle(color: Colors.grey, fontSize: 16)),
|
||||
Text(
|
||||
'No customers found.',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -107,7 +109,9 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(customersProvider.notifier).refresh(_searchCtrl.text);
|
||||
await ref
|
||||
.read(customersProvider.notifier)
|
||||
.refresh(_searchCtrl.text);
|
||||
},
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
@@ -122,8 +126,12 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
double outstandingBalance = 0.0;
|
||||
if (invoicesState.hasValue) {
|
||||
for (var inv in invoicesState.value!) {
|
||||
if (inv.customerId == customer.id && inv.status != 'PAID' && inv.status != 'CANCELLED' && inv.status != 'DRAFT') {
|
||||
outstandingBalance += (inv.totalAmount - inv.amountPaid);
|
||||
if (inv.customerId == customer.id &&
|
||||
inv.status != 'PAID' &&
|
||||
inv.status != 'CANCELLED' &&
|
||||
inv.status != 'DRAFT') {
|
||||
outstandingBalance +=
|
||||
(inv.totalAmount - inv.amountPaid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +142,11 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.grey.withOpacity(0.08), blurRadius: 10, offset: const Offset(0, 4)),
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
@@ -143,12 +155,20 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => CustomerLedgerScreen(customer: customer)));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CustomerLedgerScreen(
|
||||
customer: customer,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Avatar
|
||||
Hero(
|
||||
@@ -156,14 +176,29 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.blue.shade50,
|
||||
backgroundImage: (customer.photoUrl != null && _token != null)
|
||||
backgroundImage:
|
||||
(customer.photoUrl != null &&
|
||||
_token != null)
|
||||
? NetworkImage(
|
||||
'${DioClient().dio.options.baseUrl}/customers/${customer.id}/photo/content',
|
||||
headers: {'Authorization': 'Bearer $_token'},
|
||||
headers: {
|
||||
'Authorization':
|
||||
'Bearer $_token',
|
||||
},
|
||||
)
|
||||
: null,
|
||||
child: (customer.photoUrl == null)
|
||||
? Text(customer.name.isNotEmpty ? customer.name[0].toUpperCase() : '?', style: TextStyle(color: Colors.blue.shade700, fontWeight: FontWeight.bold, fontSize: 24))
|
||||
? Text(
|
||||
customer.name.isNotEmpty
|
||||
? customer.name[0]
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: Colors.blue.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
@@ -171,44 +206,102 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
// Details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(customer.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.black87)),
|
||||
Text(
|
||||
customer.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
if (customer.phone != null && customer.phone!.isNotEmpty)
|
||||
if (customer.phone != null &&
|
||||
customer.phone!.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.phone, size: 14, color: Colors.grey.shade600),
|
||||
Icon(
|
||||
LucideIcons.phone,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(customer.phone!, style: TextStyle(color: Colors.grey.shade700, fontSize: 14)),
|
||||
Text(
|
||||
customer.phone!,
|
||||
style: TextStyle(
|
||||
color:
|
||||
Colors.grey.shade700,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (customer.gstin != null && customer.gstin!.isNotEmpty)
|
||||
if (customer.gstin != null &&
|
||||
customer.gstin!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.building, size: 14, color: Colors.grey.shade600),
|
||||
Icon(
|
||||
LucideIcons.building,
|
||||
size: 14,
|
||||
color:
|
||||
Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(customer.gstin!, style: TextStyle(color: Colors.grey.shade700, fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
Text(
|
||||
customer.gstin!,
|
||||
style: TextStyle(
|
||||
color: Colors
|
||||
.grey
|
||||
.shade700,
|
||||
fontSize: 13,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: outstandingBalance > 0 ? Colors.red.shade50 : Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color:
|
||||
outstandingBalance > 0
|
||||
? Colors.red.shade50
|
||||
: Colors.green.shade50,
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
outstandingBalance > 0 ? 'Pending: ${formatCurrency.format(outstandingBalance)}' : 'Settled',
|
||||
outstandingBalance > 0
|
||||
? 'Pending: ${formatCurrency.format(outstandingBalance)}'
|
||||
: 'Settled',
|
||||
style: TextStyle(
|
||||
color: outstandingBalance > 0 ? Colors.red.shade700 : Colors.green.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
outstandingBalance > 0
|
||||
? Colors.red.shade700
|
||||
: Colors
|
||||
.green
|
||||
.shade700,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
@@ -216,39 +309,87 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.edit2, size: 20, color: Colors.blue),
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: const BoxConstraints(),
|
||||
icon: const Icon(
|
||||
LucideIcons.edit2,
|
||||
size: 20,
|
||||
color: Colors.blue,
|
||||
),
|
||||
visualDensity:
|
||||
VisualDensity.compact,
|
||||
constraints:
|
||||
const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => AddCustomerSheet(customer: customer),
|
||||
isScrollControlled:
|
||||
true,
|
||||
backgroundColor:
|
||||
Colors
|
||||
.transparent,
|
||||
builder: (context) =>
|
||||
AddCustomerSheet(
|
||||
customer:
|
||||
customer,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.trash2, size: 20, color: Colors.redAccent),
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: const BoxConstraints(),
|
||||
icon: const Icon(
|
||||
LucideIcons.trash2,
|
||||
size: 20,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
visualDensity:
|
||||
VisualDensity.compact,
|
||||
constraints:
|
||||
const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Delete Customer'),
|
||||
content: Text('Are you sure you want to delete ${customer.name}? This action cannot be undone.'),
|
||||
title: const Text(
|
||||
'Delete Customer',
|
||||
),
|
||||
content: Text(
|
||||
'Are you sure you want to delete ${customer.name}? This action cannot be undone.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop(
|
||||
ctx,
|
||||
),
|
||||
child:
|
||||
const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.read(customersProvider.notifier).deleteCustomer(customer.id!);
|
||||
Navigator.pop(ctx);
|
||||
ref
|
||||
.read(
|
||||
customersProvider
|
||||
.notifier,
|
||||
)
|
||||
.deleteCustomer(
|
||||
customer
|
||||
.id!,
|
||||
);
|
||||
Navigator.pop(
|
||||
ctx,
|
||||
);
|
||||
},
|
||||
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
||||
child: const Text(
|
||||
'Delete',
|
||||
style: TextStyle(
|
||||
color: Colors
|
||||
.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -288,7 +429,10 @@ class _CustomersListScreenState extends ConsumerState<CustomersListScreen> {
|
||||
);
|
||||
},
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: const Text('Add Customer', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
label: const Text(
|
||||
'Add Customer',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,31 +32,52 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
Color _getStatusColor(Invoice invoice) {
|
||||
if (invoice.isEmi && invoice.status != 'PAID') return Colors.purple;
|
||||
switch (invoice.status) {
|
||||
case 'DRAFT': return Colors.grey;
|
||||
case 'FINALIZED': return Colors.orange;
|
||||
case 'PAID': return Colors.green;
|
||||
case 'PARTIAL': return Colors.blue;
|
||||
case 'OVERDUE': return Colors.red;
|
||||
case 'CANCELLED': return Colors.black;
|
||||
default: return Colors.grey;
|
||||
case 'DRAFT':
|
||||
return Colors.grey;
|
||||
case 'FINALIZED':
|
||||
return Colors.orange;
|
||||
case 'PAID':
|
||||
return Colors.green;
|
||||
case 'PARTIAL':
|
||||
return Colors.blue;
|
||||
case 'OVERDUE':
|
||||
return Colors.red;
|
||||
case 'CANCELLED':
|
||||
return Colors.black;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
void _showPaymentHistory(BuildContext context, WidgetRef ref, Invoice invoice) {
|
||||
void _showPaymentHistory(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Invoice invoice,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Payment History: ${invoice.invoiceNumber}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
'Payment History: ${invoice.invoiceNumber}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FutureBuilder<List<InvoicePayment>>(
|
||||
future: ref.read(invoicesProvider.notifier).fetchPaymentsForInvoice(invoice.id!),
|
||||
future: ref
|
||||
.read(invoicesProvider.notifier)
|
||||
.fetchPaymentsForInvoice(invoice.id!),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -76,22 +97,67 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.resolveWith((states) => Colors.grey.shade100),
|
||||
headingRowColor: WidgetStateProperty.resolveWith(
|
||||
(states) => Colors.grey.shade100,
|
||||
),
|
||||
columnSpacing: 24,
|
||||
columns: const [
|
||||
DataColumn(label: Text('Date', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Wallet', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Method', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(label: Text('Amount', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Date',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Wallet',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Method',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Amount',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: payments.map((p) {
|
||||
final walletName = wallets.firstWhere((w) => w.id == p.walletId, orElse: () => wallets.first).name;
|
||||
final walletName = wallets
|
||||
.firstWhere(
|
||||
(w) => w.id == p.walletId,
|
||||
orElse: () => wallets.first,
|
||||
)
|
||||
.name;
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text(p.paymentDate != null ? DateFormat('dd MMM yyyy').format(p.paymentDate!) : '-')),
|
||||
DataCell(Text(p.walletId != null ? walletName : '-')),
|
||||
DataCell(
|
||||
Text(
|
||||
p.paymentDate != null
|
||||
? DateFormat(
|
||||
'dd MMM yyyy',
|
||||
).format(p.paymentDate!)
|
||||
: '-',
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(p.walletId != null ? walletName : '-'),
|
||||
),
|
||||
DataCell(Text(p.paymentMethod ?? '-')),
|
||||
DataCell(Text('₹${p.amount.toStringAsFixed(2)}', style: TextStyle(color: Colors.green.shade700, fontWeight: FontWeight.bold))),
|
||||
DataCell(
|
||||
Text(
|
||||
'₹${p.amount.toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
color: Colors.green.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
@@ -138,28 +204,26 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
appBar: AppBar(
|
||||
title: const Text('Invoices'),
|
||||
title: const Text(
|
||||
'Invoices',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search by Invoice # or Customer',
|
||||
prefixIcon: const Icon(LucideIcons.search, color: Colors.grey),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(LucideIcons.x, size: 20),
|
||||
@@ -192,9 +256,16 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
data: (allInvoices) {
|
||||
final invoices = allInvoices.where((invoice) {
|
||||
final matchesInvoice = invoice.invoiceNumber.toLowerCase().contains(_searchQuery);
|
||||
final customer = customers.firstWhere((c) => c.id == invoice.customerId, orElse: () => customers.first);
|
||||
final customerName = invoice.customerId != null ? customer.name.toLowerCase() : '';
|
||||
final matchesInvoice = invoice.invoiceNumber
|
||||
.toLowerCase()
|
||||
.contains(_searchQuery);
|
||||
final customer = customers.firstWhere(
|
||||
(c) => c.id == invoice.customerId,
|
||||
orElse: () => customers.first,
|
||||
);
|
||||
final customerName = invoice.customerId != null
|
||||
? customer.name.toLowerCase()
|
||||
: '';
|
||||
final matchesCustomer = customerName.contains(_searchQuery);
|
||||
return matchesInvoice || matchesCustomer;
|
||||
}).toList();
|
||||
@@ -208,7 +279,26 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SizedBox(height: 100),
|
||||
Center(child: Text('No invoices found.', style: TextStyle(color: Colors.grey))),
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.receipt,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'No invoices found.',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -224,139 +314,270 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
itemCount: invoices.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invoice = invoices[index];
|
||||
double remaining = invoice.totalAmount - invoice.amountPaid;
|
||||
double remaining =
|
||||
invoice.totalAmount - invoice.amountPaid;
|
||||
final customer = invoice.customerId != null
|
||||
? customers.firstWhere((c) => c.id == invoice.customerId, orElse: () => customers.first)
|
||||
? customers.firstWhere(
|
||||
(c) => c.id == invoice.customerId,
|
||||
orElse: () => customers.first,
|
||||
)
|
||||
: null;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => InvoiceDetailsScreen(invoice: invoice)),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(invoice.invoiceNumber, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
if (customer != null)
|
||||
Text(customer.name, style: TextStyle(color: Colors.grey[700], fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
InvoiceDetailsScreen(invoice: invoice),
|
||||
),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
invoice.invoiceNumber,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (customer != null)
|
||||
Text(
|
||||
customer.name,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(
|
||||
invoice,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusLabel(invoice),
|
||||
style: TextStyle(
|
||||
color: _getStatusColor(invoice),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Issued: ${formatDate.format(invoice.issueDate)}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
if (invoice.dueDate != null)
|
||||
Text(
|
||||
'Due: ${formatDate.format(invoice.dueDate!)}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
if (invoice.nextPaymentDate != null &&
|
||||
remaining > 0)
|
||||
Text(
|
||||
'Next Pmt: ${formatDate.format(invoice.nextPaymentDate!)}',
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade700,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
formatCurrency.format(
|
||||
invoice.totalAmount,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
if (invoice.amountPaid > 0)
|
||||
Text(
|
||||
'Paid: ${formatCurrency.format(invoice.amountPaid)}',
|
||||
style: TextStyle(
|
||||
color: Colors.green.shade700,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (remaining > 0 &&
|
||||
invoice.status != 'DRAFT')
|
||||
Text(
|
||||
'Bal: ${formatCurrency.format(remaining)}',
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (invoice.isEmi &&
|
||||
invoice.emiAmount != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(invoice).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusLabel(invoice),
|
||||
style: TextStyle(color: _getStatusColor(invoice), fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Issued: ${formatDate.format(invoice.issueDate)}', style: TextStyle(color: Colors.grey[600], fontSize: 13)),
|
||||
if (invoice.dueDate != null)
|
||||
Text('Due: ${formatDate.format(invoice.dueDate!)}', style: TextStyle(color: Colors.grey[600], fontSize: 13)),
|
||||
if (invoice.nextPaymentDate != null && remaining > 0)
|
||||
Text('Next Pmt: ${formatDate.format(invoice.nextPaymentDate!)}', style: TextStyle(color: Colors.orange.shade700, fontSize: 13, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
formatCurrency.format(invoice.totalAmount),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
color: Colors.purple.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.purple.withOpacity(0.2),
|
||||
),
|
||||
if (invoice.amountPaid > 0)
|
||||
Text(
|
||||
'Paid: ${formatCurrency.format(invoice.amountPaid)}',
|
||||
style: TextStyle(color: Colors.green.shade700, fontSize: 12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.calendarClock,
|
||||
size: 16,
|
||||
color: Colors.purple,
|
||||
),
|
||||
if (remaining > 0 && invoice.status != 'DRAFT')
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Bal: ${formatCurrency.format(remaining)}',
|
||||
style: TextStyle(color: Colors.red.shade700, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
'EMI: ${formatCurrency.format(invoice.emiAmount)} / ${invoice.emiCycle}',
|
||||
style: const TextStyle(
|
||||
color: Colors.purple,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (invoice.isEmi && invoice.emiAmount != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.purple.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.calendarClock, size: 16, color: Colors.purple),
|
||||
const Divider(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (invoice.amountPaid > 0)
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
_showPaymentHistory(
|
||||
context,
|
||||
ref,
|
||||
invoice,
|
||||
),
|
||||
icon: const Icon(
|
||||
LucideIcons.history,
|
||||
size: 14,
|
||||
),
|
||||
label: const Text(
|
||||
'History',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor:
|
||||
Colors.blue.shade700,
|
||||
side: BorderSide(
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (invoice.amountPaid > 0 &&
|
||||
remaining > 0)
|
||||
const SizedBox(width: 8),
|
||||
Text('EMI: ${formatCurrency.format(invoice.emiAmount)} / ${invoice.emiCycle}', style: const TextStyle(color: Colors.purple, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
)
|
||||
if (remaining > 0)
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () =>
|
||||
_showReceivePaymentSheet(
|
||||
context,
|
||||
invoice,
|
||||
),
|
||||
icon: const Icon(
|
||||
LucideIcons.indianRupee,
|
||||
size: 14,
|
||||
),
|
||||
label: const Text(
|
||||
'Receive',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Colors.blue.shade50,
|
||||
foregroundColor:
|
||||
Colors.blue.shade700,
|
||||
elevation: 0,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const Divider(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (invoice.amountPaid > 0)
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _showPaymentHistory(context, ref, invoice),
|
||||
icon: const Icon(LucideIcons.history, size: 14),
|
||||
label: const Text('History', style: TextStyle(fontSize: 13)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.blue.shade700,
|
||||
side: BorderSide(color: Colors.blue.shade200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (invoice.amountPaid > 0 && remaining > 0) const SizedBox(width: 8),
|
||||
if (remaining > 0)
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _showReceivePaymentSheet(context, invoice),
|
||||
icon: const Icon(LucideIcons.indianRupee, size: 14),
|
||||
label: const Text('Receive', style: TextStyle(fontSize: 13)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue.shade50,
|
||||
foregroundColor: Colors.blue.shade700,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -371,7 +592,10 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const InvoiceBuilderScreen()));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const InvoiceBuilderScreen()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: const Text('Create Invoice'),
|
||||
|
||||
@@ -12,7 +12,8 @@ class ReceivePaymentSheet extends ConsumerStatefulWidget {
|
||||
const ReceivePaymentSheet({super.key, required this.invoice});
|
||||
|
||||
@override
|
||||
ConsumerState<ReceivePaymentSheet> createState() => _ReceivePaymentSheetState();
|
||||
ConsumerState<ReceivePaymentSheet> createState() =>
|
||||
_ReceivePaymentSheetState();
|
||||
}
|
||||
|
||||
class _ReceivePaymentSheetState extends ConsumerState<ReceivePaymentSheet> {
|
||||
@@ -41,7 +42,9 @@ class _ReceivePaymentSheetState extends ConsumerState<ReceivePaymentSheet> {
|
||||
}
|
||||
}).toList();
|
||||
|
||||
if (wallets.isNotEmpty && (_selectedWalletId == null || !wallets.any((w) => w.id == _selectedWalletId))) {
|
||||
if (wallets.isNotEmpty &&
|
||||
(_selectedWalletId == null ||
|
||||
!wallets.any((w) => w.id == _selectedWalletId))) {
|
||||
_selectedWalletId = wallets.first.id;
|
||||
}
|
||||
|
||||
@@ -63,8 +66,14 @@ class _ReceivePaymentSheetState extends ConsumerState<ReceivePaymentSheet> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Receive Payment', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
IconButton(icon: const Icon(LucideIcons.x), onPressed: () => Navigator.pop(context)),
|
||||
const Text(
|
||||
'Receive Payment',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.x),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -74,7 +83,10 @@ class _ReceivePaymentSheetState extends ConsumerState<ReceivePaymentSheet> {
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Receive Into Wallet', style: TextStyle(fontWeight: FontWeight.w600, color: Colors.grey)),
|
||||
const Text(
|
||||
'Receive Into Wallet',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -88,14 +100,19 @@ class _ReceivePaymentSheetState extends ConsumerState<ReceivePaymentSheet> {
|
||||
isExpanded: true,
|
||||
hint: const Text('Select Wallet'),
|
||||
items: wallets
|
||||
.map((w) => DropdownMenuItem(value: w.id, child: Text(w.name)))
|
||||
.map(
|
||||
(w) => DropdownMenuItem(value: w.id, child: Text(w.name)),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (val) => setState(() => _selectedWalletId = val),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Payment Method', style: TextStyle(fontWeight: FontWeight.w600, color: Colors.grey)),
|
||||
const Text(
|
||||
'Payment Method',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -129,36 +146,64 @@ class _ReceivePaymentSheetState extends ConsumerState<ReceivePaymentSheet> {
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
onPressed: _isLoading ? null : () async {
|
||||
final amount = double.tryParse(_amountCtrl.text) ?? 0;
|
||||
if (amount <= 0) return;
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () async {
|
||||
final amount = double.tryParse(_amountCtrl.text) ?? 0;
|
||||
if (amount <= 0) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await ref.read(invoicesProvider.notifier).addPayment(
|
||||
widget.invoice.id!,
|
||||
InvoicePayment(
|
||||
amount: amount,
|
||||
paymentMethod: _paymentMethod,
|
||||
walletId: _selectedWalletId,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.pop(context, true); // true indicates success
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Payment Received Successfully!')));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await ref
|
||||
.read(invoicesProvider.notifier)
|
||||
.addPayment(
|
||||
widget.invoice.id!,
|
||||
InvoicePayment(
|
||||
amount: amount,
|
||||
paymentMethod: _paymentMethod,
|
||||
walletId: _selectedWalletId,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.pop(
|
||||
context,
|
||||
true,
|
||||
); // true indicates success
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Payment Received Successfully!'),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: _isLoading
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: const Text('Confirm Payment', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Confirm Payment',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -17,7 +17,9 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
||||
try {
|
||||
final response = await DioClient().dio.get(
|
||||
'/customers',
|
||||
queryParameters: search != null && search.isNotEmpty ? {'search': search} : null,
|
||||
queryParameters: search != null && search.isNotEmpty
|
||||
? {'search': search}
|
||||
: null,
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data;
|
||||
@@ -55,7 +57,9 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
if (e is DioException) {
|
||||
throw Exception('Failed to add customer: ${e.response?.statusCode} - ${e.response?.data}');
|
||||
throw Exception(
|
||||
'Failed to add customer: ${e.response?.statusCode} - ${e.response?.data}',
|
||||
);
|
||||
}
|
||||
throw Exception('Failed to add customer: $e');
|
||||
}
|
||||
@@ -63,10 +67,7 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
||||
|
||||
Future<void> updateCustomer(int id, Customer customer, {XFile? photo}) async {
|
||||
try {
|
||||
await DioClient().dio.put(
|
||||
'/customers/$id',
|
||||
data: customer.toJson(),
|
||||
);
|
||||
await DioClient().dio.put('/customers/$id', data: customer.toJson());
|
||||
|
||||
if (photo != null) {
|
||||
await _uploadPhoto(id, photo);
|
||||
@@ -75,7 +76,9 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
if (e is DioException) {
|
||||
throw Exception('Failed to update customer: ${e.response?.statusCode} - ${e.response?.data}');
|
||||
throw Exception(
|
||||
'Failed to update customer: ${e.response?.statusCode} - ${e.response?.data}',
|
||||
);
|
||||
}
|
||||
throw Exception('Failed to update customer: $e');
|
||||
}
|
||||
@@ -94,10 +97,7 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
||||
'file': MultipartFile.fromBytes(compressedBytes, filename: photo.name),
|
||||
});
|
||||
|
||||
await DioClient().dio.post(
|
||||
'/customers/$customerId/photo',
|
||||
data: formData,
|
||||
);
|
||||
await DioClient().dio.post('/customers/$customerId/photo', data: formData);
|
||||
}
|
||||
|
||||
Future<void> deleteCustomer(int id) async {
|
||||
@@ -110,6 +110,7 @@ class CustomersNotifier extends AsyncNotifier<List<Customer>> {
|
||||
}
|
||||
}
|
||||
|
||||
final customersProvider = AsyncNotifierProvider<CustomersNotifier, List<Customer>>(() {
|
||||
return CustomersNotifier();
|
||||
});
|
||||
final customersProvider =
|
||||
AsyncNotifierProvider<CustomersNotifier, List<Customer>>(() {
|
||||
return CustomersNotifier();
|
||||
});
|
||||
|
||||
@@ -36,14 +36,13 @@ class InvoicesNotifier extends AsyncNotifier<List<Invoice>> {
|
||||
|
||||
Future<void> createInvoice(Invoice invoice) async {
|
||||
try {
|
||||
await DioClient().dio.post(
|
||||
'/invoices',
|
||||
data: invoice.toJson(),
|
||||
);
|
||||
await DioClient().dio.post('/invoices', data: invoice.toJson());
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
if (e is DioException) {
|
||||
throw Exception('Failed to create invoice: ${e.response?.statusCode} - ${e.response?.data}');
|
||||
throw Exception(
|
||||
'Failed to create invoice: ${e.response?.statusCode} - ${e.response?.data}',
|
||||
);
|
||||
}
|
||||
throw Exception('Failed to create invoice: $e');
|
||||
}
|
||||
@@ -67,7 +66,9 @@ class InvoicesNotifier extends AsyncNotifier<List<Invoice>> {
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
if (e is DioException) {
|
||||
throw Exception('Failed to add payment: ${e.response?.statusCode} - ${e.response?.data}');
|
||||
throw Exception(
|
||||
'Failed to add payment: ${e.response?.statusCode} - ${e.response?.data}',
|
||||
);
|
||||
}
|
||||
throw Exception('Failed to add payment: $e');
|
||||
}
|
||||
@@ -75,7 +76,9 @@ class InvoicesNotifier extends AsyncNotifier<List<Invoice>> {
|
||||
|
||||
Future<List<InvoicePayment>> fetchPaymentsForInvoice(int invoiceId) async {
|
||||
try {
|
||||
final response = await DioClient().dio.get('/invoices/$invoiceId/payments');
|
||||
final response = await DioClient().dio.get(
|
||||
'/invoices/$invoiceId/payments',
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((e) => InvoicePayment.fromJson(e)).toList();
|
||||
@@ -88,6 +91,8 @@ class InvoicesNotifier extends AsyncNotifier<List<Invoice>> {
|
||||
}
|
||||
}
|
||||
|
||||
final invoicesProvider = AsyncNotifierProvider<InvoicesNotifier, List<Invoice>>(() {
|
||||
return InvoicesNotifier();
|
||||
});
|
||||
final invoicesProvider = AsyncNotifierProvider<InvoicesNotifier, List<Invoice>>(
|
||||
() {
|
||||
return InvoicesNotifier();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -970,7 +970,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: DecorationImage(
|
||||
image: _jwtToken != null
|
||||
? NetworkImage('https://app.technobeesolutions.in/api/kifi/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'})
|
||||
? NetworkImage('https://app.technobeesolutions.in/api/kifi-v2/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'})
|
||||
: const AssetImage('assets/images/placeholder.png') as ImageProvider,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
@@ -1087,7 +1087,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
// Add existing attachments
|
||||
for (final att in existingAttachments) {
|
||||
if (_jwtToken != null) {
|
||||
allImages.add(NetworkImage('https://app.technobeesolutions.in/api/kifi/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'}));
|
||||
allImages.add(NetworkImage('https://app.technobeesolutions.in/api/kifi-v2/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'}));
|
||||
} else {
|
||||
allImages.add(const AssetImage('assets/images/placeholder.png'));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ class PurchaseOrder {
|
||||
final DateTime? dueDate;
|
||||
final double subtotal;
|
||||
final double taxTotal;
|
||||
final double? cgstTotal;
|
||||
final double? sgstTotal;
|
||||
final double? igstTotal;
|
||||
final double discountTotal;
|
||||
final double totalAmount;
|
||||
final String status;
|
||||
@@ -32,6 +35,9 @@ class PurchaseOrder {
|
||||
this.dueDate,
|
||||
this.subtotal = 0.0,
|
||||
this.taxTotal = 0.0,
|
||||
this.cgstTotal,
|
||||
this.sgstTotal,
|
||||
this.igstTotal,
|
||||
this.discountTotal = 0.0,
|
||||
this.totalAmount = 0.0,
|
||||
this.status = 'PENDING',
|
||||
@@ -52,21 +58,36 @@ class PurchaseOrder {
|
||||
userId: json['userId'],
|
||||
vendorId: json['vendorId'],
|
||||
poNumber: json['poNumber'] ?? '',
|
||||
issueDate: json['issueDate'] != null ? DateTime.parse(json['issueDate']) : DateTime.now(),
|
||||
issueDate: json['issueDate'] != null
|
||||
? DateTime.parse(json['issueDate'])
|
||||
: DateTime.now(),
|
||||
dueDate: json['dueDate'] != null ? DateTime.parse(json['dueDate']) : null,
|
||||
subtotal: (json['subtotal'] ?? 0.0).toDouble(),
|
||||
taxTotal: (json['taxTotal'] ?? 0.0).toDouble(),
|
||||
cgstTotal: json['cgstTotal'] != null ? (json['cgstTotal']).toDouble() : null,
|
||||
sgstTotal: json['sgstTotal'] != null ? (json['sgstTotal']).toDouble() : null,
|
||||
igstTotal: json['igstTotal'] != null ? (json['igstTotal']).toDouble() : null,
|
||||
discountTotal: (json['discountTotal'] ?? 0.0).toDouble(),
|
||||
totalAmount: (json['totalAmount'] ?? 0.0).toDouble(),
|
||||
status: json['status'] ?? 'PENDING',
|
||||
notes: json['notes'],
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : null,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
: null,
|
||||
updatedAt: json['updatedAt'] != null
|
||||
? DateTime.parse(json['updatedAt'])
|
||||
: null,
|
||||
poDate: json['poDate'] != null ? DateTime.parse(json['poDate']) : null,
|
||||
amountPaid: (json['amountPaid'] ?? 0.0).toDouble(),
|
||||
nextPaymentDate: json['nextPaymentDate'] != null ? DateTime.parse(json['nextPaymentDate']) : null,
|
||||
nextPaymentDate: json['nextPaymentDate'] != null
|
||||
? DateTime.parse(json['nextPaymentDate'])
|
||||
: null,
|
||||
vendorInvoiceUrl: json['vendorInvoiceUrl'],
|
||||
items: json['items'] != null ? (json['items'] as List).map((i) => PurchaseOrderItem.fromJson(i)).toList() : [],
|
||||
items: json['items'] != null
|
||||
? (json['items'] as List)
|
||||
.map((i) => PurchaseOrderItem.fromJson(i))
|
||||
.toList()
|
||||
: [],
|
||||
vendor: json['vendor'] != null ? Vendor.fromJson(json['vendor']) : null,
|
||||
);
|
||||
}
|
||||
@@ -81,6 +102,9 @@ class PurchaseOrder {
|
||||
'dueDate': dueDate?.toIso8601String(),
|
||||
'subtotal': subtotal,
|
||||
'taxTotal': taxTotal,
|
||||
'cgstTotal': cgstTotal,
|
||||
'sgstTotal': sgstTotal,
|
||||
'igstTotal': igstTotal,
|
||||
'discountTotal': discountTotal,
|
||||
'totalAmount': totalAmount,
|
||||
'status': status,
|
||||
@@ -102,6 +126,9 @@ class PurchaseOrder {
|
||||
DateTime? dueDate,
|
||||
double? subtotal,
|
||||
double? taxTotal,
|
||||
double? cgstTotal,
|
||||
double? sgstTotal,
|
||||
double? igstTotal,
|
||||
double? discountTotal,
|
||||
double? totalAmount,
|
||||
String? status,
|
||||
@@ -122,6 +149,9 @@ class PurchaseOrder {
|
||||
dueDate: dueDate ?? this.dueDate,
|
||||
subtotal: subtotal ?? this.subtotal,
|
||||
taxTotal: taxTotal ?? this.taxTotal,
|
||||
cgstTotal: cgstTotal ?? this.cgstTotal,
|
||||
sgstTotal: sgstTotal ?? this.sgstTotal,
|
||||
igstTotal: igstTotal ?? this.igstTotal,
|
||||
discountTotal: discountTotal ?? this.discountTotal,
|
||||
totalAmount: totalAmount ?? this.totalAmount,
|
||||
status: status ?? this.status,
|
||||
|
||||
@@ -5,6 +5,9 @@ class PurchaseOrderItem {
|
||||
final double quantity;
|
||||
final double unitPrice;
|
||||
final double taxRate;
|
||||
final double? cgstRate;
|
||||
final double? sgstRate;
|
||||
final double? igstRate;
|
||||
final double discount;
|
||||
final double total;
|
||||
final String? description;
|
||||
@@ -12,6 +15,11 @@ class PurchaseOrderItem {
|
||||
final double otherCharges;
|
||||
final String? sku;
|
||||
|
||||
final String? huid;
|
||||
final double? weight;
|
||||
final String? photoUrl;
|
||||
final String? localPhotoPath;
|
||||
|
||||
PurchaseOrderItem({
|
||||
this.id,
|
||||
this.poId,
|
||||
@@ -19,12 +27,20 @@ class PurchaseOrderItem {
|
||||
this.quantity = 1.0,
|
||||
this.unitPrice = 0.0,
|
||||
this.taxRate = 0.0,
|
||||
this.cgstRate,
|
||||
this.sgstRate,
|
||||
this.igstRate,
|
||||
this.discount = 0.0,
|
||||
this.total = 0.0,
|
||||
this.description,
|
||||
this.makingCharge = 0.0,
|
||||
this.otherCharges = 0.0,
|
||||
this.sku,
|
||||
|
||||
this.huid,
|
||||
this.weight,
|
||||
this.photoUrl,
|
||||
this.localPhotoPath,
|
||||
});
|
||||
|
||||
factory PurchaseOrderItem.fromJson(Map<String, dynamic> json) {
|
||||
@@ -35,12 +51,19 @@ class PurchaseOrderItem {
|
||||
quantity: (json['quantity'] ?? 1.0).toDouble(),
|
||||
unitPrice: (json['unitPrice'] ?? 0.0).toDouble(),
|
||||
taxRate: (json['taxRate'] ?? 0.0).toDouble(),
|
||||
cgstRate: json['cgstRate'] != null ? (json['cgstRate']).toDouble() : null,
|
||||
sgstRate: json['sgstRate'] != null ? (json['sgstRate']).toDouble() : null,
|
||||
igstRate: json['igstRate'] != null ? (json['igstRate']).toDouble() : null,
|
||||
discount: (json['discount'] ?? 0.0).toDouble(),
|
||||
total: (json['total'] ?? 0.0).toDouble(),
|
||||
description: json['description'],
|
||||
makingCharge: (json['makingCharge'] ?? 0.0).toDouble(),
|
||||
otherCharges: (json['otherCharges'] ?? 0.0).toDouble(),
|
||||
sku: json['sku'],
|
||||
|
||||
huid: json['huid'],
|
||||
weight: json['weight'] != null ? (json['weight'] as num).toDouble() : null,
|
||||
photoUrl: json['photoUrl'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,12 +75,19 @@ class PurchaseOrderItem {
|
||||
'quantity': quantity,
|
||||
'unitPrice': unitPrice,
|
||||
'taxRate': taxRate,
|
||||
'cgstRate': cgstRate,
|
||||
'sgstRate': sgstRate,
|
||||
'igstRate': igstRate,
|
||||
'discount': discount,
|
||||
'total': total,
|
||||
'description': description,
|
||||
'makingCharge': makingCharge,
|
||||
'otherCharges': otherCharges,
|
||||
'sku': sku,
|
||||
|
||||
'huid': huid,
|
||||
'weight': weight,
|
||||
'photoUrl': photoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,12 +98,20 @@ class PurchaseOrderItem {
|
||||
double? quantity,
|
||||
double? unitPrice,
|
||||
double? taxRate,
|
||||
double? cgstRate,
|
||||
double? sgstRate,
|
||||
double? igstRate,
|
||||
double? discount,
|
||||
double? total,
|
||||
String? description,
|
||||
double? makingCharge,
|
||||
double? otherCharges,
|
||||
String? sku,
|
||||
|
||||
String? huid,
|
||||
double? weight,
|
||||
String? photoUrl,
|
||||
String? localPhotoPath,
|
||||
}) {
|
||||
return PurchaseOrderItem(
|
||||
id: id ?? this.id,
|
||||
@@ -82,12 +120,20 @@ class PurchaseOrderItem {
|
||||
quantity: quantity ?? this.quantity,
|
||||
unitPrice: unitPrice ?? this.unitPrice,
|
||||
taxRate: taxRate ?? this.taxRate,
|
||||
cgstRate: cgstRate ?? this.cgstRate,
|
||||
sgstRate: sgstRate ?? this.sgstRate,
|
||||
igstRate: igstRate ?? this.igstRate,
|
||||
discount: discount ?? this.discount,
|
||||
total: total ?? this.total,
|
||||
description: description ?? this.description,
|
||||
makingCharge: makingCharge ?? this.makingCharge,
|
||||
otherCharges: otherCharges ?? this.otherCharges,
|
||||
sku: sku ?? this.sku,
|
||||
|
||||
huid: huid ?? this.huid,
|
||||
weight: weight ?? this.weight,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
localPhotoPath: localPhotoPath ?? this.localPhotoPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user