diff --git a/lib/core/di/injection_container.dart b/lib/core/di/injection_container.dart index 287dfe5..5275920 100644 --- a/lib/core/di/injection_container.dart +++ b/lib/core/di/injection_container.dart @@ -1,6 +1,8 @@ import 'package:coda_project/data/datasources/attendance_remote_data_source.dart'; import 'package:coda_project/data/repositories/attendance_repository_impl.dart'; import 'package:coda_project/domain/repositories/attendance_repository.dart'; +import 'package:coda_project/domain/usecases/get_attendance_records_usecase.dart'; +import 'package:coda_project/presentation/bloc/finance_bloc.dart'; import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -23,6 +25,9 @@ import '../../domain/usecases/get_vacation_types_usecase.dart'; import '../../domain/usecases/get_vacations_usecase.dart'; import '../../domain/usecases/create_advance_usecase.dart'; 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 '../../presentation/blocs/login/login_bloc.dart'; final sl = GetIt.instance; @@ -73,6 +78,21 @@ Future initializeDependencies() async { sl.registerLazySingleton(() => AttendanceLogoutUseCase(repository: sl())); + sl.registerLazySingleton(() => GetAttendanceRecordsUseCase(sl())); + sl.registerLazySingleton(() => GetExtraHoursUseCase(repository: sl())); + sl.registerLazySingleton(() => GetRewardsUseCase(repository: sl())); + sl.registerLazySingleton(() => GetPunishmentsUseCase(repository: sl())); + + // Finance + sl.registerFactory( + () => FinanceBloc( + getAttendanceRecordsUseCase: sl(), + getExtraHoursUseCase: sl(), + getRewardsUseCase: sl(), + getPunishmentsUseCase: sl(), + ), + ); + // Vacation sl.registerLazySingleton( () => VacationRemoteDataSourceImpl(apiClient: sl()), diff --git a/lib/data/datasources/attendance_remote_data_source.dart b/lib/data/datasources/attendance_remote_data_source.dart index b2f78cc..7e7c31a 100644 --- a/lib/data/datasources/attendance_remote_data_source.dart +++ b/lib/data/datasources/attendance_remote_data_source.dart @@ -3,6 +3,10 @@ import 'package:dio/dio.dart'; import '../../core/error/exceptions.dart'; import '../../core/network/api_client.dart'; import '../dto/attendance_response_dto.dart'; +import '../dto/attendance_record_dto.dart'; +import '../dto/overtime_dto.dart'; +import '../dto/reward_dto.dart'; +import '../dto/punishment_dto.dart'; abstract class AttendanceRemoteDataSource { Future login({ @@ -14,6 +18,13 @@ abstract class AttendanceRemoteDataSource { required String employeeId, required File faceImage, }); + + Future> getAttendanceRecords({ + required String employeeId, + }); + Future> getExtraHours({required String employeeId}); + Future> getRewards({required String employeeId}); + Future> getPunishments({required String employeeId}); } class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource { @@ -140,9 +151,138 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource { throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا'); } } catch (e) { - if (e is ServerException || e is NetworkException) { - rethrow; + throw ServerException(message: 'خطأ غير متوقع'); + } + } + + @override + Future> getAttendanceRecords({ + required String employeeId, + }) async { + try { + final response = await apiClient.get( + '/Attendance', + queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId}, + ); + + if (response.statusCode == 200) { + final data = response.data; + if (data is Map && + data['data'] != null && + data['data']['items'] is List) { + final items = data['data']['items'] as List; + return items.map((e) => AttendanceRecordDto.fromJson(e)).toList(); + } + return []; + } else { + throw ServerException( + message: 'فشل في جلب البيانات', + statusCode: response.statusCode, + ); } + } on DioException catch (e) { + throw ServerException( + message: e.message ?? 'Unknown error', + statusCode: e.response?.statusCode, + ); + } catch (e) { + throw ServerException(message: 'خطأ غير متوقع'); + } + } + + @override + Future> getExtraHours({required String employeeId}) async { + try { + final response = await apiClient.get( + '/ExtraHours', + queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId}, + ); + + if (response.statusCode == 200) { + final responseData = response.data; + + if (responseData is Map) { + return OvertimeListResponseDto.fromJson(responseData).items; + } else { + throw ServerException( + message: 'استجابة غير صحيحة من الخادم', + statusCode: response.statusCode, + ); + } + } else { + throw ServerException( + message: 'فشل في جلب البيانات', + statusCode: response.statusCode, + ); + } + } on DioException catch (e) { + throw ServerException( + message: e.message ?? 'Unknown error', + statusCode: e.response?.statusCode, + ); + } catch (e) { + throw ServerException(message: 'خطأ غير متوقع'); + } + } + + @override + Future> getRewards({required String employeeId}) async { + try { + final response = await apiClient.get( + '/Reward', + queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId}, + ); + + if (response.statusCode == 200) { + final responseData = response.data; + if (responseData is Map) { + return RewardListResponseDto.fromJson(responseData).items; + } + return []; + } else { + throw ServerException( + message: 'فشل في جلب المكافآت', + statusCode: response.statusCode, + ); + } + } on DioException catch (e) { + throw ServerException( + message: e.message ?? 'Unknown error', + statusCode: e.response?.statusCode, + ); + } catch (e) { + throw ServerException(message: 'خطأ غير متوقع'); + } + } + + @override + Future> getPunishments({ + required String employeeId, + }) async { + try { + final response = await apiClient.get( + '/Punishment', + queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId}, + ); + + if (response.statusCode == 200) { + final responseData = response.data; + if (responseData is Map) { + return PunishmentListResponseDto.fromJson(responseData).items; + } + return []; + } else { + throw ServerException( + message: 'فشل في جلب البيانات', + 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/dto/attendance_record_dto.dart b/lib/data/dto/attendance_record_dto.dart new file mode 100644 index 0000000..32c333a --- /dev/null +++ b/lib/data/dto/attendance_record_dto.dart @@ -0,0 +1,45 @@ +class AttendanceRecordDto { + final String id; + final String employeeId; + final DateTime? login; + final DateTime? logout; + final String? reason; + final DateTime? createdAt; + final bool isDeleted; + + AttendanceRecordDto({ + required this.id, + required this.employeeId, + this.login, + this.logout, + this.reason, + this.createdAt, + required this.isDeleted, + }); + + factory AttendanceRecordDto.fromJson(Map json) { + return AttendanceRecordDto( + id: json['id']?.toString() ?? '', + employeeId: json['employeeId']?.toString() ?? '', + login: _parseDateTime(json['login']), + logout: _parseDateTime(json['logout']), + reason: json['reason'], + createdAt: _parseDateTime(json['createdAt']), + isDeleted: json['isDeleted'] ?? false, + ); + } + + static DateTime? _parseDateTime(dynamic value) { + if (value == null) return null; + if (value is DateTime) return value; + if (value is String) { + if (value.isEmpty) return null; + try { + return DateTime.parse(value); + } catch (e) { + return null; // Handle parse error gracefully + } + } + return null; + } +} diff --git a/lib/data/dto/extra_payment_dto.dart b/lib/data/dto/extra_payment_dto.dart new file mode 100644 index 0000000..4f6103e --- /dev/null +++ b/lib/data/dto/extra_payment_dto.dart @@ -0,0 +1,49 @@ +class ExtraPaymentDto { + final String id; + final DateTime date; + final double amount; + final String? reason; + final String? note; + final String employeeId; + final bool isDeleted; + + ExtraPaymentDto({ + required this.id, + required this.date, + required this.amount, + this.reason, + this.note, + required this.employeeId, + required this.isDeleted, + }); + + factory ExtraPaymentDto.fromJson(Map json) { + return ExtraPaymentDto( + id: json['id']?.toString() ?? '', + date: + json['date'] != null ? DateTime.parse(json['date']) : DateTime.now(), + amount: (json['amount'] as num?)?.toDouble() ?? 0.0, + reason: json['reason'], + note: json['note'], + employeeId: json['employeeId']?.toString() ?? '', + isDeleted: json['isDeleted'] ?? false, + ); + } +} + +class ExtraPaymentListResponseDto { + final List items; + + ExtraPaymentListResponseDto({required this.items}); + + factory ExtraPaymentListResponseDto.fromJson(Map json) { + final data = json['data']; + if (data == null || data is! Map) { + return ExtraPaymentListResponseDto(items: []); + } + final itemsJson = data['items'] as List? ?? []; + return ExtraPaymentListResponseDto( + items: itemsJson.map((e) => ExtraPaymentDto.fromJson(e)).toList(), + ); + } +} diff --git a/lib/data/dto/overtime_dto.dart b/lib/data/dto/overtime_dto.dart new file mode 100644 index 0000000..05c471a --- /dev/null +++ b/lib/data/dto/overtime_dto.dart @@ -0,0 +1,46 @@ +class OvertimeDto { + final String id; + final DateTime date; + final double hours; + final double hourlyRateAtTheTime; + final String employeeId; + final bool isDeleted; + + OvertimeDto({ + required this.id, + required this.employeeId, + required this.date, + required this.hours, + required this.hourlyRateAtTheTime, + required this.isDeleted, + }); + + factory OvertimeDto.fromJson(Map json) { + return OvertimeDto( + id: json['id']?.toString() ?? '', + date: DateTime.parse(json['date']), + hours: (json['hours'] as num?)?.toDouble() ?? 0.0, + hourlyRateAtTheTime: + (json['hourlyRateAtTheTime'] as num?)?.toDouble() ?? 0.0, + employeeId: json['employeeId']?.toString() ?? '', + isDeleted: json['isDeleted'] ?? false, + ); + } +} + +class OvertimeListResponseDto { + final List items; + + OvertimeListResponseDto({required this.items}); + + factory OvertimeListResponseDto.fromJson(Map json) { + final data = json['data']; + if (data == null || data is! Map) { + return OvertimeListResponseDto(items: []); + } + final itemsJson = data['items'] as List? ?? []; + return OvertimeListResponseDto( + items: itemsJson.map((e) => OvertimeDto.fromJson(e)).toList(), + ); + } +} diff --git a/lib/data/dto/punishment_dto.dart b/lib/data/dto/punishment_dto.dart new file mode 100644 index 0000000..5679e55 --- /dev/null +++ b/lib/data/dto/punishment_dto.dart @@ -0,0 +1,46 @@ +class PunishmentDto { + final String id; + final DateTime date; + final double amount; + final String? reason; + final String? note; + final String employeeId; + + PunishmentDto({ + required this.id, + required this.date, + required this.amount, + this.reason, + this.note, + required this.employeeId, + }); + + factory PunishmentDto.fromJson(Map json) { + return PunishmentDto( + id: json['id']?.toString() ?? '', + date: + json['date'] != null ? DateTime.parse(json['date']) : DateTime.now(), + amount: (json['amount'] as num?)?.toDouble() ?? 0.0, + reason: json['reason'], + note: json['note'], + employeeId: json['employeeId']?.toString() ?? '', + ); + } +} + +class PunishmentListResponseDto { + final List items; + + PunishmentListResponseDto({required this.items}); + + factory PunishmentListResponseDto.fromJson(Map json) { + final data = json['data']; + if (data == null || data is! Map) { + return PunishmentListResponseDto(items: []); + } + final itemsJson = data['items'] as List? ?? []; + return PunishmentListResponseDto( + items: itemsJson.map((e) => PunishmentDto.fromJson(e)).toList(), + ); + } +} diff --git a/lib/data/dto/reward_dto.dart b/lib/data/dto/reward_dto.dart new file mode 100644 index 0000000..6f68eee --- /dev/null +++ b/lib/data/dto/reward_dto.dart @@ -0,0 +1,46 @@ +class RewardDto { + final String id; + final DateTime date; + final double amount; + final String? reason; + final String? note; + final String employeeId; + + RewardDto({ + required this.id, + required this.date, + required this.amount, + this.reason, + this.note, + required this.employeeId, + }); + + factory RewardDto.fromJson(Map json) { + return RewardDto( + id: json['id']?.toString() ?? '', + date: + json['date'] != null ? DateTime.parse(json['date']) : DateTime.now(), + amount: (json['amount'] as num?)?.toDouble() ?? 0.0, + reason: json['reason'], + note: json['note'], + employeeId: json['employeeId']?.toString() ?? '', + ); + } +} + +class RewardListResponseDto { + final List items; + + RewardListResponseDto({required this.items}); + + factory RewardListResponseDto.fromJson(Map json) { + final data = json['data']; + if (data == null || data is! Map) { + return RewardListResponseDto(items: []); + } + final itemsJson = data['items'] as List? ?? []; + return RewardListResponseDto( + items: itemsJson.map((e) => RewardDto.fromJson(e)).toList(), + ); + } +} diff --git a/lib/data/repositories/attendance_repository_impl.dart b/lib/data/repositories/attendance_repository_impl.dart index 4555a75..eabcf83 100644 --- a/lib/data/repositories/attendance_repository_impl.dart +++ b/lib/data/repositories/attendance_repository_impl.dart @@ -1,6 +1,9 @@ import '../../domain/models/attendance_login_request.dart'; import '../../domain/models/attendance_logout_request.dart'; 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/repositories/attendance_repository.dart'; import '../datasources/attendance_remote_data_source.dart'; @@ -38,4 +41,94 @@ class AttendanceRepositoryImpl implements AttendanceRepository { logout: dto.logout, ); } + + @override + Future> getAttendanceRecords({ + required String employeeId, + }) async { + final dtos = await remoteDataSource.getAttendanceRecords( + employeeId: employeeId, + ); + + return dtos.map((dto) { + int? hours; + if (dto.login != null && dto.logout != null) { + hours = dto.logout!.difference(dto.login!).inHours; + } + + return AttendanceModel( + id: dto.id, + employeeId: dto.employeeId, + date: dto.createdAt ?? dto.login, + loginTime: dto.login, + logoutTime: dto.logout, + workHours: hours, + createdAt: dto.createdAt ?? dto.login, + reason: dto.reason, + isDeleted: dto.isDeleted, + ); + }).toList(); + } + + @override + Future> getExtraHours({ + required String employeeId, + }) async { + final dtos = await remoteDataSource.getExtraHours(employeeId: employeeId); + + return dtos + .map( + (dto) => OvertimeModel( + id: dto.id, + employeeId: dto.employeeId, + date: dto.date, + hours: dto.hours, + hourlyRateAtTheTime: dto.hourlyRateAtTheTime, + totalAmount: dto.hours * dto.hourlyRateAtTheTime, + ), + ) + .toList(); + } + + @override + Future> getRewards({ + required String employeeId, + }) async { + final dtos = await remoteDataSource.getRewards(employeeId: employeeId); + + return dtos + .map( + (dto) => ExtraPaymentModel( + id: dto.id, + employeeId: dto.employeeId, + date: dto.date, + amount: dto.amount, + reason: dto.reason, + note: dto.note, + isPenalty: false, + ), + ) + .toList(); + } + + @override + Future> getPunishments({ + required String employeeId, + }) async { + final dtos = await remoteDataSource.getPunishments(employeeId: employeeId); + + return dtos + .map( + (dto) => ExtraPaymentModel( + id: dto.id, + employeeId: dto.employeeId, + date: dto.date, + amount: dto.amount, + reason: dto.reason, + note: dto.note, + isPenalty: true, + ), + ) + .toList(); + } } diff --git a/lib/domain/models/attendance_model.dart b/lib/domain/models/attendance_model.dart new file mode 100644 index 0000000..df5d7b6 --- /dev/null +++ b/lib/domain/models/attendance_model.dart @@ -0,0 +1,21 @@ +import 'finance_record.dart'; + +class AttendanceModel extends FinanceRecord { + final DateTime? loginTime; + final DateTime? logoutTime; + final int? workHours; + final DateTime? createdAt; + final bool isDeleted; + + AttendanceModel({ + required super.id, + required super.employeeId, + super.date, + super.reason, + this.loginTime, + this.logoutTime, + this.workHours, + this.createdAt, + required this.isDeleted, + }); +} diff --git a/lib/domain/models/extra_payment_model.dart b/lib/domain/models/extra_payment_model.dart new file mode 100644 index 0000000..727b067 --- /dev/null +++ b/lib/domain/models/extra_payment_model.dart @@ -0,0 +1,17 @@ +import 'finance_record.dart'; + +class ExtraPaymentModel extends FinanceRecord { + final double amount; + final String? note; + final bool isPenalty; + + ExtraPaymentModel({ + required super.id, + required super.employeeId, + super.date, + super.reason, + required this.amount, + this.note, + required this.isPenalty, + }); +} diff --git a/lib/domain/models/finance_category.dart b/lib/domain/models/finance_category.dart new file mode 100644 index 0000000..03573a6 --- /dev/null +++ b/lib/domain/models/finance_category.dart @@ -0,0 +1 @@ +enum FinanceCategory { attendance, overtime, bonus, penalty } diff --git a/lib/domain/models/finance_record.dart b/lib/domain/models/finance_record.dart new file mode 100644 index 0000000..9f906db --- /dev/null +++ b/lib/domain/models/finance_record.dart @@ -0,0 +1,13 @@ +abstract class FinanceRecord { + final String id; + final String employeeId; + final DateTime? date; + final String? reason; + + FinanceRecord({ + required this.id, + required this.employeeId, + this.date, + this.reason, + }); +} diff --git a/lib/domain/models/overtime_model.dart b/lib/domain/models/overtime_model.dart new file mode 100644 index 0000000..4a4b841 --- /dev/null +++ b/lib/domain/models/overtime_model.dart @@ -0,0 +1,16 @@ +import 'finance_record.dart'; + +class OvertimeModel extends FinanceRecord { + final double hours; + final double hourlyRateAtTheTime; + final double totalAmount; + + OvertimeModel({ + required super.id, + required super.employeeId, + super.date, + required this.hours, + required this.hourlyRateAtTheTime, + required this.totalAmount, + }); +} diff --git a/lib/domain/repositories/attendance_repository.dart b/lib/domain/repositories/attendance_repository.dart index 1165144..a16e9a3 100644 --- a/lib/domain/repositories/attendance_repository.dart +++ b/lib/domain/repositories/attendance_repository.dart @@ -1,6 +1,9 @@ import '../models/attendance_login_request.dart'; import '../models/attendance_logout_request.dart'; import '../models/attendance_response_model.dart'; +import '../models/attendance_model.dart'; +import '../models/overtime_model.dart'; +import '../models/extra_payment_model.dart'; //in the following polymorphism is being used , a quich recap it is where th esame method but opperate in a different way @@ -9,4 +12,11 @@ abstract class AttendanceRepository { Future login(AttendanceLoginRequest request); Future logout(AttendanceLogoutRequest request); + + Future> getAttendanceRecords({ + required String employeeId, + }); + Future> getExtraHours({required String employeeId}); + Future> getRewards({required String employeeId}); + Future> getPunishments({required String employeeId}); } diff --git a/lib/domain/usecases/get_attendance_records_usecase.dart b/lib/domain/usecases/get_attendance_records_usecase.dart new file mode 100644 index 0000000..c642115 --- /dev/null +++ b/lib/domain/usecases/get_attendance_records_usecase.dart @@ -0,0 +1,12 @@ +import '../models/attendance_model.dart'; +import '../repositories/attendance_repository.dart'; + +class GetAttendanceRecordsUseCase { + final AttendanceRepository repository; + + GetAttendanceRecordsUseCase(this.repository); + + Future> execute({required String employeeId}) { + return repository.getAttendanceRecords(employeeId: employeeId); + } +} diff --git a/lib/domain/usecases/get_extra_hours_usecase.dart b/lib/domain/usecases/get_extra_hours_usecase.dart new file mode 100644 index 0000000..d70c24b --- /dev/null +++ b/lib/domain/usecases/get_extra_hours_usecase.dart @@ -0,0 +1,12 @@ +import '../models/overtime_model.dart'; +import '../repositories/attendance_repository.dart'; + +class GetExtraHoursUseCase { + final AttendanceRepository repository; + + GetExtraHoursUseCase({required this.repository}); + + Future> execute({required String employeeId}) async { + return await repository.getExtraHours(employeeId: employeeId); + } +} diff --git a/lib/domain/usecases/get_punishments_usecase.dart b/lib/domain/usecases/get_punishments_usecase.dart new file mode 100644 index 0000000..287e615 --- /dev/null +++ b/lib/domain/usecases/get_punishments_usecase.dart @@ -0,0 +1,12 @@ +import '../models/extra_payment_model.dart'; +import '../repositories/attendance_repository.dart'; + +class GetPunishmentsUseCase { + final AttendanceRepository repository; + + GetPunishmentsUseCase({required this.repository}); + + Future> execute({required String employeeId}) async { + return await repository.getPunishments(employeeId: employeeId); + } +} diff --git a/lib/domain/usecases/get_rewards_usecase.dart b/lib/domain/usecases/get_rewards_usecase.dart new file mode 100644 index 0000000..c91d332 --- /dev/null +++ b/lib/domain/usecases/get_rewards_usecase.dart @@ -0,0 +1,12 @@ +import '../models/extra_payment_model.dart'; +import '../repositories/attendance_repository.dart'; + +class GetRewardsUseCase { + final AttendanceRepository repository; + + GetRewardsUseCase({required this.repository}); + + Future> execute({required String employeeId}) async { + return await repository.getRewards(employeeId: employeeId); + } +} diff --git a/lib/main.dart b/lib/main.dart index c7cb5ec..4689e2e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,10 +7,10 @@ import 'presentation/screens/splash_screen.dart'; void main() async { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - + // Initialize dependency injection await initializeDependencies(); - + runApp(const CodaApp()); } diff --git a/lib/presentation/bloc/finance_bloc.dart b/lib/presentation/bloc/finance_bloc.dart new file mode 100644 index 0000000..9e14a7c --- /dev/null +++ b/lib/presentation/bloc/finance_bloc.dart @@ -0,0 +1,99 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +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/models/finance_record.dart'; +import '../../../domain/models/finance_category.dart'; +import '../../../core/error/exceptions.dart'; + +// Events +abstract class FinanceEvent {} + +class LoadFinanceDataEvent extends FinanceEvent { + final String employeeId; + final FinanceCategory category; + + LoadFinanceDataEvent({required this.employeeId, required this.category}); +} + +// States +abstract class FinanceState {} + +class FinanceInitial extends FinanceState {} + +class FinanceLoading extends FinanceState {} + +class FinanceLoaded extends FinanceState { + final List records; + final FinanceCategory category; + + FinanceLoaded({required this.records, required this.category}); +} + +class FinanceError extends FinanceState { + final String message; + + FinanceError({required this.message}); +} + +// Bloc +class FinanceBloc extends Bloc { + final GetAttendanceRecordsUseCase getAttendanceRecordsUseCase; + final GetExtraHoursUseCase getExtraHoursUseCase; + final GetRewardsUseCase getRewardsUseCase; + final GetPunishmentsUseCase getPunishmentsUseCase; + + FinanceBloc({ + required this.getAttendanceRecordsUseCase, + required this.getExtraHoursUseCase, + required this.getRewardsUseCase, + required this.getPunishmentsUseCase, + }) : super(FinanceInitial()) { + on(_onLoadFinanceData); + } + + Future _onLoadFinanceData( + LoadFinanceDataEvent event, + Emitter emit, + ) async { + if (event.employeeId.isEmpty) { + emit(FinanceError(message: 'Employee ID is missing or invalid')); + return; + } + + emit(FinanceLoading()); + try { + List records; + switch (event.category) { + case FinanceCategory.attendance: + records = await getAttendanceRecordsUseCase.execute( + employeeId: event.employeeId, + ); + break; + case FinanceCategory.overtime: + records = await getExtraHoursUseCase.execute( + employeeId: event.employeeId, + ); + break; + case FinanceCategory.bonus: + records = await getRewardsUseCase.execute( + employeeId: event.employeeId, + ); + break; + case FinanceCategory.penalty: + records = await getPunishmentsUseCase.execute( + employeeId: event.employeeId, + ); + break; + } + emit(FinanceLoaded(records: records, category: event.category)); + } on ServerException catch (e) { + emit(FinanceError(message: e.message)); + } on NetworkException catch (e) { + emit(FinanceError(message: e.message)); + } catch (e) { + emit(FinanceError(message: 'Unexpected error occurred: ${e.toString()}')); + } + } +} diff --git a/lib/presentation/screens/finance_screen.dart b/lib/presentation/screens/finance_screen.dart index 52c0cba..751bf22 100644 --- a/lib/presentation/screens/finance_screen.dart +++ b/lib/presentation/screens/finance_screen.dart @@ -1,9 +1,14 @@ import 'package:coda_project/presentation/screens/notifications_screen.dart'; import 'package:coda_project/presentation/screens/user_settings_screen.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../widgets/finance_summary_card.dart'; import '../widgets/work_day_card.dart'; import '../widgets/settings_bar.dart'; +import '../bloc/finance_bloc.dart'; +import '../../core/di/injection_container.dart'; +import '../../data/datasources/user_local_data_source.dart'; +import '../../domain/models/finance_category.dart'; class FinanceScreen extends StatefulWidget { final void Function(bool isScrollingDown)? onScrollEvent; @@ -15,13 +20,15 @@ class FinanceScreen extends StatefulWidget { } class _FinanceScreenState extends State { - String dropdownValue = "الكل"; + FinanceCategory currentCategory = FinanceCategory.attendance; late ScrollController scrollController; + String? _employeeId; @override void initState() { super.initState(); scrollController = ScrollController(); + _loadInitialData(); } @override @@ -30,74 +37,152 @@ class _FinanceScreenState extends State { super.dispose(); } + void _loadInitialData() async { + _employeeId = await sl().getCachedEmployeeId(); + if (mounted) { + setState(() {}); + } + } + + void _triggerLoad(BuildContext context) { + if (_employeeId != null && _employeeId!.isNotEmpty) { + final bloc = context.read(); + if (bloc.state is FinanceInitial) { + bloc.add( + LoadFinanceDataEvent( + employeeId: _employeeId!, + category: currentCategory, + ), + ); + } + } + } + @override Widget build(BuildContext context) { - return Directionality( - textDirection: TextDirection.ltr, - child: SafeArea( - child: CustomScrollView( - - controller: scrollController, - physics: const BouncingScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: SettingsBar( - selectedIndex: 0, - showBackButton: false, - iconPaths: [ - 'assets/images/user.svg', - 'assets/images/ball.svg', - ], - onTap: (index) { - if (index == 0) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => UserSettingsScreen(), + return BlocProvider( + create: (context) => sl(), + child: Directionality( + textDirection: TextDirection.ltr, + child: SafeArea( + child: Builder( + builder: (context) { + // Trigger initial load when bloc is ready + _triggerLoad(context); + + 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(), + ), + ); + } + }, + ), + ), + 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, + ), + ); + } + }, + ), + ), + + /// 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 (index == 1) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NotificationsScreen(), + } 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 const SliverToBoxAdapter(child: SizedBox()); }, ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 5)), - - /// SUMMARY CARD - SliverToBoxAdapter( - child: FinanceSummaryCard( - totalAmount: "333,000", - dropdownValue: dropdownValue, - onCalendarTap: () => showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(2020), - lastDate: DateTime(2030), - ), - onDropdownChanged: (value) { - setState(() => dropdownValue = value!); - }, - ), - ), - - /// WORK DAY CARDS - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return const WorkDayCard(); - }, - childCount: 3, - ), - ), - - const SliverToBoxAdapter(child: SizedBox(height: 120)), - ], + + const SliverToBoxAdapter(child: SizedBox(height: 120)), + ], + ); + }, + ), ), ), ); diff --git a/lib/presentation/widgets/finance_summary_card.dart b/lib/presentation/widgets/finance_summary_card.dart index 9da7efa..c1a50e8 100644 --- a/lib/presentation/widgets/finance_summary_card.dart +++ b/lib/presentation/widgets/finance_summary_card.dart @@ -1,18 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../../domain/models/finance_category.dart'; class FinanceSummaryCard extends StatelessWidget { final String totalAmount; - final String dropdownValue; + final FinanceCategory currentCategory; final VoidCallback onCalendarTap; - final ValueChanged onDropdownChanged; + final ValueChanged onCategoryChanged; const FinanceSummaryCard({ super.key, - required this.totalAmount, - required this.dropdownValue, + this.totalAmount = "0", + required this.currentCategory, required this.onCalendarTap, - required this.onDropdownChanged, + required this.onCategoryChanged, }); @override @@ -112,8 +113,8 @@ class FinanceSummaryCard extends StatelessWidget { borderRadius: BorderRadius.circular(14), ), child: DropdownButtonHideUnderline( - child: DropdownButton( - value: dropdownValue, + child: DropdownButton( + value: currentCategory, icon: const Icon( Icons.arrow_drop_down, size: 35, @@ -121,45 +122,26 @@ class FinanceSummaryCard extends StatelessWidget { ), style: const TextStyle( fontSize: 18, - color: Color.from( - alpha: 1, - red: 0, - green: 0, - blue: 0, - ), + color: Colors.black, fontWeight: FontWeight.bold, ), - onChanged: onDropdownChanged, + onChanged: onCategoryChanged, items: const [ DropdownMenuItem( - value: "الكل", - child: Directionality( - textDirection: TextDirection.rtl, - child: Text("الكل"), - ), - ), - - DropdownMenuItem( - value: "ساعات أضافية", - child: Directionality( - textDirection: TextDirection.rtl, - child: Text("ساعات أضافية"), - ), - ), - - DropdownMenuItem( - value: "مكافئة", - child: Directionality( - textDirection: TextDirection.rtl, - child: Text("مكافئة"), - ), + value: FinanceCategory.attendance, + child: Text("الحظور"), ), DropdownMenuItem( - value: " عقوبة", - child: Directionality( - textDirection: TextDirection.rtl, - child: Text(" عقوبة"), - ), + value: FinanceCategory.overtime, + child: Text("ساعات أضافية"), + ), + DropdownMenuItem( + value: FinanceCategory.bonus, + child: Text("مكافئة"), + ), + DropdownMenuItem( + value: FinanceCategory.penalty, + child: Text("عقوبة"), ), ], ), diff --git a/lib/presentation/widgets/work_day_card.dart b/lib/presentation/widgets/work_day_card.dart index 6666219..9d89895 100644 --- a/lib/presentation/widgets/work_day_card.dart +++ b/lib/presentation/widgets/work_day_card.dart @@ -1,13 +1,31 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import '../../domain/models/attendance_model.dart'; +import '../../domain/models/overtime_model.dart'; +import '../../domain/models/extra_payment_model.dart'; +import '../../domain/models/finance_record.dart'; import 'gradient_line.dart'; import 'status_circle.dart'; +import 'package:intl/intl.dart'; class WorkDayCard extends StatelessWidget { - const WorkDayCard({super.key}); + final FinanceRecord record; + + const WorkDayCard({super.key, required this.record}); @override Widget build(BuildContext context) { + final dateFormat = DateFormat('yyyy.MM.dd'); + + String title = "يوم عمل"; + if (record is OvertimeModel) title = "ساعات أضافية"; + if (record is ExtraPaymentModel) { + title = (record as ExtraPaymentModel).isPenalty ? "عقوبة" : "مكافئة"; + } + + final dateStr = + record.date != null ? dateFormat.format(record.date!) : '--.--.--'; + return Container( margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -25,87 +43,152 @@ class WorkDayCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text( - "يوم عمل", - textAlign: TextAlign.right, - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - - const SizedBox(height: 16), - - /// 🔥 FIXED: CENTERED LINES BETWEEN CIRCLES Row( - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _StatusItem( - color: const Color(0xFFD16400), - icon: SvgPicture.asset('assets/images/money3.svg', width: 20), - label: "سعر كلي\n18,250 د.ع", + Text( + dateStr, + style: const TextStyle(fontSize: 12, color: Colors.grey), ), - - /// LINE CENTERED VERTICALLY - Expanded( - child: Center( - child: GradientLine( - start: const Color(0xFFD16400), - end: const Color(0xFF1266A8), - ), + Text( + title, + textAlign: TextAlign.right, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, ), ), - - _StatusItem( - color: const Color(0xFF1266A8), - icon: SvgPicture.asset('assets/images/watch.svg', width: 20), - label: "عدد ساعات\n5.50", - ), - - Expanded( - child: Center( - child: GradientLine( - start: const Color(0xFF1266A8), - end: const Color(0xFFB00000), - ), - ), - ), - - _StatusItem( - color: const Color(0xFFB00000), - icon: SvgPicture.asset('assets/images/out.svg', width: 20), - label: "خروج\n1:14pm", - ), - - Expanded( - child: Center( - child: GradientLine( - start: const Color(0xFFB00000), - end: const Color(0xFF0A8F6B), - ), - ), - ), - - _StatusItem( - color: const Color(0xFF0A8F6B), - icon: SvgPicture.asset('assets/images/in.svg', width: 20), - label: "دخول\n1:14pm", - ), ], ), + const SizedBox(height: 16), + + /// CONTENT + _buildContent(context), const SizedBox(height: 12), - const Divider(color: Colors.black38), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - Text("2025.12.1", style: TextStyle(fontSize: 12)), - Text("ملاحظات ان وجدت", style: TextStyle(fontSize: 12)), + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + record.reason ?? + (record is ExtraPaymentModel + ? ((record as ExtraPaymentModel).note ?? + "لا يوجد ملاحظات") + : "لا يوجد ملاحظات"), + style: const TextStyle(fontSize: 12), + ), ], ), ], ), ); } + + Widget _buildContent(BuildContext context) { + if (record is AttendanceModel) { + return _buildAttendanceContent(record as AttendanceModel); + } else if (record is OvertimeModel) { + return _buildOvertimeContent(record as OvertimeModel); + } else if (record is ExtraPaymentModel) { + return _buildExtraPaymentContent(record as ExtraPaymentModel); + } + return const SizedBox(); + } + + Widget _buildAttendanceContent(AttendanceModel attendance) { + final timeFormat = DateFormat('h:mm a'); + final loginStr = + attendance.loginTime != null + ? timeFormat.format(attendance.loginTime!) + : '--:--'; + final logoutStr = + attendance.logoutTime != null + ? timeFormat.format(attendance.logoutTime!) + : '--:--'; + final hoursStr = attendance.workHours?.toString() ?? '0.00'; + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _StatusItem( + color: const Color(0xFF1266A8), + icon: SvgPicture.asset('assets/images/watch.svg', width: 20), + label: "عدد ساعات\n$hoursStr", + ), + _buildDivider(const Color(0xFF1266A8), const Color(0xFFB00000)), + _StatusItem( + color: const Color(0xFFB00000), + icon: SvgPicture.asset('assets/images/out.svg', width: 20), + label: "خروج\n$logoutStr", + ), + _buildDivider(const Color(0xFFB00000), const Color(0xFF0A8F6B)), + _StatusItem( + color: const Color(0xFF0A8F6B), + icon: SvgPicture.asset('assets/images/in.svg', width: 20), + label: "دخول\n$loginStr", + ), + ], + ); + } + + Widget _buildOvertimeContent(OvertimeModel overtime) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _StatusItem( + color: const Color(0xFFD16400), + icon: SvgPicture.asset('assets/images/money3.svg', width: 20), + label: "سعر كلي\n${overtime.totalAmount.toStringAsFixed(0)} د.ع", + ), + _buildDivider(const Color(0xFFD16400), const Color(0xFF1266A8)), + _StatusItem( + color: const Color(0xFF1266A8), + icon: SvgPicture.asset('assets/images/watch.svg', width: 20), + label: "ساعات إضافية\n${overtime.hours.toStringAsFixed(2)}", + ), + ], + ); + } + + Widget _buildExtraPaymentContent(ExtraPaymentModel payment) { + // Reason goes in the middle column. + // Note goes next to the amount (as part of the amount label). + final reasonText = payment.reason ?? "لا يوجد سبب"; + final noteText = payment.note != null ? "\n(${payment.note})" : ""; + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _StatusItem( + color: const Color(0xFFD16400), + icon: SvgPicture.asset('assets/images/money3.svg', width: 20), + label: "المبلغ\n${payment.amount.toStringAsFixed(0)} د.ع$noteText", + ), + _buildDivider(const Color(0xFFD16400), const Color(0xFF1266A8)), + Expanded( + flex: 2, + child: Column( + children: [ + Text( + reasonText, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const Text("السبب", style: TextStyle(fontSize: 12)), + ], + ), + ), + ], + ); + } + + Widget _buildDivider(Color start, Color end) { + return Expanded(child: Center(child: GradientLine(start: start, end: end))); + } } class _StatusItem extends StatelessWidget { @@ -125,7 +208,7 @@ class _StatusItem extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ StatusCircle(color: color, icon: icon), - // const SizedBox(height: 3), + const SizedBox(height: 3), Text( label, textAlign: TextAlign.center, diff --git a/pubspec.lock b/pubspec.lock index 98e1ebe..9b97053 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" bloc: dependency: transitive description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -312,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" json_annotation: dependency: transitive description: @@ -324,10 +332,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -641,10 +649,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fc826a9..11123db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: coda_project description: "A new Flutter project." -publish_to: 'none' +publish_to: "none" version: 1.0.0+1 @@ -18,6 +18,7 @@ dependencies: equatable: ^2.0.5 shared_preferences: ^2.2.2 flutter_bloc: ^8.1.6 + intl: ^0.19.0 dev_dependencies: flutter_test: @@ -26,14 +27,12 @@ dev_dependencies: flutter_native_splash: ^2.4.1 flutter_launcher_icons: ^0.13.1 - flutter_launcher_icons: android: true ios: true image_path: "assets/images/app_icon.png" - flutter: uses-material-design: true assets: