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/attendance/attendance_screen_controller.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: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';
class AttendanceFilterBottomSheet extends StatefulWidget {
@ -27,21 +25,6 @@ class AttendanceFilterBottomSheet extends StatefulWidget {
class _AttendanceFilterBottomSheetState
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({
required String currentValue,
@ -51,12 +34,8 @@ class _AttendanceFilterBottomSheetState
return PopupMenuButton<String>(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onSelected: onSelected,
itemBuilder: (context) => items
.map((e) => PopupMenuItem<String>(
value: e,
child: MyText(e),
))
.toList(),
itemBuilder: (context) =>
items.map((e) => PopupMenuItem<String>(value: e, child: MyText(e))).toList(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
@ -107,46 +86,11 @@ class _AttendanceFilterBottomSheetState
);
}
List<Widget> buildMainFilters() {
final hasRegularizationPermission = widget.permissionController
.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!),
);
}),
];
List<Widget> _buildFilters() {
final List<Widget> widgets = [];
// Organization selector
widgets.addAll([
const Divider(),
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 12),
child: Align(
@ -180,7 +124,8 @@ class _AttendanceFilterBottomSheetState
}),
]);
if (tempSelectedTab == 'attendanceLogs') {
// Date range (only for Attendance Logs)
if (widget.selectedTab == 'attendanceLogs') {
widgets.addAll([
const Divider(),
Padding(
@ -208,24 +153,20 @@ class _AttendanceFilterBottomSheetState
@override
Widget build(BuildContext context) {
return SafeArea(
// FIX: avoids hiding under navigation buttons
child: BaseBottomSheet(
title: "Attendance Filter",
submitText: "Apply",
onCancel: () => Navigator.pop(context),
onSubmit: () => Navigator.pop(context, {
'selectedTab': tempSelectedTab,
'selectedOrganization': widget.controller.selectedOrganization?.id,
}),
child: Padding(
padding:
const EdgeInsets.only(bottom: 24), // FIX: extra safe padding
padding: const EdgeInsets.only(bottom: 24),
child: SingleChildScrollView(
// FIX: full scrollable in landscape
physics: const BouncingScrollPhysics(),
child: Column(
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/helpers/utils/date_time_utils.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_spacing.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/helpers/utils/attendance_actions.dart';
class AttendanceLogsTab extends StatefulWidget {
class AttendanceLogsTab extends StatelessWidget {
final AttendanceController controller;
const AttendanceLogsTab({super.key, required this.controller});
@override
State<AttendanceLogsTab> createState() => _AttendanceLogsTabState();
}
class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
Widget _buildStatusHeader() {
return Obx(() {
if (!widget.controller.showPendingOnly.value) {
return const SizedBox.shrink();
}
if (!controller.showPendingOnly.value) return const SizedBox.shrink();
return Container(
width: double.infinity,
@ -46,7 +38,7 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
),
),
InkWell(
onTap: () => widget.controller.showPendingOnly.value = false,
onTap: () => controller.showPendingOnly.value = false,
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) {
final text = AttendanceButtonHelper.getButtonText(
activity: employee.activity,
@ -77,32 +68,20 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
final isCheckoutAction =
text.contains("checkout") || text.contains("check out");
int priority;
if (isYesterdayCheckIn && isMissingCheckout && isCheckoutAction) {
priority = 0;
} else if (isCheckoutAction) {
priority = 0;
} else if (text.contains("regular")) {
priority = 1;
} else if (text == "requested") {
priority = 2;
} else if (text == "approved") {
priority = 3;
} else if (text == "rejected") {
priority = 4;
} else {
priority = 5;
}
return priority;
if (isYesterdayCheckIn && isMissingCheckout && isCheckoutAction) return 0;
if (isCheckoutAction) return 0;
if (text.contains("regular")) return 1;
if (text == "requested") return 2;
if (text == "approved") return 3;
if (text == "rejected") return 4;
return 5;
}
@override
Widget build(BuildContext context) {
return Obx(() {
final allLogs = List.of(widget.controller.filteredLogs);
// Filter logs if "pending only"
final showPendingOnly = widget.controller.showPendingOnly.value;
final allLogs = List.of(controller.filteredLogs);
final showPendingOnly = controller.showPendingOnly.value;
final filteredLogs = showPendingOnly
? allLogs.where((emp) => emp.activity == 1).toList()
: allLogs;
@ -116,7 +95,6 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
groupedLogs.putIfAbsent(dateKey, () => []).add(log);
}
// Sort dates (latest first)
final sortedDates = groupedLogs.keys.toList()
..sort((a, b) {
final da = DateTimeUtils.parseDate(a, 'dd MMM yyyy') ?? DateTime(0);
@ -125,175 +103,188 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
});
final dateRangeText =
'${DateTimeUtils.formatDate(widget.controller.startDateAttendance.value, 'dd MMM yyyy')} - '
'${DateTimeUtils.formatDate(widget.controller.endDateAttendance.value, 'dd MMM yyyy')}';
'${DateTimeUtils.formatDate(controller.startDateAttendance.value, 'dd MMM yyyy')} - '
'${DateTimeUtils.formatDate(controller.endDateAttendance.value, 'dd MMM yyyy')}';
return SingleChildScrollView(
padding: MySpacing.only(bottom: 80), // Added bottom spacing for scroll view
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header row
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium("Attendance Logs", fontWeight: 600),
widget.controller.isLoading.value
? SkeletonLoaders.dateSkeletonLoader()
: MyText.bodySmall(
dateRangeText,
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),
// Sticky header + scrollable list
return Column(
children: [
// Header Row
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
controller.isLoadingAttendanceLogs.value
? SkeletonLoaders.dateSkeletonLoader()
: MyText.bodySmall(
dateRangeText,
fontWeight: 600,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
),
],
),
),
// Sort employees inside this date by action priority first, then latest entry
for (final emp in (groupedLogs[date]!
..sort(
(a, b) {
final priorityCompare = _getActionPriority(a)
.compareTo(_getActionPriority(b));
if (priorityCompare != 0) return priorityCompare;
// Pending-only header
_buildStatusHeader(),
final aTime = a.checkOut ?? a.checkIn ?? DateTime(0);
final bTime = b.checkOut ?? b.checkIn ?? DateTime(0);
return bTime.compareTo(
aTime);
},
))) ...[
MyContainer(
paddingAll: 8,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: emp.firstName,
lastName: emp.lastName,
size: 31,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
// Divider between header and list
const Divider(height: 1),
// Scrollable attendance logs
Expanded(
child: controller.isLoadingAttendanceLogs.value
? SkeletonLoaders.employeeListSkeletonLoader()
: filteredLogs.isEmpty
? Center(
child: Text(showPendingOnly
? "No Pending Actions Found"
: "No Attendance Logs Found for this Project"),
)
: ListView.builder(
padding: MySpacing.all(8),
itemCount: sortedDates.length,
itemBuilder: (context, dateIndex) {
final date = sortedDates[dateIndex];
final employees = groupedLogs[date]!
..sort((a, b) {
final priorityCompare = _getActionPriority(a)
.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: [
Flexible(
child: MyText.bodyMedium(
emp.name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
),
Avatar(
firstName: emp.firstName,
lastName: emp.lastName,
size: 31,
),
MySpacing.width(6),
Flexible(
child: MyText.bodySmall(
'(${emp.designation})',
fontWeight: 600,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
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 {
final allTabs = [
{'label': "Today's Attendance", 'value': 'todaysAttendance'},
{'label': "Attendance Logs", 'value': 'attendanceLogs'},
{'label': "Regularization Requests", 'value': 'regularizationRequests'},
{'label': "Today's", 'value': 'todaysAttendance'},
{'label': "Logs", 'value': 'attendanceLogs'},
{'label': "Regularization", 'value': 'regularizationRequests'},
];
final hasRegularizationPermission =
@ -133,8 +133,7 @@ class _AttendanceScreenState extends State<AttendanceScreen>
final query = attendanceController.searchQuery.value;
return TextField(
controller: TextEditingController(text: query)
..selection =
TextSelection.collapsed(offset: query.length),
..selection = TextSelection.collapsed(offset: query.length),
onChanged: (value) {
attendanceController.searchQuery.value = value;
},
@ -325,8 +324,9 @@ class _AttendanceScreenState extends State<AttendanceScreen>
child: noProjectSelected
? _buildNoProjectWidget()
: SizedBox(
height: MediaQuery.of(context).size.height -
200,
height:
MediaQuery.of(context).size.height -
200,
child: _buildTabBarView(),
),
),

View File

@ -1,4 +1,3 @@
// lib/view/attendance/tabs/regularization_requests_tab.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
@ -19,140 +18,136 @@ class RegularizationRequestsTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 4.0),
child: MyText.titleMedium("Regularization Requests", fontWeight: 600),
),
Obx(() {
final employees = controller.filteredRegularizationLogs;
return Obx(() {
final isLoading = controller.isLoadingRegularizationLogs.value;
final employees = controller.filteredRegularizationLogs;
if (controller.isLoadingRegularizationLogs.value) {
return SkeletonLoaders.employeeListSkeletonLoader();
}
if (isLoading) {
return SkeletonLoaders.employeeListSkeletonLoader();
}
if (employees.isEmpty) {
return const SizedBox(
height: 120,
child: Center(
child:
Text("No Regularization Requests Found for this Project"),
),
);
}
if (employees.isEmpty) {
return const SizedBox(
height: 120,
child: Center(
child: Text("No Regularization Requests Found for this Project"),
),
);
}
return MyCard.bordered(
paddingAll: 8,
child: Column(
children: List.generate(employees.length, (index) {
final employee = employees[index];
return Column(
children: [
MyContainer(
paddingAll: 8,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 31,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: MyText.bodyMedium(
employee.name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
),
return ListView.builder(
itemCount: employees.length,
padding: MySpacing.only(bottom: 80),
itemBuilder: (context, index) {
final employee = employees[index]; // Corrected index
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: MyCard.bordered(
paddingAll: 8,
child: Column(
children: [
MyContainer(
paddingAll: 8,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 35,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
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(
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 (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),
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/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_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
@ -36,11 +35,9 @@ class TodaysAttendanceTab extends StatelessWidget {
}
return ListView.builder(
itemCount: employees.length + 1, // +1 for header
padding: MySpacing.only(
bottom: 80), // Adjusted padding to add spacing at the bottom
itemCount: employees.length + 1,
padding: MySpacing.only(bottom: 80),
itemBuilder: (context, index) {
// --- Header Row ---
if (index == 0) {
return Padding(
padding: const EdgeInsets.only(bottom: 12, top: 4),
@ -59,68 +56,62 @@ class TodaysAttendanceTab extends StatelessWidget {
final employee = employees[index - 1];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.only(bottom: 8),
child: MyCard.bordered(
paddingAll: 12,
paddingAll: 10,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Employee Info Row
// --- 1. Employee Info Row (Avatar, Name, Designation ONLY) ---
Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Avatar
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 35,
size: 30,
),
MySpacing.width(16),
MySpacing.width(10),
// Employee Details (Expanded to use remaining space)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.titleMedium(employee.name, fontWeight: 600),
MySpacing.height(2),
MyText.bodySmall(
MyText.titleSmall(employee.name,
fontWeight: 600, overflow: TextOverflow.ellipsis),
MyText.labelSmall(
employee.designation,
fontWeight: 500,
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
if (employee.checkIn != null || employee.checkOut != null)
const Divider(height: 24),
// 2. Attendance Time Details Row
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),
// --- Separator before buttons ---
MySpacing.height(12),
// --- 2. Action Buttons Row (Below main info) ---
Row(
mainAxisAlignment: MainAxisAlignment.end,
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,
),
],
),
],
),
);
}
}
}