baseproject
This commit is contained in:
384
base_project/lib/view_model/auth/auth_view_model.dart
Normal file
384
base_project/lib/view_model/auth/auth_view_model.dart
Normal file
@@ -0,0 +1,384 @@
|
||||
import 'dart:async';
|
||||
import 'package:base_project/repository/auth_repo.dart';
|
||||
import 'package:base_project/routes/route_names.dart';
|
||||
import 'package:base_project/utils/flushbar_messages/flush_message_util.dart';
|
||||
import 'package:base_project/utils/managers/user_manager.dart';
|
||||
import 'package:base_project/utils/toast_messages/toast_message_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AuthViewModel extends ChangeNotifier {
|
||||
final AuthRepo _authRepo = AuthRepo();
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Timer? _timer;
|
||||
int _start = 30;
|
||||
bool _isResendEnabled = false;
|
||||
|
||||
int get start => _start;
|
||||
bool get isResendEnabled => _isResendEnabled;
|
||||
|
||||
void startTimer() {
|
||||
_isResendEnabled = false;
|
||||
_start = 30; // Set the timer duration (in seconds)
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) {
|
||||
if (_start == 0) {
|
||||
_timer?.cancel();
|
||||
_isResendEnabled = true;
|
||||
notifyListeners();
|
||||
} else {
|
||||
_start--;
|
||||
print(_start);
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var accId = "";
|
||||
var uEmail = "";
|
||||
|
||||
Future<void> login(BuildContext context, dynamic data) async {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
final value = await _authRepo.loginApi(data);
|
||||
print('Value is -$value');
|
||||
|
||||
// Check the response structure
|
||||
if (value['operationStatus'] == 'SUCCESS') {
|
||||
final item = value['item'];
|
||||
await UserManager().setUser(item);
|
||||
// Try to persist sys account from login response if present
|
||||
try {
|
||||
final dynamic accountCandidate =
|
||||
item['sysAccount'] ?? item['account'] ?? item['sys_account'];
|
||||
if (accountCandidate is Map<String, dynamic>) {
|
||||
await UserManager().setAccount(accountCandidate);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool('isLoggedIn', true);
|
||||
// Handle successful login
|
||||
ToastMessageUtil.showToast(
|
||||
message: 'Logged in as ${value['item']['email']}',
|
||||
toastType: ToastType.general);
|
||||
// nav to home
|
||||
Navigator.pushReplacementNamed(context, RouteNames.homeView);
|
||||
} else {
|
||||
print('Error got ');
|
||||
|
||||
// Handle login failure with backend message if available
|
||||
final backendMessage = (value is Map)
|
||||
? (value['operationMessage'] ?? value['message'] ?? value['msg'])
|
||||
: null;
|
||||
final message = (backendMessage?.toString().trim().isNotEmpty == true)
|
||||
? backendMessage.toString()
|
||||
: "Failed!! Email or Password is incorrect";
|
||||
|
||||
FlushBarMessageUtil.showFlushBar(
|
||||
context: context,
|
||||
message: message,
|
||||
flushBarType: FlushBarType.error,
|
||||
);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
print("Error -$error");
|
||||
print("st -$stackTrace");
|
||||
// Handle any other errors
|
||||
FlushBarMessageUtil.showFlushBar(
|
||||
context: context,
|
||||
message: error.toString(),
|
||||
flushBarType: FlushBarType.error,
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signUp(BuildContext context, dynamic data) async {
|
||||
setLoading(true);
|
||||
final createUserData = {...data, 'account_id': accId};
|
||||
print("CreateUserData--$createUserData");
|
||||
|
||||
_authRepo.createUserApi(createUserData).then((value) {
|
||||
print(value);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Account Created", toastType: ToastType.success);
|
||||
setLoading(false);
|
||||
Navigator.pushReplacementNamed(context, RouteNames.loginView);
|
||||
}).onError(
|
||||
(error, stackTrace) {
|
||||
print(error);
|
||||
print(stackTrace);
|
||||
FlushBarMessageUtil.showFlushBar(
|
||||
context: context,
|
||||
message: error.toString(),
|
||||
flushBarType: FlushBarType.error);
|
||||
setLoading(false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// New method for combined registration flow (account + user)
|
||||
Future<void> signUpWithAccountCreation(
|
||||
BuildContext context, dynamic data) async {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Step 1: Create account with just email and password
|
||||
final accountData = {
|
||||
"email": data["email"],
|
||||
};
|
||||
|
||||
print("Creating account with data: $accountData");
|
||||
final accountResponse = await _authRepo.createAcApi(accountData);
|
||||
print("Account creation response: $accountResponse");
|
||||
|
||||
// Extract account ID from response
|
||||
final accountId = accountResponse['account_id']?.toString();
|
||||
if (accountId == null) {
|
||||
throw Exception("Account ID not received from server");
|
||||
}
|
||||
|
||||
// Step 2: Create user with account ID
|
||||
final createUserData = {...data, 'account_id': accountId};
|
||||
|
||||
print("Creating user with data: $createUserData");
|
||||
final userResponse = await _authRepo.createUserApi(createUserData);
|
||||
print("User creation response: $userResponse");
|
||||
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Account Created Successfully",
|
||||
toastType: ToastType.success);
|
||||
|
||||
// Navigate to login
|
||||
Navigator.pushReplacementNamed(context, RouteNames.loginView);
|
||||
} catch (error, stackTrace) {
|
||||
print("SignUp error: $error");
|
||||
print("Stack trace: $stackTrace");
|
||||
|
||||
String errorMessage = "Registration failed. Please try again.";
|
||||
if (error.toString().contains('already exist')) {
|
||||
errorMessage = "Email already exists. Please use a different email.";
|
||||
} else if (error.toString().contains('account_id')) {
|
||||
errorMessage = "Account creation failed. Please try again.";
|
||||
}
|
||||
|
||||
FlushBarMessageUtil.showFlushBar(
|
||||
context: context,
|
||||
message: errorMessage,
|
||||
flushBarType: FlushBarType.error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createAcc(BuildContext context, dynamic data) async {
|
||||
setLoading(true);
|
||||
_authRepo.createAcApi(data).then(
|
||||
(value) {
|
||||
print("Value--$value");
|
||||
accId = value['account_id'].toString();
|
||||
setLoading(false);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Success creating account", toastType: ToastType.success);
|
||||
// Navigator.pushNamed(context, RouteNames.signUp,arguments: uEmail);
|
||||
},
|
||||
).onError(
|
||||
(error, stackTrace) {
|
||||
print("error--$error");
|
||||
setLoading(false);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Error registering your account",
|
||||
toastType: ToastType.error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getOtp(BuildContext context, dynamic data) async {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
final value = await _authRepo.getOtpApi(data);
|
||||
print(value);
|
||||
|
||||
// Handle non-exception responses like { msg: "... already exist" }
|
||||
if (value is Map) {
|
||||
final map = Map<String, dynamic>.from(value);
|
||||
final message = (map['msg'] ?? map['message'] ?? '').toString();
|
||||
final operationStatus = (map['operationStatus'] ?? '').toString();
|
||||
final otpSent = (map['otp_sent'] == true) || (map['otpSent'] == true);
|
||||
|
||||
// Block when backend says email already exists
|
||||
if (message.toLowerCase().contains('already exist')) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: 'Email already exists',
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return; // do not navigate
|
||||
}
|
||||
|
||||
// Consider success scenarios (handle variants like "Otp send successfully")
|
||||
final lowerMsg = message.toLowerCase();
|
||||
final messageLooksSuccess = lowerMsg.contains('otp sent') ||
|
||||
lowerMsg.contains('sent successfully') ||
|
||||
(lowerMsg.contains('otp') && lowerMsg.contains('success')) ||
|
||||
lowerMsg.contains('send successfully');
|
||||
|
||||
if (operationStatus == 'SUCCESS' || otpSent || messageLooksSuccess) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: message.isNotEmpty ? message : 'OTP sent successfully',
|
||||
toastType: ToastType.success,
|
||||
);
|
||||
// Start resend countdown when OTP is sent
|
||||
startTimer();
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
RouteNames.verifyOtpView,
|
||||
arguments: {'email': data['email']},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: show backend message if present
|
||||
if (message.isNotEmpty) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: message,
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown response structure
|
||||
ToastMessageUtil.showToast(
|
||||
message: 'Failed to send OTP',
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
} catch (error) {
|
||||
// Handle any other exceptions
|
||||
print("getOtp error: $error");
|
||||
final msg = error.toString();
|
||||
String uiMessage = 'Failed to send OTP';
|
||||
if (msg.contains('already exist') || msg.contains('already exists')) {
|
||||
uiMessage = 'Email already exists';
|
||||
} else if (msg.contains('invalid') || msg.contains('Invalid')) {
|
||||
uiMessage = 'Invalid email address';
|
||||
} else if (msg.contains('timeout')) {
|
||||
uiMessage = 'Request timed out. Please try again.';
|
||||
}
|
||||
|
||||
ToastMessageUtil.showToast(
|
||||
message: uiMessage,
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resendOtp(BuildContext context, dynamic data) async {
|
||||
startTimer();
|
||||
await _authRepo.resendOtpApi(data).then((value) {
|
||||
print(value);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "OTP resend successfully",
|
||||
toastType: ToastType.success,
|
||||
);
|
||||
}).onError(
|
||||
(error, stackTrace) {
|
||||
print(error);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Failed to resend OTP",
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns null on success, or a human-readable error message on failure
|
||||
Future<String?> verifyOtp(BuildContext context, dynamic queryParams) async {
|
||||
print("Verifying otp");
|
||||
setLoading(true);
|
||||
try {
|
||||
final value = await _authRepo.verifyOtpApi(queryParams);
|
||||
print(value);
|
||||
|
||||
// Backend returns 200 with EntityResponse("OTP Verified")
|
||||
// and 400 with EntityResponse("Wrong OTP") or MessageResponse("... not exist")
|
||||
if (value is Map) {
|
||||
final map = Map<String, dynamic>.from(value);
|
||||
final message = (map['msg'] ?? map['message'] ?? '').toString();
|
||||
final lower = message.toLowerCase();
|
||||
|
||||
if (lower.contains('otp verified')) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: 'OTP Verified',
|
||||
toastType: ToastType.success,
|
||||
);
|
||||
uEmail = queryParams['email'];
|
||||
cancelTimer();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lower.contains('wrong otp')) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: 'Wrong OTP',
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return 'Wrong OTP';
|
||||
}
|
||||
|
||||
if (lower.contains('not exist')) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: 'User not found',
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return 'User not found';
|
||||
}
|
||||
|
||||
// Fallback: show message if present
|
||||
if (message.isNotEmpty) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: message,
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
ToastMessageUtil.showToast(
|
||||
message: 'OTP verification failed',
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return 'OTP verification failed';
|
||||
} catch (error) {
|
||||
print(error);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "OTP verification failed",
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return 'OTP verification failed';
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void cancelTimer() {
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cancelTimer(); // Ensure the timer is cancelled
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
209
base_project/lib/view_model/profile/profile_view_model.dart
Normal file
209
base_project/lib/view_model/profile/profile_view_model.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:base_project/data/response/api_response.dart';
|
||||
import 'package:base_project/model/user/user_profile.dart';
|
||||
import 'package:base_project/repository/profile/profile_repo.dart';
|
||||
import 'package:base_project/utils/managers/user_manager.dart';
|
||||
import 'package:base_project/utils/toast_messages/toast_message_util.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class ProfileViewModel extends ChangeNotifier {
|
||||
final _repo = ProfileRepo();
|
||||
Map<String, dynamic>? _sysAccount;
|
||||
Map<String, dynamic>? get sysAccount => _sysAccount;
|
||||
|
||||
ApiResponse<UserProfile> userProfile = ApiResponse.loading();
|
||||
|
||||
Uint8List? _profileImageBytes; // Variable for storing fetched image bytes
|
||||
|
||||
bool _isUpdating = false;
|
||||
bool get isUpdating => _isUpdating;
|
||||
|
||||
XFile? _profileImage;
|
||||
XFile? get profileImg => _profileImage;
|
||||
|
||||
// Setters
|
||||
setUserProfile(ApiResponse<UserProfile> val) {
|
||||
userProfile = val;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setUpdating(bool val) {
|
||||
_isUpdating = val;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setImage(XFile? val) {
|
||||
_profileImage = val;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setProfileImageBytes(Uint8List? val) {
|
||||
_profileImageBytes = val;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Picking an image from the gallery
|
||||
void pickImg(BuildContext context) async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
|
||||
|
||||
if (image != null) {
|
||||
setImage(image);
|
||||
updateImage(context);
|
||||
} else {
|
||||
ToastMessageUtil.showToast(
|
||||
message: "No image picked !!", toastType: ToastType.warning);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetching the user profile details
|
||||
Future<void> getProfile() async {
|
||||
setUserProfile(ApiResponse.loading());
|
||||
_repo.getProfileApi().then((value) {
|
||||
log("Profile Data--$value");
|
||||
setUserProfile(ApiResponse.success(UserProfile.fromJson(value)));
|
||||
try {
|
||||
_sysAccount =
|
||||
UserManager().account ?? value['sysAccount'] ?? value['account'];
|
||||
} catch (_) {}
|
||||
}).onError((error, stackTrace) {
|
||||
log("ERROR--$error");
|
||||
setUserProfile(ApiResponse.error(error.toString()));
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Error fetching profile data", toastType: ToastType.error);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateAccount(
|
||||
BuildContext context, Map<String, dynamic> form) async {
|
||||
setUpdating(true);
|
||||
try {
|
||||
final accId = UserManager().accountId;
|
||||
if (accId == null) {
|
||||
throw Exception('Account not found');
|
||||
}
|
||||
final body = {
|
||||
'companyName': form['companyName'],
|
||||
'workspace': form['workspace'],
|
||||
'gstNumber': form['gstNumber'],
|
||||
'mobile': form['mobile'],
|
||||
'email': form['email'],
|
||||
'pancard': form['pancard'],
|
||||
'working': form['working'],
|
||||
'active': form['active'],
|
||||
};
|
||||
final res = await _repo.updateAccountApi(accId, body);
|
||||
if (res is Map<String, dynamic>) {
|
||||
_sysAccount = res;
|
||||
await UserManager().setAccount(res);
|
||||
}
|
||||
ToastMessageUtil.showToast(
|
||||
message: 'Account Updated', toastType: ToastType.success);
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: 'Failed to update account', toastType: ToastType.error);
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetching the profile image
|
||||
Future<void> getProfileImg() async {
|
||||
_repo.getProfileImgApi().then((value) {
|
||||
String base64Image = value['image'];
|
||||
// Remove data URL prefix and decode base64
|
||||
Uint8List imageBytes = base64Decode(base64Image.split(',').last);
|
||||
setProfileImageBytes(imageBytes); // Store image bytes
|
||||
}).onError((error, stackTrace) {
|
||||
log("Error Fetching Image--$error");
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Error fetching profile image", toastType: ToastType.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Updating the profile details
|
||||
Future<void> updateProfile(BuildContext context, dynamic data) async {
|
||||
setUpdating(true);
|
||||
final uId = UserManager().userId;
|
||||
data['userId'] = uId.toString();
|
||||
print("Data--$data");
|
||||
_repo.updateProfileApi(data, uId).then((value) {
|
||||
log("Profile Update--$value");
|
||||
setUpdating(false);
|
||||
getProfile();
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Profile Updated", toastType: ToastType.success);
|
||||
Navigator.pop(context); // Close dialog/screen after update
|
||||
}).onError((error, stackTrace) {
|
||||
log("Error Updating Profile--$error");
|
||||
setUpdating(false);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Error updating profile!!", toastType: ToastType.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Updating the profile image
|
||||
Future<void> updateImage(BuildContext context) async {
|
||||
if (_profileImage == null) return; // Ensure an image is selected
|
||||
|
||||
setUpdating(true);
|
||||
Uint8List fileBytes = await _profileImage!.readAsBytes();
|
||||
|
||||
FormData formData = FormData.fromMap({
|
||||
'imageFile': MultipartFile.fromBytes(
|
||||
fileBytes,
|
||||
filename: _profileImage!.name,
|
||||
),
|
||||
});
|
||||
|
||||
_repo.updateProfileImg(formData).then((value) {
|
||||
log("Image Update--$value");
|
||||
setUpdating(false);
|
||||
getProfileImg(); // Refresh the displayed image
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Profile Image Updated", toastType: ToastType.success);
|
||||
}).onError((error, stackTrace) {
|
||||
log("Error Updating Image--$error");
|
||||
setUpdating(false);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Error updating image!!", toastType: ToastType.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Changing the password
|
||||
Future<void> changePassword(dynamic data) async {
|
||||
setUpdating(true);
|
||||
print("BODY--$data");
|
||||
|
||||
_repo.changPassApi(data).then((value) {
|
||||
print("Password Change--$value");
|
||||
setUpdating(false);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Password updated", toastType: ToastType.success);
|
||||
}).onError((error, stackTrace) {
|
||||
print("Error Changing Password--$error");
|
||||
setUpdating(false);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Error updating password!!", toastType: ToastType.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper method to generate headers
|
||||
// Network headers are now centralized in NetworkApiService via UserManager
|
||||
|
||||
// Method to return image bytes (can be used in the UI to display)
|
||||
Uint8List? get profileImageBytes => _profileImageBytes;
|
||||
|
||||
void reset() {
|
||||
_profileImage = null;
|
||||
_profileImageBytes = null;
|
||||
userProfile = ApiResponse.loading(); // Or any default state
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
44
base_project/lib/view_model/splash_view_model.dart
Normal file
44
base_project/lib/view_model/splash_view_model.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:base_project/routes/route_names.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SplashViewModel {
|
||||
Future<void> checkNavigation(BuildContext context) async {
|
||||
print("Checking navigation...");
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
|
||||
|
||||
// Wait for splash animations to complete (3 seconds total)
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
|
||||
if (!context.mounted) return; // Check if widget is still mounted
|
||||
|
||||
if (!isLoggedIn) {
|
||||
print("Navigating to login...");
|
||||
// Navigate to login with fade transition
|
||||
Navigator.pushReplacementNamed(
|
||||
context,
|
||||
RouteNames.loginView,
|
||||
);
|
||||
} else {
|
||||
print("Navigating to home...");
|
||||
// Navigate to home with fade transition
|
||||
Navigator.pushReplacementNamed(
|
||||
context,
|
||||
RouteNames.homeView,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error in splash navigation: $e");
|
||||
// Fallback to login on error
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacementNamed(
|
||||
context,
|
||||
RouteNames.loginView,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
import 'package:base_project/model/system_params_model.dart';
|
||||
import 'package:base_project/repository/system_params_repo.dart';
|
||||
import 'package:base_project/utils/managers/user_manager.dart';
|
||||
import 'package:base_project/utils/toast_messages/toast_message_util.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../core/providers/dynamic_theme_provider.dart';
|
||||
|
||||
class SystemParamsViewModel extends ChangeNotifier {
|
||||
final _repo = SystemParamsRepo();
|
||||
|
||||
Uint8List? _profileImageBytes;
|
||||
XFile? _profileImage;
|
||||
bool _loading = false;
|
||||
|
||||
// Store system parameters as model
|
||||
SystemParamsModel? _systemParams;
|
||||
Map<String, dynamic> _systemParamsList = {};
|
||||
|
||||
// Getter for system parameters model
|
||||
SystemParamsModel? get systemParams => _systemParams;
|
||||
|
||||
// Getter for system parameters list (for backward compatibility)
|
||||
Map<String, dynamic> get systemParamsList => _systemParamsList;
|
||||
|
||||
// Getter for profile image bytes
|
||||
Uint8List? get profileImageBytes => _profileImageBytes;
|
||||
|
||||
// Getter for profile image
|
||||
XFile? get profileImg => _profileImage;
|
||||
|
||||
// Getter for loading status
|
||||
bool get loading => _loading;
|
||||
|
||||
// Setter for loading status with notification to listeners
|
||||
set loading(bool value) {
|
||||
_loading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Setter for profile image
|
||||
setImage(XFile? val) {
|
||||
_profileImage = val;
|
||||
// Avoid notify during build; schedule for next frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
// Setter for profile image bytes
|
||||
setProfileImageBytes(Uint8List? val) {
|
||||
_profileImageBytes = val;
|
||||
// Avoid notify during build; schedule for next frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
notifyListeners();
|
||||
});
|
||||
// Persist to SharedPreferences for future sessions
|
||||
_persistLogoToLocal(val);
|
||||
}
|
||||
|
||||
// Method to pick an image from the gallery
|
||||
void pickImg(BuildContext context) async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
|
||||
|
||||
if (image != null) {
|
||||
setImage(image);
|
||||
// Convert image to bytes and store
|
||||
final bytes = await image.readAsBytes();
|
||||
setProfileImageBytes(bytes);
|
||||
|
||||
// Generate dynamic theme IMMEDIATELY when image is selected
|
||||
print("Image selected - generating dynamic theme immediately...");
|
||||
await _generateDynamicThemeFromLogo(context);
|
||||
print("Dynamic theme generated immediately after image selection!");
|
||||
|
||||
// Show success message
|
||||
ToastMessageUtil.showToast(
|
||||
message:
|
||||
"Logo selected! Dynamic theme applied. Click 'Upload Logo' to save permanently.",
|
||||
toastType: ToastType.success);
|
||||
} else {
|
||||
ToastMessageUtil.showToast(
|
||||
message: "No image picked !!", toastType: ToastType.warning);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to upload logo image separately (like edit profile)
|
||||
Future<void> uploadLogo(BuildContext context) async {
|
||||
if (_profileImage == null) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Please pick an image first!", toastType: ToastType.warning);
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
Uint8List fileBytes = await _profileImage!.readAsBytes();
|
||||
print("Logo file size: ${fileBytes.length} bytes");
|
||||
|
||||
// Generate dynamic theme IMMEDIATELY after image selection (before API call)
|
||||
print("Generating dynamic theme immediately...");
|
||||
await _generateDynamicThemeFromLogo(context);
|
||||
print(
|
||||
"Dynamic theme generated immediately - user sees instant feedback!");
|
||||
|
||||
// Now proceed with API upload
|
||||
FormData formData = FormData.fromMap({
|
||||
'imageFile': MultipartFile.fromBytes(
|
||||
fileBytes,
|
||||
filename: _profileImage!.name,
|
||||
),
|
||||
});
|
||||
|
||||
dynamic uploadResult;
|
||||
try {
|
||||
uploadResult = await _repo.uploadSystemParamLogo(formData);
|
||||
print("----LOGO-UPLOADED----");
|
||||
print(uploadResult);
|
||||
} catch (e) {
|
||||
// If backend returns intermittent error but file actually uploaded,
|
||||
// avoid showing a scary error immediately. Log and proceed to metadata update.
|
||||
print("Upload API error (may be intermittent): $e");
|
||||
}
|
||||
|
||||
// Try to update system parameters regardless (backend may have the file already)
|
||||
bool metaUpdated = false;
|
||||
try {
|
||||
await _updateLogoInSystemParams();
|
||||
metaUpdated = true;
|
||||
} catch (e) {
|
||||
print("Logo metadata update failed: $e");
|
||||
}
|
||||
|
||||
loading = false;
|
||||
|
||||
if (metaUpdated || uploadResult != null) {
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Company logo uploaded! Dynamic theme applied.",
|
||||
toastType: ToastType.success);
|
||||
} else {
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Logo upload may have failed. Please retry.",
|
||||
toastType: ToastType.warning);
|
||||
}
|
||||
} catch (error) {
|
||||
loading = false;
|
||||
print("Error uploading logo: $error");
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Error uploading logo: ${error.toString()}",
|
||||
toastType: ToastType.error);
|
||||
// Even if API fails, theme is already applied - user keeps the visual benefit!
|
||||
print(
|
||||
"API failed but dynamic theme is already applied - user keeps the visual benefit!");
|
||||
}
|
||||
}
|
||||
|
||||
// Generate dynamic theme from logo
|
||||
Future<void> _generateDynamicThemeFromLogo(BuildContext context) async {
|
||||
try {
|
||||
print("_generateDynamicThemeFromLogo called");
|
||||
print("_profileImageBytes is null: ${_profileImageBytes == null}");
|
||||
print("_profileImageBytes length: ${_profileImageBytes?.length ?? 0}");
|
||||
|
||||
if (_profileImageBytes != null) {
|
||||
final dynamicThemeProvider =
|
||||
Provider.of<DynamicThemeProvider>(context, listen: false);
|
||||
print("DynamicThemeProvider found: ${dynamicThemeProvider != null}");
|
||||
print(
|
||||
"Before theme generation - isUsingDynamicTheme: ${dynamicThemeProvider.isUsingDynamicTheme}");
|
||||
|
||||
await dynamicThemeProvider.generateThemeFromLogo(_profileImageBytes!);
|
||||
|
||||
print(
|
||||
"After theme generation - isUsingDynamicTheme: ${dynamicThemeProvider.isUsingDynamicTheme}");
|
||||
print("Dynamic theme generated successfully from logo!");
|
||||
} else {
|
||||
print("_profileImageBytes is null, cannot generate theme");
|
||||
}
|
||||
} catch (error) {
|
||||
print("Error generating dynamic theme: $error");
|
||||
}
|
||||
}
|
||||
|
||||
// Update system parameters to include logo information
|
||||
Future<void> _updateLogoInSystemParams() async {
|
||||
try {
|
||||
final logoData = {
|
||||
'upload_Logo': _profileImageBytes,
|
||||
'upload_Logo_name': _profileImage?.name ?? 'logo.png',
|
||||
'upload_Logo_path': _profileImage?.path ?? '',
|
||||
};
|
||||
|
||||
await _repo.updateSystemParameters(logoData);
|
||||
print("Logo info updated in system parameters");
|
||||
} catch (error) {
|
||||
print("Error updating logo info in system params: $error");
|
||||
}
|
||||
}
|
||||
|
||||
// Method to fetch system parameters and store them in the model
|
||||
Future<void> getSystemParams() async {
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
final value = await _repo.getSystemParameters();
|
||||
print("----SYSTEM-PARAMS----");
|
||||
|
||||
// Normalize response to Map<String, dynamic>
|
||||
Map<String, dynamic> normalized = {};
|
||||
if (value is Map<String, dynamic>) {
|
||||
normalized = value;
|
||||
} else if (value != null) {
|
||||
try {
|
||||
normalized = Map<String, dynamic>.from(value as Map);
|
||||
} catch (_) {
|
||||
normalized = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Store in both formats for backward compatibility
|
||||
_systemParamsList = normalized;
|
||||
_systemParams = SystemParamsModel.fromJson(normalized);
|
||||
|
||||
// Load existing logo if available
|
||||
_loadExistingLogo(value);
|
||||
|
||||
loading = false;
|
||||
print(normalized);
|
||||
} catch (error) {
|
||||
loading = false;
|
||||
print("Error-$error");
|
||||
}
|
||||
}
|
||||
|
||||
// Load existing logo from system parameters
|
||||
void _loadExistingLogo(Map<String, dynamic> systemParams) {
|
||||
try {
|
||||
// Check if logo data exists in system parameters
|
||||
if (systemParams['upload_Logo'] != null) {
|
||||
// If logo is stored as base64 string
|
||||
if (systemParams['upload_Logo'] is String) {
|
||||
final String base64Image = systemParams['upload_Logo'];
|
||||
if (base64Image.isNotEmpty) {
|
||||
// Remove data URL prefix if present and decode base64
|
||||
String cleanBase64 = base64Image;
|
||||
if (base64Image.contains(',')) {
|
||||
cleanBase64 = base64Image.split(',').last;
|
||||
}
|
||||
try {
|
||||
final Uint8List imageBytes = base64Decode(cleanBase64);
|
||||
setProfileImageBytes(imageBytes);
|
||||
print("Existing logo loaded from system parameters");
|
||||
} catch (e) {
|
||||
print("Error decoding logo: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
// If logo is stored as bytes
|
||||
else if (systemParams['upload_Logo'] is List) {
|
||||
final List<int> logoBytes =
|
||||
List<int>.from(systemParams['upload_Logo']);
|
||||
setProfileImageBytes(Uint8List.fromList(logoBytes));
|
||||
print("Existing logo loaded from system parameters");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
print("Error loading existing logo: $error");
|
||||
}
|
||||
// Also attempt to load from local storage if not present in systemParams
|
||||
if (_profileImageBytes == null) {
|
||||
_loadLogoFromLocal();
|
||||
}
|
||||
}
|
||||
|
||||
// Method to update system parameters
|
||||
Future<void> updateSystemParams(Map<String, dynamic> body) async {
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
// Convert the form data to proper backend format
|
||||
final backendData = _convertFormDataToBackendFormat(body);
|
||||
|
||||
final value = await _repo.updateSystemParameters(backendData);
|
||||
print("----SYSTEM-PARAMS-UPDATED----");
|
||||
loading = false;
|
||||
print(value);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "System parameters updated!!", toastType: ToastType.success);
|
||||
} catch (error) {
|
||||
loading = false;
|
||||
print("Error-$error");
|
||||
ToastMessageUtil.showToast(
|
||||
message: error.toString(), toastType: ToastType.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert form data to backend format
|
||||
Map<String, dynamic> _convertFormDataToBackendFormat(
|
||||
Map<String, dynamic> formData) {
|
||||
final Map<String, dynamic> backendData = {};
|
||||
|
||||
// Map form field names to backend field names
|
||||
for (String key in formData.keys) {
|
||||
switch (key) {
|
||||
case 'Scheduler Timer':
|
||||
backendData['schedulerTime'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'Lease Tax Code':
|
||||
backendData['leaseTaxCode'] = formData[key];
|
||||
break;
|
||||
case 'Vessel Confirmation':
|
||||
backendData['vesselConfProcessLimit'] =
|
||||
int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'Row to Display':
|
||||
backendData['rowToDisplay'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'Link to Display':
|
||||
backendData['linkToDisplay'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'Row to Add':
|
||||
backendData['rowToAdd'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'LOV Row to Display':
|
||||
backendData['lovRowToDisplay'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'LOV Link to Display':
|
||||
backendData['lovLinkToDisplay'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'OID Server Name':
|
||||
backendData['oidserverName'] = formData[key];
|
||||
break;
|
||||
case 'OID Base':
|
||||
backendData['oidBase'] = formData[key];
|
||||
break;
|
||||
case 'OID Admin User':
|
||||
backendData['oidAdminUser'] = formData[key];
|
||||
break;
|
||||
case 'OID Server Port':
|
||||
backendData['oidServerPort'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'User Default Group':
|
||||
backendData['userDefaultGroup'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'Default Department':
|
||||
backendData['defaultDepartment'] = formData[key];
|
||||
break;
|
||||
case 'Default Position':
|
||||
backendData['defaultPosition'] = formData[key];
|
||||
break;
|
||||
case 'Single Charge':
|
||||
backendData['singleCharge'] = formData[key];
|
||||
break;
|
||||
case 'First Day of the Week':
|
||||
backendData['firstDayOftheWeek'] = formData[key];
|
||||
break;
|
||||
case 'Hours Per Shift':
|
||||
backendData['hourPerShift'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'CN Billing Frequency':
|
||||
backendData['cnBillingFrequency'] = int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'Billing Department Code':
|
||||
backendData['billingDepartmentCode'] = formData[key];
|
||||
break;
|
||||
case 'Base Price List':
|
||||
backendData['basePriceList'] = formData[key];
|
||||
break;
|
||||
case 'Non-Container Service Order':
|
||||
backendData['nonContainerServiceOrder'] = formData[key];
|
||||
break;
|
||||
case 'EDI MAE Scheduler':
|
||||
backendData['ediMaeSchedulerONOFF'] =
|
||||
int.tryParse(formData[key]) ?? 0;
|
||||
break;
|
||||
case 'EDI Scheduler':
|
||||
backendData['ediSchedulerONOFF'] = formData[key];
|
||||
break;
|
||||
case 'Company Name':
|
||||
backendData['Company_Display_Name'] = formData[key];
|
||||
break;
|
||||
case 'Registration Allowed':
|
||||
backendData['isRegitrationAllowed'] = formData[key] == 'true';
|
||||
break;
|
||||
default:
|
||||
// For any unmapped fields, pass through as is
|
||||
backendData[key] = formData[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add logo upload fields if available
|
||||
if (_profileImageBytes != null) {
|
||||
backendData['upload_Logo'] = _profileImageBytes;
|
||||
backendData['upload_Logo_name'] = _profileImage?.name ?? 'logo.png';
|
||||
backendData['upload_Logo_path'] = _profileImage?.path ?? '';
|
||||
}
|
||||
|
||||
return backendData;
|
||||
}
|
||||
|
||||
// Updating the profile image
|
||||
Future<void> updateImage(BuildContext context) async {
|
||||
if (_profileImage == null) return; // Ensure an image is selected
|
||||
|
||||
loading = true;
|
||||
Uint8List fileBytes = await _profileImage!.readAsBytes();
|
||||
|
||||
FormData formData = FormData.fromMap({
|
||||
'imageFile': MultipartFile.fromBytes(
|
||||
fileBytes,
|
||||
filename: _profileImage!.name,
|
||||
),
|
||||
});
|
||||
|
||||
_repo.updateProfileImg(formData).then((value) {
|
||||
log("Image Update--$value");
|
||||
loading = false;
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Profile Image Updated", toastType: ToastType.success);
|
||||
}).onError((error, stackTrace) {
|
||||
log("Error Updating Image--$error");
|
||||
loading = false;
|
||||
// setUpdating(false);
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Error updating image!!", toastType: ToastType.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Method to clear logo and reset theme
|
||||
Future<void> clearLogo(BuildContext context) async {
|
||||
try {
|
||||
// Clear the image data
|
||||
setImage(null);
|
||||
setProfileImageBytes(null);
|
||||
|
||||
// Reset to default theme
|
||||
final dynamicThemeProvider =
|
||||
Provider.of<DynamicThemeProvider>(context, listen: false);
|
||||
dynamicThemeProvider.resetToDefaultTheme();
|
||||
print("Logo cleared and theme reset to default");
|
||||
|
||||
// Update system parameters to remove logo info
|
||||
await _updateLogoInSystemParams();
|
||||
// Remove locally persisted logo
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('cached_logo_bytes');
|
||||
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Logo cleared! Theme reset to default.",
|
||||
toastType: ToastType.info);
|
||||
} catch (error) {
|
||||
print("Error clearing logo: $error");
|
||||
ToastMessageUtil.showToast(
|
||||
message: "Error clearing logo: ${error.toString()}",
|
||||
toastType: ToastType.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist logo bytes locally
|
||||
Future<void> _persistLogoToLocal(Uint8List? bytes) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (bytes == null || bytes.isEmpty) {
|
||||
await prefs.remove('cached_logo_bytes');
|
||||
return;
|
||||
}
|
||||
final base64Str = base64Encode(bytes);
|
||||
await prefs.setString('cached_logo_bytes', base64Str);
|
||||
} catch (e) {
|
||||
print('Error persisting logo locally: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Load logo bytes from local storage
|
||||
Future<void> _loadLogoFromLocal() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final base64Str = prefs.getString('cached_logo_bytes');
|
||||
if (base64Str != null && base64Str.isNotEmpty) {
|
||||
final bytes = base64Decode(base64Str);
|
||||
setProfileImageBytes(bytes);
|
||||
print('Loaded logo from local storage');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading logo from local storage: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user