2025-08-12 23:27:34 +07:00
|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
2025-08-12 17:36:41 +07:00
|
|
|
import 'package:auto_route/auto_route.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
2025-08-12 23:27:34 +07:00
|
|
|
import 'package:line_icons/line_icons.dart';
|
2025-08-12 17:36:41 +07:00
|
|
|
|
2025-08-12 21:27:13 +07:00
|
|
|
import '../../../common/theme/theme.dart';
|
2025-08-12 23:27:34 +07:00
|
|
|
import '../../components/button/button.dart';
|
|
|
|
|
import '../../components/spacer/spacer.dart';
|
|
|
|
|
import 'widgets/appbar.dart';
|
|
|
|
|
import 'widgets/status_tile.dart';
|
|
|
|
|
import 'widgets/transaction_tile.dart';
|
2025-08-12 21:27:13 +07:00
|
|
|
|
2025-08-12 17:36:41 +07:00
|
|
|
@RoutePage()
|
2025-08-12 21:27:13 +07:00
|
|
|
class TransactionPage extends StatefulWidget {
|
2025-08-12 17:36:41 +07:00
|
|
|
const TransactionPage({super.key});
|
|
|
|
|
|
2025-08-12 21:27:13 +07:00
|
|
|
@override
|
|
|
|
|
State<TransactionPage> createState() => _TransactionPageState();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
class _TransactionPageState extends State<TransactionPage>
|
|
|
|
|
with TickerProviderStateMixin {
|
|
|
|
|
late AnimationController _fadeController;
|
|
|
|
|
late AnimationController _slideController;
|
|
|
|
|
late AnimationController _rotationController;
|
|
|
|
|
late Animation<double> _fadeAnimation;
|
|
|
|
|
late Animation<Offset> _slideAnimation;
|
|
|
|
|
late Animation<double> _rotationAnimation;
|
2025-08-12 21:27:13 +07:00
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
// Filter state
|
|
|
|
|
String selectedFilter = 'All';
|
|
|
|
|
final List<String> filterOptions = [
|
|
|
|
|
'All',
|
|
|
|
|
'Completed',
|
|
|
|
|
'Pending',
|
|
|
|
|
'Refunded',
|
2025-08-12 21:27:13 +07:00
|
|
|
];
|
|
|
|
|
|
2025-08-12 17:36:41 +07:00
|
|
|
@override
|
2025-08-12 23:27:34 +07:00
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2025-08-12 21:27:13 +07:00
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
_fadeController = AnimationController(
|
|
|
|
|
duration: const Duration(milliseconds: 800),
|
|
|
|
|
vsync: this,
|
2025-08-12 21:27:13 +07:00
|
|
|
);
|
|
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
_slideController = AnimationController(
|
|
|
|
|
duration: const Duration(milliseconds: 1000),
|
|
|
|
|
vsync: this,
|
2025-08-12 21:27:13 +07:00
|
|
|
);
|
|
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
_rotationController = AnimationController(
|
|
|
|
|
duration: const Duration(seconds: 3),
|
|
|
|
|
vsync: this,
|
|
|
|
|
)..repeat();
|
2025-08-12 21:27:13 +07:00
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
|
|
|
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
|
2025-08-12 21:27:13 +07:00
|
|
|
);
|
|
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
_slideAnimation =
|
|
|
|
|
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
|
|
|
|
|
CurvedAnimation(parent: _slideController, curve: Curves.elasticOut),
|
|
|
|
|
);
|
2025-08-12 21:27:13 +07:00
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
_rotationAnimation = Tween<double>(
|
|
|
|
|
begin: 0,
|
|
|
|
|
end: 2 * math.pi,
|
|
|
|
|
).animate(_rotationController);
|
|
|
|
|
|
|
|
|
|
_fadeController.forward();
|
|
|
|
|
_slideController.forward();
|
2025-08-12 21:27:13 +07:00
|
|
|
}
|
|
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_fadeController.dispose();
|
|
|
|
|
_slideController.dispose();
|
|
|
|
|
_rotationController.dispose();
|
|
|
|
|
super.dispose();
|
2025-08-12 21:27:13 +07:00
|
|
|
}
|
|
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
final sampleTransactions = [
|
|
|
|
|
Transaction(
|
|
|
|
|
id: 'TXN001',
|
|
|
|
|
customerName: 'John Doe',
|
|
|
|
|
date: DateTime.now().subtract(const Duration(hours: 2)),
|
|
|
|
|
totalAmount: 125000,
|
|
|
|
|
itemCount: 3,
|
|
|
|
|
paymentMethod: 'Cash',
|
|
|
|
|
status: TransactionStatus.completed,
|
|
|
|
|
receiptNumber: 'RCP-2024-001',
|
|
|
|
|
),
|
|
|
|
|
Transaction(
|
|
|
|
|
id: 'TXN002',
|
|
|
|
|
customerName: 'Jane Smith',
|
|
|
|
|
date: DateTime.now().subtract(const Duration(hours: 5)),
|
|
|
|
|
totalAmount: 87500,
|
|
|
|
|
itemCount: 2,
|
|
|
|
|
paymentMethod: 'QRIS',
|
|
|
|
|
status: TransactionStatus.pending,
|
|
|
|
|
receiptNumber: 'RCP-2024-002',
|
|
|
|
|
),
|
|
|
|
|
Transaction(
|
|
|
|
|
id: 'TXN003',
|
|
|
|
|
customerName: 'Bob Johnson',
|
|
|
|
|
date: DateTime.now().subtract(const Duration(days: 1)),
|
|
|
|
|
totalAmount: 250000,
|
|
|
|
|
itemCount: 5,
|
|
|
|
|
paymentMethod: 'Credit Card',
|
|
|
|
|
status: TransactionStatus.refunded,
|
|
|
|
|
receiptNumber: 'RCP-2024-003',
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Filter transactions based on selected status
|
|
|
|
|
List<Transaction> get filteredTransactions {
|
|
|
|
|
if (selectedFilter == 'All') {
|
|
|
|
|
return sampleTransactions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TransactionStatus? filterStatus;
|
|
|
|
|
switch (selectedFilter) {
|
2025-08-12 21:27:13 +07:00
|
|
|
case 'Completed':
|
2025-08-12 23:27:34 +07:00
|
|
|
filterStatus = TransactionStatus.completed;
|
|
|
|
|
break;
|
2025-08-12 21:27:13 +07:00
|
|
|
case 'Pending':
|
2025-08-12 23:27:34 +07:00
|
|
|
filterStatus = TransactionStatus.pending;
|
|
|
|
|
break;
|
|
|
|
|
case 'Refunded':
|
|
|
|
|
filterStatus = TransactionStatus.refunded;
|
|
|
|
|
break;
|
2025-08-12 21:27:13 +07:00
|
|
|
}
|
|
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
return sampleTransactions
|
|
|
|
|
.where((transaction) => transaction.status == filterStatus)
|
|
|
|
|
.toList();
|
2025-08-12 21:27:13 +07:00
|
|
|
}
|
|
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
// Build filter chip
|
2025-08-12 21:27:13 +07:00
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
backgroundColor: AppColor.background,
|
|
|
|
|
body: CustomScrollView(
|
|
|
|
|
slivers: [
|
|
|
|
|
// Custom App Bar with Hero Effect
|
|
|
|
|
SliverAppBar(
|
|
|
|
|
expandedHeight: 120,
|
|
|
|
|
floating: true,
|
|
|
|
|
pinned: true,
|
|
|
|
|
backgroundColor: AppColor.primary,
|
|
|
|
|
centerTitle: false,
|
|
|
|
|
flexibleSpace: TransactionAppBar(
|
|
|
|
|
rotationAnimation: _rotationAnimation,
|
|
|
|
|
),
|
|
|
|
|
actions: [
|
|
|
|
|
ActionIconButton(onTap: () {}, icon: LineIcons.filter),
|
|
|
|
|
SpaceWidth(8),
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-08-12 21:27:13 +07:00
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
// Pinned Filter Section
|
|
|
|
|
SliverPersistentHeader(
|
|
|
|
|
pinned: true,
|
|
|
|
|
delegate: _FilterHeaderDelegate(
|
|
|
|
|
child: Container(
|
|
|
|
|
color: AppColor.background,
|
|
|
|
|
padding: EdgeInsets.fromLTRB(
|
|
|
|
|
AppValue.padding,
|
|
|
|
|
10,
|
|
|
|
|
AppValue.padding,
|
|
|
|
|
10,
|
2025-08-12 21:27:13 +07:00
|
|
|
),
|
2025-08-12 23:27:34 +07:00
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
SingleChildScrollView(
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
child: Row(
|
|
|
|
|
children: filterOptions.map((option) {
|
|
|
|
|
final index = filterOptions.indexOf(option);
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: EdgeInsets.only(
|
|
|
|
|
right: index < filterOptions.length - 1 ? 8 : 0,
|
|
|
|
|
),
|
|
|
|
|
child: TransactionStatusTile(
|
|
|
|
|
label: option,
|
|
|
|
|
isSelected: option == selectedFilter,
|
|
|
|
|
onSelected: (isSelected) {
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
setState(() {
|
|
|
|
|
selectedFilter = option;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
2025-08-12 21:27:13 +07:00
|
|
|
),
|
|
|
|
|
),
|
2025-08-12 23:27:34 +07:00
|
|
|
],
|
|
|
|
|
),
|
2025-08-12 21:27:13 +07:00
|
|
|
),
|
2025-08-12 23:27:34 +07:00
|
|
|
),
|
2025-08-12 21:27:13 +07:00
|
|
|
),
|
|
|
|
|
|
2025-08-12 23:27:34 +07:00
|
|
|
// Content
|
|
|
|
|
SliverPadding(
|
|
|
|
|
padding: EdgeInsets.all(AppValue.padding),
|
|
|
|
|
sliver: SliverList(
|
|
|
|
|
delegate: SliverChildListDelegate([
|
|
|
|
|
FadeTransition(
|
|
|
|
|
opacity: _fadeAnimation,
|
|
|
|
|
child: SlideTransition(
|
|
|
|
|
position: _slideAnimation,
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
// Show filtered transaction count
|
|
|
|
|
if (selectedFilter != 'All')
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'${filteredTransactions.length} ${selectedFilter.toLowerCase()} transaction${filteredTransactions.length != 1 ? 's' : ''}',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: AppColor.textSecondary,
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-08-12 21:27:13 +07:00
|
|
|
),
|
2025-08-12 23:27:34 +07:00
|
|
|
|
|
|
|
|
// Transaction List
|
|
|
|
|
filteredTransactions.isEmpty
|
|
|
|
|
? Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
vertical: 40,
|
|
|
|
|
),
|
2025-08-12 21:27:13 +07:00
|
|
|
child: Column(
|
|
|
|
|
children: [
|
2025-08-12 23:27:34 +07:00
|
|
|
Icon(
|
|
|
|
|
LineIcons.receipt,
|
|
|
|
|
size: 64,
|
|
|
|
|
color: AppColor.textSecondary.withOpacity(
|
|
|
|
|
0.5,
|
2025-08-12 21:27:13 +07:00
|
|
|
),
|
|
|
|
|
),
|
2025-08-12 23:27:34 +07:00
|
|
|
const SizedBox(height: 16),
|
2025-08-12 21:27:13 +07:00
|
|
|
Text(
|
2025-08-12 23:27:34 +07:00
|
|
|
'No ${selectedFilter.toLowerCase()} transactions found',
|
2025-08-12 21:27:13 +07:00
|
|
|
style: TextStyle(
|
|
|
|
|
color: AppColor.textSecondary,
|
2025-08-12 23:27:34 +07:00
|
|
|
fontSize: 16,
|
2025-08-12 21:27:13 +07:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-08-12 23:27:34 +07:00
|
|
|
)
|
|
|
|
|
: Column(
|
|
|
|
|
children: filteredTransactions.map((
|
|
|
|
|
transaction,
|
|
|
|
|
) {
|
|
|
|
|
return TransactionTile(
|
|
|
|
|
transaction: transaction,
|
|
|
|
|
onTap: () {},
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
2025-08-12 21:27:13 +07:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-08-12 23:27:34 +07:00
|
|
|
),
|
|
|
|
|
]),
|
2025-08-12 21:27:13 +07:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
2025-08-12 17:36:41 +07:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-12 23:27:34 +07:00
|
|
|
|
|
|
|
|
// Custom delegate for pinned filter header
|
|
|
|
|
class _FilterHeaderDelegate extends SliverPersistentHeaderDelegate {
|
|
|
|
|
final Widget child;
|
|
|
|
|
|
|
|
|
|
_FilterHeaderDelegate({required this.child});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
double get minExtent => 70; // Minimum height when collapsed
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
double get maxExtent => 70; // Maximum height when expanded
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(
|
|
|
|
|
BuildContext context,
|
|
|
|
|
double shrinkOffset,
|
|
|
|
|
bool overlapsContent,
|
|
|
|
|
) {
|
|
|
|
|
return child;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|