first commit
This commit is contained in:
364
lib/Screens/hrm/attendance/add_new_attendance.dart
Normal file
364
lib/Screens/hrm/attendance/add_new_attendance.dart
Normal file
@@ -0,0 +1,364 @@
|
||||
// File: add_new_attendance.dart (Modified)
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/attendance/repo/attendence_repo.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
// --- Local Imports ---
|
||||
import '../../../constant.dart';
|
||||
import '../employee/model/employee_list_model.dart' as employee;
|
||||
import '../widgets/set_time.dart'; // setTime function import
|
||||
|
||||
// --- Data Layer Imports ---
|
||||
import 'package:mobile_pos/Screens/hrm/employee/provider/emplpyee_list_provider.dart';
|
||||
// *** CORRECT SHIFT IMPORTS ***
|
||||
import 'package:mobile_pos/Screens/hrm/shift/Model/shift_list_model.dart' as shift;
|
||||
|
||||
import 'model/attendence_list_model.dart';
|
||||
|
||||
class AddNewAttendance extends ConsumerStatefulWidget {
|
||||
final AttendanceData? attendanceData;
|
||||
|
||||
const AddNewAttendance({super.key, this.attendanceData});
|
||||
|
||||
@override
|
||||
ConsumerState<AddNewAttendance> createState() => _AddNewAttendanceState();
|
||||
}
|
||||
|
||||
class _AddNewAttendanceState extends ConsumerState<AddNewAttendance> {
|
||||
// --- Form Controllers ---
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
final dateController = TextEditingController();
|
||||
final shiftController = TextEditingController();
|
||||
final timeInController = TextEditingController();
|
||||
final timeOutController = TextEditingController();
|
||||
final noteController = TextEditingController();
|
||||
|
||||
// --- Selected Values (API payload) ---
|
||||
employee.EmployeeData? _selectedEmployee;
|
||||
DateTime? _selectedDate;
|
||||
String? _selectedMonth;
|
||||
|
||||
// --- UI/API Helpers ---
|
||||
final DateFormat _displayDateFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd');
|
||||
final DateFormat _apiTimeFormat = DateFormat('HH:mm');
|
||||
final DateFormat _monthFormat = DateFormat('MMMM');
|
||||
|
||||
bool get isEditing => widget.attendanceData != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (isEditing) {
|
||||
final data = widget.attendanceData!;
|
||||
noteController.text = data.note ?? '';
|
||||
|
||||
try {
|
||||
if (data.date != null) {
|
||||
_selectedDate = DateTime.parse(data.date!);
|
||||
dateController.text = _displayDateFormat.format(_selectedDate!);
|
||||
_selectedMonth = data.month;
|
||||
}
|
||||
timeInController.text = data.timeIn ?? '';
|
||||
timeOutController.text = data.timeOut ?? '';
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing dates/times for editing: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
dateController.dispose();
|
||||
timeOutController.dispose();
|
||||
timeInController.dispose();
|
||||
noteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
setState(() {
|
||||
if (picked != null) {
|
||||
_selectedDate = picked;
|
||||
dateController.text = _displayDateFormat.format(picked);
|
||||
_selectedMonth = _monthFormat.format(picked).toLowerCase();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String? _convertDisplayTimeToAPI(String displayTime) {
|
||||
try {
|
||||
final dateTime = DateFormat('hh:mm a').parse(displayTime);
|
||||
return _apiTimeFormat.format(dateTime);
|
||||
} catch (e) {
|
||||
debugPrint('Time conversion error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
String? _formatDateForAPI(String dateDisplay) {
|
||||
// Converts display format (dd/MM/yyyy) to API format (YYYY-MM-DD)
|
||||
if (dateDisplay.isEmpty) return null;
|
||||
try {
|
||||
final dateTime = DateFormat('dd/MM/yyyy').parse(dateDisplay);
|
||||
return DateFormat('yyyy-MM-dd').format(dateTime);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (_key.currentState!.validate() && _selectedEmployee != null) {
|
||||
final repo = AttendanceRepo();
|
||||
|
||||
final apiTimeIn = _convertDisplayTimeToAPI(timeInController.text);
|
||||
final apiTimeOut = _convertDisplayTimeToAPI(timeOutController.text);
|
||||
|
||||
if (apiTimeIn == null || apiTimeOut == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Error converting time format.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final payload = {
|
||||
'employee_id': _selectedEmployee!.id!,
|
||||
'shift_id': _selectedEmployee!.shiftId!,
|
||||
'time_in': (apiTimeIn),
|
||||
'time_out': apiTimeOut,
|
||||
'date': _apiDateFormat.format(_selectedDate!),
|
||||
// 'month': _selectedMonth!,
|
||||
'note': noteController.text,
|
||||
};
|
||||
if (isEditing) {
|
||||
await repo.updateAttendance(
|
||||
ref: ref,
|
||||
context: context,
|
||||
id: widget.attendanceData!.id!,
|
||||
employeeId: payload['employee_id'] as num,
|
||||
shiftId: payload['shift_id'] as num,
|
||||
timeIn: payload['time_in'] as String,
|
||||
timeOut: payload['time_out'] as String,
|
||||
date: payload['date'] as String,
|
||||
// month: payload['month'] as String,
|
||||
note: payload['note'] as String?,
|
||||
);
|
||||
} else {
|
||||
await repo.createAttendance(
|
||||
ref: ref,
|
||||
context: context,
|
||||
employeeId: payload['employee_id'] as num,
|
||||
shiftId: payload['shift_id'] as num,
|
||||
timeIn: payload['time_in'] as String,
|
||||
timeOut: payload['time_out'] as String,
|
||||
date: payload['date'] as String,
|
||||
// month: payload['month'] as String,
|
||||
note: payload['note'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _resetForm() {
|
||||
if (!isEditing) {
|
||||
setState(() {
|
||||
_key.currentState?.reset();
|
||||
dateController.clear();
|
||||
shiftController.clear();
|
||||
timeInController.clear();
|
||||
timeOutController.clear();
|
||||
noteController.clear();
|
||||
_selectedEmployee = null;
|
||||
_selectedMonth = null;
|
||||
_selectedDate = null;
|
||||
});
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Watch required providers
|
||||
final _lang = lang.S.of(context);
|
||||
final employeesAsync = ref.watch(employeeListProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(isEditing ? _lang.editAttendance : _lang.addNewAttendance),
|
||||
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) {
|
||||
final employees = employeeModel.employees ?? [];
|
||||
|
||||
if (isEditing) {
|
||||
final data = widget.attendanceData!;
|
||||
|
||||
_selectedEmployee ??= employees.firstWhere(
|
||||
(e) => e.id == data.employeeId,
|
||||
orElse: () => _selectedEmployee ?? employees.first,
|
||||
);
|
||||
shiftController.text = _selectedEmployee?.shift?.name ?? '';
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildEmployeeDropdown(employees),
|
||||
const SizedBox(height: 20),
|
||||
_buildShiftDropdown(), // Pass actual shift data
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildMonthDropdown()),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildDateInput()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTimeInput(true, context)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildTimeInput(false, context)),
|
||||
],
|
||||
),
|
||||
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<employee.EmployeeData> employees) {
|
||||
final _lang = lang.S.of(context);
|
||||
return DropdownButtonFormField<employee.EmployeeData>(
|
||||
value: _selectedEmployee,
|
||||
icon: const Icon(Icons.keyboard_arrow_down, color: kNeutral800),
|
||||
decoration: InputDecoration(labelText: _lang.employee, hintText: _lang.selectOne),
|
||||
validator: (value) => value == null ? _lang.pleaseSelectAnEmployee : null,
|
||||
items: employees.map((entry) {
|
||||
return DropdownMenuItem<employee.EmployeeData>(
|
||||
value: entry,
|
||||
child: Text(entry.name ?? 'N/A'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (employee.EmployeeData? value) {
|
||||
setState(() {
|
||||
_selectedEmployee = value;
|
||||
shiftController.text = _selectedEmployee?.shift?.name ?? '';
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// *** SHIFT DROPDOWN USING ShiftData ***
|
||||
Widget _buildShiftDropdown() {
|
||||
return TextFormField(
|
||||
controller: shiftController,
|
||||
readOnly: true,
|
||||
decoration:
|
||||
InputDecoration(labelText: lang.S.of(context).shift, hintText: lang.S.of(context).selectEmployeeFirst),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthDropdown() {
|
||||
final monthDisplay = _selectedMonth != null
|
||||
? _selectedMonth![0].toUpperCase() + _selectedMonth!.substring(1)
|
||||
: lang.S.of(context).selectDateFirst;
|
||||
|
||||
return TextFormField(
|
||||
// initialValue: monthDisplay,
|
||||
controller: TextEditingController(text: monthDisplay),
|
||||
readOnly: true,
|
||||
// icon: const Icon(Icons.keyboard_arrow_down, color: kNeutral800),
|
||||
decoration: InputDecoration(labelText: lang.S.of(context).month, hintText: lang.S.of(context).autoSelected),
|
||||
validator: (value) => _selectedDate == null ? lang.S.of(context).pleaseSelectDate : null,
|
||||
// items: [
|
||||
// DropdownMenuItem(value: monthDisplay, child: Text(monthDisplay)),
|
||||
// ],
|
||||
onChanged: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput() {
|
||||
return TextFormField(
|
||||
keyboardType: TextInputType.name,
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).date,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () => _selectDate(context),
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? lang.S.of(context).pleaseSelectDate : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeInput(bool isTimeIn, BuildContext context) {
|
||||
final controller = isTimeIn ? timeInController : timeOutController;
|
||||
final label = isTimeIn ? lang.S.of(context).timeIn : lang.S.of(context).timeOut;
|
||||
|
||||
return TextFormField(
|
||||
onTap: () => setTime(controller, context),
|
||||
readOnly: true,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: isTimeIn ? '09:00 AM' : '05:00 PM',
|
||||
suffixIcon: Icon(
|
||||
AntDesign.clock_circle_outline,
|
||||
size: 18,
|
||||
color: kNeutral800,
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? '${lang.S.of(context).selectDate} $label' : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
450
lib/Screens/hrm/attendance/attendance_screen.dart
Normal file
450
lib/Screens/hrm/attendance/attendance_screen.dart
Normal file
@@ -0,0 +1,450 @@
|
||||
// File: attendance_screen.dart (Final Code)
|
||||
|
||||
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/attendance/add_new_attendance.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/attendance/provider/attendence_provider.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/attendance/repo/attendence_repo.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/employee/provider/emplpyee_list_provider.dart';
|
||||
|
||||
import 'model/attendence_list_model.dart';
|
||||
|
||||
class AttendanceScreen extends ConsumerStatefulWidget {
|
||||
const AttendanceScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<AttendanceScreen> createState() => _AttendanceScreenState();
|
||||
}
|
||||
|
||||
class _AttendanceScreenState extends ConsumerState<AttendanceScreen> {
|
||||
// --- Filter State ---
|
||||
String? _selectedEmployeeFilter;
|
||||
String? _selectedTimeFilter;
|
||||
|
||||
List<AttendanceData> _filteredList = [];
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
final List<String> _timeFilters = ['Today', 'Weekly', 'Monthly', 'Yearly'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(_applyFilters);
|
||||
_selectedEmployeeFilter = 'All Employee';
|
||||
_selectedTimeFilter = 'Today';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.removeListener(_applyFilters);
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- Filtering Logic ---
|
||||
|
||||
void _applyFilters() {
|
||||
setState(() {}); // Trigger rebuild to re-run filtering
|
||||
}
|
||||
|
||||
void _filterAttendance(List<AttendanceData> allAttendance) {
|
||||
_filteredList = allAttendance.where((att) {
|
||||
final name = (att.employee?.name ?? '').toLowerCase();
|
||||
|
||||
// 1. Employee Filter
|
||||
final employeeNameMatches =
|
||||
_selectedEmployeeFilter == 'All Employee' || name == _selectedEmployeeFilter!.toLowerCase();
|
||||
|
||||
// 2. Time Filter (Simplified date logic)
|
||||
bool timeMatches = true;
|
||||
if (_selectedTimeFilter == 'Today') {
|
||||
final today = DateFormat('yyyy-MM-dd').format(DateTime.now());
|
||||
timeMatches = att.date == today;
|
||||
} else {
|
||||
// Show all for other filter types, as full date range logic is complex
|
||||
timeMatches = true;
|
||||
}
|
||||
|
||||
return employeeNameMatches && timeMatches;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// --- Utility Functions ---
|
||||
|
||||
String _formatTimeForDisplay(String? time) {
|
||||
if (time == null) return 'N/A';
|
||||
try {
|
||||
final dateTime = DateFormat('HH:mm:ss').parse(time);
|
||||
return DateFormat('hh:mm a').format(dateTime);
|
||||
} catch (_) {
|
||||
return time;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDateForDisplay(String? date) {
|
||||
if (date == null) return 'N/A';
|
||||
try {
|
||||
final dateTime = DateFormat('yyyy-MM-dd').parse(date);
|
||||
return DateFormat('dd MMM yyyy').format(dateTime);
|
||||
} catch (_) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 = AttendanceRepo();
|
||||
await repo.deleteAttendance(id: id, context: context, ref: ref);
|
||||
ref.invalidate(attendanceListProvider); // Force list refresh
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pull to Refresh ---
|
||||
|
||||
Future<void> _refreshData() async {
|
||||
ref.invalidate(attendanceListProvider);
|
||||
return ref.watch(attendanceListProvider.future);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = lang.S.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final attendanceAsync = ref.watch(attendanceListProvider);
|
||||
final employeesAsync = ref.watch(employeeListProvider); // Employee List for filter data
|
||||
final permissionService = PermissionService(ref);
|
||||
|
||||
// Combine data fetching results for UI
|
||||
final combinedAsync = attendanceAsync.asData != null && employeesAsync.asData != null
|
||||
? AsyncValue.data(true)
|
||||
: attendanceAsync.hasError || employeesAsync.hasError
|
||||
? AsyncValue.error(attendanceAsync.error ?? employeesAsync.error!, StackTrace.current)
|
||||
: const AsyncValue.loading();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(_lang.attendance),
|
||||
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 FIX: Accessing Employee List Data via .data
|
||||
...(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),
|
||||
// Time Filter Dropdown
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: FilterDropdownButton<String>(
|
||||
buttonDecoration: BoxDecoration(
|
||||
color: kBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(color: kBorderColor),
|
||||
),
|
||||
value: _selectedTimeFilter,
|
||||
items: _timeFilters.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(() {
|
||||
_selectedTimeFilter = 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: (_) {
|
||||
if (!permissionService.hasPermission(Permit.attendancesRead.value)) {
|
||||
return const Center(child: PermitDenyWidget());
|
||||
}
|
||||
// Data is loaded, apply filter
|
||||
_filterAttendance(attendanceAsync.value?.data ?? []);
|
||||
|
||||
if (_filteredList.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: Text(_lang.noAvailableRecordFound),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _filteredList.length,
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
color: kBackgroundColor,
|
||||
height: 1.5,
|
||||
),
|
||||
itemBuilder: (_, index) => _buildAttendanceItem(
|
||||
context: context,
|
||||
ref: ref,
|
||||
attendance: _filteredList[index],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
bottomNavigationBar: permissionService.hasPermission(Permit.attendancesCreate.value)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const AddNewAttendance()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
label: Text(_lang.addAttendance),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// --- List Item Builder ---
|
||||
|
||||
Widget _buildAttendanceItem({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required AttendanceData attendance,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
String timeInDisplay = _formatTimeForDisplay(attendance.timeIn);
|
||||
String timeOutDisplay = _formatTimeForDisplay(attendance.timeOut);
|
||||
String dateDisplay = _formatDateForDisplay(attendance.date);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => viewModalSheet(
|
||||
context: context,
|
||||
item: {
|
||||
"Employee": attendance.employee?.name ?? 'N/A',
|
||||
"Shift": attendance.shift?.name ?? 'N/A',
|
||||
"Month": attendance.month ?? 'N/A',
|
||||
"Date": dateDisplay,
|
||||
"Time In": timeInDisplay,
|
||||
"Time Out": timeOutDisplay,
|
||||
},
|
||||
description: attendance.note ?? lang.S.of(context).noNoteProvided,
|
||||
),
|
||||
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(
|
||||
attendance.employee?.name ?? 'N/A Employee',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
dateDisplay,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: kNeutral800),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildActionButtons(
|
||||
context,
|
||||
ref,
|
||||
attendance.id,
|
||||
attendance.employee?.name ?? lang.S.of(context).attendance,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildTimeColumn(
|
||||
time: timeInDisplay,
|
||||
label: lang.S.of(context).timeIn,
|
||||
theme: theme,
|
||||
),
|
||||
_buildTimeColumn(
|
||||
time: timeOutDisplay,
|
||||
label: lang.S.of(context).timeOut,
|
||||
theme: theme,
|
||||
),
|
||||
_buildTimeColumn(
|
||||
time: attendance.duration ?? 'N/A',
|
||||
label: lang.S.of(context).duration,
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeColumn({
|
||||
required String time,
|
||||
required String label,
|
||||
required ThemeData theme,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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, num? id, String name) {
|
||||
final permissionService = PermissionService(ref);
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (!permissionService.hasPermission(Permit.attendancesUpdate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).youDoNotHavePermissionToViewAttendance),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (id != null) {
|
||||
final attendanceData = _filteredList.firstWhere((a) => a.id == id);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddNewAttendance(attendanceData: attendanceData),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPencilEdit02,
|
||||
color: kSuccessColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (!permissionService.hasPermission(Permit.attendancesDelete.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).youDoNotHavePermissionToViewAttendance),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (id != null) {
|
||||
_showDeleteConfirmationDialog(context, ref, id, name);
|
||||
}
|
||||
},
|
||||
child: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete03,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/Screens/hrm/attendance/model/attendence_list_model.dart
Normal file
113
lib/Screens/hrm/attendance/model/attendence_list_model.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
// File: attendance_model.dart
|
||||
|
||||
class AttendanceListModel {
|
||||
AttendanceListModel({
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
AttendanceListModel.fromJson(dynamic json) {
|
||||
message = json['message'];
|
||||
if (json['data'] != null) {
|
||||
data = [];
|
||||
json['data'].forEach((v) {
|
||||
data?.add(AttendanceData.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? message;
|
||||
List<AttendanceData>? data;
|
||||
}
|
||||
|
||||
class AttendanceData {
|
||||
AttendanceData({
|
||||
this.id,
|
||||
this.businessId,
|
||||
this.branchId,
|
||||
this.employeeId,
|
||||
this.shiftId,
|
||||
this.timeIn,
|
||||
this.timeOut,
|
||||
this.date,
|
||||
this.duration,
|
||||
this.month,
|
||||
this.note,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.employee,
|
||||
this.shift,
|
||||
});
|
||||
|
||||
AttendanceData.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
businessId = json['business_id'];
|
||||
branchId = json['branch_id'];
|
||||
employeeId = json['employee_id'];
|
||||
shiftId = json['shift_id'];
|
||||
timeIn = json['time_in'];
|
||||
timeOut = json['time_out'];
|
||||
date = json['date'];
|
||||
duration = json['duration'];
|
||||
month = json['month'];
|
||||
note = json['note'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
employee = json['employee'] != null ? Employee.fromJson(json['employee']) : null;
|
||||
shift = json['shift'] != null ? Shift.fromJson(json['shift']) : null;
|
||||
}
|
||||
num? id;
|
||||
num? businessId;
|
||||
dynamic branchId;
|
||||
num? employeeId;
|
||||
num? shiftId;
|
||||
String? timeIn;
|
||||
String? timeOut;
|
||||
String? date;
|
||||
String? duration;
|
||||
String? month;
|
||||
String? note;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
Employee? employee;
|
||||
Shift? shift;
|
||||
}
|
||||
|
||||
class Employee {
|
||||
Employee({this.id, this.name});
|
||||
Employee.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
}
|
||||
|
||||
class Shift {
|
||||
Shift({this.id, this.name});
|
||||
Shift.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
}
|
||||
|
||||
// Assume EmployeeData model exists elsewhere for provider list consumption
|
||||
class EmployeeData {
|
||||
num? id;
|
||||
String? name;
|
||||
EmployeeData.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
}
|
||||
}
|
||||
|
||||
// Assume ShiftData model exists elsewhere for provider list consumption
|
||||
class ShiftData {
|
||||
num? id;
|
||||
String? name;
|
||||
ShiftData.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../model/attendence_list_model.dart';
|
||||
import '../repo/attendence_repo.dart';
|
||||
|
||||
final repo = AttendanceRepo();
|
||||
final attendanceListProvider = FutureProvider<AttendanceListModel>((ref) => repo.fetchAllAttendance());
|
||||
182
lib/Screens/hrm/attendance/repo/attendence_repo.dart
Normal file
182
lib/Screens/hrm/attendance/repo/attendence_repo.dart
Normal file
@@ -0,0 +1,182 @@
|
||||
// File: attendance_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/attendence_list_model.dart';
|
||||
import '../provider/attendence_provider.dart';
|
||||
|
||||
class AttendanceRepo {
|
||||
static const String _endpoint = '/attendances'; // Assuming a suitable endpoint
|
||||
|
||||
///---------------- FETCH ALL ATTENDANCE (GET) ----------------///
|
||||
Future<AttendanceListModel> fetchAllAttendance() 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 AttendanceListModel.fromJson(parsedData);
|
||||
} else {
|
||||
throw Exception('Failed to fetch attendance list. Status: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- CREATE ATTENDANCE (POST) ----------------///
|
||||
Future<void> createAttendance({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num employeeId,
|
||||
required num shiftId,
|
||||
required String timeIn, // HH:MM:SS format
|
||||
required String timeOut, // HH:MM:SS format
|
||||
required String date, // YYYY-MM-DD format
|
||||
// required String month,
|
||||
String? note,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
'employee_id': employeeId,
|
||||
'shift_id': shiftId,
|
||||
'time_in': timeIn,
|
||||
'time_out': timeOut,
|
||||
'date': date,
|
||||
// 'month': month,
|
||||
'note': note,
|
||||
});
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Recording Attendance...');
|
||||
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.invalidate(attendanceListProvider); // Refresh the list
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Attendance recorded successfully')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Recording failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- UPDATE ATTENDANCE (PUT) ----------------///
|
||||
Future<void> updateAttendance({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num id,
|
||||
required num employeeId,
|
||||
required num shiftId,
|
||||
required String timeIn,
|
||||
required String timeOut,
|
||||
required String date,
|
||||
// required String month,
|
||||
String? note,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$id');
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
'_method': 'put',
|
||||
'employee_id': employeeId,
|
||||
'shift_id': shiftId,
|
||||
'time_in': timeIn,
|
||||
'time_out': timeOut,
|
||||
'date': date,
|
||||
// 'month': month,
|
||||
'note': note,
|
||||
});
|
||||
|
||||
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.invalidate(attendanceListProvider); // Refresh the list
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Attendance 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 ATTENDANCE ----------------///
|
||||
Future<bool> deleteAttendance({
|
||||
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.invalidate(attendanceListProvider); // Refresh the list
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Attendance 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