From 0cccdc6b05f7bbb245694e1c62f6e718b107a3f0 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 11 Sep 2025 17:06:26 +0530 Subject: [PATCH] feat: Add joining date functionality and enhance document upload validation --- .../employee/add_employee_controller.dart | 8 + lib/helpers/services/api_service.dart | 2 + .../document_upload_bottom_sheet.dart | 90 +++++++++- .../employees/add_employee_bottom_sheet.dart | 161 ++++++++++++++++-- lib/view/document/user_document_screen.dart | 9 +- 5 files changed, 244 insertions(+), 26 deletions(-) diff --git a/lib/controller/employee/add_employee_controller.dart b/lib/controller/employee/add_employee_controller.dart index 68e3b58..b23e842 100644 --- a/lib/controller/employee/add_employee_controller.dart +++ b/lib/controller/employee/add_employee_controller.dart @@ -25,6 +25,7 @@ class AddEmployeeController extends MyController { String selectedCountryCode = "+91"; bool showOnline = true; final List categories = []; + DateTime? joiningDate; @override void onInit() { @@ -34,6 +35,12 @@ class AddEmployeeController extends MyController { fetchRoles(); } + void setJoiningDate(DateTime date) { + joiningDate = date; + logSafe("Joining date selected: $date"); + update(); + } + void _initializeFields() { basicValidator.addField( 'first_name', @@ -109,6 +116,7 @@ class AddEmployeeController extends MyController { phoneNumber: phoneNumber!, gender: selectedGender!.name, jobRoleId: selectedRoleId!, + joiningDate: joiningDate?.toIso8601String() ?? "", ); logSafe("Response: $response"); diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 4b97336..d0f12e2 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -1885,6 +1885,7 @@ class ApiService { required String phoneNumber, required String gender, required String jobRoleId, + required String joiningDate, }) async { final body = { "firstName": firstName, @@ -1892,6 +1893,7 @@ class ApiService { "phoneNumber": phoneNumber, "gender": gender, "jobRoleId": jobRoleId, + "joiningDate": joiningDate }; final response = await _postRequest( diff --git a/lib/model/document/document_upload_bottom_sheet.dart b/lib/model/document/document_upload_bottom_sheet.dart index 7876abb..a4a4f87 100644 --- a/lib/model/document/document_upload_bottom_sheet.dart +++ b/lib/model/document/document_upload_bottom_sheet.dart @@ -13,10 +13,11 @@ import 'package:marco/helpers/widgets/my_snackbar.dart'; class DocumentUploadBottomSheet extends StatefulWidget { final Function(Map) onSubmit; - + final bool isEmployee; const DocumentUploadBottomSheet({ Key? key, required this.onSubmit, + this.isEmployee = false, }) : super(key: key); @override @@ -43,8 +44,31 @@ class _DocumentUploadBottomSheetState extends State { } void _handleSubmit() { - if (!(_formKey.currentState?.validate() ?? false)) return; + final formState = _formKey.currentState; + // 1️⃣ Validate form fields + if (!(formState?.validate() ?? false)) { + // Collect first validation error + final errorFields = [ + {"label": "Document ID", "value": _docIdController.text.trim()}, + {"label": "Document Name", "value": _docNameController.text.trim()}, + {"label": "Description", "value": _descriptionController.text.trim()}, + ]; + + for (var field in errorFields) { + if (field["value"] == null || (field["value"] as String).isEmpty) { + showAppSnackbar( + title: "Error", + message: "${field["label"]} is required", + type: SnackbarType.error, + ); + return; + } + } + return; + } + + // 2️⃣ Validate file attachment if (selectedFile == null) { showAppSnackbar( title: "Error", @@ -54,7 +78,38 @@ class _DocumentUploadBottomSheetState extends State { return; } - // ✅ Validate file size + // 3️⃣ Validate document category based on employee/project + if (controller.selectedCategory != null) { + final selectedCategoryName = controller.selectedCategory!.name; + + if (widget.isEmployee && selectedCategoryName != 'Employee Documents') { + showAppSnackbar( + title: "Error", + message: + "Only 'Employee Documents' can be uploaded from the Employee screen. Please select the correct document type.", + type: SnackbarType.error, + ); + return; + } else if (!widget.isEmployee && + selectedCategoryName != 'Project Documents') { + showAppSnackbar( + title: "Error", + message: + "Only 'Project Documents' can be uploaded from the Project screen. Please select the correct document type.", + type: SnackbarType.error, + ); + return; + } + } else { + showAppSnackbar( + title: "Error", + message: "Please select a Document Category before uploading.", + type: SnackbarType.error, + ); + return; + } + + // 4️⃣ Validate file size final maxSizeMB = controller.selectedType?.maxSizeAllowedInMB; if (maxSizeMB != null && controller.selectedFileSize != null) { final fileSizeMB = controller.selectedFileSize! / (1024 * 1024); @@ -68,7 +123,7 @@ class _DocumentUploadBottomSheetState extends State { } } - // ✅ Validate file type + // 5️⃣ Validate file type final allowedType = controller.selectedType?.allowedContentType; if (allowedType != null && controller.selectedFileContentType != null) { if (!allowedType @@ -83,6 +138,7 @@ class _DocumentUploadBottomSheetState extends State { } } + // 6️⃣ Prepare payload final payload = { "documentId": _docIdController.text.trim(), "name": _docNameController.text.trim(), @@ -100,7 +156,10 @@ class _DocumentUploadBottomSheetState extends State { .toList(), }; + // 7️⃣ Submit widget.onSubmit(payload); + + // 8️⃣ Show success message showAppSnackbar( title: "Success", message: "Document submitted successfully", @@ -152,10 +211,28 @@ class _DocumentUploadBottomSheetState extends State { label: "Document ID", hint: "Enter Document ID", controller: _docIdController, - validator: (value) => - value == null || value.trim().isEmpty ? "Required" : null, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "Required"; + } + + // ✅ Regex validation if enabled + final selectedType = controller.selectedType; + if (selectedType != null && + selectedType.isValidationRequired && + selectedType.regexExpression != null && + selectedType.regexExpression!.isNotEmpty) { + final regExp = RegExp(selectedType.regexExpression!); + if (!regExp.hasMatch(value.trim())) { + return "Invalid ${selectedType.name} format"; + } + } + + return null; + }, isRequired: true, ), + MySpacing.height(16), /// Document Name @@ -479,7 +556,6 @@ class AttachmentSectionSingle extends StatelessWidget { } } - // ---- Reusable Widgets ---- class LabeledInput extends StatelessWidget { diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart index 993bb3d..2a30f14 100644 --- a/lib/model/employees/add_employee_bottom_sheet.dart +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -8,6 +8,9 @@ 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'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:intl/intl.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; + class AddEmployeeBottomSheet extends StatefulWidget { @override @@ -54,6 +57,18 @@ class _AddEmployeeBottomSheetState extends State _controller.basicValidator.getValidation('last_name'), ), MySpacing.height(16), + _sectionLabel("Joining Details"), + MySpacing.height(16), + _buildDatePickerField( + label: "Joining Date", + value: _controller.joiningDate != null + ? DateFormat("dd MMM yyyy") + .format(_controller.joiningDate!) + : "", + hint: "Select Joining Date", + onTap: () => _pickJoiningDate(context), + ), + MySpacing.height(16), _sectionLabel("Contact Details"), MySpacing.height(16), _buildPhoneInput(context), @@ -83,12 +98,113 @@ class _AddEmployeeBottomSheetState extends State ); } - // Submit logic + // --- Common label with red star --- + Widget _requiredLabel(String text) { + return Row( + children: [ + MyText.labelMedium(text), + const SizedBox(width: 4), + const Text("*", style: TextStyle(color: Colors.red)), + ], + ); + } + + // --- Date Picker field --- + Widget _buildDatePickerField({ + required String label, + required String value, + required String hint, + required VoidCallback onTap, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _requiredLabel(label), + MySpacing.height(8), + GestureDetector( + onTap: onTap, + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + controller: TextEditingController(text: value), + validator: (val) { + if (val == null || val.trim().isEmpty) { + return "$label is required"; + } + return null; + }, + decoration: _inputDecoration(hint).copyWith( + suffixIcon: const Icon(Icons.calendar_today), + ), + ), + ), + ), + ], + ); + } + + Future _pickJoiningDate(BuildContext context) async { + final picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + + if (picked != null) { + _controller.setJoiningDate(picked); + _controller.update(); + } + } + + // --- Submit logic --- Future _handleSubmit() async { + // Run form validation first + final isValid = + _controller.basicValidator.formKey.currentState?.validate() ?? false; + + if (!isValid) { + showAppSnackbar( + title: "Missing Fields", + message: "Please fill all required fields before submitting.", + type: SnackbarType.warning, + ); + return; + } + + // Additional check for dropdowns & joining date + 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) { - final employeeData = result['data']; // ✅ Safe now + final employeeData = result['data']; final employeeController = Get.find(); final projectId = employeeController.selectedProjectId; @@ -100,18 +216,20 @@ class _AddEmployeeBottomSheetState extends State employeeController.update(['employee_screen_controller']); + // Reset form _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 + // --- Section label widget --- Widget _sectionLabel(String title) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -121,7 +239,7 @@ class _AddEmployeeBottomSheetState extends State ], ); - // Input field with icon + // --- Input field with icon --- Widget _inputWithIcon({ required String label, required String hint, @@ -132,11 +250,16 @@ class _AddEmployeeBottomSheetState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.labelMedium(label), + _requiredLabel(label), MySpacing.height(8), TextFormField( controller: controller, - validator: validator, + validator: (val) { + if (val == null || val.trim().isEmpty) { + return "$label is required"; + } + return validator?.call(val); + }, decoration: _inputDecoration(hint).copyWith( prefixIcon: Icon(icon, size: 20), ), @@ -145,12 +268,12 @@ class _AddEmployeeBottomSheetState extends State ); } - // Phone input with country code selector + // --- Phone input --- Widget _buildPhoneInput(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.labelMedium("Phone Number"), + _requiredLabel("Phone Number"), MySpacing.height(8), Row( children: [ @@ -161,7 +284,7 @@ class _AddEmployeeBottomSheetState extends State borderRadius: BorderRadius.circular(12), color: Colors.grey.shade100, ), - child: Text("+91"), + child: const Text("+91"), ), MySpacing.width(12), Expanded( @@ -170,13 +293,11 @@ class _AddEmployeeBottomSheetState extends State _controller.basicValidator.getController('phone_number'), validator: (value) { if (value == null || value.trim().isEmpty) { - return "Phone number is required"; + return "Phone Number is required"; } - if (!RegExp(r'^\d{10}$').hasMatch(value.trim())) { return "Enter a valid 10-digit number"; } - return null; }, keyboardType: TextInputType.phone, @@ -198,7 +319,7 @@ class _AddEmployeeBottomSheetState extends State ); } - // Gender/Role field (read-only dropdown) + // --- Dropdown (Gender/Role) --- Widget _buildDropdownField({ required String label, required String value, @@ -208,7 +329,7 @@ class _AddEmployeeBottomSheetState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.labelMedium(label), + _requiredLabel(label), MySpacing.height(8), GestureDetector( onTap: onTap, @@ -216,6 +337,12 @@ class _AddEmployeeBottomSheetState extends State child: TextFormField( readOnly: true, controller: TextEditingController(text: value), + validator: (val) { + if (val == null || val.trim().isEmpty) { + return "$label is required"; + } + return null; + }, decoration: _inputDecoration(hint).copyWith( suffixIcon: const Icon(Icons.expand_more), ), @@ -226,7 +353,7 @@ class _AddEmployeeBottomSheetState extends State ); } - // Common input decoration + // --- Common input decoration --- InputDecoration _inputDecoration(String hint) { return InputDecoration( hintText: hint, @@ -249,7 +376,7 @@ class _AddEmployeeBottomSheetState extends State ); } - // Gender popup menu + // --- Gender popup --- void _showGenderPopup(BuildContext context) async { final selected = await showMenu( context: context, @@ -268,7 +395,7 @@ class _AddEmployeeBottomSheetState extends State } } - // Role popup menu + // --- Role popup --- void _showRolePopup(BuildContext context) async { final selected = await showMenu( context: context, diff --git a/lib/view/document/user_document_screen.dart b/lib/view/document/user_document_screen.dart index 5a56e6e..a33651e 100644 --- a/lib/view/document/user_document_screen.dart +++ b/lib/view/document/user_document_screen.dart @@ -576,6 +576,8 @@ class _UserDocumentsPageState extends State { isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => DocumentUploadBottomSheet( + isEmployee: + widget.isEmployee, // 👈 Pass the employee flag here onSubmit: (data) async { final success = await uploadController.uploadDocument( name: data["name"], @@ -605,8 +607,11 @@ class _UserDocumentsPageState extends State { ); }, icon: const Icon(Icons.add, color: Colors.white), - label: MyText.bodyMedium("Add Document", - color: Colors.white, fontWeight: 600), + label: MyText.bodyMedium( + "Add Document", + color: Colors.white, + fontWeight: 600, + ), backgroundColor: Colors.red, ) : null,