chnages has been made and net salary is being displayed

This commit is contained in:
Daniah Ayad Al-sultani
2026-02-11 14:31:03 +03:00
parent 1002937045
commit a7930d19e5
23 changed files with 691 additions and 141 deletions

View File

@@ -28,7 +28,10 @@ import '../../domain/usecases/get_advances_usecase.dart';
import '../../domain/usecases/get_extra_hours_usecase.dart'; import '../../domain/usecases/get_extra_hours_usecase.dart';
import '../../domain/usecases/get_rewards_usecase.dart'; import '../../domain/usecases/get_rewards_usecase.dart';
import '../../domain/usecases/get_punishments_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/login/login_bloc.dart';
import '../../presentation/blocs/change_password/change_password_bloc.dart';
final sl = GetIt.instance; final sl = GetIt.instance;
@@ -61,9 +64,11 @@ Future<void> initializeDependencies() async {
// Use cases // Use cases
sl.registerLazySingleton(() => LoginUseCase(repository: sl())); sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
sl.registerLazySingleton(() => ChangePasswordUseCase(repository: sl()));
// Blocs // Blocs
sl.registerFactory(() => LoginBloc(loginUseCase: sl())); sl.registerFactory(() => LoginBloc(loginUseCase: sl()));
sl.registerFactory(() => ChangePasswordBloc(changePasswordUseCase: sl()));
//Attendence //Attendence
sl.registerLazySingleton<AttendanceRemoteDataSource>( sl.registerLazySingleton<AttendanceRemoteDataSource>(
@@ -82,6 +87,7 @@ Future<void> initializeDependencies() async {
sl.registerLazySingleton(() => GetExtraHoursUseCase(repository: sl())); sl.registerLazySingleton(() => GetExtraHoursUseCase(repository: sl()));
sl.registerLazySingleton(() => GetRewardsUseCase(repository: sl())); sl.registerLazySingleton(() => GetRewardsUseCase(repository: sl()));
sl.registerLazySingleton(() => GetPunishmentsUseCase(repository: sl())); sl.registerLazySingleton(() => GetPunishmentsUseCase(repository: sl()));
sl.registerLazySingleton(() => GetSalarySummaryUseCase(sl()));
// Finance // Finance
sl.registerFactory( sl.registerFactory(
@@ -90,6 +96,7 @@ Future<void> initializeDependencies() async {
getExtraHoursUseCase: sl(), getExtraHoursUseCase: sl(),
getRewardsUseCase: sl(), getRewardsUseCase: sl(),
getPunishmentsUseCase: sl(), getPunishmentsUseCase: sl(),
getSalarySummaryUseCase: sl(),
), ),
); );

View File

@@ -3,6 +3,9 @@ class ServerException implements Exception {
final int? statusCode; final int? statusCode;
ServerException({required this.message, this.statusCode}); ServerException({required this.message, this.statusCode});
@override
String toString() => 'ServerException: $message (Status: $statusCode)';
} }
class NetworkException implements Exception { class NetworkException implements Exception {

View File

@@ -7,6 +7,7 @@ import '../dto/attendance_record_dto.dart';
import '../dto/overtime_dto.dart'; import '../dto/overtime_dto.dart';
import '../dto/reward_dto.dart'; import '../dto/reward_dto.dart';
import '../dto/punishment_dto.dart'; import '../dto/punishment_dto.dart';
import '../dto/salary_response_dto.dart';
abstract class AttendanceRemoteDataSource { abstract class AttendanceRemoteDataSource {
Future<AttendanceResponseDto> login({ Future<AttendanceResponseDto> login({
@@ -25,6 +26,11 @@ abstract class AttendanceRemoteDataSource {
Future<List<OvertimeDto>> getExtraHours({required String employeeId}); Future<List<OvertimeDto>> getExtraHours({required String employeeId});
Future<List<RewardDto>> getRewards({required String employeeId}); Future<List<RewardDto>> getRewards({required String employeeId});
Future<List<PunishmentDto>> getPunishments({required String employeeId}); Future<List<PunishmentDto>> getPunishments({required String employeeId});
Future<SalaryResponseDto> calculateSalary({
required String employeeId,
required int month,
required int year,
});
} }
class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource { class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
@@ -286,4 +292,66 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
throw ServerException(message: 'خطأ غير متوقع'); throw ServerException(message: 'خطأ غير متوقع');
} }
} }
@override
Future<SalaryResponseDto> 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<String, dynamic>) {
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: 'خطأ غير متوقع');
}
}
} }

View File

@@ -3,9 +3,14 @@ import '../../core/error/exceptions.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../dto/login_dto.dart'; import '../dto/login_dto.dart';
import '../dto/login_response_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 { abstract class AuthRemoteDataSource {
Future<LoginResponseDto> login(LoginDto dto); Future<LoginResponseDto> login(LoginDto dto);
Future<ChangePasswordResponseDto> changePassword(
ChangePasswordRequestDto dto,
);
} }
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
@@ -72,4 +77,37 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
throw ServerException(message: 'خطأ غير متوقع'); throw ServerException(message: 'خطأ غير متوقع');
} }
} }
@override
Future<ChangePasswordResponseDto> 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');
}
}
} }

View File

@@ -0,0 +1,13 @@
class ChangePasswordRequestDto {
final String oldPassword;
final String newPassword;
ChangePasswordRequestDto({
required this.oldPassword,
required this.newPassword,
});
Map<String, dynamic> toJson() {
return {'oldPassword': oldPassword, 'newPassword': newPassword};
}
}

View File

@@ -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<String, dynamic> json) {
return ChangePasswordResponseDto(
statusCode: json['statusCode'] ?? 0,
isSuccess: json['isSuccess'] ?? false,
message: json['message'] ?? '',
data:
json['data'] != null && json['data'] is Map<String, dynamic>
? LoginDataDto.fromJson(json['data'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'statusCode': statusCode,
'isSuccess': isSuccess,
'message': message,
'data': data?.toJson(),
};
}
}

View File

@@ -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<String, dynamic> 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<String, dynamic> json) {
return SalaryDataDto(
netAmount: (json['netAmount'] ?? json['NetAmount'] ?? 0.0).toDouble(),
);
}
}

View File

@@ -4,6 +4,7 @@ import '../../domain/models/attendance_response_model.dart';
import '../../domain/models/attendance_model.dart'; import '../../domain/models/attendance_model.dart';
import '../../domain/models/overtime_model.dart'; import '../../domain/models/overtime_model.dart';
import '../../domain/models/extra_payment_model.dart'; import '../../domain/models/extra_payment_model.dart';
import '../../domain/models/salary_model.dart';
import '../../domain/repositories/attendance_repository.dart'; import '../../domain/repositories/attendance_repository.dart';
import '../datasources/attendance_remote_data_source.dart'; import '../datasources/attendance_remote_data_source.dart';
@@ -131,4 +132,19 @@ class AttendanceRepositoryImpl implements AttendanceRepository {
) )
.toList(); .toList();
} }
@override
Future<SalaryModel> 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);
}
} }

View File

@@ -8,6 +8,10 @@ import '../../domain/models/login_request.dart';
import '../../domain/models/login_response_model.dart'; import '../../domain/models/login_response_model.dart';
import '../../domain/repositories/auth_repository.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 { class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource; final AuthRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource; final UserLocalDataSource localDataSource;
@@ -71,4 +75,45 @@ class AuthRepositoryImpl implements AuthRepository {
return Left(ServerFailure('خطأ غير متوقع: $e')); return Left(ServerFailure('خطأ غير متوقع: $e'));
} }
} }
@override
Future<Either<Failure, GeneralResponseModel>> 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'));
}
}
} }

View File

@@ -0,0 +1,6 @@
class ChangePasswordRequest {
final String oldPassword;
final String newPassword;
ChangePasswordRequest({required this.oldPassword, required this.newPassword});
}

View File

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

View File

@@ -0,0 +1,4 @@
class SalaryModel {
final double netAmount;
SalaryModel({required this.netAmount});
}

View File

@@ -4,6 +4,7 @@ import '../models/attendance_response_model.dart';
import '../models/attendance_model.dart'; import '../models/attendance_model.dart';
import '../models/overtime_model.dart'; import '../models/overtime_model.dart';
import '../models/extra_payment_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 //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<List<OvertimeModel>> getExtraHours({required String employeeId}); Future<List<OvertimeModel>> getExtraHours({required String employeeId});
Future<List<ExtraPaymentModel>> getRewards({required String employeeId}); Future<List<ExtraPaymentModel>> getRewards({required String employeeId});
Future<List<ExtraPaymentModel>> getPunishments({required String employeeId}); Future<List<ExtraPaymentModel>> getPunishments({required String employeeId});
Future<SalaryModel> calculateSalary({
required String employeeId,
required int month,
required int year,
});
} }

View File

@@ -2,7 +2,12 @@ import 'package:dartz/dartz.dart';
import '../../core/error/failures.dart'; import '../../core/error/failures.dart';
import '../models/login_request.dart'; import '../models/login_request.dart';
import '../models/login_response_model.dart'; import '../models/login_response_model.dart';
import '../models/change_password_request.dart';
import '../models/general_response_model.dart';
abstract class AuthRepository { abstract class AuthRepository {
Future<Either<Failure, LoginResponseModel>> login(LoginRequest request); Future<Either<Failure, LoginResponseModel>> login(LoginRequest request);
Future<Either<Failure, GeneralResponseModel>> changePassword(
ChangePasswordRequest request,
);
} }

View File

@@ -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<Either<Failure, GeneralResponseModel>> call(
ChangePasswordRequest request,
) async {
return await repository.changePassword(request);
}
}

View File

@@ -0,0 +1,20 @@
import '../repositories/attendance_repository.dart';
import '../models/salary_model.dart';
class GetSalarySummaryUseCase {
final AttendanceRepository repository;
GetSalarySummaryUseCase(this.repository);
Future<SalaryModel> execute({
required String employeeId,
required int month,
required int year,
}) {
return repository.calculateSalary(
employeeId: employeeId,
month: month,
year: year,
);
}
}

View File

@@ -3,6 +3,7 @@ import '../../../domain/usecases/get_attendance_records_usecase.dart';
import '../../../domain/usecases/get_extra_hours_usecase.dart'; import '../../../domain/usecases/get_extra_hours_usecase.dart';
import '../../../domain/usecases/get_rewards_usecase.dart'; import '../../../domain/usecases/get_rewards_usecase.dart';
import '../../../domain/usecases/get_punishments_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_record.dart';
import '../../../domain/models/finance_category.dart'; import '../../../domain/models/finance_category.dart';
import '../../../core/error/exceptions.dart'; import '../../../core/error/exceptions.dart';
@@ -13,8 +14,15 @@ abstract class FinanceEvent {}
class LoadFinanceDataEvent extends FinanceEvent { class LoadFinanceDataEvent extends FinanceEvent {
final String employeeId; final String employeeId;
final FinanceCategory category; 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 // States
@@ -27,8 +35,13 @@ class FinanceLoading extends FinanceState {}
class FinanceLoaded extends FinanceState { class FinanceLoaded extends FinanceState {
final List<FinanceRecord> records; final List<FinanceRecord> records;
final FinanceCategory category; 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 { class FinanceError extends FinanceState {
@@ -43,12 +56,14 @@ class FinanceBloc extends Bloc<FinanceEvent, FinanceState> {
final GetExtraHoursUseCase getExtraHoursUseCase; final GetExtraHoursUseCase getExtraHoursUseCase;
final GetRewardsUseCase getRewardsUseCase; final GetRewardsUseCase getRewardsUseCase;
final GetPunishmentsUseCase getPunishmentsUseCase; final GetPunishmentsUseCase getPunishmentsUseCase;
final GetSalarySummaryUseCase getSalarySummaryUseCase;
FinanceBloc({ FinanceBloc({
required this.getAttendanceRecordsUseCase, required this.getAttendanceRecordsUseCase,
required this.getExtraHoursUseCase, required this.getExtraHoursUseCase,
required this.getRewardsUseCase, required this.getRewardsUseCase,
required this.getPunishmentsUseCase, required this.getPunishmentsUseCase,
required this.getSalarySummaryUseCase,
}) : super(FinanceInitial()) { }) : super(FinanceInitial()) {
on<LoadFinanceDataEvent>(_onLoadFinanceData); on<LoadFinanceDataEvent>(_onLoadFinanceData);
} }
@@ -64,6 +79,27 @@ class FinanceBloc extends Bloc<FinanceEvent, FinanceState> {
emit(FinanceLoading()); emit(FinanceLoading());
try { 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<FinanceRecord> records; List<FinanceRecord> records;
switch (event.category) { switch (event.category) {
case FinanceCategory.attendance: case FinanceCategory.attendance:
@@ -87,7 +123,13 @@ class FinanceBloc extends Bloc<FinanceEvent, FinanceState> {
); );
break; break;
} }
emit(FinanceLoaded(records: records, category: event.category)); emit(
FinanceLoaded(
records: records,
category: event.category,
netSalary: netSalary,
),
);
} on ServerException catch (e) { } on ServerException catch (e) {
emit(FinanceError(message: e.message)); emit(FinanceError(message: e.message));
} on NetworkException catch (e) { } on NetworkException catch (e) {

View File

@@ -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<ChangePasswordEvent, ChangePasswordState> {
final ChangePasswordUseCase changePasswordUseCase;
ChangePasswordBloc({required this.changePasswordUseCase})
: super(const ChangePasswordInitial()) {
on<ChangePasswordSubmitted>(_onChangePasswordSubmitted);
on<ChangePasswordReset>(_onChangePasswordReset);
}
Future<void> _onChangePasswordSubmitted(
ChangePasswordSubmitted event,
Emitter<ChangePasswordState> 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<ChangePasswordState> emit,
) {
emit(const ChangePasswordInitial());
}
}

View File

@@ -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 {}

View File

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

View File

@@ -22,18 +22,22 @@ class FinanceScreen extends StatefulWidget {
class _FinanceScreenState extends State<FinanceScreen> { class _FinanceScreenState extends State<FinanceScreen> {
FinanceCategory currentCategory = FinanceCategory.attendance; FinanceCategory currentCategory = FinanceCategory.attendance;
late ScrollController scrollController; late ScrollController scrollController;
late FinanceBloc _financeBloc;
String? _employeeId; String? _employeeId;
DateTime selectedDate = DateTime.now();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
scrollController = ScrollController(); scrollController = ScrollController();
_financeBloc = sl<FinanceBloc>();
_loadInitialData(); _loadInitialData();
} }
@override @override
void dispose() { void dispose() {
scrollController.dispose(); scrollController.dispose();
_financeBloc.close();
super.dispose(); super.dispose();
} }
@@ -41,17 +45,19 @@ class _FinanceScreenState extends State<FinanceScreen> {
_employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId(); _employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
_triggerLoad();
} }
} }
void _triggerLoad(BuildContext context) { void _triggerLoad() {
if (_employeeId != null && _employeeId!.isNotEmpty) { if (_employeeId != null && _employeeId!.isNotEmpty) {
final bloc = context.read<FinanceBloc>(); if (_financeBloc.state is FinanceInitial) {
if (bloc.state is FinanceInitial) { _financeBloc.add(
bloc.add(
LoadFinanceDataEvent( LoadFinanceDataEvent(
employeeId: _employeeId!, employeeId: _employeeId!,
category: currentCategory, category: currentCategory,
month: selectedDate.month,
year: selectedDate.year,
), ),
); );
} }
@@ -60,128 +66,150 @@ class _FinanceScreenState extends State<FinanceScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider.value(
create: (context) => sl<FinanceBloc>(), value: _financeBloc,
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: SafeArea( child: SafeArea(
child: Builder( child: CustomScrollView(
builder: (context) { controller: scrollController,
// Trigger initial load when bloc is ready physics: const BouncingScrollPhysics(),
_triggerLoad(context); 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( /// SUMMARY CARD
controller: scrollController, SliverToBoxAdapter(
physics: const BouncingScrollPhysics(), child: BlocBuilder<FinanceBloc, FinanceState>(
slivers: [ buildWhen: (previous, current) => current is FinanceLoaded,
SliverToBoxAdapter( builder: (context, state) {
child: SettingsBar( String amount = "0";
selectedIndex: 0, if (state is FinanceLoaded) {
showBackButton: false, amount = state.netSalary.toStringAsFixed(0);
iconPaths: const [ amount = amount.replaceAllMapped(
'assets/images/user.svg', RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
'assets/images/ball.svg', (Match m) => '${m[1]},',
], );
onTap: (index) { }
if (index == 0) {
Navigator.push( return FinanceSummaryCard(
context, totalAmount: amount,
MaterialPageRoute( currentCategory: currentCategory,
builder: (context) => const UserSettingsScreen(), onCalendarTap: () async {
), final date = await showDatePicker(
); context: context,
} else if (index == 1) { initialDate: selectedDate,
Navigator.push( firstDate: DateTime(2020),
context, lastDate: DateTime(2030),
MaterialPageRoute( );
builder: (context) => const NotificationsScreen(), 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) { onCategoryChanged: (category) {
if (category != null) { if (category != null) {
setState(() => currentCategory = category); setState(() => currentCategory = category);
context.read<FinanceBloc>().add( if (_employeeId != null && _employeeId!.isNotEmpty) {
LoadFinanceDataEvent( _financeBloc.add(
employeeId: _employeeId ?? '', LoadFinanceDataEvent(
category: currentCategory, employeeId: _employeeId!,
), category: currentCategory,
); month: selectedDate.month,
year: selectedDate.year,
),
);
}
} }
}, },
), );
), },
),
),
/// DATA LIST /// DATA LIST
BlocBuilder<FinanceBloc, FinanceState>( BlocBuilder<FinanceBloc, FinanceState>(
builder: (context, state) { builder: (context, state) {
if (state is FinanceLoading || state is FinanceInitial) { if (state is FinanceLoading || state is FinanceInitial) {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
child: Center( child: Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(20.0), padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(), 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( return SliverList(
child: Center( delegate: SliverChildBuilderDelegate((context, index) {
child: Padding( return WorkDayCard(record: state.records[index]);
padding: EdgeInsets.all(20.0), }, childCount: state.records.length),
child: Text("لا توجد سجلات"), );
), } else if (state is FinanceError) {
), return SliverToBoxAdapter(
); child: Center(
} child: Padding(
return SliverList( padding: EdgeInsets.all(20.0),
delegate: SliverChildBuilderDelegate(( child: Text(
context, state.message,
index, style: const TextStyle(color: Colors.red),
) { textAlign: TextAlign.center,
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)),
], ],
);
},
), ),
), ),
), ),

View File

@@ -13,11 +13,11 @@ import '../../core/services/request_service.dart';
import '../../core/di/injection_container.dart'; import '../../core/di/injection_container.dart';
import '../../domain/usecases/get_vacations_usecase.dart'; import '../../domain/usecases/get_vacations_usecase.dart';
import '../../domain/usecases/get_advances_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/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 '../../domain/models/advance_request_model.dart';
import '../../core/error/failures.dart'; // import '../../core/error/failures.dart';
class HolidayScreen extends StatefulWidget { class HolidayScreen extends StatefulWidget {
final void Function(bool isScrollingDown)? onScrollEvent; final void Function(bool isScrollingDown)? onScrollEvent;
@@ -118,9 +118,10 @@ class _HolidayScreenState extends State<HolidayScreen> {
(response) { (response) {
if (mounted && response.data != null) { if (mounted && response.data != null) {
setState(() { setState(() {
_leaveRequests = response.data!.items _leaveRequests =
.map((vacation) => _convertVacationToLeaveRequest(vacation)) response.data!.items
.toList(); .map((vacation) => _convertVacationToLeaveRequest(vacation))
.toList();
_isLoadingVacations = false; _isLoadingVacations = false;
}); });
} }
@@ -141,7 +142,8 @@ class _HolidayScreenState extends State<HolidayScreen> {
String leaveTypeName = _getArabicVacationTypeName(vacation.type); String leaveTypeName = _getArabicVacationTypeName(vacation.type);
// Check if it's timed leave (same day but different times) // 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.month == vacation.endDate.month &&
vacation.startDate.day == vacation.endDate.day && vacation.startDate.day == vacation.endDate.day &&
vacation.startDate.hour != vacation.endDate.hour; vacation.startDate.hour != vacation.endDate.hour;
@@ -200,9 +202,10 @@ class _HolidayScreenState extends State<HolidayScreen> {
(response) { (response) {
if (mounted && response.data != null) { if (mounted && response.data != null) {
setState(() { setState(() {
_advanceRequests = response.data!.items _advanceRequests =
.map((advance) => _convertAdvanceToAdvanceRequest(advance)) response.data!.items
.toList(); .map((advance) => _convertAdvanceToAdvanceRequest(advance))
.toList();
_isLoadingAdvances = false; _isLoadingAdvances = false;
}); });
} }
@@ -417,9 +420,7 @@ class _HolidayScreenState extends State<HolidayScreen> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(40.0), padding: const EdgeInsets.all(40.0),
child: Center( child: Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(color: Color(0xFF8EFDC2)),
color: Color(0xFF8EFDC2),
),
), ),
), ),
); );
@@ -458,9 +459,7 @@ class _HolidayScreenState extends State<HolidayScreen> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(40.0), padding: const EdgeInsets.all(40.0),
child: Center( child: Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(color: Color(0xFF8EFDC2)),
color: Color(0xFF8EFDC2),
),
), ),
), ),
); );

View File

@@ -1,4 +1,10 @@
import 'package:flutter/material.dart'; 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'; import 'onboarding_button.dart';
class ChangePasswordModal extends StatefulWidget { class ChangePasswordModal extends StatefulWidget {
@@ -10,7 +16,17 @@ class ChangePasswordModal extends StatefulWidget {
class _ChangePasswordModalState extends State<ChangePasswordModal> { class _ChangePasswordModalState extends State<ChangePasswordModal> {
bool _oldObscure = true; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -73,10 +89,11 @@ class _ChangePasswordModalState extends State<ChangePasswordModal> {
const SizedBox(height: 8), const SizedBox(height: 8),
_buildField( _buildField(
controller: _oldPasswordController,
hint: "أدخل كلمة المرور", hint: "أدخل كلمة المرور",
obscure: _oldObscure, obscure: _oldObscure,
fontSize: fieldFontSize, fontSize: fieldFontSize,
hasEye: true, // ✅ eye here hasEye: true,
onEyeTap: onEyeTap:
() => setState(() => _oldObscure = !_oldObscure), () => setState(() => _oldObscure = !_oldObscure),
), ),
@@ -98,21 +115,75 @@ class _ChangePasswordModalState extends State<ChangePasswordModal> {
const SizedBox(height: 8), const SizedBox(height: 8),
_buildField( _buildField(
controller: _newPasswordController,
hint: "كلمة المرور", hint: "كلمة المرور",
obscure: false, obscure: _newObscure,
fontSize: fieldFontSize, fontSize: fieldFontSize,
hasEye: false, hasEye: true,
onEyeTap: () {}, // unused onEyeTap:
() => setState(() => _newObscure = !_newObscure),
), ),
SizedBox(height: buttonSpacing), SizedBox(height: buttonSpacing),
Center( BlocProvider(
child: OnboardingButton( create: (context) => sl<ChangePasswordBloc>(),
text: "حفظ التغيير", child: BlocConsumer<
backgroundColor: const Color(0xEE23574A), ChangePasswordBloc,
onPressed: () { ChangePasswordState
Navigator.pop(context); >(
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<ChangePasswordBloc>().add(
ChangePasswordSubmitted(
ChangePasswordRequest(
oldPassword: _oldPasswordController.text,
newPassword: _newPasswordController.text,
),
),
);
},
),
);
}, },
), ),
), ),
@@ -127,6 +198,7 @@ class _ChangePasswordModalState extends State<ChangePasswordModal> {
} }
Widget _buildField({ Widget _buildField({
required TextEditingController controller,
required String hint, required String hint,
required bool obscure, required bool obscure,
required double fontSize, required double fontSize,
@@ -142,6 +214,7 @@ class _ChangePasswordModalState extends State<ChangePasswordModal> {
], ],
), ),
child: TextField( child: TextField(
controller: controller,
obscureText: obscure, obscureText: obscure,
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: TextStyle(fontSize: fontSize), style: TextStyle(fontSize: fontSize),