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 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),
),
),
),
],
),
],
),
],
),
),

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/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),

View File

@ -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(),
),
),
);

View File

@ -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),
],
),
],
),
);
},
),
),
);
}

View File

@ -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)),