chnages has been made
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<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" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||||
<application
|
<application
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package com.example.coda_project
|
package com.example.coda_project
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
|
|
||||||
class MainActivity : FlutterActivity()
|
class MainActivity : FlutterFragmentActivity()
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ class ApiClient {
|
|||||||
dio.interceptors.add(
|
dio.interceptors.add(
|
||||||
InterceptorsWrapper(
|
InterceptorsWrapper(
|
||||||
onRequest: (options, handler) async {
|
onRequest: (options, handler) async {
|
||||||
|
// Skip auth if the request explicitly opts out
|
||||||
|
final skipAuth = options.extra['skipAuth'] == true;
|
||||||
|
if (!skipAuth) {
|
||||||
// Get token from SharedPreferences
|
// Get token from SharedPreferences
|
||||||
final token = sharedPreferences?.getString(_tokenKey);
|
final token = sharedPreferences?.getString(_tokenKey);
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return handler.next(options);
|
return handler.next(options);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ abstract class AttendanceRemoteDataSource {
|
|||||||
Future<AttendanceResponseDto> login({
|
Future<AttendanceResponseDto> login({
|
||||||
required String employeeId,
|
required String employeeId,
|
||||||
required File faceImage,
|
required File faceImage,
|
||||||
|
bool localAuth = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<AttendanceResponseDto> logout({
|
Future<AttendanceResponseDto> logout({
|
||||||
required String employeeId,
|
required String employeeId,
|
||||||
required File faceImage,
|
required File faceImage,
|
||||||
|
bool localAuth = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<List<AttendanceRecordDto>> getAttendanceRecords({
|
Future<List<AttendanceRecordDto>> getAttendanceRecords({
|
||||||
@@ -43,11 +45,13 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
Future<AttendanceResponseDto> login({
|
Future<AttendanceResponseDto> login({
|
||||||
required String employeeId,
|
required String employeeId,
|
||||||
required File faceImage,
|
required File faceImage,
|
||||||
|
bool localAuth = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final formData = FormData.fromMap({
|
final formData = FormData.fromMap({
|
||||||
'EmployeeId': employeeId,
|
'EmployeeId': employeeId,
|
||||||
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
||||||
|
'IsAuth': localAuth.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
final response = await apiClient.post(
|
final response = await apiClient.post(
|
||||||
@@ -107,11 +111,13 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
Future<AttendanceResponseDto> logout({
|
Future<AttendanceResponseDto> logout({
|
||||||
required String employeeId,
|
required String employeeId,
|
||||||
required File faceImage,
|
required File faceImage,
|
||||||
|
bool localAuth = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final formData = FormData.fromMap({
|
final formData = FormData.fromMap({
|
||||||
'EmployeeId': employeeId,
|
'EmployeeId': employeeId,
|
||||||
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
||||||
|
'IsAuth': localAuth.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
final response = await apiClient.post(
|
final response = await apiClient.post(
|
||||||
|
|||||||
49
lib/data/datasources/theme_remote_data_source.dart
Normal file
49
lib/data/datasources/theme_remote_data_source.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/data/dto/theme_response_dto.dart
Normal file
36
lib/data/dto/theme_response_dto.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ class AttendanceRepositoryImpl implements AttendanceRepository {
|
|||||||
final dto = await remoteDataSource.login(
|
final dto = await remoteDataSource.login(
|
||||||
employeeId: request.employeeId,
|
employeeId: request.employeeId,
|
||||||
faceImage: request.faceImage,
|
faceImage: request.faceImage,
|
||||||
|
localAuth: request.localAuth,
|
||||||
);
|
);
|
||||||
|
|
||||||
return AttendanceResponseModel(
|
return AttendanceResponseModel(
|
||||||
@@ -34,6 +35,7 @@ class AttendanceRepositoryImpl implements AttendanceRepository {
|
|||||||
final dto = await remoteDataSource.logout(
|
final dto = await remoteDataSource.logout(
|
||||||
employeeId: request.employeeId,
|
employeeId: request.employeeId,
|
||||||
faceImage: request.faceImage,
|
faceImage: request.faceImage,
|
||||||
|
localAuth: request.localAuth,
|
||||||
);
|
);
|
||||||
|
|
||||||
return AttendanceResponseModel(
|
return AttendanceResponseModel(
|
||||||
|
|||||||
24
lib/data/repositories/theme_repository_impl.dart
Normal file
24
lib/data/repositories/theme_repository_impl.dart
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,11 @@ import 'dart:io';
|
|||||||
class AttendanceLoginRequest {
|
class AttendanceLoginRequest {
|
||||||
final String employeeId;
|
final String employeeId;
|
||||||
final File faceImage;
|
final File faceImage;
|
||||||
|
final bool localAuth;
|
||||||
|
|
||||||
AttendanceLoginRequest({required this.employeeId, required this.faceImage});
|
AttendanceLoginRequest({
|
||||||
|
required this.employeeId,
|
||||||
|
required this.faceImage,
|
||||||
|
this.localAuth = false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import 'dart:io';
|
|||||||
class AttendanceLogoutRequest {
|
class AttendanceLogoutRequest {
|
||||||
final String employeeId;
|
final String employeeId;
|
||||||
final File faceImage;
|
final File faceImage;
|
||||||
|
final bool localAuth;
|
||||||
|
|
||||||
AttendanceLogoutRequest({required this.employeeId, required this.faceImage});
|
AttendanceLogoutRequest({
|
||||||
|
required this.employeeId,
|
||||||
|
required this.faceImage,
|
||||||
|
this.localAuth = false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
6
lib/domain/models/theme_model.dart
Normal file
6
lib/domain/models/theme_model.dart
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class ThemeModel {
|
||||||
|
final String name;
|
||||||
|
final String logo; // filename or url
|
||||||
|
|
||||||
|
const ThemeModel({required this.name, required this.logo});
|
||||||
|
}
|
||||||
7
lib/domain/repositories/theme_repository.dart
Normal file
7
lib/domain/repositories/theme_repository.dart
Normal 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();
|
||||||
|
}
|
||||||
11
lib/domain/usecases/get_theme_usecase.dart
Normal file
11
lib/domain/usecases/get_theme_usecase.dart
Normal 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();
|
||||||
|
}
|
||||||
56
lib/presentation/blocs/theme/theme_cubit.dart
Normal file
56
lib/presentation/blocs/theme/theme_cubit.dart
Normal 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));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/presentation/blocs/theme/theme_state.dart
Normal file
36
lib/presentation/blocs/theme/theme_state.dart
Normal 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];
|
||||||
|
}
|
||||||
@@ -1,3 +1,392 @@
|
|||||||
|
// // import 'package:coda_project/presentation/screens/face_screen2.dart';
|
||||||
|
// // import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||||
|
// // import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||||
|
// // import 'package:flutter/material.dart';
|
||||||
|
// // import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
// // import '../widgets/settings_bar.dart';
|
||||||
|
// // import '../../core/di/injection_container.dart';
|
||||||
|
// // import '../../domain/models/attendance_login_request.dart';
|
||||||
|
// // import '../../domain/models/attendance_logout_request.dart';
|
||||||
|
// // import '../../domain/usecases/attendance_login_usecase.dart';
|
||||||
|
// // import '../../domain/usecases/attendance_logout_usecase.dart';
|
||||||
|
// // import '../../data/datasources/user_local_data_source.dart';
|
||||||
|
// // import '../../data/datasources/attendance_remote_data_source.dart';
|
||||||
|
|
||||||
|
// // class AttendanceScreen extends StatelessWidget {
|
||||||
|
// // const AttendanceScreen({super.key});
|
||||||
|
|
||||||
|
// // @override
|
||||||
|
// // Widget build(BuildContext context) {
|
||||||
|
// // final screenWidth = MediaQuery.sizeOf(context).width;
|
||||||
|
// // final screenHeight = MediaQuery.sizeOf(context).height;
|
||||||
|
// // return Directionality(
|
||||||
|
// // textDirection: TextDirection.ltr,
|
||||||
|
// // child: Stack(
|
||||||
|
// // children: [
|
||||||
|
// // SizedBox(height: MediaQuery.of(context).size.height),
|
||||||
|
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // /// SETTINGS BAR (STATIC)
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // SafeArea(
|
||||||
|
// // child: SettingsBar(
|
||||||
|
// // selectedIndex: 0,
|
||||||
|
// // showBackButton: false,
|
||||||
|
// // iconPaths: ['assets/images/user.svg', 'assets/images/ball.svg'],
|
||||||
|
// // onTap: (index) {
|
||||||
|
// // if (index == 0) {
|
||||||
|
// // Navigator.push(
|
||||||
|
// // context,
|
||||||
|
// // MaterialPageRoute(
|
||||||
|
// // builder: (context) => UserSettingsScreen(),
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // } else if (index == 1) {
|
||||||
|
// // Navigator.push(
|
||||||
|
// // context,
|
||||||
|
// // MaterialPageRoute(
|
||||||
|
// // builder: (context) => NotificationsScreen(),
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // },
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // /// GREETING TEXT
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // Positioned(
|
||||||
|
// // top:
|
||||||
|
// // screenHeight *
|
||||||
|
// // 0.14, // moved down because settings bar now exists
|
||||||
|
// // left: 0,
|
||||||
|
// // right: 0,
|
||||||
|
// // child: Center(
|
||||||
|
// // child: Text(
|
||||||
|
// // "صباح الخير, محمد",
|
||||||
|
// // style: TextStyle(
|
||||||
|
// // fontSize: 24,
|
||||||
|
// // fontWeight: FontWeight.w600,
|
||||||
|
// // color: Colors.white,
|
||||||
|
// // shadows: [Shadow(color: Color(0x42000000), blurRadius: 6)],
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // /// MAIN CARD AREA
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // Positioned(
|
||||||
|
// // top:
|
||||||
|
// // screenHeight *
|
||||||
|
// // 0.2, // pushed down because of settings bar + greeting
|
||||||
|
// // left: 0,
|
||||||
|
// // right: 0,
|
||||||
|
// // child: Center(
|
||||||
|
// // child: Padding(
|
||||||
|
// // padding: EdgeInsets.symmetric(vertical: screenHeight * 0.05),
|
||||||
|
// // child: Stack(
|
||||||
|
// // children: [
|
||||||
|
// // Container(
|
||||||
|
// // height: screenHeight * 0.5,
|
||||||
|
// // width: screenWidth * 0.7,
|
||||||
|
// // decoration: BoxDecoration(
|
||||||
|
// // borderRadius: BorderRadius.circular(32),
|
||||||
|
// // boxShadow: [
|
||||||
|
// // BoxShadow(
|
||||||
|
// // color: Color(0x1F2B2B2B),
|
||||||
|
// // blurRadius: 5,
|
||||||
|
// // offset: Offset(10, -10),
|
||||||
|
// // ),
|
||||||
|
// // BoxShadow(
|
||||||
|
// // color: Color(0xABCECECE),
|
||||||
|
// // blurRadius: 5,
|
||||||
|
// // offset: Offset(-2, 5),
|
||||||
|
// // ),
|
||||||
|
// // BoxShadow(
|
||||||
|
// // color: Color.fromARGB(148, 2, 70, 35),
|
||||||
|
// // blurRadius: 80,
|
||||||
|
// // offset: Offset(0, 10),
|
||||||
|
// // ),
|
||||||
|
// // ],
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // Container(
|
||||||
|
// // height: screenHeight * 0.5,
|
||||||
|
// // width: screenWidth * 0.7,
|
||||||
|
// // decoration: BoxDecoration(
|
||||||
|
// // color: Color(0x92757575),
|
||||||
|
// // borderRadius: BorderRadius.circular(32),
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // ],
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // /// LOGIN BUTTON
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // Positioned(
|
||||||
|
// // top: screenHeight * 0.21,
|
||||||
|
// // left: screenWidth * 0.05,
|
||||||
|
// // child: _ShadowedCard(
|
||||||
|
// // shadow: [
|
||||||
|
// // BoxShadow(
|
||||||
|
// // color: Color(0x62000000),
|
||||||
|
// // blurRadius: 10,
|
||||||
|
// // spreadRadius: 5,
|
||||||
|
// // offset: Offset(5, 5),
|
||||||
|
// // ),
|
||||||
|
// // ],
|
||||||
|
// // child: _FingerButton(
|
||||||
|
// // icon: "assets/images/faceLogin.svg",
|
||||||
|
// // label: "تسجيل الدخول",
|
||||||
|
// // onTap: () async {
|
||||||
|
// // final employeeId =
|
||||||
|
// // await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
|
// // print("ATTENDANCE_SCREEN: Retrieved EmployeeId: $employeeId");
|
||||||
|
// // if (employeeId == null) {
|
||||||
|
// // if (context.mounted) {
|
||||||
|
// // ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// // const SnackBar(
|
||||||
|
// // content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // return;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // // ------------------------------
|
||||||
|
// // // ACTIVE SESSION CHECK (LOGIN)
|
||||||
|
// // // ------------------------------
|
||||||
|
// // try {
|
||||||
|
// // // Optional: Show a loading dialog if it takes too long
|
||||||
|
// // final hasActive = await sl<AttendanceRemoteDataSource>()
|
||||||
|
// // .hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
|
// // if (hasActive) {
|
||||||
|
// // if (context.mounted) {
|
||||||
|
// // ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// // const SnackBar(content: Text('أنت مسجل دخول بالفعل')),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // return;
|
||||||
|
// // }
|
||||||
|
// // } catch (e) {
|
||||||
|
// // if (context.mounted) {
|
||||||
|
// // ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// // SnackBar(content: Text('فشل التحقق من الجلسة: $e')),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // return;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // if (context.mounted) {
|
||||||
|
// // Navigator.of(context).push(
|
||||||
|
// // MaterialPageRoute(
|
||||||
|
// // builder:
|
||||||
|
// // (_) => OvalCameraCapturePage(
|
||||||
|
// // isLogin: true,
|
||||||
|
// // onCapture: (imageFile) async {
|
||||||
|
// // final loginUseCase =
|
||||||
|
// // sl<AttendanceLoginUsecase>();
|
||||||
|
// // await loginUseCase(
|
||||||
|
// // AttendanceLoginRequest(
|
||||||
|
// // employeeId: employeeId,
|
||||||
|
// // faceImage: imageFile,
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // },
|
||||||
|
// // checkIfLoggedIn: () {},
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // },
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // /// LOGOUT BUTTON
|
||||||
|
// // /// ------------------------------
|
||||||
|
// // Positioned(
|
||||||
|
// // bottom: screenHeight * 0.2,
|
||||||
|
// // right: screenWidth * 0.1,
|
||||||
|
// // child: _ShadowedCard(
|
||||||
|
// // shadow: [
|
||||||
|
// // BoxShadow(
|
||||||
|
// // color: Color(0xABCECECE),
|
||||||
|
// // blurRadius: 5,
|
||||||
|
// // spreadRadius: 3,
|
||||||
|
// // offset: Offset(-6, -6),
|
||||||
|
// // ),
|
||||||
|
// // BoxShadow(
|
||||||
|
// // color: Color(0x92014221),
|
||||||
|
// // blurRadius: 10,
|
||||||
|
// // offset: Offset(-5, -5),
|
||||||
|
// // ),
|
||||||
|
// // BoxShadow(
|
||||||
|
// // color: Color(0x7D1A1A1A),
|
||||||
|
// // blurRadius: 10,
|
||||||
|
// // spreadRadius: 3,
|
||||||
|
// // offset: Offset(5, 5),
|
||||||
|
// // ),
|
||||||
|
// // ],
|
||||||
|
// // child: _FingerButton(
|
||||||
|
// // icon: "assets/images/faceLogout.svg",
|
||||||
|
// // label: "تسجيل خروج",
|
||||||
|
// // onTap: () async {
|
||||||
|
// // final employeeId =
|
||||||
|
// // await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
|
// // if (employeeId == null) {
|
||||||
|
// // if (context.mounted) {
|
||||||
|
// // ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// // const SnackBar(
|
||||||
|
// // content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // return;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // // ------------------------------
|
||||||
|
// // // ACTIVE SESSION CHECK (LOGOUT)
|
||||||
|
// // // ------------------------------
|
||||||
|
// // try {
|
||||||
|
// // final hasActive = await sl<AttendanceRemoteDataSource>()
|
||||||
|
// // .hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
|
// // if (!hasActive) {
|
||||||
|
// // if (context.mounted) {
|
||||||
|
// // ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// // const SnackBar(
|
||||||
|
// // content: Text(
|
||||||
|
// // 'لا يوجد تسجيل دخول فعال لتسجيل الخروج',
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // return;
|
||||||
|
// // }
|
||||||
|
// // } catch (e) {
|
||||||
|
// // if (context.mounted) {
|
||||||
|
// // ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// // SnackBar(content: Text('فشل التحقق من الجلسة: $e')),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // return;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // if (context.mounted) {
|
||||||
|
// // Navigator.of(context).push(
|
||||||
|
// // MaterialPageRoute(
|
||||||
|
// // builder:
|
||||||
|
// // (_) => OvalCameraCapturePage(
|
||||||
|
// // isLogin: false,
|
||||||
|
// // onCapture: (imageFile) async {
|
||||||
|
// // final logoutUseCase =
|
||||||
|
// // sl<AttendanceLogoutUseCase>();
|
||||||
|
// // await logoutUseCase(
|
||||||
|
// // AttendanceLogoutRequest(
|
||||||
|
// // employeeId: employeeId,
|
||||||
|
// // faceImage: imageFile,
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // },
|
||||||
|
// // checkIfLoggedIn: () {},
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // },
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // ],
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // /// ---------------------------------------------
|
||||||
|
// // /// SHADOW WRAPPER
|
||||||
|
// // /// ---------------------------------------------
|
||||||
|
|
||||||
|
// // class _ShadowedCard extends StatelessWidget {
|
||||||
|
// // final Widget child;
|
||||||
|
// // final List<BoxShadow> shadow;
|
||||||
|
|
||||||
|
// // const _ShadowedCard({required this.child, required this.shadow});
|
||||||
|
|
||||||
|
// // @override
|
||||||
|
// // Widget build(BuildContext context) {
|
||||||
|
// // return Stack(
|
||||||
|
// // children: [
|
||||||
|
// // Container(
|
||||||
|
// // height: 160,
|
||||||
|
// // width: 160,
|
||||||
|
// // decoration: BoxDecoration(
|
||||||
|
// // borderRadius: BorderRadius.circular(32),
|
||||||
|
// // boxShadow: shadow,
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // child,
|
||||||
|
// // ],
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // /// ---------------------------------------------
|
||||||
|
// // /// BUTTON WIDGET
|
||||||
|
// // /// ---------------------------------------------
|
||||||
|
|
||||||
|
// // class _FingerButton extends StatelessWidget {
|
||||||
|
// // final String icon;
|
||||||
|
// // final String label;
|
||||||
|
// // final VoidCallback onTap;
|
||||||
|
|
||||||
|
// // const _FingerButton({
|
||||||
|
// // required this.icon,
|
||||||
|
// // required this.label,
|
||||||
|
// // required this.onTap,
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// // @override
|
||||||
|
// // Widget build(BuildContext context) {
|
||||||
|
// // return GestureDetector(
|
||||||
|
// // onTap: onTap,
|
||||||
|
// // child: Container(
|
||||||
|
// // height: 160,
|
||||||
|
// // width: 160,
|
||||||
|
// // decoration: BoxDecoration(
|
||||||
|
// // color: Color(0xFFEAFBF3),
|
||||||
|
// // borderRadius: BorderRadius.circular(32),
|
||||||
|
// // ),
|
||||||
|
// // child: Column(
|
||||||
|
// // mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
// // children: [
|
||||||
|
// // SvgPicture.asset(icon, width: 75, height: 75),
|
||||||
|
// // SizedBox(height: 10),
|
||||||
|
// // Text(
|
||||||
|
// // label,
|
||||||
|
// // style: TextStyle(
|
||||||
|
// // fontSize: 18,
|
||||||
|
// // fontWeight: FontWeight.w600,
|
||||||
|
// // color: Colors.black,
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // ],
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
|
||||||
// import 'package:coda_project/presentation/screens/face_screen2.dart';
|
// import 'package:coda_project/presentation/screens/face_screen2.dart';
|
||||||
// import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
// import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||||
// import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
// import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||||
@@ -12,9 +401,31 @@
|
|||||||
// import '../../data/datasources/user_local_data_source.dart';
|
// import '../../data/datasources/user_local_data_source.dart';
|
||||||
// import '../../data/datasources/attendance_remote_data_source.dart';
|
// import '../../data/datasources/attendance_remote_data_source.dart';
|
||||||
|
|
||||||
// class AttendanceScreen extends StatelessWidget {
|
// class AttendanceScreen extends StatefulWidget {
|
||||||
// const AttendanceScreen({super.key});
|
// const AttendanceScreen({super.key});
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// State<AttendanceScreen> createState() => _AttendanceScreenState();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class _AttendanceScreenState extends State<AttendanceScreen> {
|
||||||
|
// String _userName = '';
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// void initState() {
|
||||||
|
// super.initState();
|
||||||
|
// _loadUserName();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Future<void> _loadUserName() async {
|
||||||
|
// final name = await sl<UserLocalDataSource>().getCachedFullName();
|
||||||
|
// if (mounted) {
|
||||||
|
// setState(() {
|
||||||
|
// _userName = name ?? 'مستخدم';
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// @override
|
// @override
|
||||||
// Widget build(BuildContext context) {
|
// Widget build(BuildContext context) {
|
||||||
// final screenWidth = MediaQuery.sizeOf(context).width;
|
// final screenWidth = MediaQuery.sizeOf(context).width;
|
||||||
@@ -38,14 +449,14 @@
|
|||||||
// Navigator.push(
|
// Navigator.push(
|
||||||
// context,
|
// context,
|
||||||
// MaterialPageRoute(
|
// MaterialPageRoute(
|
||||||
// builder: (context) => UserSettingsScreen(),
|
// builder: (context) => const UserSettingsScreen(),
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
// } else if (index == 1) {
|
// } else if (index == 1) {
|
||||||
// Navigator.push(
|
// Navigator.push(
|
||||||
// context,
|
// context,
|
||||||
// MaterialPageRoute(
|
// MaterialPageRoute(
|
||||||
// builder: (context) => NotificationsScreen(),
|
// builder: (context) => const NotificationsScreen(),
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
@@ -57,14 +468,12 @@
|
|||||||
// /// GREETING TEXT
|
// /// GREETING TEXT
|
||||||
// /// ------------------------------
|
// /// ------------------------------
|
||||||
// Positioned(
|
// Positioned(
|
||||||
// top:
|
// top: screenHeight * 0.14,
|
||||||
// screenHeight *
|
|
||||||
// 0.14, // moved down because settings bar now exists
|
|
||||||
// left: 0,
|
// left: 0,
|
||||||
// right: 0,
|
// right: 0,
|
||||||
// child: Center(
|
// child: Center(
|
||||||
// child: Text(
|
// child: Text(
|
||||||
// "صباح الخير, محمد",
|
// "صباح الخير $_userName",
|
||||||
// style: TextStyle(
|
// style: TextStyle(
|
||||||
// fontSize: 24,
|
// fontSize: 24,
|
||||||
// fontWeight: FontWeight.w600,
|
// fontWeight: FontWeight.w600,
|
||||||
@@ -79,9 +488,7 @@
|
|||||||
// /// MAIN CARD AREA
|
// /// MAIN CARD AREA
|
||||||
// /// ------------------------------
|
// /// ------------------------------
|
||||||
// Positioned(
|
// Positioned(
|
||||||
// top:
|
// top: screenHeight * 0.2,
|
||||||
// screenHeight *
|
|
||||||
// 0.2, // pushed down because of settings bar + greeting
|
|
||||||
// left: 0,
|
// left: 0,
|
||||||
// right: 0,
|
// right: 0,
|
||||||
// child: Center(
|
// child: Center(
|
||||||
@@ -95,20 +502,20 @@
|
|||||||
// decoration: BoxDecoration(
|
// decoration: BoxDecoration(
|
||||||
// borderRadius: BorderRadius.circular(32),
|
// borderRadius: BorderRadius.circular(32),
|
||||||
// boxShadow: [
|
// boxShadow: [
|
||||||
// BoxShadow(
|
// const BoxShadow(
|
||||||
// color: Color(0x1F2B2B2B),
|
// color: Color(0x1F2B2B2B),
|
||||||
// blurRadius: 5,
|
// blurRadius: 5,
|
||||||
// offset: Offset(10, -10),
|
// offset: Offset(10, -10),
|
||||||
// ),
|
// ),
|
||||||
// BoxShadow(
|
// const BoxShadow(
|
||||||
// color: Color(0xABCECECE),
|
// color: Color(0xABCECECE),
|
||||||
// blurRadius: 5,
|
// blurRadius: 5,
|
||||||
// offset: Offset(-2, 5),
|
// offset: Offset(-2, 5),
|
||||||
// ),
|
// ),
|
||||||
// BoxShadow(
|
// BoxShadow(
|
||||||
// color: Color.fromARGB(148, 2, 70, 35),
|
// color: const Color.fromARGB(148, 2, 70, 35),
|
||||||
// blurRadius: 80,
|
// blurRadius: 80,
|
||||||
// offset: Offset(0, 10),
|
// offset: const Offset(0, 10),
|
||||||
// ),
|
// ),
|
||||||
// ],
|
// ],
|
||||||
// ),
|
// ),
|
||||||
@@ -117,7 +524,7 @@
|
|||||||
// height: screenHeight * 0.5,
|
// height: screenHeight * 0.5,
|
||||||
// width: screenWidth * 0.7,
|
// width: screenWidth * 0.7,
|
||||||
// decoration: BoxDecoration(
|
// decoration: BoxDecoration(
|
||||||
// color: Color(0x92757575),
|
// color: const Color(0x92757575),
|
||||||
// borderRadius: BorderRadius.circular(32),
|
// borderRadius: BorderRadius.circular(32),
|
||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
@@ -135,7 +542,7 @@
|
|||||||
// left: screenWidth * 0.05,
|
// left: screenWidth * 0.05,
|
||||||
// child: _ShadowedCard(
|
// child: _ShadowedCard(
|
||||||
// shadow: [
|
// shadow: [
|
||||||
// BoxShadow(
|
// const BoxShadow(
|
||||||
// color: Color(0x62000000),
|
// color: Color(0x62000000),
|
||||||
// blurRadius: 10,
|
// blurRadius: 10,
|
||||||
// spreadRadius: 5,
|
// spreadRadius: 5,
|
||||||
@@ -160,18 +567,33 @@
|
|||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // ------------------------------
|
|
||||||
// // ACTIVE SESSION CHECK (LOGIN)
|
// // ACTIVE SESSION CHECK (LOGIN)
|
||||||
// // ------------------------------
|
|
||||||
// try {
|
// try {
|
||||||
// // Optional: Show a loading dialog if it takes too long
|
|
||||||
// final hasActive = await sl<AttendanceRemoteDataSource>()
|
// final hasActive = await sl<AttendanceRemoteDataSource>()
|
||||||
// .hasActiveLogin(employeeId: employeeId);
|
// .hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
// if (hasActive) {
|
// if (hasActive) {
|
||||||
// if (context.mounted) {
|
// if (context.mounted) {
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
// showDialog(
|
||||||
// const SnackBar(content: Text('أنت مسجل دخول بالفعل')),
|
// context: context,
|
||||||
|
// builder:
|
||||||
|
// (_) => AlertDialog(
|
||||||
|
// title: const Text(
|
||||||
|
// 'تنبيه',
|
||||||
|
// textAlign: TextAlign.center,
|
||||||
|
// ),
|
||||||
|
// content: const Text(
|
||||||
|
// 'أنت مسجل دخول بالفعل، لا يمكنك تسجيل الدخول مرة أخرى.',
|
||||||
|
// textAlign: TextAlign.center,
|
||||||
|
// ),
|
||||||
|
// actions: [
|
||||||
|
// TextButton(
|
||||||
|
// onPressed:
|
||||||
|
// () => Navigator.of(context).pop(),
|
||||||
|
// child: const Text('حسناً'),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
// return;
|
// return;
|
||||||
@@ -186,25 +608,51 @@
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// if (context.mounted) {
|
// if (context.mounted) {
|
||||||
// Navigator.of(context).push(
|
// final result = await Navigator.of(context).push(
|
||||||
// MaterialPageRoute(
|
// MaterialPageRoute(
|
||||||
// builder:
|
// builder:
|
||||||
// (_) => OvalCameraCapturePage(
|
// (_) => OvalCameraCapturePage(
|
||||||
// isLogin: true,
|
// isLogin: true,
|
||||||
// onCapture: (imageFile) async {
|
// onCapture: (
|
||||||
|
// imageFile, {
|
||||||
|
// required bool localAuth,
|
||||||
|
// }) async {
|
||||||
// final loginUseCase =
|
// final loginUseCase =
|
||||||
// sl<AttendanceLoginUsecase>();
|
// sl<AttendanceLoginUsecase>();
|
||||||
|
|
||||||
// await loginUseCase(
|
// await loginUseCase(
|
||||||
// AttendanceLoginRequest(
|
// AttendanceLoginRequest(
|
||||||
// employeeId: employeeId,
|
// employeeId: employeeId,
|
||||||
// faceImage: imageFile,
|
// faceImage: imageFile,
|
||||||
|
// localAuth: localAuth, // ✅ NEW FIELD
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
// },
|
// },
|
||||||
// checkIfLoggedIn: () {},
|
|
||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
// if (!context.mounted) return;
|
||||||
|
|
||||||
|
// if (result == "local_auth_failed") {
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// const SnackBar(
|
||||||
|
// content: Text("فشل التحقق بالبصمة أو رمز القفل"),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// } else if (result == "retry_failed") {
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// const SnackBar(
|
||||||
|
// content: Text("فشل تسجيل الحضور بعد التحقق المحلي"),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// } else if (result == "retry_missing_file") {
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// const SnackBar(
|
||||||
|
// content: Text("تعذر إعادة المحاولة. حاول مرة أخرى."),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
@@ -218,7 +666,7 @@
|
|||||||
// bottom: screenHeight * 0.2,
|
// bottom: screenHeight * 0.2,
|
||||||
// right: screenWidth * 0.1,
|
// right: screenWidth * 0.1,
|
||||||
// child: _ShadowedCard(
|
// child: _ShadowedCard(
|
||||||
// shadow: [
|
// shadow: const [
|
||||||
// BoxShadow(
|
// BoxShadow(
|
||||||
// color: Color(0xABCECECE),
|
// color: Color(0xABCECECE),
|
||||||
// blurRadius: 5,
|
// blurRadius: 5,
|
||||||
@@ -254,20 +702,32 @@
|
|||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // ------------------------------
|
|
||||||
// // ACTIVE SESSION CHECK (LOGOUT)
|
// // ACTIVE SESSION CHECK (LOGOUT)
|
||||||
// // ------------------------------
|
|
||||||
// try {
|
// try {
|
||||||
// final hasActive = await sl<AttendanceRemoteDataSource>()
|
// final hasActive = await sl<AttendanceRemoteDataSource>()
|
||||||
// .hasActiveLogin(employeeId: employeeId);
|
// .hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
// if (!hasActive) {
|
// if (!hasActive) {
|
||||||
// if (context.mounted) {
|
// if (context.mounted) {
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
// showDialog(
|
||||||
// const SnackBar(
|
// context: context,
|
||||||
// content: Text(
|
// builder:
|
||||||
// 'لا يوجد تسجيل دخول فعال لتسجيل الخروج',
|
// (_) => AlertDialog(
|
||||||
|
// title: const Text(
|
||||||
|
// 'تنبيه',
|
||||||
|
// textAlign: TextAlign.center,
|
||||||
// ),
|
// ),
|
||||||
|
// content: const Text(
|
||||||
|
// 'لا يوجد تسجيل دخول فعال لتسجيل الخروج.',
|
||||||
|
// textAlign: TextAlign.center,
|
||||||
|
// ),
|
||||||
|
// actions: [
|
||||||
|
// TextButton(
|
||||||
|
// onPressed:
|
||||||
|
// () => Navigator.of(context).pop(),
|
||||||
|
// child: const Text('حسناً'),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
@@ -298,7 +758,6 @@
|
|||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
// },
|
// },
|
||||||
// checkIfLoggedIn: () {},
|
|
||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
@@ -364,17 +823,17 @@
|
|||||||
// height: 160,
|
// height: 160,
|
||||||
// width: 160,
|
// width: 160,
|
||||||
// decoration: BoxDecoration(
|
// decoration: BoxDecoration(
|
||||||
// color: Color(0xFFEAFBF3),
|
// color: const Color(0xFFEAFBF3),
|
||||||
// borderRadius: BorderRadius.circular(32),
|
// borderRadius: BorderRadius.circular(32),
|
||||||
// ),
|
// ),
|
||||||
// child: Column(
|
// child: Column(
|
||||||
// mainAxisAlignment: MainAxisAlignment.center,
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
// children: [
|
// children: [
|
||||||
// SvgPicture.asset(icon, width: 75, height: 75),
|
// SvgPicture.asset(icon, width: 75, height: 75),
|
||||||
// SizedBox(height: 10),
|
// const SizedBox(height: 10),
|
||||||
// Text(
|
// Text(
|
||||||
// label,
|
// label,
|
||||||
// style: TextStyle(
|
// style: const TextStyle(
|
||||||
// fontSize: 18,
|
// fontSize: 18,
|
||||||
// fontWeight: FontWeight.w600,
|
// fontWeight: FontWeight.w600,
|
||||||
// color: Colors.black,
|
// color: Colors.black,
|
||||||
@@ -392,7 +851,9 @@ import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
|||||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
import '../widgets/settings_bar.dart';
|
import '../widgets/settings_bar.dart';
|
||||||
|
|
||||||
import '../../core/di/injection_container.dart';
|
import '../../core/di/injection_container.dart';
|
||||||
import '../../domain/models/attendance_login_request.dart';
|
import '../../domain/models/attendance_login_request.dart';
|
||||||
import '../../domain/models/attendance_logout_request.dart';
|
import '../../domain/models/attendance_logout_request.dart';
|
||||||
@@ -426,37 +887,46 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showResultSnack(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(message)));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||||
final screenHeight = MediaQuery.sizeOf(context).height;
|
final screenHeight = MediaQuery.sizeOf(context).height;
|
||||||
|
|
||||||
return Directionality(
|
return Directionality(
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: MediaQuery.of(context).size.height),
|
SizedBox(height: MediaQuery.of(context).size.height),
|
||||||
|
|
||||||
/// ------------------------------
|
/// SETTINGS BAR
|
||||||
/// SETTINGS BAR (STATIC)
|
|
||||||
/// ------------------------------
|
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: SettingsBar(
|
child: SettingsBar(
|
||||||
selectedIndex: 0,
|
selectedIndex: 0,
|
||||||
showBackButton: false,
|
showBackButton: false,
|
||||||
iconPaths: ['assets/images/user.svg', 'assets/images/ball.svg'],
|
iconPaths: const [
|
||||||
|
'assets/images/user.svg',
|
||||||
|
'assets/images/ball.svg',
|
||||||
|
],
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const UserSettingsScreen(),
|
builder: (_) => const UserSettingsScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (index == 1) {
|
} else if (index == 1) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const NotificationsScreen(),
|
builder: (_) => const NotificationsScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -464,9 +934,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// ------------------------------
|
/// GREETING
|
||||||
/// GREETING TEXT
|
|
||||||
/// ------------------------------
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: screenHeight * 0.14,
|
top: screenHeight * 0.14,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -474,7 +942,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"صباح الخير $_userName",
|
"صباح الخير $_userName",
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -484,9 +952,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// ------------------------------
|
|
||||||
/// MAIN CARD AREA
|
/// MAIN CARD AREA
|
||||||
/// ------------------------------
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: screenHeight * 0.2,
|
top: screenHeight * 0.2,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -534,15 +1000,13 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// ------------------------------
|
|
||||||
/// LOGIN BUTTON
|
/// LOGIN BUTTON
|
||||||
/// ------------------------------
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: screenHeight * 0.21,
|
top: screenHeight * 0.21,
|
||||||
left: screenWidth * 0.05,
|
left: screenWidth * 0.05,
|
||||||
child: _ShadowedCard(
|
child: _ShadowedCard(
|
||||||
shadow: [
|
shadow: const [
|
||||||
const BoxShadow(
|
BoxShadow(
|
||||||
color: Color(0x62000000),
|
color: Color(0x62000000),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
spreadRadius: 5,
|
spreadRadius: 5,
|
||||||
@@ -555,15 +1019,9 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
final employeeId =
|
final employeeId =
|
||||||
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
print("ATTENDANCE_SCREEN: Retrieved EmployeeId: $employeeId");
|
|
||||||
if (employeeId == null) {
|
if (employeeId == null) {
|
||||||
if (context.mounted) {
|
_showResultSnack('خطأ: لم يتم العثور على رقم الموظف');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,7 +1031,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
.hasActiveLogin(employeeId: employeeId);
|
.hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
if (hasActive) {
|
if (hasActive) {
|
||||||
if (context.mounted) {
|
if (!mounted) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
@@ -588,53 +1046,59 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
() => Navigator.of(context).pop(),
|
|
||||||
child: const Text('حسناً'),
|
child: const Text('حسناً'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
_showResultSnack('فشل التحقق من الجلسة: $e');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('فشل التحقق من الجلسة: $e')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (!mounted) return;
|
||||||
Navigator.of(context).push(
|
|
||||||
|
final result = await Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder:
|
builder:
|
||||||
(_) => OvalCameraCapturePage(
|
(_) => OvalCameraCapturePage(
|
||||||
isLogin: true,
|
isLogin: true,
|
||||||
onCapture: (imageFile) async {
|
onCapture: (
|
||||||
final loginUseCase =
|
imageFile, {
|
||||||
sl<AttendanceLoginUsecase>();
|
required bool localAuth,
|
||||||
|
}) async {
|
||||||
|
final loginUseCase = sl<AttendanceLoginUsecase>();
|
||||||
|
|
||||||
await loginUseCase(
|
await loginUseCase(
|
||||||
AttendanceLoginRequest(
|
AttendanceLoginRequest(
|
||||||
employeeId: employeeId,
|
employeeId: employeeId,
|
||||||
faceImage: imageFile,
|
faceImage: imageFile,
|
||||||
|
localAuth: localAuth, // ✅
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (result == "local_auth_failed") {
|
||||||
|
_showResultSnack("فشل التحقق بالبصمة أو رمز القفل");
|
||||||
|
} else if (result == "retry_failed") {
|
||||||
|
_showResultSnack("فشل تسجيل الحضور بعد التحقق المحلي");
|
||||||
|
} else if (result == "retry_missing_file") {
|
||||||
|
_showResultSnack("تعذر إعادة المحاولة. حاول مرة أخرى.");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// ------------------------------
|
|
||||||
/// LOGOUT BUTTON
|
/// LOGOUT BUTTON
|
||||||
/// ------------------------------
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: screenHeight * 0.2,
|
bottom: screenHeight * 0.2,
|
||||||
right: screenWidth * 0.1,
|
right: screenWidth * 0.1,
|
||||||
@@ -664,14 +1128,9 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
final employeeId =
|
final employeeId =
|
||||||
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
|
|
||||||
if (employeeId == null) {
|
if (employeeId == null) {
|
||||||
if (context.mounted) {
|
_showResultSnack('خطأ: لم يتم العثور على رقم الموظف');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +1140,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
.hasActiveLogin(employeeId: employeeId);
|
.hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
if (!hasActive) {
|
if (!hasActive) {
|
||||||
if (context.mounted) {
|
if (!mounted) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
@@ -696,44 +1155,53 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
() => Navigator.of(context).pop(),
|
|
||||||
child: const Text('حسناً'),
|
child: const Text('حسناً'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
_showResultSnack('فشل التحقق من الجلسة: $e');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('فشل التحقق من الجلسة: $e')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (!mounted) return;
|
||||||
Navigator.of(context).push(
|
|
||||||
|
final result = await Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder:
|
builder:
|
||||||
(_) => OvalCameraCapturePage(
|
(_) => OvalCameraCapturePage(
|
||||||
isLogin: false,
|
isLogin: false,
|
||||||
onCapture: (imageFile) async {
|
onCapture: (
|
||||||
|
imageFile, {
|
||||||
|
required bool localAuth,
|
||||||
|
}) async {
|
||||||
final logoutUseCase =
|
final logoutUseCase =
|
||||||
sl<AttendanceLogoutUseCase>();
|
sl<AttendanceLogoutUseCase>();
|
||||||
|
|
||||||
await logoutUseCase(
|
await logoutUseCase(
|
||||||
AttendanceLogoutRequest(
|
AttendanceLogoutRequest(
|
||||||
employeeId: employeeId,
|
employeeId: employeeId,
|
||||||
faceImage: imageFile,
|
faceImage: imageFile,
|
||||||
|
localAuth: localAuth, // ✅
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (result == "local_auth_failed") {
|
||||||
|
_showResultSnack("فشل التحقق بالبصمة أو رمز القفل");
|
||||||
|
} else if (result == "retry_failed") {
|
||||||
|
_showResultSnack("فشل تسجيل الخروج بعد التحقق المحلي");
|
||||||
|
} else if (result == "retry_missing_file") {
|
||||||
|
_showResultSnack("تعذر إعادة المحاولة. حاول مرة أخرى.");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -745,10 +1213,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ---------------------------------------------
|
|
||||||
/// SHADOW WRAPPER
|
/// SHADOW WRAPPER
|
||||||
/// ---------------------------------------------
|
|
||||||
|
|
||||||
class _ShadowedCard extends StatelessWidget {
|
class _ShadowedCard extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final List<BoxShadow> shadow;
|
final List<BoxShadow> shadow;
|
||||||
@@ -773,10 +1238,7 @@ class _ShadowedCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ---------------------------------------------
|
|
||||||
/// BUTTON WIDGET
|
/// BUTTON WIDGET
|
||||||
/// ---------------------------------------------
|
|
||||||
|
|
||||||
class _FingerButton extends StatelessWidget {
|
class _FingerButton extends StatelessWidget {
|
||||||
final String icon;
|
final String icon;
|
||||||
final String label;
|
final String label;
|
||||||
|
|||||||
@@ -20,7 +20,18 @@ class AuthScreen extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 60),
|
||||||
// Logo
|
// 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),
|
// const SizedBox(height: 15),
|
||||||
// Form - taking remaining space and centered
|
// Form - taking remaining space and centered
|
||||||
Expanded(child: Center(child: const AuthForm())),
|
Expanded(child: Center(child: const AuthForm())),
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
|||||||
|
|
||||||
import '../../core/error/exceptions.dart';
|
import '../../core/error/exceptions.dart';
|
||||||
import '../face/face_feedback.dart';
|
import '../face/face_feedback.dart';
|
||||||
|
import 'package:local_auth/local_auth.dart';
|
||||||
|
|
||||||
class OvalCameraCapturePage extends StatefulWidget {
|
class OvalCameraCapturePage extends StatefulWidget {
|
||||||
final bool isLogin;
|
final bool isLogin;
|
||||||
final Future<void> Function(File image) onCapture;
|
final Future<void> Function(File image, {required bool localAuth}) onCapture;
|
||||||
|
|
||||||
const OvalCameraCapturePage({
|
const OvalCameraCapturePage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -37,6 +38,13 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
bool _isStreaming = 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
|
// Smart feedback
|
||||||
FaceFeedback _feedback = FaceFeedback(
|
FaceFeedback _feedback = FaceFeedback(
|
||||||
type: FaceHintType.noFace,
|
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 {
|
Future<void> _stopImageStream() async {
|
||||||
if (!_isStreaming || _cameraController == null) return;
|
if (!_isStreaming || _cameraController == null) return;
|
||||||
try {
|
try {
|
||||||
@@ -408,7 +459,9 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
final xFile = await _cameraController!.takePicture();
|
final xFile = await _cameraController!.takePicture();
|
||||||
final file = File(xFile.path);
|
final file = File(xFile.path);
|
||||||
|
|
||||||
await widget.onCapture(file);
|
_lastCapturedFile = file;
|
||||||
|
|
||||||
|
await widget.onCapture(file, localAuth: false);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -423,8 +476,13 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} on ServerException catch (e) {
|
} on ServerException catch (e) {
|
||||||
// Check if this is an "already logged in" error from the API
|
|
||||||
final msg = e.message.toLowerCase();
|
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') ||
|
if (msg.contains('already logged in') ||
|
||||||
msg.contains('مسجل دخول بالفعل')) {
|
msg.contains('مسجل دخول بالفعل')) {
|
||||||
// Stop camera and go back with a dialog
|
// Stop camera and go back with a dialog
|
||||||
@@ -545,6 +603,86 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleFaceVerificationFailed422(ServerException e) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_handlingAuth422) return;
|
||||||
|
|
||||||
|
_handlingAuth422 = true;
|
||||||
|
|
||||||
|
// stop everything so camera doesn’t 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) {
|
Uint8List _convertYUV420ToNV21(CameraImage image) {
|
||||||
final int width = image.width;
|
final int width = image.width;
|
||||||
final int height = image.height;
|
final int height = image.height;
|
||||||
|
|||||||
@@ -88,7 +88,16 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 70),
|
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)
|
/// PAGEVIEW (SVG + TEXT ONLY)
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
final token = await sl<UserLocalDataSource>().getCachedUserToken();
|
final token = await sl<UserLocalDataSource>().getCachedUserToken();
|
||||||
|
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
// Token exists, navigate directly to MainPage
|
// Token exists — go to MainPage (theme already loaded in main.dart)
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const MainPage()),
|
MaterialPageRoute(builder: (_) => const MainPage()),
|
||||||
@@ -65,7 +65,18 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
fit: BoxFit.cover,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class SettingsBar extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.selectedIndex,
|
required this.selectedIndex,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.showBackButton = false, // to switch between back button and settings icons
|
this.showBackButton = false,
|
||||||
this.onBackTap,
|
this.onBackTap,
|
||||||
required this.iconPaths,
|
required this.iconPaths,
|
||||||
});
|
});
|
||||||
@@ -25,10 +25,15 @@ class SettingsBar extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
// Text Logo
|
||||||
children: [
|
const Text(
|
||||||
Image.asset('assets/images/logo2.png', width: 150, height: 40),
|
'LOGO',
|
||||||
],
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -50,11 +55,10 @@ class SettingsBar extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
// Always use Flutter's built-in back icon pointing to the right
|
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.arrow_forward, // Changed to arrow_forward for RTL
|
Icons.arrow_forward,
|
||||||
size: 26,
|
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) {
|
...iconPaths.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
final iconPath = entry.value;
|
final iconPath = entry.value;
|
||||||
// final isSelected = selectedIndex == index;
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 10),
|
padding: const EdgeInsets.only(left: 10),
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import local_auth_darwin
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
56
pubspec.lock
56
pubspec.lock
@@ -29,10 +29,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.12.0"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -189,10 +189,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.2"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -348,10 +348,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.9"
|
version: "10.0.8"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -376,6 +376,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
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:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -665,10 +705,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.0"
|
version: "14.3.1"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ dependencies:
|
|||||||
flutter_bloc: ^8.1.6
|
flutter_bloc: ^8.1.6
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
google_mlkit_face_detection: ^0.12.0
|
google_mlkit_face_detection: ^0.12.0
|
||||||
|
local_auth: ^2.1.8
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <local_auth_windows/local_auth_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
LocalAuthPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
local_auth_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
Reference in New Issue
Block a user