attendence records, extra hours , rewards and punishment funnctionality have been added

This commit is contained in:
Daniah Ayad Al-sultani
2026-02-10 16:27:08 +03:00
parent cd7ba8e9d5
commit 1002937045
25 changed files with 1048 additions and 181 deletions

View File

@@ -1,6 +1,8 @@
import 'package:coda_project/data/datasources/attendance_remote_data_source.dart'; 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/data/repositories/attendance_repository_impl.dart';
import 'package:coda_project/domain/repositories/attendance_repository.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:dio/dio.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.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/get_vacations_usecase.dart';
import '../../domain/usecases/create_advance_usecase.dart'; import '../../domain/usecases/create_advance_usecase.dart';
import '../../domain/usecases/get_advances_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'; import '../../presentation/blocs/login/login_bloc.dart';
final sl = GetIt.instance; final sl = GetIt.instance;
@@ -73,6 +78,21 @@ Future<void> initializeDependencies() async {
sl.registerLazySingleton(() => AttendanceLogoutUseCase(repository: sl())); 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 // Vacation
sl.registerLazySingleton<VacationRemoteDataSource>( sl.registerLazySingleton<VacationRemoteDataSource>(
() => VacationRemoteDataSourceImpl(apiClient: sl()), () => VacationRemoteDataSourceImpl(apiClient: sl()),

View File

@@ -3,6 +3,10 @@ import 'package:dio/dio.dart';
import '../../core/error/exceptions.dart'; import '../../core/error/exceptions.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../dto/attendance_response_dto.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 { abstract class AttendanceRemoteDataSource {
Future<AttendanceResponseDto> login({ Future<AttendanceResponseDto> login({
@@ -14,6 +18,13 @@ abstract class AttendanceRemoteDataSource {
required String employeeId, required String employeeId,
required File faceImage, required File faceImage,
}); });
Future<List<AttendanceRecordDto>> getAttendanceRecords({
required String employeeId,
});
Future<List<OvertimeDto>> getExtraHours({required String employeeId});
Future<List<RewardDto>> getRewards({required String employeeId});
Future<List<PunishmentDto>> getPunishments({required String employeeId});
} }
class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource { class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
@@ -140,9 +151,138 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا'); throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
} }
} catch (e) { } catch (e) {
if (e is ServerException || e is NetworkException) { throw ServerException(message: 'خطأ غير متوقع');
rethrow;
} }
}
@override
Future<List<AttendanceRecordDto>> 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<String, dynamic> &&
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<List<OvertimeDto>> 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<String, dynamic>) {
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<List<RewardDto>> 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<String, dynamic>) {
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<List<PunishmentDto>> 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<String, dynamic>) {
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: 'خطأ غير متوقع'); throw ServerException(message: 'خطأ غير متوقع');
} }
} }

View File

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

View File

@@ -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<String, dynamic> 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<ExtraPaymentDto> items;
ExtraPaymentListResponseDto({required this.items});
factory ExtraPaymentListResponseDto.fromJson(Map<String, dynamic> json) {
final data = json['data'];
if (data == null || data is! Map<String, dynamic>) {
return ExtraPaymentListResponseDto(items: []);
}
final itemsJson = data['items'] as List? ?? [];
return ExtraPaymentListResponseDto(
items: itemsJson.map((e) => ExtraPaymentDto.fromJson(e)).toList(),
);
}
}

View File

@@ -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<String, dynamic> 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<OvertimeDto> items;
OvertimeListResponseDto({required this.items});
factory OvertimeListResponseDto.fromJson(Map<String, dynamic> json) {
final data = json['data'];
if (data == null || data is! Map<String, dynamic>) {
return OvertimeListResponseDto(items: []);
}
final itemsJson = data['items'] as List? ?? [];
return OvertimeListResponseDto(
items: itemsJson.map((e) => OvertimeDto.fromJson(e)).toList(),
);
}
}

View File

@@ -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<String, dynamic> 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<PunishmentDto> items;
PunishmentListResponseDto({required this.items});
factory PunishmentListResponseDto.fromJson(Map<String, dynamic> json) {
final data = json['data'];
if (data == null || data is! Map<String, dynamic>) {
return PunishmentListResponseDto(items: []);
}
final itemsJson = data['items'] as List? ?? [];
return PunishmentListResponseDto(
items: itemsJson.map((e) => PunishmentDto.fromJson(e)).toList(),
);
}
}

View File

@@ -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<String, dynamic> 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<RewardDto> items;
RewardListResponseDto({required this.items});
factory RewardListResponseDto.fromJson(Map<String, dynamic> json) {
final data = json['data'];
if (data == null || data is! Map<String, dynamic>) {
return RewardListResponseDto(items: []);
}
final itemsJson = data['items'] as List? ?? [];
return RewardListResponseDto(
items: itemsJson.map((e) => RewardDto.fromJson(e)).toList(),
);
}
}

View File

@@ -1,6 +1,9 @@
import '../../domain/models/attendance_login_request.dart'; import '../../domain/models/attendance_login_request.dart';
import '../../domain/models/attendance_logout_request.dart'; import '../../domain/models/attendance_logout_request.dart';
import '../../domain/models/attendance_response_model.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 '../../domain/repositories/attendance_repository.dart';
import '../datasources/attendance_remote_data_source.dart'; import '../datasources/attendance_remote_data_source.dart';
@@ -38,4 +41,94 @@ class AttendanceRepositoryImpl implements AttendanceRepository {
logout: dto.logout, logout: dto.logout,
); );
} }
@override
Future<List<AttendanceModel>> 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<List<OvertimeModel>> 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<List<ExtraPaymentModel>> 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<List<ExtraPaymentModel>> 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();
}
} }

View File

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

View File

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

View File

@@ -0,0 +1 @@
enum FinanceCategory { attendance, overtime, bonus, penalty }

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
import '../models/attendance_login_request.dart'; import '../models/attendance_login_request.dart';
import '../models/attendance_logout_request.dart'; import '../models/attendance_logout_request.dart';
import '../models/attendance_response_model.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 //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<AttendanceResponseModel> login(AttendanceLoginRequest request); Future<AttendanceResponseModel> login(AttendanceLoginRequest request);
Future<AttendanceResponseModel> logout(AttendanceLogoutRequest request); Future<AttendanceResponseModel> logout(AttendanceLogoutRequest request);
Future<List<AttendanceModel>> getAttendanceRecords({
required String employeeId,
});
Future<List<OvertimeModel>> getExtraHours({required String employeeId});
Future<List<ExtraPaymentModel>> getRewards({required String employeeId});
Future<List<ExtraPaymentModel>> getPunishments({required String employeeId});
} }

View File

@@ -0,0 +1,12 @@
import '../models/attendance_model.dart';
import '../repositories/attendance_repository.dart';
class GetAttendanceRecordsUseCase {
final AttendanceRepository repository;
GetAttendanceRecordsUseCase(this.repository);
Future<List<AttendanceModel>> execute({required String employeeId}) {
return repository.getAttendanceRecords(employeeId: employeeId);
}
}

View File

@@ -0,0 +1,12 @@
import '../models/overtime_model.dart';
import '../repositories/attendance_repository.dart';
class GetExtraHoursUseCase {
final AttendanceRepository repository;
GetExtraHoursUseCase({required this.repository});
Future<List<OvertimeModel>> execute({required String employeeId}) async {
return await repository.getExtraHours(employeeId: employeeId);
}
}

View File

@@ -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<List<ExtraPaymentModel>> execute({required String employeeId}) async {
return await repository.getPunishments(employeeId: employeeId);
}
}

View File

@@ -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<List<ExtraPaymentModel>> execute({required String employeeId}) async {
return await repository.getRewards(employeeId: employeeId);
}
}

View File

@@ -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<FinanceRecord> 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<FinanceEvent, FinanceState> {
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<LoadFinanceDataEvent>(_onLoadFinanceData);
}
Future<void> _onLoadFinanceData(
LoadFinanceDataEvent event,
Emitter<FinanceState> emit,
) async {
if (event.employeeId.isEmpty) {
emit(FinanceError(message: 'Employee ID is missing or invalid'));
return;
}
emit(FinanceLoading());
try {
List<FinanceRecord> 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()}'));
}
}
}

View File

@@ -1,9 +1,14 @@
import 'package:coda_project/presentation/screens/notifications_screen.dart'; import 'package:coda_project/presentation/screens/notifications_screen.dart';
import 'package:coda_project/presentation/screens/user_settings_screen.dart'; import 'package:coda_project/presentation/screens/user_settings_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../widgets/finance_summary_card.dart'; import '../widgets/finance_summary_card.dart';
import '../widgets/work_day_card.dart'; import '../widgets/work_day_card.dart';
import '../widgets/settings_bar.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 { class FinanceScreen extends StatefulWidget {
final void Function(bool isScrollingDown)? onScrollEvent; final void Function(bool isScrollingDown)? onScrollEvent;
@@ -15,13 +20,15 @@ class FinanceScreen extends StatefulWidget {
} }
class _FinanceScreenState extends State<FinanceScreen> { class _FinanceScreenState extends State<FinanceScreen> {
String dropdownValue = "الكل"; FinanceCategory currentCategory = FinanceCategory.attendance;
late ScrollController scrollController; late ScrollController scrollController;
String? _employeeId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
scrollController = ScrollController(); scrollController = ScrollController();
_loadInitialData();
} }
@override @override
@@ -30,13 +37,40 @@ class _FinanceScreenState extends State<FinanceScreen> {
super.dispose(); super.dispose();
} }
void _loadInitialData() async {
_employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
if (mounted) {
setState(() {});
}
}
void _triggerLoad(BuildContext context) {
if (_employeeId != null && _employeeId!.isNotEmpty) {
final bloc = context.read<FinanceBloc>();
if (bloc.state is FinanceInitial) {
bloc.add(
LoadFinanceDataEvent(
employeeId: _employeeId!,
category: currentCategory,
),
);
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Directionality( return BlocProvider(
create: (context) => sl<FinanceBloc>(),
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: SafeArea( child: SafeArea(
child: CustomScrollView( child: Builder(
builder: (context) {
// Trigger initial load when bloc is ready
_triggerLoad(context);
return CustomScrollView(
controller: scrollController, controller: scrollController,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
slivers: [ slivers: [
@@ -44,7 +78,7 @@ class _FinanceScreenState extends State<FinanceScreen> {
child: SettingsBar( child: SettingsBar(
selectedIndex: 0, selectedIndex: 0,
showBackButton: false, showBackButton: false,
iconPaths: [ iconPaths: const [
'assets/images/user.svg', 'assets/images/user.svg',
'assets/images/ball.svg', 'assets/images/ball.svg',
], ],
@@ -53,14 +87,14 @@ class _FinanceScreenState extends State<FinanceScreen> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => UserSettingsScreen(), builder: (context) => const UserSettingsScreen(),
), ),
); );
} else if (index == 1) { } else if (index == 1) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => NotificationsScreen(), builder: (context) => const NotificationsScreen(),
), ),
); );
} }
@@ -73,31 +107,82 @@ class _FinanceScreenState extends State<FinanceScreen> {
SliverToBoxAdapter( SliverToBoxAdapter(
child: FinanceSummaryCard( child: FinanceSummaryCard(
totalAmount: "333,000", totalAmount: "333,000",
dropdownValue: dropdownValue, currentCategory: currentCategory,
onCalendarTap: () => showDatePicker( onCalendarTap:
() => showDatePicker(
context: context, context: context,
initialDate: DateTime.now(), initialDate: DateTime.now(),
firstDate: DateTime(2020), firstDate: DateTime(2020),
lastDate: DateTime(2030), lastDate: DateTime(2030),
), ),
onDropdownChanged: (value) { onCategoryChanged: (category) {
setState(() => dropdownValue = value!); if (category != null) {
setState(() => currentCategory = category);
context.read<FinanceBloc>().add(
LoadFinanceDataEvent(
employeeId: _employeeId ?? '',
category: currentCategory,
),
);
}
}, },
), ),
), ),
/// WORK DAY CARDS /// DATA LIST
SliverList( BlocBuilder<FinanceBloc, FinanceState>(
delegate: SliverChildBuilderDelegate( builder: (context, state) {
(context, index) { if (state is FinanceLoading || state is FinanceInitial) {
return const WorkDayCard(); return const SliverToBoxAdapter(
}, child: Center(
childCount: 3, 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("لا توجد سجلات"),
),
),
);
}
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: 120)), const SliverToBoxAdapter(child: SizedBox(height: 120)),
], ],
);
},
),
), ),
), ),
); );

View File

@@ -1,18 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import '../../domain/models/finance_category.dart';
class FinanceSummaryCard extends StatelessWidget { class FinanceSummaryCard extends StatelessWidget {
final String totalAmount; final String totalAmount;
final String dropdownValue; final FinanceCategory currentCategory;
final VoidCallback onCalendarTap; final VoidCallback onCalendarTap;
final ValueChanged<String?> onDropdownChanged; final ValueChanged<FinanceCategory?> onCategoryChanged;
const FinanceSummaryCard({ const FinanceSummaryCard({
super.key, super.key,
required this.totalAmount, this.totalAmount = "0",
required this.dropdownValue, required this.currentCategory,
required this.onCalendarTap, required this.onCalendarTap,
required this.onDropdownChanged, required this.onCategoryChanged,
}); });
@override @override
@@ -112,8 +113,8 @@ class FinanceSummaryCard extends StatelessWidget {
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
), ),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<FinanceCategory>(
value: dropdownValue, value: currentCategory,
icon: const Icon( icon: const Icon(
Icons.arrow_drop_down, Icons.arrow_drop_down,
size: 35, size: 35,
@@ -121,45 +122,26 @@ class FinanceSummaryCard extends StatelessWidget {
), ),
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
color: Color.from( color: Colors.black,
alpha: 1,
red: 0,
green: 0,
blue: 0,
),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
onChanged: onDropdownChanged, onChanged: onCategoryChanged,
items: const [ items: const [
DropdownMenuItem( DropdownMenuItem(
value: "الكل", value: FinanceCategory.attendance,
child: Directionality( child: Text("الحظور"),
textDirection: TextDirection.rtl,
child: Text("الكل"),
), ),
),
DropdownMenuItem( DropdownMenuItem(
value: "ساعات أضافية", value: FinanceCategory.overtime,
child: Directionality(
textDirection: TextDirection.rtl,
child: Text("ساعات أضافية"), child: Text("ساعات أضافية"),
), ),
),
DropdownMenuItem( DropdownMenuItem(
value: "مكافئة", value: FinanceCategory.bonus,
child: Directionality(
textDirection: TextDirection.rtl,
child: Text("مكافئة"), child: Text("مكافئة"),
), ),
),
DropdownMenuItem( DropdownMenuItem(
value: " عقوبة", value: FinanceCategory.penalty,
child: Directionality( child: Text("عقوبة"),
textDirection: TextDirection.rtl,
child: Text(" عقوبة"),
),
), ),
], ],
), ),

View File

@@ -1,13 +1,31 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.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 'gradient_line.dart';
import 'status_circle.dart'; import 'status_circle.dart';
import 'package:intl/intl.dart';
class WorkDayCard extends StatelessWidget { class WorkDayCard extends StatelessWidget {
const WorkDayCard({super.key}); final FinanceRecord record;
const WorkDayCard({super.key, required this.record});
@override @override
Widget build(BuildContext context) { 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( return Container(
margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -25,87 +43,152 @@ class WorkDayCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text(
"يوم عمل",
textAlign: TextAlign.right,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
/// 🔥 FIXED: CENTERED LINES BETWEEN CIRCLES
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_StatusItem( Text(
color: const Color(0xFFD16400), dateStr,
icon: SvgPicture.asset('assets/images/money3.svg', width: 20), style: const TextStyle(fontSize: 12, color: Colors.grey),
label: "سعر كلي\n18,250 د.ع",
), ),
Text(
/// LINE CENTERED VERTICALLY title,
Expanded( textAlign: TextAlign.right,
child: Center( style: const TextStyle(
child: GradientLine( fontSize: 20,
start: const Color(0xFFD16400), fontWeight: FontWeight.bold,
end: const Color(0xFF1266A8),
), ),
), ),
),
_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 SizedBox(height: 12),
const Divider(color: Colors.black38), const Divider(color: Colors.black38),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.end,
children: const [ children: [
Text("2025.12.1", style: TextStyle(fontSize: 12)), Text(
Text("ملاحظات ان وجدت", style: TextStyle(fontSize: 12)), 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 { class _StatusItem extends StatelessWidget {
@@ -125,7 +208,7 @@ class _StatusItem extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
StatusCircle(color: color, icon: icon), StatusCircle(color: color, icon: icon),
// const SizedBox(height: 3), const SizedBox(height: 3),
Text( Text(
label, label,
textAlign: TextAlign.center, textAlign: TextAlign.center,

View File

@@ -29,10 +29,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.12.0"
bloc: bloc:
dependency: transitive dependency: transitive
description: description:
@@ -189,10 +189,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.3" version: "1.3.2"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@@ -312,6 +312,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.4" 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: json_annotation:
dependency: transitive dependency: transitive
description: description:
@@ -324,10 +332,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.9" version: "10.0.8"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
@@ -641,10 +649,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.0" version: "14.3.1"
web: web:
dependency: transitive dependency: transitive
description: description:

View File

@@ -1,6 +1,6 @@
name: coda_project name: coda_project
description: "A new Flutter project." description: "A new Flutter project."
publish_to: 'none' publish_to: "none"
version: 1.0.0+1 version: 1.0.0+1
@@ -18,6 +18,7 @@ dependencies:
equatable: ^2.0.5 equatable: ^2.0.5
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
flutter_bloc: ^8.1.6 flutter_bloc: ^8.1.6
intl: ^0.19.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -26,14 +27,12 @@ dev_dependencies:
flutter_native_splash: ^2.4.1 flutter_native_splash: ^2.4.1
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.13.1
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true
ios: true ios: true
image_path: "assets/images/app_icon.png" image_path: "assets/images/app_icon.png"
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets: