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