diff --git a/lib/helpers/widgets/my_custom_skeleton.dart b/lib/helpers/widgets/my_custom_skeleton.dart index 2f04de7..1fbecf7 100644 --- a/lib/helpers/widgets/my_custom_skeleton.dart +++ b/lib/helpers/widgets/my_custom_skeleton.dart @@ -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( diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 3c66f6c..7e72066 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -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; @@ -29,11 +26,9 @@ class EmployeeDetailPage extends StatefulWidget { State createState() => _EmployeeDetailPageState(); } -class _EmployeeDetailPageState extends State with UIMixin { +class _EmployeeDetailPageState extends State with UIMixin { final EmployeesScreenController controller = Get.put(EmployeesScreenController()); - final PermissionController permissionController = - Get.put(PermissionController()); @override void initState() { @@ -65,77 +60,58 @@ class _EmployeeDetailPageState extends State 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( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isMultiLine) ...[ - Text( - label, - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black87, - fontSize: 14, + 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), + 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, - ), + MySpacing.width(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan( - text: value, + Text( + label, style: TextStyle( - fontWeight: FontWeight.normal, - color: - (isEmail || isPhone) ? Colors.indigo : Colors.black54, - decoration: (isEmail || isPhone) + fontSize: 12, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + 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, ), @@ -143,46 +119,53 @@ class _EmployeeDetailPageState extends State with UIMixin { ], ), ), - ), - MySpacing.height(10), - Divider(color: Colors.grey[300], height: 1), - MySpacing.height(10), - ], + if (isActionable && value != 'NA') + Icon( + Icons.chevron_right, + color: Colors.grey[400], + size: 20, + ), + ], + ), + ), ); } - Widget _buildInfoCard(employee) { + Widget _buildSectionCard({ + required String title, + required IconData titleIcon, + required List children, + }) { return Card( - elevation: 3, + elevation: 2, shadowColor: Colors.black12, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), child: Padding( - padding: const EdgeInsets.fromLTRB(12, 16, 12, 16), + padding: const EdgeInsets.all(16), 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, - ), - _buildLabelValueRow( - 'Permanent Address', - _getDisplayValue(employee.permanentAddress), - isMultiLine: true, + 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 with UIMixin { : null, body: Obx(() { if (controller.isLoadingEmployeeDetails.value) { - return const Center(child: CircularProgressIndicator()); + return SkeletonLoaders.employeeDetailSkeletonLoader(); } final employee = controller.selectedEmployeeDetails.value; @@ -228,101 +211,219 @@ class _EmployeeDetailPageState extends State with UIMixin { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( - children: [ - Avatar( - firstName: employee.firstName, - lastName: employee.lastName, - size: 45, - ), - MySpacing.width(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleMedium( - '${employee.firstName} ${employee.lastName}', - fontWeight: 700, + // 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(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( - _getDisplayValue(employee.jobRole), - fontWeight: 500, - ), - ], - ), - ), - IconButton( - icon: - Icon(Icons.edit, size: 24, color: contentTheme.primary), - onPressed: () async { - final result = - await showModalBottomSheet>( - 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(), - 'organization_id': employee.organizationId, - }, - ), - ); + ), + IconButton( + icon: Icon(Icons.edit, + size: 24, color: contentTheme.primary), + onPressed: () async { + final result = await showModalBottomSheet< + Map>( + 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); + if (result != null) { + 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), - _buildInfoCard(employee), + 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), - ), - ); - }), ); } }