chnages has been made

This commit is contained in:
Daniah Ayad Al-sultani
2026-02-22 11:18:10 +03:00
parent 3a9e7ca8db
commit f616a2c104
26 changed files with 1130 additions and 201 deletions

View File

@@ -2,6 +2,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application

View File

@@ -1,5 +1,5 @@
package com.example.coda_project
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterActivity()
class MainActivity : FlutterFragmentActivity()

View File

@@ -19,10 +19,14 @@ class ApiClient {
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';
// Skip auth if the request explicitly opts out
final skipAuth = options.extra['skipAuth'] == true;
if (!skipAuth) {
// Get token from SharedPreferences
final token = sharedPreferences?.getString(_tokenKey);
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
}
return handler.next(options);
},

View File

@@ -13,11 +13,13 @@ abstract class AttendanceRemoteDataSource {
Future<AttendanceResponseDto> login({
required String employeeId,
required File faceImage,
bool localAuth = false,
});
Future<AttendanceResponseDto> logout({
required String employeeId,
required File faceImage,
bool localAuth = false,
});
Future<List<AttendanceRecordDto>> getAttendanceRecords({
@@ -43,11 +45,13 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
Future<AttendanceResponseDto> login({
required String employeeId,
required File faceImage,
bool localAuth = false,
}) async {
try {
final formData = FormData.fromMap({
'EmployeeId': employeeId,
'FaceImage': await MultipartFile.fromFile(faceImage.path),
'IsAuth': localAuth.toString(),
});
final response = await apiClient.post(
@@ -107,11 +111,13 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
Future<AttendanceResponseDto> logout({
required String employeeId,
required File faceImage,
bool localAuth = false,
}) async {
try {
final formData = FormData.fromMap({
'EmployeeId': employeeId,
'FaceImage': await MultipartFile.fromFile(faceImage.path),
'IsAuth': localAuth.toString(),
});
final response = await apiClient.post(

View File

@@ -0,0 +1,49 @@
import 'package:flutter/foundation.dart';
import '../../core/error/exceptions.dart';
import '../../core/network/api_client.dart';
import '../dto/theme_response_dto.dart';
abstract class ThemeRemoteDataSource {
Future<ThemeDataDto> getTheme();
}
class ThemeRemoteDataSourceImpl implements ThemeRemoteDataSource {
final ApiClient apiClient;
ThemeRemoteDataSourceImpl({required this.apiClient});
@override
Future<ThemeDataDto> getTheme() async {
try {
debugPrint('[ThemeDataSource] Calling GET /Theme (with auth)...');
final res = await apiClient.get('/Theme'); // ✅ no custom headers
debugPrint('[ThemeDataSource] Status: ${res.statusCode}');
debugPrint('[ThemeDataSource] Data: ${res.data}');
if (res.statusCode == 200) {
final dto = ThemeResponseDto.fromJson(
Map<String, dynamic>.from(res.data),
);
if (dto.isSuccess && dto.data != null) {
debugPrint('[ThemeDataSource] ✅ logo = ${dto.data!.logo}');
return dto.data!;
}
throw ServerException(message: dto.message ?? 'Theme request failed');
}
throw ServerException(
message: 'Theme request failed (code ${res.statusCode})',
);
} on ServerException {
rethrow;
} catch (e, stack) {
debugPrint('[ThemeDataSource] ❌ Exception: $e');
debugPrint('[ThemeDataSource] ❌ Stack: $stack');
throw ServerException(message: e.toString());
}
}
}

View File

@@ -0,0 +1,36 @@
class ThemeResponseDto {
final int statusCode;
final bool isSuccess;
final String? message;
final ThemeDataDto? data;
ThemeResponseDto({
required this.statusCode,
required this.isSuccess,
required this.message,
required this.data,
});
factory ThemeResponseDto.fromJson(Map<String, dynamic> json) {
return ThemeResponseDto(
statusCode: json['statusCode'] ?? 0,
isSuccess: json['isSuccess'] ?? false,
message: json['message']?.toString(),
data: json['data'] == null ? null : ThemeDataDto.fromJson(json['data']),
);
}
}
class ThemeDataDto {
final String name;
final String logo;
ThemeDataDto({required this.name, required this.logo});
factory ThemeDataDto.fromJson(Map<String, dynamic> json) {
return ThemeDataDto(
name: (json['name'] ?? '').toString(),
logo: (json['logo'] ?? '').toString(),
);
}
}

View File

@@ -18,6 +18,7 @@ class AttendanceRepositoryImpl implements AttendanceRepository {
final dto = await remoteDataSource.login(
employeeId: request.employeeId,
faceImage: request.faceImage,
localAuth: request.localAuth,
);
return AttendanceResponseModel(
@@ -34,6 +35,7 @@ class AttendanceRepositoryImpl implements AttendanceRepository {
final dto = await remoteDataSource.logout(
employeeId: request.employeeId,
faceImage: request.faceImage,
localAuth: request.localAuth,
);
return AttendanceResponseModel(

View File

@@ -0,0 +1,24 @@
import 'package:dartz/dartz.dart';
import '../../core/error/failures.dart';
import '../../core/error/exceptions.dart';
import '../../domain/models/theme_model.dart';
import '../../domain/repositories/theme_repository.dart';
import '../datasources/theme_remote_data_source.dart';
class ThemeRepositoryImpl implements ThemeRepository {
final ThemeRemoteDataSource remote;
ThemeRepositoryImpl({required this.remote});
@override
Future<Either<Failure, ThemeModel>> getTheme() async {
try {
final dto = await remote.getTheme();
return Right(ThemeModel(name: dto.name, logo: dto.logo));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure(e.toString()));
}
}
}

View File

@@ -3,6 +3,11 @@ import 'dart:io';
class AttendanceLoginRequest {
final String employeeId;
final File faceImage;
final bool localAuth;
AttendanceLoginRequest({required this.employeeId, required this.faceImage});
AttendanceLoginRequest({
required this.employeeId,
required this.faceImage,
this.localAuth = false,
});
}

View File

@@ -3,6 +3,11 @@ import 'dart:io';
class AttendanceLogoutRequest {
final String employeeId;
final File faceImage;
final bool localAuth;
AttendanceLogoutRequest({required this.employeeId, required this.faceImage});
AttendanceLogoutRequest({
required this.employeeId,
required this.faceImage,
this.localAuth = false,
});
}

View File

@@ -0,0 +1,6 @@
class ThemeModel {
final String name;
final String logo; // filename or url
const ThemeModel({required this.name, required this.logo});
}

View File

@@ -0,0 +1,7 @@
import 'package:dartz/dartz.dart';
import '../../core/error/failures.dart';
import '../models/theme_model.dart';
abstract class ThemeRepository {
Future<Either<Failure, ThemeModel>> getTheme();
}

View File

@@ -0,0 +1,11 @@
import 'package:dartz/dartz.dart';
import '../../core/error/failures.dart';
import '../models/theme_model.dart';
import '../repositories/theme_repository.dart';
class GetThemeUseCase {
final ThemeRepository repo;
GetThemeUseCase(this.repo);
Future<Either<Failure, ThemeModel>> call() => repo.getTheme();
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/usecases/get_theme_usecase.dart';
import 'theme_state.dart';
class ThemeCubit extends Cubit<ThemeState> {
final GetThemeUseCase getThemeUseCase;
/// Base URL for loading theme images (logo, etc.)
static const String _imageBaseUrl = 'https://hrm.go.iq/Images/';
/// Guard against re-entrant / duplicate calls
bool _isLoading = false;
ThemeCubit({required this.getThemeUseCase}) : super(const ThemeInitial());
/// Load theme. Set [forceReload] to true after login to refresh.
Future<void> loadTheme({bool forceReload = false}) async {
// Prevent duplicate concurrent calls
if (_isLoading) {
debugPrint('[ThemeCubit] loadTheme() skipped — already loading');
return;
}
// If already loaded and not forced, skip
if (!forceReload && state is ThemeLoaded) {
debugPrint('[ThemeCubit] loadTheme() skipped — already loaded');
return;
}
_isLoading = true;
debugPrint('[ThemeCubit] loadTheme() called (forceReload=$forceReload)');
emit(const ThemeLoading());
final result = await getThemeUseCase();
_isLoading = false;
result.fold(
(failure) {
debugPrint('[ThemeCubit] ❌ FAILED: ${failure.message}');
emit(ThemeError(failure.message));
},
(theme) {
debugPrint(
'[ThemeCubit] ✅ Got theme — name: ${theme.name}, logo: ${theme.logo}',
);
// Build the full logo URL: https://hrm.go.iq/Images/{filename}
final encodedLogo = Uri.encodeFull(theme.logo);
final logoUrl = '$_imageBaseUrl$encodedLogo';
debugPrint('[ThemeCubit] 🖼️ Logo URL: $logoUrl');
emit(ThemeLoaded(theme: theme, logoUrl: logoUrl));
},
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:equatable/equatable.dart';
import '../../../domain/models/theme_model.dart';
abstract class ThemeState extends Equatable {
const ThemeState();
@override
List<Object?> get props => [];
}
class ThemeInitial extends ThemeState {
const ThemeInitial();
}
class ThemeLoading extends ThemeState {
const ThemeLoading();
}
class ThemeLoaded extends ThemeState {
final ThemeModel theme;
final String logoUrl;
const ThemeLoaded({required this.theme, required this.logoUrl});
@override
List<Object?> get props => [theme, logoUrl];
}
class ThemeError extends ThemeState {
final String message;
const ThemeError(this.message);
@override
List<Object?> get props => [message];
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,18 @@ class AuthScreen extends StatelessWidget {
children: [
const SizedBox(height: 60),
// Logo
Center(child: Image.asset("assets/images/logo2.png", width: 200)),
// Center(child: Image.asset("assets/images/logo2.png", width: 200)),
const Center(
child: Text(
'LOGO',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
),
// const SizedBox(height: 15),
// Form - taking remaining space and centered
Expanded(child: Center(child: const AuthForm())),

View File

@@ -12,10 +12,11 @@ import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
import '../../core/error/exceptions.dart';
import '../face/face_feedback.dart';
import 'package:local_auth/local_auth.dart';
class OvalCameraCapturePage extends StatefulWidget {
final bool isLogin;
final Future<void> Function(File image) onCapture;
final Future<void> Function(File image, {required bool localAuth}) onCapture;
const OvalCameraCapturePage({
super.key,
@@ -37,6 +38,13 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
bool _isSubmitting = false;
bool _isStreaming = false;
//to handel the state of auth when it gives 422 status code
final LocalAuthentication _localAuth = LocalAuthentication();
bool _handlingAuth422 = false; // prevents multiple dialogs/auth prompts
File? _lastCapturedFile; // keep the same image for retry
// Smart feedback
FaceFeedback _feedback = FaceFeedback(
type: FaceHintType.noFace,
@@ -252,6 +260,49 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
}
}
Future<bool?> _showLocalAuthDialog() {
return showDialog<bool>(
context: context,
barrierDismissible: false,
builder:
(_) => AlertDialog(
title: const Text('فشل التحقق بالوجه', textAlign: TextAlign.center),
content: const Text(
'لم يتم التعرف على الوجه.\n\nيرجى استخدام بصمة الإصبع أو رمز القفل (PIN/النمط) للمتابعة.',
textAlign: TextAlign.center,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('إلغاء'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('استخدام البصمة / رمز القفل'),
),
],
),
);
}
Future<bool> _authenticateLocally() async {
try {
final isSupported = await _localAuth.isDeviceSupported();
if (!isSupported) return false;
return await _localAuth.authenticate(
localizedReason: 'تأكيد هويتك لإكمال تسجيل الحضور.',
options: const AuthenticationOptions(
biometricOnly: false, // ✅ allows PIN/pattern fallback
stickyAuth: true,
useErrorDialogs: true,
),
);
} catch (_) {
return false;
}
}
Future<void> _stopImageStream() async {
if (!_isStreaming || _cameraController == null) return;
try {
@@ -408,7 +459,9 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
final xFile = await _cameraController!.takePicture();
final file = File(xFile.path);
await widget.onCapture(file);
_lastCapturedFile = file;
await widget.onCapture(file, localAuth: false);
if (mounted) {
setState(() {
@@ -423,8 +476,13 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
});
}
} on ServerException catch (e) {
// Check if this is an "already logged in" error from the API
final msg = e.message.toLowerCase();
// If your ServerException has statusCode, prefer that:
if (e.statusCode == 422 || msg.contains('face verification failed')) {
await _handleFaceVerificationFailed422(e);
return;
}
if (msg.contains('already logged in') ||
msg.contains('مسجل دخول بالفعل')) {
// Stop camera and go back with a dialog
@@ -545,6 +603,86 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
return null;
}
Future<void> _handleFaceVerificationFailed422(ServerException e) async {
if (!mounted) return;
if (_handlingAuth422) return;
_handlingAuth422 = true;
// stop everything so camera doesnt keep scanning
await _stopImageStream();
setState(() {
_isSubmitting = false;
_errorMessage = null;
_debugInfo = "Face verification failed (422) → Local Auth...";
});
final proceed = await _showLocalAuthDialog();
if (proceed != true) {
_handlingAuth422 = false;
_stopCameraCompletely();
if (!mounted) return;
// Go back to attendance + show message there
Navigator.of(context).pop(false);
// If you prefer to show inside this screen before pop:
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('تم الإلغاء.')));
return;
}
final ok = await _authenticateLocally();
if (!ok) {
_handlingAuth422 = false;
_stopCameraCompletely();
if (!mounted) return;
// Return to attendance; attendance screen should show snack "failed fingerprint/pattern"
Navigator.of(context).pop("local_auth_failed");
return;
}
// Local auth success → retry SAME image with localAuth=true
final file = _lastCapturedFile;
if (file == null) {
_handlingAuth422 = false;
_stopCameraCompletely();
if (!mounted) return;
Navigator.of(context).pop("retry_missing_file");
return;
}
setState(() {
_isSubmitting = true;
_debugInfo = "Local auth success → retrying with localAuth=true...";
});
try {
await widget.onCapture(file, localAuth: true);
if (!mounted) return;
setState(() {
_isSuccess = true;
_isSubmitting = false;
});
Future.delayed(const Duration(seconds: 1), () {
if (mounted) Navigator.of(context).pop(true);
});
} on ServerException catch (e2) {
_handlingAuth422 = false;
_stopCameraCompletely();
if (!mounted) return;
// Retry failed → go back to attendance
Navigator.of(context).pop("retry_failed");
} catch (_) {
_handlingAuth422 = false;
_stopCameraCompletely();
if (!mounted) return;
Navigator.of(context).pop("retry_failed");
}
}
Uint8List _convertYUV420ToNV21(CameraImage image) {
final int width = image.width;
final int height = image.height;

View File

@@ -88,7 +88,16 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
Column(
children: [
const SizedBox(height: 70),
Image.asset("assets/images/logo2.png", width: 200),
// Image.asset("assets/images/logo2.png", width: 200),
const Text(
'LOGO',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
/// PAGEVIEW (SVG + TEXT ONLY)
Expanded(

View File

@@ -31,7 +31,7 @@ class _SplashScreenState extends State<SplashScreen> {
final token = await sl<UserLocalDataSource>().getCachedUserToken();
if (token != null && token.isNotEmpty) {
// Token exists, navigate directly to MainPage
// Token exists — go to MainPage (theme already loaded in main.dart)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
@@ -65,7 +65,18 @@ class _SplashScreenState extends State<SplashScreen> {
fit: BoxFit.cover,
),
),
child: Center(child: Image.asset("assets/images/logo.png", width: 200)),
// child: Center(child: Image.asset("assets/images/logo.png", width: 200)),
child: const Center(
child: Text(
'LOGO',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
),
),
);
}

View File

@@ -12,7 +12,7 @@ class SettingsBar extends StatelessWidget {
super.key,
required this.selectedIndex,
required this.onTap,
this.showBackButton = false, // to switch between back button and settings icons
this.showBackButton = false,
this.onBackTap,
required this.iconPaths,
});
@@ -25,10 +25,15 @@ class SettingsBar extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Image.asset('assets/images/logo2.png', width: 150, height: 40),
],
// Text Logo
const Text(
'LOGO',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
Row(
children: [
@@ -50,11 +55,10 @@ class SettingsBar extends StatelessWidget {
],
),
child: Center(
// Always use Flutter's built-in back icon pointing to the right
child: const Icon(
Icons.arrow_forward, // Changed to arrow_forward for RTL
Icons.arrow_forward,
size: 26,
color: Colors.black, // Adjust color as needed
color: Colors.black,
),
),
),
@@ -65,7 +69,6 @@ class SettingsBar extends StatelessWidget {
...iconPaths.asMap().entries.map((entry) {
final index = entry.key;
final iconPath = entry.value;
// final isSelected = selectedIndex == index;
return Padding(
padding: const EdgeInsets.only(left: 10),
@@ -102,4 +105,4 @@ class SettingsBar extends StatelessWidget {
),
);
}
}
}

View File

@@ -5,8 +5,10 @@
import FlutterMacOS
import Foundation
import local_auth_darwin
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@@ -29,10 +29,10 @@ packages:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.13.0"
version: "2.12.0"
bloc:
dependency: transitive
description:
@@ -189,10 +189,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
version: "1.3.2"
ffi:
dependency: transitive
description:
@@ -348,10 +348,10 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.9"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
@@ -376,6 +376,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.1"
local_auth:
dependency: "direct main"
description:
name: local_auth
sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
local_auth_android:
dependency: transitive
description:
name: local_auth_android
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
url: "https://pub.dev"
source: hosted
version: "1.0.52"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
local_auth_platform_interface:
dependency: transitive
description:
name: local_auth_platform_interface
sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122
url: "https://pub.dev"
source: hosted
version: "1.1.0"
local_auth_windows:
dependency: transitive
description:
name: local_auth_windows
sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
url: "https://pub.dev"
source: hosted
version: "1.0.11"
matcher:
dependency: transitive
description:
@@ -665,10 +705,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "15.0.0"
version: "14.3.1"
web:
dependency: transitive
description:

View File

@@ -20,6 +20,7 @@ dependencies:
flutter_bloc: ^8.1.6
intl: ^0.19.0
google_mlkit_face_detection: ^0.12.0
local_auth: ^2.1.8
dev_dependencies:
flutter_test:

View File

@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <local_auth_windows/local_auth_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
local_auth_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST