feat: enhance bottom sheet components with dynamic button visibility and improved styling

This commit is contained in:
Vaibhav Surve 2025-07-31 18:09:17 +05:30
parent 3427c5bd26
commit 797df80890
5 changed files with 312 additions and 391 deletions

View File

@ -11,6 +11,7 @@ class BaseBottomSheet extends StatelessWidget {
final String submitText; final String submitText;
final Color submitColor; final Color submitColor;
final IconData submitIcon; final IconData submitIcon;
final bool showButtons;
const BaseBottomSheet({ const BaseBottomSheet({
super.key, super.key,
@ -22,6 +23,7 @@ class BaseBottomSheet extends StatelessWidget {
this.submitText = 'Submit', this.submitText = 'Submit',
this.submitColor = Colors.indigo, this.submitColor = Colors.indigo,
this.submitIcon = Icons.check_circle_outline, this.submitIcon = Icons.check_circle_outline,
this.showButtons = true,
}); });
@override @override
@ -32,8 +34,7 @@ class BaseBottomSheet extends StatelessWidget {
return SingleChildScrollView( return SingleChildScrollView(
padding: mediaQuery.viewInsets, padding: mediaQuery.viewInsets,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(top: 60),
top: 60),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.cardColor, color: theme.cardColor,
@ -65,49 +66,48 @@ class BaseBottomSheet extends StatelessWidget {
MySpacing.height(12), MySpacing.height(12),
child, child,
MySpacing.height(24), MySpacing.height(24),
Row( if (showButtons)
children: [ Row(
Expanded( children: [
child: ElevatedButton.icon( Expanded(
onPressed: onCancel, child: ElevatedButton.icon(
icon: const Icon(Icons.close, color: Colors.white), onPressed: onCancel,
label: MyText.bodyMedium( icon: const Icon(Icons.close, color: Colors.white),
"Cancel", label: MyText.bodyMedium(
color: Colors.white, "Cancel",
fontWeight: 600, color: Colors.white,
), fontWeight: 600,
style: ElevatedButton.styleFrom( ),
backgroundColor: Colors.grey, style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder( backgroundColor: Colors.grey,
borderRadius: BorderRadius.circular(12), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 8),
), ),
padding: const EdgeInsets.symmetric(
vertical: 8),
), ),
), ),
), const SizedBox(width: 12),
const SizedBox(width: 12), Expanded(
Expanded( child: ElevatedButton.icon(
child: ElevatedButton.icon( onPressed: isSubmitting ? null : onSubmit,
onPressed: isSubmitting ? null : onSubmit, icon: Icon(submitIcon, color: Colors.white),
icon: Icon(submitIcon, color: Colors.white), label: MyText.bodyMedium(
label: MyText.bodyMedium( isSubmitting ? "Submitting..." : submitText,
isSubmitting ? "Submitting..." : submitText, color: Colors.white,
color: Colors.white, fontWeight: 600,
fontWeight: 600, ),
), style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( backgroundColor: submitColor,
backgroundColor: submitColor, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(12), ),
padding: const EdgeInsets.symmetric(vertical: 8),
), ),
padding: const EdgeInsets.symmetric(
vertical: 8),
), ),
), ),
), ],
], ),
),
], ],
), ),
), ),

View File

@ -5,16 +5,17 @@ import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/helpers/utils/attendance_actions.dart'; import 'package:marco/helpers/utils/attendance_actions.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class AttendanceActionButton extends StatefulWidget { class AttendanceActionButton extends StatefulWidget {
final dynamic employee; final dynamic employee;
final AttendanceController attendanceController; final AttendanceController attendanceController;
const AttendanceActionButton({ const AttendanceActionButton({
Key? key, super.key,
required this.employee, required this.employee,
required this.attendanceController, required this.attendanceController,
}) : super(key: key); });
@override @override
State<AttendanceActionButton> createState() => _AttendanceActionButtonState(); State<AttendanceActionButton> createState() => _AttendanceActionButtonState();
@ -24,81 +25,59 @@ Future<String?> _showCommentBottomSheet(
BuildContext context, String actionText) async { BuildContext context, String actionText) async {
final TextEditingController commentController = TextEditingController(); final TextEditingController commentController = TextEditingController();
String? errorText; String? errorText;
Get.find<ProjectController>().selectedProject?.id;
return showModalBottomSheet<String>( return showModalBottomSheet<String>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.white, backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
), ),
builder: (context) { builder: (context) {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setModalState) { 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( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 16, bottom: MediaQuery.of(context).viewInsets.bottom,
right: 16,
top: 24,
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
), ),
child: Column( child: BaseBottomSheet(
mainAxisSize: MainAxisSize.min, title: 'Add Comment for ${capitalizeFirstLetter(actionText)}',
children: [ onCancel: () => Navigator.of(context).pop(),
Text( onSubmit: submit,
'Add Comment for ${capitalizeFirstLetter(actionText)}', isSubmitting: false,
style: const TextStyle( submitText: 'Submit',
fontWeight: FontWeight.bold, child: Column(
fontSize: 16, mainAxisSize: MainAxisSize.min,
), children: [
), TextField(
const SizedBox(height: 16), controller: commentController,
TextField( maxLines: 4,
controller: commentController, decoration: InputDecoration(
maxLines: 4, hintText: 'Type your comment here...',
decoration: InputDecoration( border: OutlineInputBorder(
hintText: 'Type your comment here...', borderRadius: BorderRadius.circular(8),
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'),
), ),
filled: true,
fillColor: Colors.grey.shade100,
errorText: errorText,
), ),
const SizedBox(width: 12), onChanged: (_) {
Expanded( if (errorText != null) {
child: ElevatedButton( setModalState(() => errorText = null);
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'),
),
),
],
),
],
), ),
); );
}, },
@ -119,13 +98,15 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
void initState() { void initState() {
super.initState(); super.initState();
uniqueLogKey = AttendanceButtonHelper.getUniqueKey( uniqueLogKey = AttendanceButtonHelper.getUniqueKey(
widget.employee.employeeId, widget.employee.id); widget.employee.employeeId,
widget.employee.id,
);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!widget.attendanceController.uploadingStates widget.attendanceController.uploadingStates.putIfAbsent(
.containsKey(uniqueLogKey)) { uniqueLogKey,
widget.attendanceController.uploadingStates[uniqueLogKey] = false.obs; () => false.obs,
} );
}); });
} }
@ -167,6 +148,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
return selectedDateTime; return selectedDateTime;
} }
return null; return null;
} }
@ -228,7 +210,6 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
DateTime? selectedTime; DateTime? selectedTime;
// New condition: Yesterday Check-In + CheckOut action
final isYesterdayCheckIn = widget.employee.checkIn != null && final isYesterdayCheckIn = widget.employee.checkIn != null &&
DateUtils.isSameDay( DateUtils.isSameDay(
widget.employee.checkIn, widget.employee.checkIn,
@ -257,15 +238,16 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
} }
bool success = false; bool success = false;
if (actionText == ButtonActions.requestRegularize) { if (actionText == ButtonActions.requestRegularize) {
final regularizeTime = selectedTime ?? final regularizeTime = selectedTime ??
await showTimePickerForRegularization( await showTimePickerForRegularization(
context: context, context: context,
checkInTime: widget.employee.checkIn!, checkInTime: widget.employee.checkIn!,
); );
if (regularizeTime != null) { if (regularizeTime != null) {
final formattedSelectedTime = final formattedTime = DateFormat("hh:mm a").format(regularizeTime);
DateFormat("hh:mm a").format(regularizeTime);
success = await widget.attendanceController.captureAndUploadAttendance( success = await widget.attendanceController.captureAndUploadAttendance(
widget.employee.id, widget.employee.id,
widget.employee.employeeId, widget.employee.employeeId,
@ -273,12 +255,11 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
comment: userComment, comment: userComment,
action: updatedAction, action: updatedAction,
imageCapture: imageCapture, imageCapture: imageCapture,
markTime: formattedSelectedTime, markTime: formattedTime,
); );
} }
} else if (selectedTime != null) { } else if (selectedTime != null) {
// If selectedTime was picked in the new condition final formattedTime = DateFormat("hh:mm a").format(selectedTime);
final formattedSelectedTime = DateFormat("hh:mm a").format(selectedTime);
success = await widget.attendanceController.captureAndUploadAttendance( success = await widget.attendanceController.captureAndUploadAttendance(
widget.employee.id, widget.employee.id,
widget.employee.employeeId, widget.employee.employeeId,
@ -286,7 +267,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
comment: userComment, comment: userComment,
action: updatedAction, action: updatedAction,
imageCapture: imageCapture, imageCapture: imageCapture,
markTime: formattedSelectedTime, markTime: formattedTime,
); );
} else { } else {
success = await widget.attendanceController.captureAndUploadAttendance( success = await widget.attendanceController.captureAndUploadAttendance(
@ -312,8 +293,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
if (success) { if (success) {
widget.attendanceController.fetchEmployeesByProject(selectedProjectId); widget.attendanceController.fetchEmployeesByProject(selectedProjectId);
widget.attendanceController.fetchAttendanceLogs(selectedProjectId); widget.attendanceController.fetchAttendanceLogs(selectedProjectId);
await widget.attendanceController await widget.attendanceController.fetchRegularizationLogs(selectedProjectId);
.fetchRegularizationLogs(selectedProjectId);
await widget.attendanceController.fetchProjectData(selectedProjectId); await widget.attendanceController.fetchProjectData(selectedProjectId);
widget.attendanceController.update(); widget.attendanceController.update();
} }
@ -327,12 +307,19 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
false; false;
final isYesterday = AttendanceButtonHelper.isLogFromYesterday( final isYesterday = AttendanceButtonHelper.isLogFromYesterday(
widget.employee.checkIn, widget.employee.checkOut); widget.employee.checkIn,
widget.employee.checkOut,
);
final isTodayApproved = AttendanceButtonHelper.isTodayApproved( final isTodayApproved = AttendanceButtonHelper.isTodayApproved(
widget.employee.activity, widget.employee.checkIn); widget.employee.activity,
final isApprovedButNotToday = widget.employee.checkIn,
AttendanceButtonHelper.isApprovedButNotToday( );
widget.employee.activity, isTodayApproved);
final isApprovedButNotToday = AttendanceButtonHelper.isApprovedButNotToday(
widget.employee.activity,
isTodayApproved,
);
final isButtonDisabled = AttendanceButtonHelper.isButtonDisabled( final isButtonDisabled = AttendanceButtonHelper.isButtonDisabled(
isUploading: isUploading, isUploading: isUploading,
@ -359,8 +346,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
isButtonDisabled: isButtonDisabled, isButtonDisabled: isButtonDisabled,
buttonText: buttonText, buttonText: buttonText,
buttonColor: buttonColor, buttonColor: buttonColor,
onPressed: onPressed: isButtonDisabled ? null : () => _handleButtonPressed(context),
isButtonDisabled ? null : () => _handleButtonPressed(context),
); );
}); });
} }
@ -374,20 +360,20 @@ class AttendanceActionButtonUI extends StatelessWidget {
final VoidCallback? onPressed; final VoidCallback? onPressed;
const AttendanceActionButtonUI({ const AttendanceActionButtonUI({
Key? key, super.key,
required this.isUploading, required this.isUploading,
required this.isButtonDisabled, required this.isButtonDisabled,
required this.buttonText, required this.buttonText,
required this.buttonColor, required this.buttonColor,
required this.onPressed, required this.onPressed,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: 30, height: 30,
child: ElevatedButton( child: ElevatedButton(
onPressed: isButtonDisabled ? null : onPressed, onPressed: onPressed,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: buttonColor, backgroundColor: buttonColor,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),

View File

@ -4,6 +4,7 @@ import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class AttendanceFilterBottomSheet extends StatefulWidget { class AttendanceFilterBottomSheet extends StatefulWidget {
final AttendanceController controller; final AttendanceController controller;
@ -62,20 +63,20 @@ class _AttendanceFilterBottomSheetState
List<Widget> widgets = [ List<Widget> widgets = [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), padding: const EdgeInsets.only(bottom: 4),
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: MyText.titleSmall( child: MyText.titleSmall("View", fontWeight: 600),
"View",
fontWeight: 600,
),
), ),
), ),
...filteredViewOptions.map((item) { ...filteredViewOptions.map((item) {
return RadioListTile<String>( return RadioListTile<String>(
dense: true, dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12), contentPadding: EdgeInsets.zero,
title: Text(item['label']!), title: MyText.bodyMedium(
item['label']!,
fontWeight: 500,
),
value: item['value']!, value: item['value']!,
groupValue: tempSelectedTab, groupValue: tempSelectedTab,
onChanged: (value) => setState(() => tempSelectedTab = value!), onChanged: (value) => setState(() => tempSelectedTab = value!),
@ -87,49 +88,38 @@ class _AttendanceFilterBottomSheetState
widgets.addAll([ widgets.addAll([
const Divider(), const Divider(),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), padding: const EdgeInsets.only(top: 12, bottom: 4),
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: MyText.titleSmall( child: MyText.titleSmall("Date Range", fontWeight: 600),
"Date Range",
fontWeight: 600,
),
), ),
), ),
Padding( InkWell(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), borderRadius: BorderRadius.circular(10),
child: InkWell( onTap: () => widget.controller.selectDateRangeForAttendance(
borderRadius: BorderRadius.circular(10), context,
onTap: () => widget.controller.selectDateRangeForAttendance( widget.controller,
context, ),
widget.controller, child: Ink(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(10),
), ),
child: Ink( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration( child: Row(
color: Colors.white, children: [
border: Border.all(color: Colors.grey.shade400), const Icon(Icons.date_range, color: Colors.black87),
borderRadius: BorderRadius.circular(10), const SizedBox(width: 12),
), Expanded(
padding: child: MyText.bodyMedium(
const EdgeInsets.symmetric(horizontal: 16, vertical: 14), getLabelText(),
child: Row( fontWeight: 500,
children: [ color: Colors.black87,
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,
),
), ),
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return ClipRRect(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: SingleChildScrollView( 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( child: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: buildMainFilters(),
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,
});
},
),
),
),
],
), ),
), ),
); );

View File

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/utils/attendance_actions.dart'; import 'package:marco/helpers/utils/attendance_actions.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class AttendanceLogViewButton extends StatelessWidget { class AttendanceLogViewButton extends StatelessWidget {
final dynamic employee; final dynamic employee;
final dynamic attendanceController; // Use correct types as needed final dynamic attendanceController;
const AttendanceLogViewButton({ const AttendanceLogViewButton({
Key? key, Key? key,
required this.employee, required this.employee,
@ -50,191 +50,164 @@ class AttendanceLogViewButton extends StatelessWidget {
void _showLogsBottomSheet(BuildContext context) async { void _showLogsBottomSheet(BuildContext context) async {
await attendanceController.fetchLogsView(employee.id.toString()); await attendanceController.fetchLogsView(employee.id.toString());
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
), ),
backgroundColor: Theme.of(context).cardColor, backgroundColor: Colors.transparent,
builder: (context) => Padding( builder: (context) => BaseBottomSheet(
padding: EdgeInsets.only( title: "Attendance Log",
left: 16, onCancel: () => Navigator.pop(context),
right: 16, onSubmit: () => Navigator.pop(context),
top: 16, showButtons: false,
bottom: MediaQuery.of(context).viewInsets.bottom + 16, child: attendanceController.attendenceLogsView.isEmpty
), ? Padding(
child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, children: const [
children: [ Icon(Icons.info_outline, size: 40, color: Colors.grey),
// Header SizedBox(height: 8),
Row( Text("No attendance logs available."),
mainAxisAlignment: MainAxisAlignment.spaceBetween, ],
children: [ ),
MyText.titleMedium( )
"Attendance Log", : ListView.separated(
fontWeight: 700, shrinkWrap: true,
), physics: const NeverScrollableScrollPhysics(),
IconButton( itemCount: attendanceController.attendenceLogsView.length,
icon: const Icon(Icons.close), separatorBuilder: (_, __) => const SizedBox(height: 16),
onPressed: () => Navigator.pop(context), itemBuilder: (_, index) {
), final log = attendanceController.attendenceLogsView[index];
], return Container(
), decoration: BoxDecoration(
const SizedBox(height: 12), color: Theme.of(context).colorScheme.surfaceVariant,
if (attendanceController.attendenceLogsView.isEmpty) borderRadius: BorderRadius.circular(12),
Padding( boxShadow: [
padding: const EdgeInsets.symmetric(vertical: 24.0), BoxShadow(
child: Column( color: Colors.black.withOpacity(0.05),
children: const [ blurRadius: 6,
Icon(Icons.info_outline, size: 40, color: Colors.grey), offset: const Offset(0, 2),
SizedBox(height: 8), )
Text("No attendance logs available."), ],
], ),
), padding: const EdgeInsets.all(8),
) child: Column(
else crossAxisAlignment: CrossAxisAlignment.start,
ListView.separated( children: [
shrinkWrap: true, Row(
physics: const NeverScrollableScrollPhysics(), crossAxisAlignment: CrossAxisAlignment.center,
itemCount: attendanceController.attendenceLogsView.length, children: [
separatorBuilder: (_, __) => const SizedBox(height: 16), Expanded(
itemBuilder: (_, index) { flex: 3,
final log = attendanceController.attendenceLogsView[index]; child: Column(
return Container( crossAxisAlignment: CrossAxisAlignment.start,
decoration: BoxDecoration( children: [
color: Theme.of(context).colorScheme.surfaceVariant, Row(
borderRadius: BorderRadius.circular(12), children: [
boxShadow: [ _getLogIcon(log),
BoxShadow( const SizedBox(width: 10),
color: Colors.black.withOpacity(0.05), Column(
blurRadius: 6, crossAxisAlignment:
offset: const Offset(0, 2), CrossAxisAlignment.start,
) children: [
], MyText.bodyLarge(
), log.formattedDate ?? '-',
padding: const EdgeInsets.all(8), fontWeight: 600,
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),
),
), ),
Expanded( MyText.bodySmall(
child: MyText.bodyMedium( "Time: ${log.formattedTime ?? '-'}",
log.comment?.isNotEmpty == true color: Colors.grey[700],
? 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);
},
),
), ),
) const SizedBox(height: 12),
else Row(
const Icon(Icons.broken_image, crossAxisAlignment:
size: 20, color: Colors.grey), 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),
],
),
],
),
);
},
),
), ),
); );
} }

View File

@ -157,7 +157,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
Map<String, dynamic>>( Map<String, dynamic>>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.white,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical( borderRadius: BorderRadius.vertical(
top: Radius.circular(12)), top: Radius.circular(12)),