From 17fc04f3ee41eb3c3cf2c6eda276889246d547ee Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 18 Nov 2025 12:11:30 +0530 Subject: [PATCH] feat: add cached_network_image dependency and improve image handling in attendance logs --- lib/model/attendance/log_details_view.dart | 526 ++++++++++----------- pubspec.yaml | 1 + 2 files changed, 239 insertions(+), 288 deletions(-) diff --git a/lib/model/attendance/log_details_view.dart b/lib/model/attendance/log_details_view.dart index d39098d..598703f 100644 --- a/lib/model/attendance/log_details_view.dart +++ b/lib/model/attendance/log_details_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; @@ -15,13 +16,11 @@ class AttendanceLogViewButton extends StatefulWidget { }) : super(key: key); @override - State createState() => - _AttendanceLogViewButtonState(); + State createState() => _AttendanceLogViewButtonState(); } class _AttendanceLogViewButtonState extends State { - Future _openGoogleMaps( - BuildContext context, double lat, double lon) async { + Future _openGoogleMaps(BuildContext context, double lat, double lon) async { final url = 'https://www.google.com/maps/search/?api=1&query=$lat,$lon'; if (await canLaunchUrl(Uri.parse(url))) { await launchUrl( @@ -39,25 +38,20 @@ class _AttendanceLogViewButtonState extends State { showDialog( context: context, builder: (_) => Dialog( - child: Image.network( - imageUrl, - fit: BoxFit.cover, - height: 400, - errorBuilder: (context, error, stackTrace) { - return const Icon( - Icons.broken_image, - size: 50, - color: Colors.grey, - ); - }, + child: InteractiveViewer( + child: CachedNetworkImage( + imageUrl: imageUrl, + fit: BoxFit.contain, + errorWidget: (_, __, ___) => const Icon(Icons.broken_image, size: 50, color: Colors.grey), + ), ), ), ); } - void _showLogsBottomSheet(BuildContext context) async { - await widget.attendanceController - .fetchLogsView(widget.employee.id.toString()); + Future _showLogsBottomSheet(BuildContext context) async { + await widget.attendanceController.fetchLogsView(widget.employee.id.toString()); + Map expandedDescription = {}; showModalBottomSheet( context: context, @@ -66,238 +60,244 @@ class _AttendanceLogViewButtonState extends State { borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), backgroundColor: Colors.transparent, - builder: (context) { - Map expandedDescription = {}; + builder: (context) => BaseBottomSheet( + title: "Attendance Log", + showButtons: false, + onCancel: () => Navigator.pop(context), + onSubmit: () => Navigator.pop(context), + child: widget.attendanceController.attendenceLogsView.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 32.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, size: 40, color: Colors.grey), + SizedBox(height: 12), + MyText.bodySmall("No attendance logs available."), + ], + ), + ) + : StatefulBuilder( + builder: (context, setStateSB) => ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget.attendanceController.attendenceLogsView.length, + separatorBuilder: (_, __) => const SizedBox(height: 16), + itemBuilder: (_, index) { + final log = widget.attendanceController.attendenceLogsView[index]; - return BaseBottomSheet( - title: "Attendance Log", - onCancel: () => Navigator.pop(context), - onSubmit: () => Navigator.pop(context), - showButtons: false, - child: widget.attendanceController.attendenceLogsView.isEmpty - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 24.0), - child: Column( - children: [ - Icon(Icons.info_outline, size: 40, color: Colors.grey), - SizedBox(height: 8), - MyText.bodySmall("No attendance logs available."), - ], - ), - ) - : StatefulBuilder( - builder: (context, setStateSB) { - return ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: - widget.attendanceController.attendenceLogsView.length, - separatorBuilder: (_, __) => const SizedBox(height: 16), - itemBuilder: (_, index) { - final log = widget - .attendanceController.attendenceLogsView[index]; + // Determine if the log date is today or yesterday + bool isTodayOrYesterday = false; + try { + if (log.formattedDate != null) { + final logDate = DateTime.parse(log.formattedDate); + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final logDay = DateTime(logDate.year, logDate.month, logDate.day); + final yesterday = today.subtract(const Duration(days: 1)); + isTodayOrYesterday = (logDay == today) || (logDay == yesterday); + } + } catch (_) { + isTodayOrYesterday = false; + } - 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), - ) + Widget _logIcon() { + int activity = log.activity ?? -1; + IconData iconData; + Color iconColor; + + switch (activity) { + case 0: + case 1: + iconData = Icons.arrow_circle_right; + iconColor = Colors.green; + break; + case 2: + iconData = Icons.hourglass_top; + iconColor = Colors.blueGrey; + break; + case 4: + if (isTodayOrYesterday) { + iconData = Icons.arrow_circle_left; + iconColor = Colors.red; + } else { + iconData = Icons.check; + iconColor = Colors.green; + } + break; + case 5: + iconData = Icons.close; + iconColor = Colors.red; + break; + default: + iconData = Icons.info; + iconColor = Colors.grey; + } + return Icon(iconData, color: iconColor, size: 20); + } + + 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(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header: Icon + Date + Time + Row( + children: [ + _logIcon(), + const SizedBox(width: 12), + MyText.bodyLarge( + (log.formattedDate != null && log.formattedDate!.isNotEmpty) + ? DateTimeUtils.convertUtcToLocal( + log.formattedDate!, + format: 'd MMM yyyy', + ) + : '-', + fontWeight: 600, + ), + const SizedBox(width: 12), + MyText.bodySmall( + log.formattedTime != null ? "Time: ${log.formattedTime}" : "", + color: Colors.grey[700], + ), ], ), - padding: const EdgeInsets.all(12), - child: Column( + const SizedBox(height: 12), + const Divider(height: 1, color: Colors.grey), + const SizedBox(height: 12), + + // Middle row: Image + Text + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header: Icon + Date + Time - Row( - children: [ - _getLogIcon(log), - const SizedBox(width: 12), - MyText.bodyLarge( - (log.formattedDate != null && - log.formattedDate!.isNotEmpty) - ? DateTimeUtils.convertUtcToLocal( - log.formattedDate!, - format: 'd MMM yyyy', - ) - : '-', - fontWeight: 600, - ), - const SizedBox(width: 12), - MyText.bodySmall( - log.formattedTime != null - ? "Time: ${log.formattedTime}" - : "", - color: Colors.grey[700], - ), - ], - ), - const SizedBox(height: 12), - const Divider(height: 1, color: Colors.grey), - // Middle Row: Image + Text (Done by, Description & Location) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Image Column - 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: (_, __, ___) => - const Icon(Icons.broken_image, - size: 40, color: Colors.grey), + if (log.thumbPreSignedUrl != null) + GestureDetector( + onTap: () { + if (log.preSignedUrl != null) { + _showImageDialog(context, log.preSignedUrl!); + } + }, + child: SizedBox( + width: 60, + height: 60, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: log.thumbPreSignedUrl!, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(strokeWidth: 2)), + errorWidget: (context, url, error) => const Icon( + Icons.broken_image, + size: 40, + color: Colors.grey, ), ), ), - if (log.thumbPreSignedUrl != null) - const SizedBox(width: 12), - - // Text Column - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - // Done by - if (log.updatedByEmployee != null) - MyText.bodySmall( - "By: ${log.updatedByEmployee!.firstName} ${log.updatedByEmployee!.lastName}", - color: Colors.grey[700], - ), - - const SizedBox(height: 8), - - // Location - if (log.latitude != null && - log.longitude != null) - Row( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - - 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( - SnackBar( - content: MyText.bodySmall( - "Invalid location coordinates")), - ); - } - }, - child: Row( - children: [ - Icon(Icons.location_on, - size: 16, - color: Colors.blue), - SizedBox(width: 4), - MyText.bodySmall( - "View Location", - color: Colors.blue, - decoration: - TextDecoration.underline, - ), - ], - ), - ), - ], - ), - const SizedBox(height: 8), - - // Description with label and more/less using MyText - if (log.comment != null && - log.comment!.isNotEmpty) - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - MyText.bodySmall( - "Description: ${log.comment!}", - maxLines: expandedDescription[ - index] == - true - ? null - : 2, - overflow: expandedDescription[ - index] == - true - ? TextOverflow.visible - : TextOverflow.ellipsis, - ), - if (log.comment!.length > 100) - GestureDetector( - onTap: () { - setStateSB(() { - expandedDescription[ - index] = - !(expandedDescription[ - index] == - true); - }); - }, - child: MyText.bodySmall( - expandedDescription[ - index] == - true - ? "less" - : "more", - color: Colors.blue, - fontWeight: 600, - ), - ), - ], - ) - else - MyText.bodySmall( - "Description: No description provided", - fontWeight: 700, - ), - ], - ), ), - ], + ), + if (log.thumbPreSignedUrl != null) const SizedBox(width: 12), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (log.updatedByEmployee != null) + MyText.bodySmall( + "By: ${log.updatedByEmployee!.firstName} ${log.updatedByEmployee!.lastName}", + color: Colors.grey[700], + ), + const SizedBox(height: 8), + + 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( + SnackBar( + content: MyText.bodySmall("Invalid location coordinates"), + ), + ); + } + }, + child: Row( + children: [ + const Icon(Icons.location_on, size: 16, color: Colors.blue), + const SizedBox(width: 4), + MyText.bodySmall( + "View Location", + color: Colors.blue, + decoration: TextDecoration.underline, + ), + ], + ), + ), + if (log.latitude != null && log.longitude != null) + const SizedBox(height: 8), + + if (log.comment != null && log.comment!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodySmall( + "Description: ${log.comment!}", + maxLines: + expandedDescription[index] == true ? null : 2, + overflow: expandedDescription[index] == true + ? TextOverflow.visible + : TextOverflow.ellipsis, + ), + if (log.comment!.length > 100) + GestureDetector( + onTap: () { + setStateSB(() { + expandedDescription[index] = + !(expandedDescription[index] == true); + }); + }, + child: MyText.bodySmall( + expandedDescription[index] == true ? "less" : "more", + color: Colors.blue, + fontWeight: 600, + ), + ), + ], + ) + else + MyText.bodySmall( + "Description: No description provided", + fontWeight: 700, + ), + ], + ), ), ], ), - ); - }, + ], + ), ); }, ), - ); - }, + ), + ), ); } @@ -311,69 +311,19 @@ class _AttendanceLogViewButtonState extends State { backgroundColor: Colors.indigo, textStyle: const TextStyle(fontSize: 12), padding: const EdgeInsets.symmetric(horizontal: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + elevation: 3, ), - child: FittedBox( + child: FittedBox( fit: BoxFit.scaleDown, child: MyText.bodySmall( "View", overflow: TextOverflow.ellipsis, color: Colors.white, + fontWeight: 600, ), ), ), ); } - - Widget _getLogIcon(dynamic log) { - int activity = log.activity ?? -1; - - IconData iconData; - Color iconColor; - bool isTodayOrYesterday = false; - - try { - if (log.formattedDate != null) { - final logDate = DateTime.parse(log.formattedDate); - final now = DateTime.now(); - - final today = DateTime(now.year, now.month, now.day); - final logDay = DateTime(logDate.year, logDate.month, logDate.day); - final yesterday = today.subtract(const Duration(days: 1)); - - isTodayOrYesterday = (logDay == today) || (logDay == yesterday); - } - } catch (_) { - isTodayOrYesterday = false; - } - - switch (activity) { - case 0: - case 1: - iconData = Icons.arrow_circle_right; - iconColor = Colors.green; - break; - case 2: - iconData = Icons.hourglass_top; - iconColor = Colors.blueGrey; - break; - case 4: - if (isTodayOrYesterday) { - iconData = Icons.arrow_circle_left; - iconColor = Colors.red; - } else { - iconData = Icons.check; - iconColor = Colors.green; - } - break; - case 5: - iconData = Icons.close; - iconColor = Colors.red; - break; - default: - iconData = Icons.info; - iconColor = Colors.grey; - } - - return Icon(iconData, color: iconColor, size: 20); - } } diff --git a/pubspec.yaml b/pubspec.yaml index 59cdd11..9b88f89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,6 +82,7 @@ dependencies: equatable: ^2.0.7 mime: ^2.0.0 timeago: ^3.7.1 + cached_network_image: ^3.4.1 timeline_tile: ^2.0.0 dev_dependencies: