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/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());
|
||||||
@ -153,6 +168,10 @@ class DashboardController extends GetxController {
|
|||||||
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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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: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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user