Merge pull request 'feat: Enhance FinanceScreen with animation and improved UI components' (#80) from PR_Dev_Vaibhav into Feature_PR

Reviewed-on: #80
This commit is contained in:
vaibhav.surve 2025-11-05 09:53:33 +00:00
commit a2f5414240

View File

@ -3,8 +3,6 @@ import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:get/get.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
@ -15,49 +13,254 @@ class FinanceScreen extends StatefulWidget {
State<FinanceScreen> createState() => _FinanceScreenState();
}
class _FinanceScreenState extends State<FinanceScreen> with UIMixin {
class _FinanceScreenState extends State<FinanceScreen>
with UIMixin, TickerProviderStateMixin {
final projectController = Get.find<ProjectController>();
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text("Finance"),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.5,
foregroundColor: Colors.black,
backgroundColor: const Color(0xFFF8F9FA),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Finance',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
),
],
),
),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFinanceStatCards(context),
],
body: FadeTransition(
opacity: _fadeAnimation,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWelcomeSection(),
MySpacing.height(24),
_buildFinanceModules(),
MySpacing.height(24),
_buildQuickStatsSection(),
],
),
),
),
);
}
Widget _buildFinanceStatCards(BuildContext context) {
Widget _buildWelcomeSection() {
final projectSelected = projectController.selectedProject != null;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
contentTheme.primary.withValues(alpha: 0.1),
contentTheme.info.withValues(alpha: 0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: contentTheme.primary.withValues(alpha: 0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: contentTheme.primary.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Icon(
LucideIcons.landmark,
color: contentTheme.primary,
size: 24,
),
),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
'Financial Management',
fontWeight: 700,
color: Colors.black87,
),
MySpacing.height(2),
MyText.bodySmall(
projectSelected
? 'Manage your project finances'
: 'Select a project to get started',
color: Colors.grey[600],
),
],
),
),
],
),
if (!projectSelected) ...[
MySpacing.height(12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.withValues(alpha: 0.3)),
),
child: Row(
children: [
Icon(
LucideIcons.badge_alert,
size: 16,
color: Colors.orange[700],
),
MySpacing.width(8),
Expanded(
child: MyText.bodySmall(
'Please select a project to access finance modules',
color: Colors.orange[700],
fontWeight: 500,
),
),
],
),
),
],
],
),
);
}
Widget _buildFinanceModules() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
'Finance Modules',
fontWeight: 700,
color: Colors.black87,
),
MySpacing.height(4),
MyText.bodySmall(
'Select a module to manage',
color: Colors.grey[600],
),
MySpacing.height(16),
_buildModuleGrid(),
],
);
}
Widget _buildModuleGrid() {
final stats = [
_FinanceStatItem(
LucideIcons.badge_dollar_sign,
"Expense",
"Track and manage expenses",
contentTheme.info,
"/dashboard/expense-main-page",
),
_FinanceStatItem(
LucideIcons.receipt_text,
"Payment Request",
"Submit payment requests",
contentTheme.primary,
"/dashboard/payment-request",
),
_FinanceStatItem(
LucideIcons.wallet,
"Advance Payment",
"Manage advance payments",
contentTheme.warning,
"/dashboard/advance-payment",
),
@ -65,77 +268,287 @@ class _FinanceScreenState extends State<FinanceScreen> with UIMixin {
final projectSelected = projectController.selectedProject != null;
return LayoutBuilder(
builder: (context, constraints) {
int crossAxisCount = (constraints.maxWidth ~/ 100).clamp(2, 6);
double cardWidth =
(constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
return Wrap(
spacing: 6,
runSpacing: 6,
alignment: WrapAlignment.start,
children: stats
.map((stat) => _buildFinanceCard(stat, projectSelected, cardWidth))
.toList(),
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.1,
),
itemCount: stats.length,
itemBuilder: (context, index) {
return _buildModernFinanceCard(
stats[index],
projectSelected,
index,
);
},
);
}
Widget _buildFinanceCard(
_FinanceStatItem statItem, bool isProjectSelected, double width) {
const double cardHeight = 80;
Widget _buildModernFinanceCard(
_FinanceStatItem statItem,
bool isProjectSelected,
int index,
) {
final bool isEnabled = isProjectSelected;
return Opacity(
opacity: isEnabled ? 1.0 : 0.4,
child: IgnorePointer(
ignoring: !isEnabled,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => _onCardTap(statItem, isEnabled),
child: MyCard.bordered(
width: width,
height: cardHeight,
paddingAll: 12,
borderRadiusAll: 8,
border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyContainer.rounded(
paddingAll: 6,
color: statItem.color.withOpacity(0.1),
child: Icon(
statItem.icon,
size: 18,
color: statItem.color,
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 400 + (index * 100)),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Opacity(
opacity: isEnabled ? 1.0 : 0.5,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _onCardTap(statItem, isEnabled),
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isEnabled
? statItem.color.withValues(alpha: 0.2)
: Colors.grey.withValues(alpha: 0.2),
width: 1.5,
),
),
child: Stack(
children: [
// Content
Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
child: Icon(
statItem.icon,
size: 28,
color: statItem.color,
),
),
MySpacing.height(12),
MyText.titleSmall(
statItem.title,
fontWeight: 700,
color: Colors.black87,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
MySpacing.height(4),
if (isEnabled)
Row(
children: [
MyText.bodySmall(
'View Details',
color: statItem.color,
fontWeight: 600,
fontSize: 11,
),
MySpacing.width(4),
Icon(
LucideIcons.arrow_right,
size: 14,
color: statItem.color,
),
],
),
],
),
),
// Lock icon for disabled state
if (!isEnabled)
Positioned(
top: 12,
right: 12,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(6),
),
child: Icon(
LucideIcons.lock,
size: 14,
color: Colors.grey[600],
),
),
),
],
),
),
MySpacing.height(8),
MyText.bodySmall(
statItem.title,
fontWeight: 600,
color: Colors.black87,
textAlign: TextAlign.center,
),
],
),
),
),
);
},
);
}
Widget _buildQuickStatsSection() {
final projectSelected = projectController.selectedProject != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
'Quick Stats',
fontWeight: 700,
color: Colors.black87,
),
MySpacing.height(4),
MyText.bodySmall(
'Overview of your finances',
color: Colors.grey[600],
),
MySpacing.height(16),
_buildStatsRow(projectSelected),
],
);
}
Widget _buildStatsRow(bool projectSelected) {
final stats = [
_QuickStat(
icon: LucideIcons.trending_up,
label: 'Total Expenses',
value: projectSelected ? '₹0' : '--',
color: contentTheme.danger,
),
_QuickStat(
icon: LucideIcons.clock,
label: 'Pending',
value: projectSelected ? '0' : '--',
color: contentTheme.warning,
),
];
return Row(
children: stats
.map((stat) => Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildStatCard(stat, projectSelected),
),
))
.toList(),
);
}
Widget _buildStatCard(_QuickStat stat, bool isEnabled) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: stat.color.withValues(alpha: 0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: stat.color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
stat.icon,
size: 20,
color: stat.color,
),
),
MySpacing.height(12),
MyText.bodySmall(
stat.label,
color: Colors.grey[600],
fontSize: 11,
),
MySpacing.height(4),
MyText.titleLarge(
stat.value,
fontWeight: 700,
color: isEnabled ? Colors.black87 : Colors.grey[400],
),
],
),
);
}
void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
if (!isEnabled) {
Get.defaultDialog(
title: "No Project Selected",
middleText: "Please select a project before accessing this section.",
confirm: ElevatedButton(
onPressed: () => Get.back(),
child: const Text("OK"),
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
LucideIcons.badge_alert,
color: Colors.orange[700],
size: 32,
),
),
MySpacing.height(16),
MyText.titleMedium(
"No Project Selected",
fontWeight: 700,
color: Colors.black87,
textAlign: TextAlign.center,
),
MySpacing.height(8),
MyText.bodyMedium(
"Please select a project before accessing this section.",
color: Colors.grey[600],
textAlign: TextAlign.center,
),
MySpacing.height(24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
backgroundColor: contentTheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
child: MyText.bodyMedium(
"OK",
color: Colors.white,
fontWeight: 600,
),
),
),
],
),
),
),
);
return;
@ -148,13 +561,29 @@ class _FinanceScreenState extends State<FinanceScreen> with UIMixin {
class _FinanceStatItem {
final IconData icon;
final String title;
final String subtitle;
final Color color;
final String route;
_FinanceStatItem(
this.icon,
this.title,
this.subtitle,
this.color,
this.route,
);
}
class _QuickStat {
final IconData icon;
final String label;
final String value;
final Color color;
_QuickStat({
required this.icon,
required this.label,
required this.value,
required this.color,
});
}