grouped the attendence logs

This commit is contained in:
Vaibhav Surve 2025-05-05 15:49:46 +05:30
parent f4ed63a8ab
commit f2c125172d
2 changed files with 365 additions and 334 deletions

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.dart'; import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/project_model.dart'; import 'package:marco/model/project_model.dart';
@ -182,6 +182,22 @@ class AttendanceController extends GetxController {
print("Failed to fetch attendance logs for project $projectId."); print("Failed to fetch attendance logs for project $projectId.");
} }
} }
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{};
for (var log in attendanceLogs) {
final checkInDate = log.checkIn != null
? DateFormat('dd MMM yyyy').format(log.checkIn!)
: 'Unknown';
if (!groupedLogs.containsKey(checkInDate)) {
groupedLogs[checkInDate] = [];
}
groupedLogs[checkInDate]!.add(log);
}
return groupedLogs;
}
Future<void> fetchRegularizationLogs( Future<void> fetchRegularizationLogs(
String? projectId, { String? projectId, {

View File

@ -340,6 +340,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
Widget reportsTab(BuildContext context) { Widget reportsTab(BuildContext context) {
final attendanceController = Get.find<AttendanceController>(); final attendanceController = Get.find<AttendanceController>();
final groupedLogs = attendanceController.groupLogsByCheckInDate();
final columns = [ final columns = [
DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)), DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)),
@ -352,360 +353,374 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
label: MyText.labelLarge('Action', color: contentTheme.primary)), label: MyText.labelLarge('Action', color: contentTheme.primary)),
]; ];
final rows = attendanceController.attendanceLogs.reversed List<DataRow> rows = [];
.toList()
.asMap() // Iterate over grouped logs
.entries groupedLogs.forEach((checkInDate, logs) {
.map((entry) { // Add a row for the check-in date as a header
var log = entry.value; rows.add(DataRow(cells: [
return DataRow(cells: [ DataCell(MyText.bodyMedium(checkInDate, fontWeight: 600)),
DataCell( DataCell(MyText.bodyMedium('')), // Placeholder for other columns
Column( DataCell(MyText.bodyMedium('')), // Placeholder for other columns
crossAxisAlignment: CrossAxisAlignment.start, DataCell(MyText.bodyMedium('')), // Placeholder for other columns
mainAxisAlignment: MainAxisAlignment.center, DataCell(MyText.bodyMedium('')), // Placeholder for other columns
children: [ ]));
MyText.bodyMedium(log.name, fontWeight: 600),
SizedBox(height: 2), // Add rows for each log in this group
MyText.bodySmall(log.role, color: Colors.grey), for (var log in logs) {
], rows.add(DataRow(cells: [
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(log.name, fontWeight: 600),
SizedBox(height: 2),
MyText.bodySmall(log.role, color: Colors.grey),
],
),
), ),
), DataCell(
DataCell( Column(
Column( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ // MyText.bodyMedium(
MyText.bodyMedium( // log.checkIn != null
log.checkIn != null // ? DateFormat('dd MMM yyyy').format(log.checkIn!)
? DateFormat('dd MMM yyyy').format(log.checkIn!) // : '-',
: '-', // fontWeight: 600,
fontWeight: 600, // ),
), MyText.bodyMedium(
MyText.bodyMedium( log.checkIn != null
log.checkIn != null ? DateFormat('hh:mm a').format(log.checkIn!)
? DateFormat('hh:mm a').format(log.checkIn!) : '',
: '', fontWeight: 600,
fontWeight: 600,
),
],
),
),
DataCell(
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium(
log.checkOut != null
? DateFormat('dd MMM yyyy').format(log.checkOut!)
: '-',
fontWeight: 600,
),
MyText.bodyMedium(
log.checkOut != null
? DateFormat('hh:mm a').format(log.checkOut!)
: '',
fontWeight: 600,
),
],
),
),
DataCell(
IconButton(
icon: const Icon(Icons.visibility, size: 18),
onPressed: () async {
await attendanceController.fetchLogsView(log.id.toString());
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
), ),
backgroundColor: Theme.of(context).cardColor, ],
builder: (context) { ),
return Padding( ),
padding: const EdgeInsets.all(16.0), DataCell(
child: Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.titleMedium("Attendance Log Details", // MyText.bodyMedium(
fontWeight: 700), // log.checkOut != null
const SizedBox(height: 16), // ? DateFormat('dd MMM yyyy').format(log.checkOut!)
if (attendanceController // : '-',
.attendenceLogsView.isNotEmpty) ...[ // fontWeight: 600,
Row( // ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, MyText.bodyMedium(
children: [ log.checkOut != null
Expanded( ? DateFormat('hh:mm a').format(log.checkOut!)
child: MyText.bodyMedium("Date", : '',
fontWeight: 600)), fontWeight: 600,
Expanded( ),
child: MyText.bodyMedium("Time", ],
fontWeight: 600)), ),
Expanded( ),
child: MyText.bodyMedium("Description", DataCell(
fontWeight: 600)), IconButton(
Expanded( icon: const Icon(Icons.visibility, size: 18),
child: MyText.bodyMedium("Image", onPressed: () async {
fontWeight: 600)), await attendanceController.fetchLogsView(log.id.toString());
], showModalBottomSheet(
), context: context,
const Divider(thickness: 1, height: 24), shape: const RoundedRectangleBorder(
], borderRadius:
if (attendanceController.attendenceLogsView.isNotEmpty) BorderRadius.vertical(top: Radius.circular(16)),
...attendanceController.attendenceLogsView ),
.map((log) => Row( backgroundColor: Theme.of(context).cardColor,
mainAxisAlignment: builder: (context) {
MainAxisAlignment.spaceBetween, return Padding(
children: [ padding: const EdgeInsets.all(16.0),
Expanded( child: Column(
child: MyText.bodyMedium( mainAxisSize: MainAxisSize.min,
log.formattedDate ?? '-', crossAxisAlignment: CrossAxisAlignment.start,
fontWeight: 600)), children: [
Expanded( MyText.titleMedium("Attendance Log Details",
child: MyText.bodyMedium( fontWeight: 700),
log.formattedTime ?? '-', const SizedBox(height: 16),
fontWeight: 600)), if (attendanceController
Expanded( .attendenceLogsView.isNotEmpty) ...[
child: Row( Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
if (log.latitude != null && children: [
log.longitude != null) Expanded(
GestureDetector( child: MyText.bodyMedium("Date",
onTap: () async { fontWeight: 600)),
final url = Expanded(
'https://www.google.com/maps/search/?api=1&query=${log.latitude},${log.longitude}'; child: MyText.bodyMedium("Time",
if (await canLaunchUrl( fontWeight: 600)),
Uri.parse(url))) { Expanded(
await launchUrl( child: MyText.bodyMedium("Description",
Uri.parse(url), fontWeight: 600)),
mode: LaunchMode Expanded(
.externalApplication); child: MyText.bodyMedium("Image",
} else { fontWeight: 600)),
ScaffoldMessenger.of( ],
context) ),
.showSnackBar( const Divider(thickness: 1, height: 24),
const SnackBar( ],
content: Text( if (attendanceController
'Could not open Google Maps')), .attendenceLogsView.isNotEmpty)
); ...attendanceController.attendenceLogsView
} .map((log) => Row(
}, mainAxisAlignment:
child: const Padding( MainAxisAlignment.spaceBetween,
padding: EdgeInsets.only( children: [
right: 4.0), Expanded(
child: Icon(Icons.location_on, child: MyText.bodyMedium(
size: 18, log.formattedDate ?? '-',
color: Colors.blue), fontWeight: 600)),
Expanded(
child: MyText.bodyMedium(
log.formattedTime ?? '-',
fontWeight: 600)),
Expanded(
child: Row(
children: [
if (log.latitude != null &&
log.longitude != null)
GestureDetector(
onTap: () async {
final url =
'https://www.google.com/maps/search/?api=1&query=${log.latitude},${log.longitude}';
if (await canLaunchUrl(
Uri.parse(url))) {
await launchUrl(
Uri.parse(url),
mode: LaunchMode
.externalApplication);
} else {
ScaffoldMessenger.of(
context)
.showSnackBar(
const SnackBar(
content: Text(
'Could not open Google Maps')),
);
}
},
child: const Padding(
padding: EdgeInsets.only(
right: 4.0),
child: Icon(
Icons.location_on,
size: 18,
color: Colors.blue),
),
),
Expanded(
child: MyText.bodyMedium(
log.comment ?? '-',
fontWeight: 600,
), ),
), ),
Expanded( ],
child: MyText.bodyMedium( ),
log.comment ?? '-',
fontWeight: 600,
),
),
],
), ),
), Expanded(
Expanded( child: GestureDetector(
child: GestureDetector( onTap: () {
onTap: () { if (log.preSignedUrl != null) {
if (log.preSignedUrl != null) { showDialog(
showDialog( context: context,
context: context, builder: (_) => Dialog(
builder: (_) => Dialog( child: Image.network(
child: Image.network( log.preSignedUrl!,
log.preSignedUrl!, fit: BoxFit.cover,
height: 400,
errorBuilder: (context,
error, stackTrace) {
return Icon(
Icons.broken_image,
size: 50,
color: Colors.grey);
},
),
),
);
}
},
child: log.thumbPreSignedUrl != null
? Image.network(
log.thumbPreSignedUrl!,
height: 40,
width: 40,
fit: BoxFit.cover, fit: BoxFit.cover,
height: 400,
errorBuilder: (context, errorBuilder: (context,
error, stackTrace) { error, stackTrace) {
return Icon( return Icon(
Icons.broken_image, Icons.broken_image,
size: 50, size: 40,
color: Colors.grey); color: Colors.grey);
}, },
), )
), : Icon(Icons.broken_image,
); size: 40,
} color: Colors.grey),
}, ),
child: log.thumbPreSignedUrl != null
? Image.network(
log.thumbPreSignedUrl!,
height: 40,
width: 40,
fit: BoxFit.cover,
errorBuilder: (context, error,
stackTrace) {
return Icon(
Icons.broken_image,
size: 40,
color: Colors.grey);
},
)
: Icon(Icons.broken_image,
size: 40, color: Colors.grey),
), ),
), ],
], )),
)), Align(
Align( alignment: Alignment.centerRight,
alignment: Alignment.centerRight, child: ElevatedButton(
child: ElevatedButton( onPressed: () => Navigator.pop(context),
onPressed: () => Navigator.pop(context), child: const Text("Close"),
child: const Text("Close"), ),
), ),
), ],
],
),
);
},
);
},
),
),
DataCell(
ElevatedButton(
onPressed: (log.activity == 5 ||
log.activity == 2 || // Add this condition for activity 2
(log.activity == 4 &&
!(log.checkOut != null &&
log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <=
2)))
? null
: () async {
if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a project first"),
),
);
return;
}
int updatedAction;
String actionText;
bool imageCapture = true;
if (log.activity == 0) {
updatedAction = 0;
actionText = "Check In";
} else if (log.activity == 1) {
DateTime currentDate = DateTime.now();
DateTime twoDaysAgo =
currentDate.subtract(Duration(days: 2));
if (log.checkOut == null &&
log.checkIn != null &&
log.checkIn!.isBefore(twoDaysAgo)) {
updatedAction = 2;
actionText = "Request Regularize";
imageCapture = false;
} else if (log.checkOut != null &&
log.checkOut!.isBefore(twoDaysAgo)) {
updatedAction = 2;
actionText = "Request Regularize";
} else {
updatedAction = 1;
actionText = "Check Out";
}
} else if (log.activity == 2) {
updatedAction = 2;
actionText = "Request Regularize";
} else if (log.activity == 4 &&
log.checkOut != null &&
log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <= 2) {
updatedAction = 0;
actionText = "Check In";
} else {
updatedAction = 0;
actionText = "Unknown Action";
}
final success =
await attendanceController.captureAndUploadAttendance(
log.id,
log.employeeId,
attendanceController.selectedProjectId!,
comment: actionText,
action: updatedAction,
imageCapture: imageCapture,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'Attendance marked successfully!'
: 'Image upload failed.'),
), ),
); );
if (success) {
attendanceController.fetchEmployeesByProject(
attendanceController.selectedProjectId!);
attendanceController.fetchAttendanceLogs(
attendanceController.selectedProjectId!);
await attendanceController.fetchRegularizationLogs(
attendanceController.selectedProjectId!);
await attendanceController.fetchProjectData(
attendanceController.selectedProjectId!);
attendanceController.update();
}
}, },
style: ElevatedButton.styleFrom( );
backgroundColor: (log.activity == 4 && },
log.checkOut != null &&
log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <= 2)
? Colors.green
: AttendanceActionColors.colors[(log.activity == 0)
? ButtonActions.checkIn
: ButtonActions.checkOut],
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
minimumSize: const Size(60, 20),
textStyle: const TextStyle(fontSize: 12),
),
child: Text(
(log.activity == 5)
? ButtonActions.rejected
: (log.activity == 4 &&
log.checkOut != null &&
log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <= 2)
? ButtonActions.checkIn
: (log.activity == 2) // Change text when activity is 2
? "Requested"
: (log.activity == 4)
? ButtonActions.approved
: (log.activity == 0)
? ButtonActions.checkIn
: (log.activity == 1 &&
log.checkOut != null &&
DateTime.now()
.difference(log.checkOut!)
.inDays <=
2)
? ButtonActions.checkOut
: (log.activity == 2 ||
(log.activity == 1 &&
log.checkOut == null &&
log.checkIn != null &&
log.checkIn!.isBefore(
DateTime.now().subtract(
Duration(days: 2)))))
? ButtonActions.requestRegularize
: ButtonActions.checkOut,
), ),
), ),
) DataCell(
]); ElevatedButton(
}).toList(); onPressed: (log.activity == 5 ||
log.activity == 2 || // Add this condition for activity 2
(log.activity == 4 &&
!(log.checkOut != null &&
log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <=
2)))
? null
: () async {
if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a project first"),
),
);
return;
}
int updatedAction;
String actionText;
bool imageCapture = true;
if (log.activity == 0) {
updatedAction = 0;
actionText = "Check In";
} else if (log.activity == 1) {
DateTime currentDate = DateTime.now();
DateTime twoDaysAgo =
currentDate.subtract(Duration(days: 2));
if (log.checkOut == null &&
log.checkIn != null &&
log.checkIn!.isBefore(twoDaysAgo)) {
updatedAction = 2;
actionText = "Request Regularize";
imageCapture = false;
} else if (log.checkOut != null &&
log.checkOut!.isBefore(twoDaysAgo)) {
updatedAction = 2;
actionText = "Request Regularize";
} else {
updatedAction = 1;
actionText = "Check Out";
}
} else if (log.activity == 2) {
updatedAction = 2;
actionText = "Request Regularize";
} else if (log.activity == 4 &&
log.checkOut != null &&
log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <= 2) {
updatedAction = 0;
actionText = "Check In";
} else {
updatedAction = 0;
actionText = "Unknown Action";
}
final success =
await attendanceController.captureAndUploadAttendance(
log.id,
log.employeeId,
attendanceController.selectedProjectId!,
comment: actionText,
action: updatedAction,
imageCapture: imageCapture,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'Attendance marked successfully!'
: 'Image upload failed.'),
),
);
if (success) {
attendanceController.fetchEmployeesByProject(
attendanceController.selectedProjectId!);
attendanceController.fetchAttendanceLogs(
attendanceController.selectedProjectId!);
await attendanceController.fetchRegularizationLogs(
attendanceController.selectedProjectId!);
await attendanceController.fetchProjectData(
attendanceController.selectedProjectId!);
attendanceController.update();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: (log.activity == 4 &&
log.checkOut != null &&
log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <= 2)
? Colors.green
: AttendanceActionColors.colors[(log.activity == 0)
? ButtonActions.checkIn
: ButtonActions.checkOut],
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
minimumSize: const Size(60, 20),
textStyle: const TextStyle(fontSize: 12),
),
child: Text(
(log.activity == 5)
? ButtonActions.rejected
: (log.activity == 4 &&
log.checkOut != null &&
log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <= 2)
? ButtonActions.checkIn
: (log.activity == 2) // Change text when activity is 2
? "Requested"
: (log.activity == 4)
? ButtonActions.approved
: (log.activity == 0)
? ButtonActions.checkIn
: (log.activity == 1 &&
log.checkOut != null &&
DateTime.now()
.difference(log.checkOut!)
.inDays <=
2)
? ButtonActions.checkOut
: (log.activity == 2 ||
(log.activity == 1 &&
log.checkOut == null &&
log.checkIn != null &&
log.checkIn!.isBefore(
DateTime.now().subtract(
Duration(
days: 2)))))
? ButtonActions.requestRegularize
: ButtonActions.checkOut,
),
),
),
]));
}
});
return SingleChildScrollView( return SingleChildScrollView(
// Wrap the Column in SingleChildScrollView to handle overflow
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [