From 8b0d849f1bec37a8e1955e21f8794ae66aa30683 Mon Sep 17 00:00:00 2001 From: Daniah Ayad Al-sultani <148902945+Cactuskiller@users.noreply.github.com> Date: Sun, 22 Feb 2026 14:06:02 +0300 Subject: [PATCH] location has been enabled in addition to sending the domain --- android/app/src/main/AndroidManifest.xml | 2 + lib/core/config/app_urls.dart | 16 +++++ lib/core/di/injection_container.dart | 20 ++++++ lib/core/location/location_service.dart | 24 +++++++ .../attendance_remote_data_source.dart | 13 ++++ .../attendance_repository_impl.dart | 4 ++ .../repositories/theme_repository_impl.dart | 10 ++- .../models/attendance_login_request.dart | 6 ++ .../models/attendance_logout_request.dart | 6 ++ lib/domain/models/location_payload.dart | 8 +++ lib/domain/models/theme_model.dart | 9 ++- lib/main.dart | 9 ++- .../screens/attendence_screen.dart | 17 +++++ lib/presentation/screens/auth_screen.dart | 37 +++++++---- .../screens/onboarding_screen.dart | 34 +++++++--- lib/presentation/screens/splash_screen.dart | 35 +++++++--- lib/presentation/widgets/settings_bar.dart | 34 +++++++--- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 64 +++++++++++++++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 22 files changed, 312 insertions(+), 43 deletions(-) create mode 100644 lib/core/config/app_urls.dart create mode 100644 lib/core/location/location_service.dart create mode 100644 lib/domain/models/location_payload.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fdc9658..8ec0adf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + diff --git a/lib/core/config/app_urls.dart b/lib/core/config/app_urls.dart new file mode 100644 index 0000000..26977a3 --- /dev/null +++ b/lib/core/config/app_urls.dart @@ -0,0 +1,16 @@ +class AppUrls { + static const String themeLogoBase = 'https://hrm.go.iq/Images/'; + + static String buildThemeLogoUrl(String logoFileName) { + if (logoFileName.isEmpty) return ''; + + // If backend ever returns a full URL, keep it + final lower = logoFileName.toLowerCase(); + if (lower.startsWith('http://') || lower.startsWith('https://')) { + return logoFileName; + } + + // Encode spaces/special chars (important!) + return themeLogoBase + Uri.encodeComponent(logoFileName); + } +} diff --git a/lib/core/di/injection_container.dart b/lib/core/di/injection_container.dart index 47748ca..ab9825e 100644 --- a/lib/core/di/injection_container.dart +++ b/lib/core/di/injection_container.dart @@ -32,6 +32,12 @@ import '../../domain/usecases/get_salary_summary_usecase.dart'; import '../../domain/usecases/change_password_usecase.dart'; import '../../presentation/blocs/login/login_bloc.dart'; import '../../presentation/blocs/change_password/change_password_bloc.dart'; +import '../../data/datasources/theme_remote_data_source.dart'; +import '../../data/repositories/theme_repository_impl.dart'; +import '../../domain/repositories/theme_repository.dart'; +import '../../domain/usecases/get_theme_usecase.dart'; +import '../../presentation/blocs/theme/theme_cubit.dart'; +import '../location/location_service.dart'; final sl = GetIt.instance; @@ -124,4 +130,18 @@ Future initializeDependencies() async { sl.registerLazySingleton(() => CreateAdvanceUseCase(repository: sl())); sl.registerLazySingleton(() => GetAdvancesUseCase(repository: sl())); + + // Theme + sl.registerLazySingleton( + () => ThemeRemoteDataSourceImpl(apiClient: sl()), + ); + + sl.registerLazySingleton( + () => ThemeRepositoryImpl(remote: sl()), + ); + + sl.registerLazySingleton(() => GetThemeUseCase(sl())); + sl.registerFactory(() => ThemeCubit(getThemeUseCase: sl())); + + sl.registerLazySingleton(() => LocationService()); } diff --git a/lib/core/location/location_service.dart b/lib/core/location/location_service.dart new file mode 100644 index 0000000..df6a037 --- /dev/null +++ b/lib/core/location/location_service.dart @@ -0,0 +1,24 @@ +import 'package:geolocator/geolocator.dart'; + +class LocationService { + Future getCurrentPosition() async { + // 1) service enabled? + final serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) return null; + + // 2) permission + var permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + if (permission == LocationPermission.denied || + permission == LocationPermission.deniedForever) { + return null; + } + + // 3) get location (geolocator v13+ API) + return Geolocator.getCurrentPosition( + locationSettings: const LocationSettings(accuracy: LocationAccuracy.high), + ); + } +} diff --git a/lib/data/datasources/attendance_remote_data_source.dart b/lib/data/datasources/attendance_remote_data_source.dart index 69f3763..df284ca 100644 --- a/lib/data/datasources/attendance_remote_data_source.dart +++ b/lib/data/datasources/attendance_remote_data_source.dart @@ -14,12 +14,16 @@ abstract class AttendanceRemoteDataSource { required String employeeId, required File faceImage, bool localAuth = false, + double? latitude, + double? longitude, }); Future logout({ required String employeeId, required File faceImage, bool localAuth = false, + double? latitude, + double? longitude, }); Future> getAttendanceRecords({ @@ -46,12 +50,17 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource { required String employeeId, required File faceImage, bool localAuth = false, + double? latitude, + double? longitude, }) async { try { final formData = FormData.fromMap({ 'EmployeeId': employeeId, 'FaceImage': await MultipartFile.fromFile(faceImage.path), 'IsAuth': localAuth.toString(), + 'Domain': 'hrm.go.iq', + if (latitude != null) 'Latitude': latitude, + if (longitude != null) 'Longitude': longitude, }); final response = await apiClient.post( @@ -112,12 +121,16 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource { required String employeeId, required File faceImage, bool localAuth = false, + double? latitude, + double? longitude, }) async { try { final formData = FormData.fromMap({ 'EmployeeId': employeeId, 'FaceImage': await MultipartFile.fromFile(faceImage.path), 'IsAuth': localAuth.toString(), + if (latitude != null) 'Latitude': latitude, + if (longitude != null) 'Longitude': longitude, }); final response = await apiClient.post( diff --git a/lib/data/repositories/attendance_repository_impl.dart b/lib/data/repositories/attendance_repository_impl.dart index ada41e8..d0e6fac 100644 --- a/lib/data/repositories/attendance_repository_impl.dart +++ b/lib/data/repositories/attendance_repository_impl.dart @@ -19,6 +19,8 @@ class AttendanceRepositoryImpl implements AttendanceRepository { employeeId: request.employeeId, faceImage: request.faceImage, localAuth: request.localAuth, + latitude: request.latitude, + longitude: request.longitude, ); return AttendanceResponseModel( @@ -36,6 +38,8 @@ class AttendanceRepositoryImpl implements AttendanceRepository { employeeId: request.employeeId, faceImage: request.faceImage, localAuth: request.localAuth, + latitude: request.latitude, + longitude: request.longitude, ); return AttendanceResponseModel( diff --git a/lib/data/repositories/theme_repository_impl.dart b/lib/data/repositories/theme_repository_impl.dart index f728c65..221e4d3 100644 --- a/lib/data/repositories/theme_repository_impl.dart +++ b/lib/data/repositories/theme_repository_impl.dart @@ -1,4 +1,5 @@ import 'package:dartz/dartz.dart'; +import '../../core/config/app_urls.dart'; import '../../core/error/failures.dart'; import '../../core/error/exceptions.dart'; import '../../domain/models/theme_model.dart'; @@ -14,7 +15,14 @@ class ThemeRepositoryImpl implements ThemeRepository { Future> getTheme() async { try { final dto = await remote.getTheme(); - return Right(ThemeModel(name: dto.name, logo: dto.logo)); + + return Right( + ThemeModel( + name: dto.name, + logo: dto.logo, + logoUrl: AppUrls.buildThemeLogoUrl(dto.logo), + ), + ); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } catch (e) { diff --git a/lib/domain/models/attendance_login_request.dart b/lib/domain/models/attendance_login_request.dart index 06bdb9d..8cfdeea 100644 --- a/lib/domain/models/attendance_login_request.dart +++ b/lib/domain/models/attendance_login_request.dart @@ -5,9 +5,15 @@ class AttendanceLoginRequest { final File faceImage; final bool localAuth; + // ✅ NEW + final double? latitude; + final double? longitude; + AttendanceLoginRequest({ required this.employeeId, required this.faceImage, this.localAuth = false, + this.latitude, + this.longitude, }); } diff --git a/lib/domain/models/attendance_logout_request.dart b/lib/domain/models/attendance_logout_request.dart index ce602cd..59570bf 100644 --- a/lib/domain/models/attendance_logout_request.dart +++ b/lib/domain/models/attendance_logout_request.dart @@ -5,9 +5,15 @@ class AttendanceLogoutRequest { final File faceImage; final bool localAuth; + // ✅ NEW + final double? latitude; + final double? longitude; + AttendanceLogoutRequest({ required this.employeeId, required this.faceImage, this.localAuth = false, + this.latitude, + this.longitude, }); } diff --git a/lib/domain/models/location_payload.dart b/lib/domain/models/location_payload.dart new file mode 100644 index 0000000..cf74c70 --- /dev/null +++ b/lib/domain/models/location_payload.dart @@ -0,0 +1,8 @@ +class LocationPayload { + final double lat; + final double lng; + + const LocationPayload({required this.lat, required this.lng}); + + Map toJson() => {'latitude': lat, 'longitude': lng}; +} diff --git a/lib/domain/models/theme_model.dart b/lib/domain/models/theme_model.dart index dd5d3cb..5eeb4f9 100644 --- a/lib/domain/models/theme_model.dart +++ b/lib/domain/models/theme_model.dart @@ -1,6 +1,11 @@ class ThemeModel { final String name; - final String logo; // filename or url + final String logo; + final String logoUrl; - const ThemeModel({required this.name, required this.logo}); + const ThemeModel({ + required this.name, + required this.logo, + required this.logoUrl, + }); } diff --git a/lib/main.dart b/lib/main.dart index 0b14721..a1421b6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'core/di/injection_container.dart'; +import 'presentation/blocs/theme/theme_cubit.dart'; import 'presentation/screens/splash_screen.dart'; void main() async { @@ -12,7 +14,12 @@ void main() async { // Initialize dependency injection await initializeDependencies(); - runApp(const CodaApp()); + runApp( + BlocProvider( + create: (_) => sl()..loadTheme(), + child: const CodaApp(), + ), + ); } catch (e) { debugPrint('CRITICAL INITIALIZATION ERROR: $e'); // If initialization fails, show a simple error screen instead of a broken app diff --git a/lib/presentation/screens/attendence_screen.dart b/lib/presentation/screens/attendence_screen.dart index 798fc84..80bebd0 100644 --- a/lib/presentation/screens/attendence_screen.dart +++ b/lib/presentation/screens/attendence_screen.dart @@ -855,6 +855,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import '../widgets/settings_bar.dart'; import '../../core/di/injection_container.dart'; +import '../../core/location/location_service.dart'; import '../../domain/models/attendance_login_request.dart'; import '../../domain/models/attendance_logout_request.dart'; import '../../domain/usecases/attendance_login_usecase.dart'; @@ -1061,6 +1062,12 @@ class _AttendanceScreenState extends State { if (!mounted) return; + // Fetch device location + final position = + await sl().getCurrentPosition(); + + if (!mounted) return; + final result = await Navigator.of(context).push( MaterialPageRoute( builder: @@ -1077,6 +1084,8 @@ class _AttendanceScreenState extends State { employeeId: employeeId, faceImage: imageFile, localAuth: localAuth, // ✅ + latitude: position?.latitude, + longitude: position?.longitude, ), ); }, @@ -1170,6 +1179,12 @@ class _AttendanceScreenState extends State { if (!mounted) return; + // Fetch device location + final position = + await sl().getCurrentPosition(); + + if (!mounted) return; + final result = await Navigator.of(context).push( MaterialPageRoute( builder: @@ -1187,6 +1202,8 @@ class _AttendanceScreenState extends State { employeeId: employeeId, faceImage: imageFile, localAuth: localAuth, // ✅ + latitude: position?.latitude, + longitude: position?.longitude, ), ); }, diff --git a/lib/presentation/screens/auth_screen.dart b/lib/presentation/screens/auth_screen.dart index 5cf7dc1..95ed2d3 100644 --- a/lib/presentation/screens/auth_screen.dart +++ b/lib/presentation/screens/auth_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/theme/theme_cubit.dart'; +import '../blocs/theme/theme_state.dart'; import '../widgets/app_background.dart'; import '../widgets/auth_form.dart'; import '../../core/di/injection_container.dart'; @@ -19,17 +21,30 @@ class AuthScreen extends StatelessWidget { child: Column( children: [ const SizedBox(height: 60), - // Logo - // 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, - ), + // Dynamic Logo from backend + Center( + child: BlocBuilder( + builder: (context, state) { + if (state is ThemeLoaded) { + return Image.network( + state.logoUrl, + width: 62, + height: 62, + errorBuilder: + (_, __, ___) => + const Icon(Icons.image_not_supported), + ); + } + return const Text( + 'LOGO', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 2, + ), + ); + }, ), ), // const SizedBox(height: 15), diff --git a/lib/presentation/screens/onboarding_screen.dart b/lib/presentation/screens/onboarding_screen.dart index 4bef705..dfdd9d5 100644 --- a/lib/presentation/screens/onboarding_screen.dart +++ b/lib/presentation/screens/onboarding_screen.dart @@ -1,6 +1,9 @@ import 'dart:async'; import 'package:coda_project/presentation/screens/auth_screen.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/theme/theme_cubit.dart'; +import '../blocs/theme/theme_state.dart'; import '../widgets/onboarding_page.dart'; import '../widgets/onboarding_button.dart'; @@ -88,15 +91,28 @@ class _OnboardingScreenState extends State { Column( children: [ const SizedBox(height: 70), - // Image.asset("assets/images/logo2.png", width: 200), - const Text( - 'LOGO', - style: TextStyle( - fontSize: 36, - fontWeight: FontWeight.bold, - color: Colors.white, - letterSpacing: 2, - ), + // Dynamic Logo from backend + BlocBuilder( + builder: (context, state) { + if (state is ThemeLoaded) { + return Image.network( + state.logoUrl, + width: 62, + height: 62, + errorBuilder: + (_, __, ___) => const Icon(Icons.image_not_supported), + ); + } + return const Text( + 'LOGO', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 2, + ), + ); + }, ), /// PAGEVIEW (SVG + TEXT ONLY) diff --git a/lib/presentation/screens/splash_screen.dart b/lib/presentation/screens/splash_screen.dart index fa4d2cb..c55399c 100644 --- a/lib/presentation/screens/splash_screen.dart +++ b/lib/presentation/screens/splash_screen.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import '../blocs/theme/theme_cubit.dart'; +import '../blocs/theme/theme_state.dart'; import 'onboarding_screen.dart'; import 'main_screen.dart'; import '../../core/di/injection_container.dart'; @@ -65,16 +68,28 @@ class _SplashScreenState extends State { fit: BoxFit.cover, ), ), - // 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, - ), + child: Center( + child: BlocBuilder( + builder: (context, state) { + if (state is ThemeLoaded) { + return Image.network( + state.logoUrl, + width: 62, + height: 62, + errorBuilder: + (_, __, ___) => const Icon(Icons.image_not_supported), + ); + } + return const Text( + 'LOGO', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 2, + ), + ); + }, ), ), ), diff --git a/lib/presentation/widgets/settings_bar.dart b/lib/presentation/widgets/settings_bar.dart index 2208f8f..1e0a724 100644 --- a/lib/presentation/widgets/settings_bar.dart +++ b/lib/presentation/widgets/settings_bar.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../blocs/theme/theme_cubit.dart'; +import '../blocs/theme/theme_state.dart'; class SettingsBar extends StatelessWidget { final int selectedIndex; @@ -25,15 +28,28 @@ class SettingsBar extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Text Logo - const Text( - 'LOGO', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - letterSpacing: 2, - ), + // Dynamic Logo from backend + BlocBuilder( + builder: (context, state) { + if (state is ThemeLoaded) { + return Image.network( + state.logoUrl, + width: 62, + height: 62, + errorBuilder: + (_, __, ___) => const Icon(Icons.image_not_supported), + ); + } + return const Text( + 'LOGO', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 2, + ), + ); + }, ), Row( children: [ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 80dc39e..f472dac 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import geolocator_apple import local_auth_darwin import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 0d99a82..74d1cf1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -272,6 +280,54 @@ packages: description: flutter source: sdk version: "0.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2 + url: "https://pub.dev" + source: hosted + version: "13.0.4" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" get_it: dependency: "direct main" description: @@ -669,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 802d696..5443aab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: intl: ^0.19.0 google_mlkit_face_detection: ^0.12.0 local_auth: ^2.1.8 + geolocator: ^13.0.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 7407ddd..edead97 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ef187dc..9fdd49c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + geolocator_windows local_auth_windows )