feat: enhance bottom sheet components with dynamic button visibility and improved styling
This commit is contained in:
parent
3427c5bd26
commit
797df80890
@ -11,6 +11,7 @@ class BaseBottomSheet extends StatelessWidget {
|
||||
final String submitText;
|
||||
final Color submitColor;
|
||||
final IconData submitIcon;
|
||||
final bool showButtons;
|
||||
|
||||
const BaseBottomSheet({
|
||||
super.key,
|
||||
@ -22,6 +23,7 @@ class BaseBottomSheet extends StatelessWidget {
|
||||
this.submitText = 'Submit',
|
||||
this.submitColor = Colors.indigo,
|
||||
this.submitIcon = Icons.check_circle_outline,
|
||||
this.showButtons = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -32,8 +34,7 @@ class BaseBottomSheet extends StatelessWidget {
|
||||
return SingleChildScrollView(
|
||||
padding: mediaQuery.viewInsets,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 60),
|
||||
padding: const EdgeInsets.only(top: 60),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
@ -65,49 +66,48 @@ class BaseBottomSheet extends StatelessWidget {
|
||||
MySpacing.height(12),
|
||||
child,
|
||||
MySpacing.height(24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onCancel,
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
label: MyText.bodyMedium(
|
||||
"Cancel",
|
||||
color: Colors.white,
|
||||
fontWeight: 600,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
if (showButtons)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onCancel,
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
label: MyText.bodyMedium(
|
||||
"Cancel",
|
||||
color: Colors.white,
|
||||
fontWeight: 600,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: isSubmitting ? null : onSubmit,
|
||||
icon: Icon(submitIcon, color: Colors.white),
|
||||
label: MyText.bodyMedium(
|
||||
isSubmitting ? "Submitting..." : submitText,
|
||||
color: Colors.white,
|
||||
fontWeight: 600,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: submitColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: isSubmitting ? null : onSubmit,
|
||||
icon: Icon(submitIcon, color: Colors.white),
|
||||
label: MyText.bodyMedium(
|
||||
isSubmitting ? "Submitting..." : submitText,
|
||||
color: Colors.white,
|
||||
fontWeight: 600,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: submitColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -5,16 +5,17 @@ import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||
import 'package:marco/helpers/utils/attendance_actions.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||
|
||||
class AttendanceActionButton extends StatefulWidget {
|
||||
final dynamic employee;
|
||||
final AttendanceController attendanceController;
|
||||
|
||||
const AttendanceActionButton({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.employee,
|
||||
required this.attendanceController,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<AttendanceActionButton> createState() => _AttendanceActionButtonState();
|
||||
@ -24,81 +25,59 @@ Future<String?> _showCommentBottomSheet(
|
||||
BuildContext context, String actionText) async {
|
||||
final TextEditingController commentController = TextEditingController();
|
||||
String? errorText;
|
||||
Get.find<ProjectController>().selectedProject?.id;
|
||||
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
void submit() {
|
||||
final comment = commentController.text.trim();
|
||||
if (comment.isEmpty) {
|
||||
setModalState(() => errorText = 'Comment cannot be empty.');
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(comment);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 24,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Add Comment for ${capitalizeFirstLetter(actionText)}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: commentController,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your comment here...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
errorText: errorText,
|
||||
),
|
||||
onChanged: (_) {
|
||||
if (errorText != null) {
|
||||
setModalState(() => errorText = null);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
child: BaseBottomSheet(
|
||||
title: 'Add Comment for ${capitalizeFirstLetter(actionText)}',
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
onSubmit: submit,
|
||||
isSubmitting: false,
|
||||
submitText: 'Submit',
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: commentController,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your comment here...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
errorText: errorText,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final comment = commentController.text.trim();
|
||||
if (comment.isEmpty) {
|
||||
setModalState(() {
|
||||
errorText = 'Comment cannot be empty.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(comment);
|
||||
},
|
||||
child: const Text('Submit'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onChanged: (_) {
|
||||
if (errorText != null) {
|
||||
setModalState(() => errorText = null);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -119,13 +98,15 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
uniqueLogKey = AttendanceButtonHelper.getUniqueKey(
|
||||
widget.employee.employeeId, widget.employee.id);
|
||||
widget.employee.employeeId,
|
||||
widget.employee.id,
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!widget.attendanceController.uploadingStates
|
||||
.containsKey(uniqueLogKey)) {
|
||||
widget.attendanceController.uploadingStates[uniqueLogKey] = false.obs;
|
||||
}
|
||||
widget.attendanceController.uploadingStates.putIfAbsent(
|
||||
uniqueLogKey,
|
||||
() => false.obs,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -167,6 +148,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
|
||||
return selectedDateTime;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -228,7 +210,6 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
|
||||
DateTime? selectedTime;
|
||||
|
||||
// ✅ New condition: Yesterday Check-In + CheckOut action
|
||||
final isYesterdayCheckIn = widget.employee.checkIn != null &&
|
||||
DateUtils.isSameDay(
|
||||
widget.employee.checkIn,
|
||||
@ -257,15 +238,16 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
|
||||
if (actionText == ButtonActions.requestRegularize) {
|
||||
final regularizeTime = selectedTime ??
|
||||
await showTimePickerForRegularization(
|
||||
context: context,
|
||||
checkInTime: widget.employee.checkIn!,
|
||||
);
|
||||
|
||||
if (regularizeTime != null) {
|
||||
final formattedSelectedTime =
|
||||
DateFormat("hh:mm a").format(regularizeTime);
|
||||
final formattedTime = DateFormat("hh:mm a").format(regularizeTime);
|
||||
success = await widget.attendanceController.captureAndUploadAttendance(
|
||||
widget.employee.id,
|
||||
widget.employee.employeeId,
|
||||
@ -273,12 +255,11 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
comment: userComment,
|
||||
action: updatedAction,
|
||||
imageCapture: imageCapture,
|
||||
markTime: formattedSelectedTime,
|
||||
markTime: formattedTime,
|
||||
);
|
||||
}
|
||||
} else if (selectedTime != null) {
|
||||
// ✅ If selectedTime was picked in the new condition
|
||||
final formattedSelectedTime = DateFormat("hh:mm a").format(selectedTime);
|
||||
final formattedTime = DateFormat("hh:mm a").format(selectedTime);
|
||||
success = await widget.attendanceController.captureAndUploadAttendance(
|
||||
widget.employee.id,
|
||||
widget.employee.employeeId,
|
||||
@ -286,7 +267,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
comment: userComment,
|
||||
action: updatedAction,
|
||||
imageCapture: imageCapture,
|
||||
markTime: formattedSelectedTime,
|
||||
markTime: formattedTime,
|
||||
);
|
||||
} else {
|
||||
success = await widget.attendanceController.captureAndUploadAttendance(
|
||||
@ -312,8 +293,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
if (success) {
|
||||
widget.attendanceController.fetchEmployeesByProject(selectedProjectId);
|
||||
widget.attendanceController.fetchAttendanceLogs(selectedProjectId);
|
||||
await widget.attendanceController
|
||||
.fetchRegularizationLogs(selectedProjectId);
|
||||
await widget.attendanceController.fetchRegularizationLogs(selectedProjectId);
|
||||
await widget.attendanceController.fetchProjectData(selectedProjectId);
|
||||
widget.attendanceController.update();
|
||||
}
|
||||
@ -327,12 +307,19 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
false;
|
||||
|
||||
final isYesterday = AttendanceButtonHelper.isLogFromYesterday(
|
||||
widget.employee.checkIn, widget.employee.checkOut);
|
||||
widget.employee.checkIn,
|
||||
widget.employee.checkOut,
|
||||
);
|
||||
|
||||
final isTodayApproved = AttendanceButtonHelper.isTodayApproved(
|
||||
widget.employee.activity, widget.employee.checkIn);
|
||||
final isApprovedButNotToday =
|
||||
AttendanceButtonHelper.isApprovedButNotToday(
|
||||
widget.employee.activity, isTodayApproved);
|
||||
widget.employee.activity,
|
||||
widget.employee.checkIn,
|
||||
);
|
||||
|
||||
final isApprovedButNotToday = AttendanceButtonHelper.isApprovedButNotToday(
|
||||
widget.employee.activity,
|
||||
isTodayApproved,
|
||||
);
|
||||
|
||||
final isButtonDisabled = AttendanceButtonHelper.isButtonDisabled(
|
||||
isUploading: isUploading,
|
||||
@ -359,8 +346,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
isButtonDisabled: isButtonDisabled,
|
||||
buttonText: buttonText,
|
||||
buttonColor: buttonColor,
|
||||
onPressed:
|
||||
isButtonDisabled ? null : () => _handleButtonPressed(context),
|
||||
onPressed: isButtonDisabled ? null : () => _handleButtonPressed(context),
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -374,20 +360,20 @@ class AttendanceActionButtonUI extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const AttendanceActionButtonUI({
|
||||
Key? key,
|
||||
super.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,
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: buttonColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
|
@ -4,6 +4,7 @@ import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||
|
||||
class AttendanceFilterBottomSheet extends StatefulWidget {
|
||||
final AttendanceController controller;
|
||||
@ -62,20 +63,20 @@ class _AttendanceFilterBottomSheetState
|
||||
|
||||
List<Widget> widgets = [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: MyText.titleSmall(
|
||||
"View",
|
||||
fontWeight: 600,
|
||||
),
|
||||
child: MyText.titleSmall("View", fontWeight: 600),
|
||||
),
|
||||
),
|
||||
...filteredViewOptions.map((item) {
|
||||
return RadioListTile<String>(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
title: Text(item['label']!),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: MyText.bodyMedium(
|
||||
item['label']!,
|
||||
fontWeight: 500,
|
||||
),
|
||||
value: item['value']!,
|
||||
groupValue: tempSelectedTab,
|
||||
onChanged: (value) => setState(() => tempSelectedTab = value!),
|
||||
@ -87,49 +88,38 @@ class _AttendanceFilterBottomSheetState
|
||||
widgets.addAll([
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 4),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: MyText.titleSmall(
|
||||
"Date Range",
|
||||
fontWeight: 600,
|
||||
),
|
||||
child: MyText.titleSmall("Date Range", fontWeight: 600),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => widget.controller.selectDateRangeForAttendance(
|
||||
context,
|
||||
widget.controller,
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => widget.controller.selectDateRangeForAttendance(
|
||||
context,
|
||||
widget.controller,
|
||||
),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.date_range, color: Colors.black87),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
getLabelText(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.date_range, color: Colors.black87),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
getLabelText(),
|
||||
fontWeight: 500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.black87),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.black87),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -141,49 +131,20 @@ class _AttendanceFilterBottomSheetState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: SingleChildScrollView(
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
child: BaseBottomSheet(
|
||||
title: "Attendance Filter",
|
||||
onCancel: () => Navigator.pop(context),
|
||||
onSubmit: () => Navigator.pop(context, {
|
||||
'selectedTab': tempSelectedTab,
|
||||
}),
|
||||
submitText: "Apply Filter",
|
||||
submitIcon: Icons.filter_alt_outlined,
|
||||
submitColor: const Color.fromARGB(255, 95, 132, 255),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[400],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...buildMainFilters(),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color.fromARGB(255, 95, 132, 255),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Apply Filter'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, {
|
||||
'selectedTab': tempSelectedTab,
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: buildMainFilters(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/utils/attendance_actions.dart';
|
||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||
|
||||
class AttendanceLogViewButton extends StatelessWidget {
|
||||
final dynamic employee;
|
||||
final dynamic attendanceController; // Use correct types as needed
|
||||
|
||||
final dynamic attendanceController;
|
||||
const AttendanceLogViewButton({
|
||||
Key? key,
|
||||
required this.employee,
|
||||
@ -50,191 +50,164 @@ class AttendanceLogViewButton extends StatelessWidget {
|
||||
|
||||
void _showLogsBottomSheet(BuildContext context) async {
|
||||
await attendanceController.fetchLogsView(employee.id.toString());
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium(
|
||||
"Attendance Log",
|
||||
fontWeight: 700,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (attendanceController.attendenceLogsView.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: Column(
|
||||
children: const [
|
||||
Icon(Icons.info_outline, size: 40, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text("No attendance logs available."),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: attendanceController.attendenceLogsView.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 16),
|
||||
itemBuilder: (_, index) {
|
||||
final log = attendanceController.attendenceLogsView[index];
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_getLogIcon(log),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyLarge(
|
||||
log.formattedDate ?? '-',
|
||||
fontWeight: 600,
|
||||
),
|
||||
MyText.bodySmall(
|
||||
"Time: ${log.formattedTime ?? '-'}",
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (log.latitude != null &&
|
||||
log.longitude != null)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final lat = double.tryParse(log
|
||||
.latitude
|
||||
.toString()) ??
|
||||
0.0;
|
||||
final lon = double.tryParse(log
|
||||
.longitude
|
||||
.toString()) ??
|
||||
0.0;
|
||||
if (lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lon >= -180 &&
|
||||
lon <= 180) {
|
||||
_openGoogleMaps(
|
||||
context, lat, lon);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Invalid location coordinates')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Padding(
|
||||
padding:
|
||||
EdgeInsets.only(right: 8.0),
|
||||
child: Icon(Icons.location_on,
|
||||
size: 18, color: Colors.blue),
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => BaseBottomSheet(
|
||||
title: "Attendance Log",
|
||||
onCancel: () => Navigator.pop(context),
|
||||
onSubmit: () => Navigator.pop(context),
|
||||
showButtons: false,
|
||||
child: attendanceController.attendenceLogsView.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: Column(
|
||||
children: const [
|
||||
Icon(Icons.info_outline, size: 40, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text("No attendance logs available."),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: attendanceController.attendenceLogsView.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 16),
|
||||
itemBuilder: (_, index) {
|
||||
final log = attendanceController.attendenceLogsView[index];
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_getLogIcon(log),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyLarge(
|
||||
log.formattedDate ?? '-',
|
||||
fontWeight: 600,
|
||||
),
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
log.comment?.isNotEmpty == true
|
||||
? log.comment
|
||||
: "No description provided",
|
||||
fontWeight: 500,
|
||||
MyText.bodySmall(
|
||||
"Time: ${log.formattedTime ?? '-'}",
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
if (log.thumbPreSignedUrl != null)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (log.preSignedUrl != null) {
|
||||
_showImageDialog(
|
||||
context, log.preSignedUrl!);
|
||||
}
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
log.thumbPreSignedUrl!,
|
||||
height: 60,
|
||||
width: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) {
|
||||
return const Icon(Icons.broken_image,
|
||||
size: 20, color: Colors.grey);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const Icon(Icons.broken_image,
|
||||
size: 20, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (log.latitude != null &&
|
||||
log.longitude != null)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final lat = double.tryParse(
|
||||
log.latitude.toString()) ??
|
||||
0.0;
|
||||
final lon = double.tryParse(
|
||||
log.longitude.toString()) ??
|
||||
0.0;
|
||||
if (lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lon >= -180 &&
|
||||
lon <= 180) {
|
||||
_openGoogleMaps(
|
||||
context, lat, lon);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Invalid location coordinates')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Padding(
|
||||
padding:
|
||||
EdgeInsets.only(right: 8.0),
|
||||
child: Icon(Icons.location_on,
|
||||
size: 18, color: Colors.blue),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
log.comment?.isNotEmpty == true
|
||||
? log.comment
|
||||
: "No description provided",
|
||||
fontWeight: 500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
if (log.thumbPreSignedUrl != null)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (log.preSignedUrl != null) {
|
||||
_showImageDialog(
|
||||
context, log.preSignedUrl!);
|
||||
}
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
log.thumbPreSignedUrl!,
|
||||
height: 60,
|
||||
width: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(Icons.broken_image,
|
||||
size: 20, color: Colors.grey);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const Icon(Icons.broken_image,
|
||||
size: 20, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -157,7 +157,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12)),
|
||||
|
Loading…
x
Reference in New Issue
Block a user