From 474ecac53c605eb1088b4afa3433b96833058e73 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 18 Nov 2025 11:17:33 +0530 Subject: [PATCH] refactor: improve null safety in employee details and enhance UI handling --- .../employee_detail_bottom_sheet.dart | 6 +- .../employees/employee_details_model.dart | 97 +++++++++-------- .../employees/employee_detail_screen.dart | 101 ++++++++---------- 3 files changed, 98 insertions(+), 106 deletions(-) diff --git a/lib/model/employees/employee_detail_bottom_sheet.dart b/lib/model/employees/employee_detail_bottom_sheet.dart index 29e9f24..5509992 100644 --- a/lib/model/employees/employee_detail_bottom_sheet.dart +++ b/lib/model/employees/employee_detail_bottom_sheet.dart @@ -123,8 +123,8 @@ class _EmployeeDetailBottomSheetState extends State { radius: 40, backgroundColor: Colors.blueGrey[200], child: Avatar( - firstName: employee.firstName, - lastName: employee.lastName, + firstName: employee.firstName ?? '', + lastName: employee.lastName ?? '', size: 60, ), ), @@ -172,7 +172,7 @@ class _EmployeeDetailBottomSheetState extends State { backgroundColor: Colors.transparent, builder: (context) => AssignProjectBottomSheet( employeeId: widget.employeeId, - jobRoleId: employee.jobRoleId, + jobRoleId: employee.jobRoleId ?? '', ), ); }, diff --git a/lib/model/employees/employee_details_model.dart b/lib/model/employees/employee_details_model.dart index e999836..de882d5 100644 --- a/lib/model/employees/employee_details_model.dart +++ b/lib/model/employees/employee_details_model.dart @@ -1,52 +1,51 @@ class EmployeeDetailsModel { - final String id; - final String firstName; - final String lastName; + final String? id; + final String? firstName; + final String? lastName; final String? middleName; final String? email; - final String gender; + final String? gender; final DateTime? birthDate; final DateTime? joiningDate; final String? permanentAddress; final String? currentAddress; - final String phoneNumber; + final String? phoneNumber; final String? emergencyPhoneNumber; final String? emergencyContactPerson; - final bool isActive; - final bool isRootUser; - final bool isSystem; - final String jobRole; - final String jobRoleId; + final bool? isActive; + final bool? isRootUser; + final bool? isSystem; + final String? jobRole; + final String? jobRoleId; final String? photo; final String? applicationUserId; - final bool hasApplicationAccess; + final bool? hasApplicationAccess; final String? organizationId; final String? aadharNumber; final String? panNumber; - - + EmployeeDetailsModel({ - required this.id, - required this.firstName, - required this.lastName, + this.id, + this.firstName, + this.lastName, this.middleName, this.email, - required this.gender, + this.gender, this.birthDate, this.joiningDate, this.permanentAddress, this.currentAddress, - required this.phoneNumber, + this.phoneNumber, this.emergencyPhoneNumber, this.emergencyContactPerson, - required this.isActive, - required this.isRootUser, - required this.isSystem, - required this.jobRole, - required this.jobRoleId, + this.isActive, + this.isRootUser, + this.isSystem, + this.jobRole, + this.jobRoleId, this.photo, this.applicationUserId, - required this.hasApplicationAccess, + this.hasApplicationAccess, this.organizationId, this.aadharNumber, this.panNumber, @@ -54,30 +53,30 @@ class EmployeeDetailsModel { factory EmployeeDetailsModel.fromJson(Map json) { return EmployeeDetailsModel( - id: json['id'], - firstName: json['firstName'], - lastName: json['lastName'], - middleName: json['middleName'], - email: json['email'], - gender: json['gender'], - birthDate: _parseDate(json['birthDate']), - joiningDate: _parseDate(json['joiningDate']), - permanentAddress: json['permanentAddress'], - currentAddress: json['currentAddress'], - phoneNumber: json['phoneNumber'], - emergencyPhoneNumber: json['emergencyPhoneNumber'], - emergencyContactPerson: json['emergencyContactPerson'], - isActive: json['isActive'], - isRootUser: json['isRootUser'], - isSystem: json['isSystem'], - jobRole: json['jobRole'], - jobRoleId: json['jobRoleId'], - photo: json['photo'], - applicationUserId: json['applicationUserId'], - hasApplicationAccess: json['hasApplicationAccess'], - organizationId: json['organizationId'], - aadharNumber: json['aadharNumber'], - panNumber: json['panNumber'], + id: json['id'] as String?, + firstName: json['firstName'] as String?, + lastName: json['lastName'] as String?, + middleName: json['middleName'] as String?, + email: json['email'] as String?, + gender: json['gender'] as String?, + birthDate: _parseDate(json['birthDate'] as String?), + joiningDate: _parseDate(json['joiningDate'] as String?), + permanentAddress: json['permanentAddress'] as String?, + currentAddress: json['currentAddress'] as String?, + phoneNumber: json['phoneNumber'] as String?, + emergencyPhoneNumber: json['emergencyPhoneNumber'] as String?, + emergencyContactPerson: json['emergencyContactPerson'] as String?, + isActive: json['isActive'] as bool?, + isRootUser: json['isRootUser'] as bool?, + isSystem: json['isSystem'] as bool?, + jobRole: json['jobRole'] as String?, + jobRoleId: json['jobRoleId'] as String?, + photo: json['photo'] as String?, + applicationUserId: json['applicationUserId'] as String?, + hasApplicationAccess: json['hasApplicationAccess'] as bool?, + organizationId: json['organizationId'] as String?, + aadharNumber: json['aadharNumber'] as String?, + panNumber: json['panNumber'] as String?, ); } @@ -116,4 +115,4 @@ class EmployeeDetailsModel { } return DateTime.tryParse(dateStr); } -} \ No newline at end of file +} diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 3177f8c..18d35c4 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -13,9 +13,8 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; -import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; +import 'package:marco/model/employees/employee_details_model.dart'; class EmployeeDetailPage extends StatefulWidget { final String employeeId; @@ -177,16 +176,19 @@ class _EmployeeDetailPageState extends State with UIMixin { return SkeletonLoaders.employeeDetailSkeletonLoader(); } - final employee = controller.selectedEmployeeDetails.value; + final EmployeeDetailsModel? employee = + controller.selectedEmployeeDetails.value; if (employee == null) { - return Center(child: MyText("No employee details found.")); + return Center(child: MyText("No employee details found.")); } return SafeArea( child: MyRefreshIndicator( onRefresh: () async { await controller.fetchEmployeeDetails(widget.employeeId); - await controller.fetchReportingManagers(employee.id); + if (employee.id != null) { + await controller.fetchReportingManagers(employee.id!); + } }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), @@ -206,8 +208,8 @@ class _EmployeeDetailPageState extends State with UIMixin { child: Row( children: [ Avatar( - firstName: employee.firstName, - lastName: employee.lastName, + firstName: employee.firstName ?? "", + lastName: employee.lastName ?? "", size: 35, ), MySpacing.width(16), @@ -216,7 +218,7 @@ class _EmployeeDetailPageState extends State with UIMixin { crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleMedium( - '${employee.firstName} ${employee.lastName}', + '${employee.firstName ?? ""} ${employee.lastName ?? ""}', fontWeight: 700, ), MySpacing.height(6), @@ -231,8 +233,8 @@ class _EmployeeDetailPageState extends State with UIMixin { icon: Icon(Icons.edit, size: 24, color: contentTheme.primary), onPressed: () async { - final result = await showModalBottomSheet< - Map>( + final result = + await showModalBottomSheet>( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, @@ -244,11 +246,11 @@ class _EmployeeDetailPageState extends State with UIMixin { 'phone_number': employee.phoneNumber, 'email': employee.email, 'hasApplicationAccess': - employee.hasApplicationAccess, - 'gender': employee.gender.toLowerCase(), + employee.hasApplicationAccess ?? false, + 'gender': employee.gender?.toLowerCase() ?? '', 'job_role_id': employee.jobRoleId, - 'joining_date': employee.joiningDate - ?.toIso8601String(), + 'joining_date': + employee.joiningDate?.toIso8601String(), }, ), ); @@ -279,28 +281,29 @@ class _EmployeeDetailPageState extends State with UIMixin { backgroundColor: Colors.transparent, builder: (_) => ManageReportingBottomSheet( initialEmployee: EmployeeModel( - id: employee.id, - employeeId: employee.id.toString(), - firstName: employee.firstName ?? "", - lastName: employee.lastName ?? "", + id: employee.id ?? '', + employeeId: employee.id ?? '', + firstName: employee.firstName ?? '', + lastName: employee.lastName ?? '', name: - "${employee.firstName} ${employee.lastName}", - email: employee.email ?? "", - jobRole: employee.jobRole ?? "", + "${employee.firstName ?? ''} ${employee.lastName ?? ''}", + email: employee.email ?? '', + jobRole: employee.jobRole ?? '', jobRoleID: "0", - designation: employee.jobRole ?? "", - phoneNumber: employee.phoneNumber ?? "", + designation: employee.jobRole ?? '', + phoneNumber: employee.phoneNumber ?? '', activity: 0, action: 0, ), hideMainSelector: true, hideLoggedUserFromSelection: true, - loggedUserId: - controller.selectedEmployeeDetails.value?.id, + loggedUserId: controller.selectedEmployeeDetails.value?.id, ), ); - await controller.fetchReportingManagers(employee.id); + if (employee.id != null) { + await controller.fetchReportingManagers(employee.id!); + } }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), @@ -353,14 +356,12 @@ class _EmployeeDetailPageState extends State with UIMixin { Text( 'Primary → ${_getManagerNames(primary)}', style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600), + fontSize: 14, fontWeight: FontWeight.w600), ), Text( 'Secondary → ${_getManagerNames(secondary)}', style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600), + fontSize: 14, fontWeight: FontWeight.w600), ), ], ), @@ -383,16 +384,15 @@ class _EmployeeDetailPageState extends State with UIMixin { isActionable: true, onTap: () { if (employee.email != null && - employee.email.toString().trim().isNotEmpty) { + employee.email!.trim().isNotEmpty) { LauncherUtils.launchEmail(employee.email!); } }, onLongPress: () { if (employee.email != null && - employee.email.toString().trim().isNotEmpty) { + employee.email!.trim().isNotEmpty) { LauncherUtils.copyToClipboard( - employee.email!, - typeLabel: 'Email'); + employee.email!, typeLabel: 'Email'); } }, ), @@ -402,16 +402,16 @@ class _EmployeeDetailPageState extends State with UIMixin { value: _getDisplayValue(employee.phoneNumber), isActionable: true, onTap: () { - if (employee.phoneNumber.trim().isNotEmpty) { - LauncherUtils.launchPhone(employee.phoneNumber); + if (employee.phoneNumber != null && + employee.phoneNumber!.trim().isNotEmpty) { + LauncherUtils.launchPhone(employee.phoneNumber!); } }, onLongPress: () { - if (employee.phoneNumber.trim().isNotEmpty) { + if (employee.phoneNumber != null && + employee.phoneNumber!.trim().isNotEmpty) { LauncherUtils.copyToClipboard( - employee.phoneNumber, - typeLabel: 'Phone Number', - ); + employee.phoneNumber!, typeLabel: 'Phone Number'); } }, ), @@ -428,29 +428,22 @@ class _EmployeeDetailPageState extends State with UIMixin { _buildDetailRow( icon: Icons.person_outline, label: 'Contact Person', - value: - _getDisplayValue(employee.emergencyContactPerson), + value: _getDisplayValue(employee.emergencyContactPerson), ), _buildDetailRow( icon: Icons.phone_in_talk_outlined, label: 'Emergency Phone', - value: - _getDisplayValue(employee.emergencyPhoneNumber), + value: _getDisplayValue(employee.emergencyPhoneNumber), isActionable: true, onTap: () { if (employee.emergencyPhoneNumber != null && - employee.emergencyPhoneNumber! - .trim() - .isNotEmpty) { - LauncherUtils.launchPhone( - employee.emergencyPhoneNumber!); + employee.emergencyPhoneNumber!.trim().isNotEmpty) { + LauncherUtils.launchPhone(employee.emergencyPhoneNumber!); } }, onLongPress: () { if (employee.emergencyPhoneNumber != null && - employee.emergencyPhoneNumber! - .trim() - .isNotEmpty) { + employee.emergencyPhoneNumber!.trim().isNotEmpty) { LauncherUtils.copyToClipboard( employee.emergencyPhoneNumber!, typeLabel: 'Emergency Phone'); @@ -513,7 +506,7 @@ class _EmployeeDetailPageState extends State with UIMixin { /// ------------------ FLOATING BUTTON ------------------ floatingActionButton: Obx(() { - final employee = controller.selectedEmployeeDetails.value; + final EmployeeDetailsModel? employee = controller.selectedEmployeeDetails.value; if (employee == null) return const SizedBox.shrink(); return FloatingActionButton.extended( @@ -524,7 +517,7 @@ class _EmployeeDetailPageState extends State with UIMixin { backgroundColor: Colors.transparent, builder: (context) => AssignProjectBottomSheet( employeeId: widget.employeeId, - jobRoleId: employee.jobRoleId, + jobRoleId: employee.jobRoleId ?? '', ), ); },