feat: Add Expense Type Report feature with chart visualization and API integration

This commit is contained in:
Vaibhav Surve 2025-10-30 15:50:55 +05:30
parent f01608e4e7
commit 6d5137b103
6 changed files with 426 additions and 120 deletions

View File

@ -4,6 +4,7 @@ import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:marco/model/dashboard/project_progress_model.dart'; import 'package:marco/model/dashboard/project_progress_model.dart';
import 'package:marco/model/dashboard/pending_expenses_model.dart'; import 'package:marco/model/dashboard/pending_expenses_model.dart';
import 'package:marco/model/dashboard/expense_type_report_model.dart';
class DashboardController extends GetxController { class DashboardController extends GetxController {
// ========================= // =========================
@ -54,6 +55,15 @@ class DashboardController extends GetxController {
final RxBool isPendingExpensesLoading = false.obs; final RxBool isPendingExpensesLoading = false.obs;
final Rx<PendingExpensesData?> pendingExpensesData = final Rx<PendingExpensesData?> pendingExpensesData =
Rx<PendingExpensesData?>(null); Rx<PendingExpensesData?>(null);
// =========================
// Expense Type Report
// =========================
final RxBool isExpenseTypeReportLoading = false.obs;
final Rx<ExpenseTypeReportData?> expenseTypeReportData =
Rx<ExpenseTypeReportData?>(null);
final Rx<DateTime> expenseReportStartDate = DateTime.now().obs;
final Rx<DateTime> expenseReportEndDate = DateTime.now().obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -69,7 +79,12 @@ class DashboardController extends GetxController {
ever<String>(projectController.selectedProjectId, (id) { ever<String>(projectController.selectedProjectId, (id) {
fetchAllDashboardData(); fetchAllDashboardData();
}); });
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
fetchExpenseTypeReport(
startDate: expenseReportStartDate.value,
endDate: expenseReportEndDate.value,
);
});
// React to range changes // React to range changes
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
ever(projectSelectedRange, (_) => fetchProjectProgress()); ever(projectSelectedRange, (_) => fetchProjectProgress());
@ -152,7 +167,11 @@ class DashboardController extends GetxController {
fetchProjectProgress(), fetchProjectProgress(),
fetchDashboardTasks(projectId: projectId), fetchDashboardTasks(projectId: projectId),
fetchDashboardTeams(projectId: projectId), fetchDashboardTeams(projectId: projectId),
fetchPendingExpenses(), fetchPendingExpenses(),
fetchExpenseTypeReport(
startDate: expenseReportStartDate.value,
endDate: expenseReportEndDate.value,
)
]); ]);
} }
@ -213,6 +232,39 @@ class DashboardController extends GetxController {
} }
} }
Future<void> fetchExpenseTypeReport({
required DateTime startDate,
required DateTime endDate,
}) async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
try {
isExpenseTypeReportLoading.value = true;
final response = await ApiService.getExpenseTypeReportApi(
projectId: projectId,
startDate: startDate,
endDate: endDate,
);
if (response != null && response.success) {
expenseTypeReportData.value = response.data;
logSafe('Expense Type Report fetched successfully.',
level: LogLevel.info);
} else {
expenseTypeReportData.value = null;
logSafe('Failed to fetch Expense Type Report.', level: LogLevel.error);
}
} catch (e, st) {
expenseTypeReportData.value = null;
logSafe('Error fetching Expense Type Report',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isExpenseTypeReportLoading.value = false;
}
}
Future<void> fetchProjectProgress() async { Future<void> fetchProjectProgress() async {
final String projectId = projectController.selectedProjectId.value; final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return; if (projectId.isEmpty) return;

View File

@ -20,13 +20,14 @@ import 'package:marco/model/document/document_details_model.dart';
import 'package:marco/model/document/document_version_model.dart'; import 'package:marco/model/document/document_version_model.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart';
import 'package:marco/model/dashboard/pending_expenses_model.dart'; import 'package:marco/model/dashboard/pending_expenses_model.dart';
import 'package:marco/model/dashboard/expense_type_report_model.dart';
class ApiService { class ApiService {
static const bool enableLogs = true; static const bool enableLogs = true;
static const Duration extendedTimeout = Duration(seconds: 60); static const Duration extendedTimeout = Duration(seconds: 60);
static Future<String?> _getToken() async { static Future<String?> _getToken() async {
final token = LocalStorage.getJwtToken(); final token = LocalStorage.getJwtToken();
if (token == null) { if (token == null) {
logSafe("No JWT token found. Logging out..."); logSafe("No JWT token found. Logging out...");
@ -39,7 +40,7 @@ class ApiService {
logSafe("Access token is expired. Attempting refresh..."); logSafe("Access token is expired. Attempting refresh...");
final refreshed = await AuthService.refreshToken(); final refreshed = await AuthService.refreshToken();
if (refreshed) { if (refreshed) {
return LocalStorage.getJwtToken(); return LocalStorage.getJwtToken();
} else { } else {
logSafe("Token refresh failed. Logging out immediately..."); logSafe("Token refresh failed. Logging out immediately...");
await LocalStorage.logout(); await LocalStorage.logout();
@ -56,7 +57,7 @@ class ApiService {
"Access token is about to expire in ${difference.inSeconds}s. Refreshing..."); "Access token is about to expire in ${difference.inSeconds}s. Refreshing...");
final refreshed = await AuthService.refreshToken(); final refreshed = await AuthService.refreshToken();
if (refreshed) { if (refreshed) {
return LocalStorage.getJwtToken(); return LocalStorage.getJwtToken();
} else { } else {
logSafe("Token refresh failed (near expiry). Logging out..."); logSafe("Token refresh failed (near expiry). Logging out...");
await LocalStorage.logout(); await LocalStorage.logout();
@ -289,6 +290,46 @@ class ApiService {
} }
} }
/// Get Expense Type Report
static Future<ExpenseTypeReportResponse?> getExpenseTypeReportApi({
required String projectId,
required DateTime startDate,
required DateTime endDate,
}) async {
const endpoint = ApiEndpoints.getExpenseTypeReport;
logSafe("Fetching Expense Type Report for projectId: $projectId");
try {
final response = await _getRequest(
endpoint,
queryParams: {
'projectId': projectId,
'startDate': startDate.toIso8601String(),
'endDate': endDate.toIso8601String(),
},
);
if (response == null) {
logSafe("Expense Type Report request failed: null response",
level: LogLevel.error);
return null;
}
final jsonResponse =
_parseResponseForAllData(response, label: "Expense Type Report");
if (jsonResponse != null) {
return ExpenseTypeReportResponse.fromJson(jsonResponse);
}
} catch (e, stack) {
logSafe("Exception during getExpenseTypeReportApi: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
/// Get Pending Expenses /// Get Pending Expenses
static Future<PendingExpensesResponse?> getPendingExpensesApi({ static Future<PendingExpensesResponse?> getPendingExpensesApi({
required String projectId, required String projectId,

View File

@ -1,3 +1,4 @@
import 'package:intl/intl.dart';
import 'package:marco/helpers/extensions/date_time_extension.dart'; import 'package:marco/helpers/extensions/date_time_extension.dart';
class Utils { class Utils {
@ -76,4 +77,12 @@ class Utils {
return "${b.toStringAsFixed(2)} Bytes"; return "${b.toStringAsFixed(2)} Bytes";
} }
} }
static String formatCurrency(num amount,
{String currency = "INR", String locale = "en_US"}) {
// Use en_US for standard K, M, B formatting
final symbol = NumberFormat.simpleCurrency(name: currency).currencySymbol;
final formatter = NumberFormat.compact(locale: 'en_US');
return "$symbol${formatter.format(amount)}";
}
} }

View File

@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/model/dashboard/expense_type_report_model.dart';
import 'package:marco/helpers/utils/utils.dart';
class ExpenseTypeReportChart extends StatelessWidget {
ExpenseTypeReportChart({Key? key}) : super(key: key);
final DashboardController _controller = Get.find<DashboardController>();
static const List<Color> _flatColors = [
Color(0xFFE57373), // Red 300
Color(0xFF64B5F6), // Blue 300
Color(0xFF81C784), // Green 300
Color(0xFFFFB74D), // Orange 300
Color(0xFFBA68C8), // Purple 300
Color(0xFFFF8A65), // Deep Orange 300
Color(0xFF4DB6AC), // Teal 300
Color(0xFFA1887F), // Brown 400
Color(0xFFDCE775), // Lime 300
Color(0xFF9575CD), // Deep Purple 300
Color(0xFF7986CB), // Indigo 300
Color(0xFFAED581), // Light Green 300
Color(0xFFFF7043), // Deep Orange 400
Color(0xFF4FC3F7), // Light Blue 300
Color(0xFFFFD54F), // Amber 300
Color(0xFF90A4AE), // Blue Grey 300
Color(0xFFE573BB), // Pink 300
Color(0xFF81D4FA), // Light Blue 200
Color(0xFFBCAAA4), // Brown 300
Color(0xFFA5D6A7), // Green 300
Color(0xFFCE93D8), // Purple 200
Color(0xFFFF8A65), // Deep Orange 300 (repeat to fill)
Color(0xFF80CBC4), // Teal 200
Color(0xFFFFF176), // Yellow 300
Color(0xFF90CAF9), // Blue 200
Color(0xFFE0E0E0), // Grey 300
Color(0xFFF48FB1), // Pink 200
Color(0xFFA1887F), // Brown 400 (repeat)
Color(0xFFB0BEC5), // Blue Grey 200
Color(0xFF81C784), // Green 300 (repeat)
Color(0xFFFFB74D), // Orange 300 (repeat)
Color(0xFF64B5F6), // Blue 300 (repeat)
];
Color _getSeriesColor(int index) => _flatColors[index % _flatColors.length];
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Obx(() {
final isLoading = _controller.isExpenseTypeReportLoading.value;
final data = _controller.expenseTypeReportData.value;
return Container(
decoration: _containerDecoration,
padding: EdgeInsets.symmetric(
vertical: 16,
horizontal: screenWidth < 600 ? 8 : 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Header(),
const SizedBox(height: 12),
// 👇 replace Expanded with fixed height
SizedBox(
height: 350, // choose based on your design
child: isLoading
? const Center(child: CircularProgressIndicator())
: data == null || data.report.isEmpty
? const _NoDataMessage()
: _ExpenseChart(
data: data,
getSeriesColor: _getSeriesColor,
),
),
],
),
);
});
}
BoxDecoration get _containerDecoration => BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 6,
spreadRadius: 1,
offset: const Offset(0, 2),
),
],
);
}
class _Header extends StatelessWidget {
const _Header({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium('Expense Type Overview', fontWeight: 700),
const SizedBox(height: 2),
MyText.bodySmall(
'Project-wise approved, pending, rejected & processed expenses',
color: Colors.grey),
],
);
}
}
// No data
class _NoDataMessage extends StatelessWidget {
const _NoDataMessage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, color: Colors.grey.shade400, size: 48),
const SizedBox(height: 10),
MyText.bodyMedium(
'No expense data available.',
textAlign: TextAlign.center,
color: Colors.grey.shade500,
),
],
),
),
);
}
}
// Chart
class _ExpenseChart extends StatelessWidget {
const _ExpenseChart({
Key? key,
required this.data,
required this.getSeriesColor,
}) : super(key: key);
final ExpenseTypeReportData data;
final Color Function(int index) getSeriesColor;
@override
Widget build(BuildContext context) {
final List<Map<String, dynamic>> chartSeries = [
{
'name': 'Approved',
'color': getSeriesColor(0),
'yValue': (ExpenseTypeReportItem e) => e.totalApprovedAmount,
},
{
'name': 'Pending',
'color': getSeriesColor(1),
'yValue': (ExpenseTypeReportItem e) => e.totalPendingAmount,
},
{
'name': 'Rejected',
'color': getSeriesColor(2),
'yValue': (ExpenseTypeReportItem e) => e.totalRejectedAmount,
},
{
'name': 'Processed',
'color': getSeriesColor(3),
'yValue': (ExpenseTypeReportItem e) => e.totalProcessedAmount,
},
];
return SfCartesianChart(
tooltipBehavior: TooltipBehavior(
enable: true,
shared: true,
builder: (data, point, series, pointIndex, seriesIndex) {
final ExpenseTypeReportItem item = data;
final value = chartSeries[seriesIndex]['yValue'](item);
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${chartSeries[seriesIndex]['name']}: ${Utils.formatCurrency(value)}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
);
},
),
legend: Legend(isVisible: true, position: LegendPosition.bottom),
primaryXAxis: CategoryAxis(labelRotation: 45),
primaryYAxis: NumericAxis(
// Format axis labels with Utils
axisLabelFormatter: (AxisLabelRenderDetails details) {
final num value = details.value;
return ChartAxisLabel(
Utils.formatCurrency(value), const TextStyle(fontSize: 10));
},
axisLine: const AxisLine(width: 0),
majorGridLines: const MajorGridLines(width: 0.5),
),
series: chartSeries.map((seriesInfo) {
return ColumnSeries<ExpenseTypeReportItem, String>(
dataSource: data.report,
xValueMapper: (item, _) => item.projectName,
yValueMapper: (item, _) => seriesInfo['yValue'](item),
name: seriesInfo['name'],
color: seriesInfo['color'],
dataLabelSettings: const DataLabelSettings(isVisible: true),
// Format data labels as well
dataLabelMapper: (item, _) =>
Utils.formatCurrency(seriesInfo['yValue'](item)),
);
}).toList(),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/utils/utils.dart';
class ExpenseByStatusWidget extends StatelessWidget { class ExpenseByStatusWidget extends StatelessWidget {
final DashboardController controller; final DashboardController controller;
@ -15,43 +16,28 @@ class ExpenseByStatusWidget extends StatelessWidget {
required String amount, required String amount,
required String count, required String count,
}) { }) {
return Container( return Padding(
margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row( child: Row(
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundColor: Colors.white, backgroundColor: color.withOpacity(0.15),
radius: 20, radius: 22,
child: Icon(icon, color: color, size: 22), child: Icon(icon, color: color, size: 24),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.titleMedium( MyText.bodyMedium(title, fontWeight: 600),
title,
fontWeight: 600,
),
const SizedBox(height: 2), const SizedBox(height: 2),
MyText.bodyMedium( MyText.titleMedium(amount, color: Colors.blue, fontWeight: 700),
amount,
color: Colors.blue,
),
], ],
), ),
), ),
MyText.titleMedium( MyText.titleMedium(count, color: Colors.blue, fontWeight: 700),
count, const Icon(Icons.chevron_right, color: Colors.blue, size: 24),
color: Colors.blue,
fontWeight: 600,
),
const Icon(Icons.chevron_right, color: Colors.blue, size: 22),
], ],
), ),
); );
@ -72,90 +58,75 @@ class ExpenseByStatusWidget extends StatelessWidget {
); );
} }
return Card( return Container(
elevation: 2, padding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), decoration: BoxDecoration(
child: Padding( color: Colors.white,
padding: const EdgeInsets.all(16), borderRadius: BorderRadius.circular(5),
child: Column( boxShadow: [
crossAxisAlignment: CrossAxisAlignment.start, BoxShadow(
children: [ color: Colors.grey.withOpacity(0.05),
MyText.titleLarge( blurRadius: 6,
"Expense - By Status", spreadRadius: 1,
fontWeight: 700, offset: const Offset(0, 2),
), ),
const SizedBox(height: 4), ],
MyText.bodyMedium( ),
controller.projectController.selectedProjectName.value, child: Column(
color: Colors.grey.shade600, crossAxisAlignment: CrossAxisAlignment.start,
), children: [
const SizedBox(height: 16), MyText.titleMedium("Expense - By Status", fontWeight: 700),
const SizedBox(height: 16),
// Pending Payment _buildStatusTile(
_buildStatusTile( icon: Icons.currency_rupee,
icon: Icons.currency_rupee, color: Colors.blue,
color: Colors.blue, title: "Pending Payment",
title: "Pending Payment", amount: Utils.formatCurrency(data.processPending.totalAmount),
amount: "${data.processPending.amount.toStringAsFixed(1)}K", count: data.processPending.count.toString(),
count: data.processPending.count.toString(), ),
), _buildStatusTile(
icon: Icons.check_circle_outline,
// Pending Approve color: Colors.orange,
_buildStatusTile( title: "Pending Approve",
icon: Icons.check_circle_outline, amount: Utils.formatCurrency(data.approvePending.totalAmount),
color: Colors.orange, count: data.approvePending.count.toString(),
title: "Pending Approve", ),
amount: "${data.approvePending.amount.toStringAsFixed(1)}K", _buildStatusTile(
count: data.approvePending.count.toString(), icon: Icons.search,
), color: Colors.grey.shade700,
title: "Pending Review",
// Pending Review amount: Utils.formatCurrency(data.reviewPending.totalAmount),
_buildStatusTile( count: data.reviewPending.count.toString(),
icon: Icons.search, ),
color: Colors.grey.shade700, _buildStatusTile(
title: "Pending Review", icon: Icons.insert_drive_file_outlined,
amount: "${data.reviewPending.amount.toStringAsFixed(1)}K", color: Colors.cyan,
count: data.reviewPending.count.toString(), title: "Draft",
), amount: Utils.formatCurrency(data.draft.totalAmount),
count: data.draft.count.toString(),
// Draft ),
_buildStatusTile( const SizedBox(height: 16),
icon: Icons.insert_drive_file_outlined, Divider(color: Colors.grey.shade300),
color: Colors.cyan, const SizedBox(height: 12),
title: "Draft", Row(
amount: "${data.draft.amount.toStringAsFixed(1)}K", mainAxisAlignment: MainAxisAlignment.spaceBetween,
count: data.draft.count.toString(), children: [
), Column(
crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 8), children: [
Divider(color: Colors.grey.shade300), MyText.bodyMedium("Project Spendings:", fontWeight: 600),
const SizedBox(height: 8), MyText.bodySmall("(All Processed Payments)",
color: Colors.grey.shade600),
Row( ],
mainAxisAlignment: MainAxisAlignment.spaceBetween, ),
children: [ MyText.titleLarge(
Column( "${Utils.formatCurrency(data.totalAmount)} >",
crossAxisAlignment: CrossAxisAlignment.start, color: Colors.blue,
children: [ fontWeight: 700,
MyText.bodyMedium( )
"Project Spendings:", ],
fontWeight: 600, ),
), ],
MyText.bodySmall(
"(All Processed Payments)",
color: Colors.grey.shade600,
),
],
),
MyText.titleLarge(
"${(data.totalAmount / 1000).toStringAsFixed(2)}K >",
color: Colors.blue,
fontWeight: 700,
),
],
),
],
),
), ),
); );
}); });

View File

@ -10,7 +10,9 @@ import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/dashbaord/attendance_overview_chart.dart'; import 'package:marco/helpers/widgets/dashbaord/attendance_overview_chart.dart';
import 'package:marco/helpers/widgets/dashbaord/expense_by_status_widget.dart';
import 'package:marco/view/layouts/layout.dart'; import 'package:marco/view/layouts/layout.dart';
import 'package:marco/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
class DashboardScreen extends StatefulWidget { class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key}); const DashboardScreen({super.key});
@ -51,14 +53,22 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
children: [ children: [
_buildDashboardStats(context), _buildDashboardStats(context),
MySpacing.height(24), MySpacing.height(24),
// 📊 Attendance Section
_buildAttendanceChartSection(), _buildAttendanceChartSection(),
MySpacing.height(24),
ExpenseByStatusWidget(controller: dashboardController),
MySpacing.height(24),
// Expense Type Report Chart
ExpenseTypeReportChart(),
], ],
), ),
), ),
); );
} }
/// Attendance Chart Section
Widget _buildAttendanceChartSection() { Widget _buildAttendanceChartSection() {
return GetBuilder<ProjectController>( return GetBuilder<ProjectController>(
id: 'dashboard_controller', id: 'dashboard_controller',
@ -81,7 +91,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
); );
} }
/// No Project Assigned Message
Widget _buildNoProjectMessage() { Widget _buildNoProjectMessage() {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
@ -106,8 +115,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
); );
} }
/// Dashboard Statistics Section
Widget _buildDashboardStats(BuildContext context) { Widget _buildDashboardStats(BuildContext context) {
final stats = [ final stats = [
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success,
@ -150,7 +157,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
); );
} }
/// Stat Card (Compact + Small)
Widget _buildStatCard( Widget _buildStatCard(
_StatItem statItem, bool isProjectSelected, double width) { _StatItem statItem, bool isProjectSelected, double width) {
const double cardHeight = 60; const double cardHeight = 60;
@ -195,7 +201,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
); );
} }
/// Compact Icon (smaller)
Widget _buildStatCardIconCompact(_StatItem statItem, {double size = 12}) { Widget _buildStatCardIconCompact(_StatItem statItem, {double size = 12}) {
return MyContainer.rounded( return MyContainer.rounded(
paddingAll: 4, paddingAll: 4,
@ -208,7 +213,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
); );
} }
/// Handle Tap
void _handleStatCardTap(_StatItem statItem, bool isEnabled) { void _handleStatCardTap(_StatItem statItem, bool isEnabled) {
if (!isEnabled) { if (!isEnabled) {
Get.defaultDialog( Get.defaultDialog(