update info
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
/// User Info Remote Data Source
|
||||
///
|
||||
/// Handles API calls for fetching user information.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/account/data/models/user_info_model.dart';
|
||||
|
||||
/// User Info Remote Data Source
|
||||
///
|
||||
/// Provides methods for:
|
||||
/// - Fetching current user information from API
|
||||
/// - Uses existing Frappe authentication (cookies/tokens)
|
||||
class UserInfoRemoteDataSource {
|
||||
UserInfoRemoteDataSource(this._dioClient);
|
||||
|
||||
final DioClient _dioClient;
|
||||
|
||||
/// Get User Info
|
||||
///
|
||||
/// Fetches the current authenticated user's information.
|
||||
/// Uses existing Frappe session cookies/tokens for authentication.
|
||||
///
|
||||
/// API: POST https://land.dbiz.com/api/method/building_material.building_material.api.user.get_user_info
|
||||
/// Request: Empty POST (no body required)
|
||||
///
|
||||
/// Response structure:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "message": {
|
||||
/// "user_id": "...",
|
||||
/// "full_name": "...",
|
||||
/// "email": "...",
|
||||
/// "phone_number": "...",
|
||||
/// "role": "customer",
|
||||
/// "status": "active",
|
||||
/// "loyalty_tier": "gold",
|
||||
/// "total_points": 1000,
|
||||
/// "available_points": 800,
|
||||
/// "expiring_points": 200,
|
||||
/// // ... other fields
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Throws:
|
||||
/// - [UnauthorizedException] if user not authenticated (401)
|
||||
/// - [NotFoundException] if endpoint not found (404)
|
||||
/// - [ServerException] if server error occurs (500+)
|
||||
/// - [NetworkException] for other network errors
|
||||
Future<UserInfoModel> getUserInfo() async {
|
||||
try {
|
||||
debugPrint('🔵 [UserInfoDataSource] Fetching user info...');
|
||||
final startTime = DateTime.now();
|
||||
|
||||
// Make POST request with empty body
|
||||
// Authentication is handled by auth interceptor (uses existing session)
|
||||
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||
'/api/method/building_material.building_material.api.user.get_user_info',
|
||||
data: const <String, dynamic>{}, // Empty body as per API spec
|
||||
);
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
debugPrint('🟢 [UserInfoDataSource] Response received in ${duration.inMilliseconds}ms');
|
||||
debugPrint('🟢 [UserInfoDataSource] Status: ${response.statusCode}');
|
||||
debugPrint('🟢 [UserInfoDataSource] Data: ${response.data}');
|
||||
|
||||
// Check response status and data
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
// Parse response to model
|
||||
final model = UserInfoModel.fromJson(response.data!);
|
||||
debugPrint('✅ [UserInfoDataSource] Successfully parsed user: ${model.fullName}');
|
||||
return model;
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Failed to get 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('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 get 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');
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh User Info
|
||||
///
|
||||
/// Same as getUserInfo but with force refresh parameter.
|
||||
/// Useful when you want to bypass cache and get fresh data.
|
||||
Future<UserInfoModel> refreshUserInfo() async {
|
||||
// For now, same as getUserInfo
|
||||
// Could add cache-busting headers in the future if needed
|
||||
return getUserInfo();
|
||||
}
|
||||
}
|
||||
280
lib/features/account/data/models/user_info_model.dart
Normal file
280
lib/features/account/data/models/user_info_model.dart
Normal file
@@ -0,0 +1,280 @@
|
||||
/// User Info Model
|
||||
///
|
||||
/// Data model for user information from API.
|
||||
/// Handles JSON serialization and conversion to domain entity.
|
||||
library;
|
||||
|
||||
import 'package:worker/core/database/models/enums.dart';
|
||||
import 'package:worker/features/account/domain/entities/user_info.dart';
|
||||
|
||||
/// User Info Model
|
||||
///
|
||||
/// Maps API response from get_user_info endpoint to UserInfo entity.
|
||||
class UserInfoModel {
|
||||
const UserInfoModel({
|
||||
required this.userId,
|
||||
required this.fullName,
|
||||
this.email,
|
||||
this.phoneNumber,
|
||||
required this.role,
|
||||
required this.status,
|
||||
required this.loyaltyTier,
|
||||
this.totalPoints = 0,
|
||||
this.availablePoints = 0,
|
||||
this.expiringPoints = 0,
|
||||
this.avatarUrl,
|
||||
this.companyName,
|
||||
this.taxId,
|
||||
this.address,
|
||||
this.cccd,
|
||||
this.referralCode,
|
||||
this.erpnextCustomerId,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
final String fullName;
|
||||
final String? email;
|
||||
final String? phoneNumber;
|
||||
final UserRole role;
|
||||
final UserStatus status;
|
||||
final LoyaltyTier loyaltyTier;
|
||||
final int totalPoints;
|
||||
final int availablePoints;
|
||||
final int expiringPoints;
|
||||
final String? avatarUrl;
|
||||
final String? companyName;
|
||||
final String? taxId;
|
||||
final String? address;
|
||||
final String? cccd;
|
||||
final String? referralCode;
|
||||
final String? erpnextCustomerId;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
// =========================================================================
|
||||
// JSON SERIALIZATION
|
||||
// =========================================================================
|
||||
|
||||
/// Create UserInfoModel from API JSON response
|
||||
///
|
||||
/// Expected API response structure:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "message": {
|
||||
/// "user_id": "...",
|
||||
/// "full_name": "...",
|
||||
/// "email": "...",
|
||||
/// "phone_number": "...",
|
||||
/// "role": "customer",
|
||||
/// "status": "active",
|
||||
/// "loyalty_tier": "gold",
|
||||
/// "total_points": 1000,
|
||||
/// "available_points": 800,
|
||||
/// "expiring_points": 200,
|
||||
/// "avatar_url": "...",
|
||||
/// "company_name": "...",
|
||||
/// "tax_id": "...",
|
||||
/// "address": "...",
|
||||
/// "cccd": "...",
|
||||
/// "referral_code": "...",
|
||||
/// "erpnext_customer_id": "...",
|
||||
/// "created_at": "...",
|
||||
/// "updated_at": "...",
|
||||
/// "last_login_at": "..."
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
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;
|
||||
|
||||
return UserInfoModel(
|
||||
// Use email as userId since API doesn't provide user_id
|
||||
userId: data['email'] as String? ?? data['phone'] as String? ?? 'unknown',
|
||||
fullName: data['full_name'] as String? ?? '',
|
||||
email: data['email'] as String?,
|
||||
phoneNumber: data['phone'] as String?,
|
||||
// Default values for fields not in this API
|
||||
role: UserRole.customer, // Default to customer
|
||||
status: UserStatus.active, // Default to active
|
||||
loyaltyTier: LoyaltyTier.bronze, // Default to bronze
|
||||
totalPoints: 0,
|
||||
availablePoints: 0,
|
||||
expiringPoints: 0,
|
||||
avatarUrl: data['avatar'] as String?,
|
||||
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
|
||||
referralCode: null,
|
||||
erpnextCustomerId: null,
|
||||
createdAt: _parseDateTime(data['date_of_birth'] as String?),
|
||||
updatedAt: _parseDateTime(data['date_of_birth'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert UserInfoModel to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'user_id': userId,
|
||||
'full_name': fullName,
|
||||
'email': email,
|
||||
'phone_number': phoneNumber,
|
||||
'role': role.name,
|
||||
'status': status.name,
|
||||
'loyalty_tier': loyaltyTier.name,
|
||||
'total_points': totalPoints,
|
||||
'available_points': availablePoints,
|
||||
'expiring_points': expiringPoints,
|
||||
'avatar_url': avatarUrl,
|
||||
'company_name': companyName,
|
||||
'tax_id': taxId,
|
||||
'address': address,
|
||||
'cccd': cccd,
|
||||
'referral_code': referralCode,
|
||||
'erpnext_customer_id': erpnextCustomerId,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DOMAIN CONVERSION
|
||||
// =========================================================================
|
||||
|
||||
/// Convert to domain entity
|
||||
UserInfo toEntity() {
|
||||
final now = DateTime.now();
|
||||
return UserInfo(
|
||||
userId: userId,
|
||||
fullName: fullName,
|
||||
email: email,
|
||||
phoneNumber: phoneNumber,
|
||||
role: role,
|
||||
status: status,
|
||||
loyaltyTier: loyaltyTier,
|
||||
totalPoints: totalPoints,
|
||||
availablePoints: availablePoints,
|
||||
expiringPoints: expiringPoints,
|
||||
avatarUrl: avatarUrl,
|
||||
companyName: companyName,
|
||||
taxId: taxId,
|
||||
address: address,
|
||||
cccd: cccd,
|
||||
referralCode: referralCode,
|
||||
erpnextCustomerId: erpnextCustomerId,
|
||||
createdAt: createdAt ?? now,
|
||||
updatedAt: updatedAt ?? now,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create model from domain entity
|
||||
factory UserInfoModel.fromEntity(UserInfo entity) {
|
||||
return UserInfoModel(
|
||||
userId: entity.userId,
|
||||
fullName: entity.fullName,
|
||||
email: entity.email,
|
||||
phoneNumber: entity.phoneNumber,
|
||||
role: entity.role,
|
||||
status: entity.status,
|
||||
loyaltyTier: entity.loyaltyTier,
|
||||
totalPoints: entity.totalPoints,
|
||||
availablePoints: entity.availablePoints,
|
||||
expiringPoints: entity.expiringPoints,
|
||||
avatarUrl: entity.avatarUrl,
|
||||
companyName: entity.companyName,
|
||||
taxId: entity.taxId,
|
||||
address: entity.address,
|
||||
cccd: entity.cccd,
|
||||
referralCode: entity.referralCode,
|
||||
erpnextCustomerId: entity.erpnextCustomerId,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HELPER METHODS
|
||||
// =========================================================================
|
||||
|
||||
/// Parse user role from string
|
||||
static UserRole _parseUserRole(String? role) {
|
||||
if (role == null) return UserRole.customer;
|
||||
|
||||
return UserRole.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == role.toLowerCase(),
|
||||
orElse: () => UserRole.customer,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse user status from string
|
||||
static UserStatus _parseUserStatus(String? status) {
|
||||
if (status == null) return UserStatus.pending;
|
||||
|
||||
// Handle ERPNext status values
|
||||
final normalizedStatus = status.toLowerCase();
|
||||
if (normalizedStatus == 'enabled' || normalizedStatus == 'active') {
|
||||
return UserStatus.active;
|
||||
}
|
||||
if (normalizedStatus == 'disabled' || normalizedStatus == 'inactive') {
|
||||
return UserStatus.suspended;
|
||||
}
|
||||
|
||||
return UserStatus.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == normalizedStatus,
|
||||
orElse: () => UserStatus.pending,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse loyalty tier from string
|
||||
static LoyaltyTier _parseLoyaltyTier(String? tier) {
|
||||
if (tier == null) return LoyaltyTier.bronze;
|
||||
|
||||
return LoyaltyTier.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == tier.toLowerCase(),
|
||||
orElse: () => LoyaltyTier.bronze,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse integer from dynamic value
|
||||
static int _parseInt(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is double) return value.toInt();
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Parse DateTime from string
|
||||
static DateTime? _parseDateTime(String? dateString) {
|
||||
if (dateString == null || dateString.isEmpty) return null;
|
||||
try {
|
||||
return DateTime.parse(dateString);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// EQUALITY
|
||||
// =========================================================================
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is UserInfoModel && other.userId == userId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => userId.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserInfoModel(userId: $userId, fullName: $fullName, '
|
||||
'tier: $loyaltyTier, points: $totalPoints)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/// User Info Repository Implementation
|
||||
///
|
||||
/// Implements the UserInfoRepository interface with API integration.
|
||||
library;
|
||||
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/account/data/datasources/user_info_remote_datasource.dart';
|
||||
import 'package:worker/features/account/domain/entities/user_info.dart';
|
||||
import 'package:worker/features/account/domain/repositories/user_info_repository.dart';
|
||||
|
||||
/// User Info Repository Implementation
|
||||
///
|
||||
/// Handles user information operations with:
|
||||
/// - API data fetching via remote datasource
|
||||
/// - Error handling and transformation
|
||||
/// - Entity conversion
|
||||
class UserInfoRepositoryImpl implements UserInfoRepository {
|
||||
UserInfoRepositoryImpl({
|
||||
required this.remoteDataSource,
|
||||
});
|
||||
|
||||
final UserInfoRemoteDataSource remoteDataSource;
|
||||
|
||||
// =========================================================================
|
||||
// GET USER INFO
|
||||
// =========================================================================
|
||||
|
||||
@override
|
||||
Future<UserInfo> getUserInfo() async {
|
||||
try {
|
||||
_debugPrint('Fetching user info from API');
|
||||
|
||||
// Fetch from remote datasource
|
||||
final userInfoModel = await remoteDataSource.getUserInfo();
|
||||
|
||||
_debugPrint('Successfully fetched user info: ${userInfoModel.fullName}');
|
||||
|
||||
// Convert model to entity
|
||||
return userInfoModel.toEntity();
|
||||
} on UnauthorizedException catch (e) {
|
||||
_debugPrint('Unauthorized error: $e');
|
||||
rethrow;
|
||||
} on NotFoundException catch (e) {
|
||||
_debugPrint('Not found error: $e');
|
||||
rethrow;
|
||||
} on ServerException catch (e) {
|
||||
_debugPrint('Server error: $e');
|
||||
rethrow;
|
||||
} on NetworkException catch (e) {
|
||||
_debugPrint('Network error: $e');
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
_debugPrint('Unexpected error: $e');
|
||||
throw ServerException('Failed to get user info: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// REFRESH USER INFO
|
||||
// =========================================================================
|
||||
|
||||
@override
|
||||
Future<UserInfo> refreshUserInfo() async {
|
||||
try {
|
||||
_debugPrint('Refreshing user info from API');
|
||||
|
||||
// Fetch fresh data from remote datasource
|
||||
final userInfoModel = await remoteDataSource.refreshUserInfo();
|
||||
|
||||
_debugPrint('Successfully refreshed user info: ${userInfoModel.fullName}');
|
||||
|
||||
// Convert model to entity
|
||||
return userInfoModel.toEntity();
|
||||
} on UnauthorizedException catch (e) {
|
||||
_debugPrint('Unauthorized error on refresh: $e');
|
||||
rethrow;
|
||||
} on NotFoundException catch (e) {
|
||||
_debugPrint('Not found error on refresh: $e');
|
||||
rethrow;
|
||||
} on ServerException catch (e) {
|
||||
_debugPrint('Server error on refresh: $e');
|
||||
rethrow;
|
||||
} on NetworkException catch (e) {
|
||||
_debugPrint('Network error on refresh: $e');
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
_debugPrint('Unexpected error on refresh: $e');
|
||||
throw ServerException('Failed to refresh user info: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DEBUG UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[UserInfoRepository] $message');
|
||||
}
|
||||
Reference in New Issue
Block a user