- Added TaskListModel for managing daily tasks with JSON parsing. - Introduced WorkStatusResponseModel and WorkStatus for handling work status data. - Created MenuResponse and MenuItem models for dynamic menu management. - Updated routes to reflect correct naming conventions for task planning screens. - Enhanced DashboardScreen to include dynamic menu functionality and improved task statistics display. - Developed DailyProgressReportScreen for displaying daily progress reports with filtering options. - Implemented DailyTaskPlanningScreen for planning daily tasks with detailed views and actions. - Refactored left navigation bar to align with updated task planning routes.
679 lines
23 KiB
Dart
679 lines
23 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
// --- Assumed Imports (ensure these paths are correct in your project) ---
|
|
import 'package:marco/controller/task_planning/report_task_controller.dart';
|
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
import 'package:marco/helpers/widgets/my_button.dart';
|
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
import 'package:marco/helpers/widgets/my_text.dart';
|
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
|
import 'package:marco/helpers/widgets/avatar.dart';
|
|
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
|
|
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
|
import 'package:marco/model/dailyTaskPlanning/create_task_botom_sheet.dart';
|
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
|
|
|
// --- Form Field Keys (Unchanged) ---
|
|
class _FormFieldKeys {
|
|
static const String assignedDate = 'assigned_date';
|
|
static const String assignedBy = 'assigned_by';
|
|
static const String workArea = 'work_area';
|
|
static const String activity = 'activity';
|
|
static const String plannedWork = 'planned_work';
|
|
static const String completedWork = 'completed_work';
|
|
static const String teamMembers = 'team_members';
|
|
static const String assigned = 'assigned';
|
|
static const String taskId = 'task_id';
|
|
static const String comment = 'comment';
|
|
}
|
|
|
|
// --- Main Widget: CommentTaskBottomSheet ---
|
|
class CommentTaskBottomSheet extends StatefulWidget {
|
|
final Map<String, dynamic> taskData;
|
|
final VoidCallback? onCommentSuccess;
|
|
final String taskDataId;
|
|
final String workAreaId;
|
|
final String activityId;
|
|
|
|
const CommentTaskBottomSheet({
|
|
super.key,
|
|
required this.taskData,
|
|
this.onCommentSuccess,
|
|
required this.taskDataId,
|
|
required this.workAreaId,
|
|
required this.activityId,
|
|
});
|
|
|
|
@override
|
|
State<CommentTaskBottomSheet> createState() => _CommentTaskBottomSheetState();
|
|
}
|
|
|
|
class _Member {
|
|
final String firstName;
|
|
_Member(this.firstName);
|
|
}
|
|
|
|
class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|
with UIMixin {
|
|
late final ReportTaskController controller;
|
|
List<Map<String, dynamic>> _sortedComments = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
controller = Get.put(ReportTaskController(),
|
|
tag: widget.taskData['taskId'] ?? UniqueKey().toString());
|
|
_initializeControllerData();
|
|
|
|
final comments = List<Map<String, dynamic>>.from(
|
|
widget.taskData['taskComments'] as List? ?? []);
|
|
comments.sort((a, b) {
|
|
final aDate = DateTime.tryParse(a['date'] ?? '') ??
|
|
DateTime.fromMillisecondsSinceEpoch(0);
|
|
final bDate = DateTime.tryParse(b['date'] ?? '') ??
|
|
DateTime.fromMillisecondsSinceEpoch(0);
|
|
return bDate.compareTo(aDate); // Newest first
|
|
});
|
|
_sortedComments = comments;
|
|
}
|
|
|
|
void _initializeControllerData() {
|
|
final data = widget.taskData;
|
|
|
|
final fieldMappings = {
|
|
_FormFieldKeys.assignedDate: data['assignedOn'],
|
|
_FormFieldKeys.assignedBy: data['assignedBy'],
|
|
_FormFieldKeys.workArea: data['location'],
|
|
_FormFieldKeys.activity: data['activity'],
|
|
_FormFieldKeys.plannedWork: data['plannedWork'],
|
|
_FormFieldKeys.completedWork: data['completedWork'],
|
|
_FormFieldKeys.teamMembers: (data['teamMembers'] as List?)?.join(', '),
|
|
_FormFieldKeys.assigned: data['assigned'],
|
|
_FormFieldKeys.taskId: data['taskId'],
|
|
};
|
|
|
|
for (final entry in fieldMappings.entries) {
|
|
controller.basicValidator.getController(entry.key)?.text =
|
|
entry.value ?? '';
|
|
}
|
|
|
|
controller.basicValidator.getController(_FormFieldKeys.comment)?.clear();
|
|
controller.selectedImages.clear();
|
|
}
|
|
|
|
String _timeAgo(String dateString) {
|
|
// This logic remains unchanged
|
|
try {
|
|
final date = DateTime.parse(dateString + "Z").toLocal();
|
|
final difference = DateTime.now().difference(date);
|
|
|
|
if (difference.inDays > 8) return DateFormat('dd-MM-yyyy').format(date);
|
|
if (difference.inDays >= 1)
|
|
return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago';
|
|
if (difference.inHours >= 1)
|
|
return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago';
|
|
if (difference.inMinutes >= 1)
|
|
return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago';
|
|
return 'just now';
|
|
} catch (e) {
|
|
debugPrint('Error parsing date for timeAgo: $e');
|
|
return dateString;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// --- REFACTORING POINT ---
|
|
// The entire widget now returns a BaseBottomSheet, passing the content as its child.
|
|
// The GetBuilder provides reactive state (like isLoading) to the BaseBottomSheet.
|
|
return GetBuilder<ReportTaskController>(
|
|
tag: widget.taskData['taskId'] ?? '',
|
|
builder: (controller) {
|
|
return BaseBottomSheet(
|
|
title: "Task Details & Comments",
|
|
onCancel: () => Navigator.of(context).pop(),
|
|
onSubmit: _submitComment,
|
|
isSubmitting: controller.isLoading.value,
|
|
bottomContent: _buildCommentsSection(),
|
|
child: Form(
|
|
// moved to last
|
|
key: controller.basicValidator.formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildHeaderActions(),
|
|
MySpacing.height(12),
|
|
_buildTaskDetails(),
|
|
_buildReportedImages(),
|
|
_buildCommentInput(),
|
|
_buildImagePicker(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// --- REFACTORING POINT ---
|
|
// The original _buildHeader is now split. The title is handled by BaseBottomSheet.
|
|
// This new widget contains the remaining actions from the header.
|
|
Widget _buildHeaderActions() {
|
|
return Align(
|
|
alignment: Alignment.centerRight,
|
|
child: InkWell(
|
|
onTap: () => _showCreateTaskBottomSheet(),
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blueAccent.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: MyText.bodySmall(
|
|
"+ Create Task",
|
|
fontWeight: 600,
|
|
color: Colors.blueAccent,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTaskDetails() {
|
|
return Column(
|
|
children: [
|
|
_buildDetailRow(
|
|
"Assigned By",
|
|
controller.basicValidator
|
|
.getController(_FormFieldKeys.assignedBy)
|
|
?.text,
|
|
icon: Icons.person_outline),
|
|
_buildDetailRow(
|
|
"Work Area",
|
|
controller.basicValidator
|
|
.getController(_FormFieldKeys.workArea)
|
|
?.text,
|
|
icon: Icons.place_outlined),
|
|
_buildDetailRow(
|
|
"Activity",
|
|
controller.basicValidator
|
|
.getController(_FormFieldKeys.activity)
|
|
?.text,
|
|
icon: Icons.assignment_outlined),
|
|
_buildDetailRow(
|
|
"Planned Work",
|
|
controller.basicValidator
|
|
.getController(_FormFieldKeys.plannedWork)
|
|
?.text,
|
|
icon: Icons.schedule_outlined),
|
|
_buildDetailRow(
|
|
"Completed Work",
|
|
controller.basicValidator
|
|
.getController(_FormFieldKeys.completedWork)
|
|
?.text,
|
|
icon: Icons.done_all_outlined),
|
|
_buildTeamMembers(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildReportedImages() {
|
|
final imageUrls =
|
|
List<String>.from(widget.taskData['reportedPreSignedUrls'] ?? []);
|
|
if (imageUrls.isEmpty) return const SizedBox.shrink();
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
child: _buildSectionHeader("Reported Images", Icons.image_outlined),
|
|
),
|
|
// --- Refactoring Note ---
|
|
// Using the reusable _ImageHorizontalListView widget.
|
|
_ImageHorizontalListView(
|
|
imageSources: imageUrls,
|
|
onPreview: (index) => _showImageViewer(imageUrls, index),
|
|
),
|
|
MySpacing.height(16),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCommentInput() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionHeader("Add Comment", Icons.comment_outlined),
|
|
MySpacing.height(8),
|
|
TextFormField(
|
|
validator:
|
|
controller.basicValidator.getValidation(_FormFieldKeys.comment),
|
|
controller:
|
|
controller.basicValidator.getController(_FormFieldKeys.comment),
|
|
keyboardType: TextInputType.multiline,
|
|
maxLines: null, // Allows for multiline input
|
|
decoration: InputDecoration(
|
|
hintText: "eg: Work done successfully",
|
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
border: outlineInputBorder,
|
|
enabledBorder: outlineInputBorder,
|
|
focusedBorder: focusedInputBorder,
|
|
contentPadding: MySpacing.all(16),
|
|
isCollapsed: true,
|
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
),
|
|
),
|
|
MySpacing.height(16),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildImagePicker() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionHeader("Attach Photos", Icons.camera_alt_outlined),
|
|
MySpacing.height(12),
|
|
Obx(() {
|
|
final images = controller.selectedImages;
|
|
return Column(
|
|
children: [
|
|
// --- Refactoring Note ---
|
|
// Using the reusable _ImageHorizontalListView for picked images.
|
|
_ImageHorizontalListView(
|
|
imageSources: images.toList(),
|
|
onPreview: (index) => _showImageViewer(images.toList(), index),
|
|
onRemove: (index) => controller.removeImageAt(index),
|
|
emptyStatePlaceholder: Container(
|
|
height: 70,
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.grey.shade300, width: 1.5),
|
|
color: Colors.grey.shade100,
|
|
),
|
|
child: Center(
|
|
child: Icon(Icons.photo_library_outlined,
|
|
size: 36, color: Colors.grey.shade400),
|
|
),
|
|
),
|
|
),
|
|
MySpacing.height(16),
|
|
Row(
|
|
children: [
|
|
_buildPickerButton(
|
|
onTap: () => controller.pickImages(fromCamera: true),
|
|
icon: Icons.camera_alt,
|
|
label: 'Capture',
|
|
),
|
|
MySpacing.width(12),
|
|
_buildPickerButton(
|
|
onTap: () => controller.pickImages(fromCamera: false),
|
|
icon: Icons.upload_file,
|
|
label: 'Upload',
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCommentsSection() {
|
|
if (_sortedComments.isEmpty) return const SizedBox.shrink();
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MySpacing.height(24),
|
|
_buildSectionHeader("Comments", Icons.chat_bubble_outline),
|
|
MySpacing.height(12),
|
|
// --- Refactoring Note ---
|
|
// Using a ListView instead of a fixed-height SizedBox for better responsiveness.
|
|
// It's constrained by the parent SingleChildScrollView.
|
|
ListView.builder(
|
|
shrinkWrap:
|
|
true, // Important for ListView inside SingleChildScrollView
|
|
physics:
|
|
const NeverScrollableScrollPhysics(), // Parent handles scrolling
|
|
itemCount: _sortedComments.length,
|
|
itemBuilder: (context, index) {
|
|
final comment = _sortedComments[index];
|
|
// --- Refactoring Note ---
|
|
// Extracted the comment item into its own widget for clarity.
|
|
return _CommentCard(
|
|
comment: comment,
|
|
timeAgo: _timeAgo(comment['date'] ?? ''),
|
|
onPreviewImage: (imageUrls, idx) =>
|
|
_showImageViewer(imageUrls, idx),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// --- Helper and Builder methods ---
|
|
|
|
Widget _buildDetailRow(String label, String? value,
|
|
{required IconData icon}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 8.0, top: 2),
|
|
child: Icon(icon, size: 18, color: Colors.grey[700]),
|
|
),
|
|
MyText.titleSmall("$label:", fontWeight: 600),
|
|
MySpacing.width(12),
|
|
Expanded(
|
|
child: MyText.bodyMedium(
|
|
value != null && value.isNotEmpty ? value : "-",
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionHeader(String title, IconData icon) {
|
|
return Row(
|
|
children: [
|
|
Icon(icon, size: 18, color: Colors.grey[700]),
|
|
MySpacing.width(8),
|
|
MyText.titleSmall(title, fontWeight: 600),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTeamMembers() {
|
|
final teamMembersText = controller.basicValidator
|
|
.getController(_FormFieldKeys.teamMembers)
|
|
?.text ??
|
|
'';
|
|
final members = teamMembersText
|
|
.split(',')
|
|
.map((e) => e.trim())
|
|
.where((e) => e.isNotEmpty)
|
|
.toList();
|
|
if (members.isEmpty) return const SizedBox.shrink();
|
|
|
|
const double avatarSize = 32.0;
|
|
const double avatarOverlap = 22.0;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 16.0),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.group_outlined, size: 18, color: Colors.grey[700]),
|
|
MySpacing.width(8),
|
|
MyText.titleSmall("Team:", fontWeight: 600),
|
|
MySpacing.width(12),
|
|
GestureDetector(
|
|
onTap: () => TeamBottomSheet.show(
|
|
context: context,
|
|
teamMembers: members.map((name) => _Member(name)).toList()),
|
|
child: SizedBox(
|
|
height: avatarSize,
|
|
// Calculate width based on number of avatars shown
|
|
width: (math.min(members.length, 3) * avatarOverlap) +
|
|
(avatarSize - avatarOverlap),
|
|
child: Stack(
|
|
children: [
|
|
...List.generate(math.min(members.length, 3), (i) {
|
|
return Positioned(
|
|
left: i * avatarOverlap,
|
|
child: Tooltip(
|
|
message: members[i],
|
|
child: Avatar(
|
|
firstName: members[i],
|
|
lastName: '',
|
|
size: avatarSize),
|
|
),
|
|
);
|
|
}),
|
|
if (members.length > 3)
|
|
Positioned(
|
|
left: 3 * avatarOverlap,
|
|
child: CircleAvatar(
|
|
radius: avatarSize / 2,
|
|
backgroundColor: Colors.grey.shade300,
|
|
child: MyText.bodySmall('+${members.length - 3}',
|
|
fontWeight: 600),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPickerButton(
|
|
{required VoidCallback onTap,
|
|
required IconData icon,
|
|
required String label}) {
|
|
return Expanded(
|
|
child: MyButton.outlined(
|
|
onPressed: onTap,
|
|
padding: MySpacing.xy(12, 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 18, color: Colors.blueAccent),
|
|
MySpacing.width(8),
|
|
MyText.bodySmall(label, color: Colors.blueAccent, fontWeight: 600),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// --- Action Handlers ---
|
|
|
|
void _showCreateTaskBottomSheet() {
|
|
showCreateTaskBottomSheet(
|
|
workArea: widget.taskData['location'] ?? '',
|
|
activity: widget.taskData['activity'] ?? '',
|
|
completedWork: widget.taskData['completedWork'] ?? '',
|
|
unit: widget.taskData['unit'] ?? '',
|
|
onCategoryChanged: (category) =>
|
|
debugPrint("Category changed to: $category"),
|
|
parentTaskId: widget.taskDataId,
|
|
plannedTask: int.tryParse(widget.taskData['plannedWork'] ?? '0') ?? 0,
|
|
activityId: widget.activityId,
|
|
workAreaId: widget.workAreaId,
|
|
onSubmit: () => Navigator.of(context).pop(),
|
|
);
|
|
}
|
|
|
|
void _showImageViewer(List<dynamic> sources, int initialIndex) {
|
|
showDialog(
|
|
context: context,
|
|
barrierColor: Colors.black87,
|
|
builder: (_) => ImageViewerDialog(
|
|
imageSources: sources,
|
|
initialIndex: initialIndex,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _submitComment() async {
|
|
if (controller.basicValidator.validateForm()) {
|
|
await controller.commentTask(
|
|
projectId: controller.basicValidator
|
|
.getController(_FormFieldKeys.taskId)
|
|
?.text ??
|
|
'',
|
|
comment: controller.basicValidator
|
|
.getController(_FormFieldKeys.comment)
|
|
?.text ??
|
|
'',
|
|
images: controller.selectedImages,
|
|
);
|
|
// Callback to the parent widget to refresh data if needed
|
|
widget.onCommentSuccess?.call();
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Refactoring Note ---
|
|
// A reusable widget for displaying a horizontal list of images.
|
|
// It can handle both network URLs (String) and local files (File).
|
|
class _ImageHorizontalListView extends StatelessWidget {
|
|
final List<dynamic> imageSources; // Can be List<String> or List<File>
|
|
final Function(int) onPreview;
|
|
final Function(int)? onRemove;
|
|
final Widget? emptyStatePlaceholder;
|
|
|
|
const _ImageHorizontalListView({
|
|
required this.imageSources,
|
|
required this.onPreview,
|
|
this.onRemove,
|
|
this.emptyStatePlaceholder,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (imageSources.isEmpty) {
|
|
return emptyStatePlaceholder ?? const SizedBox.shrink();
|
|
}
|
|
|
|
return SizedBox(
|
|
height: 70,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: imageSources.length,
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
itemBuilder: (context, index) {
|
|
final source = imageSources[index];
|
|
return GestureDetector(
|
|
onTap: () => onPreview(index),
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: source is File
|
|
? Image.file(source,
|
|
width: 70, height: 70, fit: BoxFit.cover)
|
|
: Image.network(
|
|
source as String,
|
|
width: 70,
|
|
height: 70,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
Container(
|
|
width: 70,
|
|
height: 70,
|
|
color: Colors.grey.shade200,
|
|
child: Icon(Icons.broken_image,
|
|
color: Colors.grey[600]),
|
|
),
|
|
),
|
|
),
|
|
if (onRemove != null)
|
|
Positioned(
|
|
top: -6,
|
|
right: -6,
|
|
child: GestureDetector(
|
|
onTap: () => onRemove!(index),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(2),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.red, shape: BoxShape.circle),
|
|
child: const Icon(Icons.close,
|
|
size: 16, color: Colors.white),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// --- Refactoring Note ---
|
|
// A dedicated widget for a single comment card. This cleans up the main
|
|
// widget's build method and makes the comment layout easier to manage.
|
|
class _CommentCard extends StatelessWidget {
|
|
final Map<String, dynamic> comment;
|
|
final String timeAgo;
|
|
final Function(List<String> imageUrls, int index) onPreviewImage;
|
|
|
|
const _CommentCard({
|
|
required this.comment,
|
|
required this.timeAgo,
|
|
required this.onPreviewImage,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final commentedBy = comment['commentedBy'] ?? 'Unknown';
|
|
final commentText = comment['text'] ?? '-';
|
|
final imageUrls = List<String>.from(comment['preSignedUrls'] ?? []);
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(vertical: 6),
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.grey.shade200)),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Avatar(
|
|
firstName: commentedBy.split(' ').first,
|
|
lastName: commentedBy.split(' ').length > 1
|
|
? commentedBy.split(' ').last
|
|
: '',
|
|
size: 32,
|
|
),
|
|
MySpacing.width(12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MyText.bodyMedium(commentedBy,
|
|
fontWeight: 700, color: Colors.black87),
|
|
MyText.bodySmall(timeAgo,
|
|
color: Colors.black54, fontSize: 12),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(12),
|
|
MyText.bodyMedium(commentText, color: Colors.black87),
|
|
if (imageUrls.isNotEmpty) ...[
|
|
MySpacing.height(12),
|
|
_ImageHorizontalListView(
|
|
imageSources: imageUrls,
|
|
onPreview: (index) => onPreviewImage(imageUrls, index),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|