diff --git a/lib/core/di/injection_container.dart b/lib/core/di/injection_container.dart index 5275920..47748ca 100644 --- a/lib/core/di/injection_container.dart +++ b/lib/core/di/injection_container.dart @@ -28,7 +28,10 @@ import '../../domain/usecases/get_advances_usecase.dart'; import '../../domain/usecases/get_extra_hours_usecase.dart'; import '../../domain/usecases/get_rewards_usecase.dart'; import '../../domain/usecases/get_punishments_usecase.dart'; +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'; final sl = GetIt.instance; @@ -61,9 +64,11 @@ Future initializeDependencies() async { // Use cases sl.registerLazySingleton(() => LoginUseCase(repository: sl())); + sl.registerLazySingleton(() => ChangePasswordUseCase(repository: sl())); // Blocs sl.registerFactory(() => LoginBloc(loginUseCase: sl())); + sl.registerFactory(() => ChangePasswordBloc(changePasswordUseCase: sl())); //Attendence sl.registerLazySingleton( @@ -82,6 +87,7 @@ Future initializeDependencies() async { sl.registerLazySingleton(() => GetExtraHoursUseCase(repository: sl())); sl.registerLazySingleton(() => GetRewardsUseCase(repository: sl())); sl.registerLazySingleton(() => GetPunishmentsUseCase(repository: sl())); + sl.registerLazySingleton(() => GetSalarySummaryUseCase(sl())); // Finance sl.registerFactory( @@ -90,6 +96,7 @@ Future initializeDependencies() async { getExtraHoursUseCase: sl(), getRewardsUseCase: sl(), getPunishmentsUseCase: sl(), + getSalarySummaryUseCase: sl(), ), ); diff --git a/lib/core/error/exceptions.dart b/lib/core/error/exceptions.dart index 2ced2ac..0b89683 100644 --- a/lib/core/error/exceptions.dart +++ b/lib/core/error/exceptions.dart @@ -3,6 +3,9 @@ class ServerException implements Exception { final int? statusCode; ServerException({required this.message, this.statusCode}); + + @override + String toString() => 'ServerException: $message (Status: $statusCode)'; } class NetworkException implements Exception { diff --git a/lib/data/datasources/attendance_remote_data_source.dart b/lib/data/datasources/attendance_remote_data_source.dart index 7e7c31a..ad1dc23 100644 --- a/lib/data/datasources/attendance_remote_data_source.dart +++ b/lib/data/datasources/attendance_remote_data_source.dart @@ -7,6 +7,7 @@ import '../dto/attendance_record_dto.dart'; import '../dto/overtime_dto.dart'; import '../dto/reward_dto.dart'; import '../dto/punishment_dto.dart'; +import '../dto/salary_response_dto.dart'; abstract class AttendanceRemoteDataSource { Future login({ @@ -25,6 +26,11 @@ abstract class AttendanceRemoteDataSource { Future> getExtraHours({required String employeeId}); Future> getRewards({required String employeeId}); Future> getPunishments({required String employeeId}); + Future calculateSalary({ + required String employeeId, + required int month, + required int year, + }); } class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource { @@ -286,4 +292,66 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource { throw ServerException(message: 'خطأ غير متوقع'); } } + + @override + Future calculateSalary({ + required String employeeId, + required int month, + required int year, + }) async { + try { + final response = await apiClient.get( + '/SalaryRecord/calculate', + queryParameters: { + 'EmployeeId': employeeId, + 'Month': month, + 'Year': year, + }, + ); + + print('Salary Response Status: ${response.statusCode}'); + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = response.data; + print( + 'Salary Response Data: $responseData (${responseData.runtimeType})', + ); + + if (responseData is Map) { + return SalaryResponseDto.fromJson(responseData); + } else if (responseData is num) { + // Handle case where API returns raw number + return SalaryResponseDto( + isSuccess: true, + message: 'Success', + data: SalaryDataDto(netAmount: responseData.toDouble()), + ); + } else if (responseData is String && + double.tryParse(responseData) != null) { + // Handle case where API returns raw numeric string + return SalaryResponseDto( + isSuccess: true, + message: 'Success', + data: SalaryDataDto(netAmount: double.parse(responseData)), + ); + } else { + throw ServerException( + message: 'استجابة غير صحيحة من الخادم: $responseData', + statusCode: response.statusCode, + ); + } + } else { + throw ServerException( + message: 'فشل في حساب الراتب (Status: ${response.statusCode})', + statusCode: response.statusCode, + ); + } + } on DioException catch (e) { + throw ServerException( + message: e.message ?? 'Unknown error', + statusCode: e.response?.statusCode, + ); + } catch (e) { + throw ServerException(message: 'خطأ غير متوقع'); + } + } } diff --git a/lib/data/datasources/auth_remote_data_source.dart b/lib/data/datasources/auth_remote_data_source.dart index e207113..ee25470 100644 --- a/lib/data/datasources/auth_remote_data_source.dart +++ b/lib/data/datasources/auth_remote_data_source.dart @@ -3,9 +3,14 @@ import '../../core/error/exceptions.dart'; import '../../core/network/api_client.dart'; import '../dto/login_dto.dart'; import '../dto/login_response_dto.dart'; +import '../dto/change_password_request_dto.dart'; +import '../dto/change_password_response_dto.dart'; abstract class AuthRemoteDataSource { Future login(LoginDto dto); + Future changePassword( + ChangePasswordRequestDto dto, + ); } class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { @@ -72,4 +77,37 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { throw ServerException(message: 'خطأ غير متوقع'); } } + + @override + Future changePassword( + ChangePasswordRequestDto dto, + ) async { + try { + final response = await apiClient.post( + '/Auth/change-password', + data: dto.toJson(), + ); + + if (response.statusCode == 200) { + return ChangePasswordResponseDto.fromJson(response.data); + } else { + throw ServerException( + message: response.data['message'] ?? 'فشل تغيير كلمة المرور', + statusCode: response.statusCode, + ); + } + } on DioException catch (e) { + final message = + e.response?.data?['message'] ?? + e.response?.data?['error'] ?? + 'فشل تغيير كلمة المرور'; + throw ServerException( + message: message, + statusCode: e.response?.statusCode, + ); + } catch (e) { + if (e is ServerException) rethrow; + throw ServerException(message: 'خطأ غير متوقع: $e'); + } + } } diff --git a/lib/data/dto/change_password_request_dto.dart b/lib/data/dto/change_password_request_dto.dart new file mode 100644 index 0000000..ecfc6ad --- /dev/null +++ b/lib/data/dto/change_password_request_dto.dart @@ -0,0 +1,13 @@ +class ChangePasswordRequestDto { + final String oldPassword; + final String newPassword; + + ChangePasswordRequestDto({ + required this.oldPassword, + required this.newPassword, + }); + + Map toJson() { + return {'oldPassword': oldPassword, 'newPassword': newPassword}; + } +} diff --git a/lib/data/dto/change_password_response_dto.dart b/lib/data/dto/change_password_response_dto.dart new file mode 100644 index 0000000..769902a --- /dev/null +++ b/lib/data/dto/change_password_response_dto.dart @@ -0,0 +1,36 @@ +import 'login_response_dto.dart'; + +class ChangePasswordResponseDto { + final int statusCode; + final bool isSuccess; + final String message; + final LoginDataDto? data; + + ChangePasswordResponseDto({ + required this.statusCode, + required this.isSuccess, + required this.message, + this.data, + }); + + factory ChangePasswordResponseDto.fromJson(Map json) { + return ChangePasswordResponseDto( + statusCode: json['statusCode'] ?? 0, + isSuccess: json['isSuccess'] ?? false, + message: json['message'] ?? '', + data: + json['data'] != null && json['data'] is Map + ? LoginDataDto.fromJson(json['data']) + : null, + ); + } + + Map toJson() { + return { + 'statusCode': statusCode, + 'isSuccess': isSuccess, + 'message': message, + 'data': data?.toJson(), + }; + } +} diff --git a/lib/data/dto/salary_response_dto.dart b/lib/data/dto/salary_response_dto.dart new file mode 100644 index 0000000..9843225 --- /dev/null +++ b/lib/data/dto/salary_response_dto.dart @@ -0,0 +1,36 @@ +class SalaryResponseDto { + final bool isSuccess; + final String message; + final SalaryDataDto? data; + + SalaryResponseDto({ + required this.isSuccess, + required this.message, + this.data, + }); + + factory SalaryResponseDto.fromJson(Map json) { + return SalaryResponseDto( + isSuccess: json['isSuccess'] ?? json['IsSuccess'] ?? false, + message: json['message'] ?? json['Message'] ?? '', + data: + json['data'] != null + ? SalaryDataDto.fromJson(json['data']) + : json['Data'] != null + ? SalaryDataDto.fromJson(json['Data']) + : null, + ); + } +} + +class SalaryDataDto { + final double netAmount; + + SalaryDataDto({required this.netAmount}); + + factory SalaryDataDto.fromJson(Map json) { + return SalaryDataDto( + netAmount: (json['netAmount'] ?? json['NetAmount'] ?? 0.0).toDouble(), + ); + } +} diff --git a/lib/data/repositories/attendance_repository_impl.dart b/lib/data/repositories/attendance_repository_impl.dart index eabcf83..09abefa 100644 --- a/lib/data/repositories/attendance_repository_impl.dart +++ b/lib/data/repositories/attendance_repository_impl.dart @@ -4,6 +4,7 @@ import '../../domain/models/attendance_response_model.dart'; import '../../domain/models/attendance_model.dart'; import '../../domain/models/overtime_model.dart'; import '../../domain/models/extra_payment_model.dart'; +import '../../domain/models/salary_model.dart'; import '../../domain/repositories/attendance_repository.dart'; import '../datasources/attendance_remote_data_source.dart'; @@ -131,4 +132,19 @@ class AttendanceRepositoryImpl implements AttendanceRepository { ) .toList(); } + + @override + Future calculateSalary({ + required String employeeId, + required int month, + required int year, + }) async { + final dto = await remoteDataSource.calculateSalary( + employeeId: employeeId, + month: month, + year: year, + ); + + return SalaryModel(netAmount: dto.data?.netAmount ?? 0.0); + } } diff --git a/lib/data/repositories/auth_repository_impl.dart b/lib/data/repositories/auth_repository_impl.dart index 5836e01..3e2e659 100644 --- a/lib/data/repositories/auth_repository_impl.dart +++ b/lib/data/repositories/auth_repository_impl.dart @@ -8,6 +8,10 @@ import '../../domain/models/login_request.dart'; import '../../domain/models/login_response_model.dart'; import '../../domain/repositories/auth_repository.dart'; +import '../dto/change_password_request_dto.dart'; +import '../../domain/models/change_password_request.dart'; +import '../../domain/models/general_response_model.dart'; + class AuthRepositoryImpl implements AuthRepository { final AuthRemoteDataSource remoteDataSource; final UserLocalDataSource localDataSource; @@ -71,4 +75,45 @@ class AuthRepositoryImpl implements AuthRepository { return Left(ServerFailure('خطأ غير متوقع: $e')); } } + + @override + Future> changePassword( + ChangePasswordRequest request, + ) async { + try { + final dto = ChangePasswordRequestDto( + oldPassword: request.oldPassword, + newPassword: request.newPassword, + ); + + final responseDto = await remoteDataSource.changePassword(dto); + + // Re-cache token and employeeId if provided in response + if (responseDto.data != null) { + if (responseDto.data!.token != null) { + await localDataSource.cacheUserToken(responseDto.data!.token!); + } + if (responseDto.data!.employeeId != null) { + await localDataSource.cacheEmployeeId(responseDto.data!.employeeId!); + } + } + + final model = GeneralResponseModel( + statusCode: responseDto.statusCode, + isSuccess: responseDto.isSuccess, + message: responseDto.message, + data: + responseDto + .isSuccess, // Use isSuccess as data for the boolean model + ); + + return Right(model); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return Left(NetworkFailure(e.message)); + } catch (e) { + return Left(ServerFailure('خطأ غير متوقع: $e')); + } + } } diff --git a/lib/domain/models/change_password_request.dart b/lib/domain/models/change_password_request.dart new file mode 100644 index 0000000..e9a4b4c --- /dev/null +++ b/lib/domain/models/change_password_request.dart @@ -0,0 +1,6 @@ +class ChangePasswordRequest { + final String oldPassword; + final String newPassword; + + ChangePasswordRequest({required this.oldPassword, required this.newPassword}); +} diff --git a/lib/domain/models/general_response_model.dart b/lib/domain/models/general_response_model.dart new file mode 100644 index 0000000..873a9f0 --- /dev/null +++ b/lib/domain/models/general_response_model.dart @@ -0,0 +1,13 @@ +class GeneralResponseModel { + final int statusCode; + final bool isSuccess; + final String message; + final bool? data; + + GeneralResponseModel({ + required this.statusCode, + required this.isSuccess, + required this.message, + this.data, + }); +} diff --git a/lib/domain/models/salary_model.dart b/lib/domain/models/salary_model.dart new file mode 100644 index 0000000..999397d --- /dev/null +++ b/lib/domain/models/salary_model.dart @@ -0,0 +1,4 @@ +class SalaryModel { + final double netAmount; + SalaryModel({required this.netAmount}); +} diff --git a/lib/domain/repositories/attendance_repository.dart b/lib/domain/repositories/attendance_repository.dart index a16e9a3..e93b254 100644 --- a/lib/domain/repositories/attendance_repository.dart +++ b/lib/domain/repositories/attendance_repository.dart @@ -4,6 +4,7 @@ import '../models/attendance_response_model.dart'; import '../models/attendance_model.dart'; import '../models/overtime_model.dart'; import '../models/extra_payment_model.dart'; +import '../models/salary_model.dart'; //in the following polymorphism is being used , a quich recap it is where th esame method but opperate in a different way @@ -19,4 +20,9 @@ abstract class AttendanceRepository { Future> getExtraHours({required String employeeId}); Future> getRewards({required String employeeId}); Future> getPunishments({required String employeeId}); + Future calculateSalary({ + required String employeeId, + required int month, + required int year, + }); } diff --git a/lib/domain/repositories/auth_repository.dart b/lib/domain/repositories/auth_repository.dart index e5a101e..a3cf75d 100644 --- a/lib/domain/repositories/auth_repository.dart +++ b/lib/domain/repositories/auth_repository.dart @@ -2,7 +2,12 @@ import 'package:dartz/dartz.dart'; import '../../core/error/failures.dart'; import '../models/login_request.dart'; import '../models/login_response_model.dart'; +import '../models/change_password_request.dart'; +import '../models/general_response_model.dart'; abstract class AuthRepository { Future> login(LoginRequest request); + Future> changePassword( + ChangePasswordRequest request, + ); } diff --git a/lib/domain/usecases/change_password_usecase.dart b/lib/domain/usecases/change_password_usecase.dart new file mode 100644 index 0000000..256e600 --- /dev/null +++ b/lib/domain/usecases/change_password_usecase.dart @@ -0,0 +1,17 @@ +import 'package:dartz/dartz.dart'; +import '../../core/error/failures.dart'; +import '../models/change_password_request.dart'; +import '../models/general_response_model.dart'; +import '../repositories/auth_repository.dart'; + +class ChangePasswordUseCase { + final AuthRepository repository; + + ChangePasswordUseCase({required this.repository}); + + Future> call( + ChangePasswordRequest request, + ) async { + return await repository.changePassword(request); + } +} diff --git a/lib/domain/usecases/get_salary_summary_usecase.dart b/lib/domain/usecases/get_salary_summary_usecase.dart new file mode 100644 index 0000000..c0ad114 --- /dev/null +++ b/lib/domain/usecases/get_salary_summary_usecase.dart @@ -0,0 +1,20 @@ +import '../repositories/attendance_repository.dart'; +import '../models/salary_model.dart'; + +class GetSalarySummaryUseCase { + final AttendanceRepository repository; + + GetSalarySummaryUseCase(this.repository); + + Future execute({ + required String employeeId, + required int month, + required int year, + }) { + return repository.calculateSalary( + employeeId: employeeId, + month: month, + year: year, + ); + } +} diff --git a/lib/presentation/bloc/finance_bloc.dart b/lib/presentation/bloc/finance_bloc.dart index 9e14a7c..ef670c7 100644 --- a/lib/presentation/bloc/finance_bloc.dart +++ b/lib/presentation/bloc/finance_bloc.dart @@ -3,6 +3,7 @@ import '../../../domain/usecases/get_attendance_records_usecase.dart'; import '../../../domain/usecases/get_extra_hours_usecase.dart'; import '../../../domain/usecases/get_rewards_usecase.dart'; import '../../../domain/usecases/get_punishments_usecase.dart'; +import '../../../domain/usecases/get_salary_summary_usecase.dart'; import '../../../domain/models/finance_record.dart'; import '../../../domain/models/finance_category.dart'; import '../../../core/error/exceptions.dart'; @@ -13,8 +14,15 @@ abstract class FinanceEvent {} class LoadFinanceDataEvent extends FinanceEvent { final String employeeId; final FinanceCategory category; + final int? month; + final int? year; - LoadFinanceDataEvent({required this.employeeId, required this.category}); + LoadFinanceDataEvent({ + required this.employeeId, + required this.category, + this.month, + this.year, + }); } // States @@ -27,8 +35,13 @@ class FinanceLoading extends FinanceState {} class FinanceLoaded extends FinanceState { final List records; final FinanceCategory category; + final double netSalary; - FinanceLoaded({required this.records, required this.category}); + FinanceLoaded({ + required this.records, + required this.category, + this.netSalary = 0.0, + }); } class FinanceError extends FinanceState { @@ -43,12 +56,14 @@ class FinanceBloc extends Bloc { final GetExtraHoursUseCase getExtraHoursUseCase; final GetRewardsUseCase getRewardsUseCase; final GetPunishmentsUseCase getPunishmentsUseCase; + final GetSalarySummaryUseCase getSalarySummaryUseCase; FinanceBloc({ required this.getAttendanceRecordsUseCase, required this.getExtraHoursUseCase, required this.getRewardsUseCase, required this.getPunishmentsUseCase, + required this.getSalarySummaryUseCase, }) : super(FinanceInitial()) { on(_onLoadFinanceData); } @@ -64,6 +79,27 @@ class FinanceBloc extends Bloc { emit(FinanceLoading()); try { + final now = DateTime.now(); + final month = event.month ?? now.month; + final year = event.year ?? now.year; + double netSalary = 0.0; + + print( + 'Fetching salary for Employee: ${event.employeeId}, Month: $month, Year: $year', + ); + try { + final salaryModel = await getSalarySummaryUseCase.execute( + employeeId: event.employeeId, + month: month, + year: year, + ); + print('Salary Model fetched: ${salaryModel.netAmount}'); + netSalary = salaryModel.netAmount; + } catch (e) { + print('Error fetching salary: $e'); + // We continue even if salary fails, to show the list at least + } + List records; switch (event.category) { case FinanceCategory.attendance: @@ -87,7 +123,13 @@ class FinanceBloc extends Bloc { ); break; } - emit(FinanceLoaded(records: records, category: event.category)); + emit( + FinanceLoaded( + records: records, + category: event.category, + netSalary: netSalary, + ), + ); } on ServerException catch (e) { emit(FinanceError(message: e.message)); } on NetworkException catch (e) { diff --git a/lib/presentation/blocs/change_password/change_password_bloc.dart b/lib/presentation/blocs/change_password/change_password_bloc.dart new file mode 100644 index 0000000..54f680e --- /dev/null +++ b/lib/presentation/blocs/change_password/change_password_bloc.dart @@ -0,0 +1,41 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../domain/usecases/change_password_usecase.dart'; +import 'change_password_event.dart'; +import 'change_password_state.dart'; + +class ChangePasswordBloc + extends Bloc { + final ChangePasswordUseCase changePasswordUseCase; + + ChangePasswordBloc({required this.changePasswordUseCase}) + : super(const ChangePasswordInitial()) { + on(_onChangePasswordSubmitted); + on(_onChangePasswordReset); + } + + Future _onChangePasswordSubmitted( + ChangePasswordSubmitted event, + Emitter emit, + ) async { + emit(const ChangePasswordLoading()); + + final result = await changePasswordUseCase(event.request); + + result.fold((failure) => emit(ChangePasswordError(failure.message)), ( + response, + ) { + if (response.isSuccess) { + emit(ChangePasswordSuccess(response.message)); + } else { + emit(ChangePasswordError(response.message)); + } + }); + } + + void _onChangePasswordReset( + ChangePasswordReset event, + Emitter emit, + ) { + emit(const ChangePasswordInitial()); + } +} diff --git a/lib/presentation/blocs/change_password/change_password_event.dart b/lib/presentation/blocs/change_password/change_password_event.dart new file mode 100644 index 0000000..d202633 --- /dev/null +++ b/lib/presentation/blocs/change_password/change_password_event.dart @@ -0,0 +1,11 @@ +import '../../../domain/models/change_password_request.dart'; + +abstract class ChangePasswordEvent {} + +class ChangePasswordSubmitted extends ChangePasswordEvent { + final ChangePasswordRequest request; + + ChangePasswordSubmitted(this.request); +} + +class ChangePasswordReset extends ChangePasswordEvent {} diff --git a/lib/presentation/blocs/change_password/change_password_state.dart b/lib/presentation/blocs/change_password/change_password_state.dart new file mode 100644 index 0000000..28151fd --- /dev/null +++ b/lib/presentation/blocs/change_password/change_password_state.dart @@ -0,0 +1,23 @@ +abstract class ChangePasswordState { + const ChangePasswordState(); +} + +class ChangePasswordInitial extends ChangePasswordState { + const ChangePasswordInitial(); +} + +class ChangePasswordLoading extends ChangePasswordState { + const ChangePasswordLoading(); +} + +class ChangePasswordSuccess extends ChangePasswordState { + final String message; + + const ChangePasswordSuccess(this.message); +} + +class ChangePasswordError extends ChangePasswordState { + final String message; + + const ChangePasswordError(this.message); +} diff --git a/lib/presentation/screens/finance_screen.dart b/lib/presentation/screens/finance_screen.dart index 751bf22..c621642 100644 --- a/lib/presentation/screens/finance_screen.dart +++ b/lib/presentation/screens/finance_screen.dart @@ -22,18 +22,22 @@ class FinanceScreen extends StatefulWidget { class _FinanceScreenState extends State { FinanceCategory currentCategory = FinanceCategory.attendance; late ScrollController scrollController; + late FinanceBloc _financeBloc; String? _employeeId; + DateTime selectedDate = DateTime.now(); @override void initState() { super.initState(); scrollController = ScrollController(); + _financeBloc = sl(); _loadInitialData(); } @override void dispose() { scrollController.dispose(); + _financeBloc.close(); super.dispose(); } @@ -41,17 +45,19 @@ class _FinanceScreenState extends State { _employeeId = await sl().getCachedEmployeeId(); if (mounted) { setState(() {}); + _triggerLoad(); } } - void _triggerLoad(BuildContext context) { + void _triggerLoad() { if (_employeeId != null && _employeeId!.isNotEmpty) { - final bloc = context.read(); - if (bloc.state is FinanceInitial) { - bloc.add( + if (_financeBloc.state is FinanceInitial) { + _financeBloc.add( LoadFinanceDataEvent( employeeId: _employeeId!, category: currentCategory, + month: selectedDate.month, + year: selectedDate.year, ), ); } @@ -60,128 +66,150 @@ class _FinanceScreenState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => sl(), + return BlocProvider.value( + value: _financeBloc, child: Directionality( textDirection: TextDirection.ltr, child: SafeArea( - child: Builder( - builder: (context) { - // Trigger initial load when bloc is ready - _triggerLoad(context); + child: CustomScrollView( + controller: scrollController, + physics: const BouncingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: SettingsBar( + selectedIndex: 0, + showBackButton: false, + iconPaths: const [ + 'assets/images/user.svg', + 'assets/images/ball.svg', + ], + onTap: (index) { + if (index == 0) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const UserSettingsScreen(), + ), + ); + } else if (index == 1) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const NotificationsScreen(), + ), + ); + } + }, + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 5)), - return CustomScrollView( - controller: scrollController, - physics: const BouncingScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: SettingsBar( - selectedIndex: 0, - showBackButton: false, - iconPaths: const [ - 'assets/images/user.svg', - 'assets/images/ball.svg', - ], - onTap: (index) { - if (index == 0) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const UserSettingsScreen(), - ), - ); - } else if (index == 1) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NotificationsScreen(), - ), - ); + /// SUMMARY CARD + SliverToBoxAdapter( + child: BlocBuilder( + buildWhen: (previous, current) => current is FinanceLoaded, + builder: (context, state) { + String amount = "0"; + if (state is FinanceLoaded) { + amount = state.netSalary.toStringAsFixed(0); + amount = amount.replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},', + ); + } + + return FinanceSummaryCard( + totalAmount: amount, + currentCategory: currentCategory, + onCalendarTap: () async { + final date = await showDatePicker( + context: context, + initialDate: selectedDate, + firstDate: DateTime(2020), + lastDate: DateTime(2030), + ); + if (date != null && mounted) { + setState(() => selectedDate = date); + if (_employeeId != null && _employeeId!.isNotEmpty) { + _financeBloc.add( + LoadFinanceDataEvent( + employeeId: _employeeId!, + category: currentCategory, + month: selectedDate.month, + year: selectedDate.year, + ), + ); + } } }, - ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 5)), - - /// SUMMARY CARD - SliverToBoxAdapter( - child: FinanceSummaryCard( - totalAmount: "333,000", - currentCategory: currentCategory, - onCalendarTap: - () => showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(2020), - lastDate: DateTime(2030), - ), onCategoryChanged: (category) { if (category != null) { setState(() => currentCategory = category); - context.read().add( - LoadFinanceDataEvent( - employeeId: _employeeId ?? '', - category: currentCategory, - ), - ); + if (_employeeId != null && _employeeId!.isNotEmpty) { + _financeBloc.add( + LoadFinanceDataEvent( + employeeId: _employeeId!, + category: currentCategory, + month: selectedDate.month, + year: selectedDate.year, + ), + ); + } } }, - ), - ), + ); + }, + ), + ), - /// DATA LIST - BlocBuilder( - builder: (context, state) { - if (state is FinanceLoading || state is FinanceInitial) { - return const SliverToBoxAdapter( - child: Center( - child: Padding( - padding: EdgeInsets.all(20.0), - child: CircularProgressIndicator(), - ), + /// DATA LIST + BlocBuilder( + builder: (context, state) { + if (state is FinanceLoading || state is FinanceInitial) { + return const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: CircularProgressIndicator(), + ), + ), + ); + } else if (state is FinanceLoaded) { + if (state.records.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Text("لا توجد سجلات"), ), - ); - } else if (state is FinanceLoaded) { - if (state.records.isEmpty) { - return const SliverToBoxAdapter( - child: Center( - child: Padding( - padding: EdgeInsets.all(20.0), - child: Text("لا توجد سجلات"), - ), - ), - ); - } - return SliverList( - delegate: SliverChildBuilderDelegate(( - context, - index, - ) { - return WorkDayCard(record: state.records[index]); - }, childCount: state.records.length), - ); - } else if (state is FinanceError) { - return SliverToBoxAdapter( - child: Center( - child: Padding( - padding: EdgeInsets.all(20.0), - child: Text( - state.message, - style: const TextStyle(color: Colors.red), - textAlign: TextAlign.center, - ), - ), + ), + ); + } + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return WorkDayCard(record: state.records[index]); + }, childCount: state.records.length), + ); + } else if (state is FinanceError) { + return SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Text( + state.message, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, ), - ); - } - return const SliverToBoxAdapter(child: SizedBox()); - }, - ), + ), + ), + ); + } + return const SliverToBoxAdapter(child: SizedBox()); + }, + ), - const SliverToBoxAdapter(child: SizedBox(height: 120)), - ], - ); - }, + const SliverToBoxAdapter(child: SizedBox(height: 120)), + ], ), ), ), diff --git a/lib/presentation/screens/holiday_screen.dart b/lib/presentation/screens/holiday_screen.dart index 6d1b571..5707526 100644 --- a/lib/presentation/screens/holiday_screen.dart +++ b/lib/presentation/screens/holiday_screen.dart @@ -13,11 +13,11 @@ import '../../core/services/request_service.dart'; import '../../core/di/injection_container.dart'; import '../../domain/usecases/get_vacations_usecase.dart'; import '../../domain/usecases/get_advances_usecase.dart'; -import '../../domain/models/vacations_list_response_model.dart'; +// import '../../domain/models/vacations_list_response_model.dart'; import '../../domain/models/vacation_response_model.dart'; -import '../../domain/models/advances_list_response_model.dart'; +// import '../../domain/models/advances_list_response_model.dart'; import '../../domain/models/advance_request_model.dart'; -import '../../core/error/failures.dart'; +// import '../../core/error/failures.dart'; class HolidayScreen extends StatefulWidget { final void Function(bool isScrollingDown)? onScrollEvent; @@ -118,9 +118,10 @@ class _HolidayScreenState extends State { (response) { if (mounted && response.data != null) { setState(() { - _leaveRequests = response.data!.items - .map((vacation) => _convertVacationToLeaveRequest(vacation)) - .toList(); + _leaveRequests = + response.data!.items + .map((vacation) => _convertVacationToLeaveRequest(vacation)) + .toList(); _isLoadingVacations = false; }); } @@ -141,7 +142,8 @@ class _HolidayScreenState extends State { String leaveTypeName = _getArabicVacationTypeName(vacation.type); // Check if it's timed leave (same day but different times) - bool isTimedLeave = vacation.startDate.year == vacation.endDate.year && + bool isTimedLeave = + vacation.startDate.year == vacation.endDate.year && vacation.startDate.month == vacation.endDate.month && vacation.startDate.day == vacation.endDate.day && vacation.startDate.hour != vacation.endDate.hour; @@ -200,9 +202,10 @@ class _HolidayScreenState extends State { (response) { if (mounted && response.data != null) { setState(() { - _advanceRequests = response.data!.items - .map((advance) => _convertAdvanceToAdvanceRequest(advance)) - .toList(); + _advanceRequests = + response.data!.items + .map((advance) => _convertAdvanceToAdvanceRequest(advance)) + .toList(); _isLoadingAdvances = false; }); } @@ -417,9 +420,7 @@ class _HolidayScreenState extends State { child: Padding( padding: const EdgeInsets.all(40.0), child: Center( - child: CircularProgressIndicator( - color: Color(0xFF8EFDC2), - ), + child: CircularProgressIndicator(color: Color(0xFF8EFDC2)), ), ), ); @@ -458,9 +459,7 @@ class _HolidayScreenState extends State { child: Padding( padding: const EdgeInsets.all(40.0), child: Center( - child: CircularProgressIndicator( - color: Color(0xFF8EFDC2), - ), + child: CircularProgressIndicator(color: Color(0xFF8EFDC2)), ), ), ); diff --git a/lib/presentation/widgets/change_password_modal.dart b/lib/presentation/widgets/change_password_modal.dart index 661ee04..ecfbcf1 100644 --- a/lib/presentation/widgets/change_password_modal.dart +++ b/lib/presentation/widgets/change_password_modal.dart @@ -1,4 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../core/di/injection_container.dart'; +import '../../domain/models/change_password_request.dart'; +import '../blocs/change_password/change_password_bloc.dart'; +import '../blocs/change_password/change_password_event.dart'; +import '../blocs/change_password/change_password_state.dart'; import 'onboarding_button.dart'; class ChangePasswordModal extends StatefulWidget { @@ -10,7 +16,17 @@ class ChangePasswordModal extends StatefulWidget { class _ChangePasswordModalState extends State { bool _oldObscure = true; - // bool _newObscure = true; + bool _newObscure = true; + + final TextEditingController _oldPasswordController = TextEditingController(); + final TextEditingController _newPasswordController = TextEditingController(); + + @override + void dispose() { + _oldPasswordController.dispose(); + _newPasswordController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -73,10 +89,11 @@ class _ChangePasswordModalState extends State { const SizedBox(height: 8), _buildField( + controller: _oldPasswordController, hint: "أدخل كلمة المرور", obscure: _oldObscure, fontSize: fieldFontSize, - hasEye: true, // ✅ eye here + hasEye: true, onEyeTap: () => setState(() => _oldObscure = !_oldObscure), ), @@ -98,21 +115,75 @@ class _ChangePasswordModalState extends State { const SizedBox(height: 8), _buildField( + controller: _newPasswordController, hint: "كلمة المرور", - obscure: false, + obscure: _newObscure, fontSize: fieldFontSize, - hasEye: false, - onEyeTap: () {}, // unused + hasEye: true, + onEyeTap: + () => setState(() => _newObscure = !_newObscure), ), SizedBox(height: buttonSpacing), - Center( - child: OnboardingButton( - text: "حفظ التغيير", - backgroundColor: const Color(0xEE23574A), - onPressed: () { - Navigator.pop(context); + BlocProvider( + create: (context) => sl(), + child: BlocConsumer< + ChangePasswordBloc, + ChangePasswordState + >( + listener: (context, state) { + if (state is ChangePasswordSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.green, + ), + ); + Navigator.pop(context); + } else if (state is ChangePasswordError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, state) { + if (state is ChangePasswordLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + return Center( + child: OnboardingButton( + text: "حفظ التغيير", + backgroundColor: const Color(0xEE23574A), + onPressed: () { + if (_oldPasswordController.text.isEmpty || + _newPasswordController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("يرجى ملء جميع الحقول"), + backgroundColor: Colors.orange, + ), + ); + return; + } + + context.read().add( + ChangePasswordSubmitted( + ChangePasswordRequest( + oldPassword: _oldPasswordController.text, + newPassword: _newPasswordController.text, + ), + ), + ); + }, + ), + ); }, ), ), @@ -127,6 +198,7 @@ class _ChangePasswordModalState extends State { } Widget _buildField({ + required TextEditingController controller, required String hint, required bool obscure, required double fontSize, @@ -142,6 +214,7 @@ class _ChangePasswordModalState extends State { ], ), child: TextField( + controller: controller, obscureText: obscure, textAlign: TextAlign.right, style: TextStyle(fontSize: fontSize),