This commit is contained in:
Daniah Ayad Al-sultani
2026-01-13 14:18:05 +03:00
36 changed files with 1504 additions and 28 deletions

173
lib/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,173 @@
# هيكلية المشروع (Project Architecture)
هذا المشروع يتبع نمط Clean Architecture مع فصل واضح بين الطبقات.
## الهيكلية العامة
```
lib/
├── core/ # المكونات الأساسية المشتركة
│ ├── constants/ # الثوابت (مثل الأبعاد)
│ ├── di/ # Dependency Injection (GetIt)
│ ├── enums/ # التعدادات
│ ├── error/ # معالجة الأخطاء (Exceptions & Failures)
│ ├── network/ # عميل API (ApiClient)
│ └── utils/ # الأدوات المساعدة
├── data/ # طبقة البيانات
│ ├── datasources/ # مصادر البيانات (Remote & Local)
│ ├── dto/ # Data Transfer Objects (للاتصال مع API)
│ └── repositories/ # تطبيقات الـ Repositories
├── domain/ # طبقة الأعمال (Business Logic)
│ ├── models/ # نماذج الأعمال
│ ├── repositories/ # واجهات الـ Repositories
│ └── usecases/ # حالات الاستخدام (Use Cases)
├── presentation/ # طبقة العرض
│ ├── blocs/ # State Management (BLoC)
│ ├── screens/ # الشاشات
│ └── widgets/ # الويدجتات القابلة لإعادة الاستخدام
├── models/ # النماذج القديمة (يمكن نقلها لـ domain/models)
├── screens/ # الشاشات القديمة (يمكن نقلها لـ presentation/screens)
├── services/ # الخدمات القديمة
└── widgets/ # الويدجتات القديمة (يمكن نقلها لـ presentation/widgets)
```
## كيفية الاستخدام
### 1. إضافة API جديد
#### أ) إنشاء DTO في `data/dto/`
```dart
class LoginDto {
final String phoneNumber;
final String password;
LoginDto({required this.phoneNumber, required this.password});
Map<String, dynamic> toJson() => {
'phoneNumber': phoneNumber,
'password': password,
};
}
```
#### ب) إنشاء Remote Data Source في `data/datasources/`
```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());
// Handle response
} on DioException catch (e) {
// Handle errors
}
}
}
```
#### ج) إنشاء Repository Interface في `domain/repositories/`
```dart
abstract class AuthRepository {
Future<Either<Failure, LoginResponseModel>> login(LoginRequest request);
}
```
#### د) إنشاء Repository Implementation في `data/repositories/`
```dart
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
AuthRepositoryImpl({required this.remoteDataSource});
@override
Future<Either<Failure, LoginResponseModel>> login(LoginRequest request) async {
try {
final dto = LoginDto(...);
final responseDto = await remoteDataSource.login(dto);
final model = _convertDtoToModel(responseDto);
return Right(model);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
}
}
}
```
#### هـ) إنشاء Use Case في `domain/usecases/`
```dart
class LoginUseCase {
final AuthRepository repository;
LoginUseCase({required this.repository});
Future<Either<Failure, LoginResponseModel>> call(LoginRequest request) {
return repository.login(request);
}
}
```
#### و) تسجيل في `core/di/injection_container.dart`
```dart
// Data source
sl.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(apiClient: sl()),
);
// Repository
sl.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(remoteDataSource: sl()),
);
// Use case
sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
```
### 2. استخدام في BLoC
```dart
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final LoginUseCase loginUseCase;
LoginBloc({required this.loginUseCase}) : super(LoginInitial()) {
on<LoginSubmitted>(_onLoginSubmitted);
}
Future<void> _onLoginSubmitted(
LoginSubmitted event,
Emitter<LoginState> emit,
) async {
emit(LoginLoading());
final result = await loginUseCase(event.request);
result.fold(
(failure) => emit(LoginError(failure.message)),
(response) => emit(LoginSuccess(response)),
);
}
}
```
## الحزم المستخدمة
- `dio`: للاتصال بالـ API
- `get_it`: لإدارة Dependency Injection
- `dartz`: لاستخدام `Either` للتعامل مع الأخطاء
- `equatable`: للمساواة بين الكائنات
- `shared_preferences`: للتخزين المحلي
## ملاحظات مهمة
1. **تحديث baseUrl**: قم بتحديث `baseUrl` في `core/network/api_client.dart`
2. **إضافة Token**: يمكن إضافة interceptor في `ApiClient` لإضافة token تلقائياً
3. **معالجة الأخطاء**: جميع الأخطاء تمر عبر `Exceptions` ثم `Failures`
4. **التحويل**: DTOs للـ API، Models للـ Domain

113
lib/LOGIN_SETUP.md Normal file
View 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 تلقائياً عند انتهاء الصلاحية

View File

@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
class AppDimensions {
// Private constructor to prevent instantiation
AppDimensions._();
// Screen breakpoints
static const double smallScreenWidth = 460;
static const double tabletWidth = 600;
static const double largeScreenWidth = 1200;
// Padding values
static const double paddingXS = 4.0;
static const double paddingS = 8.0;
static const double paddingM = 12.0;
static const double paddingL = 16.0;
static const double paddingXL = 25.0;
static const double paddingXXL = 30.0;
static const double paddingXXXL = 30.0;
static const double paddingHuge = 40.0;
// Spacing values
static const double spacingXS = 4.0;
static const double spacingS = 8.0;
static const double spacingM = 12.0;
static const double spacingL = 16.0;
static const double spacingXL = 20.0;
static const double spacingXXL = 23.0;
static const double spacingXXXL = 30.0;
static const double spacingHuge = 40.0;
// Font sizes
static const double fontSizeXS = 10.0;
static const double fontSizeS = 12.0;
static const double fontSizeM = 14.0;
static const double fontSizeL = 16.0;
static const double fontSizeXL = 18.0;
static const double fontSizeXXL = 20.0;
static const double fontSizeXXXL = 22.0;
static const double fontSizeHuge = 24.0;
static const double fontSizeTitle = 36.0;
static const double fontSizeTitleLarge = 42.0;
// Icon sizes
static const double iconSizeXS = 16.0;
static const double iconSizeS = 20.0;
static const double iconSizeM = 24.0;
static const double iconSizeL = 30.0;
static const double iconSizeXL = 33.0;
static const double iconSizeXXL = 40.0;
static const double iconSizeHuge = 60.0;
// Border radius
static const double radiusXS = 3.0;
static const double radiusS = 5.0;
static const double radiusM = 8.0;
static const double radiusL = 10.0;
static const double radiusXL = 15.0;
static const double radiusXXL = 20.0;
static const double radiusCircle = 50.0;
// Button heights
static const double buttonHeightS = 40.0;
static const double buttonHeightM = 48.0;
static const double buttonHeightL = 56.0;
// Image heights
static const double imageHeightS = 150.0;
static const double imageHeightM = 200.0;
static const double imageHeightL = 250.0;
// Page indicator sizes
static const double pageIndicatorSize = 10.0;
static const double pageIndicatorSizeActive = 16.0;
// Responsive methods
static bool isSmallScreen(BuildContext context) {
return MediaQuery.of(context).size.width < smallScreenWidth;
}
static bool isTablet(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width >= tabletWidth && width < largeScreenWidth;
}
static bool isLargeScreen(BuildContext context) {
return MediaQuery.of(context).size.width >= largeScreenWidth;
}
// Responsive padding
static double getHorizontalPadding(BuildContext context) {
return isSmallScreen(context) ? paddingXL : paddingXXL;
}
static double getVerticalPadding(BuildContext context) {
return isSmallScreen(context) ? paddingL : paddingXXL;
}
// Responsive font size
static double getTitleFontSize(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
if (isSmallScreen(context)) {
return screenWidth * 0.07;
} else if (isTablet(context)) {
return fontSizeTitleLarge;
} else {
return screenWidth * 0.08;
}
}
static double getSubtitleFontSize(BuildContext context) {
return isSmallScreen(context) ? fontSizeM : fontSizeL;
}
static double getBodyFontSize(BuildContext context) {
return isSmallScreen(context) ? fontSizeS : fontSizeM;
}
// Responsive spacing
static double getVerticalSpacing(BuildContext context) {
return isSmallScreen(context) ? spacingL : spacingXXL;
}
static double getSmallVerticalSpacing(BuildContext context) {
return isSmallScreen(context) ? spacingM : spacingL;
}
// Responsive icon size
static double getIconSize(BuildContext context) {
return isSmallScreen(context) ? iconSizeM : iconSizeXL;
}
// Screen dimensions
static double screenWidth(BuildContext context) {
return MediaQuery.of(context).size.width;
}
static double screenHeight(BuildContext context) {
return MediaQuery.of(context).size.height;
}
// Safe area padding
static EdgeInsets getSafeAreaPadding(BuildContext context) {
return MediaQuery.of(context).padding;
}
}

View File

@@ -0,0 +1,49 @@
import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.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;
Future<void> initializeDependencies() async {
// External
sl.registerLazySingleton<Dio>(() => Dio());
// SharedPreferences
final sharedPreferences = await SharedPreferences.getInstance();
sl.registerLazySingleton<SharedPreferences>(() => sharedPreferences);
// Core
sl.registerLazySingleton<ApiClient>(
() => ApiClient(dio: sl(), sharedPreferences: sl()),
);
// Data sources
sl.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(apiClient: sl()),
);
sl.registerLazySingleton<UserLocalDataSource>(
() => UserLocalDataSourceImpl(sharedPreferences: sl()),
);
// Repositories
sl.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remoteDataSource: sl(),
localDataSource: sl(),
),
);
// Use cases
sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
// Blocs will be registered here
// Example:
// sl.registerFactory(() => LoginBloc(loginUseCase: sl()));
}

View File

@@ -0,0 +1,7 @@
// Add your app-specific enums here
// Example:
// enum UserRole {
// admin,
// user,
// guest,
// }

View File

@@ -0,0 +1,18 @@
class ServerException implements Exception {
final String message;
final int? statusCode;
ServerException({required this.message, this.statusCode});
}
class NetworkException implements Exception {
final String message;
NetworkException({required this.message});
}
class ValidationException implements Exception {
final String message;
ValidationException({required this.message});
}

View File

@@ -0,0 +1,22 @@
import 'package:equatable/equatable.dart';
abstract class Failure extends Equatable {
final String message;
const Failure(this.message);
@override
List<Object> get props => [message];
}
class ServerFailure extends Failure {
const ServerFailure(super.message);
}
class NetworkFailure extends Failure {
const NetworkFailure(super.message);
}
class ValidationFailure extends Failure {
const ValidationFailure(super.message);
}

View File

@@ -0,0 +1,120 @@
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ApiClient {
final Dio dio;
final SharedPreferences? sharedPreferences;
static const String baseUrl = 'https://hrm.go.iq/api';
static const String _tokenKey = 'user_token';
ApiClient({required this.dio, this.sharedPreferences}) {
dio.options = BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'accept': 'text/plain',
},
);
// 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
dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
error: true,
requestHeader: true,
responseHeader: false,
),
);
}
Future<Response> post(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
try {
final response = await dio.post(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
return response;
} catch (e) {
rethrow;
}
}
Future<Response> get(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
try {
final response = await dio.get(
path,
queryParameters: queryParameters,
options: options,
);
return response;
} catch (e) {
rethrow;
}
}
Future<Response> put(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
try {
final response = await dio.put(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
return response;
} catch (e) {
rethrow;
}
}
Future<Response> delete(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
try {
final response = await dio.delete(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
return response;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,37 @@
class Validators {
static String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'البريد الإلكتروني مطلوب';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'البريد الإلكتروني غير صحيح';
}
return null;
}
static String? validatePhone(String? value) {
if (value == null || value.isEmpty) {
return 'رقم الهاتف مطلوب';
}
// Add your phone validation logic here
return null;
}
static String? validateRequired(String? value, String fieldName) {
if (value == null || value.isEmpty) {
return '$fieldName مطلوب';
}
return null;
}
static String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'كلمة المرور مطلوبة';
}
if (value.length < 6) {
return 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
}
return null;
}
}

View File

@@ -0,0 +1,25 @@
# Data sources directory
# Create your remote data sources here following this pattern:
#
# abstract class YourRemoteDataSource {
# Future<YourDto> yourMethod(YourRequest request);
# }
#
# class YourRemoteDataSourceImpl implements YourRemoteDataSource {
# final ApiClient apiClient;
#
# YourRemoteDataSourceImpl({required this.apiClient});
#
# @override
# Future<YourDto> yourMethod(YourRequest request) async {
# try {
# final response = await apiClient.post(
# '/your-endpoint',
# data: request.toJson(),
# );
# // Handle response and return DTO
# } on DioException catch (e) {
# // Handle errors
# }
# }
# }

View 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: 'خطأ غير متوقع');
}
}
}

View File

@@ -0,0 +1,29 @@
import 'package:shared_preferences/shared_preferences.dart';
abstract class UserLocalDataSource {
Future<void> cacheUserToken(String token);
Future<String?> getCachedUserToken();
Future<void> clearCache();
}
class UserLocalDataSourceImpl implements UserLocalDataSource {
final SharedPreferences sharedPreferences;
static const String _tokenKey = 'user_token';
UserLocalDataSourceImpl({required this.sharedPreferences});
@override
Future<void> cacheUserToken(String token) async {
await sharedPreferences.setString(_tokenKey, token);
}
@override
Future<String?> getCachedUserToken() async {
return sharedPreferences.getString(_tokenKey);
}
@override
Future<void> clearCache() async {
await sharedPreferences.remove(_tokenKey);
}
}

24
lib/data/dto/.gitkeep Normal file
View File

@@ -0,0 +1,24 @@
# DTO (Data Transfer Objects) directory
# Create your DTOs here for API request/response mapping
# Example:
#
# class LoginDto {
# final String phoneNumber;
# final String password;
#
# LoginDto({required this.phoneNumber, required this.password});
#
# Map<String, dynamic> toJson() {
# return {
# 'phoneNumber': phoneNumber,
# 'password': password,
# };
# }
#
# factory LoginDto.fromJson(Map<String, dynamic> json) {
# return LoginDto(
# phoneNumber: json['phoneNumber'],
# password: json['password'],
# );
# }
# }

View 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,
};
}
}

View 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,
};
}
}

View File

@@ -0,0 +1,27 @@
# Repository implementations directory
# Create your repository implementations here
# Example:
#
# 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(...);
# final responseDto = await remoteDataSource.login(dto);
# final responseModel = _convertDtoToModel(responseDto);
# return Right(responseModel);
# } on ServerException catch (e) {
# return Left(ServerFailure(e.message));
# } on NetworkException catch (e) {
# return Left(NetworkFailure(e.message));
# }
# }
# }

View 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'));
}
}
}

View File

@@ -0,0 +1,3 @@
# Domain models directory
# Create your domain models here (business logic models)
# These are different from DTOs - they represent your app's domain entities

View File

@@ -0,0 +1,9 @@
class LoginRequest {
final String phoneNumber;
final String password;
LoginRequest({
required this.phoneNumber,
required this.password,
});
}

View 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,
});
}

View File

@@ -0,0 +1,8 @@
# Repository interfaces directory
# Create your repository interfaces here
# Example:
#
# abstract class AuthRepository {
# Future<Either<Failure, LoginResponseModel>> login(LoginRequest request);
# Future<Either<Failure, RegisterResponseModel>> register(RegisterRequest request);
# }

View 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);
}

View File

@@ -0,0 +1,13 @@
# Use cases directory
# Create your use cases here (business logic)
# Example:
#
# class LoginUseCase {
# final AuthRepository repository;
#
# LoginUseCase({required this.repository});
#
# Future<Either<Failure, LoginResponseModel>> call(LoginRequest request) {
# return repository.login(request);
# }
# }

View 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);
}
}

View File

@@ -1,11 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'core/di/injection_container.dart';
import 'screens/splash_screen.dart';
void main() {
void main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
// Initialize dependency injection
await initializeDependencies();
runApp(const CodaApp());
}

View File

@@ -0,0 +1,6 @@
# BLoC directory
# Create your BLoCs here for state management
# Each BLoC should have its own folder with:
# - bloc_name_bloc.dart
# - bloc_name_event.dart
# - bloc_name_state.dart

View File

@@ -1,5 +1,8 @@
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';
class AuthForm extends StatefulWidget {
@@ -13,25 +16,86 @@ class AuthForm extends StatefulWidget {
class _AuthFormState extends State<AuthForm> {
bool _obscure = true;
bool _isLoading = false;
// Text controllers
final TextEditingController _phoneNumberController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
// Focus nodes for text fields
late FocusNode _usernameFocusNode;
late FocusNode _phoneNumberFocusNode;
late FocusNode _passwordFocusNode;
void _handleLogin() {
// Unfocus any focused text field
_usernameFocusNode.unfocus();
_passwordFocusNode.unfocus();
// Get LoginUseCase from dependency injection
final LoginUseCase _loginUseCase = sl<LoginUseCase>();
// Call the onSubmit callback if provided (for any other logic you might have)
if (widget.onSubmit != null) {
widget.onSubmit!();
Future<void> _handleLogin() async {
// Validate inputs
if (_phoneNumberController.text.trim().isEmpty) {
_showError('الرجاء إدخال رقم الهاتف');
return;
}
// Navigate to the AttendancePage
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainPage()),
if (_passwordController.text.trim().isEmpty) {
_showError('الرجاء إدخال كلمة المرور');
return;
}
// 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() {
super.initState();
// Initialize focus nodes
_usernameFocusNode = FocusNode();
_phoneNumberFocusNode = FocusNode();
_passwordFocusNode = FocusNode();
}
@override
void dispose() {
// Clean up focus nodes when widget is disposed
_usernameFocusNode.dispose();
// Clean up controllers and focus nodes
_phoneNumberController.dispose();
_passwordController.dispose();
_phoneNumberFocusNode.dispose();
_passwordFocusNode.dispose();
super.dispose();
}
@@ -130,11 +196,11 @@ class _AuthFormState extends State<AuthForm> {
SizedBox(height: verticalSpacing),
/// Username Label
/// Phone Number Label
Align(
alignment: Alignment.centerRight,
child: Text(
"اسم المستخدم",
"رقم الهاتف",
style: TextStyle(
fontSize: labelFontSize,
color: Colors.black87,
@@ -144,9 +210,11 @@ class _AuthFormState extends State<AuthForm> {
const SizedBox(height: 8),
_buildField(
hint: "اسم المستخدم",
controller: _phoneNumberController,
hint: "رقم الهاتف",
obscure: false,
focusNode: _usernameFocusNode,
keyboardType: TextInputType.phone,
focusNode: _phoneNumberFocusNode,
textInputAction: TextInputAction.next,
onSubmitted: (_) {
// Move focus to password field when next is pressed
@@ -171,14 +239,13 @@ class _AuthFormState extends State<AuthForm> {
const SizedBox(height: 8),
_buildField(
controller: _passwordController,
hint: "كلمة المرور",
obscure: _obscure,
hasEye: true,
focusNode: _passwordFocusNode,
textInputAction: TextInputAction.done,
onSubmitted:
(_) =>
_handleLogin(), // Added parentheses to call the method
onSubmitted: (_) => _handleLogin(),
fontSize: fieldFontSize,
),
@@ -186,9 +253,9 @@ class _AuthFormState extends State<AuthForm> {
Center(
child: OnboardingButton(
text: "تسجيل دخول",
text: _isLoading ? "جاري تسجيل الدخول..." : "تسجيل دخول",
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({
TextEditingController? controller,
required String hint,
required bool obscure,
bool hasEye = false,
TextInputType? keyboardType,
FocusNode? focusNode,
TextInputAction? textInputAction,
Function(String)? onSubmitted,
@@ -220,8 +289,10 @@ class _AuthFormState extends State<AuthForm> {
],
),
child: TextField(
controller: controller,
focusNode: focusNode,
obscureText: obscure,
keyboardType: keyboardType,
textAlign: TextAlign.right,
textInputAction: textInputAction,
onSubmitted: onSubmitted,