attendence records, extra hours , rewards and punishment funnctionality have been added
This commit is contained in:
@@ -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<void> 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<VacationRemoteDataSource>(
|
||||
() => VacationRemoteDataSourceImpl(apiClient: sl()),
|
||||
|
||||
@@ -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<AttendanceResponseDto> login({
|
||||
@@ -14,6 +18,13 @@ abstract class AttendanceRemoteDataSource {
|
||||
required String employeeId,
|
||||
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 {
|
||||
@@ -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<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: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
|
||||
45
lib/data/dto/attendance_record_dto.dart
Normal file
45
lib/data/dto/attendance_record_dto.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
49
lib/data/dto/extra_payment_dto.dart
Normal file
49
lib/data/dto/extra_payment_dto.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/data/dto/overtime_dto.dart
Normal file
46
lib/data/dto/overtime_dto.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/data/dto/punishment_dto.dart
Normal file
46
lib/data/dto/punishment_dto.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/data/dto/reward_dto.dart
Normal file
46
lib/data/dto/reward_dto.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<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();
|
||||
}
|
||||
}
|
||||
|
||||
21
lib/domain/models/attendance_model.dart
Normal file
21
lib/domain/models/attendance_model.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
17
lib/domain/models/extra_payment_model.dart
Normal file
17
lib/domain/models/extra_payment_model.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
1
lib/domain/models/finance_category.dart
Normal file
1
lib/domain/models/finance_category.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum FinanceCategory { attendance, overtime, bonus, penalty }
|
||||
13
lib/domain/models/finance_record.dart
Normal file
13
lib/domain/models/finance_record.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
16
lib/domain/models/overtime_model.dart
Normal file
16
lib/domain/models/overtime_model.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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<AttendanceResponseModel> login(AttendanceLoginRequest 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});
|
||||
}
|
||||
|
||||
12
lib/domain/usecases/get_attendance_records_usecase.dart
Normal file
12
lib/domain/usecases/get_attendance_records_usecase.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
12
lib/domain/usecases/get_extra_hours_usecase.dart
Normal file
12
lib/domain/usecases/get_extra_hours_usecase.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
12
lib/domain/usecases/get_punishments_usecase.dart
Normal file
12
lib/domain/usecases/get_punishments_usecase.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
12
lib/domain/usecases/get_rewards_usecase.dart
Normal file
12
lib/domain/usecases/get_rewards_usecase.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
99
lib/presentation/bloc/finance_bloc.dart
Normal file
99
lib/presentation/bloc/finance_bloc.dart
Normal 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()}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<FinanceScreen> {
|
||||
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<FinanceScreen> {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SafeArea(
|
||||
child: CustomScrollView(
|
||||
return BlocProvider(
|
||||
create: (context) => sl<FinanceBloc>(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SafeArea(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
// Trigger initial load when bloc is ready
|
||||
_triggerLoad(context);
|
||||
|
||||
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 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<FinanceBloc>().add(
|
||||
LoadFinanceDataEvent(
|
||||
employeeId: _employeeId ?? '',
|
||||
category: currentCategory,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
/// DATA LIST
|
||||
BlocBuilder<FinanceBloc, FinanceState>(
|
||||
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)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<String?> onDropdownChanged;
|
||||
final ValueChanged<FinanceCategory?> 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<String>(
|
||||
value: dropdownValue,
|
||||
child: DropdownButton<FinanceCategory>(
|
||||
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("عقوبة"),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
24
pubspec.lock
24
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user