546 lines
21 KiB
Dart
546 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:marco/controller/employee/employees_screen_controller.dart';
|
|
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/helpers/utils/launcher_utils.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';
|
|
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/model/employees/employee_details_model.dart';
|
|
|
|
class EmployeeDetailPage extends StatefulWidget {
|
|
final String employeeId;
|
|
final bool fromProfile;
|
|
|
|
const EmployeeDetailPage({
|
|
super.key,
|
|
required this.employeeId,
|
|
this.fromProfile = false,
|
|
});
|
|
|
|
@override
|
|
State<EmployeeDetailPage> createState() => _EmployeeDetailPageState();
|
|
}
|
|
|
|
class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
|
|
final EmployeesScreenController controller =
|
|
Get.put(EmployeesScreenController());
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
controller.fetchEmployeeDetails(widget.employeeId);
|
|
controller.fetchReportingManagers(widget.employeeId);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
controller.selectedEmployeeDetails.value = null;
|
|
super.dispose();
|
|
}
|
|
|
|
String _getDisplayValue(dynamic value) {
|
|
if (value == null || value.toString().trim().isEmpty || value == "null") {
|
|
return "NA";
|
|
}
|
|
return value.toString();
|
|
}
|
|
|
|
String _formatDate(DateTime? date) {
|
|
if (date == null || date == DateTime(1)) return "NA";
|
|
try {
|
|
return DateFormat('d/M/yyyy').format(date);
|
|
} catch (_) {
|
|
return "NA";
|
|
}
|
|
}
|
|
|
|
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),
|
|
child: Icon(icon, size: 20),
|
|
),
|
|
MySpacing.width(16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MyText(
|
|
label,
|
|
color: Colors.grey[600],
|
|
fontWeight: 500,
|
|
),
|
|
MySpacing.height(4),
|
|
MyText(
|
|
value,
|
|
color: isActionable && value != "NA"
|
|
? Colors.blueAccent
|
|
: Colors.black87,
|
|
fontWeight: 500,
|
|
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),
|
|
MySpacing.width(8),
|
|
MyText(
|
|
title,
|
|
fontSize: 16,
|
|
fontWeight: 700,
|
|
color: Colors.black87,
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
const Divider(),
|
|
...children,
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final bool showAppBar = !widget.fromProfile;
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF1F1F1),
|
|
appBar: showAppBar
|
|
? CustomAppBar(
|
|
title: "Employee Details",
|
|
onBackPressed: () {
|
|
if (widget.fromProfile) {
|
|
Get.back();
|
|
} else {
|
|
Get.offNamed('/dashboard/employees');
|
|
}
|
|
},
|
|
)
|
|
: null,
|
|
body: Obx(() {
|
|
if (controller.isLoadingEmployeeDetails.value) {
|
|
return SkeletonLoaders.employeeDetailSkeletonLoader();
|
|
}
|
|
|
|
final EmployeeDetailsModel? employee =
|
|
controller.selectedEmployeeDetails.value;
|
|
if (employee == null) {
|
|
return Center(child: MyText("No employee details found."));
|
|
}
|
|
|
|
return SafeArea(
|
|
child: MyRefreshIndicator(
|
|
onRefresh: () async {
|
|
await controller.fetchEmployeeDetails(widget.employeeId);
|
|
if (employee.id != null) {
|
|
await controller.fetchReportingManagers(employee.id!);
|
|
}
|
|
},
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.fromLTRB(12, 20, 12, 80),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
/// ------------------ HEADER CARD ------------------
|
|
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: 35,
|
|
),
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
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 ?? false,
|
|
'gender': employee.gender?.toLowerCase() ?? '',
|
|
'job_role_id': employee.jobRoleId,
|
|
'joining_date':
|
|
employee.joiningDate?.toIso8601String(),
|
|
},
|
|
),
|
|
);
|
|
|
|
if (result != null) {
|
|
controller
|
|
.fetchEmployeeDetails(widget.employeeId);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
MySpacing.height(16),
|
|
|
|
/// ------------------ MANAGE REPORTING ------------------
|
|
_buildSectionCard(
|
|
title: 'Manage Reporting',
|
|
titleIcon: Icons.people_outline,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () async {
|
|
await showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (_) => ManageReportingBottomSheet(
|
|
initialEmployee: EmployeeModel(
|
|
id: employee.id ?? '',
|
|
employeeId: employee.id ?? '',
|
|
firstName: employee.firstName ?? '',
|
|
lastName: employee.lastName ?? '',
|
|
name:
|
|
"${employee.firstName ?? ''} ${employee.lastName ?? ''}",
|
|
email: employee.email ?? '',
|
|
jobRole: employee.jobRole ?? '',
|
|
jobRoleID: "0",
|
|
designation: employee.jobRole ?? '',
|
|
phoneNumber: employee.phoneNumber ?? '',
|
|
activity: 0,
|
|
action: 0,
|
|
),
|
|
hideMainSelector: true,
|
|
hideLoggedUserFromSelection: true,
|
|
loggedUserId: controller.selectedEmployeeDetails.value?.id,
|
|
),
|
|
);
|
|
|
|
if (employee.id != null) {
|
|
await controller.fetchReportingManagers(employee.id!);
|
|
}
|
|
},
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.manage_accounts_outlined,
|
|
color: Colors.grey),
|
|
const SizedBox(width: 16),
|
|
const Expanded(
|
|
child: Text(
|
|
'View / Update Reporting Managers',
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
const Icon(Icons.arrow_forward_ios_rounded,
|
|
size: 16, color: Colors.grey),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Obx(() {
|
|
final primary =
|
|
controller.selectedEmployeePrimaryManagers;
|
|
final secondary =
|
|
controller.selectedEmployeeSecondaryManagers;
|
|
|
|
if (primary.isEmpty && secondary.isEmpty) {
|
|
return const Padding(
|
|
padding: EdgeInsets.only(top: 8.0),
|
|
child: Text(
|
|
'No reporting managers assigned',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey,
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Primary → ${_getManagerNames(primary)}',
|
|
style: const TextStyle(
|
|
fontSize: 14, fontWeight: FontWeight.w600),
|
|
),
|
|
Text(
|
|
'Secondary → ${_getManagerNames(secondary)}',
|
|
style: const TextStyle(
|
|
fontSize: 14, fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
|
|
MySpacing.height(16),
|
|
|
|
/// ------------------ CONTACT INFO ------------------
|
|
_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!.trim().isNotEmpty) {
|
|
LauncherUtils.launchEmail(employee.email!);
|
|
}
|
|
},
|
|
onLongPress: () {
|
|
if (employee.email != null &&
|
|
employee.email!.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 != null &&
|
|
employee.phoneNumber!.trim().isNotEmpty) {
|
|
LauncherUtils.launchPhone(employee.phoneNumber!);
|
|
}
|
|
},
|
|
onLongPress: () {
|
|
if (employee.phoneNumber != null &&
|
|
employee.phoneNumber!.trim().isNotEmpty) {
|
|
LauncherUtils.copyToClipboard(
|
|
employee.phoneNumber!, typeLabel: 'Phone Number');
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
|
|
MySpacing.height(16),
|
|
|
|
/// ------------------ EMERGENCY CONTACT ------------------
|
|
_buildSectionCard(
|
|
title: 'Emergency Contact',
|
|
titleIcon: Icons.emergency,
|
|
children: [
|
|
_buildDetailRow(
|
|
icon: Icons.person_outline,
|
|
label: 'Contact Person',
|
|
value: _getDisplayValue(employee.emergencyContactPerson),
|
|
),
|
|
_buildDetailRow(
|
|
icon: Icons.phone_in_talk_outlined,
|
|
label: 'Emergency Phone',
|
|
value: _getDisplayValue(employee.emergencyPhoneNumber),
|
|
isActionable: true,
|
|
onTap: () {
|
|
if (employee.emergencyPhoneNumber != null &&
|
|
employee.emergencyPhoneNumber!.trim().isNotEmpty) {
|
|
LauncherUtils.launchPhone(employee.emergencyPhoneNumber!);
|
|
}
|
|
},
|
|
onLongPress: () {
|
|
if (employee.emergencyPhoneNumber != null &&
|
|
employee.emergencyPhoneNumber!.trim().isNotEmpty) {
|
|
LauncherUtils.copyToClipboard(
|
|
employee.emergencyPhoneNumber!,
|
|
typeLabel: 'Emergency Phone');
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
|
|
MySpacing.height(16),
|
|
|
|
/// ------------------ PERSONAL INFO ------------------
|
|
_buildSectionCard(
|
|
title: 'Personal Information',
|
|
titleIcon: Icons.person,
|
|
children: [
|
|
_buildDetailRow(
|
|
icon: Icons.wc_outlined,
|
|
label: 'Gender',
|
|
value: _getDisplayValue(employee.gender),
|
|
),
|
|
_buildDetailRow(
|
|
icon: Icons.cake_outlined,
|
|
label: 'Birth Date',
|
|
value: _formatDate(employee.birthDate),
|
|
),
|
|
_buildDetailRow(
|
|
icon: Icons.work_outline,
|
|
label: 'Joining Date',
|
|
value: _formatDate(employee.joiningDate),
|
|
),
|
|
],
|
|
),
|
|
|
|
MySpacing.height(16),
|
|
|
|
/// ------------------ ADDRESS INFO ------------------
|
|
_buildSectionCard(
|
|
title: 'Address Information',
|
|
titleIcon: Icons.location_on,
|
|
children: [
|
|
_buildDetailRow(
|
|
icon: Icons.home_outlined,
|
|
label: 'Current Address',
|
|
value: _getDisplayValue(employee.currentAddress),
|
|
),
|
|
_buildDetailRow(
|
|
icon: Icons.home_work_outlined,
|
|
label: 'Permanent Address',
|
|
value: _getDisplayValue(employee.permanentAddress),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
|
|
/// ------------------ FLOATING BUTTON ------------------
|
|
floatingActionButton: Obx(() {
|
|
final EmployeeDetailsModel? employee = controller.selectedEmployeeDetails.value;
|
|
if (employee == null) return const SizedBox.shrink();
|
|
|
|
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.add),
|
|
label: MyText(
|
|
'Assign to Project',
|
|
fontSize: 14,
|
|
fontWeight: 500,
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
/// ------------------ UTIL ------------------
|
|
String _getManagerNames(List<EmployeeModel> managers) {
|
|
if (managers.isEmpty) return '—';
|
|
return managers
|
|
.map((m) =>
|
|
'${(m.firstName ?? '').trim()} ${(m.lastName ?? '').trim()}'.trim())
|
|
.where((name) => name.isNotEmpty)
|
|
.join(', ');
|
|
}
|
|
}
|