feat: Add Expense Type Report feature with chart visualization and API integration
This commit is contained in:
parent
f01608e4e7
commit
6d5137b103
@ -4,6 +4,7 @@ import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/model/dashboard/project_progress_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 {
|
||||
// =========================
|
||||
@ -54,6 +55,15 @@ class DashboardController extends GetxController {
|
||||
final RxBool isPendingExpensesLoading = false.obs;
|
||||
final Rx<PendingExpensesData?> pendingExpensesData =
|
||||
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
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -69,7 +79,12 @@ class DashboardController extends GetxController {
|
||||
ever<String>(projectController.selectedProjectId, (id) {
|
||||
fetchAllDashboardData();
|
||||
});
|
||||
|
||||
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
||||
fetchExpenseTypeReport(
|
||||
startDate: expenseReportStartDate.value,
|
||||
endDate: expenseReportEndDate.value,
|
||||
);
|
||||
});
|
||||
// React to range changes
|
||||
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
||||
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
||||
@ -152,7 +167,11 @@ class DashboardController extends GetxController {
|
||||
fetchProjectProgress(),
|
||||
fetchDashboardTasks(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 {
|
||||
final String projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
@ -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/attendance/organization_per_project_list_model.dart';
|
||||
import 'package:marco/model/dashboard/pending_expenses_model.dart';
|
||||
import 'package:marco/model/dashboard/expense_type_report_model.dart';
|
||||
|
||||
class ApiService {
|
||||
static const bool enableLogs = true;
|
||||
static const Duration extendedTimeout = Duration(seconds: 60);
|
||||
|
||||
static Future<String?> _getToken() async {
|
||||
final token = LocalStorage.getJwtToken();
|
||||
final token = LocalStorage.getJwtToken();
|
||||
|
||||
if (token == null) {
|
||||
logSafe("No JWT token found. Logging out...");
|
||||
@ -39,7 +40,7 @@ class ApiService {
|
||||
logSafe("Access token is expired. Attempting refresh...");
|
||||
final refreshed = await AuthService.refreshToken();
|
||||
if (refreshed) {
|
||||
return LocalStorage.getJwtToken();
|
||||
return LocalStorage.getJwtToken();
|
||||
} else {
|
||||
logSafe("Token refresh failed. Logging out immediately...");
|
||||
await LocalStorage.logout();
|
||||
@ -56,7 +57,7 @@ class ApiService {
|
||||
"Access token is about to expire in ${difference.inSeconds}s. Refreshing...");
|
||||
final refreshed = await AuthService.refreshToken();
|
||||
if (refreshed) {
|
||||
return LocalStorage.getJwtToken();
|
||||
return LocalStorage.getJwtToken();
|
||||
} else {
|
||||
logSafe("Token refresh failed (near expiry). Logging out...");
|
||||
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
|
||||
static Future<PendingExpensesResponse?> getPendingExpensesApi({
|
||||
required String projectId,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/extensions/date_time_extension.dart';
|
||||
|
||||
class Utils {
|
||||
@ -76,4 +77,12 @@ class Utils {
|
||||
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)}";
|
||||
}
|
||||
}
|
||||
|
||||
229
lib/helpers/widgets/dashbaord/expense_breakdown_chart.dart
Normal file
229
lib/helpers/widgets/dashbaord/expense_breakdown_chart.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/utils/utils.dart';
|
||||
|
||||
class ExpenseByStatusWidget extends StatelessWidget {
|
||||
final DashboardController controller;
|
||||
@ -15,43 +16,28 @@ class ExpenseByStatusWidget extends StatelessWidget {
|
||||
required String amount,
|
||||
required String count,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
radius: 20,
|
||||
child: Icon(icon, color: color, size: 22),
|
||||
backgroundColor: color.withOpacity(0.15),
|
||||
radius: 22,
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium(
|
||||
title,
|
||||
fontWeight: 600,
|
||||
),
|
||||
MyText.bodyMedium(title, fontWeight: 600),
|
||||
const SizedBox(height: 2),
|
||||
MyText.bodyMedium(
|
||||
amount,
|
||||
color: Colors.blue,
|
||||
),
|
||||
MyText.titleMedium(amount, color: Colors.blue, fontWeight: 700),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyText.titleMedium(
|
||||
count,
|
||||
color: Colors.blue,
|
||||
fontWeight: 600,
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: Colors.blue, size: 22),
|
||||
MyText.titleMedium(count, color: Colors.blue, fontWeight: 700),
|
||||
const Icon(Icons.chevron_right, color: Colors.blue, size: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -72,90 +58,75 @@ class ExpenseByStatusWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
"Expense - By Status",
|
||||
fontWeight: 700,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
MyText.bodyMedium(
|
||||
controller.projectController.selectedProjectName.value,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Pending Payment
|
||||
_buildStatusTile(
|
||||
icon: Icons.currency_rupee,
|
||||
color: Colors.blue,
|
||||
title: "Pending Payment",
|
||||
amount: "₹${data.processPending.amount.toStringAsFixed(1)}K",
|
||||
count: data.processPending.count.toString(),
|
||||
),
|
||||
|
||||
// Pending Approve
|
||||
_buildStatusTile(
|
||||
icon: Icons.check_circle_outline,
|
||||
color: Colors.orange,
|
||||
title: "Pending Approve",
|
||||
amount: "₹${data.approvePending.amount.toStringAsFixed(1)}K",
|
||||
count: data.approvePending.count.toString(),
|
||||
),
|
||||
|
||||
// Pending Review
|
||||
_buildStatusTile(
|
||||
icon: Icons.search,
|
||||
color: Colors.grey.shade700,
|
||||
title: "Pending Review",
|
||||
amount: "₹${data.reviewPending.amount.toStringAsFixed(1)}K",
|
||||
count: data.reviewPending.count.toString(),
|
||||
),
|
||||
|
||||
// Draft
|
||||
_buildStatusTile(
|
||||
icon: Icons.insert_drive_file_outlined,
|
||||
color: Colors.cyan,
|
||||
title: "Draft",
|
||||
amount: "₹${data.draft.amount.toStringAsFixed(1)}K",
|
||||
count: data.draft.count.toString(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
Divider(color: Colors.grey.shade300),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium("Expense - By Status", fontWeight: 700),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusTile(
|
||||
icon: Icons.currency_rupee,
|
||||
color: Colors.blue,
|
||||
title: "Pending Payment",
|
||||
amount: Utils.formatCurrency(data.processPending.totalAmount),
|
||||
count: data.processPending.count.toString(),
|
||||
),
|
||||
_buildStatusTile(
|
||||
icon: Icons.check_circle_outline,
|
||||
color: Colors.orange,
|
||||
title: "Pending Approve",
|
||||
amount: Utils.formatCurrency(data.approvePending.totalAmount),
|
||||
count: data.approvePending.count.toString(),
|
||||
),
|
||||
_buildStatusTile(
|
||||
icon: Icons.search,
|
||||
color: Colors.grey.shade700,
|
||||
title: "Pending Review",
|
||||
amount: Utils.formatCurrency(data.reviewPending.totalAmount),
|
||||
count: data.reviewPending.count.toString(),
|
||||
),
|
||||
_buildStatusTile(
|
||||
icon: Icons.insert_drive_file_outlined,
|
||||
color: Colors.cyan,
|
||||
title: "Draft",
|
||||
amount: Utils.formatCurrency(data.draft.totalAmount),
|
||||
count: data.draft.count.toString(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Divider(color: Colors.grey.shade300),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Project Spendings:", fontWeight: 600),
|
||||
MyText.bodySmall("(All Processed Payments)",
|
||||
color: Colors.grey.shade600),
|
||||
],
|
||||
),
|
||||
MyText.titleLarge(
|
||||
"${Utils.formatCurrency(data.totalAmount)} >",
|
||||
color: Colors.blue,
|
||||
fontWeight: 700,
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@ -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_text.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/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
@ -51,14 +53,22 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
children: [
|
||||
_buildDashboardStats(context),
|
||||
MySpacing.height(24),
|
||||
|
||||
// 📊 Attendance Section
|
||||
_buildAttendanceChartSection(),
|
||||
MySpacing.height(24),
|
||||
|
||||
ExpenseByStatusWidget(controller: dashboardController),
|
||||
MySpacing.height(24),
|
||||
|
||||
// Expense Type Report Chart
|
||||
ExpenseTypeReportChart(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Attendance Chart Section
|
||||
Widget _buildAttendanceChartSection() {
|
||||
return GetBuilder<ProjectController>(
|
||||
id: 'dashboard_controller',
|
||||
@ -81,7 +91,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
);
|
||||
}
|
||||
|
||||
/// No Project Assigned Message
|
||||
Widget _buildNoProjectMessage() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
@ -106,8 +115,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// Dashboard Statistics Section
|
||||
Widget _buildDashboardStats(BuildContext context) {
|
||||
final stats = [
|
||||
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success,
|
||||
@ -150,7 +157,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
);
|
||||
}
|
||||
|
||||
/// Stat Card (Compact + Small)
|
||||
Widget _buildStatCard(
|
||||
_StatItem statItem, bool isProjectSelected, double width) {
|
||||
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}) {
|
||||
return MyContainer.rounded(
|
||||
paddingAll: 4,
|
||||
@ -208,7 +213,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle Tap
|
||||
void _handleStatCardTap(_StatItem statItem, bool isEnabled) {
|
||||
if (!isEnabled) {
|
||||
Get.defaultDialog(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user