made chnages in employee details screen
This commit is contained in:
parent
910bb5e6b4
commit
eb46194679
@ -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(
|
||||
|
||||
@ -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;
|
||||
@ -32,8 +29,6 @@ class EmployeeDetailPage extends StatefulWidget {
|
||||
class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
||||
final EmployeesScreenController controller =
|
||||
Get.put(EmployeesScreenController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -65,124 +60,112 @@ 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(
|
||||
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: [
|
||||
if (isMultiLine) ...[
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
fontSize: 14,
|
||||
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,
|
||||
),
|
||||
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),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
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,
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
_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,
|
||||
body: Obx(() {
|
||||
if (controller.isLoadingEmployeeDetails.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return SkeletonLoaders.employeeDetailSkeletonLoader();
|
||||
}
|
||||
|
||||
final employee = controller.selectedEmployeeDetails.value;
|
||||
@ -228,14 +211,23 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
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: [
|
||||
Avatar(
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
size: 45,
|
||||
),
|
||||
MySpacing.width(12),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -253,11 +245,11 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon:
|
||||
Icon(Icons.edit, size: 24, color: contentTheme.primary),
|
||||
icon: Icon(Icons.edit,
|
||||
size: 24, color: contentTheme.primary),
|
||||
onPressed: () async {
|
||||
final result =
|
||||
await showModalBottomSheet<Map<String, dynamic>>(
|
||||
final result = await showModalBottomSheet<
|
||||
Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
@ -274,55 +266,164 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
||||
'job_role_id': employee.jobRoleId,
|
||||
'joining_date':
|
||||
employee.joiningDate?.toIso8601String(),
|
||||
'organization_id': employee.organizationId,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
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),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user