feat: refactor AddEmployeeBottomSheet and AssignProjectBottomSheet for improved UI and functionality
This commit is contained in:
parent
797df80890
commit
6d29d444fa
@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/dashboard/add_employee_controller.dart';
|
import 'package:marco/controller/dashboard/add_employee_controller.dart';
|
||||||
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
|
import 'package:marco/controller/dashboard/employees_screen_controller.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_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/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
|
|
||||||
class AddEmployeeBottomSheet extends StatefulWidget {
|
class AddEmployeeBottomSheet extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -18,60 +18,101 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
with UIMixin {
|
with UIMixin {
|
||||||
final AddEmployeeController _controller = Get.put(AddEmployeeController());
|
final AddEmployeeController _controller = Get.put(AddEmployeeController());
|
||||||
|
|
||||||
late TextEditingController genderController;
|
|
||||||
late TextEditingController roleController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
Widget build(BuildContext context) {
|
||||||
super.initState();
|
return GetBuilder<AddEmployeeController>(
|
||||||
genderController = TextEditingController();
|
init: _controller,
|
||||||
roleController = TextEditingController();
|
builder: (_) {
|
||||||
|
return BaseBottomSheet(
|
||||||
|
title: "Add Employee",
|
||||||
|
onCancel: () => Navigator.pop(context),
|
||||||
|
onSubmit: _handleSubmit,
|
||||||
|
child: Form(
|
||||||
|
key: _controller.basicValidator.formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_sectionLabel("Personal Info"),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_inputWithIcon(
|
||||||
|
label: "First Name",
|
||||||
|
hint: "e.g., John",
|
||||||
|
icon: Icons.person,
|
||||||
|
controller:
|
||||||
|
_controller.basicValidator.getController('first_name')!,
|
||||||
|
validator:
|
||||||
|
_controller.basicValidator.getValidation('first_name'),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_inputWithIcon(
|
||||||
|
label: "Last Name",
|
||||||
|
hint: "e.g., Doe",
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
controller:
|
||||||
|
_controller.basicValidator.getController('last_name')!,
|
||||||
|
validator:
|
||||||
|
_controller.basicValidator.getValidation('last_name'),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_sectionLabel("Contact Details"),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_buildPhoneInput(context),
|
||||||
|
MySpacing.height(24),
|
||||||
|
_sectionLabel("Other Details"),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_buildDropdownField(
|
||||||
|
label: "Gender",
|
||||||
|
value: _controller.selectedGender?.name.capitalizeFirst ?? '',
|
||||||
|
hint: "Select Gender",
|
||||||
|
onTap: () => _showGenderPopup(context),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_buildDropdownField(
|
||||||
|
label: "Role",
|
||||||
|
value: _controller.roles.firstWhereOrNull((role) =>
|
||||||
|
role['id'] == _controller.selectedRoleId)?['name'] ??
|
||||||
|
"",
|
||||||
|
hint: "Select Role",
|
||||||
|
onTap: () => _showRolePopup(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
RelativeRect _popupMenuPosition(BuildContext context) {
|
// Submit logic
|
||||||
final RenderBox overlay =
|
Future<void> _handleSubmit() async {
|
||||||
Overlay.of(context).context.findRenderObject() as RenderBox;
|
final result = await _controller.createEmployees();
|
||||||
return RelativeRect.fromLTRB(100, 300, overlay.size.width - 100, 0);
|
|
||||||
|
if (result != null && result['success'] == true) {
|
||||||
|
final employeeData = result['data']; // ✅ Safe now
|
||||||
|
final employeeController = Get.find<EmployeesScreenController>();
|
||||||
|
final projectId = employeeController.selectedProjectId;
|
||||||
|
|
||||||
|
if (projectId == null) {
|
||||||
|
await employeeController.fetchAllEmployees();
|
||||||
|
} else {
|
||||||
|
await employeeController.fetchEmployeesByProject(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showGenderPopup(BuildContext context) async {
|
employeeController.update(['employee_screen_controller']);
|
||||||
final selected = await showMenu<Gender>(
|
|
||||||
context: context,
|
|
||||||
position: _popupMenuPosition(context),
|
|
||||||
items: Gender.values.map((gender) {
|
|
||||||
return PopupMenuItem<Gender>(
|
|
||||||
value: gender,
|
|
||||||
child: Text(gender.name.capitalizeFirst!),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selected != null) {
|
_controller.basicValidator.getController("first_name")?.clear();
|
||||||
_controller.onGenderSelected(selected);
|
_controller.basicValidator.getController("last_name")?.clear();
|
||||||
|
_controller.basicValidator.getController("phone_number")?.clear();
|
||||||
|
_controller.selectedGender = null;
|
||||||
|
_controller.selectedRoleId = null;
|
||||||
_controller.update();
|
_controller.update();
|
||||||
|
|
||||||
|
Navigator.pop(context, employeeData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showRolePopup(BuildContext context) async {
|
// Section label widget
|
||||||
final selected = await showMenu<String>(
|
Widget _sectionLabel(String title) => Column(
|
||||||
context: context,
|
|
||||||
position: _popupMenuPosition(context),
|
|
||||||
items: _controller.roles.map((role) {
|
|
||||||
return PopupMenuItem<String>(
|
|
||||||
value: role['id'],
|
|
||||||
child: Text(role['name']),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selected != null) {
|
|
||||||
_controller.onRoleSelected(selected);
|
|
||||||
_controller.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _sectionLabel(String title) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.labelLarge(title, fontWeight: 600),
|
MyText.labelLarge(title, fontWeight: 600),
|
||||||
@ -79,8 +120,8 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
Divider(thickness: 1, color: Colors.grey.shade200),
|
Divider(thickness: 1, color: Colors.grey.shade200),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
// Input field with icon
|
||||||
Widget _inputWithIcon({
|
Widget _inputWithIcon({
|
||||||
required String label,
|
required String label,
|
||||||
required String hint,
|
required String hint,
|
||||||
@ -104,103 +145,12 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDecoration _inputDecoration(String hint) {
|
// Phone input with country code selector
|
||||||
return InputDecoration(
|
Widget _buildPhoneInput(BuildContext context) {
|
||||||
hintText: hint,
|
return Row(
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.grey.shade100,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(color: Colors.blueAccent, width: 1.5),
|
|
||||||
),
|
|
||||||
contentPadding: MySpacing.all(16),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return GetBuilder<AddEmployeeController>(
|
|
||||||
init: _controller,
|
|
||||||
builder: (_) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: MediaQuery.of(context).viewInsets,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.cardColor,
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.vertical(top: Radius.circular(24)),
|
|
||||||
boxShadow: const [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black12,
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: Offset(0, -2))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// Drag Handle
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(12),
|
|
||||||
Text("Add Employee",
|
|
||||||
style: MyTextStyle.titleLarge(fontWeight: 700)),
|
|
||||||
MySpacing.height(24),
|
|
||||||
Form(
|
|
||||||
key: _controller.basicValidator.formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_sectionLabel("Personal Info"),
|
|
||||||
MySpacing.height(16),
|
|
||||||
_inputWithIcon(
|
|
||||||
label: "First Name",
|
|
||||||
hint: "e.g., John",
|
|
||||||
icon: Icons.person,
|
|
||||||
controller: _controller.basicValidator
|
|
||||||
.getController('first_name')!,
|
|
||||||
validator: _controller.basicValidator
|
|
||||||
.getValidation('first_name'),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
_inputWithIcon(
|
|
||||||
label: "Last Name",
|
|
||||||
hint: "e.g., Doe",
|
|
||||||
icon: Icons.person_outline,
|
|
||||||
controller: _controller.basicValidator
|
|
||||||
.getController('last_name')!,
|
|
||||||
validator: _controller.basicValidator
|
|
||||||
.getValidation('last_name'),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
_sectionLabel("Contact Details"),
|
|
||||||
MySpacing.height(16),
|
|
||||||
MyText.labelMedium("Phone Number"),
|
|
||||||
MySpacing.height(8),
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||||
horizontal: 12, vertical: 14),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@ -208,8 +158,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
),
|
),
|
||||||
child: PopupMenuButton<Map<String, String>>(
|
child: PopupMenuButton<Map<String, String>>(
|
||||||
onSelected: (country) {
|
onSelected: (country) {
|
||||||
_controller.selectedCountryCode =
|
_controller.selectedCountryCode = country['code']!;
|
||||||
country['code']!;
|
|
||||||
_controller.update();
|
_controller.update();
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
@ -220,14 +169,11 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
height: 200,
|
height: 200,
|
||||||
width: 100,
|
width: 100,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: _controller.countries
|
children: _controller.countries.map((country) {
|
||||||
.map((country) {
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(
|
title: Text("${country['name']} (${country['code']})"),
|
||||||
"${country['name']} (${country['code']})"),
|
onTap: () => Navigator.pop(context, country),
|
||||||
onTap: () =>
|
|
||||||
Navigator.pop(context, country),
|
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
@ -245,8 +191,8 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
MySpacing.width(12),
|
MySpacing.width(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _controller.basicValidator
|
controller:
|
||||||
.getController('phone_number'),
|
_controller.basicValidator.getController('phone_number'),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return "Phone number is required";
|
return "Phone number is required";
|
||||||
@ -254,16 +200,13 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
|
|
||||||
final digitsOnly = value.trim();
|
final digitsOnly = value.trim();
|
||||||
final minLength = _controller
|
final minLength = _controller
|
||||||
.minDigitsPerCountry[
|
.minDigitsPerCountry[_controller.selectedCountryCode] ??
|
||||||
_controller.selectedCountryCode] ??
|
|
||||||
7;
|
7;
|
||||||
final maxLength = _controller
|
final maxLength = _controller
|
||||||
.maxDigitsPerCountry[
|
.maxDigitsPerCountry[_controller.selectedCountryCode] ??
|
||||||
_controller.selectedCountryCode] ??
|
|
||||||
15;
|
15;
|
||||||
|
|
||||||
if (!RegExp(r'^[0-9]+$')
|
if (!RegExp(r'^[0-9]+$').hasMatch(digitsOnly)) {
|
||||||
.hasMatch(digitsOnly)) {
|
|
||||||
return "Only digits allowed";
|
return "Only digits allowed";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,155 +219,113 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
|||||||
},
|
},
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
// Allow only digits
|
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
// Limit to 10 digits
|
LengthLimitingTextInputFormatter(15),
|
||||||
LengthLimitingTextInputFormatter(10),
|
|
||||||
],
|
],
|
||||||
decoration: _inputDecoration("e.g., 9876543210")
|
decoration: _inputDecoration("e.g., 9876543210").copyWith(
|
||||||
.copyWith(
|
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: const Icon(Icons.contacts),
|
icon: const Icon(Icons.contacts),
|
||||||
onPressed: () =>
|
onPressed: () => _controller.pickContact(context),
|
||||||
_controller.pickContact(context),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
MySpacing.height(24),
|
}
|
||||||
_sectionLabel("Other Details"),
|
|
||||||
MySpacing.height(16),
|
// Gender/Role field (read-only dropdown)
|
||||||
MyText.labelMedium("Gender"),
|
Widget _buildDropdownField({
|
||||||
MySpacing.height(8),
|
required String label,
|
||||||
GestureDetector(
|
required String value,
|
||||||
onTap: () => _showGenderPopup(context),
|
required String hint,
|
||||||
child: AbsorbPointer(
|
required VoidCallback onTap,
|
||||||
child: TextFormField(
|
}) {
|
||||||
readOnly: true,
|
return Column(
|
||||||
controller: TextEditingController(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
text: _controller
|
|
||||||
.selectedGender?.name.capitalizeFirst,
|
|
||||||
),
|
|
||||||
decoration:
|
|
||||||
_inputDecoration("Select Gender").copyWith(
|
|
||||||
suffixIcon: const Icon(Icons.expand_more),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
MyText.labelMedium("Role"),
|
|
||||||
MySpacing.height(8),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _showRolePopup(context),
|
|
||||||
child: AbsorbPointer(
|
|
||||||
child: TextFormField(
|
|
||||||
readOnly: true,
|
|
||||||
controller: TextEditingController(
|
|
||||||
text: _controller.roles.firstWhereOrNull(
|
|
||||||
(role) =>
|
|
||||||
role['id'] ==
|
|
||||||
_controller.selectedRoleId,
|
|
||||||
)?['name'] ??
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
decoration:
|
|
||||||
_inputDecoration("Select Role").copyWith(
|
|
||||||
suffixIcon: const Icon(Icons.expand_more),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
MyText.labelMedium(label),
|
||||||
child: OutlinedButton.icon(
|
MySpacing.height(8),
|
||||||
onPressed: () => Navigator.pop(context),
|
GestureDetector(
|
||||||
icon:
|
onTap: onTap,
|
||||||
const Icon(Icons.close, color: Colors.red),
|
child: AbsorbPointer(
|
||||||
label: MyText.bodyMedium("Cancel",
|
child: TextFormField(
|
||||||
color: Colors.red, fontWeight: 600),
|
readOnly: true,
|
||||||
style: OutlinedButton.styleFrom(
|
controller: TextEditingController(text: value),
|
||||||
side: const BorderSide(color: Colors.red),
|
decoration: _inputDecoration(hint).copyWith(
|
||||||
shape: RoundedRectangleBorder(
|
suffixIcon: const Icon(Icons.expand_more),
|
||||||
borderRadius: BorderRadius.circular(12)),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20, vertical: 14),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
),
|
||||||
Expanded(
|
],
|
||||||
child: ElevatedButton.icon(
|
);
|
||||||
onPressed: () async {
|
|
||||||
if (_controller.basicValidator
|
|
||||||
.validateForm()) {
|
|
||||||
final result =
|
|
||||||
await _controller.createEmployees();
|
|
||||||
|
|
||||||
if (result != null &&
|
|
||||||
result['success'] == true) {
|
|
||||||
final employeeData = result['data'];
|
|
||||||
final employeeController =
|
|
||||||
Get.find<EmployeesScreenController>();
|
|
||||||
final projectId =
|
|
||||||
employeeController.selectedProjectId;
|
|
||||||
|
|
||||||
if (projectId == null) {
|
|
||||||
await employeeController
|
|
||||||
.fetchAllEmployees();
|
|
||||||
} else {
|
|
||||||
await employeeController
|
|
||||||
.fetchEmployeesByProject(projectId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
employeeController.update(
|
// Common input decoration
|
||||||
['employee_screen_controller']);
|
InputDecoration _inputDecoration(String hint) {
|
||||||
|
return InputDecoration(
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
|
||||||
|
),
|
||||||
|
contentPadding: MySpacing.all(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_controller.basicValidator
|
// Gender popup menu
|
||||||
.getController("first_name")
|
void _showGenderPopup(BuildContext context) async {
|
||||||
?.clear();
|
final selected = await showMenu<Gender>(
|
||||||
_controller.basicValidator
|
context: context,
|
||||||
.getController("last_name")
|
position: _popupMenuPosition(context),
|
||||||
?.clear();
|
items: Gender.values.map((gender) {
|
||||||
_controller.basicValidator
|
return PopupMenuItem<Gender>(
|
||||||
.getController("phone_number")
|
value: gender,
|
||||||
?.clear();
|
child: Text(gender.name.capitalizeFirst!),
|
||||||
_controller.selectedGender = null;
|
);
|
||||||
_controller.selectedRoleId = null;
|
}).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selected != null) {
|
||||||
|
_controller.onGenderSelected(selected);
|
||||||
_controller.update();
|
_controller.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Navigator.pop(context, employeeData);
|
// Role popup menu
|
||||||
|
void _showRolePopup(BuildContext context) async {
|
||||||
|
final selected = await showMenu<String>(
|
||||||
|
context: context,
|
||||||
|
position: _popupMenuPosition(context),
|
||||||
|
items: _controller.roles.map((role) {
|
||||||
|
return PopupMenuItem<String>(
|
||||||
|
value: role['id'],
|
||||||
|
child: Text(role['name']),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selected != null) {
|
||||||
|
_controller.onRoleSelected(selected);
|
||||||
|
_controller.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
icon: const Icon(Icons.check_circle_outline,
|
RelativeRect _popupMenuPosition(BuildContext context) {
|
||||||
color: Colors.white),
|
final RenderBox overlay =
|
||||||
label: MyText.bodyMedium("Save",
|
Overlay.of(context).context.findRenderObject() as RenderBox;
|
||||||
color: Colors.white, fontWeight: 600),
|
return RelativeRect.fromLTRB(100, 300, overlay.size.width - 100, 0);
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.indigo,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12)),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 28, vertical: 14),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.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/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/controller/employee/assign_projects_controller.dart';
|
import 'package:marco/controller/employee/assign_projects_controller.dart';
|
||||||
import 'package:marco/model/global_project_model.dart';
|
import 'package:marco/model/global_project_model.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
|
|
||||||
class AssignProjectBottomSheet extends StatefulWidget {
|
class AssignProjectBottomSheet extends StatefulWidget {
|
||||||
final String employeeId;
|
final String employeeId;
|
||||||
@ -23,6 +24,7 @@ class AssignProjectBottomSheet extends StatefulWidget {
|
|||||||
|
|
||||||
class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
|
class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
|
||||||
late final AssignProjectController assignController;
|
late final AssignProjectController assignController;
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -38,30 +40,14 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
return GetBuilder<AssignProjectController>(
|
||||||
|
tag: '${widget.employeeId}_${widget.jobRoleId}',
|
||||||
return SafeArea(
|
builder: (_) {
|
||||||
top: false,
|
return BaseBottomSheet(
|
||||||
child: DraggableScrollableSheet(
|
title: "Assign to Project",
|
||||||
expand: false,
|
onCancel: () => Navigator.pop(context),
|
||||||
maxChildSize: 0.9,
|
onSubmit: _handleAssign,
|
||||||
minChildSize: 0.4,
|
submitText: "Assign",
|
||||||
initialChildSize: 0.7,
|
|
||||||
builder: (_, scrollController) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.cardColor,
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.vertical(top: Radius.circular(24)),
|
|
||||||
boxShadow: const [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black12,
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: Offset(0, -2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
padding: MySpacing.all(16),
|
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
if (assignController.isLoading.value) {
|
if (assignController.isLoading.value) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@ -75,36 +61,13 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Drag Handle
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(12),
|
|
||||||
|
|
||||||
// Header
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
MyText.titleMedium('Assign to Project', fontWeight: 700),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(4),
|
|
||||||
|
|
||||||
// Sub Info
|
|
||||||
MyText.bodySmall(
|
MyText.bodySmall(
|
||||||
'Select the projects to assign this employee.',
|
'Select the projects to assign this employee.',
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
|
|
||||||
// Select All Toggle
|
// Select All
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -134,12 +97,12 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// Project List
|
// List of Projects
|
||||||
Expanded(
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: scrollController,
|
controller: _scrollController,
|
||||||
itemCount: projects.length,
|
itemCount: projects.length,
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final GlobalProjectModel project = projects[index];
|
final GlobalProjectModel project = projects[index];
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
@ -150,14 +113,10 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
|
|||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context).copyWith(
|
data: Theme.of(context).copyWith(
|
||||||
checkboxTheme: CheckboxThemeData(
|
checkboxTheme: CheckboxThemeData(
|
||||||
fillColor:
|
fillColor: WidgetStateProperty.resolveWith<Color>(
|
||||||
WidgetStateProperty.resolveWith<Color>(
|
(states) => states.contains(WidgetState.selected)
|
||||||
(states) {
|
? Colors.blueAccent
|
||||||
if (states.contains(WidgetState.selected)) {
|
: Colors.white,
|
||||||
return Colors.blueAccent;
|
|
||||||
}
|
|
||||||
return Colors.white;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
side: const BorderSide(
|
side: const BorderSide(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
@ -196,33 +155,15 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MySpacing.height(16),
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel & Save Buttons
|
Future<void> _handleAssign() async {
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
icon: const Icon(Icons.close, color: Colors.red),
|
|
||||||
label: MyText.bodyMedium("Cancel",
|
|
||||||
color: Colors.red, fontWeight: 600),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
side: const BorderSide(color: Colors.red),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 7,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
if (assignController.selectedProjects.isEmpty) {
|
if (assignController.selectedProjects.isEmpty) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@ -231,36 +172,7 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await _assignProjects();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.check_circle_outline,
|
|
||||||
color: Colors.white),
|
|
||||||
label: MyText.bodyMedium("Assign",
|
|
||||||
color: Colors.white, fontWeight: 600),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.indigo,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 7,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _assignProjects() async {
|
|
||||||
final success = await assignController.assignProjectsToEmployee();
|
final success = await assignController.assignProjectsToEmployee();
|
||||||
if (success) {
|
if (success) {
|
||||||
Get.back();
|
Get.back();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user