feat: transaction page

This commit is contained in:
efrilm 2025-08-12 21:27:13 +07:00
parent 9458de5059
commit f22c36d6de
2 changed files with 702 additions and 3 deletions

View File

@ -43,7 +43,7 @@ class ThemeApp {
), ),
), ),
bottomNavigationBarTheme: BottomNavigationBarThemeData( bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: AppColor.background, backgroundColor: AppColor.white,
selectedItemColor: AppColor.primary, selectedItemColor: AppColor.primary,
unselectedItemColor: AppColor.textSecondary, unselectedItemColor: AppColor.textSecondary,
selectedLabelStyle: AppStyle.md.copyWith( selectedLabelStyle: AppStyle.md.copyWith(

View File

@ -1,12 +1,711 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../common/theme/theme.dart';
class Transaction {
final String id;
final String customerName;
final DateTime date;
final double total;
final String status;
final List<TransactionItem> items;
final String paymentMethod;
Transaction({
required this.id,
required this.customerName,
required this.date,
required this.total,
required this.status,
required this.items,
required this.paymentMethod,
});
}
class TransactionItem {
final String name;
final int quantity;
final double price;
TransactionItem({
required this.name,
required this.quantity,
required this.price,
});
}
@RoutePage() @RoutePage()
class TransactionPage extends StatelessWidget { class TransactionPage extends StatefulWidget {
const TransactionPage({super.key}); const TransactionPage({super.key});
@override
State<TransactionPage> createState() => _TransactionPageState();
}
class _TransactionPageState extends State<TransactionPage> {
String selectedFilter = 'All';
DateTime selectedDate = DateTime.now();
final List<Transaction> transactions = [
Transaction(
id: 'TRX001',
customerName: 'Ahmad Rizki',
date: DateTime.now().subtract(Duration(hours: 2)),
total: 125000,
status: 'Completed',
paymentMethod: 'Cash',
items: [
TransactionItem(name: 'Nasi Goreng', quantity: 2, price: 25000),
TransactionItem(name: 'Es Teh', quantity: 3, price: 5000),
TransactionItem(name: 'Ayam Bakar', quantity: 1, price: 35000),
],
),
Transaction(
id: 'TRX002',
customerName: 'Siti Nurhaliza',
date: DateTime.now().subtract(Duration(hours: 4)),
total: 85000,
status: 'Completed',
paymentMethod: 'QRIS',
items: [
TransactionItem(name: 'Gado-gado', quantity: 1, price: 20000),
TransactionItem(name: 'Jus Jeruk', quantity: 2, price: 12000),
TransactionItem(name: 'Kerupuk', quantity: 1, price: 5000),
],
),
Transaction(
id: 'TRX003',
customerName: 'Budi Santoso',
date: DateTime.now().subtract(Duration(hours: 6)),
total: 200000,
status: 'Pending',
paymentMethod: 'Debit Card',
items: [
TransactionItem(name: 'Paket Keluarga', quantity: 1, price: 150000),
TransactionItem(name: 'Es Campur', quantity: 2, price: 15000),
],
),
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center(child: Text('TransactionPage')); return Scaffold(
backgroundColor: AppColor.background,
appBar: AppBar(
elevation: 0,
backgroundColor: AppColor.white,
title: Text(
'Transactions',
style: TextStyle(
color: AppColor.textPrimary,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
actions: [
IconButton(
icon: Icon(Icons.search, color: AppColor.textPrimary),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.filter_list, color: AppColor.textPrimary),
onPressed: () => _showFilterBottomSheet(context),
),
],
),
body: Column(
children: [
_buildSummaryCards(),
_buildFilterTabs(),
Expanded(child: _buildTransactionList()),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {},
backgroundColor: AppColor.primary,
icon: Icon(Icons.add, color: AppColor.white),
label: Text(
'New Sale',
style: TextStyle(color: AppColor.white, fontWeight: FontWeight.w600),
),
),
);
}
Widget _buildSummaryCards() {
return Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: _buildSummaryCard(
'Today\'s Sales',
'Rp 2,450,000',
Icons.trending_up,
AppColor.success,
'+12%',
),
),
SizedBox(width: 12),
Expanded(
child: _buildSummaryCard(
'Total Orders',
'48',
Icons.receipt_long,
AppColor.info,
'+8%',
),
),
],
),
);
}
Widget _buildSummaryCard(
String title,
String value,
IconData icon,
Color color,
String change,
) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(icon, color: color, size: 24),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColor.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
change,
style: TextStyle(
color: AppColor.success,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
],
),
SizedBox(height: 12),
Text(
value,
style: TextStyle(
color: AppColor.textPrimary,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
title,
style: TextStyle(
color: AppColor.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildFilterTabs() {
final filters = ['All', 'Completed', 'Pending', 'Cancelled'];
return Container(
height: 60,
padding: EdgeInsets.symmetric(horizontal: 16),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: filters.length,
itemBuilder: (context, index) {
final filter = filters[index];
final isSelected = selectedFilter == filter;
return GestureDetector(
onTap: () {
setState(() {
selectedFilter = filter;
});
},
child: Container(
margin: EdgeInsets.only(right: 12, top: 8, bottom: 8),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppColor.primary : AppColor.white,
borderRadius: BorderRadius.circular(25),
border: Border.all(
color: isSelected ? AppColor.primary : AppColor.border,
width: 1,
),
),
child: Text(
filter,
style: TextStyle(
color: isSelected ? AppColor.white : AppColor.textSecondary,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
);
},
),
);
}
Widget _buildTransactionList() {
final filteredTransactions = transactions.where((transaction) {
if (selectedFilter == 'All') return true;
return transaction.status == selectedFilter;
}).toList();
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: filteredTransactions.length,
itemBuilder: (context, index) {
final transaction = filteredTransactions[index];
return _buildTransactionCard(transaction);
},
);
}
Widget _buildTransactionCard(Transaction transaction) {
return GestureDetector(
onTap: () => _showTransactionDetail(transaction),
child: Container(
margin: EdgeInsets.only(bottom: 12),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
transaction.id,
style: TextStyle(
color: AppColor.textPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
transaction.customerName,
style: TextStyle(
color: AppColor.textSecondary,
fontSize: 14,
),
),
SizedBox(height: 4),
Text(
'${_formatTime(transaction.date)}${transaction.paymentMethod}',
style: TextStyle(
color: AppColor.textLight,
fontSize: 12,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Rp ${_formatCurrency(transaction.total)}',
style: TextStyle(
color: AppColor.textPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusColor(
transaction.status,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
transaction.status,
style: TextStyle(
color: _getStatusColor(transaction.status),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
SizedBox(height: 12),
Row(
children: [
Icon(
Icons.shopping_bag_outlined,
color: AppColor.textLight,
size: 16,
),
SizedBox(width: 8),
Text(
'${transaction.items.length} items',
style: TextStyle(color: AppColor.textLight, fontSize: 12),
),
Spacer(),
Icon(
Icons.arrow_forward_ios,
color: AppColor.textLight,
size: 14,
),
],
),
],
),
),
);
}
Color _getStatusColor(String status) {
switch (status) {
case 'Completed':
return AppColor.success;
case 'Pending':
return AppColor.warning;
case 'Cancelled':
return AppColor.error;
default:
return AppColor.textSecondary;
}
}
String _formatCurrency(double amount) {
return amount
.toStringAsFixed(0)
.replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},',
);
}
String _formatTime(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inHours < 1) {
return '${difference.inMinutes}m ago';
} else if (difference.inHours < 24) {
return '${difference.inHours}h ago';
} else {
return '${difference.inDays}d ago';
}
}
void _showFilterBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return Container(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filter Transactions',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 20),
_buildFilterOption('Date Range', Icons.date_range),
_buildFilterOption('Payment Method', Icons.payment),
_buildFilterOption('Amount Range', Icons.attach_money),
_buildFilterOption('Customer', Icons.person),
SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppColor.border),
padding: EdgeInsets.symmetric(vertical: 12),
),
child: Text(
'Reset',
style: TextStyle(color: AppColor.textSecondary),
),
),
),
SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primary,
padding: EdgeInsets.symmetric(vertical: 12),
),
child: Text(
'Apply',
style: TextStyle(color: AppColor.white),
),
),
),
],
),
],
),
);
},
);
}
Widget _buildFilterOption(String title, IconData icon) {
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(icon, color: AppColor.textSecondary),
title: Text(title, style: TextStyle(color: AppColor.textPrimary)),
trailing: Icon(
Icons.arrow_forward_ios,
size: 16,
color: AppColor.textLight,
),
onTap: () {},
);
}
void _showTransactionDetail(Transaction transaction) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return DraggableScrollableSheet(
initialChildSize: 0.7,
maxChildSize: 0.9,
minChildSize: 0.5,
builder: (context, scrollController) {
return Container(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColor.borderLight,
borderRadius: BorderRadius.circular(2),
),
),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Transaction Detail',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusColor(
transaction.status,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
transaction.status,
style: TextStyle(
color: _getStatusColor(transaction.status),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
SizedBox(height: 20),
_buildDetailRow('Transaction ID', transaction.id),
_buildDetailRow('Customer', transaction.customerName),
_buildDetailRow(
'Date',
'${transaction.date.day}/${transaction.date.month}/${transaction.date.year}',
),
_buildDetailRow('Payment Method', transaction.paymentMethod),
SizedBox(height: 20),
Text(
'Items Ordered',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 12),
Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: transaction.items.length,
itemBuilder: (context, index) {
final item = transaction.items[index];
return Container(
margin: EdgeInsets.only(bottom: 8),
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.backgroundLight,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.name,
style: TextStyle(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
Text(
'Qty: ${item.quantity}',
style: TextStyle(
color: AppColor.textSecondary,
fontSize: 12,
),
),
],
),
),
Text(
'Rp ${_formatCurrency(item.price * item.quantity)}',
style: TextStyle(
color: AppColor.textPrimary,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
),
Container(
padding: EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: AppColor.border)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total Amount',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
'Rp ${_formatCurrency(transaction.total)}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
],
),
),
],
),
);
},
);
},
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(color: AppColor.textSecondary, fontSize: 14),
),
Text(
value,
style: TextStyle(
color: AppColor.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
);
} }
} }