1115 lines
37 KiB
Dart
1115 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: 24),
|
|
|
|
// Animated info card section
|
|
FadeTransition(
|
|
opacity: _cardFadeAnimation,
|
|
child: SlideTransition(
|
|
position: _cardSlideAnimation,
|
|
child: _buildSegmentedTabBar(
|
|
theme,
|
|
isDark,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// 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(24),
|
|
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: 28, horizontal: 24),
|
|
child: Column(
|
|
children: [
|
|
// Status badge
|
|
if (status != null) ...[
|
|
_buildStatusBadge(status, theme),
|
|
const SizedBox(height: 16),
|
|
],
|
|
|
|
// 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: 12),
|
|
|
|
// Bill name (if available)
|
|
if (billName.isNotEmpty)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 14,
|
|
vertical: 6,
|
|
),
|
|
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: 6),
|
|
|
|
// 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: 24),
|
|
|
|
// Divider with dots pattern
|
|
_buildDottedDivider(isDark),
|
|
const SizedBox(height: 20),
|
|
|
|
// Action buttons row
|
|
_buildActionButtons(theme),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────
|
|
// Dotted Divider
|
|
// ──────────────────────────────────────────────────────────────
|
|
Widget _buildDottedDivider(bool isDark) {
|
|
return Row(
|
|
children: List.generate(
|
|
60,
|
|
(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: 12,
|
|
runSpacing: 8,
|
|
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(20),
|
|
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: 16),
|
|
|
|
// Divider
|
|
Container(
|
|
height: 1,
|
|
color: isDark
|
|
? Colors.white.withValues(alpha: 0.06)
|
|
: Colors.grey.shade200,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Detail items
|
|
...items.map(
|
|
(item) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 14),
|
|
child: _buildModernDetailRow(
|
|
theme: theme,
|
|
isDark: isDark,
|
|
item: item,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Total amount divider & total
|
|
if (totalAmount != null) ...[
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Container(
|
|
height: 1,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
theme.colorScheme.primary.withValues(alpha: 0.3),
|
|
Colors.transparent,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
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,
|
|
});
|
|
}
|