first commit
This commit is contained in:
613
lib/Screens/hrm/employee/add_new_employee.dart
Normal file
613
lib/Screens/hrm/employee/add_new_employee.dart
Normal file
@@ -0,0 +1,613 @@
|
||||
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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
322
lib/Screens/hrm/employee/employee_list_screen.dart
Normal file
322
lib/Screens/hrm/employee/employee_list_screen.dart
Normal file
@@ -0,0 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/employee/add_new_employee.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/employee/provider/emplpyee_list_provider.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/employee/repo/employee_repo.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/deleteing_alart_dialog.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/global_search_appbar.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/model_bottom_sheet.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../generated/l10n.dart' as lang;
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../../widgets/empty_widget/_empty_widget.dart';
|
||||
|
||||
class EmployeeListScreen extends ConsumerStatefulWidget {
|
||||
const EmployeeListScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<EmployeeListScreen> createState() => _EmployeeListScreenState();
|
||||
}
|
||||
|
||||
class _EmployeeListScreenState extends ConsumerState<EmployeeListScreen> {
|
||||
bool _isSearch = false;
|
||||
final _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(() {
|
||||
setState(() {
|
||||
_searchQuery = _searchController.text.toLowerCase();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 2. Refresh logic using Riverpod's invalidate
|
||||
Future<void> _refreshEmployeeList() async {
|
||||
// Invalidate the provider, which forces it to refetch the data
|
||||
ref.invalidate(employeeListProvider);
|
||||
// Wait for the new future to complete
|
||||
await ref.read(employeeListProvider.future);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
final employeeListAsync = ref.watch(employeeListProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
final _lang = lang.S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: GlobalSearchAppBar(
|
||||
isSearch: _isSearch,
|
||||
onSearchToggle: () {
|
||||
setState(() {
|
||||
_isSearch = !_isSearch;
|
||||
if (!_isSearch) {
|
||||
_searchController.clear();
|
||||
_searchQuery = '';
|
||||
}
|
||||
});
|
||||
},
|
||||
title: 'Employee',
|
||||
controller: _searchController,
|
||||
onChanged: (query) {
|
||||
// Listener handles the search logic
|
||||
},
|
||||
),
|
||||
body: employeeListAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
data: (employeeModel) {
|
||||
if (!permissionService.hasPermission(Permit.employeesRead.value)) {
|
||||
return const Center(child: PermitDenyWidget());
|
||||
}
|
||||
final allEmployees = employeeModel.employees ?? [];
|
||||
|
||||
// Filtering logic
|
||||
final filteredEmployees = allEmployees.where((employee) {
|
||||
final nameLower = employee.name?.toLowerCase() ?? '';
|
||||
final phoneLower = employee.phone?.toLowerCase() ?? '';
|
||||
final emailLower = employee.email?.toLowerCase() ?? '';
|
||||
|
||||
return nameLower.contains(_searchQuery) || phoneLower.contains(_searchQuery) || emailLower.contains(_searchQuery);
|
||||
}).toList();
|
||||
|
||||
if (filteredEmployees.isEmpty) {
|
||||
return Center(child: Text(_searchQuery.isEmpty ? 'No employees found.' : 'No results found for "$_searchQuery".'));
|
||||
}
|
||||
|
||||
// 3. Wrap the ListView with RefreshIndicator
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshEmployeeList, // Calls the Riverpod refresh logic
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (_, index) {
|
||||
final employee = filteredEmployees[index];
|
||||
|
||||
// Dynamic Data Mapping
|
||||
final name = employee.name ?? 'N/A';
|
||||
final phone = employee.phone ?? 'N/A';
|
||||
final designation = employee.designation?.name ?? 'N/A';
|
||||
final department = employee.department?.name ?? 'N/A';
|
||||
final image = employee.image;
|
||||
final email = employee.email ?? 'N/A';
|
||||
final country = employee.country ?? 'N/A';
|
||||
final salary = '\$${employee.amount?.toStringAsFixed(2) ?? '0.00'}';
|
||||
final gender = employee.gender ?? 'N/A';
|
||||
final shift = employee.shift?.name ?? 'N/A';
|
||||
final birthDate = employee.birthDate ?? 'N/A';
|
||||
final joinDate = employee.joinDate ?? 'N/A';
|
||||
final status = employee.status ?? 'N/A';
|
||||
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
// Displaying dynamic data in Modal Sheet
|
||||
viewModalSheet(
|
||||
context: context,
|
||||
showImage: true,
|
||||
image: image,
|
||||
item: {
|
||||
"Full Name": name,
|
||||
"Designation ": designation,
|
||||
"Department ": department,
|
||||
"Email ": email,
|
||||
"Phone ": phone,
|
||||
"Country": country,
|
||||
"Salary": salary,
|
||||
"Gender": gender,
|
||||
"Shift": shift,
|
||||
"Birth Date": birthDate,
|
||||
"Join Date": joinDate,
|
||||
"Status": status,
|
||||
},
|
||||
);
|
||||
},
|
||||
contentPadding: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 0,
|
||||
),
|
||||
horizontalTitleGap: 14,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 40,
|
||||
width: 40,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: kMainColor.withValues(alpha: 0.1)),
|
||||
child: image == null
|
||||
? Text(
|
||||
name.substring(0, 1),
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: kMainColor,
|
||||
),
|
||||
)
|
||||
: Image.network(
|
||||
fit: BoxFit.fill,
|
||||
"${APIConfig.domain}$image",
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
name,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 28,
|
||||
width: 28,
|
||||
child: IconButton(
|
||||
style: const ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
onPressed: () {
|
||||
if (!permissionService.hasPermission(Permit.employeesUpdate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text("You do not have permission to update Employee."),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Navigation to edit employee screen
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddNewEmployee(
|
||||
isEdit: true,
|
||||
employeeToEdit: employee,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPencilEdit02,
|
||||
color: kSuccessColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
phone,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kNeutral800,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
SizedBox(
|
||||
height: 28,
|
||||
width: 28,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
style: const ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
onPressed: () async {
|
||||
if (!permissionService.hasPermission(Permit.employeesDelete.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text("You do not have permission to delete Employee."),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final confirm = await showDeleteConfirmationDialog(
|
||||
itemName: 'Employee',
|
||||
context: context,
|
||||
);
|
||||
|
||||
if (confirm) {
|
||||
EasyLoading.show(status: _lang.deleting);
|
||||
final repo = EmployeeRepo();
|
||||
try {
|
||||
final result = await repo.deleteEmployee(id: employee.id.toString(), ref: ref, context: context);
|
||||
if (result) {
|
||||
ref.refresh(employeeListProvider);
|
||||
EasyLoading.showSuccess(_lang.deletedSuccessFully);
|
||||
} else {
|
||||
EasyLoading.showError("Failed to delete the Employee");
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.showError('Error deleting: $e');
|
||||
} finally {
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete03,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => Divider(
|
||||
color: kBackgroundColor,
|
||||
height: 2,
|
||||
),
|
||||
itemCount: filteredEmployees.length),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: permissionService.hasPermission(Permit.employeesCreate.value)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const AddNewEmployee()));
|
||||
},
|
||||
label: const Text('Add Employee'),
|
||||
icon: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
220
lib/Screens/hrm/employee/model/employee_list_model.dart
Normal file
220
lib/Screens/hrm/employee/model/employee_list_model.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
class EmployeeListModel {
|
||||
EmployeeListModel({
|
||||
this.message,
|
||||
this.employees,
|
||||
});
|
||||
|
||||
EmployeeListModel.fromJson(dynamic json) {
|
||||
message = json['message'];
|
||||
if (json['data'] != null) {
|
||||
employees = [];
|
||||
json['data'].forEach((v) {
|
||||
employees?.add(EmployeeData.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? message;
|
||||
List<EmployeeData>? employees;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['message'] = message;
|
||||
if (employees != null) {
|
||||
map['data'] = employees?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class EmployeeData {
|
||||
EmployeeData({
|
||||
this.id,
|
||||
this.name,
|
||||
this.businessId,
|
||||
this.branchId,
|
||||
this.designationId,
|
||||
this.departmentId,
|
||||
this.shiftId,
|
||||
this.amount,
|
||||
this.image,
|
||||
this.phone,
|
||||
this.email,
|
||||
this.gender,
|
||||
this.country,
|
||||
this.birthDate,
|
||||
this.joinDate,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.department,
|
||||
this.designation,
|
||||
this.shift,
|
||||
this.branch,
|
||||
});
|
||||
|
||||
EmployeeData.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
businessId = json['business_id'];
|
||||
branchId = json['branch_id'];
|
||||
designationId = json['designation_id'];
|
||||
departmentId = json['department_id'];
|
||||
shiftId = json['shift_id'];
|
||||
amount = json['amount'];
|
||||
image = json['image'];
|
||||
phone = json['phone'];
|
||||
email = json['email'];
|
||||
gender = json['gender'];
|
||||
country = json['country'];
|
||||
birthDate = json['birth_date'];
|
||||
joinDate = json['join_date'];
|
||||
status = json['status'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
department = json['department'] != null ? Department.fromJson(json['department']) : null;
|
||||
designation = json['designation'] != null ? Designation.fromJson(json['designation']) : null;
|
||||
shift = json['shift'] != null ? Shift.fromJson(json['shift']) : null;
|
||||
branch = json['branch'] != null ? Branch.fromJson(json['branch']) : null;
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
num? businessId;
|
||||
num? branchId;
|
||||
num? designationId;
|
||||
num? departmentId;
|
||||
num? shiftId;
|
||||
num? amount;
|
||||
dynamic image;
|
||||
String? phone;
|
||||
String? email;
|
||||
String? gender;
|
||||
String? country;
|
||||
String? birthDate;
|
||||
String? joinDate;
|
||||
String? status;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
Department? department;
|
||||
Designation? designation;
|
||||
Shift? shift;
|
||||
Branch? branch;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['name'] = name;
|
||||
map['business_id'] = businessId;
|
||||
map['branch_id'] = branchId;
|
||||
map['designation_id'] = designationId;
|
||||
map['department_id'] = departmentId;
|
||||
map['shift_id'] = shiftId;
|
||||
map['amount'] = amount;
|
||||
map['image'] = image;
|
||||
map['phone'] = phone;
|
||||
map['email'] = email;
|
||||
map['gender'] = gender;
|
||||
map['country'] = country;
|
||||
map['birth_date'] = birthDate;
|
||||
map['join_date'] = joinDate;
|
||||
map['status'] = status;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
if (department != null) {
|
||||
map['department'] = department?.toJson();
|
||||
}
|
||||
if (designation != null) {
|
||||
map['designation'] = designation?.toJson();
|
||||
}
|
||||
if (shift != null) {
|
||||
map['shift'] = shift?.toJson();
|
||||
}
|
||||
if (branch != null) {
|
||||
map['branch'] = branch?.toJson();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class Branch {
|
||||
Branch({
|
||||
this.id,
|
||||
this.name,
|
||||
});
|
||||
|
||||
Branch.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['name'] = name;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class Shift {
|
||||
Shift({
|
||||
this.id,
|
||||
this.name,
|
||||
});
|
||||
|
||||
Shift.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['name'] = name;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class Designation {
|
||||
Designation({
|
||||
this.id,
|
||||
this.name,
|
||||
});
|
||||
|
||||
Designation.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['name'] = name;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class Department {
|
||||
Department({
|
||||
this.id,
|
||||
this.name,
|
||||
});
|
||||
|
||||
Department.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['name'] = name;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/employee/model/employee_list_model.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/employee/repo/employee_repo.dart';
|
||||
|
||||
final repo = EmployeeRepo();
|
||||
final employeeListProvider = FutureProvider<EmployeeListModel>((ref) => repo.fetchAllEmployee());
|
||||
98
lib/Screens/hrm/employee/repo/employee_repo.dart
Normal file
98
lib/Screens/hrm/employee/repo/employee_repo.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_pos/Screens/hrm/employee/model/employee_list_model.dart';
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../http_client/custome_http_client.dart';
|
||||
import '../../../../http_client/customer_http_client_get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../provider/emplpyee_list_provider.dart';
|
||||
|
||||
class EmployeeRepo {
|
||||
Future<EmployeeListModel> fetchAllEmployee() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/employees');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
|
||||
return EmployeeListModel.fromJson(parsedData);
|
||||
} else {
|
||||
throw Exception('Failed to fetch Employee list');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveEmployee({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required Map<String, String> formData,
|
||||
required bool isEdit,
|
||||
required File? image,
|
||||
String? employeeId,
|
||||
}) async {
|
||||
final url = isEdit ? Uri.parse('${APIConfig.url}/employees/$employeeId') : Uri.parse('${APIConfig.url}/employees');
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: isEdit ? 'Updating...' : 'Saving...');
|
||||
|
||||
final client = http.Client();
|
||||
|
||||
// We assume CustomHttpClient handles form-data and authorization.
|
||||
CustomHttpClient customClient = CustomHttpClient(client: client, context: context, ref: ref);
|
||||
|
||||
// We need to use post for both create and update (with _method: put)
|
||||
final response = await customClient.uploadFile(
|
||||
url: url,
|
||||
fields: formData, // Passing the map directly for form-data
|
||||
file: image,
|
||||
fileFieldName: 'image',
|
||||
);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
final responseData = await response.stream.bytesToString();
|
||||
final data = jsonDecode(responseData);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
// Refresh the main employee list provider after successful operation
|
||||
ref.invalidate(employeeListProvider);
|
||||
Navigator.pop(context);
|
||||
EasyLoading.showSuccess(isEdit ? 'Employee Updated Successfully' : 'Employee Saved Successfully');
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed: ${data['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///________Delete_Employee______________________________________________________
|
||||
Future<bool> deleteEmployee({required String id, required BuildContext context, required WidgetRef ref}) async {
|
||||
try {
|
||||
final url = Uri.parse('${APIConfig.url}/employees/$id');
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(ref: ref, context: context, client: http.Client());
|
||||
final response = await customHttpClient.delete(url: url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
print('Error deleting Employee: ${response.statusCode} - ${response.body}');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
print('Error during delete operation: $error');
|
||||
return false;
|
||||
} finally {
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user