653 lines
21 KiB
Dart
653 lines
21 KiB
Dart
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:http/http.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../../../../core/routing/routes.dart';
|
|
import '../../../../core/services/api_service.dart';
|
|
import '../../../../data/utils/Pagination.dart';
|
|
import '../../../data/utils/validators.dart';
|
|
import '../../layouts/main_layout.dart';
|
|
import '../../layouts/responsive_layout.dart';
|
|
import '../../providers/manager_provider.dart';
|
|
import '../../themes/indicators/date_field_theme.dart';
|
|
import '../../themes/indicators/export_btn.dart';
|
|
import '../../themes/indicators/month_field_theme.dart';
|
|
import '../../themes/indicators/search_field_theme.dart';
|
|
import '../../widgets/custom_Stdate_EnDate_Filter.dart';
|
|
import '../../widgets/custom_action_popup.dart';
|
|
import '../../widgets/custom_month_filter.dart';
|
|
|
|
class IndividualAttendanceDetails extends ConsumerStatefulWidget {
|
|
const IndividualAttendanceDetails({super.key});
|
|
@override
|
|
ConsumerState<IndividualAttendanceDetails> createState() =>
|
|
IndividualAttendanceDetailsState();
|
|
}
|
|
|
|
class IndividualAttendanceDetailsState
|
|
extends ConsumerState<IndividualAttendanceDetails> {
|
|
int currentPage = 1;
|
|
int itemsPerPage = 10;
|
|
late ApiService apiService;
|
|
dynamic staffId;
|
|
dynamic staffName;
|
|
dynamic userId;
|
|
final _formKey = GlobalKey<FormState>();
|
|
// final monthController = TextEditingController();
|
|
final _monthFormKey = GlobalKey<FormState>();
|
|
|
|
// List<Map<String, dynamic>> dataVal = [];
|
|
List<Map<String, dynamic>> getStaffData = [];
|
|
List<Map<String, dynamic>> originalData = [];
|
|
List<Map<String, dynamic>> filteredData = [];
|
|
|
|
bool isLoading = false;
|
|
Map<String, TextEditingController> controllers = {};
|
|
List<String> tabHeader = ['startDate', 'endDate', 'month'];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
apiService = ApiService();
|
|
for (String field in tabHeader) {
|
|
controllers[field] = TextEditingController();
|
|
}
|
|
|
|
Future.microtask(() {
|
|
final id = ref.read(managerIdProvider);
|
|
staffId = ref.read(staffIndiviualAttendanceIdProvider);
|
|
staffName = ref.read(staffIndiviualAttendanceNameIdProvider);
|
|
userId = ref.read(userIdProvider);
|
|
|
|
print(
|
|
"A56 => r : $staffId | mId: $id | uId: $userId |staffName : $staffName",
|
|
);
|
|
|
|
// Staff - sanjeev.p@venbainfotech.com - T E S T S - 9 evarukku 8th id is manager
|
|
// Manager - venbalap08@gmail.com - T E S T M - 8 evarukku 8th id is manager
|
|
// Agent - venba2026@gmail.com - T E S T A - 5 evarkku 8th id is manager
|
|
// so here i used userId
|
|
|
|
if (staffId != null && staffName != null) {
|
|
getStaffList(userId, staffId);
|
|
}
|
|
});
|
|
}
|
|
|
|
void filterDateRange1() {
|
|
print("filterDateRange");
|
|
// if (!_formKey.currentState!.validate()) return;
|
|
|
|
// final fromDate = controllers['startDate']?.text ?? '';
|
|
// final toDate = controllers['endDate']?.text ?? '';
|
|
print("filterDateRange1");
|
|
// Call with selected dates
|
|
getStaffList(userId, staffId);
|
|
}
|
|
|
|
void filterMonth() {
|
|
print('Month');
|
|
// ✅ Validate the form first
|
|
// if (!_formKey.currentState!.validate()) {
|
|
// // stop execution if validation fails
|
|
// return;
|
|
// }
|
|
|
|
final fromDateText = controllers['month']?.text ?? '';
|
|
print('monthmonth - $fromDateText');
|
|
|
|
if (fromDateText.isEmpty) {
|
|
print('No month selected');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Convert "10-2025" → "2025-10"
|
|
// final parsedDate = DateFormat('MM-yyyy').parse(fromDateText);
|
|
// final apiMonth = DateFormat('yyyy-MM').format(parsedDate);
|
|
|
|
// Parse "Nov 2025" → DateTime
|
|
final parsedDate = DateFormat('MMM yyyy').parse(fromDateText);
|
|
final apiMonth = DateFormat('yyyy-MM').format(parsedDate);
|
|
|
|
print('Converted month for API: $apiMonth');
|
|
|
|
// ✅ Call your API with the converted format
|
|
getStaffList(userId, staffId, month: apiMonth);
|
|
} catch (e) {
|
|
print('Error parsing month: $e');
|
|
}
|
|
}
|
|
|
|
void refrshfilterDateRange() {
|
|
setState(() {
|
|
controllers['month']!.clear();
|
|
controllers['month']?.text = '';
|
|
|
|
// Reset the FormField validation
|
|
_formKey.currentState?.reset();
|
|
_monthFormKey.currentState?.reset();
|
|
});
|
|
getStaffList(userId, staffId);
|
|
}
|
|
|
|
Future<void> getStaffList(int managerId, role, {String month = ''}) async {
|
|
final monthForApi = month.isNotEmpty
|
|
? month
|
|
: DateFormat('yyyy-MM').format(DateTime.now());
|
|
|
|
print('A72 => Fns called => $managerId | $role | month: $monthForApi');
|
|
setState(() {
|
|
isLoading = true;
|
|
});
|
|
|
|
try {
|
|
final response = await apiService.fetchAttndanceOfIndiviualStaffList(
|
|
month: monthForApi,
|
|
id: staffId,
|
|
// toDate: controllers['endDate']?.text ?? '',
|
|
);
|
|
|
|
if (response['status'] == 'success') {
|
|
final data = response['data'];
|
|
final fromDate = response['month'] ?? '';
|
|
|
|
print('FromDate : $fromDate');
|
|
print('Frdata : $data');
|
|
|
|
setState(() {
|
|
print('test 1');
|
|
|
|
if (fromDate != null && fromDate.isNotEmpty) {
|
|
// Parse the string to DateTime first
|
|
// final parsedDate = DateFormat('yyyy-MM').parse(fromDate);
|
|
// final monthText = DateFormat('MM-yyyy').format(parsedDate);
|
|
|
|
// Parse the string "2025-10" to a DateTime
|
|
final parsedDate = DateFormat('yyyy-MM').parse(fromDate);
|
|
// Convert it to "Oct 2025"
|
|
final monthText = DateFormat('MMM yyyy').format(parsedDate);
|
|
controllers['month']?.text = monthText;
|
|
}
|
|
|
|
print('test 2');
|
|
getStaffData = (data as List)
|
|
.map<Map<String, dynamic>>(
|
|
(item) => Map<String, dynamic>.from(item),
|
|
)
|
|
.toList();
|
|
print('test 3');
|
|
originalData = getStaffData;
|
|
filteredData = List.from(originalData);
|
|
print('testfilteredData $filteredData');
|
|
});
|
|
} else {
|
|
getStaffData = [];
|
|
originalData = [];
|
|
}
|
|
} catch (e) {
|
|
print('Exception occurred: $e');
|
|
} finally {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
List<dynamic> get _paginatedData {
|
|
print('test 31');
|
|
// Sort descending by id first
|
|
final sortedData = [...filteredData]
|
|
..sort((a, b) => int.parse(b['id']) - int.parse(a['id']));
|
|
|
|
print('test 311- $sortedData');
|
|
print('test 311');
|
|
if (sortedData.isEmpty) return [];
|
|
|
|
// Ensure currentPage is valid
|
|
final maxPage = (sortedData.length / itemsPerPage).ceil();
|
|
final safePage = currentPage.clamp(1, maxPage);
|
|
|
|
final startIndex = (safePage - 1) * itemsPerPage;
|
|
final endIndex = (startIndex + itemsPerPage).clamp(0, sortedData.length);
|
|
|
|
return sortedData.sublist(startIndex, endIndex);
|
|
}
|
|
|
|
Future<void> handleEdit(item) async {
|
|
// Navigator.pop(context);
|
|
print('EDITStaff - ${item['id']}');
|
|
// dynamic id = data['id'];
|
|
// context.go('/tabEnquiry/$id');
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
// ✅ Remove old value (if any)
|
|
await prefs.remove('enqAgentDataId');
|
|
|
|
// setState(() {
|
|
final id = item['id'].toString();
|
|
// ✅ Save the new id
|
|
await prefs.setString('enqAgentDataId', id.toString());
|
|
ref.read(enquiryIdProvider.notifier).state = id;
|
|
|
|
// });
|
|
// update provider
|
|
|
|
context.go(AppRoutes.tabEnquiry);
|
|
}
|
|
|
|
void filterData(String query) {
|
|
print("FilterDAta - $query");
|
|
setState(() {
|
|
print('test 31f');
|
|
filteredData = getStaffData.where((item) {
|
|
// final isActiveStatus = item['is_active'] == "1" ? "active" : "inactive";
|
|
|
|
return (item['date'] ?? '-').toLowerCase().contains(
|
|
query.toLowerCase(),
|
|
) ||
|
|
(item['name'] ?? '-').toLowerCase().contains(query.toLowerCase()) ||
|
|
(item['login_time'] ?? '-').toLowerCase().contains(
|
|
query.toLowerCase(),
|
|
) ||
|
|
(item['logout_time'] ?? '-').toLowerCase().contains(
|
|
query.toLowerCase(),
|
|
) ||
|
|
(item['no_of_logged_in_time'] ?? '-').toLowerCase().contains(
|
|
query.toLowerCase(),
|
|
);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
final TextEditingController _searchStaffController = TextEditingController();
|
|
|
|
String _formatDate(String rawDate) {
|
|
try {
|
|
final dateTime = DateTime.parse(rawDate);
|
|
return DateFormat('dd-MM-yyyy HH:mm').format(dateTime); // 24-hour format
|
|
} catch (e) {
|
|
return rawDate; // fallback if parsing fails
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MainLayout(
|
|
title: "Staff Attendance",
|
|
body: SelectionArea(
|
|
child: Container(
|
|
// color: Colors.yellow.shade50,
|
|
width: MediaQuery.of(context).size.width,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 30,
|
|
width: MediaQuery.of(context).size.width,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
context.go(AppRoutes.allStaffAttendance);
|
|
},
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
Tooltip(
|
|
message: 'Back',
|
|
child: IconButton(
|
|
icon: Icon(
|
|
Icons.arrow_left_sharp,
|
|
size: ResponsiveLayout.isMobile(context) ? 25 : 25,
|
|
color: const Color(0xFF425B5B),
|
|
),
|
|
onPressed: () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove('attendanceIndvStaff');
|
|
await prefs.remove('attendanceIndvStaffName');
|
|
context.go(AppRoutes.allStaffAttendance);
|
|
},
|
|
splashRadius: 18,
|
|
hoverColor: Colors.black12,
|
|
padding: const EdgeInsets.all(4),
|
|
constraints: const BoxConstraints(),
|
|
),
|
|
),
|
|
const SizedBox(width: 5), // spacing between icon and text
|
|
Text(
|
|
"Attendance",
|
|
style: GoogleFonts.poppins(
|
|
fontSize: ResponsiveLayout.isMobile(context) ? 11 : 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: ResponsiveLayout.isMobile(context) ? 5 : 5),
|
|
Expanded(
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width,
|
|
padding: EdgeInsets.all(8.0),
|
|
child: _buildContent(context),
|
|
),
|
|
),
|
|
|
|
Container(
|
|
// height: 20,
|
|
width: MediaQuery.of(context).size.width,
|
|
// color: Colors.green.shade50,
|
|
child: PaginationControls(
|
|
currentPage: currentPage,
|
|
itemsPerPage: itemsPerPage,
|
|
// totalItems: dataVal.length,
|
|
totalItems: filteredData.length,
|
|
// activeColor: layoutColor, // your theme color
|
|
onPageChanged: (page) {
|
|
setState(() {
|
|
currentPage = page;
|
|
});
|
|
},
|
|
onItemsPerPageChanged: (items) {
|
|
setState(() {
|
|
itemsPerPage = items;
|
|
currentPage = 1;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),),
|
|
);
|
|
}
|
|
|
|
Widget _buildContent(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
Container(
|
|
// height: 40,
|
|
// color: Colors.pink,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
MonthFilterRow(
|
|
monthController: controllers['month']!,
|
|
formKey: _monthFormKey,
|
|
onFilter: () {
|
|
print(
|
|
'Filter triggered with month: ${controllers['month']!.text}',
|
|
);
|
|
filterMonth();
|
|
},
|
|
onRefresh: () {
|
|
controllers['month']!.clear();
|
|
refrshfilterDateRange();
|
|
print('Month filter cleared');
|
|
},
|
|
),
|
|
|
|
Spacer(),
|
|
|
|
ThemedSearchField(
|
|
hintText: 'Search',
|
|
backgroundColor: Color(0xFFFFFFFF),
|
|
txtHeight: 30,
|
|
// backgroundColor: Color(0xFFF6F8F8),
|
|
onChanged: filterData,
|
|
controller: _searchStaffController,
|
|
txtwidth: ResponsiveLayout.isMobile(context)
|
|
? MediaQuery.of(context).size.width * 0.6
|
|
: MediaQuery.of(context).size.width * 0.15,
|
|
),
|
|
ResponsiveLayout.isMobile(context)
|
|
? Spacer()
|
|
: SizedBox(width: 10),
|
|
|
|
ExportBtn(
|
|
sheetName: "Staff Individual Attendance",
|
|
fileName: "staff_individual_attendance_list",
|
|
data: filteredData,
|
|
txt: !ResponsiveLayout.isMobile(context) ? true : false,
|
|
displayHeaders: [
|
|
"S.No.",
|
|
"Name",
|
|
"Date",
|
|
"Login",
|
|
"Logout",
|
|
"Number Of Hours",
|
|
],
|
|
keys: [
|
|
"sno", // handled internally as i + 1
|
|
"name",
|
|
"date",
|
|
"login_time",
|
|
"logout_time",
|
|
'no_of_logged_in_time',
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 10),
|
|
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Color(0xFFF1F5F9),
|
|
// color: Color(0xFFEDF6F5),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
Expanded(flex: 1, child: Text('S.No.', style: _headerStyle)),
|
|
Expanded(flex: 2, child: Text('Name', style: _headerStyle)),
|
|
Expanded(flex: 2, child: Text('Date', style: _headerStyle)),
|
|
|
|
Expanded(flex: 2, child: Text('Login', style: _headerStyle)),
|
|
Expanded(flex: 2, child: Text('Logout', style: _headerStyle)),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text('Number Of Hours', style: _headerStyle),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
ResponsiveLayout.isMobile(context)
|
|
? _buildDataTable(context)
|
|
: Expanded(
|
|
child: Container(
|
|
color: Colors.white,
|
|
child: _buildDataTable(context),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDataTable(BuildContext context) {
|
|
print('test 32');
|
|
if (filteredData.isEmpty) {
|
|
return const SizedBox(
|
|
height: 50,
|
|
child: Center(child: Text('No available data')),
|
|
);
|
|
}
|
|
|
|
final sortedData = [..._paginatedData];
|
|
print('test 4');
|
|
|
|
// Desktop: keep ListView.builder
|
|
return ListView.builder(
|
|
itemCount: sortedData.length + 1,
|
|
itemBuilder: (context, index) {
|
|
if (index == 0) return _buildHeader();
|
|
final startIndex = (currentPage - 1) * itemsPerPage;
|
|
final item = sortedData[index - 1];
|
|
final sno = startIndex + index;
|
|
return _buildDataRow(item, sno);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return SizedBox.shrink();
|
|
}
|
|
|
|
Widget _buildDataRow(Map<String, dynamic> item, sno) {
|
|
print('test 5');
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
|
// margin: const EdgeInsets.only(top: 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
// color: Color(0xFFE0F7F9),
|
|
border: const Border(
|
|
bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1),
|
|
),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(flex: 1, child: Text('$sno' ?? '-', style: _dataBold)),
|
|
Expanded(flex: 2, child: Text(item['name'] ?? '-', style: _dataBold)),
|
|
Expanded(flex: 2, child: Text(item['date'] ?? '-', style: _dataBold)),
|
|
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(item['login_time'] ?? '-', style: _dataBold),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(item['logout_time'] ?? '-', style: _dataBold),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(item['no_of_logged_in_time'] ?? '-', style: _dataBold),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildStartDate(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Start Date', style: _textStyle),
|
|
SizedBox(height: 5),
|
|
|
|
ThemedMonthField(
|
|
hintText: "Select Month",
|
|
txtwidth: MediaQuery.of(context).size.width * 0.27,
|
|
txtheight: 40,
|
|
backgroundColor: const Color(0xFFECECEC),
|
|
validator: (value) => Validators.requiredField(value, "date"),
|
|
borderColor: Colors.grey.shade300,
|
|
controller: controllers['startDate']!,
|
|
onDateSelected: (date) {
|
|
print("Picked Date: $date");
|
|
|
|
controllers['startDate']?.text = DateFormat('MM-yyyy').format(date);
|
|
// controllers['date']?.text = date as String;
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget buildEndDate(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('End Date', style: _textStyle),
|
|
SizedBox(height: 5),
|
|
ThemedDateField(
|
|
hintText: "Select Date",
|
|
txtwidth: ResponsiveLayout.isMobile(context)
|
|
? null
|
|
: MediaQuery.of(context).size.width * 0.16,
|
|
// txtheight: 50,
|
|
// backgroundColor: const Color(0xFFECECEC),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return "End Date is required";
|
|
}
|
|
|
|
final fromText = controllers['startDate']?.text ?? '';
|
|
if (fromText.isNotEmpty) {
|
|
final startDate = DateFormat('dd-MM-yyyy').parse(fromText);
|
|
final endDate = DateFormat('dd-MM-yyyy').parse(value);
|
|
|
|
if (endDate.isBefore(startDate)) {
|
|
return "End Date cannot be earlier than Start Date";
|
|
}
|
|
}
|
|
return null; // ✅ no error
|
|
},
|
|
controller: controllers['endDate']!,
|
|
lastDate: DateTime.now(),
|
|
onDateSelected: (date) {
|
|
print("Picked Date: $date");
|
|
controllers['endDate']?.text = DateFormat(
|
|
'dd-MM-yyyy',
|
|
).format(date);
|
|
// controllers['date']?.text = date as String;
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
static final _dataBold = GoogleFonts.inter(
|
|
fontSize: 12,
|
|
|
|
fontWeight: FontWeight.w400,
|
|
color: Color(0xFF000000),
|
|
);
|
|
|
|
static final _dataSub = GoogleFonts.inter(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w300,
|
|
color: Color(0xFF585757),
|
|
);
|
|
|
|
// static const _headerStyle = GoogleFonts.inter(
|
|
// color: Colors.black,
|
|
// fontWeight: FontWeight.bold,
|
|
// );
|
|
static final _headerStyle = GoogleFonts.poppins(
|
|
fontSize: 11.2,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF1E293B),
|
|
);
|
|
static final _cardheaderStyle = GoogleFonts.inter(
|
|
color: Colors.black,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 12,
|
|
);
|
|
|
|
static final _cardBodyStyle = GoogleFonts.inter(
|
|
color: Color(0xFF545454),
|
|
fontWeight: FontWeight.w400,
|
|
fontSize: 12,
|
|
);
|
|
|
|
static const _textStyle = TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
);
|
|
}
|