feat: Implement employee editing functionality; add prefill logic and update API service for createOrUpdateEmployee

This commit is contained in:
Vaibhav Surve 2025-09-18 17:42:27 +05:30
parent 544eb4dc79
commit 4836dd994c
4 changed files with 114 additions and 77 deletions

View File

@ -7,6 +7,7 @@ import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:collection/collection.dart';
enum Gender { enum Gender {
male, male,
@ -17,6 +18,8 @@ enum Gender {
} }
class AddEmployeeController extends MyController { class AddEmployeeController extends MyController {
Map<String, dynamic>? editingEmployeeData; // For edit mode
List<PlatformFile> files = []; List<PlatformFile> files = [];
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();
Gender? selectedGender; Gender? selectedGender;
@ -33,12 +36,10 @@ class AddEmployeeController extends MyController {
logSafe("Initializing AddEmployeeController..."); logSafe("Initializing AddEmployeeController...");
_initializeFields(); _initializeFields();
fetchRoles(); fetchRoles();
}
void setJoiningDate(DateTime date) { if (editingEmployeeData != null) {
joiningDate = date; prefillFields();
logSafe("Joining date selected: $date"); }
update();
} }
void _initializeFields() { void _initializeFields() {
@ -63,6 +64,37 @@ class AddEmployeeController extends MyController {
logSafe("Fields initialized for first_name, phone_number, last_name."); logSafe("Fields initialized for first_name, phone_number, last_name.");
} }
/// Prefill fields in edit mode
// In AddEmployeeController
void prefillFields() {
logSafe("Prefilling data for editing...");
basicValidator.getController('first_name')?.text =
editingEmployeeData?['first_name'] ?? '';
basicValidator.getController('last_name')?.text =
editingEmployeeData?['last_name'] ?? '';
basicValidator.getController('phone_number')?.text =
editingEmployeeData?['phone_number'] ?? '';
selectedGender = editingEmployeeData?['gender'] != null
? Gender.values
.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
: null;
selectedRoleId = editingEmployeeData?['job_role_id'];
if (editingEmployeeData?['joining_date'] != null) {
joiningDate = DateTime.tryParse(editingEmployeeData!['joining_date']);
}
update();
}
void setJoiningDate(DateTime date) {
joiningDate = date;
logSafe("Joining date selected: $date");
update();
}
void onGenderSelected(Gender? gender) { void onGenderSelected(Gender? gender) {
selectedGender = gender; selectedGender = gender;
logSafe("Gender selected: ${gender?.name}"); logSafe("Gender selected: ${gender?.name}");
@ -92,10 +124,13 @@ class AddEmployeeController extends MyController {
update(); update();
} }
Future<Map<String, dynamic>?> createEmployees() async { /// Create or update employee
logSafe("Starting employee creation..."); Future<Map<String, dynamic>?> createOrUpdateEmployee() async {
logSafe(editingEmployeeData != null
? "Starting employee update..."
: "Starting employee creation...");
if (selectedGender == null || selectedRoleId == null) { if (selectedGender == null || selectedRoleId == null) {
logSafe("Missing gender or role.", level: LogLevel.warning);
showAppSnackbar( showAppSnackbar(
title: "Missing Fields", title: "Missing Fields",
message: "Please select both Gender and Role.", message: "Please select both Gender and Role.",
@ -111,6 +146,7 @@ class AddEmployeeController extends MyController {
try { try {
final response = await ApiService.createEmployee( final response = await ApiService.createEmployee(
id: editingEmployeeData?['id'], // Pass id if editing
firstName: firstName!, firstName: firstName!,
lastName: lastName!, lastName: lastName!,
phoneNumber: phoneNumber!, phoneNumber: phoneNumber!,
@ -122,25 +158,25 @@ class AddEmployeeController extends MyController {
logSafe("Response: $response"); logSafe("Response: $response");
if (response != null && response['success'] == true) { if (response != null && response['success'] == true) {
logSafe("Employee created successfully.");
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "Employee created successfully!", message: editingEmployeeData != null
? "Employee updated successfully!"
: "Employee created successfully!",
type: SnackbarType.success, type: SnackbarType.success,
); );
return response; return response;
} else { } else {
logSafe("Failed to create employee (response false)", logSafe("Failed operation", level: LogLevel.error);
level: LogLevel.error);
} }
} catch (e, st) { } catch (e, st) {
logSafe("Error creating employee", logSafe("Error creating/updating employee",
level: LogLevel.error, error: e, stackTrace: st); level: LogLevel.error, error: e, stackTrace: st);
} }
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to create employee.", message: "Failed to save employee.",
type: SnackbarType.error, type: SnackbarType.error,
); );
return null; return null;

View File

@ -1880,6 +1880,7 @@ class ApiService {
_getRequest(ApiEndpoints.getRoles).then( _getRequest(ApiEndpoints.getRoles).then(
(res) => res != null ? _parseResponse(res, label: 'Roles') : null); (res) => res != null ? _parseResponse(res, label: 'Roles') : null);
static Future<Map<String, dynamic>?> createEmployee({ static Future<Map<String, dynamic>?> createEmployee({
String? id, // Optional, for editing
required String firstName, required String firstName,
required String lastName, required String lastName,
required String phoneNumber, required String phoneNumber,
@ -1888,12 +1889,13 @@ class ApiService {
required String joiningDate, required String joiningDate,
}) async { }) async {
final body = { final body = {
if (id != null) "id": id, // Include id only if editing
"firstName": firstName, "firstName": firstName,
"lastName": lastName, "lastName": lastName,
"phoneNumber": phoneNumber, "phoneNumber": phoneNumber,
"gender": gender, "gender": gender,
"jobRoleId": jobRoleId, "jobRoleId": jobRoleId,
"joiningDate": joiningDate "joiningDate": joiningDate,
}; };
final response = await _postRequest( final response = await _postRequest(
@ -1907,7 +1909,7 @@ class ApiService {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
return { return {
"success": response.statusCode == 200 && json['success'] == true, "success": response.statusCode == 200 && json['success'] == true,
"data": json "data": json,
}; };
} }

View File

@ -11,15 +11,31 @@ import 'package:marco/helpers/utils/base_bottom_sheet.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
class AddEmployeeBottomSheet extends StatefulWidget { class AddEmployeeBottomSheet extends StatefulWidget {
final Map<String, dynamic>? employeeData;
AddEmployeeBottomSheet({this.employeeData});
@override @override
State<AddEmployeeBottomSheet> createState() => _AddEmployeeBottomSheetState(); State<AddEmployeeBottomSheet> createState() => _AddEmployeeBottomSheetState();
} }
class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet> class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
with UIMixin { with UIMixin {
final AddEmployeeController _controller = Get.put(AddEmployeeController()); late final AddEmployeeController _controller;
@override
void initState() {
super.initState();
_controller = Get.put(
AddEmployeeController(),
tag: UniqueKey().toString(),
);
if (widget.employeeData != null) {
_controller.editingEmployeeData = widget.employeeData;
_controller.prefillFields();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -27,7 +43,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
init: _controller, init: _controller,
builder: (_) { builder: (_) {
return BaseBottomSheet( return BaseBottomSheet(
title: "Add Employee", title: widget.employeeData != null ? "Edit Employee" : "Add Employee",
onCancel: () => Navigator.pop(context), onCancel: () => Navigator.pop(context),
onSubmit: _handleSubmit, onSubmit: _handleSubmit,
child: Form( child: Form(
@ -98,7 +114,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
); );
} }
// --- Common label with red star ---
Widget _requiredLabel(String text) { Widget _requiredLabel(String text) {
return Row( return Row(
children: [ children: [
@ -109,7 +124,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
); );
} }
// --- Date Picker field ---
Widget _buildDatePickerField({ Widget _buildDatePickerField({
required String label, required String label,
required String value, required String value,
@ -146,7 +160,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
Future<void> _pickJoiningDate(BuildContext context) async { Future<void> _pickJoiningDate(BuildContext context) async {
final picked = await showDatePicker( final picked = await showDatePicker(
context: context, context: context,
initialDate: DateTime.now(), initialDate: _controller.joiningDate ?? DateTime.now(),
firstDate: DateTime(2000), firstDate: DateTime(2000),
lastDate: DateTime(2100), lastDate: DateTime(2100),
); );
@ -157,54 +171,25 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
} }
} }
// --- Submit logic ---
Future<void> _handleSubmit() async { Future<void> _handleSubmit() async {
// Run form validation first
final isValid = final isValid =
_controller.basicValidator.formKey.currentState?.validate() ?? false; _controller.basicValidator.formKey.currentState?.validate() ?? false;
if (!isValid) { if (!isValid ||
_controller.joiningDate == null ||
_controller.selectedGender == null ||
_controller.selectedRoleId == null) {
showAppSnackbar( showAppSnackbar(
title: "Missing Fields", title: "Missing Fields",
message: "Please fill all required fields before submitting.", message: "Please complete all required fields.",
type: SnackbarType.warning, type: SnackbarType.warning,
); );
return; return;
} }
// Additional check for dropdowns & joining date final result = await _controller.createOrUpdateEmployee();
if (_controller.joiningDate == null) {
showAppSnackbar(
title: "Missing Fields",
message: "Please select Joining Date.",
type: SnackbarType.warning,
);
return;
}
if (_controller.selectedGender == null) {
showAppSnackbar(
title: "Missing Fields",
message: "Please select Gender.",
type: SnackbarType.warning,
);
return;
}
if (_controller.selectedRoleId == null) {
showAppSnackbar(
title: "Missing Fields",
message: "Please select Role.",
type: SnackbarType.warning,
);
return;
}
// All validations passed Call API
final result = await _controller.createEmployees();
if (result != null && result['success'] == true) { if (result != null && result['success'] == true) {
final employeeData = result['data'];
final employeeController = Get.find<EmployeesScreenController>(); final employeeController = Get.find<EmployeesScreenController>();
final projectId = employeeController.selectedProjectId; final projectId = employeeController.selectedProjectId;
@ -216,20 +201,10 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
employeeController.update(['employee_screen_controller']); employeeController.update(['employee_screen_controller']);
// Reset form Navigator.pop(context, result['data']);
_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.joiningDate = null;
_controller.update();
Navigator.pop(context, employeeData);
} }
} }
// --- Section label widget ---
Widget _sectionLabel(String title) => Column( Widget _sectionLabel(String title) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -239,7 +214,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
], ],
); );
// --- Input field with icon ---
Widget _inputWithIcon({ Widget _inputWithIcon({
required String label, required String label,
required String hint, required String hint,
@ -268,7 +242,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
); );
} }
// --- Phone input ---
Widget _buildPhoneInput(BuildContext context) { Widget _buildPhoneInput(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -322,7 +295,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
); );
} }
// --- Dropdown (Gender/Role) ---
Widget _buildDropdownField({ Widget _buildDropdownField({
required String label, required String label,
required String value, required String value,
@ -356,7 +328,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
); );
} }
// --- Common input decoration ---
InputDecoration _inputDecoration(String hint) { InputDecoration _inputDecoration(String hint) {
return InputDecoration( return InputDecoration(
hintText: hint, hintText: hint,
@ -379,7 +350,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
); );
} }
// --- Gender popup ---
void _showGenderPopup(BuildContext context) async { void _showGenderPopup(BuildContext context) async {
final selected = await showMenu<Gender>( final selected = await showMenu<Gender>(
context: context, context: context,
@ -398,7 +368,6 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
} }
} }
// --- Role popup ---
void _showRolePopup(BuildContext context) async { void _showRolePopup(BuildContext context) async {
final selected = await showMenu<String>( final selected = await showMenu<String>(
context: context, context: context,

View File

@ -11,6 +11,7 @@ import 'package:marco/helpers/utils/launcher_utils.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/model/employees/add_employee_bottom_sheet.dart';
class EmployeeDetailPage extends StatefulWidget { class EmployeeDetailPage extends StatefulWidget {
final String employeeId; final String employeeId;
@ -92,8 +93,9 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: (isEmail || isPhone) ? Colors.indigo : Colors.black54, color: (isEmail || isPhone) ? Colors.indigo : Colors.black54,
fontSize: 14, fontSize: 14,
decoration: decoration: (isEmail || isPhone)
(isEmail || isPhone) ? TextDecoration.underline : TextDecoration.none, ? TextDecoration.underline
: TextDecoration.none,
), ),
), ),
); );
@ -231,7 +233,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
lastName: employee.lastName, lastName: employee.lastName,
size: 45, size: 45,
), ),
MySpacing.width(16), MySpacing.width(12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -248,6 +250,34 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
], ],
), ),
), ),
IconButton(
icon: const Icon(Icons.edit,
size: 24, color: Colors.red),
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,
'gender': employee.gender.toLowerCase(),
'job_role_id': employee.jobRoleId,
'joining_date':
employee.joiningDate?.toIso8601String(),
},
),
);
if (result != null) {
controller.fetchEmployeeDetails(widget.employeeId);
}
},
),
], ],
), ),
MySpacing.height(14), MySpacing.height(14),