Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9bf455631 | ||
|
|
b9bd506ee6 | ||
|
|
d84778a5a7 | ||
|
|
f4ac8e0e56 | ||
|
|
42046b4e87 |
@@ -41,7 +41,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
|||||||
late Animation<double> _pieChartHeightFactor;
|
late Animation<double> _pieChartHeightFactor;
|
||||||
|
|
||||||
// State variables
|
// State variables
|
||||||
int _selectedPieIndex = -1; // Index of the selected pie chart slice
|
// Removed _selectedPieIndex as it's now managed by SpendingPieChart
|
||||||
int _selectedNavIndex = 0; // Index for bottom navigation bar
|
int _selectedNavIndex = 0; // Index for bottom navigation bar
|
||||||
bool _isPieChartExpanded = true; // Controls visibility of the pie chart section
|
bool _isPieChartExpanded = true; // Controls visibility of the pie chart section
|
||||||
bool _isFilterVisible = false; // Controls visibility of the filter chips
|
bool _isFilterVisible = false; // Controls visibility of the filter chips
|
||||||
@@ -113,16 +113,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles selection of a pie chart slice
|
// Removed _selectPieCategory method as it's now handled internally by SpendingPieChart
|
||||||
void _selectPieCategory(int index) {
|
|
||||||
setState(() {
|
|
||||||
// If the same index is selected, deselect (-1), otherwise select the new index
|
|
||||||
_selectedPieIndex = (_selectedPieIndex == index) ? -1 : index;
|
|
||||||
});
|
|
||||||
// Optionally apply filter when pie slice is selected/deselected
|
|
||||||
// This requires getting the category name from the index, potentially async
|
|
||||||
// _applyFilterBasedOnPieSelection(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Applies the selected filter to the transaction list
|
// Applies the selected filter to the transaction list
|
||||||
void _applyFilter(String filter) {
|
void _applyFilter(String filter) {
|
||||||
@@ -358,8 +349,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
|||||||
child: SpendingPieChart(
|
child: SpendingPieChart(
|
||||||
categories: categories, // Pass categories from stream snapshot
|
categories: categories, // Pass categories from stream snapshot
|
||||||
totalExpenses: totalExpenses, // Pass calculated total
|
totalExpenses: totalExpenses, // Pass calculated total
|
||||||
selectedPieIndex: _selectedPieIndex,
|
// Removed selectedPieIndex and onSelectPieCategory props
|
||||||
onSelectPieCategory: _selectPieCategory, // Callback for selection
|
|
||||||
animation: _pieChartAnimation, // Appearance animation
|
animation: _pieChartAnimation, // Appearance animation
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,22 +4,36 @@ import 'package:intl/intl.dart'; // For number formatting
|
|||||||
|
|
||||||
import '../models/category.dart'; // Keep using the Category model for UI structure
|
import '../models/category.dart'; // Keep using the Category model for UI structure
|
||||||
|
|
||||||
class SpendingPieChart extends StatelessWidget {
|
// Changed to StatefulWidget to manage its own selection state
|
||||||
|
class SpendingPieChart extends StatefulWidget {
|
||||||
final List<Category> categories; // Expect List<Category> from database calculation
|
final List<Category> categories; // Expect List<Category> from database calculation
|
||||||
final double totalExpenses;
|
final double totalExpenses;
|
||||||
final int selectedPieIndex;
|
|
||||||
final Function(int) onSelectPieCategory; // Callback when a slice is selected/deselected
|
|
||||||
final Animation<double> animation; // For fade/scale animation
|
final Animation<double> animation; // For fade/scale animation
|
||||||
|
|
||||||
const SpendingPieChart({
|
const SpendingPieChart({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.categories,
|
required this.categories,
|
||||||
required this.totalExpenses,
|
required this.totalExpenses,
|
||||||
required this.selectedPieIndex,
|
|
||||||
required this.onSelectPieCategory,
|
|
||||||
required this.animation,
|
required this.animation,
|
||||||
|
// Removed selectedPieIndex and onSelectPieCategory
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SpendingPieChart> createState() => _SpendingPieChartState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpendingPieChartState extends State<SpendingPieChart> {
|
||||||
|
// State for the selected index is now managed internally
|
||||||
|
int _selectedPieIndex = -1;
|
||||||
|
|
||||||
|
// Handles selection logic internally
|
||||||
|
void _handlePieTap(int index) {
|
||||||
|
setState(() {
|
||||||
|
// If the same index is selected, deselect (-1), otherwise select the new index
|
||||||
|
_selectedPieIndex = (_selectedPieIndex == index) ? -1 : index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
@@ -27,11 +41,11 @@ class SpendingPieChart extends StatelessWidget {
|
|||||||
final currencyFormatter = NumberFormat.currency(locale: 'en_US', symbol: '\$'); // Or your preferred locale/symbol
|
final currencyFormatter = NumberFormat.currency(locale: 'en_US', symbol: '\$'); // Or your preferred locale/symbol
|
||||||
|
|
||||||
// Handle the case where there are no categories to display
|
// Handle the case where there are no categories to display
|
||||||
if (categories.isEmpty) {
|
if (widget.categories.isEmpty) {
|
||||||
return AnimatedBuilder( // Still use animation for consistency
|
return AnimatedBuilder( // Still use animation for consistency
|
||||||
animation: animation,
|
animation: widget.animation,
|
||||||
builder: (context, child) => Opacity(
|
builder: (context, child) => Opacity(
|
||||||
opacity: animation.value,
|
opacity: widget.animation.value,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 230, // Maintain similar height to the chart version
|
height: 230, // Maintain similar height to the chart version
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@@ -48,12 +62,12 @@ class SpendingPieChart extends StatelessWidget {
|
|||||||
|
|
||||||
// Use AnimatedBuilder to apply the fade/scale animation
|
// Use AnimatedBuilder to apply the fade/scale animation
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: animation,
|
animation: widget.animation,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Transform.scale(
|
return Transform.scale(
|
||||||
scale: animation.value, // Apply scale animation
|
scale: widget.animation.value, // Apply scale animation
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: animation.value, // Apply fade animation
|
opacity: widget.animation.value, // Apply fade animation
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0), // Reduced vertical padding
|
padding: const EdgeInsets.symmetric(vertical: 8.0), // Reduced vertical padding
|
||||||
height: 230, // Fixed height for the chart and legend area
|
height: 230, // Fixed height for the chart and legend area
|
||||||
@@ -76,13 +90,13 @@ class SpendingPieChart extends StatelessWidget {
|
|||||||
if (section != null) {
|
if (section != null) {
|
||||||
// Tap occurred ON a section
|
// Tap occurred ON a section
|
||||||
final touchedIndex = section.touchedSectionIndex;
|
final touchedIndex = section.touchedSectionIndex;
|
||||||
// Toggle selection: if tapped section is already selected, deselect (-1), otherwise select it.
|
// Use internal handler to update state
|
||||||
onSelectPieCategory(touchedIndex == selectedPieIndex ? -1 : touchedIndex);
|
_handlePieTap(touchedIndex);
|
||||||
} else {
|
} else {
|
||||||
// Tap occurred OUTSIDE any section
|
// Tap occurred OUTSIDE any section
|
||||||
// Deselect if something was selected
|
// Deselect if something was selected
|
||||||
if (selectedPieIndex != -1) {
|
if (_selectedPieIndex != -1) {
|
||||||
onSelectPieCategory(-1);
|
_handlePieTap(-1); // Pass -1 to deselect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +113,7 @@ class SpendingPieChart extends StatelessWidget {
|
|||||||
swapAnimationCurve: Curves.easeInOut,
|
swapAnimationCurve: Curves.easeInOut,
|
||||||
),
|
),
|
||||||
// --- Center Text (Displayed when a slice is selected) ---
|
// --- Center Text (Displayed when a slice is selected) ---
|
||||||
if (selectedPieIndex != -1)
|
if (_selectedPieIndex != -1)
|
||||||
_buildCenterText(context, currencyFormatter)
|
_buildCenterText(context, currencyFormatter)
|
||||||
else // Optional: Display total or default text when nothing is selected
|
else // Optional: Display total or default text when nothing is selected
|
||||||
_buildDefaultCenterText(context, currencyFormatter),
|
_buildDefaultCenterText(context, currencyFormatter),
|
||||||
@@ -113,7 +127,7 @@ class SpendingPieChart extends StatelessWidget {
|
|||||||
flex: 4, // Allocate space for the legend
|
flex: 4, // Allocate space for the legend
|
||||||
// Use ListView for scrollable legend if many categories
|
// Use ListView for scrollable legend if many categories
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: categories.length,
|
itemCount: widget.categories.length,
|
||||||
padding: const EdgeInsets.only(right: 8), // Padding for legend items
|
padding: const EdgeInsets.only(right: 8), // Padding for legend items
|
||||||
itemBuilder: (context, index) => _buildPieLegendItem(context, index),
|
itemBuilder: (context, index) => _buildPieLegendItem(context, index),
|
||||||
),
|
),
|
||||||
@@ -131,11 +145,11 @@ class SpendingPieChart extends StatelessWidget {
|
|||||||
Widget _buildCenterText(BuildContext context, NumberFormat formatter) {
|
Widget _buildCenterText(BuildContext context, NumberFormat formatter) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
// Check if selectedPieIndex is valid before accessing categories
|
// Check if selectedPieIndex is valid before accessing categories
|
||||||
if (selectedPieIndex < 0 || selectedPieIndex >= categories.length) {
|
if (_selectedPieIndex < 0 || _selectedPieIndex >= widget.categories.length) {
|
||||||
// Return an empty container or default text if index is invalid
|
// Return an empty container or default text if index is invalid
|
||||||
return _buildDefaultCenterText(context, formatter);
|
return _buildDefaultCenterText(context, formatter);
|
||||||
}
|
}
|
||||||
final selectedCategory = categories[selectedPieIndex];
|
final selectedCategory = widget.categories[_selectedPieIndex];
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -177,7 +191,7 @@ class SpendingPieChart extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
formatter.format(totalExpenses), // Display total expenses
|
formatter.format(widget.totalExpenses), // Display total expenses
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: theme.textTheme.bodyLarge?.color // Use default text color
|
color: theme.textTheme.bodyLarge?.color // Use default text color
|
||||||
@@ -193,18 +207,18 @@ class SpendingPieChart extends StatelessWidget {
|
|||||||
Widget _buildPieLegendItem(BuildContext context, int index) {
|
Widget _buildPieLegendItem(BuildContext context, int index) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isSelected = index == selectedPieIndex; // Check if this item is selected
|
final isSelected = index == _selectedPieIndex; // Check if this item is selected using internal state
|
||||||
// Check if index is valid before accessing categories
|
// Check if index is valid before accessing categories
|
||||||
if (index < 0 || index >= categories.length) {
|
if (index < 0 || index >= widget.categories.length) {
|
||||||
return const SizedBox.shrink(); // Return empty if index is invalid
|
return const SizedBox.shrink(); // Return empty if index is invalid
|
||||||
}
|
}
|
||||||
final category = categories[index];
|
final category = widget.categories[index];
|
||||||
// Calculate percentage, handle totalExpenses being zero
|
// Calculate percentage, handle totalExpenses being zero
|
||||||
final percentage = totalExpenses > 0 ? (category.amount / totalExpenses * 100) : 0.0;
|
final percentage = widget.totalExpenses > 0 ? (category.amount / widget.totalExpenses * 100) : 0.0;
|
||||||
|
|
||||||
// Use InkWell for tap feedback and GestureDetector for tap logic
|
// Use InkWell for tap feedback and GestureDetector for tap logic
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => onSelectPieCategory(isSelected ? -1 : index), // Toggle selection on tap
|
onTap: () => _handlePieTap(index), // Use internal handler on tap
|
||||||
borderRadius: BorderRadius.circular(8), // Match border radius
|
borderRadius: BorderRadius.circular(8), // Match border radius
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200), // Animation for selection change
|
duration: const Duration(milliseconds: 200), // Animation for selection change
|
||||||
@@ -264,21 +278,21 @@ class SpendingPieChart extends StatelessWidget {
|
|||||||
List<PieChartSectionData> _generatePieSections(BuildContext context) {
|
List<PieChartSectionData> _generatePieSections(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return List.generate(categories.length, (i) {
|
return List.generate(widget.categories.length, (i) {
|
||||||
// Check if index is valid before accessing categories
|
// Check if index is valid before accessing categories
|
||||||
if (i < 0 || i >= categories.length) {
|
if (i < 0 || i >= widget.categories.length) {
|
||||||
// This should ideally not happen if List.generate is used correctly,
|
// This should ideally not happen if List.generate is used correctly,
|
||||||
// but adding a safeguard.
|
// but adding a safeguard.
|
||||||
return PieChartSectionData(); // Return an empty section
|
return PieChartSectionData(); // Return an empty section
|
||||||
}
|
}
|
||||||
final isTouched = i == selectedPieIndex; // Check if this slice is selected
|
final isTouched = i == _selectedPieIndex; // Check if this slice is selected using internal state
|
||||||
// Make selected slice slightly larger
|
// Make selected slice slightly larger
|
||||||
final double radius = isTouched ? 65 : 55;
|
final double radius = isTouched ? 65 : 55;
|
||||||
// Make title font slightly larger when selected
|
// Make title font slightly larger when selected
|
||||||
final double titleFontSize = isTouched ? 14 : 12;
|
final double titleFontSize = isTouched ? 14 : 12;
|
||||||
final category = categories[i];
|
final category = widget.categories[i];
|
||||||
// Calculate percentage for the title
|
// Calculate percentage for the title
|
||||||
final percentage = totalExpenses > 0 ? (category.amount / totalExpenses * 100) : 0;
|
final percentage = widget.totalExpenses > 0 ? (category.amount / widget.totalExpenses * 100) : 0;
|
||||||
|
|
||||||
return PieChartSectionData(
|
return PieChartSectionData(
|
||||||
color: category.colorCode.withOpacity(isDark ? 0.85 : 1.0), // Use category color
|
color: category.colorCode.withOpacity(isDark ? 0.85 : 1.0), // Use category color
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../database/database.dart' as db; // Import database with prefix 'db'
|
import '../database/database.dart' as db; // Keep alias if needed elsewhere, though not used here directly
|
||||||
import '../models/transaction_record.dart';
|
import '../models/transaction_record.dart';
|
||||||
import '../utils/category_utils.dart'; // Import category utils
|
import '../utils/category_utils.dart';
|
||||||
|
|
||||||
class TransactionListItem extends StatelessWidget {
|
class TransactionListItem extends StatelessWidget {
|
||||||
final TransactionRecord transaction; // Use the Drift-generated Transaction class
|
final TransactionRecord transaction;
|
||||||
final Animation<double> animation; // Keep animation for potential future use
|
final Animation<double> animation;
|
||||||
|
|
||||||
const TransactionListItem({
|
const TransactionListItem({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -19,10 +19,10 @@ class TransactionListItem extends StatelessWidget {
|
|||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
// Get category details (icon, color) using the utility
|
// Get category details
|
||||||
final categoryDetails = CategoryUtils.getCategoryDetails(transaction.categoryName);
|
final categoryDetails = CategoryUtils.getCategoryDetails(transaction.categoryName);
|
||||||
|
|
||||||
// Format the date and time using intl package
|
// Format dates
|
||||||
final dateFormatter = DateFormat.MMMd(); // e.g., Sep 10
|
final dateFormatter = DateFormat.MMMd(); // e.g., Sep 10
|
||||||
final timeFormatter = DateFormat.jm(); // e.g., 5:08 PM
|
final timeFormatter = DateFormat.jm(); // e.g., 5:08 PM
|
||||||
|
|
||||||
@@ -34,87 +34,118 @@ class TransactionListItem extends StatelessWidget {
|
|||||||
|
|
||||||
String displayDate;
|
String displayDate;
|
||||||
if (transactionDay == today) {
|
if (transactionDay == today) {
|
||||||
displayDate = 'Today, ${timeFormatter.format(transaction.date)}';
|
displayDate = 'Today';
|
||||||
} else if (transactionDay == yesterday) {
|
} else if (transactionDay == yesterday) {
|
||||||
displayDate = 'Yesterday, ${timeFormatter.format(transaction.date)}';
|
displayDate = 'Yesterday';
|
||||||
} else {
|
} else {
|
||||||
// Format for other dates (e.g., "Sep 10, 5:08 PM")
|
displayDate = dateFormatter.format(transaction.date);
|
||||||
displayDate = '${dateFormatter.format(transaction.date)}, ${timeFormatter.format(transaction.date)}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String displayTime = timeFormatter.format(transaction.date);
|
||||||
|
|
||||||
// Determine text colors based on theme
|
// Determine text colors based on theme
|
||||||
Color primaryTextColor = theme.textTheme.bodyLarge?.color ?? (isDark ? Colors.white : Colors.black87);
|
|
||||||
Color secondaryTextColor = theme.textTheme.bodyMedium?.color ?? (isDark ? Colors.white70 : Colors.grey.shade600);
|
Color secondaryTextColor = theme.textTheme.bodyMedium?.color ?? (isDark ? Colors.white70 : Colors.grey.shade600);
|
||||||
Color amountColor = isDark ? Colors.red.shade200 : Colors.red.shade700; // Expense color
|
Color amountColor = isDark ? Colors.redAccent.shade100 : Colors.red.shade700; // Keep amount color distinct
|
||||||
|
|
||||||
// Use FadeTransition for item appearance (works with ListView.builder)
|
// Use FadeTransition for item appearance
|
||||||
return FadeTransition(
|
return FadeTransition(
|
||||||
opacity: animation, // Apply fade animation
|
opacity: animation,
|
||||||
child: InkWell( // Make the item tappable
|
// Use SizeTransition for a smoother vertical entrance/exit animation
|
||||||
onTap: () {
|
child: SizeTransition(
|
||||||
// TODO: Implement navigation to transaction details screen or edit action
|
sizeFactor: animation,
|
||||||
print('Tapped transaction: ${transaction.id}');
|
child: Card(
|
||||||
},
|
// Reduce vertical margin to decrease space between items
|
||||||
child: Padding(
|
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.5), // Reduced vertical margin
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), // Consistent padding
|
shape: RoundedRectangleBorder(
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
children: [
|
),
|
||||||
// Icon container with category color/icon
|
elevation: 1.5, // Subtle elevation
|
||||||
Container(
|
child: ListTile(
|
||||||
padding: const EdgeInsets.all(10),
|
// Reduce vertical padding and apply compact density to decrease height
|
||||||
decoration: BoxDecoration(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 2.0), // Reduced vertical padding
|
||||||
color: categoryDetails.colorCode.withOpacity(isDark ? 0.25 : 0.15), // Use category color with opacity
|
visualDensity: VisualDensity.compact, // Make the ListTile more compact vertically
|
||||||
borderRadius: BorderRadius.circular(12), // Rounded corners
|
// Leading icon
|
||||||
),
|
leading: Container(
|
||||||
child: Icon(
|
width: 40, // Slightly larger icon background
|
||||||
categoryDetails.iconCode, // Use category icon
|
height: 40,
|
||||||
color: categoryDetails.colorCode, // Use category color for icon
|
decoration: BoxDecoration(
|
||||||
size: 20, // Icon size
|
// Use a slightly less opaque background for better icon visibility
|
||||||
),
|
color: categoryDetails.colorCode.withOpacity(isDark ? 0.25 : 0.18),
|
||||||
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12), // Spacing
|
child: Icon(
|
||||||
|
categoryDetails.iconCode,
|
||||||
|
color: categoryDetails.colorCode.withOpacity(isDark ? 0.95 : 1.0),
|
||||||
|
size: 20, // Slightly larger icon
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Transaction details (Merchant/Category and Date/Time)
|
// Title: Category Name
|
||||||
Expanded(
|
title: Text(
|
||||||
child: Column(
|
transaction.categoryName,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
style: const TextStyle(
|
||||||
children: [
|
fontWeight: FontWeight.w600,
|
||||||
// Display Merchant if available, otherwise Category Name
|
fontSize: 14.5, // Slightly larger title font
|
||||||
Text(
|
|
||||||
transaction.merchant.isNotEmpty ? transaction.merchant : transaction.categoryName,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500, // Medium weight for primary text
|
|
||||||
color: primaryTextColor,
|
|
||||||
),
|
|
||||||
maxLines: 1, // Prevent wrapping
|
|
||||||
overflow: TextOverflow.ellipsis, // Handle long text
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4), // Spacing between lines
|
|
||||||
// Display formatted date/time
|
|
||||||
Text(
|
|
||||||
displayDate,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: secondaryTextColor, // Lighter color for secondary text
|
|
||||||
fontSize: 12, // Smaller font size
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12), // Spacing before amount
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
|
||||||
// Transaction Amount
|
// Subtitle: Merchant and Date
|
||||||
Text(
|
subtitle: Row(
|
||||||
// Format amount as currency (negative for expense)
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Push date to the right
|
||||||
NumberFormat.currency(symbol: '-\$', decimalDigits: 2).format(transaction.amount),
|
children: [
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
Expanded( // Allow merchant name to take available space and ellipsis
|
||||||
fontWeight: FontWeight.w600, // Bold weight for amount
|
child: Text(
|
||||||
color: amountColor, // Use expense color
|
transaction.merchant,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.5, // Slightly larger subtitle font
|
||||||
|
color: secondaryTextColor,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
// Optionally add a small spacer if needed
|
||||||
],
|
// const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
displayDate, // Show only the date part in the subtitle
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11.5, // Smaller date font
|
||||||
|
color: secondaryTextColor.withOpacity(0.8), // Slightly faded date
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
// Trailing: Amount and Time
|
||||||
|
trailing: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center, // Center vertically
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end, // Align text to the right
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'- \$${transaction.amount.toStringAsFixed(2)}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: amountColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14, // Consistent font size with title
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3), // Small space between amount and time
|
||||||
|
Text(
|
||||||
|
displayTime, // Show time below the amount
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11.5, // Smaller time font
|
||||||
|
color: secondaryTextColor.withOpacity(0.8), // Slightly faded time
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Implement navigation to transaction details
|
||||||
|
print('Tapped transaction: ${transaction.id}');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user