marco.pms.mobileapp/lib/view/employees/employee_detail_screen.dart
2025-11-17 15:23:51 +05:30

553 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/manage_reporting_bottom_sheet.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/view/employees/assign_employee_bottom_sheet.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 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);
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,
'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.toString(),
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,
),
);
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.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 ------------------
_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 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(', ');
}
}