refactor: improve null safety in employee details and enhance UI handling

This commit is contained in:
Vaibhav Surve 2025-11-18 11:17:33 +05:30
parent dc4ea7979c
commit 474ecac53c
3 changed files with 98 additions and 106 deletions

View File

@ -123,8 +123,8 @@ class _EmployeeDetailBottomSheetState extends State<EmployeeDetailBottomSheet> {
radius: 40, radius: 40,
backgroundColor: Colors.blueGrey[200], backgroundColor: Colors.blueGrey[200],
child: Avatar( child: Avatar(
firstName: employee.firstName, firstName: employee.firstName ?? '',
lastName: employee.lastName, lastName: employee.lastName ?? '',
size: 60, size: 60,
), ),
), ),
@ -172,7 +172,7 @@ class _EmployeeDetailBottomSheetState extends State<EmployeeDetailBottomSheet> {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
builder: (context) => AssignProjectBottomSheet( builder: (context) => AssignProjectBottomSheet(
employeeId: widget.employeeId, employeeId: widget.employeeId,
jobRoleId: employee.jobRoleId, jobRoleId: employee.jobRoleId ?? '',
), ),
); );
}, },

View File

@ -1,52 +1,51 @@
class EmployeeDetailsModel { class EmployeeDetailsModel {
final String id; final String? id;
final String firstName; final String? firstName;
final String lastName; final String? lastName;
final String? middleName; final String? middleName;
final String? email; final String? email;
final String gender; final String? gender;
final DateTime? birthDate; final DateTime? birthDate;
final DateTime? joiningDate; final DateTime? joiningDate;
final String? permanentAddress; final String? permanentAddress;
final String? currentAddress; final String? currentAddress;
final String phoneNumber; final String? phoneNumber;
final String? emergencyPhoneNumber; final String? emergencyPhoneNumber;
final String? emergencyContactPerson; final String? emergencyContactPerson;
final bool isActive; final bool? isActive;
final bool isRootUser; final bool? isRootUser;
final bool isSystem; final bool? isSystem;
final String jobRole; final String? jobRole;
final String jobRoleId; final String? jobRoleId;
final String? photo; final String? photo;
final String? applicationUserId; final String? applicationUserId;
final bool hasApplicationAccess; final bool? hasApplicationAccess;
final String? organizationId; final String? organizationId;
final String? aadharNumber; final String? aadharNumber;
final String? panNumber; final String? panNumber;
EmployeeDetailsModel({ EmployeeDetailsModel({
required this.id, this.id,
required this.firstName, this.firstName,
required this.lastName, this.lastName,
this.middleName, this.middleName,
this.email, this.email,
required this.gender, this.gender,
this.birthDate, this.birthDate,
this.joiningDate, this.joiningDate,
this.permanentAddress, this.permanentAddress,
this.currentAddress, this.currentAddress,
required this.phoneNumber, this.phoneNumber,
this.emergencyPhoneNumber, this.emergencyPhoneNumber,
this.emergencyContactPerson, this.emergencyContactPerson,
required this.isActive, this.isActive,
required this.isRootUser, this.isRootUser,
required this.isSystem, this.isSystem,
required this.jobRole, this.jobRole,
required this.jobRoleId, this.jobRoleId,
this.photo, this.photo,
this.applicationUserId, this.applicationUserId,
required this.hasApplicationAccess, this.hasApplicationAccess,
this.organizationId, this.organizationId,
this.aadharNumber, this.aadharNumber,
this.panNumber, this.panNumber,
@ -54,30 +53,30 @@ class EmployeeDetailsModel {
factory EmployeeDetailsModel.fromJson(Map<String, dynamic> json) { factory EmployeeDetailsModel.fromJson(Map<String, dynamic> json) {
return EmployeeDetailsModel( return EmployeeDetailsModel(
id: json['id'], id: json['id'] as String?,
firstName: json['firstName'], firstName: json['firstName'] as String?,
lastName: json['lastName'], lastName: json['lastName'] as String?,
middleName: json['middleName'], middleName: json['middleName'] as String?,
email: json['email'], email: json['email'] as String?,
gender: json['gender'], gender: json['gender'] as String?,
birthDate: _parseDate(json['birthDate']), birthDate: _parseDate(json['birthDate'] as String?),
joiningDate: _parseDate(json['joiningDate']), joiningDate: _parseDate(json['joiningDate'] as String?),
permanentAddress: json['permanentAddress'], permanentAddress: json['permanentAddress'] as String?,
currentAddress: json['currentAddress'], currentAddress: json['currentAddress'] as String?,
phoneNumber: json['phoneNumber'], phoneNumber: json['phoneNumber'] as String?,
emergencyPhoneNumber: json['emergencyPhoneNumber'], emergencyPhoneNumber: json['emergencyPhoneNumber'] as String?,
emergencyContactPerson: json['emergencyContactPerson'], emergencyContactPerson: json['emergencyContactPerson'] as String?,
isActive: json['isActive'], isActive: json['isActive'] as bool?,
isRootUser: json['isRootUser'], isRootUser: json['isRootUser'] as bool?,
isSystem: json['isSystem'], isSystem: json['isSystem'] as bool?,
jobRole: json['jobRole'], jobRole: json['jobRole'] as String?,
jobRoleId: json['jobRoleId'], jobRoleId: json['jobRoleId'] as String?,
photo: json['photo'], photo: json['photo'] as String?,
applicationUserId: json['applicationUserId'], applicationUserId: json['applicationUserId'] as String?,
hasApplicationAccess: json['hasApplicationAccess'], hasApplicationAccess: json['hasApplicationAccess'] as bool?,
organizationId: json['organizationId'], organizationId: json['organizationId'] as String?,
aadharNumber: json['aadharNumber'], aadharNumber: json['aadharNumber'] as String?,
panNumber: json['panNumber'], panNumber: json['panNumber'] as String?,
); );
} }

View File

@ -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/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart';
import 'package:marco/model/employees/employee_model.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/view/employees/assign_employee_bottom_sheet.dart';
import 'package:marco/model/employees/employee_details_model.dart';
class EmployeeDetailPage extends StatefulWidget { class EmployeeDetailPage extends StatefulWidget {
final String employeeId; final String employeeId;
@ -177,7 +176,8 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
return SkeletonLoaders.employeeDetailSkeletonLoader(); return SkeletonLoaders.employeeDetailSkeletonLoader();
} }
final employee = controller.selectedEmployeeDetails.value; final EmployeeDetailsModel? employee =
controller.selectedEmployeeDetails.value;
if (employee == null) { if (employee == null) {
return Center(child: MyText("No employee details found.")); return Center(child: MyText("No employee details found."));
} }
@ -186,7 +186,9 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
child: MyRefreshIndicator( child: MyRefreshIndicator(
onRefresh: () async { onRefresh: () async {
await controller.fetchEmployeeDetails(widget.employeeId); await controller.fetchEmployeeDetails(widget.employeeId);
await controller.fetchReportingManagers(employee.id); if (employee.id != null) {
await controller.fetchReportingManagers(employee.id!);
}
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
@ -206,8 +208,8 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
child: Row( child: Row(
children: [ children: [
Avatar( Avatar(
firstName: employee.firstName, firstName: employee.firstName ?? "",
lastName: employee.lastName, lastName: employee.lastName ?? "",
size: 35, size: 35,
), ),
MySpacing.width(16), MySpacing.width(16),
@ -216,7 +218,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.titleMedium( MyText.titleMedium(
'${employee.firstName} ${employee.lastName}', '${employee.firstName ?? ""} ${employee.lastName ?? ""}',
fontWeight: 700, fontWeight: 700,
), ),
MySpacing.height(6), MySpacing.height(6),
@ -231,8 +233,8 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
icon: Icon(Icons.edit, icon: Icon(Icons.edit,
size: 24, color: contentTheme.primary), size: 24, color: contentTheme.primary),
onPressed: () async { onPressed: () async {
final result = await showModalBottomSheet< final result =
Map<String, dynamic>>( await showModalBottomSheet<Map<String, dynamic>>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
@ -244,11 +246,11 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
'phone_number': employee.phoneNumber, 'phone_number': employee.phoneNumber,
'email': employee.email, 'email': employee.email,
'hasApplicationAccess': 'hasApplicationAccess':
employee.hasApplicationAccess, employee.hasApplicationAccess ?? false,
'gender': employee.gender.toLowerCase(), 'gender': employee.gender?.toLowerCase() ?? '',
'job_role_id': employee.jobRoleId, 'job_role_id': employee.jobRoleId,
'joining_date': employee.joiningDate 'joining_date':
?.toIso8601String(), employee.joiningDate?.toIso8601String(),
}, },
), ),
); );
@ -279,28 +281,29 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
builder: (_) => ManageReportingBottomSheet( builder: (_) => ManageReportingBottomSheet(
initialEmployee: EmployeeModel( initialEmployee: EmployeeModel(
id: employee.id, id: employee.id ?? '',
employeeId: employee.id.toString(), employeeId: employee.id ?? '',
firstName: employee.firstName ?? "", firstName: employee.firstName ?? '',
lastName: employee.lastName ?? "", lastName: employee.lastName ?? '',
name: name:
"${employee.firstName} ${employee.lastName}", "${employee.firstName ?? ''} ${employee.lastName ?? ''}",
email: employee.email ?? "", email: employee.email ?? '',
jobRole: employee.jobRole ?? "", jobRole: employee.jobRole ?? '',
jobRoleID: "0", jobRoleID: "0",
designation: employee.jobRole ?? "", designation: employee.jobRole ?? '',
phoneNumber: employee.phoneNumber ?? "", phoneNumber: employee.phoneNumber ?? '',
activity: 0, activity: 0,
action: 0, action: 0,
), ),
hideMainSelector: true, hideMainSelector: true,
hideLoggedUserFromSelection: true, hideLoggedUserFromSelection: true,
loggedUserId: loggedUserId: controller.selectedEmployeeDetails.value?.id,
controller.selectedEmployeeDetails.value?.id,
), ),
); );
await controller.fetchReportingManagers(employee.id); if (employee.id != null) {
await controller.fetchReportingManagers(employee.id!);
}
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
@ -353,14 +356,12 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
Text( Text(
'Primary → ${_getManagerNames(primary)}', 'Primary → ${_getManagerNames(primary)}',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14, fontWeight: FontWeight.w600),
fontWeight: FontWeight.w600),
), ),
Text( Text(
'Secondary → ${_getManagerNames(secondary)}', 'Secondary → ${_getManagerNames(secondary)}',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14, fontWeight: FontWeight.w600),
fontWeight: FontWeight.w600),
), ),
], ],
), ),
@ -383,16 +384,15 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
isActionable: true, isActionable: true,
onTap: () { onTap: () {
if (employee.email != null && if (employee.email != null &&
employee.email.toString().trim().isNotEmpty) { employee.email!.trim().isNotEmpty) {
LauncherUtils.launchEmail(employee.email!); LauncherUtils.launchEmail(employee.email!);
} }
}, },
onLongPress: () { onLongPress: () {
if (employee.email != null && if (employee.email != null &&
employee.email.toString().trim().isNotEmpty) { employee.email!.trim().isNotEmpty) {
LauncherUtils.copyToClipboard( LauncherUtils.copyToClipboard(
employee.email!, employee.email!, typeLabel: 'Email');
typeLabel: 'Email');
} }
}, },
), ),
@ -402,16 +402,16 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
value: _getDisplayValue(employee.phoneNumber), value: _getDisplayValue(employee.phoneNumber),
isActionable: true, isActionable: true,
onTap: () { onTap: () {
if (employee.phoneNumber.trim().isNotEmpty) { if (employee.phoneNumber != null &&
LauncherUtils.launchPhone(employee.phoneNumber); employee.phoneNumber!.trim().isNotEmpty) {
LauncherUtils.launchPhone(employee.phoneNumber!);
} }
}, },
onLongPress: () { onLongPress: () {
if (employee.phoneNumber.trim().isNotEmpty) { if (employee.phoneNumber != null &&
employee.phoneNumber!.trim().isNotEmpty) {
LauncherUtils.copyToClipboard( LauncherUtils.copyToClipboard(
employee.phoneNumber, employee.phoneNumber!, typeLabel: 'Phone Number');
typeLabel: 'Phone Number',
);
} }
}, },
), ),
@ -428,29 +428,22 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
_buildDetailRow( _buildDetailRow(
icon: Icons.person_outline, icon: Icons.person_outline,
label: 'Contact Person', label: 'Contact Person',
value: value: _getDisplayValue(employee.emergencyContactPerson),
_getDisplayValue(employee.emergencyContactPerson),
), ),
_buildDetailRow( _buildDetailRow(
icon: Icons.phone_in_talk_outlined, icon: Icons.phone_in_talk_outlined,
label: 'Emergency Phone', label: 'Emergency Phone',
value: value: _getDisplayValue(employee.emergencyPhoneNumber),
_getDisplayValue(employee.emergencyPhoneNumber),
isActionable: true, isActionable: true,
onTap: () { onTap: () {
if (employee.emergencyPhoneNumber != null && if (employee.emergencyPhoneNumber != null &&
employee.emergencyPhoneNumber! employee.emergencyPhoneNumber!.trim().isNotEmpty) {
.trim() LauncherUtils.launchPhone(employee.emergencyPhoneNumber!);
.isNotEmpty) {
LauncherUtils.launchPhone(
employee.emergencyPhoneNumber!);
} }
}, },
onLongPress: () { onLongPress: () {
if (employee.emergencyPhoneNumber != null && if (employee.emergencyPhoneNumber != null &&
employee.emergencyPhoneNumber! employee.emergencyPhoneNumber!.trim().isNotEmpty) {
.trim()
.isNotEmpty) {
LauncherUtils.copyToClipboard( LauncherUtils.copyToClipboard(
employee.emergencyPhoneNumber!, employee.emergencyPhoneNumber!,
typeLabel: 'Emergency Phone'); typeLabel: 'Emergency Phone');
@ -513,7 +506,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
/// ------------------ FLOATING BUTTON ------------------ /// ------------------ FLOATING BUTTON ------------------
floatingActionButton: Obx(() { floatingActionButton: Obx(() {
final employee = controller.selectedEmployeeDetails.value; final EmployeeDetailsModel? employee = controller.selectedEmployeeDetails.value;
if (employee == null) return const SizedBox.shrink(); if (employee == null) return const SizedBox.shrink();
return FloatingActionButton.extended( return FloatingActionButton.extended(
@ -524,7 +517,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
builder: (context) => AssignProjectBottomSheet( builder: (context) => AssignProjectBottomSheet(
employeeId: widget.employeeId, employeeId: widget.employeeId,
jobRoleId: employee.jobRoleId, jobRoleId: employee.jobRoleId ?? '',
), ),
); );
}, },