first commit

This commit is contained in:
2026-02-07 15:57:09 +07:00
commit 157096f164
1153 changed files with 415766 additions and 0 deletions

View 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)),
],
),
);
}
}

View 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,
);
}
}

View 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;
}
}

View File

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

View 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();
}
}
}