Merge pull request 'Refactor code structure for improved readability and maintainability, Changed the Attendance Screen UI' (#31) from Vaibhav_Enhancement-#312 into main
Reviewed-on: #31
This commit is contained in:
commit
6db47cce88
293
lib/model/attendance/attendence_action_button.dart
Normal file
293
lib/model/attendance/attendence_action_button.dart
Normal file
@ -0,0 +1,293 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||
import 'package:marco/helpers/utils/attendance_actions.dart';
|
||||
|
||||
class AttendanceActionButton extends StatefulWidget {
|
||||
final dynamic employee;
|
||||
final AttendanceController attendanceController;
|
||||
|
||||
const AttendanceActionButton({
|
||||
Key? key,
|
||||
required this.employee,
|
||||
required this.attendanceController,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AttendanceActionButton> createState() => _AttendanceActionButtonState();
|
||||
}
|
||||
|
||||
class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
late final String uniqueLogKey;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
uniqueLogKey = AttendanceButtonHelper.getUniqueKey(
|
||||
widget.employee.employeeId, widget.employee.id);
|
||||
|
||||
// Defer the Rx initialization after first frame to avoid setState during build
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!widget.attendanceController.uploadingStates
|
||||
.containsKey(uniqueLogKey)) {
|
||||
widget.attendanceController.uploadingStates[uniqueLogKey] = false.obs;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<DateTime?> showTimePickerForRegularization({
|
||||
required BuildContext context,
|
||||
required DateTime checkInTime,
|
||||
}) async {
|
||||
final pickedTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(DateTime.now()),
|
||||
);
|
||||
|
||||
if (pickedTime != null) {
|
||||
final selectedDateTime = DateTime(
|
||||
checkInTime.year,
|
||||
checkInTime.month,
|
||||
checkInTime.day,
|
||||
pickedTime.hour,
|
||||
pickedTime.minute,
|
||||
);
|
||||
|
||||
if (selectedDateTime.isAfter(checkInTime)) {
|
||||
return selectedDateTime;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Please select a time after check-in time.")),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _handleButtonPressed(BuildContext context) async {
|
||||
// Set uploading state true safely
|
||||
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true;
|
||||
|
||||
if (widget.attendanceController.selectedProjectId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Please select a project first"),
|
||||
),
|
||||
);
|
||||
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int updatedAction;
|
||||
String actionText;
|
||||
bool imageCapture = true;
|
||||
|
||||
switch (widget.employee.activity) {
|
||||
case 0:
|
||||
updatedAction = 0;
|
||||
actionText = ButtonActions.checkIn;
|
||||
break;
|
||||
case 1:
|
||||
if (widget.employee.checkOut == null &&
|
||||
AttendanceButtonHelper.isOlderThanDays(
|
||||
widget.employee.checkIn, 2)) {
|
||||
updatedAction = 2;
|
||||
actionText = ButtonActions.requestRegularize;
|
||||
imageCapture = false;
|
||||
} else if (widget.employee.checkOut != null &&
|
||||
AttendanceButtonHelper.isOlderThanDays(
|
||||
widget.employee.checkOut, 2)) {
|
||||
updatedAction = 2;
|
||||
actionText = ButtonActions.requestRegularize;
|
||||
} else {
|
||||
updatedAction = 1;
|
||||
actionText = ButtonActions.checkOut;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
updatedAction = 2;
|
||||
actionText = ButtonActions.requestRegularize;
|
||||
break;
|
||||
case 4:
|
||||
updatedAction = 0;
|
||||
actionText = ButtonActions.checkIn;
|
||||
break;
|
||||
default:
|
||||
updatedAction = 0;
|
||||
actionText = "Unknown Action";
|
||||
break;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
if (actionText == ButtonActions.requestRegularize) {
|
||||
final selectedTime = await showTimePickerForRegularization(
|
||||
context: context,
|
||||
checkInTime: widget.employee.checkIn!,
|
||||
);
|
||||
if (selectedTime != null) {
|
||||
final formattedSelectedTime =
|
||||
DateFormat("hh:mm a").format(selectedTime);
|
||||
success = await widget.attendanceController.captureAndUploadAttendance(
|
||||
widget.employee.id,
|
||||
widget.employee.employeeId,
|
||||
widget.attendanceController.selectedProjectId!,
|
||||
comment: actionText,
|
||||
action: updatedAction,
|
||||
imageCapture: imageCapture,
|
||||
markTime: formattedSelectedTime,
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(success
|
||||
? '${actionText.toLowerCase()} marked successfully!'
|
||||
: 'Failed to ${actionText.toLowerCase()}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
success = await widget.attendanceController.captureAndUploadAttendance(
|
||||
widget.employee.id,
|
||||
widget.employee.employeeId,
|
||||
widget.attendanceController.selectedProjectId!,
|
||||
comment: actionText,
|
||||
action: updatedAction,
|
||||
imageCapture: imageCapture,
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(success
|
||||
? '${actionText.toLowerCase()} marked successfully!'
|
||||
: 'Failed to ${actionText.toLowerCase()}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
|
||||
|
||||
if (success) {
|
||||
widget.attendanceController.fetchEmployeesByProject(
|
||||
widget.attendanceController.selectedProjectId!);
|
||||
widget.attendanceController
|
||||
.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!);
|
||||
await widget.attendanceController.fetchRegularizationLogs(
|
||||
widget.attendanceController.selectedProjectId!);
|
||||
await widget.attendanceController
|
||||
.fetchProjectData(widget.attendanceController.selectedProjectId!);
|
||||
widget.attendanceController.update();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final isUploading =
|
||||
widget.attendanceController.uploadingStates[uniqueLogKey]?.value ??
|
||||
false;
|
||||
|
||||
final isYesterday = AttendanceButtonHelper.isLogFromYesterday(
|
||||
widget.employee.checkIn, widget.employee.checkOut);
|
||||
final isTodayApproved = AttendanceButtonHelper.isTodayApproved(
|
||||
widget.employee.activity, widget.employee.checkIn);
|
||||
final isApprovedButNotToday =
|
||||
AttendanceButtonHelper.isApprovedButNotToday(
|
||||
widget.employee.activity, isTodayApproved);
|
||||
|
||||
final isButtonDisabled = AttendanceButtonHelper.isButtonDisabled(
|
||||
isUploading: isUploading,
|
||||
isYesterday: isYesterday,
|
||||
activity: widget.employee.activity,
|
||||
isApprovedButNotToday: isApprovedButNotToday,
|
||||
);
|
||||
|
||||
final buttonText = AttendanceButtonHelper.getButtonText(
|
||||
activity: widget.employee.activity,
|
||||
checkIn: widget.employee.checkIn,
|
||||
checkOut: widget.employee.checkOut,
|
||||
isTodayApproved: isTodayApproved,
|
||||
);
|
||||
|
||||
final buttonColor = AttendanceButtonHelper.getButtonColor(
|
||||
isYesterday: isYesterday,
|
||||
isTodayApproved: isTodayApproved,
|
||||
activity: widget.employee.activity,
|
||||
);
|
||||
|
||||
return AttendanceActionButtonUI(
|
||||
isUploading: isUploading,
|
||||
isButtonDisabled: isButtonDisabled,
|
||||
buttonText: buttonText,
|
||||
buttonColor: buttonColor,
|
||||
onPressed:
|
||||
isButtonDisabled ? null : () => _handleButtonPressed(context),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class AttendanceActionButtonUI extends StatelessWidget {
|
||||
final bool isUploading;
|
||||
final bool isButtonDisabled;
|
||||
final String buttonText;
|
||||
final Color buttonColor;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const AttendanceActionButtonUI({
|
||||
Key? key,
|
||||
required this.isUploading,
|
||||
required this.isButtonDisabled,
|
||||
required this.buttonText,
|
||||
required this.buttonColor,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: ElevatedButton(
|
||||
onPressed: isButtonDisabled ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: buttonColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
child: isUploading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (buttonText.toLowerCase() == 'approved') ...[
|
||||
const Icon(Icons.check, size: 16, color: Colors.green),
|
||||
const SizedBox(width: 4),
|
||||
] else if (buttonText.toLowerCase() == 'rejected') ...[
|
||||
const Icon(Icons.close, size: 16, color: Colors.red),
|
||||
const SizedBox(width: 4),
|
||||
] else if (buttonText.toLowerCase() == 'requested') ...[
|
||||
const Icon(Icons.hourglass_top,
|
||||
size: 16, color: Colors.orange),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
buttonText,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
158
lib/model/attendance/info_card.dart
Normal file
158
lib/model/attendance/info_card.dart
Normal file
@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef ItemWidgetBuilder<T> = Widget Function(BuildContext context, T item, int index);
|
||||
typedef ButtonActionCallback<T> = Future<bool> Function(T item);
|
||||
|
||||
class ReusableListCard<T> extends StatelessWidget {
|
||||
final List<T> items;
|
||||
final Widget emptyWidget;
|
||||
final ItemWidgetBuilder<T> itemBuilder;
|
||||
final ButtonActionCallback<T> onActionPressed;
|
||||
final ButtonActionCallback<T> onViewPressed;
|
||||
final String Function(T item) getPrimaryText;
|
||||
final String Function(T item) getSecondaryText;
|
||||
final String Function(T item)? getInTimeText;
|
||||
final String Function(T item)? getOutTimeText;
|
||||
final bool Function(T item)? isUploading;
|
||||
final String Function(T item)? getButtonText;
|
||||
final Color Function(String buttonText)? getButtonColor;
|
||||
|
||||
const ReusableListCard({
|
||||
Key? key,
|
||||
required this.items,
|
||||
required this.emptyWidget,
|
||||
required this.itemBuilder,
|
||||
required this.onActionPressed,
|
||||
required this.onViewPressed,
|
||||
required this.getPrimaryText,
|
||||
required this.getSecondaryText,
|
||||
this.getInTimeText,
|
||||
this.getOutTimeText,
|
||||
this.isUploading,
|
||||
this.getButtonText,
|
||||
this.getButtonColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
elevation: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: items.isEmpty
|
||||
? emptyWidget
|
||||
: Column(
|
||||
children: List.generate(items.length, (index) {
|
||||
final item = items[index];
|
||||
final buttonText = getButtonText?.call(item) ?? 'Action';
|
||||
final uploading = isUploading?.call(item) ?? false;
|
||||
final buttonColor =
|
||||
getButtonColor?.call(buttonText) ?? Theme.of(context).primaryColor;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Theme.of(context).cardColor,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Custom item widget builder for avatar or image
|
||||
itemBuilder(context, item, index),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
getPrimaryText(item),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
getSecondaryText(item),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (getInTimeText != null && getOutTimeText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text("In: ", style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text(getInTimeText!(item), style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(width: 16),
|
||||
const Text("Out: ", style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text(getOutTimeText!(item), style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => onViewPressed(item),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blueGrey,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
child: const Text('View', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: ElevatedButton(
|
||||
onPressed: uploading
|
||||
? null
|
||||
: () => onActionPressed(item),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: buttonColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
child: uploading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(buttonText, style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (index != items.length - 1)
|
||||
Divider(color: Colors.grey.withOpacity(0.3), thickness: 1, height: 1),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
126
lib/model/attendance/regualrize_action_button.dart
Normal file
126
lib/model/attendance/regualrize_action_button.dart
Normal file
@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:marco/helpers/utils/attendance_actions.dart';
|
||||
|
||||
enum ButtonActions { approve, reject }
|
||||
|
||||
class RegularizeActionButton extends StatefulWidget {
|
||||
final dynamic attendanceController; // Replace dynamic with your controller's type
|
||||
final dynamic log; // Replace dynamic with your log model type
|
||||
final String uniqueLogKey;
|
||||
final ButtonActions action;
|
||||
|
||||
const RegularizeActionButton({
|
||||
Key? key,
|
||||
required this.attendanceController,
|
||||
required this.log,
|
||||
required this.uniqueLogKey,
|
||||
required this.action,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RegularizeActionButton> createState() => _RegularizeActionButtonState();
|
||||
}
|
||||
|
||||
class _RegularizeActionButtonState extends State<RegularizeActionButton> {
|
||||
bool isUploading = false;
|
||||
|
||||
static const Map<ButtonActions, String> _buttonTexts = {
|
||||
ButtonActions.approve: "Approve",
|
||||
ButtonActions.reject: "Reject",
|
||||
};
|
||||
|
||||
static const Map<ButtonActions, String> _buttonComments = {
|
||||
ButtonActions.approve: "Accepted",
|
||||
ButtonActions.reject: "Rejected",
|
||||
};
|
||||
|
||||
static const Map<ButtonActions, int> _buttonActionCodes = {
|
||||
ButtonActions.approve: 4,
|
||||
ButtonActions.reject: 5,
|
||||
};
|
||||
|
||||
Color get backgroundColor {
|
||||
// Use string keys for correct color lookup
|
||||
return AttendanceActionColors.colors[_buttonTexts[widget.action]!] ?? Colors.grey;
|
||||
}
|
||||
|
||||
Future<void> _handlePress() async {
|
||||
if (widget.attendanceController.selectedProjectId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Please select a project first")),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isUploading = true;
|
||||
});
|
||||
|
||||
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = true;
|
||||
|
||||
final success = await widget.attendanceController.captureAndUploadAttendance(
|
||||
widget.log.id,
|
||||
widget.log.employeeId,
|
||||
widget.attendanceController.selectedProjectId!,
|
||||
comment: _buttonComments[widget.action]!,
|
||||
action: _buttonActionCodes[widget.action]!,
|
||||
imageCapture: false,
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(success
|
||||
? '${_buttonTexts[widget.action]} marked successfully!'
|
||||
: 'Failed to mark ${_buttonTexts[widget.action]}.'),
|
||||
),
|
||||
);
|
||||
|
||||
if (success) {
|
||||
widget.attendanceController.fetchEmployeesByProject(widget.attendanceController.selectedProjectId!);
|
||||
widget.attendanceController.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!);
|
||||
await widget.attendanceController.fetchRegularizationLogs(widget.attendanceController.selectedProjectId!);
|
||||
await widget.attendanceController.fetchProjectData(widget.attendanceController.selectedProjectId!);
|
||||
}
|
||||
|
||||
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = false;
|
||||
|
||||
setState(() {
|
||||
isUploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final buttonText = _buttonTexts[widget.action]!;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 70, maxWidth: 120),
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
child: ElevatedButton(
|
||||
onPressed: isUploading ? null : _handlePress,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: Colors.white, // Ensures visibility on all backgrounds
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
|
||||
minimumSize: const Size(60, 20),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
child: isUploading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
buttonText,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,9 @@ class AttendanceLogModel {
|
||||
final DateTime? checkIn;
|
||||
final DateTime? checkOut;
|
||||
final int activity;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String designation;
|
||||
|
||||
AttendanceLogModel({
|
||||
required this.id,
|
||||
@ -15,6 +18,9 @@ class AttendanceLogModel {
|
||||
this.checkIn,
|
||||
this.checkOut,
|
||||
required this.activity,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.designation,
|
||||
});
|
||||
|
||||
factory AttendanceLogModel.fromJson(Map<String, dynamic> json) {
|
||||
@ -30,6 +36,9 @@ class AttendanceLogModel {
|
||||
? DateTime.tryParse(json['checkOutTime'])
|
||||
: null,
|
||||
activity: json['activity'] ?? 0,
|
||||
firstName: json['firstName'] ?? '',
|
||||
lastName: json['lastName'] ?? '',
|
||||
designation: json['jobRoleName'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class AttendanceLogViewModel {
|
||||
final DateTime? activityTime;
|
||||
final String? imageUrl;
|
||||
@ -7,6 +8,7 @@ class AttendanceLogViewModel {
|
||||
final String? preSignedUrl;
|
||||
final String? longitude;
|
||||
final String? latitude;
|
||||
final int? activity;
|
||||
|
||||
AttendanceLogViewModel({
|
||||
this.activityTime,
|
||||
@ -16,6 +18,7 @@ class AttendanceLogViewModel {
|
||||
this.preSignedUrl,
|
||||
this.longitude,
|
||||
this.latitude,
|
||||
required this.activity,
|
||||
});
|
||||
|
||||
factory AttendanceLogViewModel.fromJson(Map<String, dynamic> json) {
|
||||
@ -29,6 +32,7 @@ class AttendanceLogViewModel {
|
||||
preSignedUrl: json['preSignedUrl']?.toString(),
|
||||
longitude: json['longitude']?.toString(),
|
||||
latitude: json['latitude']?.toString(),
|
||||
activity: json['activity'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@ -41,12 +45,14 @@ class AttendanceLogViewModel {
|
||||
'preSignedUrl': preSignedUrl,
|
||||
'longitude': longitude,
|
||||
'latitude': latitude,
|
||||
'activity': activity,
|
||||
};
|
||||
}
|
||||
|
||||
String? get formattedDate =>
|
||||
activityTime != null ? DateFormat('yyyy-MM-dd').format(activityTime!) : null;
|
||||
String? get formattedDate => activityTime != null
|
||||
? DateFormat('yyyy-MM-dd').format(activityTime!)
|
||||
: null;
|
||||
|
||||
String? get formattedTime =>
|
||||
activityTime != null ? DateFormat('hh:mm a').format(activityTime!) : null;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ class EmployeeInfo {
|
||||
final String lastName;
|
||||
final String gender;
|
||||
final String birthDate;
|
||||
final String joiningDate;
|
||||
final String joiningDate;
|
||||
final String currentAddress;
|
||||
final String phoneNumber;
|
||||
final String emergencyPhoneNumber;
|
||||
|
@ -3,8 +3,10 @@ class EmployeeModel {
|
||||
final String employeeId;
|
||||
final String name;
|
||||
final String designation;
|
||||
final String checkIn;
|
||||
final String checkOut;
|
||||
final DateTime? checkIn;
|
||||
final DateTime? checkOut;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final int activity;
|
||||
int action;
|
||||
final String jobRole;
|
||||
@ -16,8 +18,10 @@ class EmployeeModel {
|
||||
required this.employeeId,
|
||||
required this.name,
|
||||
required this.designation,
|
||||
required this.checkIn,
|
||||
required this.checkOut,
|
||||
this.checkIn,
|
||||
this.checkOut,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.activity,
|
||||
required this.action,
|
||||
required this.jobRole,
|
||||
@ -31,13 +35,19 @@ class EmployeeModel {
|
||||
employeeId: json['employeeId']?.toString() ?? '',
|
||||
name: '${json['firstName'] ?? ''} ${json['lastName'] ?? ''}'.trim(),
|
||||
designation: json['jobRoleName'] ?? '',
|
||||
checkIn: json['checkIn']?.toString() ?? '-',
|
||||
checkOut: json['checkOut']?.toString() ?? '-',
|
||||
checkIn: json['checkInTime'] != null
|
||||
? DateTime.tryParse(json['checkInTime'])
|
||||
: null,
|
||||
checkOut: json['checkOutTime'] != null
|
||||
? DateTime.tryParse(json['checkOutTime'])
|
||||
: null,
|
||||
action: json['action'] ?? 0,
|
||||
activity: json['activity'] ?? 0,
|
||||
jobRole: json['jobRole']?.toString() ?? '-',
|
||||
email: json['email']?.toString() ?? '-',
|
||||
phoneNumber: json['phoneNumber']?.toString() ?? '-',
|
||||
email: json['email']?.toString() ?? '-',
|
||||
phoneNumber: json['phoneNumber']?.toString() ?? '-',
|
||||
firstName: json['firstName'] ?? '',
|
||||
lastName: json['lastName'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@ -48,8 +58,8 @@ class EmployeeModel {
|
||||
'firstName': name.split(' ').first,
|
||||
'lastName': name.split(' ').length > 1 ? name.split(' ').last : '',
|
||||
'jobRoleName': designation,
|
||||
'checkIn': checkIn,
|
||||
'checkOut': checkOut,
|
||||
'checkInTime': checkIn?.toIso8601String(),
|
||||
'checkOutTime': checkOut?.toIso8601String(),
|
||||
'action': action,
|
||||
'activity': activity,
|
||||
'jobRole': jobRole.isEmpty ? '-' : jobRole,
|
||||
|
@ -6,6 +6,8 @@ class RegularizationLogModel {
|
||||
final DateTime? checkIn;
|
||||
final DateTime? checkOut;
|
||||
final int activity;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
|
||||
RegularizationLogModel({
|
||||
required this.id,
|
||||
@ -15,6 +17,8 @@ class RegularizationLogModel {
|
||||
this.checkIn,
|
||||
this.checkOut,
|
||||
required this.activity,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
});
|
||||
|
||||
factory RegularizationLogModel.fromJson(Map<String, dynamic> json) {
|
||||
@ -30,6 +34,8 @@ class RegularizationLogModel {
|
||||
? DateTime.tryParse(json['checkOutTime'].toString())
|
||||
: null,
|
||||
activity: json['activity'] ?? 0,
|
||||
firstName: json['firstName'] ?? '',
|
||||
lastName: json['lastName'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -10,13 +10,14 @@ import 'package:marco/view/error_pages/coming_soon_screen.dart';
|
||||
import 'package:marco/view/error_pages/error_404_screen.dart';
|
||||
import 'package:marco/view/error_pages/error_500_screen.dart';
|
||||
// import 'package:marco/view/dashboard/attendance_screen.dart';
|
||||
import 'package:marco/view/dashboard/attendanceScreen.dart';
|
||||
// import 'package:marco/view/dashboard/attendanceScreen.dart';
|
||||
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
||||
import 'package:marco/view/dashboard/add_employee_screen.dart';
|
||||
import 'package:marco/view/dashboard/employee_screen.dart';
|
||||
import 'package:marco/view/dashboard/daily_task_screen.dart';
|
||||
import 'package:marco/view/taskPlaning/report_task_screen.dart';
|
||||
import 'package:marco/view/taskPlaning/comment_task_screen.dart';
|
||||
import 'package:marco/view/dashboard/Attendence/attendance_screen.dart';
|
||||
|
||||
class AuthMiddleware extends GetMiddleware {
|
||||
@override
|
||||
@ -29,7 +30,7 @@ getPageRoute() {
|
||||
var routes = [
|
||||
GetPage(
|
||||
name: '/',
|
||||
page: () => const AttendanceScreen(),
|
||||
page: () => AttendanceScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
|
||||
// Dashboard
|
||||
|
748
lib/view/dashboard/Attendence/attendance_screen.dart
Normal file
748
lib/view/dashboard/Attendence/attendance_screen.dart
Normal file
@ -0,0 +1,748 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/model/attendance/log_details_view.dart';
|
||||
import 'package:marco/model/attendance/attendence_action_button.dart';
|
||||
import 'package:marco/model/attendance/regualrize_action_button.dart';
|
||||
import 'package:marco/model/attendance/attendence_filter_sheet.dart';
|
||||
|
||||
class AttendanceScreen extends StatefulWidget {
|
||||
AttendanceScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AttendanceScreen> createState() => _AttendanceScreenState();
|
||||
}
|
||||
|
||||
class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
final AttendanceController attendanceController =
|
||||
Get.put(AttendanceController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
|
||||
String selectedTab = 'todaysAttendance';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder<AttendanceController>(
|
||||
init: attendanceController,
|
||||
tag: 'attendance_dashboard_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Attendance",
|
||||
fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Attendance', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
"Filter",
|
||||
fontWeight: 600,
|
||||
),
|
||||
// Wrap with Tooltip and InkWell for interactive feedback
|
||||
Tooltip(
|
||||
message: 'Filter Project',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final result =
|
||||
await showModalBottomSheet<Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
builder: (context) => AttendanceFilterBottomSheet(
|
||||
controller: attendanceController,
|
||||
permissionController: permissionController,
|
||||
selectedTab: selectedTab,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final selectedProjectId =
|
||||
result['projectId'] as String?;
|
||||
final selectedView = result['selectedTab'] as String?;
|
||||
|
||||
if (selectedProjectId != null &&
|
||||
selectedProjectId !=
|
||||
attendanceController.selectedProjectId) {
|
||||
attendanceController.selectedProjectId =
|
||||
selectedProjectId;
|
||||
try {
|
||||
await attendanceController
|
||||
.fetchEmployeesByProject(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchAttendanceLogs(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchRegularizationLogs(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchProjectData(selectedProjectId);
|
||||
} catch (_) {}
|
||||
attendanceController
|
||||
.update(['attendance_dashboard_controller']);
|
||||
}
|
||||
|
||||
if (selectedView != null &&
|
||||
selectedView != selectedTab) {
|
||||
setState(() {
|
||||
selectedTab = selectedView;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.filter_list_alt,
|
||||
color: Colors.blueAccent,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
MyText.bodyMedium(
|
||||
"Refresh",
|
||||
fontWeight: 600,
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final projectId =
|
||||
attendanceController.selectedProjectId;
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
try {
|
||||
await attendanceController
|
||||
.fetchEmployeesByProject(projectId);
|
||||
await attendanceController
|
||||
.fetchAttendanceLogs(projectId);
|
||||
await attendanceController
|
||||
.fetchRegularizationLogs(projectId);
|
||||
await attendanceController
|
||||
.fetchProjectData(projectId);
|
||||
attendanceController
|
||||
.update(['attendance_dashboard_controller']);
|
||||
} catch (e) {
|
||||
debugPrint("Error refreshing data: $e");
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.green,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(children: [
|
||||
MyFlexItem(
|
||||
sizes: 'lg-12 md-12 sm-12',
|
||||
child: selectedTab == 'todaysAttendance'
|
||||
? employeeListTab()
|
||||
: selectedTab == 'attendanceLogs'
|
||||
? employeeLog()
|
||||
: regularizationScreen(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget employeeListTab() {
|
||||
return Obx(() {
|
||||
final isLoading = attendanceController.isLoadingEmployees.value;
|
||||
final employees = attendanceController.employees;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.titleMedium(
|
||||
"Today's Attendance",
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 8,
|
||||
child: isLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: employees.isEmpty
|
||||
? Center(
|
||||
child: MyText.bodySmall(
|
||||
"No Employees Assigned to This Project",
|
||||
fontWeight: 600,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: List.generate(employees.length, (index) {
|
||||
final employee = employees[index];
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: MyContainer(
|
||||
paddingAll: 5,
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
size: 31,
|
||||
),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
employee.name,
|
||||
fontWeight: 600,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
MySpacing.width(6),
|
||||
MyText.bodySmall(
|
||||
'(${employee.designation})',
|
||||
fontWeight: 600,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
color: Colors.grey[
|
||||
700], // optional styling
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
(employee.checkIn != null ||
|
||||
employee.checkOut != null)
|
||||
? Row(
|
||||
children: [
|
||||
if (employee.checkIn !=
|
||||
null) ...[
|
||||
Icon(
|
||||
Icons
|
||||
.arrow_circle_right,
|
||||
size: 16,
|
||||
color:
|
||||
Colors.green),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child:
|
||||
MyText.bodySmall(
|
||||
DateFormat(
|
||||
'hh:mm a')
|
||||
.format(employee
|
||||
.checkIn!),
|
||||
fontWeight: 600,
|
||||
overflow:
|
||||
TextOverflow
|
||||
.ellipsis,
|
||||
),
|
||||
),
|
||||
MySpacing.width(16),
|
||||
],
|
||||
if (employee.checkOut !=
|
||||
null) ...[
|
||||
Icon(
|
||||
Icons
|
||||
.arrow_circle_left,
|
||||
size: 16,
|
||||
color: Colors.red),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child:
|
||||
MyText.bodySmall(
|
||||
DateFormat(
|
||||
'hh:mm a')
|
||||
.format(employee
|
||||
.checkOut!),
|
||||
fontWeight: 600,
|
||||
overflow:
|
||||
TextOverflow
|
||||
.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
MySpacing.height(12),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end,
|
||||
children: [
|
||||
AttendanceActionButton(
|
||||
employee: employee,
|
||||
attendanceController:
|
||||
attendanceController,
|
||||
),
|
||||
if (employee.checkIn !=
|
||||
null) ...[
|
||||
MySpacing.width(8),
|
||||
AttendanceLogViewButton(
|
||||
employee: employee,
|
||||
attendanceController:
|
||||
attendanceController,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (index != employees.length - 1)
|
||||
Divider(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
thickness: 1,
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget employeeLog() {
|
||||
return Obx(() {
|
||||
final logs = List.of(attendanceController.attendanceLogs);
|
||||
logs.sort((a, b) {
|
||||
final aDate = a.checkIn ?? DateTime(0);
|
||||
final bDate = b.checkIn ?? DateTime(0);
|
||||
return bDate.compareTo(aDate);
|
||||
});
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.titleMedium(
|
||||
"Attendance Logs",
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 8,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (attendanceController.isLoadingAttendanceLogs.value)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 32),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (logs.isEmpty)
|
||||
MyText.bodySmall(
|
||||
"No Attendance Logs Found for this Project",
|
||||
fontWeight: 600,
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: List.generate(logs.length, (index) {
|
||||
final employee = logs[index];
|
||||
final currentDate = employee.checkIn != null
|
||||
? DateFormat('dd MMM yyyy').format(employee.checkIn!)
|
||||
: '';
|
||||
final previousDate =
|
||||
index > 0 && logs[index - 1].checkIn != null
|
||||
? DateFormat('dd MMM yyyy')
|
||||
.format(logs[index - 1].checkIn!)
|
||||
: '';
|
||||
|
||||
final showDateHeader =
|
||||
index == 0 || currentDate != previousDate;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showDateHeader)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: MyText.bodyMedium(
|
||||
currentDate,
|
||||
fontWeight: 700,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: MyContainer(
|
||||
paddingAll: 8,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
size: 31,
|
||||
),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: MyText.bodyMedium(
|
||||
employee.name,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
MySpacing.width(6),
|
||||
Flexible(
|
||||
child: MyText.bodySmall(
|
||||
'(${employee.designation})',
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
(employee.checkIn != null ||
|
||||
employee.checkOut != null)
|
||||
? Row(
|
||||
children: [
|
||||
if (employee.checkIn !=
|
||||
null) ...[
|
||||
Icon(
|
||||
Icons
|
||||
.arrow_circle_right,
|
||||
size: 16,
|
||||
color: Colors.green),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
DateFormat('hh:mm a')
|
||||
.format(employee
|
||||
.checkIn!),
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow
|
||||
.ellipsis,
|
||||
),
|
||||
),
|
||||
MySpacing.width(16),
|
||||
],
|
||||
if (employee.checkOut !=
|
||||
null) ...[
|
||||
Icon(
|
||||
Icons.arrow_circle_left,
|
||||
size: 16,
|
||||
color: Colors.red),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
DateFormat('hh:mm a')
|
||||
.format(employee
|
||||
.checkOut!),
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow
|
||||
.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
MySpacing.height(12),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: AttendanceActionButton(
|
||||
employee: employee,
|
||||
attendanceController:
|
||||
attendanceController,
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Flexible(
|
||||
child: AttendanceLogViewButton(
|
||||
employee: employee,
|
||||
attendanceController:
|
||||
attendanceController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (index != logs.length - 1)
|
||||
Divider(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
thickness: 1,
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget regularizationScreen() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 4.0),
|
||||
child: MyText.titleMedium(
|
||||
"Regularization Requests",
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final employees = attendanceController.regularizationLogs;
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 8,
|
||||
child: attendanceController.isLoadingRegularizationLogs.value
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 32.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: employees.isEmpty
|
||||
? MyText.bodySmall(
|
||||
"No Regularization Requests Found for this Project",
|
||||
fontWeight: 600,
|
||||
)
|
||||
: Column(
|
||||
children: List.generate(employees.length, (index) {
|
||||
final employee = employees[index];
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: MyContainer(
|
||||
paddingAll: 8,
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
size: 31,
|
||||
),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: MyText.bodyMedium(
|
||||
employee.name,
|
||||
fontWeight: 600,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
MySpacing.width(6),
|
||||
Flexible(
|
||||
child: MyText.bodySmall(
|
||||
'(${employee.role})',
|
||||
fontWeight: 600,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
Row(
|
||||
children: [
|
||||
if (employee.checkIn !=
|
||||
null) ...[
|
||||
Icon(Icons.arrow_circle_right,
|
||||
size: 16,
|
||||
color: Colors.green),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
DateFormat('hh:mm a')
|
||||
.format(employee
|
||||
.checkIn!),
|
||||
fontWeight: 600,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
MySpacing.width(16),
|
||||
],
|
||||
if (employee.checkOut !=
|
||||
null) ...[
|
||||
Icon(Icons.arrow_circle_left,
|
||||
size: 16,
|
||||
color: Colors.red),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
DateFormat('hh:mm a')
|
||||
.format(employee
|
||||
.checkOut!),
|
||||
fontWeight: 600,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end,
|
||||
children: [
|
||||
RegularizeActionButton(
|
||||
attendanceController:
|
||||
attendanceController,
|
||||
log: employee,
|
||||
uniqueLogKey:
|
||||
employee.employeeId,
|
||||
action: ButtonActions.approve,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RegularizeActionButton(
|
||||
attendanceController:
|
||||
attendanceController,
|
||||
log: employee,
|
||||
uniqueLogKey:
|
||||
employee.employeeId,
|
||||
action: ButtonActions.reject,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (index != employees.length - 1)
|
||||
Divider(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
thickness: 1,
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
227
lib/view/dashboard/Attendence/attendence_log_screen.dart
Normal file
227
lib/view/dashboard/Attendence/attendence_log_screen.dart
Normal file
@ -0,0 +1,227 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/model/attendance/attendence_action_button.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/model/attendance/log_details_view.dart';
|
||||
|
||||
class AttendenceLogScreen extends StatefulWidget {
|
||||
const AttendenceLogScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AttendenceLogScreen> createState() => _AttendenceLogScreenState();
|
||||
}
|
||||
|
||||
class _AttendenceLogScreenState extends State<AttendenceLogScreen>
|
||||
with UIMixin {
|
||||
final AttendanceController attendanceController =
|
||||
Get.put(AttendanceController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder(
|
||||
init: attendanceController,
|
||||
tag: 'attendence_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Attendence",
|
||||
fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Attendence', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(children: [
|
||||
MyFlexItem(sizes: 'lg-12 md-12 sm-12', child: employeeLog()),
|
||||
]),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget employeeLog() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 8,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
attendanceController.employees.isEmpty
|
||||
? MyText.bodySmall("No Employees Assigned to This Project",
|
||||
fontWeight: 600)
|
||||
: Column(
|
||||
children: List.generate(attendanceController.employees.length,
|
||||
(index) {
|
||||
final employee = attendanceController.employees[index];
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: MyContainer(
|
||||
paddingAll: 12,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
size: 31,
|
||||
)
|
||||
],
|
||||
),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
'${employee.name} '
|
||||
' (${employee.designation})',
|
||||
fontWeight: 600,
|
||||
maxLines: null,
|
||||
overflow: TextOverflow.visible,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
Row(
|
||||
children: [
|
||||
MyText.bodySmall("In: ",
|
||||
fontWeight: 600),
|
||||
MyText.bodySmall(
|
||||
employee.checkIn != null
|
||||
? DateFormat('hh:mm a')
|
||||
.format(employee.checkIn!)
|
||||
: '--',
|
||||
fontWeight: 600,
|
||||
),
|
||||
MySpacing.width(16),
|
||||
MyText.bodySmall("Out: ",
|
||||
fontWeight: 600),
|
||||
MyText.bodySmall(
|
||||
employee.checkOut != null
|
||||
? DateFormat('hh:mm a')
|
||||
.format(employee.checkOut!)
|
||||
: '--',
|
||||
fontWeight: 600,
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
Row(
|
||||
children: [
|
||||
AttendanceLogViewButton(
|
||||
employee: employee,
|
||||
attendanceController:
|
||||
attendanceController,
|
||||
),
|
||||
MySpacing.width(8),
|
||||
AttendanceActionButton(
|
||||
employee: employee,
|
||||
attendanceController:
|
||||
attendanceController,
|
||||
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (index != attendanceController.employees.length - 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(),
|
||||
child: Divider(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
thickness: 1,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<DateTime?> showTimePickerForRegularization({
|
||||
required BuildContext context,
|
||||
required DateTime checkInTime,
|
||||
}) async {
|
||||
final pickedTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(DateTime.now()),
|
||||
);
|
||||
|
||||
if (pickedTime != null) {
|
||||
final selectedDateTime = DateTime(
|
||||
checkInTime.year,
|
||||
checkInTime.month,
|
||||
checkInTime.day,
|
||||
pickedTime.hour,
|
||||
pickedTime.minute,
|
||||
);
|
||||
|
||||
// Ensure selected time is after check-in time
|
||||
if (selectedDateTime.isAfter(checkInTime)) {
|
||||
return selectedDateTime;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Please select a time after check-in time.")),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user