Refactor attendance management screens for improved readability and g

This commit is contained in:
Vaibhav Surve 2025-11-29 14:36:44 +05:30
parent 3ad48016f3
commit ed2eb014d8
5 changed files with 351 additions and 479 deletions

View File

@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
import 'package:on_field_work/controller/permission_controller.dart'; import 'package:on_field_work/controller/permission_controller.dart';
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart'; import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/utils/date_time_utils.dart';
import 'package:on_field_work/helpers/widgets/date_range_picker.dart'; import 'package:on_field_work/helpers/widgets/date_range_picker.dart';
class AttendanceFilterBottomSheet extends StatefulWidget { class AttendanceFilterBottomSheet extends StatefulWidget {
@ -27,21 +25,6 @@ class AttendanceFilterBottomSheet extends StatefulWidget {
class _AttendanceFilterBottomSheetState class _AttendanceFilterBottomSheetState
extends State<AttendanceFilterBottomSheet> { extends State<AttendanceFilterBottomSheet> {
late String tempSelectedTab;
@override
void initState() {
super.initState();
tempSelectedTab = widget.selectedTab;
}
String getLabelText() {
final start = DateTimeUtils.formatDate(
widget.controller.startDateAttendance.value, 'dd MMM yyyy');
final end = DateTimeUtils.formatDate(
widget.controller.endDateAttendance.value, 'dd MMM yyyy');
return "$start - $end";
}
Widget _popupSelector({ Widget _popupSelector({
required String currentValue, required String currentValue,
@ -51,12 +34,8 @@ class _AttendanceFilterBottomSheetState
return PopupMenuButton<String>( return PopupMenuButton<String>(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onSelected: onSelected, onSelected: onSelected,
itemBuilder: (context) => items itemBuilder: (context) =>
.map((e) => PopupMenuItem<String>( items.map((e) => PopupMenuItem<String>(value: e, child: MyText(e))).toList(),
value: e,
child: MyText(e),
))
.toList(),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -107,46 +86,11 @@ class _AttendanceFilterBottomSheetState
); );
} }
List<Widget> buildMainFilters() { List<Widget> _buildFilters() {
final hasRegularizationPermission = widget.permissionController final List<Widget> widgets = [];
.hasPermission(Permissions.regularizeAttendance);
final viewOptions = [
{'label': 'Today\'s Attendance', 'value': 'todaysAttendance'},
{'label': 'Attendance Logs', 'value': 'attendanceLogs'},
{'label': 'Regularization Requests', 'value': 'regularizationRequests'},
];
final filteredOptions = viewOptions.where((item) {
return item['value'] != 'regularizationRequests' ||
hasRegularizationPermission;
}).toList();
final List<Widget> widgets = [
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Align(
alignment: Alignment.centerLeft,
child: MyText.titleSmall("View", fontWeight: 600),
),
),
...filteredOptions.map((item) {
return RadioListTile<String>(
dense: true,
contentPadding: EdgeInsets.zero,
title: MyText.bodyMedium(
item['label']!,
fontWeight: 500,
),
value: item['value']!,
groupValue: tempSelectedTab,
onChanged: (value) => setState(() => tempSelectedTab = value!),
);
}),
];
// Organization selector
widgets.addAll([ widgets.addAll([
const Divider(),
Padding( Padding(
padding: const EdgeInsets.only(top: 12, bottom: 12), padding: const EdgeInsets.only(top: 12, bottom: 12),
child: Align( child: Align(
@ -180,7 +124,8 @@ class _AttendanceFilterBottomSheetState
}), }),
]); ]);
if (tempSelectedTab == 'attendanceLogs') { // Date range (only for Attendance Logs)
if (widget.selectedTab == 'attendanceLogs') {
widgets.addAll([ widgets.addAll([
const Divider(), const Divider(),
Padding( Padding(
@ -208,24 +153,20 @@ class _AttendanceFilterBottomSheetState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(
// FIX: avoids hiding under navigation buttons
child: BaseBottomSheet( child: BaseBottomSheet(
title: "Attendance Filter", title: "Attendance Filter",
submitText: "Apply", submitText: "Apply",
onCancel: () => Navigator.pop(context), onCancel: () => Navigator.pop(context),
onSubmit: () => Navigator.pop(context, { onSubmit: () => Navigator.pop(context, {
'selectedTab': tempSelectedTab,
'selectedOrganization': widget.controller.selectedOrganization?.id, 'selectedOrganization': widget.controller.selectedOrganization?.id,
}), }),
child: Padding( child: Padding(
padding: padding: const EdgeInsets.only(bottom: 24),
const EdgeInsets.only(bottom: 24), // FIX: extra safe padding
child: SingleChildScrollView( child: SingleChildScrollView(
// FIX: full scrollable in landscape
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: buildMainFilters(), children: _buildFilters(),
), ),
), ),
), ),

View File

@ -3,7 +3,6 @@ import 'package:get/get.dart';
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart'; import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart';
import 'package:on_field_work/helpers/utils/date_time_utils.dart'; import 'package:on_field_work/helpers/utils/date_time_utils.dart';
import 'package:on_field_work/helpers/widgets/avatar.dart'; import 'package:on_field_work/helpers/widgets/avatar.dart';
import 'package:on_field_work/helpers/widgets/my_card.dart';
import 'package:on_field_work/helpers/widgets/my_container.dart'; import 'package:on_field_work/helpers/widgets/my_container.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
@ -12,21 +11,14 @@ import 'package:on_field_work/model/attendance/log_details_view.dart';
import 'package:on_field_work/model/attendance/attendence_action_button.dart'; import 'package:on_field_work/model/attendance/attendence_action_button.dart';
import 'package:on_field_work/helpers/utils/attendance_actions.dart'; import 'package:on_field_work/helpers/utils/attendance_actions.dart';
class AttendanceLogsTab extends StatefulWidget { class AttendanceLogsTab extends StatelessWidget {
final AttendanceController controller; final AttendanceController controller;
const AttendanceLogsTab({super.key, required this.controller}); const AttendanceLogsTab({super.key, required this.controller});
@override
State<AttendanceLogsTab> createState() => _AttendanceLogsTabState();
}
class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
Widget _buildStatusHeader() { Widget _buildStatusHeader() {
return Obx(() { return Obx(() {
if (!widget.controller.showPendingOnly.value) { if (!controller.showPendingOnly.value) return const SizedBox.shrink();
return const SizedBox.shrink();
}
return Container( return Container(
width: double.infinity, width: double.infinity,
@ -46,7 +38,7 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
), ),
), ),
InkWell( InkWell(
onTap: () => widget.controller.showPendingOnly.value = false, onTap: () => controller.showPendingOnly.value = false,
child: const Icon(Icons.close, size: 18, color: Colors.orange), child: const Icon(Icons.close, size: 18, color: Colors.orange),
), ),
], ],
@ -55,7 +47,6 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
}); });
} }
/// Return button text priority for sorting inside same date
int _getActionPriority(employee) { int _getActionPriority(employee) {
final text = AttendanceButtonHelper.getButtonText( final text = AttendanceButtonHelper.getButtonText(
activity: employee.activity, activity: employee.activity,
@ -77,32 +68,20 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
final isCheckoutAction = final isCheckoutAction =
text.contains("checkout") || text.contains("check out"); text.contains("checkout") || text.contains("check out");
int priority; if (isYesterdayCheckIn && isMissingCheckout && isCheckoutAction) return 0;
if (isYesterdayCheckIn && isMissingCheckout && isCheckoutAction) { if (isCheckoutAction) return 0;
priority = 0; if (text.contains("regular")) return 1;
} else if (isCheckoutAction) { if (text == "requested") return 2;
priority = 0; if (text == "approved") return 3;
} else if (text.contains("regular")) { if (text == "rejected") return 4;
priority = 1; return 5;
} else if (text == "requested") {
priority = 2;
} else if (text == "approved") {
priority = 3;
} else if (text == "rejected") {
priority = 4;
} else {
priority = 5;
}
return priority;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
final allLogs = List.of(widget.controller.filteredLogs); final allLogs = List.of(controller.filteredLogs);
final showPendingOnly = controller.showPendingOnly.value;
// Filter logs if "pending only"
final showPendingOnly = widget.controller.showPendingOnly.value;
final filteredLogs = showPendingOnly final filteredLogs = showPendingOnly
? allLogs.where((emp) => emp.activity == 1).toList() ? allLogs.where((emp) => emp.activity == 1).toList()
: allLogs; : allLogs;
@ -116,7 +95,6 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
groupedLogs.putIfAbsent(dateKey, () => []).add(log); groupedLogs.putIfAbsent(dateKey, () => []).add(log);
} }
// Sort dates (latest first)
final sortedDates = groupedLogs.keys.toList() final sortedDates = groupedLogs.keys.toList()
..sort((a, b) { ..sort((a, b) {
final da = DateTimeUtils.parseDate(a, 'dd MMM yyyy') ?? DateTime(0); final da = DateTimeUtils.parseDate(a, 'dd MMM yyyy') ?? DateTime(0);
@ -125,174 +103,187 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
}); });
final dateRangeText = final dateRangeText =
'${DateTimeUtils.formatDate(widget.controller.startDateAttendance.value, 'dd MMM yyyy')} - ' '${DateTimeUtils.formatDate(controller.startDateAttendance.value, 'dd MMM yyyy')} - '
'${DateTimeUtils.formatDate(widget.controller.endDateAttendance.value, 'dd MMM yyyy')}'; '${DateTimeUtils.formatDate(controller.endDateAttendance.value, 'dd MMM yyyy')}';
return SingleChildScrollView( // Sticky header + scrollable list
padding: MySpacing.only(bottom: 80), // Added bottom spacing for scroll view return Column(
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, // Header Row
children: [ Padding(
// Header row padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
Padding( child: Row(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, controller.isLoadingAttendanceLogs.value
children: [ ? SkeletonLoaders.dateSkeletonLoader()
MyText.titleMedium("Attendance Logs", fontWeight: 600), : MyText.bodySmall(
widget.controller.isLoading.value dateRangeText,
? SkeletonLoaders.dateSkeletonLoader() fontWeight: 600,
: MyText.bodySmall( color: Colors.grey[700],
dateRangeText, overflow: TextOverflow.ellipsis,
fontWeight: 600,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
),
],
),
),
// Pending-only header
_buildStatusHeader(),
MySpacing.height(8),
// Content: loader, empty, or logs
if (widget.controller.isLoadingAttendanceLogs.value)
SkeletonLoaders.employeeListSkeletonLoader()
else if (filteredLogs.isEmpty)
SizedBox(
height: 120,
child: Center(
child: Text(showPendingOnly
? "No Pending Actions Found"
: "No Attendance Logs Found for this Project"),
),
)
else
MyCard.bordered(
paddingAll: 8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final date in sortedDates) ...[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: MyText.bodyMedium(date, fontWeight: 700),
), ),
],
),
),
// Sort employees inside this date by action priority first, then latest entry // Pending-only header
for (final emp in (groupedLogs[date]! _buildStatusHeader(),
..sort(
(a, b) {
final priorityCompare = _getActionPriority(a)
.compareTo(_getActionPriority(b));
if (priorityCompare != 0) return priorityCompare;
final aTime = a.checkOut ?? a.checkIn ?? DateTime(0); // Divider between header and list
final bTime = b.checkOut ?? b.checkIn ?? DateTime(0); const Divider(height: 1),
return bTime.compareTo(
aTime); // Scrollable attendance logs
}, Expanded(
))) ...[ child: controller.isLoadingAttendanceLogs.value
MyContainer( ? SkeletonLoaders.employeeListSkeletonLoader()
paddingAll: 8, : filteredLogs.isEmpty
child: Row( ? Center(
crossAxisAlignment: CrossAxisAlignment.start, child: Text(showPendingOnly
children: [ ? "No Pending Actions Found"
Avatar( : "No Attendance Logs Found for this Project"),
firstName: emp.firstName, )
lastName: emp.lastName, : ListView.builder(
size: 31, padding: MySpacing.all(8),
), itemCount: sortedDates.length,
MySpacing.width(16), itemBuilder: (context, dateIndex) {
Expanded( final date = sortedDates[dateIndex];
child: Column( final employees = groupedLogs[date]!
crossAxisAlignment: CrossAxisAlignment.start, ..sort((a, b) {
children: [ final priorityCompare = _getActionPriority(a)
Row( .compareTo(_getActionPriority(b));
if (priorityCompare != 0) return priorityCompare;
final aTime =
a.checkOut ?? a.checkIn ?? DateTime(0);
final bTime =
b.checkOut ?? b.checkIn ?? DateTime(0);
return bTime.compareTo(aTime);
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: MyText.bodyMedium(date, fontWeight: 700),
),
...employees.map(
(emp) => Column(
children: [
MyContainer(
paddingAll: 8,
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Flexible( Avatar(
child: MyText.bodyMedium( firstName: emp.firstName,
emp.name, lastName: emp.lastName,
fontWeight: 600, size: 31,
overflow: TextOverflow.ellipsis,
),
), ),
MySpacing.width(6), MySpacing.width(16),
Flexible( Expanded(
child: MyText.bodySmall( child: Column(
'(${emp.designation})', crossAxisAlignment:
fontWeight: 600, CrossAxisAlignment.start,
color: Colors.grey[700], children: [
overflow: TextOverflow.ellipsis, Row(
children: [
Flexible(
child: MyText.bodyMedium(
emp.name,
fontWeight: 600,
overflow: TextOverflow
.ellipsis,
),
),
MySpacing.width(6),
Flexible(
child: MyText.bodySmall(
'(${emp.designation})',
fontWeight: 600,
color: Colors.grey[700],
overflow: TextOverflow
.ellipsis,
),
),
],
),
MySpacing.height(8),
if (emp.checkIn != null ||
emp.checkOut != null)
Row(
children: [
if (emp.checkIn !=
null) ...[
const Icon(
Icons
.arrow_circle_right,
size: 16,
color:
Colors.green),
MySpacing.width(4),
MyText.bodySmall(
DateTimeUtils
.formatDate(
emp.checkIn!,
'hh:mm a'),
fontWeight: 600,
),
MySpacing.width(16),
],
if (emp.checkOut !=
null) ...[
const Icon(
Icons
.arrow_circle_left,
size: 16,
color: Colors.red),
MySpacing.width(4),
MyText.bodySmall(
DateTimeUtils
.formatDate(
emp.checkOut!,
'hh:mm a'),
fontWeight: 600,
),
],
],
),
MySpacing.height(12),
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
AttendanceActionButton(
employee: emp,
attendanceController:
controller,
),
MySpacing.width(8),
AttendanceLogViewButton(
employee: emp,
attendanceController:
controller,
),
],
),
],
), ),
), ),
], ],
), ),
MySpacing.height(8), ),
if (emp.checkIn != null || ],
emp.checkOut != null)
Row(
children: [
if (emp.checkIn != null) ...[
const Icon(Icons.arrow_circle_right,
size: 16, color: Colors.green),
MySpacing.width(4),
MyText.bodySmall(
DateTimeUtils.formatDate(
emp.checkIn!, 'hh:mm a'),
fontWeight: 600,
),
MySpacing.width(16),
],
if (emp.checkOut != null) ...[
const Icon(Icons.arrow_circle_left,
size: 16, color: Colors.red),
MySpacing.width(4),
MyText.bodySmall(
DateTimeUtils.formatDate(
emp.checkOut!, 'hh:mm a'),
fontWeight: 600,
),
],
],
),
MySpacing.height(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AttendanceActionButton(
employee: emp,
attendanceController: widget.controller,
),
MySpacing.width(8),
AttendanceLogViewButton(
employee: emp,
attendanceController: widget.controller,
),
],
),
],
),
), ),
], ),
), ],
), );
Divider(color: Colors.grey.withOpacity(0.3)), },
], ),
], ),
// Remove the trailing Divider if we are at the end of the logs ],
if (sortedDates.isNotEmpty)
// We can use MySpacing.height(8) here if we need to ensure the last divider doesn't show
// But keeping the original structure, the divider is inside the inner loop.
// A clean up would be needed to manage that last divider, but for now,
// the bottom padding handles the visible spacing.
const SizedBox.shrink(),
],
),
),
],
),
); );
}); });
} }

View File

@ -61,9 +61,9 @@ class _AttendanceScreenState extends State<AttendanceScreen>
void _initializeTabs() async { void _initializeTabs() async {
final allTabs = [ final allTabs = [
{'label': "Today's Attendance", 'value': 'todaysAttendance'}, {'label': "Today's", 'value': 'todaysAttendance'},
{'label': "Attendance Logs", 'value': 'attendanceLogs'}, {'label': "Logs", 'value': 'attendanceLogs'},
{'label': "Regularization Requests", 'value': 'regularizationRequests'}, {'label': "Regularization", 'value': 'regularizationRequests'},
]; ];
final hasRegularizationPermission = final hasRegularizationPermission =
@ -133,8 +133,7 @@ class _AttendanceScreenState extends State<AttendanceScreen>
final query = attendanceController.searchQuery.value; final query = attendanceController.searchQuery.value;
return TextField( return TextField(
controller: TextEditingController(text: query) controller: TextEditingController(text: query)
..selection = ..selection = TextSelection.collapsed(offset: query.length),
TextSelection.collapsed(offset: query.length),
onChanged: (value) { onChanged: (value) {
attendanceController.searchQuery.value = value; attendanceController.searchQuery.value = value;
}, },
@ -325,8 +324,9 @@ class _AttendanceScreenState extends State<AttendanceScreen>
child: noProjectSelected child: noProjectSelected
? _buildNoProjectWidget() ? _buildNoProjectWidget()
: SizedBox( : SizedBox(
height: MediaQuery.of(context).size.height - height:
200, MediaQuery.of(context).size.height -
200,
child: _buildTabBarView(), child: _buildTabBarView(),
), ),
), ),

View File

@ -1,4 +1,3 @@
// lib/view/attendance/tabs/regularization_requests_tab.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -19,140 +18,136 @@ class RegularizationRequestsTab extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Obx(() {
crossAxisAlignment: CrossAxisAlignment.start, final isLoading = controller.isLoadingRegularizationLogs.value;
children: [ final employees = controller.filteredRegularizationLogs;
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 4.0),
child: MyText.titleMedium("Regularization Requests", fontWeight: 600),
),
Obx(() {
final employees = controller.filteredRegularizationLogs;
if (controller.isLoadingRegularizationLogs.value) { if (isLoading) {
return SkeletonLoaders.employeeListSkeletonLoader(); return SkeletonLoaders.employeeListSkeletonLoader();
} }
if (employees.isEmpty) { if (employees.isEmpty) {
return const SizedBox( return const SizedBox(
height: 120, height: 120,
child: Center( child: Center(
child: child: Text("No Regularization Requests Found for this Project"),
Text("No Regularization Requests Found for this Project"), ),
), );
); }
}
return MyCard.bordered( return ListView.builder(
paddingAll: 8, itemCount: employees.length,
child: Column( padding: MySpacing.only(bottom: 80),
children: List.generate(employees.length, (index) { itemBuilder: (context, index) {
final employee = employees[index]; final employee = employees[index]; // Corrected index
return Column(
children: [ return Padding(
MyContainer( padding: const EdgeInsets.only(bottom: 12),
paddingAll: 8, child: MyCard.bordered(
child: Row( paddingAll: 8,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ children: [
Avatar( MyContainer(
firstName: employee.firstName, paddingAll: 8,
lastName: employee.lastName, child: Row(
size: 31, crossAxisAlignment: CrossAxisAlignment.start,
), children: [
MySpacing.width(16), Avatar(
Expanded( firstName: employee.firstName,
child: Column( lastName: employee.lastName,
crossAxisAlignment: CrossAxisAlignment.start, size: 35,
children: [ ),
Row( MySpacing.width(16),
children: [ Expanded(
Flexible( child: Column(
child: MyText.bodyMedium( crossAxisAlignment: CrossAxisAlignment.start,
employee.name, children: [
fontWeight: 600, Row(
overflow: TextOverflow.ellipsis, children: [
), Flexible(
child: MyText.bodyMedium(
employee.name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
), ),
MySpacing.width(6),
Flexible(
child: MyText.bodySmall(
'(${employee.role})',
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
),
MySpacing.height(8),
if (employee.checkIn != null ||
employee.checkOut != null)
Row(
children: [
if (employee.checkIn != null) ...[
const Icon(Icons.arrow_circle_right,
size: 16, color: Colors.green),
MySpacing.width(4),
MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee.checkIn!),
fontWeight: 600,
),
MySpacing.width(16),
],
if (employee.checkOut != null) ...[
const Icon(Icons.arrow_circle_left,
size: 16, color: Colors.red),
MySpacing.width(4),
MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee.checkOut!),
fontWeight: 600,
),
],
],
), ),
MySpacing.height(12), MySpacing.width(6),
Flexible(
child: MyText.bodySmall(
'(${employee.role})',
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
),
MySpacing.height(8),
if (employee.checkIn != null ||
employee.checkOut != null)
Row( Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
RegularizeActionButton( if (employee.checkIn != null) ...[
attendanceController: controller, const Icon(Icons.arrow_circle_right,
log: employee, size: 16, color: Colors.green),
uniqueLogKey: employee.employeeId, MySpacing.width(4),
action: ButtonActions.approve, MyText.bodySmall(
), DateFormat('hh:mm a')
const SizedBox(width: 8), .format(employee.checkIn!),
RegularizeActionButton( fontWeight: 600,
attendanceController: controller,
log: employee,
uniqueLogKey: employee.employeeId,
action: ButtonActions.reject,
),
const SizedBox(width: 8),
if (employee.checkIn != null)
AttendanceLogViewButton(
employee: employee,
attendanceController: controller,
), ),
MySpacing.width(16),
],
if (employee.checkOut != null) ...[
const Icon(Icons.arrow_circle_left,
size: 16, color: Colors.red),
MySpacing.width(4),
MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee.checkOut!),
fontWeight: 600,
),
],
], ],
), ),
], MySpacing.height(12),
), Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
RegularizeActionButton(
attendanceController: controller,
log: employee,
uniqueLogKey: employee.employeeId,
action: ButtonActions.approve,
),
const SizedBox(width: 8),
RegularizeActionButton(
attendanceController: controller,
log: employee,
uniqueLogKey: employee.employeeId,
action: ButtonActions.reject,
),
const SizedBox(width: 8),
if (employee.checkIn != null)
AttendanceLogViewButton(
employee: employee,
attendanceController: controller,
),
],
),
],
), ),
], ),
), ],
), ),
if (index != employees.length - 1) ),
Divider(color: Colors.grey.withOpacity(0.3)),
], ],
); ),
}),
), ),
); );
}), },
], );
); });
} }
} }

View File

@ -4,7 +4,6 @@ import 'package:on_field_work/controller/attendance/attendance_screen_controller
import 'package:on_field_work/helpers/utils/date_time_utils.dart'; import 'package:on_field_work/helpers/utils/date_time_utils.dart';
import 'package:on_field_work/helpers/widgets/avatar.dart'; import 'package:on_field_work/helpers/widgets/avatar.dart';
import 'package:on_field_work/helpers/widgets/my_card.dart'; import 'package:on_field_work/helpers/widgets/my_card.dart';
import 'package:on_field_work/helpers/widgets/my_container.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart'; import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
@ -36,11 +35,9 @@ class TodaysAttendanceTab extends StatelessWidget {
} }
return ListView.builder( return ListView.builder(
itemCount: employees.length + 1, // +1 for header itemCount: employees.length + 1,
padding: MySpacing.only( padding: MySpacing.only(bottom: 80),
bottom: 80), // Adjusted padding to add spacing at the bottom
itemBuilder: (context, index) { itemBuilder: (context, index) {
// --- Header Row ---
if (index == 0) { if (index == 0) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12, top: 4), padding: const EdgeInsets.only(bottom: 12, top: 4),
@ -59,68 +56,62 @@ class TodaysAttendanceTab extends StatelessWidget {
final employee = employees[index - 1]; final employee = employees[index - 1];
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 8),
child: MyCard.bordered( child: MyCard.bordered(
paddingAll: 12, paddingAll: 10,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 1. Employee Info Row // --- 1. Employee Info Row (Avatar, Name, Designation ONLY) ---
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Avatar
Avatar( Avatar(
firstName: employee.firstName, firstName: employee.firstName,
lastName: employee.lastName, lastName: employee.lastName,
size: 35, size: 30,
), ),
MySpacing.width(16), MySpacing.width(10),
// Employee Details (Expanded to use remaining space)
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
MyText.titleMedium(employee.name, fontWeight: 600), MyText.titleSmall(employee.name,
MySpacing.height(2), fontWeight: 600, overflow: TextOverflow.ellipsis),
MyText.bodySmall( MyText.labelSmall(
employee.designation, employee.designation,
fontWeight: 500, fontWeight: 500,
color: Colors.grey[600], color: Colors.grey[600],
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
), ),
// Status Text (Added back for context)
if (employee.checkIn == null)
MyText.bodySmall(
'Check In Pending',
fontWeight: 600,
color: Colors.red,
)
else if (employee.checkOut == null)
MyText.bodySmall(
'Checked In',
fontWeight: 600,
color: Colors.green,
),
], ],
), ),
// Separator // --- Separator before buttons ---
if (employee.checkIn != null || employee.checkOut != null) MySpacing.height(12),
const Divider(height: 24),
// 2. Attendance Time Details Row // --- 2. Action Buttons Row (Below main info) ---
if (employee.checkIn != null || employee.checkOut != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Check-in Time
_buildLogTime(
icon: Icons.login,
color: Colors.green,
label: 'Check-in',
time: employee.checkIn,
),
// Check-out Time
_buildLogTime(
icon: Icons.logout,
color: Colors.red,
label: 'Check-out',
time: employee.checkOut,
),
],
),
// 3. Action Buttons Row
MySpacing.height(16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -145,50 +136,4 @@ class TodaysAttendanceTab extends StatelessWidget {
); );
}); });
} }
// Helper function to build a cleaner log time widget
Widget _buildLogTime({
required IconData icon,
required Color color,
required String label,
required DateTime? time,
}) {
if (time == null) {
return MyContainer(
padding: MySpacing.xy(12, 6),
borderRadiusAll: 5,
color: Colors.grey[100],
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey),
MySpacing.width(6),
MyText.bodySmall('$label: **N/A**',
fontWeight: 600, color: Colors.grey),
],
),
);
}
return MyContainer(
padding: MySpacing.xy(12, 6),
borderRadiusAll: 6,
color: color.withOpacity(0.1),
child: Row(
children: [
Icon(icon, size: 16, color: color),
MySpacing.width(6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelSmall(label, color: color, fontWeight: 600),
MyText.bodyMedium(
DateTimeUtils.formatDate(time, 'hh:mm a'),
fontWeight: 600,
color: Colors.black87,
),
],
),
],
),
);
}
} }