made chnages in employee details screen

This commit is contained in:
Vaibhav Surve 2025-11-08 12:17:17 +05:30
parent 910bb5e6b4
commit eb46194679
2 changed files with 425 additions and 187 deletions

View File

@ -33,6 +33,143 @@ class SkeletonLoaders {
);
}
// Employee Detail Skeleton Loader
static Widget employeeDetailSkeletonLoader() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(12, 20, 12, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Header skeleton (avatar + name + role)
MyCard(
borderRadiusAll: 8,
paddingAll: 16,
margin: MySpacing.bottom(16),
shadow: MyShadow(elevation: 2),
child: Row(
children: [
// Avatar
Container(
width: 45,
height: 45,
decoration: BoxDecoration(
color: Colors.grey.shade300,
shape: BoxShape.circle,
),
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 14,
width: 120,
color: Colors.grey.shade300,
),
MySpacing.height(6),
Container(
height: 12,
width: 80,
color: Colors.grey.shade300,
),
],
),
),
Container(
width: 24,
height: 24,
color: Colors.grey.shade300,
),
],
),
),
// Sections skeleton
...List.generate(
4,
(_) => Column(
children: [
MyCard(
borderRadiusAll: 8,
paddingAll: 16,
margin: MySpacing.bottom(16),
shadow: MyShadow(elevation: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section header
Row(
children: [
Container(
width: 20,
height: 20,
color: Colors.grey.shade300,
),
MySpacing.width(8),
Container(
width: 120,
height: 14,
color: Colors.grey.shade300,
),
],
),
MySpacing.height(8),
const Divider(color: Colors.grey),
// 2 rows placeholder
...List.generate(
2,
(_) => Padding(
padding: const EdgeInsets.symmetric(
vertical: 12),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Container(
width: 20,
height: 20,
color: Colors.grey.shade300,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Container(
height: 12,
width: 100,
color: Colors.grey.shade300,
),
MySpacing.height(4),
Container(
height: 12,
width: 150,
color: Colors.grey.shade300,
),
],
),
),
Container(
width: 20,
height: 20,
color: Colors.grey.shade300,
),
],
),
)),
],
),
),
],
)),
],
),
);
}
// Daily Progress Planning - Infra (Expanded) Skeleton Loader
static Widget dailyProgressPlanningInfraSkeleton() {
return Column(

View File

@ -6,14 +6,11 @@ import 'package:marco/helpers/widgets/custom_app_bar.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/employees/assign_employee_bottom_sheet.dart';
import 'package:marco/helpers/utils/launcher_utils.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/model/employees/add_employee_bottom_sheet.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
class EmployeeDetailPage extends StatefulWidget {
final String employeeId;
@ -29,11 +26,9 @@ class EmployeeDetailPage extends StatefulWidget {
State<EmployeeDetailPage> createState() => _EmployeeDetailPageState();
}
class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
final EmployeesScreenController controller =
Get.put(EmployeesScreenController());
final PermissionController permissionController =
Get.put(PermissionController());
@override
void initState() {
@ -65,77 +60,58 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
}
}
Widget _buildLabelValueRow(String label, String value,
{bool isMultiLine = false}) {
final lowerLabel = label.toLowerCase();
final isEmail = lowerLabel == 'email';
final isPhone =
lowerLabel == 'phone number' || lowerLabel == 'emergency phone number';
void handleTap() {
if (value == 'NA') return;
if (isEmail) {
LauncherUtils.launchEmail(value);
} else if (isPhone) {
LauncherUtils.launchPhone(value);
}
}
void handleLongPress() {
if (value == 'NA') return;
LauncherUtils.copyToClipboard(value, typeLabel: label);
}
final valueWidget = GestureDetector(
onTap: (isEmail || isPhone) ? handleTap : null,
onLongPress: (isEmail || isPhone) ? handleLongPress : null,
child: Text(
value,
style: TextStyle(
fontWeight: FontWeight.normal,
color: (isEmail || isPhone) ? Colors.indigo : Colors.black54,
fontSize: 14,
decoration: (isEmail || isPhone)
? TextDecoration.underline
: TextDecoration.none,
),
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isMultiLine) ...[
Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
fontSize: 14,
Widget _buildDetailRow({
required IconData icon,
required String label,
required String value,
VoidCallback? onTap,
VoidCallback? onLongPress,
bool isActionable = false,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: InkWell(
onTap: isActionable && value != 'NA' ? onTap : null,
onLongPress: isActionable && value != 'NA' ? onLongPress : null,
borderRadius: BorderRadius.circular(5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: contentTheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(5),
),
child: Icon(
icon,
size: 20,
color: contentTheme.primary,
),
),
),
MySpacing.height(4),
valueWidget,
] else
GestureDetector(
onTap: (isEmail || isPhone) ? handleTap : null,
onLongPress: (isEmail || isPhone) ? handleLongPress : null,
child: RichText(
text: TextSpan(
text: "$label: ",
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
fontSize: 14,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextSpan(
text: value,
Text(
label,
style: TextStyle(
fontWeight: FontWeight.normal,
color:
(isEmail || isPhone) ? Colors.indigo : Colors.black54,
decoration: (isEmail || isPhone)
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
MySpacing.height(4),
Text(
value,
style: TextStyle(
fontSize: 15,
color: isActionable && value != 'NA'
? contentTheme.primary
: Colors.black87,
fontWeight: FontWeight.w500,
decoration: isActionable && value != 'NA'
? TextDecoration.underline
: TextDecoration.none,
),
@ -143,46 +119,53 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
],
),
),
),
MySpacing.height(10),
Divider(color: Colors.grey[300], height: 1),
MySpacing.height(10),
],
if (isActionable && value != 'NA')
Icon(
Icons.chevron_right,
color: Colors.grey[400],
size: 20,
),
],
),
),
);
}
Widget _buildInfoCard(employee) {
Widget _buildSectionCard({
required String title,
required IconData titleIcon,
required List<Widget> children,
}) {
return Card(
elevation: 3,
elevation: 2,
shadowColor: Colors.black12,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 16, 12, 16),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(12),
_buildLabelValueRow('Email', _getDisplayValue(employee.email)),
_buildLabelValueRow(
'Phone Number', _getDisplayValue(employee.phoneNumber)),
_buildLabelValueRow('Emergency Contact Person',
_getDisplayValue(employee.emergencyContactPerson)),
_buildLabelValueRow('Emergency Phone Number',
_getDisplayValue(employee.emergencyPhoneNumber)),
_buildLabelValueRow('Gender', _getDisplayValue(employee.gender)),
_buildLabelValueRow('Birth Date', _formatDate(employee.birthDate)),
_buildLabelValueRow(
'Joining Date', _formatDate(employee.joiningDate)),
_buildLabelValueRow(
'Current Address',
_getDisplayValue(employee.currentAddress),
isMultiLine: true,
),
_buildLabelValueRow(
'Permanent Address',
_getDisplayValue(employee.permanentAddress),
isMultiLine: true,
Row(
children: [
Icon(
titleIcon,
size: 20,
color: contentTheme.primary,
),
MySpacing.width(8),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
MySpacing.height(8),
const Divider(),
...children,
],
),
),
@ -209,7 +192,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
: null,
body: Obx(() {
if (controller.isLoadingEmployeeDetails.value) {
return const Center(child: CircularProgressIndicator());
return SkeletonLoaders.employeeDetailSkeletonLoader();
}
final employee = controller.selectedEmployeeDetails.value;
@ -228,101 +211,219 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 45,
),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
'${employee.firstName} ${employee.lastName}',
fontWeight: 700,
// Header Section
Card(
elevation: 2,
shadowColor: Colors.black12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 45,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
'${employee.firstName} ${employee.lastName}',
fontWeight: 700,
),
MySpacing.height(6),
MyText.bodySmall(
_getDisplayValue(employee.jobRole),
fontWeight: 500,
),
],
),
MySpacing.height(6),
MyText.bodySmall(
_getDisplayValue(employee.jobRole),
fontWeight: 500,
),
],
),
),
IconButton(
icon:
Icon(Icons.edit, size: 24, color: contentTheme.primary),
onPressed: () async {
final result =
await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => AddEmployeeBottomSheet(
employeeData: {
'id': employee.id,
'first_name': employee.firstName,
'last_name': employee.lastName,
'phone_number': employee.phoneNumber,
'email': employee.email,
'hasApplicationAccess':
employee.hasApplicationAccess,
'gender': employee.gender.toLowerCase(),
'job_role_id': employee.jobRoleId,
'joining_date':
employee.joiningDate?.toIso8601String(),
'organization_id': employee.organizationId,
},
),
);
),
IconButton(
icon: Icon(Icons.edit,
size: 24, color: contentTheme.primary),
onPressed: () async {
final result = await showModalBottomSheet<
Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => AddEmployeeBottomSheet(
employeeData: {
'id': employee.id,
'first_name': employee.firstName,
'last_name': employee.lastName,
'phone_number': employee.phoneNumber,
'email': employee.email,
'hasApplicationAccess':
employee.hasApplicationAccess,
'gender': employee.gender.toLowerCase(),
'job_role_id': employee.jobRoleId,
'joining_date':
employee.joiningDate?.toIso8601String(),
},
),
);
if (result != null) {
controller.fetchEmployeeDetails(widget.employeeId);
if (result != null) {
controller
.fetchEmployeeDetails(widget.employeeId);
}
},
),
],
),
),
),
MySpacing.height(16),
// Contact Information Section
_buildSectionCard(
title: 'Contact Information',
titleIcon: Icons.contact_phone,
children: [
_buildDetailRow(
icon: Icons.email_outlined,
label: 'Email',
value: _getDisplayValue(employee.email),
isActionable: true,
onTap: () {
if (employee.email != null &&
employee.email.toString().trim().isNotEmpty) {
LauncherUtils.launchEmail(employee.email!);
}
},
onLongPress: () {
if (employee.email != null &&
employee.email.toString().trim().isNotEmpty) {
LauncherUtils.copyToClipboard(employee.email!,
typeLabel: 'Email');
}
},
),
_buildDetailRow(
icon: Icons.phone_outlined,
label: 'Phone Number',
value: _getDisplayValue(employee.phoneNumber),
isActionable: true,
onTap: () {
if (employee.phoneNumber.trim().isNotEmpty) {
LauncherUtils.launchPhone(employee.phoneNumber);
}
},
onLongPress: () {
if (employee.phoneNumber.trim().isNotEmpty) {
LauncherUtils.copyToClipboard(
employee.phoneNumber,
typeLabel: 'Phone Number',
);
}
},
),
],
),
MySpacing.height(14),
_buildInfoCard(employee),
MySpacing.height(16),
// Emergency Contact Section
_buildSectionCard(
title: 'Emergency Contact',
titleIcon: Icons.emergency,
children: [
_buildDetailRow(
icon: Icons.person_outline,
label: 'Contact Person',
value:
_getDisplayValue(employee.emergencyContactPerson),
isActionable: false,
),
_buildDetailRow(
icon: Icons.phone_in_talk_outlined,
label: 'Emergency Phone',
value: _getDisplayValue(employee.emergencyPhoneNumber),
isActionable: true,
onTap: () {
if (employee.emergencyPhoneNumber != null &&
employee.emergencyPhoneNumber
.toString()
.trim()
.isNotEmpty) {
LauncherUtils.launchPhone(
employee.emergencyPhoneNumber!);
}
},
onLongPress: () {
if (employee.emergencyPhoneNumber != null &&
employee.emergencyPhoneNumber
.toString()
.trim()
.isNotEmpty) {
LauncherUtils.copyToClipboard(
employee.emergencyPhoneNumber!,
typeLabel: 'Emergency Phone');
}
},
),
],
),
MySpacing.height(16),
// Personal Information Section
_buildSectionCard(
title: 'Personal Information',
titleIcon: Icons.person,
children: [
_buildDetailRow(
icon: Icons.wc_outlined,
label: 'Gender',
value: _getDisplayValue(employee.gender),
isActionable: false,
),
_buildDetailRow(
icon: Icons.cake_outlined,
label: 'Birth Date',
value: _formatDate(employee.birthDate),
isActionable: false,
),
_buildDetailRow(
icon: Icons.work_outline,
label: 'Joining Date',
value: _formatDate(employee.joiningDate),
isActionable: false,
),
],
),
MySpacing.height(16),
// Address Information Section
_buildSectionCard(
title: 'Address Information',
titleIcon: Icons.location_on,
children: [
_buildDetailRow(
icon: Icons.home_outlined,
label: 'Current Address',
value: _getDisplayValue(employee.currentAddress),
isActionable: false,
),
_buildDetailRow(
icon: Icons.home_work_outlined,
label: 'Permanent Address',
value: _getDisplayValue(employee.permanentAddress),
isActionable: false,
),
],
),
],
),
),
),
);
}),
floatingActionButton: Obx(() {
if (!permissionController.hasPermission(Permissions.assignToProject)) {
return const SizedBox.shrink();
}
if (controller.isLoadingEmployeeDetails.value ||
controller.selectedEmployeeDetails.value == null) {
return const SizedBox.shrink();
}
final employee = controller.selectedEmployeeDetails.value!;
return FloatingActionButton.extended(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => AssignProjectBottomSheet(
employeeId: widget.employeeId,
jobRoleId: employee.jobRoleId,
),
);
},
backgroundColor: contentTheme.primary,
icon: const Icon(Icons.assignment),
label: const Text(
'Assign to Project',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
),
);
}),
);
}
}