marco.pms.mobileapp/lib/model/employees/add_employee_bottom_sheet.dart

421 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/dashboard/add_employee_controller.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
class AddEmployeeBottomSheet extends StatefulWidget {
@override
State<AddEmployeeBottomSheet> createState() => _AddEmployeeBottomSheetState();
}
class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
with UIMixin {
final AddEmployeeController _controller = Get.put(AddEmployeeController());
late TextEditingController genderController;
late TextEditingController roleController;
@override
void initState() {
super.initState();
genderController = TextEditingController();
roleController = TextEditingController();
}
RelativeRect _popupMenuPosition(BuildContext context) {
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
return RelativeRect.fromLTRB(100, 300, overlay.size.width - 100, 0);
}
void _showGenderPopup(BuildContext context) async {
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.onGenderSelected(selected);
_controller.update();
}
}
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();
}
}
Widget _sectionLabel(String title) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelLarge(title, fontWeight: 600),
MySpacing.height(4),
Divider(thickness: 1, color: Colors.grey.shade200),
],
);
}
Widget _inputWithIcon({
required String label,
required String hint,
required IconData icon,
required TextEditingController controller,
required String? Function(String?)? validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium(label),
MySpacing.height(8),
TextFormField(
controller: controller,
validator: validator,
decoration: _inputDecoration(hint).copyWith(
prefixIcon: Icon(icon, size: 20),
),
),
],
);
}
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: 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: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 14),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
color: Colors.grey.shade100,
),
child: PopupMenuButton<Map<String, String>>(
onSelected: (country) {
_controller.selectedCountryCode =
country['code']!;
_controller.update();
},
itemBuilder: (context) => [
PopupMenuItem(
enabled: false,
padding: EdgeInsets.zero,
child: SizedBox(
height: 200,
width: 100,
child: ListView(
children: _controller.countries
.map((country) {
return ListTile(
dense: true,
title: Text(
"${country['name']} (${country['code']})"),
onTap: () =>
Navigator.pop(context, country),
);
}).toList(),
),
),
),
],
child: Row(
children: [
Text(_controller.selectedCountryCode),
const Icon(Icons.arrow_drop_down),
],
),
),
),
MySpacing.width(12),
Expanded(
child: TextFormField(
controller: _controller.basicValidator
.getController('phone_number'),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Phone number is required";
}
final digitsOnly = value.trim();
final minLength = _controller
.minDigitsPerCountry[
_controller.selectedCountryCode] ??
7;
final maxLength = _controller
.maxDigitsPerCountry[
_controller.selectedCountryCode] ??
15;
if (!RegExp(r'^[0-9]+$')
.hasMatch(digitsOnly)) {
return "Only digits allowed";
}
if (digitsOnly.length < minLength ||
digitsOnly.length > maxLength) {
return "Between $minLength$maxLength digits";
}
return null;
},
keyboardType: TextInputType.phone,
decoration: _inputDecoration("e.g., 9876543210")
.copyWith(
suffixIcon: IconButton(
icon: const Icon(Icons.contacts),
onPressed: () =>
_controller.pickContact(context),
),
),
),
),
],
),
MySpacing.height(24),
_sectionLabel("Other Details"),
MySpacing.height(16),
MyText.labelMedium("Gender"),
MySpacing.height(8),
GestureDetector(
onTap: () => _showGenderPopup(context),
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: TextEditingController(
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: [
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: 20, vertical: 14),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
if (_controller.basicValidator
.validateForm()) {
final success =
await _controller.createEmployees();
if (success) {
final employeeController =
Get.find<EmployeesScreenController>();
final projectId =
employeeController.selectedProjectId;
if (projectId == null) {
await employeeController
.fetchAllEmployees();
} else {
await employeeController
.fetchEmployeesByProject(projectId);
}
employeeController.update(
['employee_screen_controller']);
_controller.basicValidator
.getController("first_name")
?.clear();
_controller.basicValidator
.getController("last_name")
?.clear();
_controller.basicValidator
.getController("phone_number")
?.clear();
_controller.selectedGender = null;
_controller.selectedRoleId = null;
_controller.update();
Navigator.pop(context);
}
}
},
icon: const Icon(Icons.check_circle_outline,
color: Colors.white),
label: MyText.bodyMedium("Save",
color: Colors.white, fontWeight: 600),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(
horizontal: 28, vertical: 14),
),
),
),
],
),
],
),
),
],
),
),
),
);
},
);
}
}