feat: Add joining date functionality and enhance document upload validation

This commit is contained in:
Vaibhav Surve 2025-09-11 17:06:26 +05:30
parent 6d70afc779
commit 0cccdc6b05
5 changed files with 244 additions and 26 deletions

View File

@ -25,6 +25,7 @@ class AddEmployeeController extends MyController {
String selectedCountryCode = "+91";
bool showOnline = true;
final List<String> 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");

View File

@ -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(

View File

@ -13,10 +13,11 @@ import 'package:marco/helpers/widgets/my_snackbar.dart';
class DocumentUploadBottomSheet extends StatefulWidget {
final Function(Map<String, dynamic>) 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<DocumentUploadBottomSheet> {
}
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<DocumentUploadBottomSheet> {
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<DocumentUploadBottomSheet> {
}
}
// 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<DocumentUploadBottomSheet> {
}
}
// 6 Prepare payload
final payload = {
"documentId": _docIdController.text.trim(),
"name": _docNameController.text.trim(),
@ -100,7 +156,10 @@ class _DocumentUploadBottomSheetState extends State<DocumentUploadBottomSheet> {
.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<DocumentUploadBottomSheet> {
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 {

View File

@ -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<AddEmployeeBottomSheet>
_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<AddEmployeeBottomSheet>
);
}
// 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<void> _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<void> _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<EmployeesScreenController>();
final projectId = employeeController.selectedProjectId;
@ -100,18 +216,20 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
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<AddEmployeeBottomSheet>
],
);
// Input field with icon
// --- Input field with icon ---
Widget _inputWithIcon({
required String label,
required String hint,
@ -132,11 +250,16 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
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<AddEmployeeBottomSheet>
);
}
// 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<AddEmployeeBottomSheet>
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<AddEmployeeBottomSheet>
_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<AddEmployeeBottomSheet>
);
}
// 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<AddEmployeeBottomSheet>
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<AddEmployeeBottomSheet>
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<AddEmployeeBottomSheet>
);
}
// Common input decoration
// --- Common input decoration ---
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
@ -249,7 +376,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
);
}
// Gender popup menu
// --- Gender popup ---
void _showGenderPopup(BuildContext context) async {
final selected = await showMenu<Gender>(
context: context,
@ -268,7 +395,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
}
}
// Role popup menu
// --- Role popup ---
void _showRolePopup(BuildContext context) async {
final selected = await showMenu<String>(
context: context,

View File

@ -576,6 +576,8 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
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<UserDocumentsPage> {
);
},
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,