Enhance attendance management with tabbed navigation and permission handling; improve UI consistency and loading states

This commit is contained in:
Vaibhav Surve 2025-11-29 12:34:22 +05:30
parent 7bef2e9d89
commit 37ce612fca
7 changed files with 599 additions and 548 deletions

View File

@ -15,6 +15,9 @@ class PermissionController extends GetxController {
Timer? _refreshTimer; Timer? _refreshTimer;
var isLoading = true.obs; var isLoading = true.obs;
/// NEW: reactive flag to signal permissions are loaded
var permissionsLoaded = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -52,6 +55,10 @@ class PermissionController extends GetxController {
_updateState(userData); _updateState(userData);
await _storeData(); await _storeData();
logSafe("Data loaded and state updated successfully."); logSafe("Data loaded and state updated successfully.");
// NEW: mark permissions as loaded
permissionsLoaded.value = true;
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error loading data from API", logSafe("Error loading data from API",
level: LogLevel.error, error: e, stackTrace: stacktrace); level: LogLevel.error, error: e, stackTrace: stacktrace);
@ -103,7 +110,7 @@ class PermissionController extends GetxController {
} }
void _startAutoRefresh() { void _startAutoRefresh() {
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async { _refreshTimer = Timer.periodic(const Duration(minutes: 30), (timer) async {
logSafe("Auto-refresh triggered."); logSafe("Auto-refresh triggered.");
final token = await _getAuthToken(); final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) { if (token?.isNotEmpty ?? false) {
@ -117,8 +124,6 @@ class PermissionController extends GetxController {
bool hasPermission(String permissionId) { bool hasPermission(String permissionId) {
final hasPerm = permissions.any((p) => p.id == permissionId); final hasPerm = permissions.any((p) => p.id == permissionId);
// logSafe("Checking permission $permissionId: $hasPerm",
// level: LogLevel.debug);
return hasPerm; return hasPerm;
} }

View File

@ -7,7 +7,7 @@ class PillTabBar extends StatelessWidget {
final Color unselectedColor; final Color unselectedColor;
final Color indicatorColor; final Color indicatorColor;
final double height; final double height;
final ValueChanged<int>? onTap;
const PillTabBar({ const PillTabBar({
Key? key, Key? key,
required this.controller, required this.controller,
@ -16,6 +16,7 @@ class PillTabBar extends StatelessWidget {
this.unselectedColor = Colors.grey, this.unselectedColor = Colors.grey,
this.indicatorColor = Colors.blueAccent, this.indicatorColor = Colors.blueAccent,
this.height = 48, this.height = 48,
this.onTap,
}) : super(key: key); }) : super(key: key);
@override @override
@ -42,7 +43,8 @@ class PillTabBar extends StatelessWidget {
borderRadius: BorderRadius.circular(height / 2), borderRadius: BorderRadius.circular(height / 2),
), ),
indicatorSize: TabBarIndicatorSize.tab, indicatorSize: TabBarIndicatorSize.tab,
indicatorPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), indicatorPadding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
labelColor: selectedColor, labelColor: selectedColor,
unselectedLabelColor: unselectedColor, unselectedLabelColor: unselectedColor,
labelStyle: const TextStyle( labelStyle: const TextStyle(

View File

@ -128,7 +128,9 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
'${DateTimeUtils.formatDate(widget.controller.startDateAttendance.value, 'dd MMM yyyy')} - ' '${DateTimeUtils.formatDate(widget.controller.startDateAttendance.value, 'dd MMM yyyy')} - '
'${DateTimeUtils.formatDate(widget.controller.endDateAttendance.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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header row // Header row
@ -279,10 +281,18 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
Divider(color: Colors.grey.withOpacity(0.3)), 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

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/my_flex.dart'; import 'package:on_field_work/helpers/widgets/my_flex.dart';
import 'package:on_field_work/helpers/widgets/my_flex_item.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/helpers/widgets/my_text.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/controller/permission_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/controller/project_controller.dart';
import 'package:on_field_work/view/Attendence/regularization_requests_tab.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/attendance_logs_tab.dart';
import 'package:on_field_work/view/Attendence/todays_attendance_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/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.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 { class AttendanceScreen extends StatefulWidget {
const AttendanceScreen({super.key}); const AttendanceScreen({super.key});
@ -23,43 +24,83 @@ class AttendanceScreen extends StatefulWidget {
State<AttendanceScreen> createState() => _AttendanceScreenState(); 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 attendanceController = Get.put(AttendanceController());
final permissionController = Get.put(PermissionController()); 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 @override
void initState() { void initState() {
super.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 { 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; final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) _loadData(projectId); if (projectId.isNotEmpty) {
}); final initialTab = _tabs[_tabController.index]['value']!;
} attendanceController.selectedTab = initialTab;
await _fetchTabData(initialTab);
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");
} }
} }
Future<void> _refreshData() async { Future<void> _fetchTabData(String tab) async {
final projectId = projectController.selectedProjectId.value; final projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return; if (projectId.isEmpty) return;
// Call only the relevant API for current tab switch (tab) {
switch (selectedTab) {
case 'todaysAttendance': case 'todaysAttendance':
await attendanceController.fetchTodaysAttendance(projectId); await attendanceController.fetchTodaysAttendance(projectId);
break; break;
@ -76,6 +117,10 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
} }
} }
Future<void> _refreshData() async {
await _fetchTabData(attendanceController.selectedTab);
}
Widget _buildFilterSearchRow() { Widget _buildFilterSearchRow() {
return Padding( return Padding(
padding: MySpacing.xy(8, 8), padding: MySpacing.xy(8, 8),
@ -88,7 +133,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
final query = attendanceController.searchQuery.value; final query = attendanceController.searchQuery.value;
return TextField( return TextField(
controller: TextEditingController(text: query) controller: TextEditingController(text: query)
..selection = TextSelection.collapsed(offset: query.length), ..selection =
TextSelection.collapsed(offset: query.length),
onChanged: (value) { onChanged: (value) {
attendanceController.searchQuery.value = value; attendanceController.searchQuery.value = value;
}, },
@ -109,11 +155,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide: BorderSide(color: Colors.grey.shade300),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide: BorderSide(color: Colors.grey.shade300),
), ),
), ),
@ -121,17 +167,14 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
}), }),
), ),
), ),
MySpacing.width(8), MySpacing.width(8),
// 🛠 Filter Icon (no red dot here anymore)
Container( Container(
height: 35, height: 35,
width: 35, width: 35,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border.all(color: Colors.grey.shade300), border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(5),
), ),
child: IconButton( child: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@ -144,19 +187,18 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: borderRadius:
BorderRadius.vertical(top: Radius.circular(12)), BorderRadius.vertical(top: Radius.circular(5)),
), ),
builder: (context) => AttendanceFilterBottomSheet( builder: (context) => AttendanceFilterBottomSheet(
controller: attendanceController, controller: attendanceController,
permissionController: permissionController, permissionController: permissionController,
selectedTab: selectedTab, selectedTab: _tabs[_tabController.index]['value']!,
), ),
); );
if (result != null) { if (result != null) {
final selectedProjectId = final selectedProjectId =
projectController.selectedProjectId.value; projectController.selectedProjectId.value;
final selectedView = result['selectedTab'] as String?;
final selectedOrgId = final selectedOrgId =
result['selectedOrganization'] as String?; result['selectedOrganization'] as String?;
@ -167,111 +209,12 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
} }
if (selectedProjectId.isNotEmpty) { if (selectedProjectId.isNotEmpty) {
try { await _fetchTabData(attendanceController.selectedTab);
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);
}
} }
} }
}, },
), ),
), ),
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() { Widget _buildTabBarView() {
switch (selectedTab) { return TabBarView(
controller: _tabController,
children: _tabs.map((tab) {
switch (tab['value']) {
case 'attendanceLogs': case 'attendanceLogs':
return AttendanceLogsTab(controller: attendanceController); return AttendanceLogsTab(controller: attendanceController);
case 'regularizationRequests': case 'regularizationRequests':
@ -300,12 +246,25 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
default: default:
return TodaysAttendanceTab(controller: attendanceController); return TodaysAttendanceTab(controller: attendanceController);
} }
}).toList(),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary; 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( return Scaffold(
appBar: CustomAppBar( appBar: CustomAppBar(
title: "Attendance", title: "Attendance",
@ -314,7 +273,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
), ),
body: Stack( body: Stack(
children: [ children: [
// Gradient container at top
Container( Container(
height: 80, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -328,8 +286,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
), ),
), ),
), ),
// Main content
SafeArea( SafeArea(
child: GetBuilder<AttendanceController>( child: GetBuilder<AttendanceController>(
init: attendanceController, init: attendanceController,
@ -346,7 +302,21 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
padding: MySpacing.zero, padding: MySpacing.zero,
child: Column( child: Column(
children: [ 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(), _buildFilterSearchRow(),
MyFlex( MyFlex(
children: [ children: [
@ -354,7 +324,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
sizes: 'lg-12 md-12 sm-12', sizes: 'lg-12 md-12 sm-12',
child: noProjectSelected child: noProjectSelected
? _buildNoProjectWidget() ? _buildNoProjectWidget()
: _buildSelectedTabContent(), : SizedBox(
height: MediaQuery.of(context).size.height -
200,
child: _buildTabBarView(),
),
), ),
], ],
), ),
@ -372,7 +346,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
@override @override
void dispose() { void dispose() {
// 🧹 Clean up the controller when user leaves this screen _tabController.dispose();
if (Get.isRegistered<AttendanceController>()) { if (Get.isRegistered<AttendanceController>()) {
Get.delete<AttendanceController>(); Get.delete<AttendanceController>();
} }

View File

@ -22,17 +22,30 @@ class TodaysAttendanceTab extends StatelessWidget {
final isLoading = controller.isLoadingEmployees.value; final isLoading = controller.isLoadingEmployees.value;
final employees = controller.filteredEmployees; final employees = controller.filteredEmployees;
return Column( if (isLoading) {
crossAxisAlignment: CrossAxisAlignment.start, return SkeletonLoaders.employeeListSkeletonLoader();
children: [ }
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), 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( child: Row(
children: [ children: [
Expanded(
child:
MyText.titleMedium("Today's Attendance", fontWeight: 600),
),
MyText.bodySmall( MyText.bodySmall(
DateTimeUtils.formatDate(DateTime.now(), 'dd MMM yyyy'), DateTimeUtils.formatDate(DateTime.now(), 'dd MMM yyyy'),
fontWeight: 600, fontWeight: 600,
@ -40,75 +53,74 @@ class TodaysAttendanceTab extends StatelessWidget {
), ),
], ],
), ),
), );
if (isLoading) }
SkeletonLoaders.employeeListSkeletonLoader()
else if (employees.isEmpty) final employee = employees[index - 1];
const SizedBox(
height: 120, return Padding(
child: Center(child: Text("No Employees Assigned"))) padding: const EdgeInsets.only(bottom: 12),
else child: MyCard.bordered(
MyCard.bordered( paddingAll: 12,
paddingAll: 8,
child: Column( child: Column(
children: List.generate(employees.length, (index) { crossAxisAlignment: CrossAxisAlignment.start,
final employee = employees[index];
return Column(
children: [ children: [
MyContainer( // 1. Employee Info Row
paddingAll: 5, Row(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Avatar( Avatar(
firstName: employee.firstName, firstName: employee.firstName,
lastName: employee.lastName, lastName: employee.lastName,
size: 31), size: 35,
),
MySpacing.width(16), MySpacing.width(16),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Wrap( MyText.titleMedium(employee.name, fontWeight: 600),
spacing: 6, MySpacing.height(2),
children: [
MyText.bodyMedium(employee.name,
fontWeight: 600),
MyText.bodySmall( MyText.bodySmall(
'(${employee.designation})', employee.designation,
fontWeight: 600, fontWeight: 500,
color: Colors.grey[700]), 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (employee.checkIn != null) // Check-in Time
Row( _buildLogTime(
children: [ icon: Icons.login,
const Icon( color: Colors.green,
Icons.arrow_circle_right, label: 'Check-in',
size: 16, time: employee.checkIn,
color: Colors.green), ),
MySpacing.width(4),
Text(DateTimeUtils.formatDate( // Check-out Time
employee.checkIn!, _buildLogTime(
'hh:mm a')), icon: Icons.logout,
color: Colors.red,
label: 'Check-out',
time: employee.checkOut,
),
], ],
), ),
if (employee.checkOut != null) ...[
MySpacing.width(16), // 3. Action Buttons Row
const Icon(Icons.arrow_circle_left, MySpacing.height(16),
size: 16, color: Colors.red),
MySpacing.width(4),
Text(DateTimeUtils.formatDate(
employee.checkOut!, 'hh:mm a')),
],
],
),
MySpacing.height(12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ 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,
),
],
),
],
),
);
}
} }

View File

@ -92,7 +92,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(5),
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
contentTheme.primary.withOpacity(0.3), // lighter/faded contentTheme.primary.withOpacity(0.3), // lighter/faded
@ -393,7 +393,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
decoration: BoxDecoration( decoration: BoxDecoration(
color: isEnabled ? Colors.white : Colors.grey.shade100, color: isEnabled ? Colors.white : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(5),
border: Border.all( border: Border.all(
color: Colors.black12.withOpacity(.1), color: Colors.black12.withOpacity(.1),
width: 0.7, width: 0.7,

View File

@ -64,7 +64,16 @@ class _LayoutState extends State<Layout> with UIMixin {
key: controller.scaffoldKey, key: controller.scaffoldKey,
endDrawer: const UserProfileBar(), endDrawer: const UserProfileBar(),
floatingActionButton: widget.floatingActionButton, 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, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@ -78,11 +87,8 @@ class _LayoutState extends State<Layout> with UIMixin {
stops: const [0.0, 0.1, 0.3], stops: const [0.0, 0.1, 0.3],
), ),
), ),
child: Column(
children: [
_buildHeaderContent(isMobile),
Expanded(
child: SafeArea( child: SafeArea(
top: false,
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () {}, onTap: () {},
@ -94,10 +100,9 @@ class _LayoutState extends State<Layout> with UIMixin {
), ),
), ),
), ),
),
], ],
), ));
),
);
} }
Widget _buildHeaderContent(bool isMobile) { Widget _buildHeaderContent(bool isMobile) {
@ -106,14 +111,15 @@ class _LayoutState extends State<Layout> with UIMixin {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(10, 45, 10, 0), padding: const EdgeInsets.fromLTRB(10, 45, 10, 0),
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 18),
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(6),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.withOpacity(0.08),
blurRadius: 6, blurRadius: 6,
offset: const Offset(0, 3), offset: const Offset(0, 3),
), ),
@ -121,7 +127,7 @@ class _LayoutState extends State<Layout> with UIMixin {
), ),
child: Row( child: Row(
children: [ children: [
// Logo inside white background card // Logo section
Stack( Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
@ -131,6 +137,8 @@ class _LayoutState extends State<Layout> with UIMixin {
width: 50, width: 50,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
// Beta badge
if (ApiEndpoints.baseUrl.contains("stage")) if (ApiEndpoints.baseUrl.contains("stage"))
Positioned( Positioned(
bottom: 0, bottom: 0,
@ -140,7 +148,7 @@ class _LayoutState extends State<Layout> with UIMixin {
horizontal: 4, vertical: 2), horizontal: 4, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.deepPurple, color: Colors.deepPurple,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.white, width: 1.2), border: Border.all(color: Colors.white, width: 1.2),
), ),
child: const Text( child: const Text(
@ -155,7 +163,10 @@ class _LayoutState extends State<Layout> with UIMixin {
), ),
], ],
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
// Titles
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -177,6 +188,8 @@ class _LayoutState extends State<Layout> with UIMixin {
], ],
), ),
), ),
// Menu button with red dot if MPIN missing
Stack( Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
alignment: Alignment.center, alignment: Alignment.center,
@ -207,6 +220,4 @@ class _LayoutState extends State<Layout> with UIMixin {
), ),
); );
} }
} }