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;
@ -32,8 +29,6 @@ class EmployeeDetailPage extends StatefulWidget {
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,124 +60,112 @@ 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(
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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (isMultiLine) ...[ Container(
Text( padding: const EdgeInsets.all(8),
label, decoration: BoxDecoration(
style: const TextStyle( color: contentTheme.primary.withOpacity(0.1),
fontWeight: FontWeight.bold, borderRadius: BorderRadius.circular(5),
color: Colors.black87, ),
fontSize: 14, child: Icon(
icon,
size: 20,
color: contentTheme.primary,
), ),
), ),
MySpacing.height(4), MySpacing.width(16),
valueWidget, Expanded(
] 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,
),
children: [
TextSpan(
text: value,
style: TextStyle(
fontWeight: FontWeight.normal,
color:
(isEmail || isPhone) ? Colors.indigo : Colors.black54,
decoration: (isEmail || isPhone)
? TextDecoration.underline
: TextDecoration.none,
),
),
],
),
),
),
MySpacing.height(10),
Divider(color: Colors.grey[300], height: 1),
MySpacing.height(10),
],
);
}
Widget _buildInfoCard(employee) {
return Card(
elevation: 3,
shadowColor: Colors.black12,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 16, 12, 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MySpacing.height(12), Text(
_buildLabelValueRow('Email', _getDisplayValue(employee.email)), label,
_buildLabelValueRow( style: TextStyle(
'Phone Number', _getDisplayValue(employee.phoneNumber)), fontSize: 12,
_buildLabelValueRow('Emergency Contact Person', color: Colors.grey[600],
_getDisplayValue(employee.emergencyContactPerson)), fontWeight: FontWeight.w500,
_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,
), ),
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,
),
),
],
),
),
if (isActionable && value != 'NA')
Icon(
Icons.chevron_right,
color: Colors.grey[400],
size: 20,
),
],
),
),
);
}
Widget _buildSectionCard({
required String title,
required IconData titleIcon,
required List<Widget> children,
}) {
return Card(
elevation: 2,
shadowColor: Colors.black12,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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, : 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,14 +211,23 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Row( // Header Section
Card(
elevation: 2,
shadowColor: Colors.black12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [ children: [
Avatar( Avatar(
firstName: employee.firstName, firstName: employee.firstName,
lastName: employee.lastName, lastName: employee.lastName,
size: 45, size: 45,
), ),
MySpacing.width(12), MySpacing.width(16),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -253,11 +245,11 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
), ),
), ),
IconButton( IconButton(
icon: icon: Icon(Icons.edit,
Icon(Icons.edit, size: 24, color: contentTheme.primary), size: 24, color: contentTheme.primary),
onPressed: () async { onPressed: () async {
final result = final result = await showModalBottomSheet<
await showModalBottomSheet<Map<String, dynamic>>( Map<String, dynamic>>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
@ -274,55 +266,164 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
'job_role_id': employee.jobRoleId, 'job_role_id': employee.jobRoleId,
'joining_date': 'joining_date':
employee.joiningDate?.toIso8601String(), employee.joiningDate?.toIso8601String(),
'organization_id': employee.organizationId,
}, },
), ),
); );
if (result != null) { if (result != null) {
controller.fetchEmployeeDetails(widget.employeeId); controller
.fetchEmployeeDetails(widget.employeeId);
} }
}, },
), ),
], ],
), ),
MySpacing.height(14), ),
_buildInfoCard(employee), ),
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(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),
),
);
}),
); );
} }
} }