1111
This commit is contained in:
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
@@ -4,11 +4,15 @@ PODS:
|
|||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_native_splash (2.4.3):
|
- flutter_native_splash (2.4.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- shared_preferences_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
|
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
camera_avfoundation:
|
camera_avfoundation:
|
||||||
@@ -17,11 +21,14 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
|
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
|
|
||||||
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
|
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,6 @@
|
|||||||
C9C24CB30CC2EF64A5028A78 /* Pods-RunnerTests.release.xcconfig */,
|
C9C24CB30CC2EF64A5028A78 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
C8EAEACEAC43BB801D9EFF4B /* Pods-RunnerTests.profile.xcconfig */,
|
C8EAEACEAC43BB801D9EFF4B /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -471,6 +470,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 6YV3J6426X;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -653,6 +653,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 6YV3J6426X;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -675,6 +676,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 6YV3J6426X;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|||||||
113
lib/LOGIN_SETUP.md
Normal file
113
lib/LOGIN_SETUP.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# إعداد تسجيل الدخول (Login Setup)
|
||||||
|
|
||||||
|
تم ربط عملية تسجيل الدخول مع السيرفر بنجاح.
|
||||||
|
|
||||||
|
## الملفات المُنشأة
|
||||||
|
|
||||||
|
### 1. Data Layer
|
||||||
|
- `data/dto/login_dto.dart` - DTO لإرسال بيانات تسجيل الدخول
|
||||||
|
- `data/dto/login_response_dto.dart` - DTO لاستقبال استجابة تسجيل الدخول
|
||||||
|
- `data/datasources/auth_remote_data_source.dart` - مصدر البيانات البعيدة لتسجيل الدخول
|
||||||
|
|
||||||
|
### 2. Domain Layer
|
||||||
|
- `domain/models/login_request.dart` - نموذج طلب تسجيل الدخول
|
||||||
|
- `domain/models/login_response_model.dart` - نموذج استجابة تسجيل الدخول
|
||||||
|
- `domain/repositories/auth_repository.dart` - واجهة Repository
|
||||||
|
- `domain/usecases/login_usecase.dart` - Use Case لتسجيل الدخول
|
||||||
|
|
||||||
|
### 3. Data Implementation
|
||||||
|
- `data/repositories/auth_repository_impl.dart` - تطبيق Repository
|
||||||
|
|
||||||
|
### 4. Core Updates
|
||||||
|
- `core/network/api_client.dart` - تم تحديثه لإضافة token تلقائياً في الطلبات
|
||||||
|
- `core/di/injection_container.dart` - تم تسجيل جميع التبعيات
|
||||||
|
|
||||||
|
### 5. UI Updates
|
||||||
|
- `widgets/auth_form.dart` - تم تحديثه لاستخدام LoginUseCase
|
||||||
|
|
||||||
|
## كيفية الاستخدام
|
||||||
|
|
||||||
|
### في الكود:
|
||||||
|
```dart
|
||||||
|
// الحصول على LoginUseCase من dependency injection
|
||||||
|
final loginUseCase = sl<LoginUseCase>();
|
||||||
|
|
||||||
|
// إنشاء طلب تسجيل الدخول
|
||||||
|
final request = LoginRequest(
|
||||||
|
phoneNumber: '7856121557',
|
||||||
|
password: 'qaqaqa',
|
||||||
|
);
|
||||||
|
|
||||||
|
// استدعاء UseCase
|
||||||
|
final result = await loginUseCase(request);
|
||||||
|
|
||||||
|
// التعامل مع النتيجة
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
// معالجة الخطأ
|
||||||
|
print('خطأ: ${failure.message}');
|
||||||
|
},
|
||||||
|
(response) {
|
||||||
|
// معالجة النجاح
|
||||||
|
if (response.isSuccess) {
|
||||||
|
print('تم تسجيل الدخول بنجاح');
|
||||||
|
print('Token: ${response.data?.token}');
|
||||||
|
print('اسم المستخدم: ${response.data?.fullName}');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoint
|
||||||
|
|
||||||
|
- **URL**: `https://hrm.go.iq/api/Auth/login`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Headers**:
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- `accept: text/plain`
|
||||||
|
|
||||||
|
### Request Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"phoneNumber": "7856121557",
|
||||||
|
"password": "qaqaqa"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 200,
|
||||||
|
"isSuccess": true,
|
||||||
|
"message": "Login Successful",
|
||||||
|
"data": {
|
||||||
|
"token": "...",
|
||||||
|
"id": "...",
|
||||||
|
"username": "...",
|
||||||
|
"fullName": "...",
|
||||||
|
"role": "...",
|
||||||
|
"email": "...",
|
||||||
|
"phoneNumber": "...",
|
||||||
|
"permissions": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## المميزات
|
||||||
|
|
||||||
|
1. ✅ حفظ Token تلقائياً في SharedPreferences
|
||||||
|
2. ✅ إضافة Token تلقائياً في جميع الطلبات عبر ApiClient interceptor
|
||||||
|
3. ✅ معالجة الأخطاء بشكل شامل (Network, Server, Validation)
|
||||||
|
4. ✅ رسائل خطأ بالعربية
|
||||||
|
5. ✅ Loading state في واجهة المستخدم
|
||||||
|
6. ✅ التحقق من صحة المدخلات
|
||||||
|
|
||||||
|
## الخطوات التالية
|
||||||
|
|
||||||
|
1. قم بتشغيل `flutter pub get` لتثبيت الحزم
|
||||||
|
2. اختبر تسجيل الدخول باستخدام البيانات الصحيحة
|
||||||
|
3. يمكنك إضافة المزيد من الميزات مثل:
|
||||||
|
- حفظ بيانات المستخدم الكاملة
|
||||||
|
- تذكر المستخدم (Remember Me)
|
||||||
|
- تسجيل الخروج (Logout)
|
||||||
|
- تحديث Token تلقائياً عند انتهاء الصلاحية
|
||||||
@@ -2,6 +2,11 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../network/api_client.dart';
|
import '../network/api_client.dart';
|
||||||
|
import '../../data/datasources/auth_remote_data_source.dart';
|
||||||
|
import '../../data/datasources/user_local_data_source.dart';
|
||||||
|
import '../../data/repositories/auth_repository_impl.dart';
|
||||||
|
import '../../domain/repositories/auth_repository.dart';
|
||||||
|
import '../../domain/usecases/login_usecase.dart';
|
||||||
|
|
||||||
final sl = GetIt.instance;
|
final sl = GetIt.instance;
|
||||||
|
|
||||||
@@ -14,26 +19,29 @@ Future<void> initializeDependencies() async {
|
|||||||
sl.registerLazySingleton<SharedPreferences>(() => sharedPreferences);
|
sl.registerLazySingleton<SharedPreferences>(() => sharedPreferences);
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
sl.registerLazySingleton<ApiClient>(() => ApiClient(dio: sl()));
|
sl.registerLazySingleton<ApiClient>(
|
||||||
|
() => ApiClient(dio: sl(), sharedPreferences: sl()),
|
||||||
|
);
|
||||||
|
|
||||||
// Data sources will be registered here
|
// Data sources
|
||||||
// Example:
|
sl.registerLazySingleton<AuthRemoteDataSource>(
|
||||||
// sl.registerLazySingleton<AuthRemoteDataSource>(
|
() => AuthRemoteDataSourceImpl(apiClient: sl()),
|
||||||
// () => AuthRemoteDataSourceImpl(apiClient: sl()),
|
);
|
||||||
// );
|
|
||||||
|
|
||||||
// Repositories will be registered here
|
sl.registerLazySingleton<UserLocalDataSource>(
|
||||||
// Example:
|
() => UserLocalDataSourceImpl(sharedPreferences: sl()),
|
||||||
// sl.registerLazySingleton<AuthRepository>(
|
);
|
||||||
// () => AuthRepositoryImpl(
|
|
||||||
// remoteDataSource: sl(),
|
|
||||||
// localDataSource: sl(),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Use cases will be registered here
|
// Repositories
|
||||||
// Example:
|
sl.registerLazySingleton<AuthRepository>(
|
||||||
// sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
|
() => AuthRepositoryImpl(
|
||||||
|
remoteDataSource: sl(),
|
||||||
|
localDataSource: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use cases
|
||||||
|
sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
|
||||||
|
|
||||||
// Blocs will be registered here
|
// Blocs will be registered here
|
||||||
// Example:
|
// Example:
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
final Dio dio;
|
final Dio dio;
|
||||||
static const String baseUrl = 'YOUR_API_BASE_URL_HERE';
|
final SharedPreferences? sharedPreferences;
|
||||||
|
static const String baseUrl = 'https://hrm.go.iq/api';
|
||||||
|
static const String _tokenKey = 'user_token';
|
||||||
|
|
||||||
ApiClient({required this.dio}) {
|
ApiClient({required this.dio, this.sharedPreferences}) {
|
||||||
dio.options = BaseOptions(
|
dio.options = BaseOptions(
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
connectTimeout: const Duration(seconds: 30),
|
connectTimeout: const Duration(seconds: 30),
|
||||||
@@ -15,6 +18,20 @@ class ApiClient {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add interceptor to add token to requests
|
||||||
|
dio.interceptors.add(
|
||||||
|
InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) async {
|
||||||
|
// Get token from SharedPreferences
|
||||||
|
final token = sharedPreferences?.getString(_tokenKey);
|
||||||
|
if (token != null && token.isNotEmpty) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
return handler.next(options);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Add interceptors for logging and error handling
|
// Add interceptors for logging and error handling
|
||||||
dio.interceptors.add(
|
dio.interceptors.add(
|
||||||
LogInterceptor(
|
LogInterceptor(
|
||||||
|
|||||||
77
lib/data/datasources/auth_remote_data_source.dart
Normal file
77
lib/data/datasources/auth_remote_data_source.dart
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../../core/error/exceptions.dart';
|
||||||
|
import '../../core/network/api_client.dart';
|
||||||
|
import '../dto/login_dto.dart';
|
||||||
|
import '../dto/login_response_dto.dart';
|
||||||
|
|
||||||
|
abstract class AuthRemoteDataSource {
|
||||||
|
Future<LoginResponseDto> login(LoginDto dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
AuthRemoteDataSourceImpl({required this.apiClient});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LoginResponseDto> login(LoginDto dto) async {
|
||||||
|
try {
|
||||||
|
final response = await apiClient.post(
|
||||||
|
'/Auth/login',
|
||||||
|
data: dto.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
final responseData = response.data;
|
||||||
|
|
||||||
|
if (responseData is Map<String, dynamic>) {
|
||||||
|
return LoginResponseDto.fromJson(responseData);
|
||||||
|
} else {
|
||||||
|
throw ServerException(
|
||||||
|
message: 'استجابة غير صحيحة من الخادم',
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ServerException(
|
||||||
|
message: 'فشل تسجيل الدخول',
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.receiveTimeout) {
|
||||||
|
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||||
|
} else if (e.type == DioExceptionType.connectionError) {
|
||||||
|
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||||
|
} else if (e.response?.statusCode == 500) {
|
||||||
|
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||||
|
} else if (e.response != null) {
|
||||||
|
final message = e.response?.data?['message'] ??
|
||||||
|
e.response?.data?['error'] ??
|
||||||
|
'فشل تسجيل الدخول';
|
||||||
|
|
||||||
|
// Check for invalid credentials
|
||||||
|
final customMessage =
|
||||||
|
message.toString().toLowerCase().contains('invalid') ||
|
||||||
|
message.toString().toLowerCase().contains('incorrect')
|
||||||
|
? 'رقم الهاتف أو كلمة المرور غير صحيحة'
|
||||||
|
: message.toString().toLowerCase().contains('not found')
|
||||||
|
? 'المستخدم غير موجود'
|
||||||
|
: message;
|
||||||
|
|
||||||
|
throw ServerException(
|
||||||
|
message: customMessage,
|
||||||
|
statusCode: e.response?.statusCode,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e is ServerException || e is NetworkException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
throw ServerException(message: 'خطأ غير متوقع');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
lib/data/dto/login_dto.dart
Normal file
16
lib/data/dto/login_dto.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
class LoginDto {
|
||||||
|
final String phoneNumber;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
LoginDto({
|
||||||
|
required this.phoneNumber,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'phoneNumber': phoneNumber,
|
||||||
|
'password': password,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
85
lib/data/dto/login_response_dto.dart
Normal file
85
lib/data/dto/login_response_dto.dart
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
class LoginResponseDto {
|
||||||
|
final int statusCode;
|
||||||
|
final bool isSuccess;
|
||||||
|
final String message;
|
||||||
|
final LoginDataDto? data;
|
||||||
|
|
||||||
|
LoginResponseDto({
|
||||||
|
required this.statusCode,
|
||||||
|
required this.isSuccess,
|
||||||
|
required this.message,
|
||||||
|
this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginResponseDto.fromJson(Map<String, dynamic> json) {
|
||||||
|
return LoginResponseDto(
|
||||||
|
statusCode: json['statusCode'] ?? 0,
|
||||||
|
isSuccess: json['isSuccess'] ?? false,
|
||||||
|
message: json['message'] ?? '',
|
||||||
|
data: json['data'] != null ? LoginDataDto.fromJson(json['data']) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'statusCode': statusCode,
|
||||||
|
'isSuccess': isSuccess,
|
||||||
|
'message': message,
|
||||||
|
'data': data?.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginDataDto {
|
||||||
|
final String? token;
|
||||||
|
final String? id;
|
||||||
|
final String? employeeId;
|
||||||
|
final String? username;
|
||||||
|
final String? fullName;
|
||||||
|
final String? role;
|
||||||
|
final String? email;
|
||||||
|
final String? phoneNumber;
|
||||||
|
final List<String>? permissions;
|
||||||
|
|
||||||
|
LoginDataDto({
|
||||||
|
this.token,
|
||||||
|
this.id,
|
||||||
|
this.employeeId,
|
||||||
|
this.username,
|
||||||
|
this.fullName,
|
||||||
|
this.role,
|
||||||
|
this.email,
|
||||||
|
this.phoneNumber,
|
||||||
|
this.permissions,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginDataDto.fromJson(Map<String, dynamic> json) {
|
||||||
|
return LoginDataDto(
|
||||||
|
token: json['token'],
|
||||||
|
id: json['id'],
|
||||||
|
employeeId: json['employeeId'],
|
||||||
|
username: json['username'],
|
||||||
|
fullName: json['fullName'],
|
||||||
|
role: json['role'],
|
||||||
|
email: json['email'],
|
||||||
|
phoneNumber: json['phoneNumber'],
|
||||||
|
permissions: json['permissions'] != null
|
||||||
|
? List<String>.from(json['permissions'])
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'token': token,
|
||||||
|
'id': id,
|
||||||
|
'employeeId': employeeId,
|
||||||
|
'username': username,
|
||||||
|
'fullName': fullName,
|
||||||
|
'role': role,
|
||||||
|
'email': email,
|
||||||
|
'phoneNumber': phoneNumber,
|
||||||
|
'permissions': permissions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
65
lib/data/repositories/auth_repository_impl.dart
Normal file
65
lib/data/repositories/auth_repository_impl.dart
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../core/error/exceptions.dart';
|
||||||
|
import '../../core/error/failures.dart';
|
||||||
|
import '../datasources/auth_remote_data_source.dart';
|
||||||
|
import '../datasources/user_local_data_source.dart';
|
||||||
|
import '../dto/login_dto.dart';
|
||||||
|
import '../dto/login_response_dto.dart';
|
||||||
|
import '../../domain/models/login_request.dart';
|
||||||
|
import '../../domain/models/login_response_model.dart';
|
||||||
|
import '../../domain/repositories/auth_repository.dart';
|
||||||
|
|
||||||
|
class AuthRepositoryImpl implements AuthRepository {
|
||||||
|
final AuthRemoteDataSource remoteDataSource;
|
||||||
|
final UserLocalDataSource localDataSource;
|
||||||
|
|
||||||
|
AuthRepositoryImpl({
|
||||||
|
required this.remoteDataSource,
|
||||||
|
required this.localDataSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, LoginResponseModel>> login(LoginRequest request) async {
|
||||||
|
try {
|
||||||
|
final dto = LoginDto(
|
||||||
|
phoneNumber: request.phoneNumber,
|
||||||
|
password: request.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
final responseDto = await remoteDataSource.login(dto);
|
||||||
|
|
||||||
|
// Cache the token locally
|
||||||
|
if (responseDto.data?.token != null) {
|
||||||
|
await localDataSource.cacheUserToken(responseDto.data!.token!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DTO to Model
|
||||||
|
final responseModel = LoginResponseModel(
|
||||||
|
statusCode: responseDto.statusCode,
|
||||||
|
isSuccess: responseDto.isSuccess,
|
||||||
|
message: responseDto.message,
|
||||||
|
data: responseDto.data != null
|
||||||
|
? LoginDataModel(
|
||||||
|
token: responseDto.data!.token,
|
||||||
|
id: responseDto.data!.id,
|
||||||
|
employeeId: responseDto.data!.employeeId,
|
||||||
|
username: responseDto.data!.username,
|
||||||
|
fullName: responseDto.data!.fullName,
|
||||||
|
role: responseDto.data!.role,
|
||||||
|
email: responseDto.data!.email,
|
||||||
|
phoneNumber: responseDto.data!.phoneNumber,
|
||||||
|
permissions: responseDto.data!.permissions,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(responseModel);
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
return Left(NetworkFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/domain/models/login_request.dart
Normal file
9
lib/domain/models/login_request.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class LoginRequest {
|
||||||
|
final String phoneNumber;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
LoginRequest({
|
||||||
|
required this.phoneNumber,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
}
|
||||||
37
lib/domain/models/login_response_model.dart
Normal file
37
lib/domain/models/login_response_model.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
class LoginResponseModel {
|
||||||
|
final int statusCode;
|
||||||
|
final bool isSuccess;
|
||||||
|
final String message;
|
||||||
|
final LoginDataModel? data;
|
||||||
|
|
||||||
|
LoginResponseModel({
|
||||||
|
required this.statusCode,
|
||||||
|
required this.isSuccess,
|
||||||
|
required this.message,
|
||||||
|
this.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginDataModel {
|
||||||
|
final String? token;
|
||||||
|
final String? id;
|
||||||
|
final String? employeeId;
|
||||||
|
final String? username;
|
||||||
|
final String? fullName;
|
||||||
|
final String? role;
|
||||||
|
final String? email;
|
||||||
|
final String? phoneNumber;
|
||||||
|
final List<String>? permissions;
|
||||||
|
|
||||||
|
LoginDataModel({
|
||||||
|
this.token,
|
||||||
|
this.id,
|
||||||
|
this.employeeId,
|
||||||
|
this.username,
|
||||||
|
this.fullName,
|
||||||
|
this.role,
|
||||||
|
this.email,
|
||||||
|
this.phoneNumber,
|
||||||
|
this.permissions,
|
||||||
|
});
|
||||||
|
}
|
||||||
8
lib/domain/repositories/auth_repository.dart
Normal file
8
lib/domain/repositories/auth_repository.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../core/error/failures.dart';
|
||||||
|
import '../models/login_request.dart';
|
||||||
|
import '../models/login_response_model.dart';
|
||||||
|
|
||||||
|
abstract class AuthRepository {
|
||||||
|
Future<Either<Failure, LoginResponseModel>> login(LoginRequest request);
|
||||||
|
}
|
||||||
15
lib/domain/usecases/login_usecase.dart
Normal file
15
lib/domain/usecases/login_usecase.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../core/error/failures.dart';
|
||||||
|
import '../models/login_request.dart';
|
||||||
|
import '../models/login_response_model.dart';
|
||||||
|
import '../repositories/auth_repository.dart';
|
||||||
|
|
||||||
|
class LoginUseCase {
|
||||||
|
final AuthRepository repository;
|
||||||
|
|
||||||
|
LoginUseCase({required this.repository});
|
||||||
|
|
||||||
|
Future<Either<Failure, LoginResponseModel>> call(LoginRequest request) {
|
||||||
|
return repository.login(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../screens/main_screen.dart';
|
import '../screens/main_screen.dart';
|
||||||
|
import '../core/di/injection_container.dart';
|
||||||
|
import '../domain/usecases/login_usecase.dart';
|
||||||
|
import '../domain/models/login_request.dart';
|
||||||
import 'onboarding_button.dart';
|
import 'onboarding_button.dart';
|
||||||
|
|
||||||
class AuthForm extends StatefulWidget {
|
class AuthForm extends StatefulWidget {
|
||||||
@@ -13,25 +16,86 @@ class AuthForm extends StatefulWidget {
|
|||||||
|
|
||||||
class _AuthFormState extends State<AuthForm> {
|
class _AuthFormState extends State<AuthForm> {
|
||||||
bool _obscure = true;
|
bool _obscure = true;
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
// Text controllers
|
||||||
|
final TextEditingController _phoneNumberController = TextEditingController();
|
||||||
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
|
|
||||||
// Focus nodes for text fields
|
// Focus nodes for text fields
|
||||||
late FocusNode _usernameFocusNode;
|
late FocusNode _phoneNumberFocusNode;
|
||||||
late FocusNode _passwordFocusNode;
|
late FocusNode _passwordFocusNode;
|
||||||
|
|
||||||
void _handleLogin() {
|
// Get LoginUseCase from dependency injection
|
||||||
// Unfocus any focused text field
|
final LoginUseCase _loginUseCase = sl<LoginUseCase>();
|
||||||
_usernameFocusNode.unfocus();
|
|
||||||
_passwordFocusNode.unfocus();
|
|
||||||
|
|
||||||
// Call the onSubmit callback if provided (for any other logic you might have)
|
Future<void> _handleLogin() async {
|
||||||
if (widget.onSubmit != null) {
|
// Validate inputs
|
||||||
widget.onSubmit!();
|
if (_phoneNumberController.text.trim().isEmpty) {
|
||||||
|
_showError('الرجاء إدخال رقم الهاتف');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to the AttendancePage
|
if (_passwordController.text.trim().isEmpty) {
|
||||||
Navigator.pushReplacement(
|
_showError('الرجاء إدخال كلمة المرور');
|
||||||
context,
|
return;
|
||||||
MaterialPageRoute(builder: (context) => const MainPage()),
|
}
|
||||||
|
|
||||||
|
// Unfocus any focused text field
|
||||||
|
_phoneNumberFocusNode.unfocus();
|
||||||
|
_passwordFocusNode.unfocus();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final request = LoginRequest(
|
||||||
|
phoneNumber: _phoneNumberController.text.trim(),
|
||||||
|
password: _passwordController.text.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await _loginUseCase(request);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
_showError(failure.message);
|
||||||
|
},
|
||||||
|
(response) {
|
||||||
|
if (response.isSuccess) {
|
||||||
|
// Call the onSubmit callback if provided
|
||||||
|
if (widget.onSubmit != null) {
|
||||||
|
widget.onSubmit!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to the MainPage
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => const MainPage()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_showError(response.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
_showError('حدث خطأ غير متوقع: $e');
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(String message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,14 +103,16 @@ class _AuthFormState extends State<AuthForm> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Initialize focus nodes
|
// Initialize focus nodes
|
||||||
_usernameFocusNode = FocusNode();
|
_phoneNumberFocusNode = FocusNode();
|
||||||
_passwordFocusNode = FocusNode();
|
_passwordFocusNode = FocusNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Clean up focus nodes when widget is disposed
|
// Clean up controllers and focus nodes
|
||||||
_usernameFocusNode.dispose();
|
_phoneNumberController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_phoneNumberFocusNode.dispose();
|
||||||
_passwordFocusNode.dispose();
|
_passwordFocusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -130,11 +196,11 @@ class _AuthFormState extends State<AuthForm> {
|
|||||||
|
|
||||||
SizedBox(height: verticalSpacing),
|
SizedBox(height: verticalSpacing),
|
||||||
|
|
||||||
/// Username Label
|
/// Phone Number Label
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Text(
|
child: Text(
|
||||||
"اسم المستخدم",
|
"رقم الهاتف",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: labelFontSize,
|
fontSize: labelFontSize,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
@@ -144,9 +210,11 @@ class _AuthFormState extends State<AuthForm> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
_buildField(
|
_buildField(
|
||||||
hint: "اسم المستخدم",
|
controller: _phoneNumberController,
|
||||||
|
hint: "رقم الهاتف",
|
||||||
obscure: false,
|
obscure: false,
|
||||||
focusNode: _usernameFocusNode,
|
keyboardType: TextInputType.phone,
|
||||||
|
focusNode: _phoneNumberFocusNode,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
// Move focus to password field when next is pressed
|
// Move focus to password field when next is pressed
|
||||||
@@ -171,14 +239,13 @@ class _AuthFormState extends State<AuthForm> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
_buildField(
|
_buildField(
|
||||||
|
controller: _passwordController,
|
||||||
hint: "كلمة المرور",
|
hint: "كلمة المرور",
|
||||||
obscure: _obscure,
|
obscure: _obscure,
|
||||||
hasEye: true,
|
hasEye: true,
|
||||||
focusNode: _passwordFocusNode,
|
focusNode: _passwordFocusNode,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
onSubmitted:
|
onSubmitted: (_) => _handleLogin(),
|
||||||
(_) =>
|
|
||||||
_handleLogin(), // Added parentheses to call the method
|
|
||||||
fontSize: fieldFontSize,
|
fontSize: fieldFontSize,
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -186,9 +253,9 @@ class _AuthFormState extends State<AuthForm> {
|
|||||||
|
|
||||||
Center(
|
Center(
|
||||||
child: OnboardingButton(
|
child: OnboardingButton(
|
||||||
text: "تسجيل دخول",
|
text: _isLoading ? "جاري تسجيل الدخول..." : "تسجيل دخول",
|
||||||
backgroundColor: const Color.fromARGB(239, 35, 87, 74),
|
backgroundColor: const Color.fromARGB(239, 35, 87, 74),
|
||||||
onPressed: _handleLogin,
|
onPressed: _isLoading ? null : _handleLogin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -203,9 +270,11 @@ class _AuthFormState extends State<AuthForm> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildField({
|
Widget _buildField({
|
||||||
|
TextEditingController? controller,
|
||||||
required String hint,
|
required String hint,
|
||||||
required bool obscure,
|
required bool obscure,
|
||||||
bool hasEye = false,
|
bool hasEye = false,
|
||||||
|
TextInputType? keyboardType,
|
||||||
FocusNode? focusNode,
|
FocusNode? focusNode,
|
||||||
TextInputAction? textInputAction,
|
TextInputAction? textInputAction,
|
||||||
Function(String)? onSubmitted,
|
Function(String)? onSubmitted,
|
||||||
@@ -220,8 +289,10 @@ class _AuthFormState extends State<AuthForm> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
obscureText: obscure,
|
obscureText: obscure,
|
||||||
|
keyboardType: keyboardType,
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
onSubmitted: onSubmitted,
|
onSubmitted: onSubmitted,
|
||||||
|
|||||||
Reference in New Issue
Block a user