enhansed pill tab bat and loading

This commit is contained in:
Vaibhav Surve 2025-12-16 17:18:19 +05:30
parent 03082aeea9
commit 4e577bd7eb
11 changed files with 313 additions and 293 deletions

View File

@ -7,20 +7,18 @@ import 'package:on_field_work/model/employees/employee_details_model.dart';
class EmployeesScreenController extends GetxController { class EmployeesScreenController extends GetxController {
/// Data lists /// Data lists
RxList<EmployeeModel> employees = <EmployeeModel>[].obs; RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
RxList<EmployeeModel> filteredEmployees = <EmployeeModel>[].obs;
Rxn<EmployeeDetailsModel> selectedEmployeeDetails = Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
Rxn<EmployeeDetailsModel>(); Rxn<EmployeeDetailsModel>();
/// Loading states /// Loading states
RxBool isLoading = false.obs; RxBool isLoading = true.obs;
RxBool isLoadingEmployeeDetails = false.obs; RxBool isLoadingEmployeeDetails = false.obs;
/// Selection state /// Selection state
RxBool isAllEmployeeSelected = false.obs; RxBool isAllEmployeeSelected = false.obs;
RxSet<String> selectedEmployeeIds = <String>{}.obs; RxSet<String> selectedEmployeeIds = <String>{}.obs;
/// Upload state tracking (if needed later)
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
RxList<EmployeeModel> selectedEmployeePrimaryManagers = <EmployeeModel>[].obs; RxList<EmployeeModel> selectedEmployeePrimaryManagers = <EmployeeModel>[].obs;
RxList<EmployeeModel> selectedEmployeeSecondaryManagers = RxList<EmployeeModel> selectedEmployeeSecondaryManagers =
<EmployeeModel>[].obs; <EmployeeModel>[].obs;
@ -31,26 +29,51 @@ class EmployeesScreenController extends GetxController {
fetchAllEmployees(); fetchAllEmployees();
} }
/// 🔹 Fetch all employees (no project filter) /// 🔹 Search/Filter Logic
void searchEmployees(String query) {
if (query.isEmpty) {
filteredEmployees.assignAll(employees);
} else {
final searchQuery = query.toLowerCase();
final result = employees
.where((e) =>
e.name.toLowerCase().contains(searchQuery) ||
e.email.toLowerCase().contains(searchQuery) ||
e.phoneNumber.toLowerCase().contains(searchQuery) ||
e.jobRole.toLowerCase().contains(searchQuery))
.toList();
// Sort alphabetically
result
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
filteredEmployees.assignAll(result);
}
}
/// 🔹 Fetch all employees
Future<void> fetchAllEmployees({String? organizationId}) async { Future<void> fetchAllEmployees({String? organizationId}) async {
isLoading.value = true; isLoading.value = true;
update(['employee_screen_controller']);
await _handleApiCall( await _handleApiCall(
() => ApiService.getAllEmployees(organizationId: organizationId), () => ApiService.getAllEmployees(organizationId: organizationId),
onSuccess: (data) { onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); final loadedList =
data.map((json) => EmployeeModel.fromJson(json)).toList();
employees.assignAll(loadedList);
filteredEmployees.assignAll(loadedList);
logSafe( logSafe(
"All Employees fetched: ${employees.length} employees loaded.", "All Employees fetched: ${employees.length} employees loaded.",
level: LogLevel.info, level: LogLevel.info,
); );
// Reset selection states when new data arrives
selectedEmployeeIds.clear(); selectedEmployeeIds.clear();
isAllEmployeeSelected.value = false; isAllEmployeeSelected.value = false;
}, },
onEmpty: () { onEmpty: () {
employees.clear(); employees.clear();
filteredEmployees.clear();
selectedEmployeeIds.clear(); selectedEmployeeIds.clear();
isAllEmployeeSelected.value = false; isAllEmployeeSelected.value = false;
logSafe("No Employee data found or API call failed", logSafe("No Employee data found or API call failed",
@ -90,16 +113,14 @@ class EmployeesScreenController extends GetxController {
isLoadingEmployeeDetails.value = false; isLoadingEmployeeDetails.value = false;
} }
/// Fetch reporting managers for a specific employee from /organization/hierarchy/list/:employeeId /// Fetch reporting managers
Future<void> fetchReportingManagers(String? employeeId) async { Future<void> fetchReportingManagers(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return; if (employeeId == null || employeeId.isEmpty) return;
try { try {
// Always clear before new fetch (to avoid mixing old data)
selectedEmployeePrimaryManagers.clear(); selectedEmployeePrimaryManagers.clear();
selectedEmployeeSecondaryManagers.clear(); selectedEmployeeSecondaryManagers.clear();
// Fetch from existing API helper
final data = await ApiService.getOrganizationHierarchyList(employeeId); final data = await ApiService.getOrganizationHierarchyList(employeeId);
if (data == null || data.isEmpty) { if (data == null || data.isEmpty) {
@ -124,11 +145,8 @@ class EmployeesScreenController extends GetxController {
selectedEmployeeSecondaryManagers.add(emp); selectedEmployeeSecondaryManagers.add(emp);
} }
} }
} catch (_) { } catch (_) {}
// ignore malformed items
} }
}
update(['employee_screen_controller']); update(['employee_screen_controller']);
} catch (e) { } catch (e) {
logSafe("Error fetching reporting managers for $employeeId", logSafe("Error fetching reporting managers for $employeeId",
@ -139,13 +157,13 @@ class EmployeesScreenController extends GetxController {
/// 🔹 Clear all employee data /// 🔹 Clear all employee data
void clearEmployees() { void clearEmployees() {
employees.clear(); employees.clear();
filteredEmployees.clear();
selectedEmployeeIds.clear(); selectedEmployeeIds.clear();
isAllEmployeeSelected.value = false; isAllEmployeeSelected.value = false;
logSafe("Employees cleared", level: LogLevel.info); logSafe("Employees cleared", level: LogLevel.info);
update(['employee_screen_controller']); update(['employee_screen_controller']);
} }
/// 🔹 Generic handler for list API responses
Future<void> _handleApiCall( Future<void> _handleApiCall(
Future<List<dynamic>?> Function() apiCall, { Future<List<dynamic>?> Function() apiCall, {
required Function(List<dynamic>) onSuccess, required Function(List<dynamic>) onSuccess,
@ -168,7 +186,6 @@ class EmployeesScreenController extends GetxController {
} }
} }
/// 🔹 Generic handler for single-object API responses
Future<void> _handleSingleApiCall( Future<void> _handleSingleApiCall(
Future<Map<String, dynamic>?> Function() apiCall, { Future<Map<String, dynamic>?> Function() apiCall, {
required Function(Map<String, dynamic>) onSuccess, required Function(Map<String, dynamic>) onSuccess,

View File

@ -22,7 +22,7 @@ class SkeletonLoaders {
height: 16, height: 16,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
); );
}), }),
@ -94,7 +94,7 @@ class SkeletonLoaders {
width: 60, width: 60,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
], ],
@ -134,7 +134,7 @@ class SkeletonLoaders {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors color: Colors
.grey.shade300, .grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
), ),
@ -165,7 +165,7 @@ static Widget _buildDetailRowSkeleton({
width: width, width: width,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
], ],
@ -239,7 +239,7 @@ static Widget _buildDetailRowSkeleton({
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade400, color: Colors.grey.shade400,
borderRadius: borderRadius:
BorderRadius.circular(6))), // Larger button size BorderRadius.circular(5))), // Larger button size
MySpacing.width(8), MySpacing.width(8),
// Log View Button (Icon Button, approx size 28-32) // Log View Button (Icon Button, approx size 28-32)
Container( Container(
@ -302,7 +302,7 @@ static Widget _buildDetailRowSkeleton({
width: cardWidth, width: cardWidth,
height: cardHeight, height: cardHeight,
paddingAll: 4, paddingAll: 4,
borderRadiusAll: 10, borderRadiusAll: 5,
border: Border.all(color: Colors.grey.withOpacity(0.15)), border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: ShimmerEffect( child: ShimmerEffect(
child: Column( child: Column(
@ -314,7 +314,7 @@ static Widget _buildDetailRowSkeleton({
height: 16, // Reduced from 20 height: 16, // Reduced from 20
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
MySpacing.height(4), // Reduced spacing from 6 MySpacing.height(4), // Reduced spacing from 6
@ -419,7 +419,7 @@ static Widget _buildDetailRowSkeleton({
width: 120, width: 120,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
@ -432,7 +432,7 @@ static Widget _buildDetailRowSkeleton({
width: 50, width: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade200, color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -441,7 +441,7 @@ static Widget _buildDetailRowSkeleton({
height: 12, height: 12,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
), ),
@ -460,7 +460,7 @@ static Widget _buildDetailRowSkeleton({
width: 50, width: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade200, color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
@ -469,7 +469,7 @@ static Widget _buildDetailRowSkeleton({
width: 80, width: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
], ],
@ -481,7 +481,7 @@ static Widget _buildDetailRowSkeleton({
width: 60, width: 60,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
], ],
@ -502,7 +502,7 @@ static Widget _buildDetailRowSkeleton({
constraints: const BoxConstraints(maxWidth: 520), constraints: const BoxConstraints(maxWidth: 520),
child: MyCard.bordered( child: MyCard.bordered(
paddingAll: 16, paddingAll: 16,
borderRadiusAll: 8, borderRadiusAll: 5,
shadow: MyShadow(elevation: 3), shadow: MyShadow(elevation: 3),
child: ShimmerEffect( child: ShimmerEffect(
child: Column( child: Column(
@ -566,7 +566,7 @@ static Widget _buildDetailRowSkeleton({
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(5),
), ),
), ),
)), )),
@ -635,7 +635,7 @@ static Widget _buildDetailRowSkeleton({
children: [ children: [
// Header skeleton (avatar + name + role) // Header skeleton (avatar + name + role)
MyCard( MyCard(
borderRadiusAll: 8, borderRadiusAll: 5,
paddingAll: 16, paddingAll: 16,
margin: MySpacing.bottom(16), margin: MySpacing.bottom(16),
shadow: MyShadow(elevation: 2), shadow: MyShadow(elevation: 2),
@ -686,7 +686,7 @@ static Widget _buildDetailRowSkeleton({
(_) => Column( (_) => Column(
children: [ children: [
MyCard( MyCard(
borderRadiusAll: 8, borderRadiusAll: 5,
paddingAll: 16, paddingAll: 16,
margin: MySpacing.bottom(16), margin: MySpacing.bottom(16),
shadow: MyShadow(elevation: 2), shadow: MyShadow(elevation: 2),
@ -773,7 +773,7 @@ static Widget _buildDetailRowSkeleton({
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(3, (floorIndex) { children: List.generate(3, (floorIndex) {
return MyCard( return MyCard(
borderRadiusAll: 8, borderRadiusAll: 5,
paddingAll: 5, paddingAll: 5,
margin: MySpacing.bottom(10), margin: MySpacing.bottom(10),
shadow: MyShadow(elevation: 1.5), shadow: MyShadow(elevation: 1.5),
@ -787,7 +787,7 @@ static Widget _buildDetailRowSkeleton({
width: 160, width: 160,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
MySpacing.height(10), MySpacing.height(10),
@ -809,7 +809,7 @@ static Widget _buildDetailRowSkeleton({
width: 120, width: 120,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
MySpacing.height(8), MySpacing.height(8),
@ -838,7 +838,7 @@ static Widget _buildDetailRowSkeleton({
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: borderRadius:
BorderRadius.circular(4), BorderRadius.circular(5),
), ),
), ),
), ),
@ -863,7 +863,7 @@ static Widget _buildDetailRowSkeleton({
static Widget chartSkeletonLoader() { static Widget chartSkeletonLoader() {
return MyCard.bordered( return MyCard.bordered(
paddingAll: 16, paddingAll: 16,
borderRadiusAll: 12, borderRadiusAll: 5,
shadow: MyShadow( shadow: MyShadow(
elevation: 1.5, elevation: 1.5,
position: MyShadowPosition.bottom, position: MyShadowPosition.bottom,
@ -878,7 +878,7 @@ static Widget _buildDetailRowSkeleton({
width: 180, width: 180,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -907,7 +907,7 @@ static Widget _buildDetailRowSkeleton({
height: 14, height: 14,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
); );
}), }),
@ -925,7 +925,7 @@ static Widget _buildDetailRowSkeleton({
width: 90, width: 90,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
); );
@ -956,7 +956,7 @@ static Widget _buildDetailRowSkeleton({
width: 160, width: 160,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -988,7 +988,7 @@ static Widget _buildDetailRowSkeleton({
width: 100, width: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
@ -997,7 +997,7 @@ static Widget _buildDetailRowSkeleton({
width: 60, width: 60,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
], ],
@ -1010,7 +1010,7 @@ static Widget _buildDetailRowSkeleton({
width: 30, width: 30,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
@ -1037,7 +1037,7 @@ static Widget _buildDetailRowSkeleton({
width: 120, width: 120,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@ -1046,7 +1046,7 @@ static Widget _buildDetailRowSkeleton({
width: 140, width: 140,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
], ],
@ -1056,7 +1056,7 @@ static Widget _buildDetailRowSkeleton({
width: 80, width: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
), ),
), ),
], ],
@ -1084,7 +1084,7 @@ static Widget _buildDetailRowSkeleton({
width: 80, width: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
), ),
@ -1096,7 +1096,7 @@ static Widget _buildDetailRowSkeleton({
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(5),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: Colors.black.withOpacity(0.05),
@ -1114,7 +1114,7 @@ static Widget _buildDetailRowSkeleton({
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(5),
), ),
child: const Icon(Icons.description, child: const Icon(Icons.description,
color: Colors.transparent), // invisible icon color: Colors.transparent), // invisible icon
@ -1178,7 +1178,7 @@ static Widget _buildDetailRowSkeleton({
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(5),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.06), color: Colors.black.withOpacity(0.06),
@ -1235,7 +1235,7 @@ static Widget _buildDetailRowSkeleton({
width: 60, width: 60,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(5),
), ),
); );
}), }),
@ -1289,7 +1289,7 @@ static Widget _buildDetailRowSkeleton({
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(5),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -1335,7 +1335,7 @@ static Widget _buildDetailRowSkeleton({
return Column( return Column(
children: List.generate(4, (index) { children: List.generate(4, (index) {
return MyCard.bordered( return MyCard.bordered(
borderRadiusAll: 12, borderRadiusAll: 5,
paddingAll: 10, paddingAll: 10,
margin: MySpacing.bottom(12), margin: MySpacing.bottom(12),
shadow: MyShadow(elevation: 3), shadow: MyShadow(elevation: 3),
@ -1407,7 +1407,7 @@ static Widget _buildDetailRowSkeleton({
static Widget employeeListCollapsedSkeletonLoader() { static Widget employeeListCollapsedSkeletonLoader() {
return MyCard.bordered( return MyCard.bordered(
borderRadiusAll: 4, borderRadiusAll: 5,
paddingAll: 8, paddingAll: 8,
child: ShimmerEffect( child: ShimmerEffect(
child: Column( child: Column(
@ -1479,7 +1479,7 @@ static Widget _buildDetailRowSkeleton({
static Widget dailyProgressReportSkeletonLoader() { static Widget dailyProgressReportSkeletonLoader() {
return MyCard.bordered( return MyCard.bordered(
borderRadiusAll: 4, borderRadiusAll: 5,
border: Border.all(color: Colors.grey.withOpacity(0.2)), border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8, paddingAll: 8,
@ -1514,7 +1514,7 @@ static Widget _buildDetailRowSkeleton({
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(3, (index) { children: List.generate(3, (index) {
return MyCard.bordered( return MyCard.bordered(
borderRadiusAll: 12, borderRadiusAll: 5,
paddingAll: 16, paddingAll: 16,
margin: MySpacing.bottom(12), margin: MySpacing.bottom(12),
shadow: MyShadow(elevation: 3), shadow: MyShadow(elevation: 3),
@ -1573,7 +1573,7 @@ static Widget _buildDetailRowSkeleton({
width: 120, width: 120,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
Container( Container(
@ -1581,7 +1581,7 @@ static Widget _buildDetailRowSkeleton({
width: 80, width: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
], ],
@ -1595,7 +1595,7 @@ static Widget _buildDetailRowSkeleton({
width: 100, width: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
const Spacer(), const Spacer(),
@ -1604,7 +1604,7 @@ static Widget _buildDetailRowSkeleton({
width: 50, width: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(5),
), ),
), ),
], ],
@ -1620,7 +1620,7 @@ static Widget _buildDetailRowSkeleton({
return MyCard.bordered( return MyCard.bordered(
margin: MySpacing.only(bottom: 12), margin: MySpacing.only(bottom: 12),
paddingAll: 12, paddingAll: 12,
borderRadiusAll: 12, borderRadiusAll: 5,
shadow: MyShadow( shadow: MyShadow(
elevation: 1.5, elevation: 1.5,
position: MyShadowPosition.bottom, position: MyShadowPosition.bottom,
@ -1703,9 +1703,8 @@ static Widget _buildDetailRowSkeleton({
return MyCard.bordered( return MyCard.bordered(
margin: MySpacing.only(bottom: 12), margin: MySpacing.only(bottom: 12),
paddingAll: 16, paddingAll: 16,
borderRadiusAll: 16, borderRadiusAll: 5,
shadow: MyShadow( shadow: MyShadow(
elevation: 1.5,
position: MyShadowPosition.bottom, position: MyShadowPosition.bottom,
), ),
child: ShimmerEffect( child: ShimmerEffect(
@ -1859,7 +1858,7 @@ static Widget _buildDetailRowSkeleton({
// Aging Stacked Bar Placeholder // Aging Stacked Bar Placeholder
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(5),
child: Row( child: Row(
children: List.generate( children: List.generate(
4, 4,

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PillTabBar extends StatelessWidget { class PillTabBar extends StatefulWidget {
final TabController controller; final TabController controller;
final List<String> tabs; final List<String> tabs;
final List<IconData> icons;
final Color selectedColor; final Color selectedColor;
final Color unselectedColor; final Color unselectedColor;
final Color indicatorColor; final Color indicatorColor;
@ -13,6 +14,7 @@ class PillTabBar extends StatelessWidget {
Key? key, Key? key,
required this.controller, required this.controller,
required this.tabs, required this.tabs,
required this.icons,
this.selectedColor = Colors.blue, this.selectedColor = Colors.blue,
this.unselectedColor = Colors.grey, this.unselectedColor = Colors.grey,
this.indicatorColor = Colors.blueAccent, this.indicatorColor = Colors.blueAccent,
@ -21,64 +23,80 @@ class PillTabBar extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { State<PillTabBar> createState() => _PillTabBarState();
// Dynamic horizontal padding between tabs }
final screenWidth = MediaQuery.of(context).size.width;
final tabSpacing = (screenWidth / (tabs.length * 12)).clamp(8.0, 24.0);
class _PillTabBarState extends State<PillTabBar> {
@override
void initState() {
super.initState();
widget.controller.addListener(_onTabChange);
}
void _onTabChange() {
if (mounted) setState(() {});
}
@override
void dispose() {
widget.controller.removeListener(_onTabChange);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Container( child: Container(
height: height, height: widget.height,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(height / 2), borderRadius: BorderRadius.circular(widget.height / 2),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.15),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
child: TabBar( child: TabBar(
controller: controller, controller: widget.controller,
indicator: BoxDecoration( isScrollable: true, // important for dynamic spacing
color: indicatorColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(height / 2),
),
indicatorSize: TabBarIndicatorSize.tab, indicatorSize: TabBarIndicatorSize.tab,
indicatorPadding: EdgeInsets.symmetric( indicator: BoxDecoration(
horizontal: tabSpacing / 2, color: widget.indicatorColor.withOpacity(0.2),
vertical: 4, borderRadius: BorderRadius.circular(widget.height / 2),
), ),
labelColor: selectedColor, onTap: widget.onTap,
unselectedLabelColor: unselectedColor, tabs: List.generate(widget.tabs.length, (index) {
labelStyle: const TextStyle( final isSelected = widget.controller.index == index;
fontWeight: FontWeight.bold,
fontSize: 13, return AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(
horizontal:
isSelected ? 12 : 6, // reduce padding for unselected tabs
), ),
unselectedLabelStyle: const TextStyle( child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
widget.icons[index],
size: isSelected ? 18 : 16,
color: isSelected
? widget.selectedColor
: widget.unselectedColor,
),
if (isSelected) ...[
const SizedBox(width: 4),
Text(
widget.tabs[index],
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
fontSize: 13, color: widget.selectedColor,
),
tabs: tabs
.map(
(text) => Tab(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: tabSpacing),
child: Text(
text,
overflow: TextOverflow.ellipsis,
maxLines: 2,
), ),
), ),
],
],
), ),
) );
.toList(), }),
onTap: onTap, )),
),
),
); );
} }
} }

View File

@ -31,7 +31,7 @@ class _AttendanceScreenState extends State<AttendanceScreen>
final projectController = Get.put(ProjectController()); final projectController = Get.put(ProjectController());
late TabController _tabController; late TabController _tabController;
late List<Map<String, String>> _tabs; late List<Map<String, dynamic>> _tabs;
bool _tabsInitialized = false; bool _tabsInitialized = false;
@override @override
@ -62,9 +62,13 @@ class _AttendanceScreenState extends State<AttendanceScreen>
void _initializeTabs() async { void _initializeTabs() async {
final allTabs = [ final allTabs = [
{'label': "Today's", 'value': 'todaysAttendance'}, {'label': "Today's", 'value': 'todaysAttendance', 'icon': Icons.today},
{'label': "Logs", 'value': 'attendanceLogs'}, {'label': "Logs", 'value': 'attendanceLogs', 'icon': Icons.list_alt},
{'label': "Regularization", 'value': 'regularizationRequests'}, {
'label': "Regularization",
'value': 'regularizationRequests',
'icon': Icons.edit
},
]; ];
final hasRegularizationPermission = final hasRegularizationPermission =
@ -306,7 +310,11 @@ class _AttendanceScreenState extends State<AttendanceScreen>
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: PillTabBar( child: PillTabBar(
controller: _tabController, controller: _tabController,
tabs: _tabs.map((e) => e['label']!).toList(), tabs:
_tabs.map((e) => e['label'] as String).toList(),
icons: _tabs
.map((e) => e['icon'] as IconData)
.toList(),
selectedColor: contentTheme.primary, selectedColor: contentTheme.primary,
unselectedColor: Colors.grey.shade600, unselectedColor: Colors.grey.shade600,
indicatorColor: contentTheme.primary, indicatorColor: contentTheme.primary,

View File

@ -73,6 +73,10 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
PillTabBar( PillTabBar(
controller: _tabController, controller: _tabController,
tabs: const ["Directory", "Notes"], tabs: const ["Directory", "Notes"],
icons: const [
Icons.people,
Icons.notes_outlined,
],
selectedColor: contentTheme.primary, selectedColor: contentTheme.primary,
unselectedColor: Colors.grey.shade600, unselectedColor: Colors.grey.shade600,
indicatorColor: contentTheme.primary, indicatorColor: contentTheme.primary,

View File

@ -424,6 +424,8 @@ class _DirectoryViewState extends State<DirectoryView> with UIMixin {
child: controller.isLoading.value child: controller.isLoading.value
? ListView.separated( ? ListView.separated(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: MySpacing.only(
left: 10, right: 10, top: 4, bottom: 80),
itemCount: 10, itemCount: 10,
separatorBuilder: (_, __) => MySpacing.height(12), separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, __) => itemBuilder: (_, __) =>

View File

@ -9,7 +9,6 @@ import 'package:on_field_work/controller/employee/employees_screen_controller.da
import 'package:on_field_work/helpers/widgets/avatar.dart'; import 'package:on_field_work/helpers/widgets/avatar.dart';
import 'package:on_field_work/controller/project_controller.dart'; import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart'; import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/helpers/utils/launcher_utils.dart'; import 'package:on_field_work/helpers/utils/launcher_utils.dart';
import 'package:on_field_work/view/employees/assign_employee_bottom_sheet.dart'; import 'package:on_field_work/view/employees/assign_employee_bottom_sheet.dart';
import 'package:on_field_work/controller/permission_controller.dart'; import 'package:on_field_work/controller/permission_controller.dart';
@ -30,56 +29,27 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
late final EmployeesScreenController _employeeController; late final EmployeesScreenController _employeeController;
late final PermissionController _permissionController; late final PermissionController _permissionController;
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
final RxList<EmployeeModel> _filteredEmployees = <EmployeeModel>[].obs;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_employeeController = Get.put(EmployeesScreenController()); _employeeController = Get.put(EmployeesScreenController());
_permissionController = Get.put(PermissionController()); _permissionController = Get.put(PermissionController());
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _initEmployees();
_searchController.addListener(() { _searchController.addListener(() {
_filterEmployees(_searchController.text); _employeeController.searchEmployees(_searchController.text);
}); });
});
}
Future<void> _initEmployees() async {
await _employeeController.fetchAllEmployees();
_filterEmployees(_searchController.text);
} }
Future<void> _refreshEmployees() async { Future<void> _refreshEmployees() async {
try { try {
await _employeeController.fetchAllEmployees(); await _employeeController.fetchAllEmployees();
_filterEmployees(_searchController.text); _employeeController.searchEmployees(_searchController.text);
_employeeController.update(['employee_screen_controller']);
} catch (e, stackTrace) { } catch (e, stackTrace) {
debugPrint('Error refreshing employee data: $e'); debugPrint('Error refreshing employee data: $e');
debugPrintStack(stackTrace: stackTrace); debugPrintStack(stackTrace: stackTrace);
} }
} }
void _filterEmployees(String query) {
final employees = _employeeController.employees;
final searchQuery = query.toLowerCase();
final filtered = query.isEmpty
? List<EmployeeModel>.from(employees)
: employees
.where(
(e) =>
e.name.toLowerCase().contains(searchQuery) ||
e.email.toLowerCase().contains(searchQuery) ||
e.phoneNumber.toLowerCase().contains(searchQuery) ||
e.jobRole.toLowerCase().contains(searchQuery),
)
.toList();
filtered
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
_filteredEmployees.assignAll(filtered);
}
Future<void> _onAddNewEmployee() async { Future<void> _onAddNewEmployee() async {
final result = await showModalBottomSheet<Map<String, dynamic>>( final result = await showModalBottomSheet<Map<String, dynamic>>(
context: context, context: context,
@ -144,11 +114,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
// Main content // Main content
SafeArea( SafeArea(
child: GetBuilder<EmployeesScreenController>( child: Obx(() {
init: _employeeController,
tag: 'employee_screen_controller',
builder: (_) {
_filterEmployees(_searchController.text);
return MyRefreshIndicator( return MyRefreshIndicator(
onRefresh: _refreshEmployees, onRefresh: _refreshEmployees,
child: SingleChildScrollView( child: SingleChildScrollView(
@ -158,10 +124,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MySpacing.height(flexSpacing), MySpacing.height(flexSpacing),
Padding( _buildSearchField(),
padding: MySpacing.x(15),
child: _buildSearchField(),
),
MySpacing.height(flexSpacing), MySpacing.height(flexSpacing),
Padding( Padding(
padding: MySpacing.x(flexSpacing), padding: MySpacing.x(flexSpacing),
@ -171,8 +134,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
), ),
), ),
); );
}, }),
),
), ),
], ],
), ),
@ -238,7 +200,6 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
size: 20, color: Colors.grey), size: 20, color: Colors.grey),
onPressed: () { onPressed: () {
_searchController.clear(); _searchController.clear();
_filterEmployees('');
}, },
); );
}, },
@ -255,13 +216,11 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
borderSide: BorderSide(color: Colors.grey.shade300), borderSide: BorderSide(color: Colors.grey.shade300),
), ),
), ),
onChanged: (_) => _filterEmployees(_searchController.text),
), ),
), ),
), ),
MySpacing.width(10), MySpacing.width(10),
// Three dots menu (Manage Reporting)
Container( Container(
height: 35, height: 35,
width: 35, width: 35,
@ -277,10 +236,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)), borderRadius: BorderRadius.circular(5)),
itemBuilder: (context) { itemBuilder: (context) {
List<PopupMenuEntry<int>> menuItems = []; return [
// Section: Actions
menuItems.add(
const PopupMenuItem<int>( const PopupMenuItem<int>(
enabled: false, enabled: false,
height: 30, height: 30,
@ -290,10 +246,6 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
fontWeight: FontWeight.bold, color: Colors.grey), fontWeight: FontWeight.bold, color: Colors.grey),
), ),
), ),
);
// Manage Reporting option
menuItems.add(
PopupMenuItem<int>( PopupMenuItem<int>(
value: 1, value: 1,
child: Row( child: Row(
@ -317,9 +269,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
}); });
}, },
), ),
); ];
return menuItems;
}, },
), ),
), ),
@ -329,7 +279,6 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
} }
Widget _buildEmployeeList() { Widget _buildEmployeeList() {
return Obx(() {
if (_employeeController.isLoading.value) { if (_employeeController.isLoading.value) {
return ListView.separated( return ListView.separated(
shrinkWrap: true, shrinkWrap: true,
@ -340,7 +289,8 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
); );
} }
final employees = _filteredEmployees; final employees = _employeeController.filteredEmployees;
if (employees.isEmpty) { if (employees.isEmpty) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 60), padding: const EdgeInsets.only(top: 60),
@ -395,22 +345,19 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
_buildLinkRow( _buildLinkRow(
icon: Icons.phone_outlined, icon: Icons.phone_outlined,
text: e.phoneNumber, text: e.phoneNumber,
onTap: () => onTap: () => LauncherUtils.launchPhone(e.phoneNumber),
LauncherUtils.launchPhone(e.phoneNumber),
onLongPress: () => LauncherUtils.copyToClipboard( onLongPress: () => LauncherUtils.copyToClipboard(
e.phoneNumber, e.phoneNumber,
typeLabel: 'Phone')), typeLabel: 'Phone')),
], ],
), ),
), ),
const Icon(Icons.arrow_forward_ios, const Icon(Icons.arrow_forward_ios, color: Colors.grey, size: 16),
color: Colors.grey, size: 16),
], ],
), ),
); );
}, },
); );
});
} }
Widget _buildLinkRow({ Widget _buildLinkRow({

View File

@ -133,6 +133,10 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
PillTabBar( PillTabBar(
controller: _tabController, controller: _tabController,
tabs: const ["Current Month", "History"], tabs: const ["Current Month", "History"],
icons: const [
Icons.calendar_today,
Icons.history,
],
selectedColor: contentTheme.primary, selectedColor: contentTheme.primary,
unselectedColor: Colors.grey.shade600, unselectedColor: Colors.grey.shade600,
indicatorColor: contentTheme.primary, indicatorColor: contentTheme.primary,

View File

@ -142,6 +142,10 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
PillTabBar( PillTabBar(
controller: _tabController, controller: _tabController,
tabs: const ["Current Month", "History"], tabs: const ["Current Month", "History"],
icons: const [
Icons.calendar_today,
Icons.history,
],
selectedColor: contentTheme.primary, selectedColor: contentTheme.primary,
unselectedColor: Colors.grey.shade600, unselectedColor: Colors.grey.shade600,
indicatorColor: contentTheme.primary, indicatorColor: contentTheme.primary,

View File

@ -51,27 +51,33 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
} }
void _prepareTabs() { void _prepareTabs() {
_tabs.add(_InfraTab(name: "Profile", view: _buildProfileTab())); _tabs.add(_InfraTab(
_tabs.add(_InfraTab(name: "Team", view: _buildTeamTab())); name: "Profile",
icon: Icons.person,
view: _buildProfileTab(),
));
_tabs.add(_InfraTab(
name: "Team",
icon: Icons.group,
view: _buildTeamTab(),
));
final allowedMenu = menuController.menuItems.where((m) => m.available); final allowedMenu = menuController.menuItems.where((m) => m.available);
if (allowedMenu.any((m) => m.id == MenuItems.dailyTaskPlanning)) { if (allowedMenu.any((m) => m.id == MenuItems.dailyTaskPlanning)) {
_tabs.add( _tabs.add(_InfraTab(
_InfraTab(
name: "Task Planning", name: "Task Planning",
icon: Icons.task,
view: DailyTaskPlanningScreen(projectId: widget.projectId), view: DailyTaskPlanningScreen(projectId: widget.projectId),
), ));
);
} }
if (allowedMenu.any((m) => m.id == MenuItems.dailyProgressReport)) { if (allowedMenu.any((m) => m.id == MenuItems.dailyProgressReport)) {
_tabs.add( _tabs.add(_InfraTab(
_InfraTab(
name: "Task Progress", name: "Task Progress",
icon: Icons.trending_up,
view: DailyProgressReportScreen(projectId: widget.projectId), view: DailyProgressReportScreen(projectId: widget.projectId),
), ));
);
} }
_tabController = TabController(length: _tabs.length, vsync: this); _tabController = TabController(length: _tabs.length, vsync: this);
@ -507,6 +513,7 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
PillTabBar( PillTabBar(
controller: _tabController, controller: _tabController,
tabs: _tabs.map((e) => e.name).toList(), tabs: _tabs.map((e) => e.name).toList(),
icons: _tabs.map((e) => e.icon).toList(),
selectedColor: contentTheme.primary, selectedColor: contentTheme.primary,
unselectedColor: Colors.grey.shade600, unselectedColor: Colors.grey.shade600,
indicatorColor: contentTheme.primary, indicatorColor: contentTheme.primary,
@ -529,7 +536,12 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
/// INTERNAL MODEL /// INTERNAL MODEL
class _InfraTab { class _InfraTab {
final String name; final String name;
final IconData icon;
final Widget view; final Widget view;
_InfraTab({required this.name, required this.view}); _InfraTab({
required this.name,
required this.icon,
required this.view,
});
} }

View File

@ -475,6 +475,11 @@ class _ServiceProjectDetailsScreenState
PillTabBar( PillTabBar(
controller: _tabController, controller: _tabController,
tabs: const ["Profile", "Jobs", "Teams"], tabs: const ["Profile", "Jobs", "Teams"],
icons: const [
Icons.person,
Icons.work,
Icons.group,
],
selectedColor: contentTheme.primary, selectedColor: contentTheme.primary,
unselectedColor: Colors.grey.shade600, unselectedColor: Colors.grey.shade600,
indicatorColor: contentTheme.primary.withOpacity(0.1), indicatorColor: contentTheme.primary.withOpacity(0.1),