614 lines
24 KiB
Dart
614 lines
24 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:iconly/iconly.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:mobile_pos/Const/api_config.dart';
|
|
import 'package:mobile_pos/Screens/hrm/employee/repo/employee_repo.dart';
|
|
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
|
// Assuming these imports are correct based on your previous code
|
|
import '../../../constant.dart';
|
|
import '../department/provider/department_list_provider.dart';
|
|
import '../designation/provider/designation_list_provider.dart';
|
|
import '../shift/provider/shift_list_provider.dart';
|
|
import 'model/employee_list_model.dart';
|
|
|
|
class AddNewEmployee extends ConsumerStatefulWidget {
|
|
const AddNewEmployee({super.key, this.isEdit = false, this.employeeToEdit});
|
|
|
|
final bool isEdit;
|
|
final EmployeeData? employeeToEdit; // Assume you pass the data here
|
|
|
|
@override
|
|
ConsumerState<AddNewEmployee> createState() => _AddNewEmployeeState();
|
|
}
|
|
|
|
class _AddNewEmployeeState extends ConsumerState<AddNewEmployee> {
|
|
// Assuming 'status' is num (1 for active) or string ('Active')
|
|
bool _isActive(dynamic item) {
|
|
if (item == null) return false;
|
|
if (item.status is num) {
|
|
return item.status == 1;
|
|
}
|
|
if (item.status is String) {
|
|
return item.status.toLowerCase() == 'active';
|
|
}
|
|
// Default to true if status is missing or unknown (for safety)
|
|
return true;
|
|
}
|
|
|
|
final nameController = TextEditingController();
|
|
final emailController = TextEditingController();
|
|
final phoneController = TextEditingController();
|
|
final countryController = TextEditingController();
|
|
final salaryController = TextEditingController();
|
|
final birthDateController = TextEditingController();
|
|
final joinDateController = TextEditingController();
|
|
final GlobalKey<FormState> _key = GlobalKey();
|
|
|
|
// Storing IDs for API submission
|
|
int? selectedDesignationId;
|
|
int? selectedDepartmentId;
|
|
int? selectShiftId;
|
|
|
|
// Storing names for Dropdown display (if initial value is set)
|
|
String? selectedDesignationName;
|
|
String? selectedDepartmentName;
|
|
String? selectedShiftName;
|
|
|
|
String? selectedGender;
|
|
String? selectedStatus;
|
|
|
|
final ImagePicker _picker = ImagePicker();
|
|
XFile? pickedImage;
|
|
|
|
// Repositories for API calls
|
|
final _employeeCrudRepo = EmployeeRepo();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.isEdit && widget.employeeToEdit != null) {
|
|
_loadInitialData(widget.employeeToEdit!);
|
|
}
|
|
}
|
|
|
|
void _loadInitialData(EmployeeData employee) {
|
|
nameController.text = employee.name ?? '';
|
|
emailController.text = employee.email ?? '';
|
|
phoneController.text = employee.phone ?? '';
|
|
countryController.text = employee.country ?? '';
|
|
salaryController.text = employee.amount?.toString() ?? '';
|
|
birthDateController.text = _formatDateForDisplay(employee.birthDate);
|
|
joinDateController.text = _formatDateForDisplay(employee.joinDate);
|
|
|
|
// Set initial values for dropdowns (using IDs for submission)
|
|
selectedDesignationId = employee.designationId?.toInt();
|
|
selectedDesignationName = employee.designation?.name;
|
|
|
|
selectedDepartmentId = employee.departmentId?.toInt();
|
|
selectedDepartmentName = employee.department?.name;
|
|
|
|
selectShiftId = employee.shiftId?.toInt();
|
|
selectedShiftName = employee.shift?.name;
|
|
|
|
selectedGender = employee.gender;
|
|
selectedStatus = employee.status;
|
|
|
|
// Note: Image needs a separate logic if you want to load the existing one from URL
|
|
}
|
|
|
|
String _formatDateForDisplay(String? date) {
|
|
if (date == null || date.isEmpty) return '';
|
|
try {
|
|
final dateTime = DateTime.parse(date);
|
|
return DateFormat('dd/MM/yyyy').format(dateTime);
|
|
} catch (_) {
|
|
return date; // return as is if parsing fails
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
nameController.dispose();
|
|
emailController.dispose();
|
|
phoneController.dispose();
|
|
countryController.dispose();
|
|
salaryController.dispose();
|
|
birthDateController.dispose();
|
|
joinDateController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// --- API Submission Logic ---
|
|
Future<void> _submitForm() async {
|
|
if (!_key.currentState!.validate()) return;
|
|
|
|
final isEdit = widget.isEdit;
|
|
final employeeId = widget.employeeToEdit?.id?.toString();
|
|
|
|
// Prepare formData for API (using form-data structure from Postman)
|
|
final Map<String, String> formData = {
|
|
if (isEdit) '_method': 'put',
|
|
'name': nameController.text,
|
|
'designation_id': selectedDesignationId?.toString() ?? "",
|
|
'department_id': selectedDepartmentId?.toString() ?? "",
|
|
'shift_id': selectShiftId?.toString() ?? "",
|
|
'amount': salaryController.text,
|
|
'phone': phoneController.text,
|
|
'email': emailController.text,
|
|
'gender': selectedGender?.toLowerCase() ?? "",
|
|
'country': countryController.text,
|
|
// Date formatting to YYYY-MM-DD for API
|
|
'birth_date': _formatDateForAPI(birthDateController.text) ?? "",
|
|
'join_date': _formatDateForAPI(joinDateController.text) ?? "",
|
|
'status': selectedStatus?.toLowerCase() ?? "", // active | terminate | suspended
|
|
};
|
|
|
|
await _employeeCrudRepo.saveEmployee(
|
|
ref: ref,
|
|
context: context,
|
|
formData: formData,
|
|
isEdit: isEdit,
|
|
image: pickedImage != null ? File(pickedImage!.path) : null,
|
|
employeeId: employeeId,
|
|
);
|
|
}
|
|
|
|
String? _formatDateForAPI(String dateDisplay) {
|
|
// Converts display format (dd/MM/yyyy) to API format (YYYY-MM-DD)
|
|
if (dateDisplay.isEmpty) return null;
|
|
try {
|
|
final dateTime = DateFormat('dd/MM/yyyy').parse(dateDisplay);
|
|
return DateFormat('yyyy-MM-dd').format(dateTime);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// 1. Watch the three provider
|
|
final _lang = lang.S.of(context);
|
|
final designationAsync = ref.watch(designationListProvider);
|
|
final departmentAsync = ref.watch(departmentListProvider);
|
|
final shiftAsync = ref.watch(shiftListProvider);
|
|
|
|
return Scaffold(
|
|
backgroundColor: kWhite,
|
|
appBar: AppBar(
|
|
centerTitle: true,
|
|
title: Text(
|
|
widget.isEdit ? _lang.editEmployee : _lang.addNewEmployee,
|
|
),
|
|
bottom: PreferredSize(
|
|
preferredSize: const Size.fromHeight(1),
|
|
child: Divider(
|
|
height: 2,
|
|
color: kBackgroundColor,
|
|
),
|
|
),
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Form(
|
|
key: _key,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// --- Name ---
|
|
TextFormField(
|
|
controller: nameController,
|
|
keyboardType: TextInputType.name,
|
|
decoration: InputDecoration(
|
|
labelText: _lang.name,
|
|
hintText: _lang.enterYourFullName,
|
|
),
|
|
validator: (value) => value!.isEmpty ? _lang.enterFullName : null,
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Designation Dropdown (Dynamic) ---
|
|
designationAsync.when(
|
|
loading: () => const LinearProgressIndicator(),
|
|
error: (err, stack) => Text('Designation Error: $err'),
|
|
data: (model) {
|
|
final items = (model.data ?? []).where(_isActive).toList();
|
|
return DropdownButtonFormField<int>(
|
|
decoration: InputDecoration(
|
|
labelText: _lang.designation,
|
|
hintText: _lang.selectOne,
|
|
),
|
|
// Use ID for value, Name for display
|
|
value: selectedDesignationId,
|
|
validator: (value) => value == null ? _lang.pleaseSelectDesignation : null,
|
|
items: items.map((data) {
|
|
return DropdownMenuItem<int>(
|
|
value: data.id?.toInt(),
|
|
child: Text(data.name ?? 'N/A'),
|
|
);
|
|
}).toList(),
|
|
onChanged: (int? value) {
|
|
setState(() {
|
|
selectedDesignationId = value;
|
|
});
|
|
},
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Department Dropdown (Dynamic) ---
|
|
departmentAsync.when(
|
|
loading: () => const LinearProgressIndicator(),
|
|
error: (err, stack) => Text('Department Error: $err'),
|
|
data: (model) {
|
|
final items = (model.data ?? []).where(_isActive).toList();
|
|
return DropdownButtonFormField<int>(
|
|
decoration: InputDecoration(
|
|
labelText: _lang.department,
|
|
hintText: _lang.selectOne,
|
|
),
|
|
value: selectedDepartmentId,
|
|
validator: (value) => value == null ? _lang.pleaseSelectDepartment : null,
|
|
items: items.map((data) {
|
|
return DropdownMenuItem<int>(
|
|
value: data.id?.toInt(),
|
|
child: Text(data.name ?? 'N/A'),
|
|
);
|
|
}).toList(),
|
|
onChanged: (int? value) {
|
|
setState(() {
|
|
selectedDepartmentId = value;
|
|
});
|
|
},
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Email ---
|
|
TextFormField(
|
|
controller: emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: InputDecoration(
|
|
labelText: _lang.email,
|
|
hintText: _lang.enterYourEmailAddress,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Phone ---
|
|
TextFormField(
|
|
controller: phoneController,
|
|
keyboardType: TextInputType.phone,
|
|
decoration: InputDecoration(
|
|
labelText: _lang.phone,
|
|
hintText: _lang.enterYourPhoneNumber,
|
|
),
|
|
validator: (value) => value!.isEmpty ? _lang.pleaseEnterYourPhoneNumber : null,
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Country ---
|
|
TextFormField(
|
|
controller: countryController,
|
|
keyboardType: TextInputType.name,
|
|
decoration: InputDecoration(
|
|
labelText: _lang.countryName,
|
|
hintText: _lang.enterYourCountry,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Salary ---
|
|
TextFormField(
|
|
controller: salaryController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: InputDecoration(
|
|
labelText: _lang.salary,
|
|
hintText: 'Ex: \$500',
|
|
),
|
|
validator: (value) => value!.isEmpty ? _lang.pleaseEnterYourSalary : null,
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Gender & Shift (Dynamic) ---
|
|
Row(
|
|
children: [
|
|
// --- Gender ---
|
|
Expanded(
|
|
child: DropdownButtonFormField<String>(
|
|
decoration: InputDecoration(
|
|
labelText: _lang.gender,
|
|
hintText: _lang.selectOne,
|
|
),
|
|
value: selectedGender,
|
|
validator: (value) => value == null ? _lang.pleaseSelectYourGender : null,
|
|
items: ['Male', 'Female', 'Others'].map((entry) {
|
|
return DropdownMenuItem<String>(value: entry.toLowerCase(), child: Text(entry));
|
|
}).toList(),
|
|
onChanged: (String? value) {
|
|
setState(() {
|
|
selectedGender = value;
|
|
});
|
|
}),
|
|
),
|
|
const SizedBox(width: 16),
|
|
|
|
// --- Shift Dropdown (Dynamic) ---
|
|
Expanded(
|
|
child: shiftAsync.when(
|
|
loading: () => const LinearProgressIndicator(),
|
|
error: (err, stack) => Text('Shift Error: $err'),
|
|
data: (model) {
|
|
final items = (model.data ?? []).where(_isActive).toList();
|
|
return DropdownButtonFormField<int>(
|
|
decoration: InputDecoration(
|
|
labelText: _lang.shift,
|
|
hintText: _lang.selectOne,
|
|
),
|
|
value: selectShiftId,
|
|
validator: (value) => value == null ? _lang.pleaseSelectYourShift : null,
|
|
items: items.map((data) {
|
|
return DropdownMenuItem<int>(
|
|
value: data.id?.toInt(),
|
|
child: Text(data.name ?? 'N/A'),
|
|
);
|
|
}).toList(),
|
|
onChanged: (int? value) {
|
|
setState(() {
|
|
selectShiftId = value;
|
|
});
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Birth Date & Join Date ---
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextFormField(
|
|
keyboardType: TextInputType.name,
|
|
readOnly: true,
|
|
controller: birthDateController,
|
|
decoration: InputDecoration(
|
|
labelText: _lang.birthDate,
|
|
hintText: '06/02/2025',
|
|
border: const OutlineInputBorder(),
|
|
suffixIcon: IconButton(
|
|
padding: EdgeInsets.zero,
|
|
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
|
onPressed: () async {
|
|
final DateTime? picked = await showDatePicker(
|
|
initialDate: DateTime.now(),
|
|
firstDate: DateTime(1900),
|
|
lastDate: DateTime.now(),
|
|
context: context,
|
|
);
|
|
if (picked != null) {
|
|
birthDateController.text = DateFormat('dd/MM/yyyy').format(picked);
|
|
}
|
|
},
|
|
icon: const Icon(IconlyLight.calendar, size: 22),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: TextFormField(
|
|
keyboardType: TextInputType.name,
|
|
readOnly: true,
|
|
controller: joinDateController,
|
|
decoration: InputDecoration(
|
|
labelText: _lang.joinDate,
|
|
hintText: '06/02/2025',
|
|
border: const OutlineInputBorder(),
|
|
suffixIcon: IconButton(
|
|
padding: EdgeInsets.zero,
|
|
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
|
onPressed: () async {
|
|
final DateTime? picked = await showDatePicker(
|
|
initialDate: DateTime.now(),
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2101),
|
|
context: context,
|
|
);
|
|
if (picked != null) {
|
|
joinDateController.text = DateFormat('dd/MM/yyyy').format(picked);
|
|
}
|
|
},
|
|
icon: const Icon(IconlyLight.calendar, size: 22),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Status ---
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: DropdownButtonFormField<String>(
|
|
decoration: InputDecoration(
|
|
labelText: _lang.status,
|
|
hintText: _lang.selectOne,
|
|
),
|
|
value: selectedStatus,
|
|
validator: (value) => value == null ? _lang.pleaseSelectAStatus : null,
|
|
items: ['Active', 'Terminated', 'Suspended'].map((entry) {
|
|
return DropdownMenuItem<String>(value: entry.toLowerCase(), child: Text(entry));
|
|
}).toList(),
|
|
onChanged: (String? value) {
|
|
setState(() {
|
|
selectedStatus = value;
|
|
});
|
|
}),
|
|
),
|
|
const SizedBox(width: 16),
|
|
const Expanded(child: SizedBox())
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Image Picker UI (Your existing code) ---
|
|
Text(
|
|
_lang.image,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
Center(
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(height: 10),
|
|
GestureDetector(
|
|
onTap: () => showDialog(
|
|
context: context,
|
|
builder: (_) => Dialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
child: SizedBox(
|
|
height: 200,
|
|
width: MediaQuery.of(context).size.width - 80,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
_imageOption(
|
|
icon: Icons.photo_library_rounded,
|
|
label: _lang.gallery,
|
|
color: kMainColor,
|
|
source: ImageSource.gallery,
|
|
),
|
|
const SizedBox(width: 40),
|
|
_imageOption(
|
|
icon: Icons.camera,
|
|
label: _lang.camera,
|
|
color: kGreyTextColor,
|
|
source: ImageSource.camera,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Container(
|
|
height: 120,
|
|
width: 120,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.black54),
|
|
borderRadius: BorderRadius.circular(120),
|
|
image: DecorationImage(
|
|
image: pickedImage != null
|
|
? FileImage(File(pickedImage!.path))
|
|
: widget.employeeToEdit?.image != null
|
|
? NetworkImage('${APIConfig.domain}${widget.employeeToEdit?.image}')
|
|
: const AssetImage('assets/hrm/image_icon.jpg') as ImageProvider,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 0,
|
|
right: 0,
|
|
child: Container(
|
|
height: 35,
|
|
width: 35,
|
|
decoration: BoxDecoration(
|
|
color: kMainColor,
|
|
border: Border.all(color: Colors.white, width: 2),
|
|
borderRadius: BorderRadius.circular(120),
|
|
),
|
|
child: const Icon(Icons.camera_alt_outlined, size: 20, color: Colors.white),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// --- Save/Update & Reset Buttons ---
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () {
|
|
// Reset logic remains the same
|
|
setState(() {
|
|
_key.currentState?.reset();
|
|
nameController.clear();
|
|
emailController.clear();
|
|
phoneController.clear();
|
|
countryController.clear();
|
|
salaryController.clear();
|
|
birthDateController.clear();
|
|
joinDateController.clear();
|
|
selectedDesignationId = null;
|
|
selectedDepartmentId = null;
|
|
selectShiftId = null;
|
|
selectedGender = null;
|
|
selectedStatus = null;
|
|
pickedImage = null; // Reset image
|
|
});
|
|
},
|
|
child: Text(_lang.resets),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: _submitForm, // Call the submit function
|
|
child: Text(widget.isEdit ? _lang.update : _lang.save),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Helper Widget
|
|
Widget _imageOption({
|
|
required IconData icon,
|
|
required String label,
|
|
required Color color,
|
|
required ImageSource source,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: () async {
|
|
final navigator = Navigator.of(context);
|
|
pickedImage = await _picker.pickImage(source: source);
|
|
setState(() {});
|
|
Future.delayed(
|
|
const Duration(milliseconds: 100),
|
|
() => navigator.pop(),
|
|
);
|
|
},
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 60, color: color),
|
|
Text(label, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: kGreyTextColor)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|