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

@@ -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/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(
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<FinanceBloc>(),
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<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)),
],
);
},
),
),
),
);

View File

@@ -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("عقوبة"),
),
],
),

View File

@@ -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,