Add employee management features including controller, models, and UI

- Implemented AddEmployeeController for handling employee creation logic.
- Created EmployeeScreen and AddEmployeeScreen for employee management UI.
- Added API endpoints for fetching and creating employees.
- Updated EmployeeModel to include additional fields: jobRole, email, and phoneNumber.
- Refactored MyRefreshWrapper to MyRefreshableContent for consistency.
- Enhanced navigation to include employee management routes.
This commit is contained in:
Vaibhav Surve 2025-05-09 09:58:44 +05:30
parent 1ea8447d6c
commit 1ea960b0ec
11 changed files with 928 additions and 34 deletions

View File

@ -0,0 +1,119 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:get/get.dart';
import 'package:logger/logger.dart';
enum Gender {
male,
female,
other;
const Gender();
}
final Logger logger = Logger();
class AddEmployeeController extends MyController {
List<PlatformFile> files = [];
MyFormValidator basicValidator = MyFormValidator();
Gender? selectedGender;
List<Map<String, dynamic>> roles = [];
String? selectedRoleId;
@override
void onInit() {
super.onInit();
logger.i("Initializing AddEmployeeController...");
fetchRoles();
basicValidator.addField(
'first_name',
label: "First Name",
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'phone_number',
label: "Phone Number",
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'last_name',
label: "Last Name",
required: true,
controller: TextEditingController(),
);
logger.i("Fields initialized for first_name, phone_number, last_name.");
}
bool showOnline = true;
final List<String> categories = [];
void onGenderSelected(Gender? gender) {
selectedGender = gender;
logger.i("Gender selected: ${gender?.name}");
update();
}
Future<void> fetchRoles() async {
logger.i("Fetching roles...");
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logger.i("Roles fetched successfully.");
update();
} else {
logger.e("Failed to fetch roles.");
}
}
void onRoleSelected(String? roleId) {
selectedRoleId = roleId;
logger.i("Role selected: $roleId");
update();
}
Future<void> createEmployees() async {
logger.i("Starting employee creation...");
if (selectedGender == null || selectedRoleId == null) {
logger.w("Missing gender or role.");
Get.snackbar(
"Missing Fields",
"Please select both Gender and Role.",
snackPosition: SnackPosition.BOTTOM,
);
return;
}
final firstName = basicValidator.getController("first_name")?.text.trim();
final lastName = basicValidator.getController("last_name")?.text.trim();
final phoneNumber =
basicValidator.getController("phone_number")?.text.trim();
logger.i(
"Creating employee with Name: $firstName $lastName, Phone: $phoneNumber, Gender: ${selectedGender!.name}");
final response = await ApiService.createEmployee(
firstName: firstName!,
lastName: lastName!,
phoneNumber: phoneNumber!,
gender: selectedGender!.name,
jobRoleId: selectedRoleId!,
);
if (response == true) {
logger.i("Employee created successfully.");
Get.back(); // Or navigate as needed
Get.snackbar("Success", "Employee created successfully!",
snackPosition: SnackPosition.BOTTOM);
} else {
logger.e("Failed to create employee.");
Get.snackbar("Error", "Failed to create employee.",
snackPosition: SnackPosition.BOTTOM);
}
}
}

View File

@ -0,0 +1,102 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employee_model.dart';
final Logger log = Logger();
class EmployeesScreenController extends GetxController {
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = [];
RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
@override
void onInit() {
super.onInit();
fetchAllProjects();
}
Future<void> fetchAllProjects() async {
isLoading.value = true;
await _handleApiCall(
ApiService.getProjects,
onSuccess: (data) {
projects = data.map((json) => ProjectModel.fromJson(json)).toList();
log.i("Projects fetched: ${projects.length} projects loaded.");
},
onEmpty: () => log.w("No project data found or API call failed."),
);
isLoading.value = false;
update();
}
Future<void> fetchAllEmployees() async {
isLoading.value = true;
await _handleApiCall(
ApiService.getAllEmployees,
onSuccess: (data) {
employees = data.map((json) => EmployeeModel.fromJson(json)).toList();
log.i("All Employees fetched: ${employees.length} employees loaded.");
},
onEmpty: () => log.w("No Employee data found or API call failed."),
);
isLoading.value = false;
update();
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) {
log.e("Project ID is required but was null or empty.");
return;
}
isLoading.value = true;
await _handleApiCall(
() => ApiService.getAllEmployeesByProject(projectId),
onSuccess: (data) {
employees = data.map((json) => EmployeeModel.fromJson(json)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
log.i("Employees fetched: ${employees.length} for project $projectId");
update();
},
onEmpty: () {
log.w("No employees found for project $projectId.");
employees = [];
update();
},
onError: (e) =>
log.e("Error fetching employees for project $projectId: $e"),
);
isLoading.value = false;
}
Future<void> _handleApiCall(
Future<List<dynamic>?> Function() apiCall, {
required Function(List<dynamic>) onSuccess,
required Function() onEmpty,
Function(dynamic error)? onError,
}) async {
try {
final response = await apiCall();
if (response != null && response.isNotEmpty) {
onSuccess(response);
} else {
onEmpty();
}
} catch (e) {
if (onError != null) {
onError(e);
} else {
log.e("API call error: $e");
}
}
}
}

View File

@ -10,5 +10,8 @@ class ApiEndpoints {
static const String uploadAttendanceImage = "/attendance/record-image";
// Employee Screen API Endpoints
static const String getAllEmployeesByProject = "/Project/employees/get";
static const String getAllEmployees = "/employee/list";
static const String getRoles = "/roles/jobrole";
static const String createEmployee = "/employee/manage-mobile";
}

View File

@ -53,7 +53,7 @@ class ApiService {
Uri uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
.replace(queryParameters: queryParams);
_log('GET request: $uri');
_log("GET $uri");
try {
http.Response response =
@ -69,7 +69,7 @@ class ApiService {
queryParams: queryParams, hasRetried: true);
}
}
_log("Refresh failed.");
_log("Token refresh failed.");
}
return response;
} catch (e) {
@ -78,35 +78,44 @@ class ApiService {
}
}
static Future<http.Response?> _postRequest(String endpoint, dynamic body) async {
static Future<http.Response?> _postRequest(
String endpoint, dynamic body) async {
String? token = await _getToken();
if (token == null) return null;
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
_log("POST request to $uri with body: $body");
_log("POST $uri");
_log("Headers: ${_headers(token)}");
_log("Body: $body");
try {
final response = await http
.post(uri, headers: _headers(token), body: jsonEncode(body))
.timeout(timeout);
_log("Response Status: ${response.statusCode}");
return response;
} catch (e) {
_log("HTTP POST Exception: $e");
return null;
}
}
// ===== API Calls =====
// ===== Attendence Screen API Calls =====
static Future<List<dynamic>?> getProjects() async {
final response = await _getRequest(ApiEndpoints.getProjects);
return response != null ? _parseResponse(response, label: 'Projects') : null;
return response != null
? _parseResponse(response, label: 'Projects')
: null;
}
static Future<List<dynamic>?> getEmployeesByProject(String projectId) async {
final response = await _getRequest(ApiEndpoints.getEmployeesByProject,
queryParams: {"projectId": projectId});
return response != null ? _parseResponse(response, label: 'Employees') : null;
return response != null
? _parseResponse(response, label: 'Employees')
: null;
}
static Future<List<dynamic>?> getAttendanceLogs(String projectId,
@ -115,24 +124,30 @@ class ApiService {
"projectId": projectId,
if (dateFrom != null)
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
if (dateTo != null)
"dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
};
final response =
await _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query);
return response != null ? _parseResponse(response, label: 'Attendance Logs') : null;
return response != null
? _parseResponse(response, label: 'Attendance Logs')
: null;
}
static Future<List<dynamic>?> getAttendanceLogView(String id) async {
final response = await _getRequest("${ApiEndpoints.getAttendanceLogView}/$id");
return response != null ? _parseResponse(response, label: 'Log Details') : null;
final response =
await _getRequest("${ApiEndpoints.getAttendanceLogView}/$id");
return response != null
? _parseResponse(response, label: 'Log Details')
: null;
}
static Future<List<dynamic>?> getRegularizationLogs(String projectId) async {
final response = await _getRequest(ApiEndpoints.getRegularizationLogs,
queryParams: {"projectId": projectId});
return response != null ? _parseResponse(response, label: 'Regularization Logs') : null;
return response != null
? _parseResponse(response, label: 'Regularization Logs')
: null;
}
// ===== Upload Attendance Image =====
@ -204,4 +219,74 @@ class ApiService {
final imageNumber = count.toString().padLeft(3, '0');
return "${employeeId}_${dateStr}_$imageNumber.jpg";
}
// ===== Employee Screen API Calls =====
static Future<List<dynamic>?> getAllEmployeesByProject(
String projectId) async {
if (projectId.isEmpty) {
throw ArgumentError('projectId must not be empty');
}
final String endpoint =
"${ApiEndpoints.getAllEmployeesByProject}/$projectId";
final response = await _getRequest(endpoint);
return response != null
? _parseResponse(response, label: 'Employees by Project')
: null;
}
static Future<List<dynamic>?> getAllEmployees() async {
final response = await _getRequest(ApiEndpoints.getAllEmployees);
return response != null
? _parseResponse(response, label: 'All Employees')
: null;
}
static Future<List<dynamic>?> getRoles() async {
final response = await _getRequest(ApiEndpoints.getRoles);
return response != null
? _parseResponse(response, label: 'All Employees')
: null;
}
static Future<bool> createEmployee({
required String firstName,
required String lastName,
required String phoneNumber,
required String gender,
required String jobRoleId,
}) async {
final body = {
"firstName": firstName,
"lastName": lastName,
"phoneNumber": phoneNumber,
"gender": gender,
"jobRoleId": jobRoleId,
};
// Make the API request
final response = await _postRequest(ApiEndpoints.createEmployee, body);
if (response == null) {
_log("Error: No response from server.");
return false;
}
final json = jsonDecode(response.body);
if (response.statusCode == 200) {
if (json['success'] == true) {
return true;
} else {
_log(
"Failed to create employee: ${json['message'] ?? 'Unknown error'}");
return false;
}
} else {
_log(
"Failed to create employee. Status code: ${response.statusCode}, Response: ${json['message'] ?? 'No message'}");
return false;
}
}
}

View File

@ -1,26 +1,23 @@
import 'package:flutter/material.dart';
class MyRefreshWrapper extends StatelessWidget {
class MyRefreshableContent extends StatelessWidget {
final Future<void> Function() onRefresh;
final Widget child;
final EdgeInsetsGeometry? padding;
const MyRefreshWrapper({
const MyRefreshableContent({
Key? key,
required this.onRefresh,
required this.child,
this.padding,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: onRefresh,
backgroundColor: Colors.red, // Set background color to red
color: Colors.white, // Set spinner color to white
backgroundColor: Colors.red,
color: Colors.white,
child: SingleChildScrollView(
padding: padding,
physics: const AlwaysScrollableScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(),
child: child,
),
);

View File

@ -7,6 +7,9 @@ class EmployeeModel {
final String checkOut;
final int activity;
int action;
final String jobRole;
final String email;
final String phoneNumber;
EmployeeModel({
required this.id,
@ -17,6 +20,9 @@ class EmployeeModel {
required this.checkOut,
required this.activity,
required this.action,
required this.jobRole,
required this.email,
required this.phoneNumber,
});
factory EmployeeModel.fromJson(Map<String, dynamic> json) {
@ -29,6 +35,9 @@ class EmployeeModel {
checkOut: json['checkOut']?.toString() ?? '-',
action: json['action'] ?? 0,
activity: json['activity'] ?? 0,
jobRole: json['jobRole']?.toString() ?? '-',
email: json['email']?.toString() ?? '-',
phoneNumber: json['phoneNumber']?.toString() ?? '-',
);
}
@ -43,6 +52,9 @@ class EmployeeModel {
'checkOut': checkOut,
'action': action,
'activity': activity,
'jobRole': jobRole.isEmpty ? '-' : jobRole,
'email': email.isEmpty ? '-' : email,
'phoneNumber': phoneNumber.isEmpty ? '-' : phoneNumber,
};
}
}

View File

@ -12,6 +12,9 @@ import 'package:marco/view/error_pages/error_500_screen.dart';
// import 'package:marco/view/dashboard/attendance_screen.dart';
import 'package:marco/view/dashboard/attendanceScreen.dart';
import 'package:marco/view/dashboard/dashboard_screen.dart';
import 'package:marco/view/dashboard/add_employee_screen.dart';
import 'package:marco/view/dashboard/employee_screen.dart';
class AuthMiddleware extends GetMiddleware {
@override
RouteSettings? redirect(String? route) {
@ -21,20 +24,58 @@ class AuthMiddleware extends GetMiddleware {
getPageRoute() {
var routes = [
GetPage(name: '/', page: () => const AttendanceScreen(), middlewares: [AuthMiddleware()]),
GetPage(
name: '/',
page: () => const AttendanceScreen(),
middlewares: [AuthMiddleware()]),
// Dashboard
GetPage(name: '/dashboard/attendance', page: () => AttendanceScreen(), middlewares: [AuthMiddleware()]),
GetPage(name: '/dashboard', page: () => DashboardScreen(), middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/attendance',
page: () => AttendanceScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard',
page: () => DashboardScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/employees',
page: () => EmployeeScreen(),
middlewares: [AuthMiddleware()]),
// Employees Creation
GetPage(
name: '/employees/addEmployee',
page: () => AddEmployeeScreen(),
middlewares: [AuthMiddleware()]),
// Authentication
GetPage(name: '/auth/login', page: () => LoginScreen()),
GetPage(name: '/auth/register_account', page: () => const RegisterAccountScreen()),
GetPage(name: '/auth/forgot_password', page: () => const ForgotPasswordScreen()),
GetPage(name: '/auth/reset_password', page: () => const ResetPasswordScreen()),
GetPage(
name: '/auth/register_account',
page: () => const RegisterAccountScreen()),
GetPage(
name: '/auth/forgot_password',
page: () => const ForgotPasswordScreen()),
GetPage(
name: '/auth/reset_password', page: () => const ResetPasswordScreen()),
// Error
GetPage(name: '/error/coming_soon', page: () => ComingSoonScreen(), middlewares: [AuthMiddleware()]),
GetPage(name: '/error/500', page: () => Error500Screen(), middlewares: [AuthMiddleware()]),
GetPage(name: '/error/404', page: () => Error404Screen(), middlewares: [AuthMiddleware()]),
GetPage(
name: '/error/coming_soon',
page: () => ComingSoonScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/error/500',
page: () => Error500Screen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/error/404',
page: () => Error404Screen(),
middlewares: [AuthMiddleware()]),
];
return routes.map((e) => GetPage(name: e.name, page: e.page, middlewares: e.middlewares, transition: Transition.noTransition)).toList();
return routes
.map((e) => GetPage(
name: e.name,
page: e.page,
middlewares: e.middlewares,
transition: Transition.noTransition))
.toList();
}

View File

@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:get/get.dart';
import 'package:marco/controller/dashboard/add_employee_controller.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart';
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_flex.dart';
import 'package:marco/helpers/widgets/my_flex_item.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/view/layouts/layout.dart';
class AddEmployeeScreen extends StatefulWidget {
const AddEmployeeScreen({super.key});
@override
State<AddEmployeeScreen> createState() => _AddEmployeeScreenState();
}
class _AddEmployeeScreenState extends State<AddEmployeeScreen> with UIMixin {
AddEmployeeController controller = Get.put(AddEmployeeController());
@override
Widget build(BuildContext context) {
return Layout(
child: GetBuilder(
init: controller,
tag: 'add_employee_controller',
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium(
"Add Employee",
fontSize: 18,
fontWeight: 600,
),
MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Employee'),
MyBreadcrumbItem(name: 'Add Employee'),
],
),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing / 2),
child: MyFlex(
children: [
MyFlexItem(sizes: "lg-8 md-12", child: detail()),
],
),
),
],
);
},
),
);
}
Widget detail() {
return Form(
key: controller
.basicValidator.formKey, // Ensure the key is correctly assigned
child: MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 24,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(LucideIcons.server, size: 16),
MySpacing.width(12),
MyText.titleMedium("General", fontWeight: 600),
],
),
MySpacing.height(24),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium("First Name"),
MySpacing.height(8),
TextFormField(
validator:
controller.basicValidator.getValidation('first_name'),
controller:
controller.basicValidator.getController('first_name'),
keyboardType: TextInputType.name,
decoration: InputDecoration(
hintText: "eg: Jhon",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(24),
MyText.labelMedium("Last Name"),
MySpacing.height(8),
TextFormField(
validator:
controller.basicValidator.getValidation('last_name'),
controller:
controller.basicValidator.getController('last_name'),
keyboardType: TextInputType.name,
decoration: InputDecoration(
hintText: "eg: Doe",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(24),
MyText.labelMedium("Phone Number"),
MySpacing.height(8),
TextFormField(
validator:
controller.basicValidator.getValidation('phone_number'),
controller:
controller.basicValidator.getController('phone_number'),
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: "eg: +91 9876543210",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(24),
MyFlex(contentPadding: false, children: [
MyFlexItem(
sizes: 'lg-6 md-12',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium("Select Gender"),
MySpacing.height(8),
DropdownButtonFormField<Gender>(
value: controller.selectedGender,
dropdownColor: contentTheme.background,
menuMaxHeight: 200,
isDense: true,
items: Gender.values.map((gender) {
return DropdownMenuItem<Gender>(
value: gender,
child: MyText.labelMedium(
gender.name[0].toUpperCase() +
gender.name.substring(1),
),
);
}).toList(),
icon: const Icon(Icons.expand_more, size: 20),
decoration: InputDecoration(
hintText: "Select Gender",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(14),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
onChanged: controller.onGenderSelected,
),
],
),
),
]),
MySpacing.height(24),
MyFlex(contentPadding: false, children: [
MyFlexItem(
sizes: 'lg-6 md-12',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium("Select Role"),
MySpacing.height(8),
DropdownButtonFormField<String>(
value: controller.selectedRoleId,
dropdownColor: contentTheme.background,
decoration: InputDecoration(
hintText: "Select Role",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(14),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
icon: const Icon(Icons.expand_more, size: 20),
isDense: true,
items: controller.roles.map((role) {
return DropdownMenuItem<String>(
value: role['id'],
child: Text(role['name']),
);
}).toList(),
onChanged: controller.onRoleSelected,
),
],
),
),
]),
MySpacing.height(24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyButton.text(
onPressed: () {
Get.toNamed('/dashboard/employees');
},
padding: MySpacing.xy(20, 16),
splashColor:
contentTheme.secondary.withValues(alpha: 0.1),
child: MyText.bodySmall('Cancel'),
),
MySpacing.width(12),
MyButton(
onPressed: () async {
if (controller.basicValidator.validateForm()) {
await controller.createEmployees();
}
},
elevation: 0,
padding: MySpacing.xy(20, 16),
backgroundColor: contentTheme.primary,
borderRadiusAll: AppStyle.buttonRadius.medium,
child: MyText.bodySmall(
'Save',
color: contentTheme.onPrimary,
),
),
],
),
],
),
],
),
),
);
}
}

View File

@ -40,7 +40,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
@override
Widget build(BuildContext context) {
return Layout(
child: MyRefreshWrapper(
child: MyRefreshableContent(
onRefresh: () async {
if (attendanceController.selectedProjectId != null) {
await attendanceController.fetchEmployeesByProject(

View File

@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_flex.dart';
import 'package:marco/helpers/widgets/my_flex_item.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
import 'package:marco/helpers/widgets/my_loading_component.dart';
import 'package:marco/helpers/widgets/my_refresh_wrapper.dart';
import 'package:marco/model/my_paginated_table.dart';
class EmployeeScreen extends StatefulWidget {
const EmployeeScreen({super.key});
@override
State<EmployeeScreen> createState() => _EmployeeScreenState();
}
class _EmployeeScreenState extends State<EmployeeScreen> with UIMixin {
final EmployeesScreenController employeesScreenController =
Get.put(EmployeesScreenController());
final PermissionController permissionController =
Get.put(PermissionController());
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
employeesScreenController.selectedProjectId = null;
await employeesScreenController.fetchAllEmployees();
employeesScreenController.update();
});
}
@override
Widget build(BuildContext context) {
return Layout(
child: Stack(
children: [
GetBuilder<EmployeesScreenController>(
init: employeesScreenController,
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: MySpacing.x(flexSpacing),
child: MyText.titleMedium("Employee",
fontSize: 18, fontWeight: 600),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing),
child: MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Dashboard'),
MyBreadcrumbItem(name: 'Employee', active: true),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.black,
width: 1.5,
),
borderRadius: BorderRadius.circular(4),
),
child: PopupMenuButton<String>(
onSelected: (String value) async {
if (value.isEmpty) {
employeesScreenController.selectedProjectId =
null;
await employeesScreenController
.fetchAllEmployees();
} else {
employeesScreenController.selectedProjectId =
value;
await employeesScreenController
.fetchEmployeesByProject(value);
}
employeesScreenController.update();
},
itemBuilder: (BuildContext context) {
List<PopupMenuItem<String>> items = [
PopupMenuItem<String>(
value: '',
child: MyText.bodySmall('All Employees',
fontWeight: 600),
),
];
items.addAll(
employeesScreenController.projects
.map<PopupMenuItem<String>>((project) {
return PopupMenuItem<String>(
value: project.id,
child: MyText.bodySmall(project.name),
);
}).toList(),
);
return items;
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 8.0),
child: MyText.bodySmall(
employeesScreenController.selectedProjectId ==
null
? 'All Employees'
: employeesScreenController.projects
.firstWhere((project) =>
project.id ==
employeesScreenController
.selectedProjectId)
.name,
fontWeight: 600,
),
),
),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
Get.toNamed('/employees/addEmployee');
},
child: Text('Add New Employee'),
),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing / 2),
child: MyFlex(
children: [
MyFlexItem(sizes: 'lg-6 ', child: employeeListTab()),
],
),
),
],
);
},
),
Obx(() {
return employeesScreenController.isLoading.value
? Container(
color: Colors.black.withOpacity(0.05),
child: const Center(
child: LoadingComponent(
isLoading: true,
loadingText: 'Loading Employees...',
child: SizedBox.shrink(),
),
),
)
: const SizedBox.shrink();
}),
],
),
);
}
Widget employeeListTab() {
if (employeesScreenController.employees.isEmpty) {
return Center(
child: MyText.bodySmall("No Employees Assigned to This Project",
fontWeight: 600),
);
}
final columns = <DataColumn>[
DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Contact', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Actions', color: contentTheme.primary)),
];
final rows =
employeesScreenController.employees.asMap().entries.map((entry) {
var employee = entry.value;
return DataRow(
cells: [
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(employee.name, fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(employee.jobRole, color: Colors.grey),
],
),
),
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(employee.email, fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(employee.phoneNumber, color: Colors.grey),
],
),
),
DataCell(
Row(
children: [
IconButton(
icon: const Icon(Icons.visibility),
tooltip: 'View',
onPressed: () {
// View employee action
},
),
IconButton(
icon: const Icon(Icons.edit),
tooltip: 'Edit',
onPressed: () {
// Edit employee action
},
),
],
),
),
],
);
}).toList();
return MyRefreshableContent(
onRefresh: () async {
if (employeesScreenController.selectedProjectId == null) {
await employeesScreenController.fetchAllEmployees();
} else {
await employeesScreenController.fetchEmployeesByProject(
employeesScreenController.selectedProjectId!,
);
}
},
child: MyPaginatedTable(
columns: columns,
rows: rows,
),
);
}
}

View File

@ -119,6 +119,11 @@ class _LeftBarState extends State<LeftBar>
title: "Attendance",
isCondensed: isCondensed,
route: '/dashboard/attendance'),
NavigationItem(
iconData: LucideIcons.users,
title: "Employees",
isCondensed: isCondensed,
route: '/dashboard/employees'),
],
),
),