296 lines
9.7 KiB
Dart
296 lines
9.7 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:marco/controller/project_controller.dart';
|
||
import 'package:marco/controller/dynamicMenu/dynamic_menu_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_spacing.dart';
|
||
import 'package:marco/helpers/widgets/my_text.dart';
|
||
import 'package:marco/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
|
||
import 'package:marco/helpers/widgets/dashbaord/expense_by_status_widget.dart';
|
||
import 'package:marco/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
|
||
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
|
||
|
||
class FinanceScreen extends StatefulWidget {
|
||
const FinanceScreen({super.key});
|
||
|
||
@override
|
||
State<FinanceScreen> createState() => _FinanceScreenState();
|
||
}
|
||
|
||
class _FinanceScreenState extends State<FinanceScreen>
|
||
with UIMixin, TickerProviderStateMixin {
|
||
final projectController = Get.find<ProjectController>();
|
||
final DynamicMenuController menuController = Get.put(DynamicMenuController());
|
||
late AnimationController _animationController;
|
||
late Animation<double> _fadeAnimation;
|
||
final DashboardController dashboardController =
|
||
Get.put(DashboardController(), permanent: true);
|
||
|
||
@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: 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: FadeTransition(
|
||
opacity: _fadeAnimation,
|
||
child: Obx(() {
|
||
if (menuController.isLoading.value) {
|
||
return const Center(child: CircularProgressIndicator());
|
||
}
|
||
|
||
if (menuController.hasError.value || menuController.menuItems.isEmpty) {
|
||
return const Center(
|
||
child: Text(
|
||
"Failed to load menus. Please try again later.",
|
||
style: TextStyle(color: Colors.red),
|
||
),
|
||
);
|
||
}
|
||
|
||
// Filter allowed Finance menus dynamically
|
||
final financeMenuIds = [
|
||
MenuItems.expenseReimbursement,
|
||
MenuItems.paymentRequests,
|
||
MenuItems.advancePaymentStatements,
|
||
];
|
||
|
||
final financeMenus = menuController.menuItems
|
||
.where((m) => financeMenuIds.contains(m.id) && m.available)
|
||
.toList();
|
||
|
||
if (financeMenus.isEmpty) {
|
||
return const Center(
|
||
child: Text(
|
||
"You don’t have access to the Finance section.",
|
||
style: TextStyle(color: Colors.grey),
|
||
),
|
||
);
|
||
}
|
||
|
||
return SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
children: [
|
||
_buildFinanceModulesCompact(financeMenus),
|
||
MySpacing.height(24),
|
||
ExpenseByStatusWidget(controller: dashboardController),
|
||
MySpacing.height(24),
|
||
ExpenseTypeReportChart(),
|
||
MySpacing.height(24),
|
||
MonthlyExpenseDashboardChart(),
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
),
|
||
);
|
||
}
|
||
|
||
// --- Finance Modules (Compact Dashboard-style) ---
|
||
Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) {
|
||
// Map menu IDs to icon + color
|
||
final Map<String, _FinanceCardMeta> financeCardMeta = {
|
||
MenuItems.expenseReimbursement: _FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info),
|
||
MenuItems.paymentRequests: _FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary),
|
||
MenuItems.advancePaymentStatements: _FinanceCardMeta(LucideIcons.wallet, contentTheme.warning),
|
||
};
|
||
|
||
// Build the stat items using API-provided mobileLink
|
||
final stats = financeMenus.map((menu) {
|
||
final meta = financeCardMeta[menu.id]!;
|
||
|
||
// --- Log the routing info ---
|
||
debugPrint(
|
||
"[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}");
|
||
|
||
return _FinanceStatItem(
|
||
meta.icon,
|
||
menu.name,
|
||
meta.color,
|
||
menu.mobileLink, // Each card navigates to its own route
|
||
);
|
||
}).toList();
|
||
|
||
final projectSelected = projectController.selectedProject != null;
|
||
|
||
return LayoutBuilder(builder: (context, constraints) {
|
||
// Determine number of columns dynamically
|
||
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
|
||
double cardWidth = (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
|
||
|
||
return Wrap(
|
||
spacing: 6,
|
||
runSpacing: 6,
|
||
alignment: WrapAlignment.end,
|
||
children: stats
|
||
.map((stat) => _buildFinanceModuleCard(stat, projectSelected, cardWidth))
|
||
.toList(),
|
||
);
|
||
});
|
||
}
|
||
|
||
Widget _buildFinanceModuleCard(
|
||
_FinanceStatItem stat, bool isProjectSelected, double width) {
|
||
return Opacity(
|
||
opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
|
||
child: IgnorePointer(
|
||
ignoring: !isProjectSelected,
|
||
child: InkWell(
|
||
onTap: () => _onCardTap(stat, isProjectSelected),
|
||
borderRadius: BorderRadius.circular(5),
|
||
child: MyCard.bordered(
|
||
width: width,
|
||
height: 60,
|
||
paddingAll: 4,
|
||
borderRadiusAll: 5,
|
||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(4),
|
||
decoration: BoxDecoration(
|
||
color: stat.color.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Icon(
|
||
stat.icon,
|
||
size: 16,
|
||
color: stat.color,
|
||
),
|
||
),
|
||
MySpacing.height(4),
|
||
Flexible(
|
||
child: Text(
|
||
stat.title,
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(
|
||
fontSize: 10,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
maxLines: 2,
|
||
softWrap: true,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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"),
|
||
),
|
||
);
|
||
} else {
|
||
// Navigate to the card's specific route
|
||
Get.toNamed(statItem.route);
|
||
}
|
||
}
|
||
}
|
||
|
||
class _FinanceStatItem {
|
||
final IconData icon;
|
||
final String title;
|
||
final Color color;
|
||
final String route;
|
||
|
||
_FinanceStatItem(this.icon, this.title, this.color, this.route);
|
||
}
|
||
|
||
class _FinanceCardMeta {
|
||
final IconData icon;
|
||
final Color color;
|
||
_FinanceCardMeta(this.icon, this.color);
|
||
}
|