diff --git a/lib/model/attendance/attendance_log_view_model.dart b/lib/model/attendance/attendance_log_view_model.dart index 1c8ebc1..96223f6 100644 --- a/lib/model/attendance/attendance_log_view_model.dart +++ b/lib/model/attendance/attendance_log_view_model.dart @@ -1,57 +1,114 @@ import 'package:intl/intl.dart'; -class AttendanceLogViewModel { - final DateTime? activityTime; - final String? imageUrl; - final String? comment; - final String? thumbPreSignedUrl; - final String? preSignedUrl; - final String? longitude; - final String? latitude; - final int? activity; +class Employee { + final String id; + final String firstName; + final String lastName; + final String? photo; + final String jobRoleId; + final String jobRoleName; - AttendanceLogViewModel({ - this.activityTime, - this.imageUrl, - this.comment, - this.thumbPreSignedUrl, - this.preSignedUrl, - this.longitude, - this.latitude, - required this.activity, + Employee({ + required this.id, + required this.firstName, + required this.lastName, + this.photo, + required this.jobRoleId, + required this.jobRoleName, }); - factory AttendanceLogViewModel.fromJson(Map json) { - return AttendanceLogViewModel( - activityTime: json['activityTime'] != null - ? DateTime.tryParse(json['activityTime']) - : null, - imageUrl: json['imageUrl']?.toString(), - comment: json['comment']?.toString(), - thumbPreSignedUrl: json['thumbPreSignedUrl']?.toString(), - preSignedUrl: json['preSignedUrl']?.toString(), - longitude: json['longitude']?.toString(), - latitude: json['latitude']?.toString(), - activity: json['activity'] ?? 0, + factory Employee.fromJson(Map json) { + return Employee( + id: json['id'], + firstName: json['firstName'] ?? '', + lastName: json['lastName'] ?? '', + photo: json['photo']?.toString(), + jobRoleId: json['jobRoleId'] ?? '', + jobRoleName: json['jobRoleName'] ?? '', ); } Map toJson() { return { - 'activityTime': activityTime?.toIso8601String(), - 'imageUrl': imageUrl, + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'photo': photo, + 'jobRoleId': jobRoleId, + 'jobRoleName': jobRoleName, + }; + } +} + +class AttendanceLogViewModel { + final String id; + final String? comment; + final Employee employee; + final DateTime? activityTime; + final int activity; + final String? photo; + final String? thumbPreSignedUrl; + final String? preSignedUrl; + final String? longitude; + final String? latitude; + final DateTime? updatedOn; + final Employee? updatedByEmployee; + final String? documentId; + + AttendanceLogViewModel({ + required this.id, + this.comment, + required this.employee, + this.activityTime, + required this.activity, + this.photo, + this.thumbPreSignedUrl, + this.preSignedUrl, + this.longitude, + this.latitude, + this.updatedOn, + this.updatedByEmployee, + this.documentId, + }); + + factory AttendanceLogViewModel.fromJson(Map json) { + return AttendanceLogViewModel( + id: json['id'], + comment: json['comment']?.toString(), + employee: Employee.fromJson(json['employee']), + activityTime: json['activityTime'] != null ? DateTime.tryParse(json['activityTime']) : null, + activity: json['activity'] ?? 0, + photo: json['photo']?.toString(), + thumbPreSignedUrl: json['thumbPreSignedUrl']?.toString(), + preSignedUrl: json['preSignedUrl']?.toString(), + longitude: json['longitude']?.toString(), + latitude: json['latitude']?.toString(), + updatedOn: json['updatedOn'] != null ? DateTime.tryParse(json['updatedOn']) : null, + updatedByEmployee: json['updatedByEmployee'] != null ? Employee.fromJson(json['updatedByEmployee']) : null, + documentId: json['documentId']?.toString(), + ); + } + + Map toJson() { + return { + 'id': id, 'comment': comment, + 'employee': employee.toJson(), + 'activityTime': activityTime?.toIso8601String(), + 'activity': activity, + 'photo': photo, 'thumbPreSignedUrl': thumbPreSignedUrl, 'preSignedUrl': preSignedUrl, 'longitude': longitude, 'latitude': latitude, - 'activity': activity, + 'updatedOn': updatedOn?.toIso8601String(), + 'updatedByEmployee': updatedByEmployee?.toJson(), + 'documentId': documentId, }; } - String? get formattedDate => activityTime != null - ? DateFormat('yyyy-MM-dd').format(activityTime!) - : null; + String? get formattedDate => + activityTime != null ? DateFormat('yyyy-MM-dd').format(activityTime!) : null; String? get formattedTime => activityTime != null ? DateFormat('hh:mm a').format(activityTime!) : null; diff --git a/lib/model/attendance/log_details_view.dart b/lib/model/attendance/log_details_view.dart index 79f57b4..d39098d 100644 --- a/lib/model/attendance/log_details_view.dart +++ b/lib/model/attendance/log_details_view.dart @@ -2,16 +2,24 @@ 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/base_bottom_sheet.dart'; +import 'package:marco/helpers/utils/date_time_utils.dart'; -class AttendanceLogViewButton extends StatelessWidget { +class AttendanceLogViewButton extends StatefulWidget { final dynamic employee; final dynamic attendanceController; + const AttendanceLogViewButton({ Key? key, required this.employee, required this.attendanceController, }) : super(key: key); + @override + State createState() => + _AttendanceLogViewButtonState(); +} + +class _AttendanceLogViewButtonState extends State { Future _openGoogleMaps( BuildContext context, double lat, double lon) async { final url = 'https://www.google.com/maps/search/?api=1&query=$lat,$lon'; @@ -48,7 +56,8 @@ class AttendanceLogViewButton extends StatelessWidget { } void _showLogsBottomSheet(BuildContext context) async { - await attendanceController.fetchLogsView(employee.id.toString()); + await widget.attendanceController + .fetchLogsView(widget.employee.id.toString()); showModalBottomSheet( context: context, @@ -57,157 +66,238 @@ class AttendanceLogViewButton extends StatelessWidget { borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), 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, + builder: (context) { + Map expandedDescription = {}; + + 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]; + + 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: [ - 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], - ), - ], - ), - ], + _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(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: 12), + MyText.bodySmall( + log.formattedTime != null + ? "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); - }, + 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) + 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, + ), + ], + ), ), - ), - ) - else - const Icon(Icons.broken_image, - size: 20, color: Colors.grey), - ], - ), - ], - ), - ); - }, - ), - ), + ], + ), + ], + ), + ); + }, + ); + }, + ), + ); + }, ); } @@ -218,16 +308,16 @@ class AttendanceLogViewButton extends StatelessWidget { child: ElevatedButton( onPressed: () => _showLogsBottomSheet(context), style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, + backgroundColor: Colors.indigo, textStyle: const TextStyle(fontSize: 12), padding: const EdgeInsets.symmetric(horizontal: 12), ), - child: const FittedBox( + child: FittedBox( fit: BoxFit.scaleDown, - child: Text( + child: MyText.bodySmall( "View", overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 12, color: Colors.white), + color: Colors.white, ), ), ), @@ -248,7 +338,7 @@ class AttendanceLogViewButton extends StatelessWidget { final today = DateTime(now.year, now.month, now.day); final logDay = DateTime(logDate.year, logDate.month, logDate.day); - final yesterday = today.subtract(Duration(days: 1)); + final yesterday = today.subtract(const Duration(days: 1)); isTodayOrYesterday = (logDay == today) || (logDay == yesterday); }