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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
@ -206,7 +219,5 @@ class _LayoutState extends State<Layout> with UIMixin {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user