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

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

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

View File

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

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

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

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

View File

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

View File

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

View File

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