Files
velocity-pay-flutter/lib/screens/receipt/receipt_screen.dart
2026-06-05 20:25:20 +02:00

1114 lines
37 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:qpay/models/responsive_policy.dart';
import 'package:qpay/screens/gateway/gateway_controller.dart';
import 'package:qpay/screens/transaction_controller.dart'
show TransactionController;
import 'package:go_router/go_router.dart';
import 'package:qpay/screens/receipt/receipt_controller.dart';
import 'package:qpay/screens/transactions/transaction_model.dart';
import 'package:share_plus/share_plus.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:widgets_to_image/widgets_to_image.dart';
import 'package:decimal/decimal.dart';
class ReceiptScreen extends StatefulWidget {
final String? transactionId;
const ReceiptScreen({super.key, this.transactionId});
@override
State<ReceiptScreen> createState() => _ReceiptScreenState();
}
class _ReceiptScreenState extends State<ReceiptScreen>
with TickerProviderStateMixin {
late TransactionController transactionController;
late GatewayController gatewayController;
// Enhanced animation controllers
late AnimationController _headerAnimController;
late Animation<Offset> _headerSlideAnimation;
late Animation<double> _headerFadeAnimation;
late AnimationController _cardAnimController;
late Animation<Offset> _cardSlideAnimation;
late Animation<double> _cardFadeAnimation;
late AnimationController _contentAnimController;
late Animation<Offset> _contentSlideAnimation;
late Animation<double> _contentFadeAnimation;
int _selectedTabIndex = 0;
TransactionModel? _transaction;
late ReceiptController receiptController;
final _formKey = GlobalKey<FormState>();
final controller = WidgetsToImageController();
@override
void initState() {
super.initState();
transactionController = Provider.of<TransactionController>(
context,
listen: false,
);
receiptController = ReceiptController(context);
gatewayController = GatewayController(context);
_fetchTransaction();
// Header animation
_headerAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_headerSlideAnimation =
Tween<Offset>(begin: const Offset(0, -0.08), end: Offset.zero).animate(
CurvedAnimation(
parent: _headerAnimController,
curve: Curves.easeOutCubic,
),
);
_headerFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _headerAnimController, curve: Curves.easeOut),
);
// Card animation
_cardAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 700),
);
_cardSlideAnimation =
Tween<Offset>(begin: const Offset(0, 0.06), end: Offset.zero).animate(
CurvedAnimation(
parent: _cardAnimController,
curve: Curves.easeOutCubic,
),
);
_cardFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _cardAnimController, curve: Curves.easeOut),
);
// Content animation
_contentAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_contentSlideAnimation =
Tween<Offset>(begin: const Offset(0, 0.08), end: Offset.zero).animate(
CurvedAnimation(
parent: _contentAnimController,
curve: Curves.easeOutCubic,
),
);
_contentFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _contentAnimController, curve: Curves.easeOut),
);
// Start staggered animations
_headerAnimController.forward();
Future.delayed(const Duration(milliseconds: 150), () {
if (mounted) _cardAnimController.forward();
});
Future.delayed(const Duration(milliseconds: 350), () {
if (mounted) _contentAnimController.forward();
});
}
Future<void> _fetchTransaction() async {
final id =
widget.transactionId ??
transactionController.model.receiptData?["id"] ??
receiptController.model.transaction?.id;
if (id == null) return;
final response = await receiptController.getReceiptData(id);
if (response.isSuccess && response.data != null) {
setState(() {
_transaction = response.data;
});
}
}
Future<void> _captureImage() async {
final image = await controller.capture();
if (image != null) {
final params = ShareParams(
files: [
XFile.fromData(image.buffer.asUint8List(), mimeType: 'image/png'),
],
fileNameOverrides: ['receipt.png'],
);
SharePlus.instance.share(params);
}
}
_retry() async {
receiptController.setLoading(true);
final id = _transaction?.id ?? '';
await receiptController.doIntegration(id);
if (receiptController.model.status == "SUCCESS") {
await _fetchTransaction();
}
receiptController.setLoading(false);
}
_check() async {
receiptController.setLoading(true);
final id = _transaction?.id ?? '';
await gatewayController.poll(id);
if (gatewayController.model.status == "SUCCESS") {
await _fetchTransaction();
if (_transaction?.pollingStatus == "SUCCESS") {
receiptController.setLoading(false);
return;
}
await _retry();
}
receiptController.setLoading(false);
}
@override
void dispose() {
_headerAnimController.dispose();
_cardAnimController.dispose();
_contentAnimController.dispose();
receiptController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDark
? const Color(0xFF0D0D0D)
: const Color(0xFFF8F9FA),
body: ListenableBuilder(
listenable: receiptController,
builder: (context, child) {
return CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// Modern app bar
SliverAppBar(
pinned: false,
floating: true,
stretch: true,
stretchTriggerOffset: 80.0,
expandedHeight: 60.0,
collapsedHeight: 60.0,
surfaceTintColor: Colors.transparent,
backgroundColor: isDark
? const Color(0xFF0D0D0D)
: const Color(0xFFF8F9FA),
leading: context.canPop()
? IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back_rounded),
color: theme.colorScheme.primary,
)
: const SizedBox.shrink(),
title: Text(
"Receipt",
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 20,
color: isDark ? Colors.white : Colors.black87,
),
),
centerTitle: true,
),
// Main content
SliverToBoxAdapter(
child: WidgetsToImage(
controller: controller,
child: Container(
color: isDark
? const Color(0xFF0D0D0D)
: const Color(0xFFF8F9FA),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 32),
child: Form(
key: _formKey,
child: LayoutBuilder(
builder: (context, constraints) {
return Center(
child: SizedBox(
width:
constraints.maxWidth > ResponsivePolicy.md
? ResponsivePolicy.md.toDouble()
: constraints.maxWidth,
child: Column(
children: [
// Animated header section
FadeTransition(
opacity: _headerFadeAnimation,
child: SlideTransition(
position: _headerSlideAnimation,
child: _buildReceiptHeader(
theme,
isDark,
),
),
),
const SizedBox(height: 16),
// Animated info card section
FadeTransition(
opacity: _cardFadeAnimation,
child: SlideTransition(
position: _cardSlideAnimation,
child: _buildSegmentedTabBar(
theme,
isDark,
),
),
),
const SizedBox(height: 14),
// Animated content section
FadeTransition(
opacity: _contentFadeAnimation,
child: SlideTransition(
position: _contentSlideAnimation,
child: _buildSelectedTabContent(
theme,
isDark,
),
),
),
],
),
),
);
},
),
),
),
),
),
),
],
);
},
),
);
}
// ──────────────────────────────────────────────────────────────
// Status Badge Helper
// ──────────────────────────────────────────────────────────────
Widget _buildStatusBadge(String status, ThemeData theme) {
final bool isSuccess = status == 'SUCCESS';
final bool isPending = status == 'PENDING';
final Color bgColor = isSuccess
? const Color(0xFFD1FAE5)
: isPending
? const Color(0xFFFEF3C7)
: const Color(0xFFFEE2E2);
final Color textColor = isSuccess
? const Color(0xFF065F46)
: isPending
? const Color(0xFF92400E)
: const Color(0xFF991B1B);
final Color dotColor = isSuccess
? const Color(0xFF10B981)
: isPending
? const Color(0xFFF59E0B)
: const Color(0xFFEF4444);
final String label = isSuccess
? 'Success'
: isPending
? 'Pending'
: 'Failed';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: textColor,
),
),
],
),
);
}
// ──────────────────────────────────────────────────────────────
// Receipt Header Card
// ──────────────────────────────────────────────────────────────
Widget _buildReceiptHeader(ThemeData theme, bool isDark) {
final status = _transaction?.integrationStatus;
final amount = _transaction?.amount;
final currency = _transaction?.debitCurrency ?? "";
final billName = _transaction?.billName ?? "";
final createdAt = _transaction?.createdAt;
final primaryColor = theme.colorScheme.primary;
return Skeletonizer(
enabled: receiptController.model.isLoading,
child: Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor.withValues(alpha: 0.10),
primaryColor.withValues(alpha: 0.04),
primaryColor.withValues(alpha: 0.08),
],
stops: const [0.0, 0.5, 1.0],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: primaryColor.withValues(alpha: 0.12),
width: 1,
),
boxShadow: [
BoxShadow(
color: primaryColor.withValues(alpha: 0.08),
blurRadius: 20,
offset: const Offset(0, 6),
),
],
),
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
child: Column(
children: [
// Status badge
if (status != null) ...[
_buildStatusBadge(status, theme),
const SizedBox(height: 12),
],
// Amount row
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
currency,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white70 : Colors.grey.shade600,
),
),
const SizedBox(width: 4),
Text(
amount?.toStringAsFixed(2) ?? "",
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.w700,
color: isDark ? Colors.white : Colors.black87,
height: 1.1,
),
),
],
),
const SizedBox(height: 8),
// Bill name (if available)
if (billName.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(20),
),
child: Text(
billName,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: primaryColor,
),
),
),
const SizedBox(height: 4),
// Date
if (createdAt != null)
Text(
DateFormat.yMMMd().add_jm().format(createdAt),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w400,
color: isDark ? Colors.white54 : Colors.grey.shade500,
),
),
const SizedBox(height: 16),
// Divider with dots pattern
_buildDottedDivider(isDark),
const SizedBox(height: 14),
// Action buttons row
_buildActionButtons(theme),
],
),
),
);
}
// ──────────────────────────────────────────────────────────────
// Dotted Divider
// ──────────────────────────────────────────────────────────────
Widget _buildDottedDivider(bool isDark) {
return Row(
children: List.generate(
40,
(index) => Expanded(
child: Container(
height: 1.5,
color: index.isEven
? (isDark ? Colors.white24 : Colors.grey.shade300)
: Colors.transparent,
),
),
),
);
}
// ──────────────────────────────────────────────────────────────
// Action Buttons Row
// ──────────────────────────────────────────────────────────────
Widget _buildActionButtons(ThemeData theme) {
final integrationStatus = _transaction?.integrationStatus;
final paymentStatus = _transaction?.paymentStatus;
return Wrap(
spacing: 10,
runSpacing: 6,
alignment: WrapAlignment.center,
children: [
_modernActionButton(
icon: Icons.repeat_rounded,
label: "Repeat",
color: theme.colorScheme.primary,
isLoading: receiptController.model.isLoading,
onPressed: () async {
await receiptController.repeatTransaction(
receiptController.model.transaction!,
);
if (mounted) {
context.push('/make-payment');
}
},
),
_modernActionButton(
icon: Icons.ios_share_rounded,
label: "Share",
color: const Color(0xFF6366F1),
isLoading: false,
onPressed: _captureImage,
),
if (integrationStatus != "SUCCESS" && paymentStatus == "SUCCESS") ...[
_modernActionButton(
icon: Icons.refresh_rounded,
label: "Retry",
color: const Color(0xFFF59E0B),
isLoading: false,
onPressed: _retry,
),
],
if (_transaction?.pollingStatus != "SUCCESS") ...[
_modernActionButton(
icon: Icons.search_rounded,
label: "Check",
color: const Color(0xFF10B981),
isLoading: false,
onPressed: _check,
),
],
],
);
}
Widget _modernActionButton({
required IconData icon,
required String label,
required Color color,
required bool isLoading,
required VoidCallback onPressed,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
color.withValues(alpha: 0.15),
color.withValues(alpha: 0.05),
],
),
border: Border.all(color: color.withValues(alpha: 0.2), width: 1.5),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: isLoading
? Center(
child: SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation<Color>(color),
),
),
)
: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(26),
onTap: onPressed,
child: Center(child: Icon(icon, color: color, size: 22)),
),
),
),
const SizedBox(height: 6),
Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey.shade600,
),
),
],
);
}
// ──────────────────────────────────────────────────────────────
// Segmented Tab Bar (Modern UI)
// ──────────────────────────────────────────────────────────────
Widget _buildSegmentedTabBar(ThemeData theme, bool isDark) {
final segments = ["Provider Details", "Transaction Details"];
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isDark
? Colors.white.withValues(alpha: 0.06)
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: List.generate(segments.length, (index) {
final isSelected = _selectedTabIndex == index;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _selectedTabIndex = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected
? theme.colorScheme.primary
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
boxShadow: isSelected
? [
BoxShadow(
color: theme.colorScheme.primary.withValues(
alpha: 0.3,
),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: Center(
child: Text(
segments[index],
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : Colors.grey.shade600,
),
),
),
),
),
);
}),
),
);
}
// ──────────────────────────────────────────────────────────────
// Selected Tab Content with AnimatedSwitcher
// ──────────────────────────────────────────────────────────────
Widget _buildSelectedTabContent(ThemeData theme, bool isDark) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.04),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: KeyedSubtree(
key: ValueKey(_selectedTabIndex),
child: _selectedTabIndex == 0
? _buildProviderDetailsTab(theme, isDark)
: _buildTransactionDetailsTab(theme, isDark),
),
);
}
// ──────────────────────────────────────────────────────────────
// Provider Details Tab
// ──────────────────────────────────────────────────────────────
Widget _buildProviderDetailsTab(ThemeData theme, bool isDark) {
final receiptData = receiptController.model.transaction;
final additionalData = receiptData?.additionalData;
final items = <_DetailItem>[];
// Status
items.add(
_DetailItem(
icon: Icons.circle_outlined,
label: "Status",
value: _transaction?.integrationStatus ?? "",
),
);
// Error message
final errorMsg = _transaction?.errorMessage;
if (errorMsg != null && errorMsg.isNotEmpty) {
items.add(
_DetailItem(
icon: Icons.warning_amber_rounded,
label: "Message",
value: errorMsg,
isWarning: true,
),
);
}
// Additional data from provider
if (additionalData != null) {
for (final data in additionalData) {
items.add(
_DetailItem(
icon: Icons.info_outline_rounded,
label: data["name"] ?? "",
value: data["value"] ?? "",
),
);
}
}
// Standard fields
items.add(
_DetailItem(
icon: Icons.tag_rounded,
label: "Debit Reference",
value: _transaction?.debitRef ?? "",
),
);
items.add(
_DetailItem(
icon: Icons.phone_iphone_rounded,
label: "From",
value: receiptData?.debitPhone ?? "",
),
);
items.add(
_DetailItem(
icon: Icons.account_balance_wallet_rounded,
label: "To",
value: receiptData?.creditAccount ?? "",
),
);
items.add(
_DetailItem(
icon: Icons.memory_rounded,
label: "Processor",
value: receiptData?.paymentProcessorName ?? "",
),
);
if (receiptData?.billProductName != null) {
items.add(
_DetailItem(
icon: Icons.category_rounded,
label: "Bill Product",
value: receiptData?.billProductName ?? "",
),
);
}
if (additionalData == null && items.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.inbox_outlined, size: 48, color: Colors.grey.shade400),
const SizedBox(height: 12),
Text(
"No provider details found",
style: TextStyle(
fontSize: 15,
color: Colors.grey.shade500,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
return _buildInfoCard(
theme: theme,
isDark: isDark,
icon: Icons.business_rounded,
title: "Provider Details",
items: items,
);
}
// ──────────────────────────────────────────────────────────────
// Transaction Details Tab
// ──────────────────────────────────────────────────────────────
Widget _buildTransactionDetailsTab(ThemeData theme, bool isDark) {
final t = _transaction;
final amount = t?.amount ?? Decimal.zero;
final charge = t?.charge ?? Decimal.zero;
final gatewayCharge = t?.gatewayCharge ?? Decimal.zero;
final tax = t?.tax ?? Decimal.zero;
final totalAmount = t?.totalAmount ?? Decimal.zero;
final items = <_DetailItem>[
_DetailItem(
icon: Icons.monetization_on_rounded,
label: "Amount",
value: amount.toStringAsFixed(2),
trailing: const Icon(
Icons.trending_up_rounded,
size: 16,
color: Colors.green,
),
),
];
if (charge != Decimal.zero) {
items.add(
_DetailItem(
icon: Icons.percent_rounded,
label: "Our Charge",
value: charge.toStringAsFixed(2),
),
);
}
if (gatewayCharge != Decimal.zero) {
items.add(
_DetailItem(
icon: Icons.account_balance_rounded,
label: "Gateway Charge",
value: gatewayCharge.toStringAsFixed(2),
),
);
}
if (tax != Decimal.zero) {
items.add(
_DetailItem(
icon: Icons.receipt_rounded,
label: "Tax",
value: tax.toStringAsFixed(2),
),
);
}
return _buildInfoCard(
theme: theme,
isDark: isDark,
icon: Icons.receipt_long_rounded,
title: "Transaction Details",
items: items,
totalAmount: totalAmount.toStringAsFixed(2),
);
}
// ──────────────────────────────────────────────────────────────
// Modern Info Card
// ──────────────────────────────────────────────────────────────
Widget _buildInfoCard({
required ThemeData theme,
required bool isDark,
required IconData icon,
required String title,
required List<_DetailItem> items,
String? totalAmount,
}) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1A1A2E) : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.06)
: Colors.grey.shade200,
width: 1,
),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.04),
blurRadius: 16,
offset: const Offset(0, 4),
),
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.15)
: Colors.black.withValues(alpha: 0.02),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title row
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, size: 18, color: theme.colorScheme.primary),
),
const SizedBox(width: 12),
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: isDark ? Colors.white : Colors.black87,
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 12),
// Divider
Container(
height: 1,
color: isDark
? Colors.white.withValues(alpha: 0.06)
: Colors.grey.shade200,
),
const SizedBox(height: 12),
// Detail items
...items.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _buildModernDetailRow(
theme: theme,
isDark: isDark,
item: item,
),
),
),
// Total amount divider & total
if (totalAmount != null) ...[
Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary.withValues(alpha: 0.3),
Colors.transparent,
],
),
),
),
),
const SizedBox(height: 8),
Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.summarize_rounded,
size: 16,
color: theme.colorScheme.primary,
),
),
const SizedBox(width: 10),
Text(
"Total",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black87,
),
),
const Spacer(),
Text(
totalAmount,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: theme.colorScheme.primary,
),
),
],
),
],
],
),
),
);
}
// ──────────────────────────────────────────────────────────────
// Modern Detail Row
// ──────────────────────────────────────────────────────────────
Widget _buildModernDetailRow({
required ThemeData theme,
required bool isDark,
required _DetailItem item,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
item.icon,
size: 16,
color: item.isWarning
? const Color(0xFFF59E0B)
: isDark
? Colors.white38
: Colors.grey.shade500,
),
const SizedBox(width: 10),
Expanded(
flex: 2,
child: Text(
item.label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isDark ? Colors.white60 : Colors.grey.shade600,
),
),
),
if (item.trailing != null) ...[
item.trailing!,
const SizedBox(width: 6),
],
Expanded(
flex: 3,
child: Text(
item.value,
style: TextStyle(
fontSize: 14,
fontWeight: item.isWarning ? FontWeight.w500 : FontWeight.w600,
color: item.isWarning
? const Color(0xFF92400E)
: isDark
? Colors.white
: Colors.black87,
),
textAlign: TextAlign.end,
softWrap: true,
),
),
],
);
}
}
// ──────────────────────────────────────────────────────────────
// Data class for detail items
// ──────────────────────────────────────────────────────────────
class _DetailItem {
final IconData icon;
final String label;
final String value;
final bool isWarning;
final Widget? trailing;
_DetailItem({
required this.icon,
required this.label,
required this.value,
this.isWarning = false,
this.trailing,
});
}