From 37ce612fcaf6a0a6ae3b0414891323eb543cad78 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sat, 29 Nov 2025 12:34:22 +0530 Subject: [PATCH] Enhance attendance management with tabbed navigation and permission handling; improve UI consistency and loading states --- lib/controller/permission_controller.dart | 11 +- lib/helpers/widgets/pill_tab_bar.dart | 6 +- lib/view/Attendence/attendance_logs_tab.dart | 312 +++++++++--------- lib/view/Attendence/attendance_screen.dart | 270 +++++++-------- .../Attendence/todays_attendance_tab.dart | 259 +++++++++------ lib/view/dashboard/dashboard_screen.dart | 4 +- lib/view/layouts/layout.dart | 285 ++++++++-------- 7 files changed, 599 insertions(+), 548 deletions(-) diff --git a/lib/controller/permission_controller.dart b/lib/controller/permission_controller.dart index 8e17ecc..4db0cab 100644 --- a/lib/controller/permission_controller.dart +++ b/lib/controller/permission_controller.dart @@ -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; } diff --git a/lib/helpers/widgets/pill_tab_bar.dart b/lib/helpers/widgets/pill_tab_bar.dart index 204cab4..c1b8f62 100644 --- a/lib/helpers/widgets/pill_tab_bar.dart +++ b/lib/helpers/widgets/pill_tab_bar.dart @@ -7,7 +7,7 @@ class PillTabBar extends StatelessWidget { final Color unselectedColor; final Color indicatorColor; final double height; - + final ValueChanged? 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( diff --git a/lib/view/Attendence/attendance_logs_tab.dart b/lib/view/Attendence/attendance_logs_tab.dart index 2b89fc8..e71aabb 100644 --- a/lib/view/Attendence/attendance_logs_tab.dart +++ b/lib/view/Attendence/attendance_logs_tab.dart @@ -128,162 +128,172 @@ class _AttendanceLogsTabState extends State { '${DateTimeUtils.formatDate(widget.controller.startDateAttendance.value, 'dd MMM yyyy')} - ' '${DateTimeUtils.formatDate(widget.controller.endDateAttendance.value, 'dd MMM yyyy')}'; - return 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, + 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: [ - 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 - for (final emp in (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); - }, - ))) ...[ - 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( - 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: widget.controller, - ), - MySpacing.width(8), - AttendanceLogViewButton( - employee: emp, - attendanceController: widget.controller, - ), - ], - ), - ], - ), - ), - ], + MyText.titleMedium("Attendance Logs", fontWeight: 600), + widget.controller.isLoading.value + ? SkeletonLoaders.dateSkeletonLoader() + : MyText.bodySmall( + dateRangeText, + fontWeight: 600, + color: Colors.grey[700], + overflow: TextOverflow.ellipsis, ), - ), - Divider(color: Colors.grey.withOpacity(0.3)), - ], - ], ], ), ), - ], + + // 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 + for (final emp in (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); + }, + ))) ...[ + 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( + 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: 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(), + ], + ), + ), + ], + ), ); }); } -} +} \ No newline at end of file diff --git a/lib/view/Attendence/attendance_screen.dart b/lib/view/Attendence/attendance_screen.dart index fca026e..0efd9d7 100644 --- a/lib/view/Attendence/attendance_screen.dart +++ b/lib/view/Attendence/attendance_screen.dart @@ -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 createState() => _AttendanceScreenState(); } -class _AttendanceScreenState extends State with UIMixin { +class _AttendanceScreenState extends State + with SingleTickerProviderStateMixin, UIMixin { final attendanceController = Get.put(AttendanceController()); final permissionController = Get.put(PermissionController()); - final projectController = Get.find(); + final projectController = Get.put(ProjectController()); - String selectedTab = 'todaysAttendance'; + late TabController _tabController; + late List> _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(projectController.selectedProjectId, (projectId) async { - if (projectId.isNotEmpty) await _loadData(projectId); + if (projectId.isNotEmpty && _tabsInitialized) { + await _fetchTabData(attendanceController.selectedTab); + } }); - WidgetsBinding.instance.addPostFrameCallback((_) { - final projectId = projectController.selectedProjectId.value; - if (projectId.isNotEmpty) _loadData(projectId); - }); - } - - Future _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 permissions are already loaded at init + if (permissionController.permissionsLoaded.value) { + _initializeTabs(); } } - Future _refreshData() async { + 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) { + final initialTab = _tabs[_tabController.index]['value']!; + attendanceController.selectedTab = initialTab; + await _fetchTabData(initialTab); + } + } + + Future _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 with UIMixin { } } + Future _refreshData() async { + await _fetchTabData(attendanceController.selectedTab); + } + Widget _buildFilterSearchRow() { return Padding( padding: MySpacing.xy(8, 8), @@ -88,7 +133,8 @@ class _AttendanceScreenState extends State 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 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 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 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 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( - padding: EdgeInsets.zero, - icon: const Icon(Icons.more_vert, - size: 20, color: Colors.black87), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - itemBuilder: (context) => [ - const PopupMenuItem( - enabled: false, - height: 30, - child: Text( - "Preferences", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.grey, - ), - ), - ), - PopupMenuItem( - 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,22 +233,38 @@ class _AttendanceScreenState extends State with UIMixin { ); } - Widget _buildSelectedTabContent() { - switch (selectedTab) { - case 'attendanceLogs': - return AttendanceLogsTab(controller: attendanceController); - case 'regularizationRequests': - return RegularizationRequestsTab(controller: attendanceController); - case 'todaysAttendance': - default: - return TodaysAttendanceTab(controller: attendanceController); - } + Widget _buildTabBarView() { + return TabBarView( + controller: _tabController, + children: _tabs.map((tab) { + switch (tab['value']) { + case 'attendanceLogs': + return AttendanceLogsTab(controller: attendanceController); + case 'regularizationRequests': + return RegularizationRequestsTab(controller: attendanceController); + case 'todaysAttendance': + 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 with UIMixin { ), body: Stack( children: [ - // Gradient container at top Container( height: 80, decoration: BoxDecoration( @@ -328,8 +286,6 @@ class _AttendanceScreenState extends State with UIMixin { ), ), ), - - // Main content SafeArea( child: GetBuilder( init: attendanceController, @@ -346,7 +302,21 @@ class _AttendanceScreenState extends State 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 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 with UIMixin { @override void dispose() { - // ๐Ÿงน Clean up the controller when user leaves this screen + _tabController.dispose(); if (Get.isRegistered()) { Get.delete(); } diff --git a/lib/view/Attendence/todays_attendance_tab.dart b/lib/view/Attendence/todays_attendance_tab.dart index ff5d525..f0bcc0c 100644 --- a/lib/view/Attendence/todays_attendance_tab.dart +++ b/lib/view/Attendence/todays_attendance_tab.dart @@ -22,124 +22,173 @@ 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), - child: Row( - children: [ - Expanded( - child: - MyText.titleMedium("Today's Attendance", fontWeight: 600), - ), - MyText.bodySmall( - DateTimeUtils.formatDate(DateTime.now(), 'dd MMM yyyy'), - fontWeight: 600, - color: Colors.grey[700], - ), - ], - ), + if (isLoading) { + return SkeletonLoaders.employeeListSkeletonLoader(); + } + + if (employees.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text("No Employees Assigned"), ), - if (isLoading) - SkeletonLoaders.employeeListSkeletonLoader() - else if (employees.isEmpty) - const SizedBox( - height: 120, - child: Center(child: Text("No Employees Assigned"))) - else - MyCard.bordered( - paddingAll: 8, + ); + } + + 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: [ + MyText.bodySmall( + DateTimeUtils.formatDate(DateTime.now(), 'dd MMM yyyy'), + fontWeight: 600, + color: Colors.grey[700], + ), + ], + ), + ); + } + + 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: [ + // 1. Employee Info Row + Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyContainer( - paddingAll: 5, - child: Row( + Avatar( + firstName: employee.firstName, + lastName: employee.lastName, + size: 35, + ), + MySpacing.width(16), + Expanded( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Avatar( - firstName: employee.firstName, - lastName: employee.lastName, - size: 31), - MySpacing.width(16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 6, - children: [ - MyText.bodyMedium(employee.name, - fontWeight: 600), - MyText.bodySmall( - '(${employee.designation})', - fontWeight: 600, - color: Colors.grey[700]), - ], - ), - MySpacing.height(8), - if (employee.checkIn != null || - employee.checkOut != null) - Row( - 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')), - ], - ), - 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), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - AttendanceActionButton( - employee: employee, - attendanceController: controller, - ), - if (employee.checkIn != null) ...[ - MySpacing.width(8), - AttendanceLogViewButton( - employee: employee, - attendanceController: controller, - ), - ], - ], - ), - ], - ), + MyText.titleMedium(employee.name, fontWeight: 600), + MySpacing.height(2), + MyText.bodySmall( + employee.designation, + fontWeight: 500, + color: Colors.grey[600], ), ], ), ), - if (index != employees.length - 1) - Divider(color: Colors.grey.withOpacity(0.3)), ], - ); - }), + ), + + // 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), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AttendanceActionButton( + employee: employee, + attendanceController: controller, + ), + if (employee.checkIn != null) ...[ + MySpacing.width(8), + AttendanceLogViewButton( + employee: employee, + attendanceController: controller, + ), + ], + ], + ), + ], ), ), - ], + ); + }, ); }); } + + // 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, + ), + ], + ), + ], + ), + ); + } } diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 25041ae..e420913 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -92,7 +92,7 @@ class _DashboardScreenState extends State 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 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, diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index c54d2ad..983848e 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -61,152 +61,163 @@ class _LayoutState extends State with UIMixin { final primaryColor = contentTheme.primary; return Scaffold( - key: controller.scaffoldKey, - endDrawer: const UserProfileBar(), - floatingActionButton: widget.floatingActionButton, - body: Container( - width: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - primaryColor, - primaryColor.withOpacity(0.7), - primaryColor.withOpacity(0.0), - ], - stops: const [0.0, 0.1, 0.3], - ), - ), - child: Column( + key: controller.scaffoldKey, + endDrawer: const UserProfileBar(), + floatingActionButton: widget.floatingActionButton, + body: Column( children: [ - _buildHeaderContent(isMobile), + // Solid primary background area + Container( + width: double.infinity, + color: primaryColor, + child: _buildHeaderContent(isMobile), + ), Expanded( - child: SafeArea( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () {}, - child: SingleChildScrollView( - key: controller.scrollKey, - padding: EdgeInsets.zero, - child: widget.child, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + primaryColor, + primaryColor.withOpacity(0.7), + primaryColor.withOpacity(0.0), + ], + stops: const [0.0, 0.1, 0.3], + ), + ), + child: SafeArea( + top: false, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () {}, + child: SingleChildScrollView( + key: controller.scrollKey, + padding: EdgeInsets.zero, + child: widget.child, + ), ), ), ), ), ], + )); + } + + Widget _buildHeaderContent(bool isMobile) { + final selectedTenant = TenantService.currentTenant; + + 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(6), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + // Logo section + Stack( + clipBehavior: Clip.none, + children: [ + Image.asset( + Images.logoDark, + height: 50, + width: 50, + fit: BoxFit.contain, + ), + + // Beta badge + if (ApiEndpoints.baseUrl.contains("stage")) + Positioned( + bottom: 0, + left: 0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: Colors.deepPurple, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.white, width: 1.2), + ), + child: const Text( + 'B', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], + ), + + const SizedBox(width: 12), + + // Titles + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyLarge( + "Dashboard", + fontWeight: 700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: Colors.black87, + ), + if (selectedTenant != null) + MyText.bodySmall( + "Organization: ${selectedTenant.name}", + color: Colors.black54, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + + // Menu button with red dot if MPIN missing + Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + IconButton( + icon: const Icon(Icons.menu, color: Colors.black87), + onPressed: () => + controller.scaffoldKey.currentState?.openEndDrawer(), + ), + if (!hasMpin) + Positioned( + right: 10, + top: 10, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.redAccent, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ), + ], ), ), ); } - - Widget _buildHeaderContent(bool isMobile) { - final selectedTenant = TenantService.currentTenant; - - return Padding( - padding: const EdgeInsets.fromLTRB(10, 45, 10, 0), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 6, - offset: const Offset(0, 3), - ), - ], - ), - child: Row( - children: [ - // Logo inside white background card - Stack( - clipBehavior: Clip.none, - children: [ - Image.asset( - Images.logoDark, - height: 50, - width: 50, - fit: BoxFit.contain, - ), - if (ApiEndpoints.baseUrl.contains("stage")) - Positioned( - bottom: 0, - left: 0, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, vertical: 2), - decoration: BoxDecoration( - color: Colors.deepPurple, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.white, width: 1.2), - ), - child: const Text( - 'B', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - ], - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyLarge( - "Dashboard", - fontWeight: 700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - color: Colors.black87, - ), - if (selectedTenant != null) - MyText.bodySmall( - "Organization: ${selectedTenant.name}", - color: Colors.black54, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - IconButton( - icon: const Icon(Icons.menu, color: Colors.black87), - onPressed: () => - controller.scaffoldKey.currentState?.openEndDrawer(), - ), - if (!hasMpin) - Positioned( - right: 10, - top: 10, - child: Container( - width: 14, - height: 14, - decoration: BoxDecoration( - color: Colors.redAccent, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - ), - ), - ), - ], - ), - ], - ), - ), - ); -} - - }