marco.pms.mobileapp/lib/view/finance/payment_request_detail_screen.dart

884 lines
32 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/finance/payment_request_detail_controller.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/helpers/utils/date_time_utils.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:timeline_tile/timeline_tile.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/model/finance/payment_request_details_model.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:marco/model/expense/comment_bottom_sheet.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/finance/payment_request_rembursement_bottom_sheet.dart';
import 'package:marco/model/finance/make_expense_bottom_sheet.dart';
import 'package:marco/model/finance/add_payment_request_bottom_sheet.dart';
class PaymentRequestDetailScreen extends StatefulWidget {
final String paymentRequestId;
const PaymentRequestDetailScreen({super.key, required this.paymentRequestId});
@override
State<PaymentRequestDetailScreen> createState() =>
_PaymentRequestDetailScreenState();
}
class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
with UIMixin {
final controller = Get.put(PaymentRequestDetailController());
final projectController = Get.find<ProjectController>();
final permissionController = Get.put(PermissionController());
final RxBool canSubmit = false.obs;
bool _checkedPermission = false;
EmployeeInfo? employeeInfo;
@override
void initState() {
super.initState();
controller.init(widget.paymentRequestId);
_loadEmployeeInfo();
}
void _checkPermissionToSubmit(PaymentRequestData request) {
const draftStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7';
final isCreatedByCurrentUser = employeeInfo?.id == request.createdBy?.id;
final hasDraftNextStatus =
(request.nextStatus ?? []).any((s) => (s.id ?? '') == draftStatusId);
canSubmit.value = isCreatedByCurrentUser && hasDraftNextStatus;
}
Future<void> _loadEmployeeInfo() async {
employeeInfo = await LocalStorage.getEmployeeInfo();
setState(() {});
}
Color _parseColor(String? hexColor) {
try {
if (hexColor == null || hexColor.trim().isEmpty) return Colors.grey;
String hex = hexColor.toUpperCase().replaceAll('#', '');
if (hex.length == 6) hex = 'FF$hex';
return Color(int.parse(hex, radix: 16));
} catch (_) {
return Colors.grey;
}
}
void _openEditPaymentRequestBottomSheet(PaymentRequestData request) {
showPaymentRequestBottomSheet(
isEdit: true,
existingData: {
"id": request.id ?? '',
"paymentRequestId": request.paymentRequestUID ?? '',
"title": request.title ?? '',
"projectId": request.project?.id ?? '',
"projectName": request.project?.name ?? '',
"expenseCategoryId": request.expenseCategory?.id ?? '',
"expenseCategoryName": request.expenseCategory?.name ?? '',
"amount": (request.amount ?? 0).toString(),
"currencyId": request.currency?.id ?? '',
"currencySymbol": request.currency?.symbol ?? '',
"payee": request.payee ?? '',
"description": request.description ?? '',
"isAdvancePayment": request.isAdvancePayment ?? false,
"dueDate": request.dueDate?.toIso8601String() ?? '',
"attachments": (request.attachments ?? [])
.map((a) => {
"url": a.url ?? '',
"fileName": a.fileName ?? '',
"documentId": a.id ?? '',
"contentType": a.contentType ?? '',
})
.toList(),
},
onUpdated: () async {
await controller.fetchPaymentRequestDetail();
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: SafeArea(
child: Obx(() {
if (controller.isLoading.value &&
controller.paymentRequest.value == null) {
return SkeletonLoaders.paymentRequestDetailSkeletonLoader();
}
final request = controller.paymentRequest.value;
if ((controller.errorMessage.value).isNotEmpty) {
return Center(
child: MyText.bodyMedium(controller.errorMessage.value));
}
if (request == null) {
return Center(child: MyText.bodyMedium("No data to display."));
}
return MyRefreshIndicator(
onRefresh: controller.fetchPaymentRequestDetail,
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(
12,
12,
12,
60 + MediaQuery.of(context).padding.bottom,
),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 520),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Header(
request: request,
colorParser: _parseColor,
employeeInfo: employeeInfo,
onEdit: () =>
_openEditPaymentRequestBottomSheet(request),
),
const Divider(height: 30, thickness: 1.2),
_Logs(
logs: request.updateLogs ?? [],
colorParser: _parseColor,
),
const Divider(height: 30, thickness: 1.2),
_Parties(request: request),
const Divider(height: 30, thickness: 1.2),
_DetailsTable(request: request),
const Divider(height: 30, thickness: 1.2),
_Documents(documents: request.attachments ?? []),
MySpacing.height(24),
],
),
),
),
),
),
),
);
}),
),
bottomNavigationBar: _buildBottomActionBar(),
);
}
Widget _buildBottomActionBar() {
return Obx(() {
final request = controller.paymentRequest.value;
if (request == null ||
controller.isLoading.value ||
employeeInfo == null) {
return const SizedBox.shrink();
}
if (!_checkedPermission) {
_checkedPermission = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkPermissionToSubmit(request);
});
}
const reimbursementStatusId = '61578360-3a49-4c34-8604-7b35a3787b95';
const draftStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7';
final availableStatuses = (request.nextStatus ?? []).where((status) {
if ((status.id ?? '') == draftStatusId) {
return employeeInfo?.id == request.createdBy?.id;
}
return permissionController
.hasAnyPermission(status.permissionIds ?? []);
}).toList();
if (availableStatuses.isEmpty) {
return const SizedBox.shrink();
}
return SafeArea(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey.shade300)),
),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 10,
runSpacing: 10,
children: availableStatuses.map((status) {
final color = _parseColor(status.color);
return ElevatedButton(
style: ElevatedButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
backgroundColor: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () async {
final statusId = status.id ?? '';
final dispName = status.displayName ?? '';
if (statusId == reimbursementStatusId) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius:
BorderRadius.vertical(top: Radius.circular(5)),
),
builder: (ctx) => UpdatePaymentRequestWithReimbursement(
expenseId: request.paymentRequestUID ?? '',
statusId: statusId,
onClose: () {},
),
);
} else if (statusId ==
'b8586f67-dc19-49c3-b4af-224149efe1d3') {
showCreateExpenseBottomSheet(
statusId: statusId,
);
} else {
final comment =
await showCommentBottomSheet(context, dispName);
if (comment == null || comment.trim().isEmpty) return;
final success = await controller.updatePaymentRequestStatus(
statusId: statusId,
comment: comment.trim(),
);
showAppSnackbar(
title: success ? 'Success' : 'Error',
message: success
? 'Status updated successfully'
: 'Failed to update status',
type: success ? SnackbarType.success : SnackbarType.error,
);
if (success) await controller.fetchPaymentRequestDetail();
}
},
child: MyText.bodySmall(
status.displayName ?? '',
color: Colors.white,
fontWeight: 600,
),
);
}).toList(),
),
),
);
});
}
PreferredSizeWidget _buildAppBar() {
return PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.back(),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Payment Request Details',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(builder: (_) {
final name = projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
}),
],
),
),
],
),
),
),
);
}
}
class PaymentRequestPermissionHelper {
static bool canEditPaymentRequest(
EmployeeInfo? employee, PaymentRequestData request) {
return employee?.id == request.createdBy?.id &&
_isInAllowedEditStatus(request.expenseStatus?.id ?? '');
}
static bool canSubmitPaymentRequest(
EmployeeInfo? employee, PaymentRequestData request) {
return employee?.id == request.createdBy?.id &&
(request.nextStatus ?? []).isNotEmpty;
}
static bool _isInAllowedEditStatus(String statusId) {
const editableStatusIds = [
"d1ee5eec-24b6-4364-8673-a8f859c60729",
"965eda62-7907-4963-b4a1-657fb0b2724b",
"297e0d8f-f668-41b5-bfea-e03b354251c8",
];
return editableStatusIds.contains(statusId);
}
}
// ------------------ Sub-widgets ------------------
// Header widget
class _Header extends StatefulWidget {
final PaymentRequestData request;
final Color Function(String?) colorParser;
final VoidCallback? onEdit;
final EmployeeInfo? employeeInfo;
const _Header({
required this.request,
required this.colorParser,
this.onEdit,
this.employeeInfo,
});
@override
State<_Header> createState() => _HeaderState();
}
class _HeaderState extends State<_Header> with UIMixin {
@override
Widget build(BuildContext context) {
final statusColor = widget.colorParser(widget.request.expenseStatus?.color);
final canEdit = widget.employeeInfo != null &&
PaymentRequestPermissionHelper.canEditPaymentRequest(
widget.employeeInfo, widget.request);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
MyText.bodyMedium(
'ID: ${widget.request.paymentRequestUID ?? '-'}',
fontWeight: 700,
fontSize: 14,
),
// 🔥 ADVANCE CHIP — show only if true
if (widget.request.isAdvancePayment == true) ...[
const SizedBox(width: 8),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange.shade700,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'ADVANCE',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
if (canEdit)
IconButton(
onPressed: widget.onEdit,
icon: Icon(Icons.edit, color: contentTheme.primary),
tooltip: "Edit Payment Request",
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
const Icon(Icons.calendar_month,
size: 18, color: Colors.grey),
MySpacing.width(6),
MyText.bodySmall('Created At:', fontWeight: 600),
MySpacing.width(6),
Expanded(
child: MyText.bodySmall(
DateTimeUtils.convertUtcToLocal(
widget.request.createdAt?.toIso8601String() ?? '',
format: 'dd MMM yyyy'),
fontWeight: 600,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Container(
decoration: BoxDecoration(
color: statusColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(5)),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
child: Row(
children: [
Icon(Icons.flag, size: 16, color: statusColor),
MySpacing.width(4),
SizedBox(
width: 100,
child: MyText.labelSmall(
widget.request.expenseStatus?.displayName ?? '-',
color: statusColor,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
],
);
}
}
// Logs widget
class _Logs extends StatelessWidget {
final List<UpdateLog> logs;
final Color Function(String?) colorParser;
const _Logs({required this.logs, required this.colorParser});
@override
Widget build(BuildContext context) {
if (logs.isEmpty) {
return MyText.bodyMedium('No Timeline', color: Colors.grey);
}
final reversedLogs = logs.reversed.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodySmall("Timeline:", fontWeight: 600),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: reversedLogs.length,
itemBuilder: (_, index) {
final log = reversedLogs[index];
final status = log.status?.name ?? 'Unknown';
final description = log.status?.description ?? '';
final statusColor = log.status != null
? colorParser(log.status!.color)
: Colors.grey;
final comment = log.comment ?? '';
final nextStatusName = log.nextStatus?.name ?? '';
final updatedBy = log.updatedBy;
final first = updatedBy?.firstName ?? '';
final last = updatedBy?.lastName ?? '';
final initials =
'${first.isNotEmpty ? first[0] : ''}${last.isNotEmpty ? last[0] : ''}';
final name =
((first + ' ' + last).trim().isNotEmpty) ? '$first $last' : '-';
final updatedAt = log.updatedAt;
final timeAgo = (updatedAt != null)
? timeago.format(updatedAt
.toUtc()
.add(const Duration(hours: 5, minutes: 30)))
: '-';
final nextStatusColor = colorParser(log.nextStatus?.color ?? '');
return TimelineTile(
alignment: TimelineAlign.start,
isFirst: index == 0,
isLast: index == reversedLogs.length - 1,
indicatorStyle: IndicatorStyle(
width: 16,
height: 16,
indicator: Container(
decoration:
BoxDecoration(shape: BoxShape.circle, color: statusColor),
),
),
beforeLineStyle:
LineStyle(color: Colors.grey.shade300, thickness: 2),
endChild: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyMedium(status,
fontWeight: 600, color: statusColor),
MyText.bodySmall(timeAgo,
color: Colors.grey[600],
textAlign: TextAlign.right),
],
),
if (description.isNotEmpty) ...[
const SizedBox(height: 4),
MyText.bodySmall(description, color: Colors.grey[800]),
],
if (comment.isNotEmpty) ...[
const SizedBox(height: 8),
MyText.bodyMedium(comment, fontWeight: 500),
],
const SizedBox(height: 8),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4),
),
child: MyText.bodySmall(initials, fontWeight: 600),
),
const SizedBox(width: 6),
Expanded(
child: MyText.bodySmall(name,
overflow: TextOverflow.ellipsis)),
if (nextStatusName.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: nextStatusColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(4),
),
child: MyText.bodySmall(nextStatusName,
fontWeight: 600, color: nextStatusColor),
),
],
),
],
),
),
);
},
)
],
);
}
}
// Parties widget
class _Parties extends StatelessWidget {
final PaymentRequestData request;
const _Parties({required this.request});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodySmall("Parties:", fontWeight: 600),
MySpacing.height(8),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium("Payee", fontWeight: 600),
MySpacing.height(2),
MyText.bodyMedium(request.payee ?? '-'),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium("Project", fontWeight: 600),
MySpacing.height(2),
MyText.bodyMedium(request.project?.name ?? '-'),
],
),
),
],
),
],
);
}
}
// Details table widget
class _DetailsTable extends StatelessWidget {
final PaymentRequestData request;
const _DetailsTable({required this.request});
String _formatCurrencyAmount(String? symbol, double? amount) {
final sym = symbol ?? '';
final amt = amount ?? 0.0;
return '$sym ${amt.toStringAsFixed(2)}';
}
@override
Widget build(BuildContext context) {
final currencySymbol = request.currency?.symbol ?? '';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Basic Info
_labelValueRow("Payment Request ID:", request.paymentRequestUID ?? '-'),
if ((request.paidTransactionId ?? '').isNotEmpty)
_labelValueRow("Transaction ID:", request.paidTransactionId ?? ''),
_labelValueRow("Payee:", request.payee ?? '-'),
_labelValueRow("Project:", request.project?.name ?? '-'),
_labelValueRow(
"Expense Category:", request.expenseCategory?.name ?? '-'),
// Amounts
_labelValueRow(
"Amount:", _formatCurrencyAmount(currencySymbol, request.amount)),
if (request.baseAmount != null)
_labelValueRow("Base Amount:",
_formatCurrencyAmount(currencySymbol, request.baseAmount)),
if (request.taxAmount != null)
_labelValueRow("Tax Amount:",
_formatCurrencyAmount(currencySymbol, request.taxAmount)),
if (request.expenseCategory?.noOfPersonsRequired == true)
_labelValueRow("Additional Persons Required:", "Yes"),
if (request.expenseCategory?.isAttachmentRequried == true)
_labelValueRow("Attachment Required:", "Yes"),
// Dates
_labelValueRow(
"Due Date:",
DateTimeUtils.convertUtcToLocal(
request.dueDate?.toIso8601String() ?? '',
format: 'dd MMM yyyy')),
_labelValueRow(
"Created At:",
DateTimeUtils.convertUtcToLocal(
request.createdAt?.toIso8601String() ?? '',
format: 'dd MMM yyyy')),
_labelValueRow(
"Updated At:",
DateTimeUtils.convertUtcToLocal(
request.updatedAt?.toIso8601String() ?? '',
format: 'dd MMM yyyy')),
// Payment Info
if (request.paidAt != null)
_labelValueRow(
"Transaction Date:",
DateTimeUtils.convertUtcToLocal(
request.paidAt?.toIso8601String() ?? '',
format: 'dd MMM yyyy'),
),
if (request.paidBy != null)
_labelValueRow(
"Paid By:",
"${request.paidBy?.firstName ?? ''} ${request.paidBy?.lastName ?? ''}"
.trim()),
// Flags
_labelValueRow("Advance Payment:",
(request.isAdvancePayment ?? false) ? "Yes" : "No"),
_labelValueRow("Expense Created:",
(request.isExpenseCreated ?? false) ? "Yes" : "No"),
_labelValueRow("Active:", (request.isActive ?? false) ? "Yes" : "No"),
// Recurring Payment Info
if (request.recurringPayment != null) ...[
const SizedBox(height: 6),
MyText.bodySmall("Recurring Payment Info:", fontWeight: 600),
_labelValueRow("Recurring ID:",
request.recurringPayment?.recurringPaymentUID ?? '-'),
_labelValueRow(
"Amount:",
_formatCurrencyAmount(
currencySymbol, request.recurringPayment?.amount)),
_labelValueRow("Variable Amount:",
(request.recurringPayment?.isVariable ?? false) ? "Yes" : "No"),
],
// Description & Attachments
_labelValueRow("Description:", request.description ?? '-'),
_labelValueRow("Attachment:",
(request.attachments ?? []).isNotEmpty ? "Yes" : "No"),
],
);
}
}
// Documents widget
class _Documents extends StatelessWidget {
final List<Attachment> documents;
const _Documents({required this.documents});
@override
Widget build(BuildContext context) {
if (documents.isEmpty) {
return MyText.bodyMedium('No Documents', color: Colors.grey);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodySmall("Documents:", fontWeight: 600),
MySpacing.height(12),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: documents.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final doc = documents[index];
final contentType = doc.contentType ?? '';
final isImage = contentType.startsWith('image/');
return GestureDetector(
onTap: () async {
final imageDocs = documents
.where((d) => (d.contentType ?? '').startsWith('image/'))
.toList();
final initialIndex =
imageDocs.indexWhere((d) => (d.id ?? '') == (doc.id ?? ''));
if (isImage && imageDocs.isNotEmpty && initialIndex != -1) {
showDialog(
context: context,
builder: (_) => ImageViewerDialog(
imageSources: imageDocs.map((e) => e.url ?? '').toList(),
initialIndex: initialIndex,
),
);
} else {
final urlStr = doc.url ?? '';
if (urlStr.isEmpty) {
showAppSnackbar(
title: 'Error',
message: 'Document URL is missing.',
type: SnackbarType.error,
);
return;
}
final Uri url = Uri.parse(urlStr);
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
} else {
showAppSnackbar(
title: 'Error',
message: 'Could not open document.',
type: SnackbarType.error,
);
}
}
},
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(5),
color: Colors.grey.shade100,
),
child: Row(
children: [
Icon(isImage ? Icons.image : Icons.insert_drive_file,
size: 20, color: Colors.grey[600]),
const SizedBox(width: 7),
Expanded(
child: MyText.bodySmall(
doc.fileName ?? (doc.url ?? '-'),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
},
),
],
);
}
}
// Utility widget for label-value row.
Widget _labelValueRow(String label, String value) => Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: MyText.bodySmall(label, fontWeight: 600),
),
Expanded(
child: MyText.bodySmall(value, fontWeight: 500, softWrap: true),
),
],
),
);