update info

This commit is contained in:
Phuoc Nguyen
2025-11-20 16:32:33 +07:00
parent 1fcef52d5e
commit dc85157758
12 changed files with 1613 additions and 247 deletions

View File

@@ -128,4 +128,95 @@ class UserInfoRemoteDataSource {
// Could add cache-busting headers in the future if needed
return getUserInfo();
}
/// Update User Info
///
/// Updates the current user's profile information.
///
/// API: POST https://land.dbiz.com/api/method/building_material.building_material.api.user.update_user_info
///
/// Request body:
/// ```json
/// {
/// "full_name": "...",
/// "date_of_birth": "YYYY-MM-DD",
/// "gender": "Male/Female",
/// "company_name": "...",
/// "tax_code": "...",
/// "avatar_base64": null | base64_string,
/// "id_card_front_base64": null | base64_string,
/// "id_card_back_base64": null | base64_string,
/// "certificates_base64": [] | [base64_string, ...]
/// }
/// ```
///
/// Throws:
/// - [UnauthorizedException] if user not authenticated (401)
/// - [ServerException] if server error occurs (500+)
/// - [NetworkException] for other network errors
Future<UserInfoModel> updateUserInfo(Map<String, dynamic> data) async {
try {
debugPrint('🔵 [UserInfoDataSource] Updating user info...');
debugPrint('🔵 [UserInfoDataSource] Data: $data');
final startTime = DateTime.now();
// Make POST request with update data
final response = await _dioClient.post<Map<String, dynamic>>(
'/api/method/building_material.building_material.api.user.update_user_info',
data: data,
);
final duration = DateTime.now().difference(startTime);
debugPrint('🟢 [UserInfoDataSource] Update response received in ${duration.inMilliseconds}ms');
debugPrint('🟢 [UserInfoDataSource] Status: ${response.statusCode}');
// Check response status
if (response.statusCode == 200) {
// After successful update, fetch fresh user info
debugPrint('✅ [UserInfoDataSource] Successfully updated user info');
return await getUserInfo();
} else {
throw ServerException(
'Failed to update user info: ${response.statusCode}',
response.statusCode,
);
}
} on DioException catch (e) {
// Handle specific HTTP status codes
if (e.response?.statusCode == 401) {
throw const UnauthorizedException(
'Session expired. Please login again.',
);
} else if (e.response?.statusCode == 403) {
throw const ForbiddenException();
} else if (e.response?.statusCode == 404) {
throw NotFoundException('Update user info endpoint not found');
} else if (e.response?.statusCode != null &&
e.response!.statusCode! >= 500) {
throw ServerException(
'Server error: ${e.response?.statusMessage ?? "Unknown error"}',
e.response?.statusCode,
);
} else if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
throw const TimeoutException();
} else if (e.type == DioExceptionType.connectionError) {
throw const NoInternetException();
} else {
throw NetworkException(
e.message ?? 'Failed to update user info',
statusCode: e.response?.statusCode,
);
}
} catch (e) {
// Handle unexpected errors
if (e is ServerException ||
e is UnauthorizedException ||
e is NetworkException ||
e is NotFoundException) {
rethrow;
}
throw ServerException('Unexpected error: $e');
}
}
}

View File

@@ -27,6 +27,15 @@ class UserInfoModel {
this.taxId,
this.address,
this.cccd,
this.dateOfBirth,
this.gender,
this.idCardFront,
this.idCardBack,
this.certificates = const [],
this.membershipStatus,
this.membershipStatusColor,
this.isVerified = false,
this.credentialDisplay = false,
this.referralCode,
this.erpnextCustomerId,
this.createdAt,
@@ -48,6 +57,15 @@ class UserInfoModel {
final String? taxId;
final String? address;
final String? cccd;
final DateTime? dateOfBirth;
final String? gender;
final String? idCardFront;
final String? idCardBack;
final List<String> certificates;
final String? membershipStatus;
final String? membershipStatusColor;
final bool isVerified;
final bool credentialDisplay;
final String? referralCode;
final String? erpnextCustomerId;
final DateTime? createdAt;
@@ -87,9 +105,9 @@ class UserInfoModel {
/// }
/// ```
factory UserInfoModel.fromJson(Map<String, dynamic> json) {
// API response structure: { "message": { "success": true, "data": {...} } }
final message = json['message'] as Map<String, dynamic>?;
final data = message?['data'] as Map<String, dynamic>? ?? json;
// API response structure: { "message": { "full_name": "...", ... } }
// Data is directly under 'message', not nested in 'data'
final data = json['message'] as Map<String, dynamic>? ?? json;
return UserInfoModel(
// Use email as userId since API doesn't provide user_id
@@ -108,11 +126,22 @@ class UserInfoModel {
companyName: data['company_name'] as String?,
taxId: data['tax_code'] as String?,
address: data['address'] as String?,
cccd: data['id_card_front'] as String?, // Store front ID card
cccd: null, // CCCD number not in API
dateOfBirth: _parseDateTime(data['date_of_birth'] as String?),
gender: data['gender'] as String?,
idCardFront: data['id_card_front'] as String?,
idCardBack: data['id_card_back'] as String?,
certificates: (data['certificates'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ?? const [],
membershipStatus: data['membership_status'] as String?,
membershipStatusColor: data['membership_status_color'] as String?,
isVerified: data['is_verified'] as bool? ?? false,
credentialDisplay: data['credential_display'] as bool? ?? false,
referralCode: null,
erpnextCustomerId: null,
createdAt: _parseDateTime(data['date_of_birth'] as String?),
updatedAt: _parseDateTime(data['date_of_birth'] as String?),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
@@ -164,6 +193,15 @@ class UserInfoModel {
taxId: taxId,
address: address,
cccd: cccd,
dateOfBirth: dateOfBirth,
gender: gender,
idCardFront: idCardFront,
idCardBack: idCardBack,
certificates: certificates,
membershipStatus: membershipStatus,
membershipStatusColor: membershipStatusColor,
isVerified: isVerified,
credentialDisplay: credentialDisplay,
referralCode: referralCode,
erpnextCustomerId: erpnextCustomerId,
createdAt: createdAt ?? now,

View File

@@ -88,6 +88,41 @@ class UserInfoRepositoryImpl implements UserInfoRepository {
throw ServerException('Failed to refresh user info: $e');
}
}
// =========================================================================
// UPDATE USER INFO
// =========================================================================
@override
Future<UserInfo> updateUserInfo(Map<String, dynamic> data) async {
try {
_debugPrint('Updating user info via API');
_debugPrint('Update data: $data');
// Update via remote datasource (will fetch fresh data after update)
final userInfoModel = await remoteDataSource.updateUserInfo(data);
_debugPrint('Successfully updated user info: ${userInfoModel.fullName}');
// Convert model to entity
return userInfoModel.toEntity();
} on UnauthorizedException catch (e) {
_debugPrint('Unauthorized error on update: $e');
rethrow;
} on NotFoundException catch (e) {
_debugPrint('Not found error on update: $e');
rethrow;
} on ServerException catch (e) {
_debugPrint('Server error on update: $e');
rethrow;
} on NetworkException catch (e) {
_debugPrint('Network error on update: $e');
rethrow;
} catch (e) {
_debugPrint('Unexpected error on update: $e');
throw ServerException('Failed to update user info: $e');
}
}
}
// ============================================================================

View File

@@ -60,6 +60,33 @@ class UserInfo {
/// CCCD/ID card number
final String? cccd;
/// Date of birth
final DateTime? dateOfBirth;
/// Gender
final String? gender;
/// ID card front image URL
final String? idCardFront;
/// ID card back image URL
final String? idCardBack;
/// Certificate image URLs
final List<String> certificates;
/// Membership verification status text
final String? membershipStatus;
/// Membership status color indicator
final String? membershipStatusColor;
/// Whether user is verified
final bool isVerified;
/// Whether to display credential verification form
final bool credentialDisplay;
/// Referral code
final String? referralCode;
@@ -88,6 +115,15 @@ class UserInfo {
this.taxId,
this.address,
this.cccd,
this.dateOfBirth,
this.gender,
this.idCardFront,
this.idCardBack,
this.certificates = const [],
this.membershipStatus,
this.membershipStatusColor,
this.isVerified = false,
this.credentialDisplay = false,
this.referralCode,
this.erpnextCustomerId,
required this.createdAt,
@@ -151,6 +187,15 @@ class UserInfo {
String? taxId,
String? address,
String? cccd,
DateTime? dateOfBirth,
String? gender,
String? idCardFront,
String? idCardBack,
List<String>? certificates,
String? membershipStatus,
String? membershipStatusColor,
bool? isVerified,
bool? credentialDisplay,
String? referralCode,
String? erpnextCustomerId,
DateTime? createdAt,
@@ -172,6 +217,15 @@ class UserInfo {
taxId: taxId ?? this.taxId,
address: address ?? this.address,
cccd: cccd ?? this.cccd,
dateOfBirth: dateOfBirth ?? this.dateOfBirth,
gender: gender ?? this.gender,
idCardFront: idCardFront ?? this.idCardFront,
idCardBack: idCardBack ?? this.idCardBack,
certificates: certificates ?? this.certificates,
membershipStatus: membershipStatus ?? this.membershipStatus,
membershipStatusColor: membershipStatusColor ?? this.membershipStatusColor,
isVerified: isVerified ?? this.isVerified,
credentialDisplay: credentialDisplay ?? this.credentialDisplay,
referralCode: referralCode ?? this.referralCode,
erpnextCustomerId: erpnextCustomerId ?? this.erpnextCustomerId,
createdAt: createdAt ?? this.createdAt,

View File

@@ -32,4 +32,27 @@ abstract class UserInfoRepository {
///
/// Returns [UserInfo] entity with fresh user data.
Future<UserInfo> refreshUserInfo();
/// Update user information
///
/// Updates the authenticated user's profile information.
///
/// [data] should contain:
/// - full_name: String
/// - date_of_birth: String (YYYY-MM-DD format)
/// - gender: String
/// - company_name: String?
/// - tax_code: String?
/// - avatar_base64: String? (base64 encoded image)
/// - id_card_front_base64: String? (base64 encoded image)
/// - id_card_back_base64: String? (base64 encoded image)
/// - certificates_base64: List<String> (array of base64 encoded images)
///
/// Returns updated [UserInfo] entity after successful update.
///
/// Throws:
/// - [UnauthorizedException] if session expired
/// - [NetworkException] if network error occurs
/// - [ServerException] if server error occurs
Future<UserInfo> updateUserInfo(Map<String, dynamic> data);
}

File diff suppressed because it is too large Load Diff

View File

@@ -103,6 +103,36 @@ class UserInfo extends _$UserInfo {
});
}
/// Update user information
///
/// Sends updated user data to the API and refreshes the state.
///
/// [data] should contain:
/// - full_name: String
/// - date_of_birth: String (YYYY-MM-DD)
/// - gender: String
/// - company_name: String?
/// - tax_code: String?
/// - avatar_base64: String? (base64 encoded)
/// - id_card_front_base64: String? (base64 encoded)
/// - id_card_back_base64: String? (base64 encoded)
/// - certificates_base64: List<String> (base64 encoded)
///
/// Usage:
/// ```dart
/// await ref.read(userInfoProvider.notifier).updateUserInfo(updateData);
/// ```
Future<void> updateUserInfo(Map<String, dynamic> data) async {
// Set loading state
state = const AsyncValue.loading();
// Update via repository and fetch fresh data
state = await AsyncValue.guard(() async {
final repository = await ref.read(userInfoRepositoryProvider.future);
return await repository.updateUserInfo(data);
});
}
/// Update user info locally
///
/// Updates the cached state without fetching from API.

View File

@@ -232,7 +232,7 @@ final class UserInfoProvider
UserInfo create() => UserInfo();
}
String _$userInfoHash() => r'74fe20082e7acbb23f9606bd01fdf43fd4c5a893';
String _$userInfoHash() => r'ed28fdf0213dfd616592b9735cd291f147867047';
/// User Info Provider
///