first commit
This commit is contained in:
473
lib/Screens/hrm/leave_request/leave/add_new_leave.dart
Normal file
473
lib/Screens/hrm/leave_request/leave/add_new_leave.dart
Normal file
@@ -0,0 +1,473 @@
|
||||
// File: add_new_leave.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/leave_request/leave/repo/leave_repo.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
// --- Local Imports ---
|
||||
import '../../../../constant.dart';
|
||||
import '../../employee/model/employee_list_model.dart';
|
||||
import '../../employee/provider/emplpyee_list_provider.dart';
|
||||
import '../leave_type/model/leave_type_list_model.dart';
|
||||
import '../leave_type/provider/leave_type_list_provider.dart';
|
||||
import 'model/leave_list_model.dart';
|
||||
|
||||
class AddNewLeave extends ConsumerStatefulWidget {
|
||||
final LeaveRequestData? leaveRequestData;
|
||||
|
||||
const AddNewLeave({super.key, this.leaveRequestData});
|
||||
|
||||
@override
|
||||
ConsumerState<AddNewLeave> createState() => _AddNewLeaveState();
|
||||
}
|
||||
|
||||
class _AddNewLeaveState extends ConsumerState<AddNewLeave> {
|
||||
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;
|
||||
}
|
||||
|
||||
// --- Form Controllers ---
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
final startDateController = TextEditingController();
|
||||
final endDateController = TextEditingController();
|
||||
final leaveDurationController = TextEditingController();
|
||||
final noteController = TextEditingController();
|
||||
|
||||
// --- Selected Values (API payload) ---
|
||||
EmployeeData? _selectedEmployee;
|
||||
LeaveTypeData? _selectedLeaveType;
|
||||
DateTime? _selectedStartDate;
|
||||
DateTime? _selectedEndDate;
|
||||
String? _selectedMonth;
|
||||
String _selectedStatus = 'pending';
|
||||
|
||||
// --- UI/API Helpers ---
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
final DateFormat _monthFormat = DateFormat('MMMM');
|
||||
|
||||
// UI state for Department (Auto-filled)
|
||||
final TextEditingController _currentEmployeeDepartmentName = TextEditingController(text: 'Select an employee');
|
||||
|
||||
bool get isEditing => widget.leaveRequestData != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (isEditing) {
|
||||
final data = widget.leaveRequestData!;
|
||||
noteController.text = data.description ?? '';
|
||||
leaveDurationController.text = data.leaveDuration?.toString() ?? '';
|
||||
_selectedStatus = data.status ?? 'pending';
|
||||
|
||||
try {
|
||||
if (data.startDate != null) {
|
||||
_selectedStartDate = DateTime.parse(data.startDate!);
|
||||
startDateController.text = _displayFormat.format(_selectedStartDate!);
|
||||
}
|
||||
if (data.endDate != null) {
|
||||
_selectedEndDate = DateTime.parse(data.endDate!);
|
||||
endDateController.text = _displayFormat.format(_selectedEndDate!);
|
||||
}
|
||||
_selectedMonth = data.month;
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing dates for editing: $e');
|
||||
}
|
||||
_currentEmployeeDepartmentName.text = data.department?.name ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
startDateController.dispose();
|
||||
endDateController.dispose();
|
||||
leaveDurationController.dispose();
|
||||
noteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(bool isStart) async {
|
||||
final controller = isStart ? startDateController : endDateController;
|
||||
|
||||
DateTime initialDate =
|
||||
isStart ? _selectedStartDate ?? DateTime.now() : _selectedEndDate ?? _selectedStartDate ?? DateTime.now();
|
||||
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: initialDate,
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
controller.text = _displayFormat.format(picked);
|
||||
|
||||
if (isStart) {
|
||||
_selectedStartDate = picked;
|
||||
} else {
|
||||
_selectedEndDate = picked;
|
||||
}
|
||||
|
||||
_updateCalculatedFields();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateCalculatedFields() {
|
||||
// 1. Month
|
||||
if (_selectedStartDate != null) {
|
||||
_selectedMonth = _monthFormat.format(_selectedStartDate!).toLowerCase();
|
||||
}
|
||||
|
||||
// 2. Duration
|
||||
if (_selectedStartDate != null && _selectedEndDate != null) {
|
||||
final duration = _selectedEndDate!.difference(_selectedStartDate!).inDays + 1;
|
||||
leaveDurationController.text = duration.toString();
|
||||
} else {
|
||||
leaveDurationController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
if (_key.currentState!.validate() && _selectedEmployee != null && _selectedLeaveType != null) {
|
||||
final repo = LeaveRepo();
|
||||
|
||||
final String apiStartDate = _apiFormat.format(_selectedStartDate!);
|
||||
final String apiEndDate = _apiFormat.format(_selectedEndDate!);
|
||||
|
||||
final payload = {
|
||||
'employee_id': _selectedEmployee!.id!,
|
||||
'leave_type_id': _selectedLeaveType!.id!,
|
||||
'start_date': apiStartDate,
|
||||
'end_date': apiEndDate,
|
||||
'leave_duration': leaveDurationController.text,
|
||||
'month': _selectedMonth!,
|
||||
'description': noteController.text,
|
||||
'status': _selectedStatus.toLowerCase(),
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
await repo.updateLeaveRequest(
|
||||
ref: ref,
|
||||
context: context,
|
||||
id: widget.leaveRequestData!.id!,
|
||||
employeeId: payload['employee_id'] as num,
|
||||
leaveTypeId: payload['leave_type_id'] as num,
|
||||
startDate: payload['start_date'] as String,
|
||||
endDate: payload['end_date'] as String,
|
||||
leaveDuration: payload['leave_duration'],
|
||||
month: payload['month'] as String,
|
||||
description: payload['description'] as String,
|
||||
status: payload['status'] as String,
|
||||
);
|
||||
} else {
|
||||
await repo.createLeaveRequest(
|
||||
ref: ref,
|
||||
context: context,
|
||||
employeeId: payload['employee_id'] as num,
|
||||
leaveTypeId: payload['leave_type_id'] as num,
|
||||
startDate: payload['start_date'] as String,
|
||||
endDate: payload['end_date'] as String,
|
||||
leaveDuration: payload['leave_duration'],
|
||||
month: payload['month'] as String,
|
||||
description: payload['description'] as String,
|
||||
status: payload['status'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final employeesAsync = ref.watch(employeeListProvider);
|
||||
final leaveTypesAsync = ref.watch(leaveTypeListProvider);
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
isEditing ? _lang.editLeave : _lang.addNewLeave,
|
||||
),
|
||||
bottom: const PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(
|
||||
height: 2,
|
||||
color: kBackgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: employeesAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error loading employees: $err')),
|
||||
data: (employeeModel) => leaveTypesAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error loading leave types: $err')),
|
||||
data: (leaveTypeModel) {
|
||||
final employees = employeeModel.employees ?? [];
|
||||
final leaveTypes = (leaveTypeModel.data ?? []).where(_isActive).toList();
|
||||
|
||||
if (isEditing) {
|
||||
final data = widget.leaveRequestData!;
|
||||
|
||||
if (_selectedEmployee == null) {
|
||||
_selectedEmployee = employees.firstWhere(
|
||||
(e) => e.id == data.employeeId,
|
||||
orElse: () => _selectedEmployee ?? employees.first,
|
||||
);
|
||||
_currentEmployeeDepartmentName.text = data.department?.name ?? '';
|
||||
}
|
||||
|
||||
_selectedLeaveType ??= leaveTypes.firstWhere(
|
||||
(lt) => lt.id == data.leaveTypeId,
|
||||
orElse: () => _selectedLeaveType ?? leaveTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildEmployeeDropdown(employees),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 2. Department Field (Auto-filled and Read-only)
|
||||
_buildDepartmentDisplay(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildLeaveTypeDropdown(leaveTypes),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildMonthDropdown(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildDateInput(true)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildDateInput(false)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildDurationInput()),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildStatusDropdown()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
TextFormField(
|
||||
controller: noteController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.note,
|
||||
hintText: _lang.enterNote,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _resetForm,
|
||||
child: Text(isEditing ? _lang.cancel : _lang.resets),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
child: Text(isEditing ? _lang.update : _lang.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widget Builders ---
|
||||
|
||||
Widget _buildEmployeeDropdown(List<EmployeeData> employees) {
|
||||
return DropdownButtonFormField<EmployeeData>(
|
||||
value: _selectedEmployee,
|
||||
icon: const Icon(Icons.keyboard_arrow_down, color: kNeutral800),
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).employee,
|
||||
hintText: lang.S.of(context).selectOne,
|
||||
),
|
||||
validator: (value) => value == null ? lang.S.of(context).pleaseSelectAnEmployee : null,
|
||||
items: employees.map((entry) {
|
||||
return DropdownMenuItem<EmployeeData>(
|
||||
value: entry,
|
||||
child: Text(entry.name ?? 'N/A'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (EmployeeData? value) {
|
||||
setState(() {
|
||||
_selectedEmployee = value;
|
||||
// Auto-set department for display
|
||||
_currentEmployeeDepartmentName.text = value?.department?.name ?? '';
|
||||
print('Name: ${_currentEmployeeDepartmentName.text}');
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- Department Display (TextFormField equivalent) ---
|
||||
Widget _buildDepartmentDisplay() {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: _currentEmployeeDepartmentName,
|
||||
// initialValue: _selectedEmployee?.department?.name ?? 'Select an employee',
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).department,
|
||||
hintText: lang.S.of(context).autoSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- (Other builders remain the same) ---
|
||||
|
||||
Widget _buildLeaveTypeDropdown(List<LeaveTypeData> leaveTypes) {
|
||||
return DropdownButtonFormField<LeaveTypeData>(
|
||||
value: _selectedLeaveType,
|
||||
icon: const Icon(Icons.keyboard_arrow_down, color: kNeutral800),
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).leaveType,
|
||||
hintText: lang.S.of(context).selectOne,
|
||||
),
|
||||
validator: (value) => value == null ? lang.S.of(context).pleaseSelectALeaveType : null,
|
||||
items: leaveTypes.map((entry) {
|
||||
return DropdownMenuItem<LeaveTypeData>(
|
||||
value: entry,
|
||||
child: Text(entry.name ?? 'N/A'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (LeaveTypeData? value) {
|
||||
setState(() {
|
||||
_selectedLeaveType = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthDropdown() {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: TextEditingController(text: _selectedMonth),
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).month,
|
||||
hintText: lang.S.of(context).autoSelected,
|
||||
),
|
||||
validator: (value) => value == null ? lang.S.of(context).pleaseSelectAStartDate : null,
|
||||
onChanged: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput(bool isStart) {
|
||||
final controller = isStart ? startDateController : endDateController;
|
||||
final label = isStart ? lang.S.of(context).startDate : lang.S.of(context).endDate;
|
||||
|
||||
return TextFormField(
|
||||
keyboardType: TextInputType.name,
|
||||
readOnly: true,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () => _selectDate(isStart),
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) return 'Please enter $label';
|
||||
|
||||
if (!isStart &&
|
||||
_selectedStartDate != null &&
|
||||
_selectedEndDate != null &&
|
||||
_selectedEndDate!.isBefore(_selectedStartDate!)) {
|
||||
return lang.S.of(context).endDateCannotBeBeforeStartDate;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDurationInput() {
|
||||
return TextFormField(
|
||||
controller: leaveDurationController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).leaveDuration,
|
||||
hintText: lang.S.of(context).autoCalculatedDays,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusDropdown() {
|
||||
final List<String> statusOptions = ['Pending', 'Approved', 'Rejected'];
|
||||
|
||||
return DropdownButtonFormField<String>(
|
||||
value: _selectedStatus.isNotEmpty ? _selectedStatus[0].toUpperCase() + _selectedStatus.substring(1) : null,
|
||||
icon: const Icon(Icons.keyboard_arrow_down, color: kNeutral800),
|
||||
decoration: InputDecoration(labelText: lang.S.of(context).status, hintText: lang.S.of(context).selectOne),
|
||||
validator: (value) => value == null ? lang.S.of(context).pleaseSelectStatus : null,
|
||||
items: statusOptions.map((entry) {
|
||||
return DropdownMenuItem(value: entry, child: Text(entry));
|
||||
}).toList(),
|
||||
onChanged: (String? value) {
|
||||
setState(() {
|
||||
_selectedStatus = value!.toLowerCase();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _resetForm() {
|
||||
if (!isEditing) {
|
||||
setState(() {
|
||||
_key.currentState?.reset();
|
||||
startDateController.clear();
|
||||
endDateController.clear();
|
||||
leaveDurationController.clear();
|
||||
noteController.clear();
|
||||
_selectedEmployee = null;
|
||||
_currentEmployeeDepartmentName.clear(); // Clear department name
|
||||
_selectedLeaveType = null;
|
||||
_selectedMonth = null;
|
||||
_selectedStartDate = null;
|
||||
_selectedEndDate = null;
|
||||
_selectedStatus = 'pending';
|
||||
});
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
513
lib/Screens/hrm/leave_request/leave/leave_list_screen.dart
Normal file
513
lib/Screens/hrm/leave_request/leave/leave_list_screen.dart
Normal file
@@ -0,0 +1,513 @@
|
||||
// File: leave_list_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/Screens/hrm/leave_request/leave/add_new_leave.dart'; // Correct Provider Name
|
||||
import 'package:mobile_pos/Screens/hrm/leave_request/leave/provider/leave_list_provider.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/filter_dropdown.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/model_bottom_sheet.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import '../../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../../widgets/deleteing_alart_dialog.dart';
|
||||
|
||||
// --- Data Layer Imports ---
|
||||
import 'package:mobile_pos/Screens/hrm/leave_request/leave/repo/leave_repo.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/leave_request/leave/model/leave_list_model.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/employee/provider/emplpyee_list_provider.dart'; // Employee Provider
|
||||
|
||||
class LeaveListScreen extends ConsumerStatefulWidget {
|
||||
const LeaveListScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LeaveListScreen> createState() => _LeaveListScreenState();
|
||||
}
|
||||
|
||||
class _LeaveListScreenState extends ConsumerState<LeaveListScreen> {
|
||||
// --- Filter State ---
|
||||
String? _selectedEmployeeFilter;
|
||||
String? _selectedMonthFilter;
|
||||
|
||||
List<LeaveRequestData> _filteredList = [];
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
static const List<String> _monthOptions = [
|
||||
'All Month',
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(_applyFilters);
|
||||
_selectedEmployeeFilter = 'All Employee';
|
||||
_selectedMonthFilter = 'All Month';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.removeListener(_applyFilters);
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- Filtering Logic ---
|
||||
|
||||
void _applyFilters() {
|
||||
setState(() {}); // Rebuild to apply filters
|
||||
}
|
||||
|
||||
void _filterLeaveRequests(List<LeaveRequestData> allRequests) {
|
||||
final query = _searchController.text.toLowerCase().trim();
|
||||
|
||||
_filteredList = allRequests.where((leave) {
|
||||
final name = (leave.employee?.name ?? '').toLowerCase();
|
||||
final leaveType = (leave.leaveType?.name ?? '').toLowerCase();
|
||||
final month = (leave.month ?? '').toLowerCase();
|
||||
final status = (leave.status ?? '').toLowerCase();
|
||||
|
||||
// 1. Search Query Filter
|
||||
final matchesQuery = query.isEmpty ||
|
||||
name.contains(query) ||
|
||||
leaveType.contains(query) ||
|
||||
month.contains(query) ||
|
||||
status.contains(query);
|
||||
|
||||
if (!matchesQuery) return false;
|
||||
|
||||
// 2. Employee Filter
|
||||
final employeeNameMatches =
|
||||
_selectedEmployeeFilter == 'All Employee' || name == _selectedEmployeeFilter!.toLowerCase();
|
||||
|
||||
// 3. Month Filter
|
||||
final monthMatches = _selectedMonthFilter == 'All Month' || month.startsWith(_selectedMonthFilter!.toLowerCase());
|
||||
|
||||
return employeeNameMatches && monthMatches;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// --- Utility Functions ---
|
||||
|
||||
Color _getStatusColor(String? status) {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'approved':
|
||||
return kSuccessColor;
|
||||
case 'rejected':
|
||||
return Colors.red;
|
||||
case 'pending':
|
||||
return Colors.orange;
|
||||
default:
|
||||
return kNeutral800;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Delete Logic ---
|
||||
|
||||
void _showDeleteConfirmationDialog(BuildContext context, WidgetRef ref, num id, String name) async {
|
||||
bool result = await showDeleteConfirmationDialog(
|
||||
context: context,
|
||||
itemName: name,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
final repo = LeaveRepo();
|
||||
await repo.deleteLeaveRequest(id: id, context: context, ref: ref);
|
||||
|
||||
// The repo method should handle ref.invalidate(leaveRequestListProvider)
|
||||
// If it doesn't, uncomment the line below:
|
||||
// ref.invalidate(leaveRequestListProvider);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pull to Refresh ---
|
||||
|
||||
Future<void> _refreshData() async {
|
||||
// Invalidate and watch future to force reload of the leave list
|
||||
ref.invalidate(leaveRequestListProvider);
|
||||
return ref.watch(leaveRequestListProvider.future);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = lang.S.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final leaveListAsync = ref.watch(leaveRequestListProvider);
|
||||
final employeesAsync = ref.watch(employeeListProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
|
||||
// Combine data fetching results for UI
|
||||
final combinedAsync = leaveListAsync.asData != null && employeesAsync.asData != null
|
||||
? AsyncValue.data(true)
|
||||
: leaveListAsync.hasError || employeesAsync.hasError
|
||||
? AsyncValue.error(leaveListAsync.error ?? employeesAsync.error!, StackTrace.current)
|
||||
: const AsyncValue.loading();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(_lang.leaveList),
|
||||
// Filter Dropdowns in AppBar bottom section
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(65),
|
||||
child: Column(
|
||||
children: [
|
||||
const Divider(thickness: 1.5, color: kBackgroundColor, height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 13),
|
||||
child: Row(
|
||||
children: [
|
||||
// Employee Filter Dropdown
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: FilterDropdownButton<String>(
|
||||
value: _selectedEmployeeFilter,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'All Employee',
|
||||
child: Text(_lang.allEmployee),
|
||||
),
|
||||
// CRITICAL: Employee data access uses .data property
|
||||
...(employeesAsync.value?.employees ?? [])
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e.name,
|
||||
child: Text(e.name ?? 'n/a'),
|
||||
))
|
||||
.toList(),
|
||||
]
|
||||
.map((item) => item.value != null
|
||||
? DropdownMenuItem(
|
||||
value: item.value,
|
||||
child: Text(
|
||||
item.value!,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: kNeutral800),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: item)
|
||||
.toList(),
|
||||
onChanged: (String? value) {
|
||||
setState(() {
|
||||
_selectedEmployeeFilter = value;
|
||||
_applyFilters();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
// Month Filter Dropdown
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: FilterDropdownButton<String>(
|
||||
buttonDecoration: BoxDecoration(
|
||||
color: kBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(color: kBorderColor),
|
||||
),
|
||||
value: _selectedMonthFilter,
|
||||
items: _monthOptions.map((entry) {
|
||||
return DropdownMenuItem(
|
||||
value: entry,
|
||||
child: Text(
|
||||
entry,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: kNeutral800),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? value) {
|
||||
setState(() {
|
||||
_selectedMonthFilter = value;
|
||||
_applyFilters();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(thickness: 1.5, color: kBackgroundColor, height: 1),
|
||||
],
|
||||
)),
|
||||
),
|
||||
// Body handles loading/error and displays filtered list
|
||||
body: combinedAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: Failed to load data.')),
|
||||
data: (_) {
|
||||
// Permission Check
|
||||
if (!permissionService.hasPermission(Permit.leavesRead.value)) {
|
||||
return const Center(child: PermitDenyWidget());
|
||||
}
|
||||
|
||||
// Data is loaded, apply filter
|
||||
_filterLeaveRequests(leaveListAsync.value?.data ?? []);
|
||||
|
||||
if (_filteredList.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Text(
|
||||
_lang.noLeaveRequestFound,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _filteredList.length,
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
color: kBackgroundColor,
|
||||
height: 1.5,
|
||||
),
|
||||
itemBuilder: (_, index) => _buildShiftItem(
|
||||
context: context,
|
||||
ref: ref,
|
||||
leave: _filteredList[index],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
bottomNavigationBar: permissionService.hasPermission(Permit.leavesCreate.value)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const AddNewLeave()),
|
||||
),
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
label: Text(_lang.addLeave),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// --- List Item Builder ---
|
||||
|
||||
Widget _buildShiftItem({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required LeaveRequestData leave,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final status = leave.status;
|
||||
final statusColor = _getStatusColor(status);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => viewModalSheet(
|
||||
context: context,
|
||||
item: {
|
||||
"Name": leave.employee?.name ?? 'N/A',
|
||||
"Department": leave.department?.name ?? 'N/A',
|
||||
"Leave Type": leave.leaveType?.name ?? 'N/A',
|
||||
"Month": leave.month ?? 'N/A',
|
||||
"Start Date": leave.startDate ?? 'N/A',
|
||||
"End Date": leave.endDate ?? 'N/A',
|
||||
"Leave Duration": leave.leaveDuration?.toString() ?? '0',
|
||||
"Status": status?.toUpperCase() ?? 'N/A'
|
||||
},
|
||||
description: leave.description ?? lang.S.of(context).noDescriptionProvided,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13.5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
leave.employee?.name ?? 'N/A Employee',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
leave.leaveType?.name ?? 'N/A Type',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: kNeutral800),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
status?.toUpperCase() ?? 'N/A',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildActionButtons(context, ref, leave),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildTimeColumn(
|
||||
time: leave.month ?? 'N/A',
|
||||
label: lang.S.of(context).month,
|
||||
theme: theme,
|
||||
),
|
||||
_buildTimeColumn(
|
||||
time: leave.startDate ?? 'N/A',
|
||||
label: lang.S.of(context).startDate,
|
||||
theme: theme,
|
||||
),
|
||||
_buildTimeColumn(
|
||||
time: leave.endDate ?? 'N/A',
|
||||
label: lang.S.of(context).endDate,
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeColumn({
|
||||
required String time,
|
||||
required String label,
|
||||
required ThemeData theme,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
time,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kNeutral800,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(BuildContext context, WidgetRef ref, LeaveRequestData leave) {
|
||||
final permissionService = PermissionService(ref);
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// Made async to await navigation
|
||||
if (!permissionService.hasPermission(Permit.leavesUpdate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).youDoNotHavePermissionToUpdateLeaveRequest),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Await the push operation
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddNewLeave(leaveRequestData: leave),
|
||||
),
|
||||
);
|
||||
// The repo call inside AddNewLeave should invalidate the provider,
|
||||
// causing this screen to rebuild automatically upon return.
|
||||
},
|
||||
child: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPencilEdit02,
|
||||
color: kSuccessColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (!permissionService.hasPermission(Permit.leavesDelete.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).youDoNotHavePermissionToDeleteLeaveRequest),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (leave.id != null) {
|
||||
_showDeleteConfirmationDialog(
|
||||
context,
|
||||
ref,
|
||||
leave.id!,
|
||||
leave.employee?.name ?? lang.S.of(context).leaveRequest,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete03,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// void _showDeleteConfirmationDialog(BuildContext context, WidgetRef ref, num id, String name) async {
|
||||
// bool result = await showDeleteConfirmationDialog(
|
||||
// context: context,
|
||||
// itemName: name,
|
||||
// );
|
||||
//
|
||||
// if (result) {
|
||||
// final repo = LeaveRepo();
|
||||
// // This call should trigger list refresh via repo's ref.invalidate()
|
||||
// await repo.deleteLeaveRequest(id: id, context: context, ref: ref);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
181
lib/Screens/hrm/leave_request/leave/model/leave_list_model.dart
Normal file
181
lib/Screens/hrm/leave_request/leave/model/leave_list_model.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
class LeaveListModel {
|
||||
LeaveListModel({
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
LeaveListModel.fromJson(dynamic json) {
|
||||
message = json['message'];
|
||||
if (json['data'] != null) {
|
||||
data = [];
|
||||
json['data'].forEach((v) {
|
||||
data?.add(LeaveRequestData.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? message;
|
||||
List<LeaveRequestData>? data;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['message'] = message;
|
||||
if (data != null) {
|
||||
map['data'] = data?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class LeaveRequestData {
|
||||
LeaveRequestData({
|
||||
this.id,
|
||||
this.businessId,
|
||||
this.branchId,
|
||||
this.employeeId,
|
||||
this.leaveTypeId,
|
||||
this.departmentId,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.leaveDuration,
|
||||
this.month,
|
||||
this.status,
|
||||
this.description,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.employee,
|
||||
this.branch,
|
||||
this.leaveType,
|
||||
this.department,
|
||||
});
|
||||
|
||||
LeaveRequestData.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
businessId = json['business_id'];
|
||||
branchId = json['branch_id'];
|
||||
employeeId = json['employee_id'];
|
||||
leaveTypeId = json['leave_type_id'];
|
||||
departmentId = json['department_id'];
|
||||
startDate = json['start_date'];
|
||||
endDate = json['end_date'];
|
||||
leaveDuration = json['leave_duration'];
|
||||
month = json['month'];
|
||||
status = json['status'];
|
||||
description = json['description'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
employee = json['employee'] != null ? Employee.fromJson(json['employee']) : null;
|
||||
branch = json['branch'];
|
||||
leaveType = json['leave_type'] != null ? LeaveType.fromJson(json['leave_type']) : null;
|
||||
department = json['department'] != null ? Department.fromJson(json['department']) : null;
|
||||
}
|
||||
num? id;
|
||||
num? businessId;
|
||||
dynamic branchId;
|
||||
num? employeeId;
|
||||
num? leaveTypeId;
|
||||
num? departmentId;
|
||||
String? startDate;
|
||||
String? endDate;
|
||||
num? leaveDuration;
|
||||
String? month;
|
||||
String? status;
|
||||
String? description;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
Employee? employee;
|
||||
dynamic branch;
|
||||
LeaveType? leaveType;
|
||||
Department? department;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['business_id'] = businessId;
|
||||
map['branch_id'] = branchId;
|
||||
map['employee_id'] = employeeId;
|
||||
map['leave_type_id'] = leaveTypeId;
|
||||
map['department_id'] = departmentId;
|
||||
map['start_date'] = startDate;
|
||||
map['end_date'] = endDate;
|
||||
map['leave_duration'] = leaveDuration;
|
||||
map['month'] = month;
|
||||
map['status'] = status;
|
||||
map['description'] = description;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
if (employee != null) {
|
||||
map['employee'] = employee?.toJson();
|
||||
}
|
||||
map['branch'] = branch;
|
||||
if (leaveType != null) {
|
||||
map['leave_type'] = leaveType?.toJson();
|
||||
}
|
||||
if (department != null) {
|
||||
map['department'] = department?.toJson();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class LeaveType {
|
||||
LeaveType({
|
||||
this.id,
|
||||
this.name,
|
||||
});
|
||||
|
||||
LeaveType.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 Employee {
|
||||
Employee({
|
||||
this.id,
|
||||
this.name,
|
||||
});
|
||||
|
||||
Employee.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,10 @@
|
||||
// File: leave_request_provider.dart
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../model/leave_list_model.dart';
|
||||
import '../repo/leave_repo.dart';
|
||||
|
||||
final repo = LeaveRepo();
|
||||
// This provider will fetch the list of all leave requests
|
||||
final leaveRequestListProvider = FutureProvider<LeaveListModel>((ref) => repo.fetchAllLeaveRequests());
|
||||
189
lib/Screens/hrm/leave_request/leave/repo/leave_repo.dart
Normal file
189
lib/Screens/hrm/leave_request/leave/repo/leave_repo.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
// File: leave_repo.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../../Const/api_config.dart';
|
||||
import '../../../../../http_client/custome_http_client.dart';
|
||||
import '../../../../../http_client/customer_http_client_get.dart';
|
||||
import '../model/leave_list_model.dart';
|
||||
import '../provider/leave_list_provider.dart';
|
||||
|
||||
class LeaveRepo {
|
||||
// Base endpoint for leave requests from Postman screenshots
|
||||
static const String _endpoint = '/leaves';
|
||||
|
||||
///---------------- FETCH ALL LEAVE REQUESTS (GET) ----------------///
|
||||
Future<LeaveListModel> fetchAllLeaveRequests() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
return LeaveListModel.fromJson(parsedData);
|
||||
} else {
|
||||
throw Exception('Failed to fetch leave requests list. Status: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- CREATE LEAVE REQUEST (POST) ----------------///
|
||||
Future<void> createLeaveRequest({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num employeeId,
|
||||
required num leaveTypeId,
|
||||
required String startDate,
|
||||
required String endDate,
|
||||
required dynamic leaveDuration,
|
||||
required String month,
|
||||
required String description,
|
||||
required String status, // e.g., 'pending'
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
'employee_id': employeeId,
|
||||
'leave_type_id': leaveTypeId,
|
||||
'start_date': startDate,
|
||||
'end_date': endDate,
|
||||
'leave_duration': leaveDuration,
|
||||
'month': month,
|
||||
'description': description,
|
||||
'status': status,
|
||||
});
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Submitting Leave Request...');
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
addContentTypeInHeader: true,
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (responseData.statusCode == 200 || responseData.statusCode == 201) {
|
||||
ref.refresh(leaveRequestListProvider); // Assuming you'll define this provider
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Leave Request created successfully')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Submission failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- UPDATE LEAVE REQUEST (POST with _method=put) ----------------///
|
||||
Future<void> updateLeaveRequest({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num id, // Leave Request ID from Postman URL
|
||||
required num employeeId,
|
||||
required num leaveTypeId,
|
||||
required String startDate,
|
||||
required String endDate,
|
||||
required dynamic leaveDuration,
|
||||
required String month,
|
||||
required String description,
|
||||
required String status, // e.g., 'approved'
|
||||
}) async {
|
||||
// Postman URL: {{url}}/api/v1/leaves/2
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$id');
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
'_method': 'put', // Required for sending PUT data via POST
|
||||
'employee_id': employeeId,
|
||||
'leave_type_id': leaveTypeId,
|
||||
'start_date': startDate,
|
||||
'end_date': endDate,
|
||||
'leave_duration': leaveDuration,
|
||||
'month': month,
|
||||
'description': description,
|
||||
'status': status,
|
||||
});
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Updating Request...');
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
// Sending as POST request based on the Postman screenshot
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
addContentTypeInHeader: true,
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
ref.refresh(leaveRequestListProvider); // Assuming you'll define this provider
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Leave Request updated successfully')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Update failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- DELETE LEAVE REQUEST (DELETE) ----------------///
|
||||
Future<bool> deleteLeaveRequest({
|
||||
required num id,
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
}) async {
|
||||
try {
|
||||
EasyLoading.show(status: 'Deleting...');
|
||||
// URL: {{url}}/api/v1/leaves/{id}
|
||||
final url = Uri.parse('${APIConfig.url}$_endpoint/$id');
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(ref: ref, context: context, client: http.Client());
|
||||
final response = await customHttpClient.delete(url: url);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ref.refresh(leaveRequestListProvider); // Assuming you'll define this provider
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Leave Request deleted successfully')),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Deletion failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred during deletion: $error')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
186
lib/Screens/hrm/leave_request/leave_type/add_new_leave_type.dart
Normal file
186
lib/Screens/hrm/leave_request/leave_type/add_new_leave_type.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
// File: add_new_leave_type.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/leave_request/leave_type/repo/leave_type_repo.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/label_style.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
import 'model/leave_type_list_model.dart';
|
||||
|
||||
class AddNewLeaveType extends ConsumerStatefulWidget {
|
||||
final LeaveTypeData? leaveTypeData; // For editing
|
||||
|
||||
// isEdit property replaced by checking if leaveTypeData is null
|
||||
const AddNewLeaveType({super.key, this.leaveTypeData});
|
||||
|
||||
@override
|
||||
ConsumerState<AddNewLeaveType> createState() => _AddNewLeaveTypeState();
|
||||
}
|
||||
|
||||
class _AddNewLeaveTypeState extends ConsumerState<AddNewLeaveType> {
|
||||
final nameController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
String? selectedValue;
|
||||
GlobalKey<FormState> key = GlobalKey();
|
||||
|
||||
bool get isEditing => widget.leaveTypeData != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (isEditing) {
|
||||
final data = widget.leaveTypeData!;
|
||||
nameController.text = data.name ?? '';
|
||||
descriptionController.text = data.description ?? '';
|
||||
|
||||
// Convert num status (1/0) to string status ('Active'/'Inactive')
|
||||
if (data.status == 1) {
|
||||
selectedValue = 'Active';
|
||||
} else if (data.status == 0) {
|
||||
selectedValue = 'Inactive';
|
||||
}
|
||||
} else {
|
||||
// Default status for new entry
|
||||
selectedValue = 'Active';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
if (key.currentState!.validate() && selectedValue != null) {
|
||||
final repo = LeaveTypeRepo();
|
||||
final statusNum = selectedValue == 'Active' ? 1 : 0;
|
||||
|
||||
if (isEditing) {
|
||||
// --- UPDATE LEAVE TYPE ---
|
||||
await repo.updateLeaveType(
|
||||
ref: ref,
|
||||
context: context,
|
||||
id: widget.leaveTypeData!.id!,
|
||||
name: nameController.text,
|
||||
description: descriptionController.text,
|
||||
status: statusNum,
|
||||
);
|
||||
} else {
|
||||
// --- CREATE LEAVE TYPE ---
|
||||
await repo.createLeaveType(
|
||||
ref: ref,
|
||||
context: context,
|
||||
name: nameController.text,
|
||||
description: descriptionController.text,
|
||||
status: statusNum,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _resetOrCancel() {
|
||||
if (isEditing) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
setState(() {
|
||||
key.currentState?.reset();
|
||||
nameController.clear();
|
||||
descriptionController.clear();
|
||||
selectedValue = 'Active';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
isEditing ? 'Edit Leave Type' : 'Add New Leave Type',
|
||||
),
|
||||
bottom: const PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(
|
||||
height: 2,
|
||||
color: kBackgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: key,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
label: labelSpan(
|
||||
title: 'Name',
|
||||
context: context,
|
||||
),
|
||||
hintText: 'Enter leave type name',
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? 'Please Enter leave type name' : null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedValue,
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: kNeutral800,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Status',
|
||||
hintText: 'Select a status',
|
||||
),
|
||||
items: ['Active', 'Inactive'].map((String value) {
|
||||
return DropdownMenuItem<String>(value: value, child: Text(value));
|
||||
}).toList(),
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
selectedValue = newValue;
|
||||
});
|
||||
},
|
||||
validator: (value) => value == null ? 'Please select a status' : null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description ',
|
||||
hintText: 'Enter Description...',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _resetOrCancel,
|
||||
child: Text(isEditing ? 'Cancel' : 'Reset'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
child: Text(isEditing ? 'Update' : 'Save'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
298
lib/Screens/hrm/leave_request/leave_type/leave_type_list.dart
Normal file
298
lib/Screens/hrm/leave_request/leave_type/leave_type_list.dart
Normal file
@@ -0,0 +1,298 @@
|
||||
// File: leave_type_list.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/Screens/hrm/leave_request/leave_type/add_new_leave_type.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/leave_request/leave_type/provider/leave_type_list_provider.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/leave_request/leave_type/repo/leave_type_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 '../../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../../../widgets/empty_widget/_empty_widget.dart';
|
||||
import 'model/leave_type_list_model.dart';
|
||||
|
||||
class LeaveTypeList extends ConsumerStatefulWidget {
|
||||
const LeaveTypeList({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LeaveTypeList> createState() => _LeaveTypeListState();
|
||||
}
|
||||
|
||||
class _LeaveTypeListState extends ConsumerState<LeaveTypeList> {
|
||||
bool _isSearch = false;
|
||||
final _searchController = TextEditingController();
|
||||
List<LeaveTypeData> _filteredLeaveTypes = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(_onSearchChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.removeListener(_onSearchChanged);
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Method to refresh the list manually (for pull-to-refresh)
|
||||
Future<void> _refreshList() async {
|
||||
ref.invalidate(leaveTypeListProvider);
|
||||
// Wait for the future provider to reload
|
||||
await ref.read(leaveTypeListProvider.future);
|
||||
}
|
||||
|
||||
void _onSearchChanged() {
|
||||
setState(() {
|
||||
// Trigger widget rebuild to run filter
|
||||
});
|
||||
}
|
||||
|
||||
void _filterLeaveTypes(List<LeaveTypeData> allTypes) {
|
||||
final query = _searchController.text.toLowerCase().trim();
|
||||
if (query.isEmpty) {
|
||||
_filteredLeaveTypes = allTypes;
|
||||
} else {
|
||||
_filteredLeaveTypes = allTypes.where((type) {
|
||||
final nameMatch = (type.name ?? '').toLowerCase().contains(query);
|
||||
final descriptionMatch = (type.description ?? '').toLowerCase().contains(query);
|
||||
final status = type.status == 1 ? 'active' : 'inactive';
|
||||
final statusMatch = status.contains(query);
|
||||
|
||||
return nameMatch || descriptionMatch || statusMatch;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusText(num? status) {
|
||||
if (status == 1) return 'Active';
|
||||
if (status == 0) return 'Inactive';
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
Color _getStatusColor(num? status) {
|
||||
if (status == 1) return kSuccessColor;
|
||||
if (status == 0) return Colors.red;
|
||||
return kNeutral800;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
final leaveListAsync = ref.watch(leaveTypeListProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: GlobalSearchAppBar(
|
||||
isSearch: _isSearch,
|
||||
onSearchToggle: () => setState(() {
|
||||
_isSearch = !_isSearch;
|
||||
if (!_isSearch) _searchController.clear();
|
||||
}),
|
||||
title: 'Leave Type',
|
||||
controller: _searchController,
|
||||
onChanged: (query) {
|
||||
// Handled by listener
|
||||
},
|
||||
),
|
||||
body: leaveListAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(
|
||||
child: Text('Failed to load leave types: $err'),
|
||||
),
|
||||
data: (model) {
|
||||
if (!permissionService.hasPermission(Permit.leaveTypesRead.value)) {
|
||||
return const Center(child: PermitDenyWidget());
|
||||
}
|
||||
final allTypes = model.data ?? [];
|
||||
_filterLeaveTypes(allTypes);
|
||||
|
||||
if (_filteredLeaveTypes.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshList,
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Text(
|
||||
_searchController.text.isEmpty ? 'No leave types found.' : 'No results found for "${_searchController.text}".',
|
||||
style: _theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshList,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (_, index) {
|
||||
final leaveType = _filteredLeaveTypes[index];
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
viewModalSheet(
|
||||
context: context,
|
||||
item: {
|
||||
"Name": leaveType.name ?? 'N/A',
|
||||
"Status": _getStatusText(leaveType.status),
|
||||
},
|
||||
description: leaveType.description ?? 'N/A',
|
||||
);
|
||||
},
|
||||
contentPadding: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 0,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
leaveType.name ?? 'n/a',
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildEditButton(context, leaveType),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
leaveType.description ?? 'No description',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kNeutral800,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
_buildDeleteButton(context, ref, leaveType),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
color: kBackgroundColor,
|
||||
height: 2,
|
||||
),
|
||||
itemCount: _filteredLeaveTypes.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: permissionService.hasPermission(Permit.leaveTypesCreate.value)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const AddNewLeaveType()));
|
||||
},
|
||||
label: const Text('Add Leave Type'),
|
||||
icon: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEditButton(BuildContext context, LeaveTypeData leaveType) {
|
||||
final permissionService = PermissionService(ref);
|
||||
return IconButton(
|
||||
style: const ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
onPressed: () {
|
||||
if (!permissionService.hasPermission(Permit.leaveTypesUpdate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text("You do not have permission to update Leave Type."),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
// Pass data for editing
|
||||
builder: (context) => AddNewLeaveType(leaveTypeData: leaveType),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPencilEdit02,
|
||||
color: kSuccessColor,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeleteButton(BuildContext context, WidgetRef ref, LeaveTypeData leaveType) {
|
||||
return IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
style: const ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
onPressed: () {
|
||||
if (leaveType.id != null) {
|
||||
_showDeleteConfirmationDialog(context, ref, leaveType.id!, leaveType.name ?? 'this leave type');
|
||||
}
|
||||
},
|
||||
icon: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete03,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmationDialog(BuildContext context, WidgetRef ref, num id, String name) async {
|
||||
final permissionService = PermissionService(ref);
|
||||
if (!permissionService.hasPermission(Permit.leaveTypesDelete.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text("You do not have permission to delete Leave Type."),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
bool result = await showDeleteConfirmationDialog(
|
||||
context: context,
|
||||
itemName: 'Leave Type',
|
||||
);
|
||||
|
||||
if (result) {
|
||||
final repo = LeaveTypeRepo();
|
||||
await repo.deleteLeaveType(id: id, context: context, ref: ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// File: leave_type_model.dart
|
||||
|
||||
class LeaveTypeListModel {
|
||||
LeaveTypeListModel({
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
LeaveTypeListModel.fromJson(dynamic json) {
|
||||
message = json['message'];
|
||||
if (json['data'] != null) {
|
||||
data = [];
|
||||
json['data'].forEach((v) {
|
||||
data?.add(LeaveTypeData.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? message;
|
||||
List<LeaveTypeData>? data;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['message'] = message;
|
||||
if (data != null) {
|
||||
map['data'] = data?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class LeaveTypeData {
|
||||
LeaveTypeData({
|
||||
this.id,
|
||||
this.businessId,
|
||||
this.name,
|
||||
this.description,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
LeaveTypeData.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
businessId = json['business_id'];
|
||||
name = json['name'];
|
||||
description = json['description'];
|
||||
// Assuming status 1 means 'Active' and 0 means 'Inactive' in the UI
|
||||
status = json['status'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
}
|
||||
num? id;
|
||||
num? businessId;
|
||||
String? name;
|
||||
String? description;
|
||||
num? status; // status is a number (1 or 0)
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['business_id'] = businessId;
|
||||
map['name'] = name;
|
||||
map['description'] = description;
|
||||
map['status'] = status;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// File: leave_type_provider.dart
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../model/leave_type_list_model.dart';
|
||||
import '../repo/leave_type_repo.dart';
|
||||
|
||||
final repo = LeaveTypeRepo();
|
||||
final leaveTypeListProvider = FutureProvider<LeaveTypeListModel>((ref) => repo.fetchAllLeaveTypes());
|
||||
@@ -0,0 +1,166 @@
|
||||
// File: leave_type_repo.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import '../../../../../Const/api_config.dart';
|
||||
import '../../../../../http_client/custome_http_client.dart';
|
||||
import '../../../../../http_client/customer_http_client_get.dart';
|
||||
import '../model/leave_type_list_model.dart';
|
||||
import '../provider/leave_type_list_provider.dart';
|
||||
|
||||
class LeaveTypeRepo {
|
||||
static const String _endpoint = '/leave-types';
|
||||
|
||||
///---------------- FETCH ALL LEAVE TYPES ----------------///
|
||||
Future<LeaveTypeListModel> fetchAllLeaveTypes() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
return LeaveTypeListModel.fromJson(parsedData);
|
||||
} else {
|
||||
throw Exception('Failed to fetch leave types list. Status: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- CREATE LEAVE TYPE ----------------///
|
||||
Future<void> createLeaveType({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required String name,
|
||||
required String description,
|
||||
required num status, // 1 for Active, 0 for Inactive
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
'name': name,
|
||||
'description': description,
|
||||
'status': status,
|
||||
});
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Creating...');
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
addContentTypeInHeader: true,
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (responseData.statusCode == 200 || responseData.statusCode == 201) {
|
||||
ref.refresh(leaveTypeListProvider);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Leave Type created successfully')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Creation failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- UPDATE LEAVE TYPE ----------------///
|
||||
Future<void> updateLeaveType({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num id,
|
||||
required String name,
|
||||
required String description,
|
||||
required num status,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$id');
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
'_method': 'put',
|
||||
'name': name,
|
||||
'description': description,
|
||||
'status': status,
|
||||
});
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Updating...');
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
addContentTypeInHeader: true,
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
ref.refresh(leaveTypeListProvider);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Leave Type updated successfully')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Update failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- DELETE LEAVE TYPE ----------------///
|
||||
Future<bool> deleteLeaveType({
|
||||
required num id,
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
}) async {
|
||||
try {
|
||||
EasyLoading.show(status: 'Deleting...');
|
||||
final url = Uri.parse('${APIConfig.url}$_endpoint/$id');
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(ref: ref, context: context, client: http.Client());
|
||||
final response = await customHttpClient.delete(url: url);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ref.refresh(leaveTypeListProvider);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Leave Type deleted successfully')),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Deletion failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred during deletion: $error')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user