Enhance attendance management with tabbed navigation and permission handling; improve UI consistency and loading states
This commit is contained in:
parent
7bef2e9d89
commit
37ce612fca
@ -15,6 +15,9 @@ class PermissionController extends GetxController {
|
||||
Timer? _refreshTimer;
|
||||
var isLoading = true.obs;
|
||||
|
||||
/// ← NEW: reactive flag to signal permissions are loaded
|
||||
var permissionsLoaded = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -52,6 +55,10 @@ class PermissionController extends GetxController {
|
||||
_updateState(userData);
|
||||
await _storeData();
|
||||
logSafe("Data loaded and state updated successfully.");
|
||||
|
||||
// ← NEW: mark permissions as loaded
|
||||
permissionsLoaded.value = true;
|
||||
|
||||
} catch (e, stacktrace) {
|
||||
logSafe("Error loading data from API",
|
||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||
@ -103,7 +110,7 @@ class PermissionController extends GetxController {
|
||||
}
|
||||
|
||||
void _startAutoRefresh() {
|
||||
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
|
||||
_refreshTimer = Timer.periodic(const Duration(minutes: 30), (timer) async {
|
||||
logSafe("Auto-refresh triggered.");
|
||||
final token = await _getAuthToken();
|
||||
if (token?.isNotEmpty ?? false) {
|
||||
@ -117,8 +124,6 @@ class PermissionController extends GetxController {
|
||||
|
||||
bool hasPermission(String permissionId) {
|
||||
final hasPerm = permissions.any((p) => p.id == permissionId);
|
||||
// logSafe("Checking permission $permissionId: $hasPerm",
|
||||
// level: LogLevel.debug);
|
||||
return hasPerm;
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ class PillTabBar extends StatelessWidget {
|
||||
final Color unselectedColor;
|
||||
final Color indicatorColor;
|
||||
final double height;
|
||||
|
||||
final ValueChanged<int>? onTap;
|
||||
const PillTabBar({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
@ -16,6 +16,7 @@ class PillTabBar extends StatelessWidget {
|
||||
this.unselectedColor = Colors.grey,
|
||||
this.indicatorColor = Colors.blueAccent,
|
||||
this.height = 48,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -42,7 +43,8 @@ class PillTabBar extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(height / 2),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
indicatorPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
labelColor: selectedColor,
|
||||
unselectedLabelColor: unselectedColor,
|
||||
labelStyle: const TextStyle(
|
||||
|
||||
@ -128,7 +128,9 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
|
||||
'${DateTimeUtils.formatDate(widget.controller.startDateAttendance.value, 'dd MMM yyyy')} - '
|
||||
'${DateTimeUtils.formatDate(widget.controller.endDateAttendance.value, 'dd MMM yyyy')}';
|
||||
|
||||
return Column(
|
||||
return SingleChildScrollView(
|
||||
padding: MySpacing.only(bottom: 80), // Added bottom spacing for scroll view
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header row
|
||||
@ -279,10 +281,18 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_flex.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_flex_item.dart';
|
||||
@ -8,13 +7,15 @@ 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/controller/attendance/attendance_screen_controller.dart';
|
||||
import 'package:on_field_work/controller/permission_controller.dart';
|
||||
import 'package:on_field_work/model/attendance/attendence_filter_sheet.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:on_field_work/view/Attendence/regularization_requests_tab.dart';
|
||||
import 'package:on_field_work/view/Attendence/attendance_logs_tab.dart';
|
||||
import 'package:on_field_work/view/Attendence/todays_attendance_tab.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart';
|
||||
import 'package:on_field_work/model/attendance/attendence_filter_sheet.dart';
|
||||
|
||||
class AttendanceScreen extends StatefulWidget {
|
||||
const AttendanceScreen({super.key});
|
||||
@ -23,43 +24,83 @@ class AttendanceScreen extends StatefulWidget {
|
||||
State<AttendanceScreen> createState() => _AttendanceScreenState();
|
||||
}
|
||||
|
||||
class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
class _AttendanceScreenState extends State<AttendanceScreen>
|
||||
with SingleTickerProviderStateMixin, UIMixin {
|
||||
final attendanceController = Get.put(AttendanceController());
|
||||
final permissionController = Get.put(PermissionController());
|
||||
final projectController = Get.find<ProjectController>();
|
||||
final projectController = Get.put(ProjectController());
|
||||
|
||||
String selectedTab = 'todaysAttendance';
|
||||
late TabController _tabController;
|
||||
late List<Map<String, String>> _tabs;
|
||||
bool _tabsInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Watch permissions loaded
|
||||
ever(permissionController.permissionsLoaded, (loaded) {
|
||||
if (loaded == true && !_tabsInitialized) {
|
||||
_initializeTabs();
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
|
||||
// Watch project changes to reload data
|
||||
ever<String>(projectController.selectedProjectId, (projectId) async {
|
||||
if (projectId.isNotEmpty) await _loadData(projectId);
|
||||
if (projectId.isNotEmpty && _tabsInitialized) {
|
||||
await _fetchTabData(attendanceController.selectedTab);
|
||||
}
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// If permissions are already loaded at init
|
||||
if (permissionController.permissionsLoaded.value) {
|
||||
_initializeTabs();
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeTabs() async {
|
||||
final allTabs = [
|
||||
{'label': "Today's Attendance", 'value': 'todaysAttendance'},
|
||||
{'label': "Attendance Logs", 'value': 'attendanceLogs'},
|
||||
{'label': "Regularization Requests", 'value': 'regularizationRequests'},
|
||||
];
|
||||
|
||||
final hasRegularizationPermission =
|
||||
permissionController.hasPermission(Permissions.regularizeAttendance);
|
||||
|
||||
_tabs = allTabs.where((tab) {
|
||||
return tab['value'] != 'regularizationRequests' ||
|
||||
hasRegularizationPermission;
|
||||
}).toList();
|
||||
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
|
||||
// Keep selectedTab in sync and fetch data on tab change
|
||||
_tabController.addListener(() async {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
final selectedTab = _tabs[_tabController.index]['value']!;
|
||||
attendanceController.selectedTab = selectedTab;
|
||||
await _fetchTabData(selectedTab);
|
||||
}
|
||||
});
|
||||
|
||||
_tabsInitialized = true;
|
||||
|
||||
// Load initial data for default tab
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isNotEmpty) _loadData(projectId);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadData(String projectId) async {
|
||||
try {
|
||||
attendanceController.selectedTab = 'todaysAttendance';
|
||||
await attendanceController.loadAttendanceData(projectId);
|
||||
// attendanceController.update(['attendance_dashboard_controller']);
|
||||
} catch (e) {
|
||||
debugPrint("Error loading data: $e");
|
||||
if (projectId.isNotEmpty) {
|
||||
final initialTab = _tabs[_tabController.index]['value']!;
|
||||
attendanceController.selectedTab = initialTab;
|
||||
await _fetchTabData(initialTab);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshData() async {
|
||||
Future<void> _fetchTabData(String tab) async {
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
// Call only the relevant API for current tab
|
||||
switch (selectedTab) {
|
||||
switch (tab) {
|
||||
case 'todaysAttendance':
|
||||
await attendanceController.fetchTodaysAttendance(projectId);
|
||||
break;
|
||||
@ -76,6 +117,10 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshData() async {
|
||||
await _fetchTabData(attendanceController.selectedTab);
|
||||
}
|
||||
|
||||
Widget _buildFilterSearchRow() {
|
||||
return Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
@ -88,7 +133,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
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;
|
||||
},
|
||||
@ -109,11 +155,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
@ -121,17 +167,14 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
MySpacing.width(8),
|
||||
|
||||
// 🛠️ Filter Icon (no red dot here anymore)
|
||||
Container(
|
||||
height: 35,
|
||||
width: 35,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
@ -144,19 +187,18 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.vertical(top: Radius.circular(12)),
|
||||
BorderRadius.vertical(top: Radius.circular(5)),
|
||||
),
|
||||
builder: (context) => AttendanceFilterBottomSheet(
|
||||
controller: attendanceController,
|
||||
permissionController: permissionController,
|
||||
selectedTab: selectedTab,
|
||||
selectedTab: _tabs[_tabController.index]['value']!,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final selectedProjectId =
|
||||
projectController.selectedProjectId.value;
|
||||
final selectedView = result['selectedTab'] as String?;
|
||||
final selectedOrgId =
|
||||
result['selectedOrganization'] as String?;
|
||||
|
||||
@ -167,111 +209,12 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
}
|
||||
|
||||
if (selectedProjectId.isNotEmpty) {
|
||||
try {
|
||||
await attendanceController.fetchTodaysAttendance(
|
||||
selectedProjectId,
|
||||
);
|
||||
await attendanceController.fetchAttendanceLogs(
|
||||
selectedProjectId,
|
||||
);
|
||||
await attendanceController.fetchRegularizationLogs(
|
||||
selectedProjectId,
|
||||
);
|
||||
await attendanceController
|
||||
.fetchProjectData(selectedProjectId);
|
||||
} catch (_) {}
|
||||
|
||||
attendanceController
|
||||
.update(['attendance_dashboard_controller']);
|
||||
}
|
||||
|
||||
if (selectedView != null && selectedView != selectedTab) {
|
||||
setState(() => selectedTab = selectedView);
|
||||
attendanceController.selectedTab = selectedView;
|
||||
if (selectedProjectId.isNotEmpty) {
|
||||
await attendanceController
|
||||
.fetchProjectData(selectedProjectId);
|
||||
}
|
||||
await _fetchTabData(attendanceController.selectedTab);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
|
||||
// ⋮ Pending Actions Menu (red dot here instead)
|
||||
if (selectedTab == 'attendanceLogs')
|
||||
Obx(() {
|
||||
final showPending = attendanceController.showPendingOnly.value;
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 35,
|
||||
width: 35,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: PopupMenuButton<int>(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.more_vert,
|
||||
size: 20, color: Colors.black87),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem<int>(
|
||||
enabled: false,
|
||||
height: 30,
|
||||
child: Text(
|
||||
"Preferences",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<int>(
|
||||
value: 0,
|
||||
enabled: false,
|
||||
child: Obx(() => Row(
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Text('Show Pending Actions')),
|
||||
Switch.adaptive(
|
||||
value: attendanceController
|
||||
.showPendingOnly.value,
|
||||
activeColor: Colors.indigo,
|
||||
onChanged: (val) {
|
||||
attendanceController
|
||||
.showPendingOnly.value = val;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showPending)
|
||||
Positioned(
|
||||
top: 6,
|
||||
right: 6,
|
||||
child: Container(
|
||||
height: 8,
|
||||
width: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -290,8 +233,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectedTabContent() {
|
||||
switch (selectedTab) {
|
||||
Widget _buildTabBarView() {
|
||||
return TabBarView(
|
||||
controller: _tabController,
|
||||
children: _tabs.map((tab) {
|
||||
switch (tab['value']) {
|
||||
case 'attendanceLogs':
|
||||
return AttendanceLogsTab(controller: attendanceController);
|
||||
case 'regularizationRequests':
|
||||
@ -300,12 +246,25 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
default:
|
||||
return TodaysAttendanceTab(controller: attendanceController);
|
||||
}
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
if (!_tabsInitialized) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: "Attendance",
|
||||
backgroundColor: appBarColor,
|
||||
onBackPressed: () => Get.toNamed('/dashboard'),
|
||||
),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: "Attendance",
|
||||
@ -314,7 +273,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// Gradient container at top
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
@ -328,8 +286,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: GetBuilder<AttendanceController>(
|
||||
init: attendanceController,
|
||||
@ -346,7 +302,21 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
padding: MySpacing.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: PillTabBar(
|
||||
controller: _tabController,
|
||||
tabs: _tabs.map((e) => e['label']!).toList(),
|
||||
selectedColor: contentTheme.primary,
|
||||
unselectedColor: Colors.grey.shade600,
|
||||
indicatorColor: contentTheme.primary,
|
||||
onTap: (index) async {
|
||||
final selectedTab = _tabs[index]['value']!;
|
||||
attendanceController.selectedTab = selectedTab;
|
||||
await _fetchTabData(selectedTab);
|
||||
},
|
||||
),
|
||||
),
|
||||
_buildFilterSearchRow(),
|
||||
MyFlex(
|
||||
children: [
|
||||
@ -354,7 +324,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
sizes: 'lg-12 md-12 sm-12',
|
||||
child: noProjectSelected
|
||||
? _buildNoProjectWidget()
|
||||
: _buildSelectedTabContent(),
|
||||
: SizedBox(
|
||||
height: MediaQuery.of(context).size.height -
|
||||
200,
|
||||
child: _buildTabBarView(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -372,7 +346,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 🧹 Clean up the controller when user leaves this screen
|
||||
_tabController.dispose();
|
||||
if (Get.isRegistered<AttendanceController>()) {
|
||||
Get.delete<AttendanceController>();
|
||||
}
|
||||
|
||||
@ -22,17 +22,30 @@ class TodaysAttendanceTab extends StatelessWidget {
|
||||
final isLoading = controller.isLoadingEmployees.value;
|
||||
final employees = controller.filteredEmployees;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
if (isLoading) {
|
||||
return SkeletonLoaders.employeeListSkeletonLoader();
|
||||
}
|
||||
|
||||
if (employees.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text("No Employees Assigned"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: employees.length + 1, // +1 for header
|
||||
padding: MySpacing.only(
|
||||
bottom: 80), // Adjusted padding to add spacing at the bottom
|
||||
itemBuilder: (context, index) {
|
||||
// --- Header Row ---
|
||||
if (index == 0) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12, top: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
MyText.titleMedium("Today's Attendance", fontWeight: 600),
|
||||
),
|
||||
MyText.bodySmall(
|
||||
DateTimeUtils.formatDate(DateTime.now(), 'dd MMM yyyy'),
|
||||
fontWeight: 600,
|
||||
@ -40,75 +53,74 @@ class TodaysAttendanceTab extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isLoading)
|
||||
SkeletonLoaders.employeeListSkeletonLoader()
|
||||
else if (employees.isEmpty)
|
||||
const SizedBox(
|
||||
height: 120,
|
||||
child: Center(child: Text("No Employees Assigned")))
|
||||
else
|
||||
MyCard.bordered(
|
||||
paddingAll: 8,
|
||||
);
|
||||
}
|
||||
|
||||
final employee = employees[index - 1];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyCard.bordered(
|
||||
paddingAll: 12,
|
||||
child: Column(
|
||||
children: List.generate(employees.length, (index) {
|
||||
final employee = employees[index];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyContainer(
|
||||
paddingAll: 5,
|
||||
child: Row(
|
||||
// 1. Employee Info Row
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
size: 31),
|
||||
size: 35,
|
||||
),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
MyText.bodyMedium(employee.name,
|
||||
fontWeight: 600),
|
||||
MyText.titleMedium(employee.name, fontWeight: 600),
|
||||
MySpacing.height(2),
|
||||
MyText.bodySmall(
|
||||
'(${employee.designation})',
|
||||
fontWeight: 600,
|
||||
color: Colors.grey[700]),
|
||||
employee.designation,
|
||||
fontWeight: 500,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
if (employee.checkIn != null ||
|
||||
employee.checkOut != null)
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 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: [
|
||||
if (employee.checkIn != null)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.arrow_circle_right,
|
||||
size: 16,
|
||||
color: Colors.green),
|
||||
MySpacing.width(4),
|
||||
Text(DateTimeUtils.formatDate(
|
||||
employee.checkIn!,
|
||||
'hh:mm a')),
|
||||
// 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (employee.checkOut != null) ...[
|
||||
MySpacing.width(16),
|
||||
const Icon(Icons.arrow_circle_left,
|
||||
size: 16, color: Colors.red),
|
||||
MySpacing.width(4),
|
||||
Text(DateTimeUtils.formatDate(
|
||||
employee.checkOut!, 'hh:mm a')),
|
||||
],
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
|
||||
// 3. Action Buttons Row
|
||||
MySpacing.height(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -128,18 +140,55 @@ class TodaysAttendanceTab extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (index != employees.length - 1)
|
||||
Divider(color: Colors.grey.withOpacity(0.3)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
contentTheme.primary.withOpacity(0.3), // lighter/faded
|
||||
@ -393,7 +393,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
|
||||
decoration: BoxDecoration(
|
||||
color: isEnabled ? Colors.white : Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(
|
||||
color: Colors.black12.withOpacity(.1),
|
||||
width: 0.7,
|
||||
|
||||
@ -64,7 +64,16 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
key: controller.scaffoldKey,
|
||||
endDrawer: const UserProfileBar(),
|
||||
floatingActionButton: widget.floatingActionButton,
|
||||
body: Container(
|
||||
body: Column(
|
||||
children: [
|
||||
// Solid primary background area
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: primaryColor,
|
||||
child: _buildHeaderContent(isMobile),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@ -78,11 +87,8 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
stops: const [0.0, 0.1, 0.3],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeaderContent(isMobile),
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {},
|
||||
@ -94,10 +100,9 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildHeaderContent(bool isMobile) {
|
||||
@ -106,14 +111,15 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 45, 10, 0),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 18),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
@ -121,7 +127,7 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Logo inside white background card
|
||||
// Logo section
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
@ -131,6 +137,8 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
width: 50,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
|
||||
// Beta badge
|
||||
if (ApiEndpoints.baseUrl.contains("stage"))
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
@ -140,7 +148,7 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepPurple,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.white, width: 1.2),
|
||||
),
|
||||
child: const Text(
|
||||
@ -155,7 +163,10 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Titles
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -177,6 +188,8 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Menu button with red dot if MPIN missing
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
@ -207,6 +220,4 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user