added api for dashboard for collection widget

This commit is contained in:
Vaibhav Surve 2025-12-05 16:52:24 +05:30
parent 1717cd5e2b
commit 5d73fd6f4f
5 changed files with 557 additions and 569 deletions

View File

@ -8,6 +8,7 @@ import 'package:on_field_work/model/dashboard/expense_type_report_model.dart';
import 'package:on_field_work/model/dashboard/monthly_expence_model.dart'; import 'package:on_field_work/model/dashboard/monthly_expence_model.dart';
import 'package:on_field_work/model/expense/expense_type_model.dart'; import 'package:on_field_work/model/expense/expense_type_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
class DashboardController extends GetxController { class DashboardController extends GetxController {
// ========================= // =========================
@ -53,11 +54,14 @@ class DashboardController extends GetxController {
// Inject ProjectController // Inject ProjectController
final ProjectController projectController = Get.put(ProjectController()); final ProjectController projectController = Get.put(ProjectController());
// =========================
// Pending Expenses overview // Pending Expenses overview
// ========================= // =========================
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 Category Report // Expense Category Report
// ========================= // =========================
@ -67,32 +71,60 @@ class DashboardController extends GetxController {
final Rx<DateTime> expenseReportStartDate = final Rx<DateTime> expenseReportStartDate =
DateTime.now().subtract(const Duration(days: 15)).obs; DateTime.now().subtract(const Duration(days: 15)).obs;
final Rx<DateTime> expenseReportEndDate = DateTime.now().obs; final Rx<DateTime> expenseReportEndDate = DateTime.now().obs;
// ========================= // =========================
// Monthly Expense Report // Monthly Expense Report
// ========================= // =========================
final RxBool isMonthlyExpenseLoading = false.obs; final RxBool isMonthlyExpenseLoading = false.obs;
final RxList<MonthlyExpenseData> monthlyExpenseList = final RxList<MonthlyExpenseData> monthlyExpenseList =
<MonthlyExpenseData>[].obs; <MonthlyExpenseData>[].obs;
// =========================
// Monthly Expense Report Filters // Filters
// =========================
final Rx<MonthlyExpenseDuration> selectedMonthlyExpenseDuration = final Rx<MonthlyExpenseDuration> selectedMonthlyExpenseDuration =
MonthlyExpenseDuration.twelveMonths.obs; MonthlyExpenseDuration.twelveMonths.obs;
final RxInt selectedMonthsCount = 12.obs; final RxInt selectedMonthsCount = 12.obs;
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs; final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
final Rx<ExpenseTypeModel?> selectedExpenseType = Rx<ExpenseTypeModel?>(null); final Rx<ExpenseTypeModel?> selectedExpenseType = Rx<ExpenseTypeModel?>(null);
final isLoadingEmployees = true.obs; final isLoadingEmployees = true.obs;
// DashboardController
final RxList<EmployeeModel> employees = <EmployeeModel>[].obs; final RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
final uploadingStates = <String, RxBool>{}.obs; final uploadingStates = <String, RxBool>{}.obs;
// =========================
// Collection Overview
// =========================
final RxBool isCollectionOverviewLoading = false.obs;
final Rx<CollectionOverviewData?> collectionOverviewData =
Rx<CollectionOverviewData?>(null);
// ============================================================
// NEW DSO CALCULATION (Weighted Aging Method)
// ============================================================
double get calculatedDSO {
final data = collectionOverviewData.value;
if (data == null || data.totalDueAmount == 0) return 0.0;
final double totalDue = data.totalDueAmount;
// Weighted aging midpoints
const d0_30 = 15.0;
const d30_60 = 45.0;
const d60_90 = 75.0;
const d90_plus = 105.0; // conservative estimate
final double weightedDue = (data.bucket0To30Amount * d0_30) +
(data.bucket30To60Amount * d30_60) +
(data.bucket60To90Amount * d60_90) +
(data.bucket90PlusAmount * d90_plus);
return weightedDue / totalDue; // Final DSO
}
// Update selected expense type
void updateSelectedExpenseType(ExpenseTypeModel? type) { void updateSelectedExpenseType(ExpenseTypeModel? type) {
selectedExpenseType.value = type; selectedExpenseType.value = type;
// Debug print to verify
print('Selected: ${type?.name ?? "All Types"}');
if (type == null) { if (type == null) {
fetchMonthlyExpenses(); fetchMonthlyExpenses();
} else { } else {
@ -104,23 +136,17 @@ class DashboardController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
logSafe( logSafe('DashboardController initialized', level: LogLevel.info);
'DashboardController initialized',
level: LogLevel.info,
);
// React to project selection // React to project selection
ever<String>(projectController.selectedProjectId, (id) { ever<String>(projectController.selectedProjectId, (id) {
if (id.isNotEmpty) { if (id.isNotEmpty) {
logSafe('Project selected: $id', level: LogLevel.info);
fetchAllDashboardData(); fetchAllDashboardData();
fetchTodaysAttendance(id); fetchTodaysAttendance(id);
} else {
logSafe('No project selected yet.', level: LogLevel.warning);
} }
}); });
// React to expense report date changes // React to date range changes in expense report
everAll([expenseReportStartDate, expenseReportEndDate], (_) { everAll([expenseReportStartDate, expenseReportEndDate], (_) {
if (projectController.selectedProjectId.value.isNotEmpty) { if (projectController.selectedProjectId.value.isNotEmpty) {
fetchExpenseTypeReport( fetchExpenseTypeReport(
@ -130,10 +156,10 @@ class DashboardController extends GetxController {
} }
}); });
// React to attendance range changes // Attendance range
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
// React to project range changes // Project range
ever(projectSelectedRange, (_) => fetchProjectProgress()); ever(projectSelectedRange, (_) => fetchProjectProgress());
} }
@ -160,39 +186,25 @@ class DashboardController extends GetxController {
int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value); int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value);
int getProjectDays() => _getDaysFromRange(projectSelectedRange.value); int getProjectDays() => _getDaysFromRange(projectSelectedRange.value);
void updateAttendanceRange(String range) { void updateAttendanceRange(String range) =>
attendanceSelectedRange.value = range; attendanceSelectedRange.value = range;
logSafe('Attendance range updated to $range', level: LogLevel.debug);
}
void updateProjectRange(String range) { void updateProjectRange(String range) => projectSelectedRange.value = range;
projectSelectedRange.value = range;
logSafe('Project range updated to $range', level: LogLevel.debug);
}
void toggleAttendanceChartView(bool isChart) { void toggleAttendanceChartView(bool isChart) =>
attendanceIsChartView.value = isChart; attendanceIsChartView.value = isChart;
logSafe('Attendance chart view toggled to: $isChart',
level: LogLevel.debug);
}
void toggleProjectChartView(bool isChart) { void toggleProjectChartView(bool isChart) =>
projectIsChartView.value = isChart; projectIsChartView.value = isChart;
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
}
// ========================= // =========================
// Manual Refresh Methods // Manual Refresh
// ========================= // =========================
Future<void> refreshDashboard() async { Future<void> refreshDashboard() async => fetchAllDashboardData();
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
await fetchAllDashboardData();
}
Future<void> refreshAttendance() async => fetchRoleWiseAttendance(); Future<void> refreshAttendance() async => fetchRoleWiseAttendance();
Future<void> refreshTasks() async { Future<void> refreshTasks() async {
final projectId = projectController.selectedProjectId.value; final id = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) await fetchDashboardTasks(projectId: projectId); if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
} }
Future<void> refreshProjects() async => fetchProjectProgress(); Future<void> refreshProjects() async => fetchProjectProgress();
@ -202,12 +214,7 @@ class DashboardController extends GetxController {
// ========================= // =========================
Future<void> fetchAllDashboardData() async { Future<void> fetchAllDashboardData() async {
final String projectId = projectController.selectedProjectId.value; final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
if (projectId.isEmpty) {
logSafe('No project selected. Skipping dashboard API calls.',
level: LogLevel.warning);
return;
}
await Future.wait([ await Future.wait([
fetchRoleWiseAttendance(), fetchRoleWiseAttendance(),
@ -220,24 +227,45 @@ class DashboardController extends GetxController {
endDate: expenseReportEndDate.value, endDate: expenseReportEndDate.value,
), ),
fetchMonthlyExpenses(), fetchMonthlyExpenses(),
fetchMasterData() fetchMasterData(),
fetchCollectionOverview(),
]); ]);
} }
// =========================
// API Calls
// =========================
Future<void> fetchCollectionOverview() async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
try {
isCollectionOverviewLoading.value = true;
final response =
await ApiService.getCollectionOverview(projectId: projectId);
if (response != null && response.success) {
collectionOverviewData.value = response.data;
} else {
collectionOverviewData.value = null;
}
} finally {
isCollectionOverviewLoading.value = false;
}
}
Future<void> fetchTodaysAttendance(String projectId) async { Future<void> fetchTodaysAttendance(String projectId) async {
isLoadingEmployees.value = true; isLoadingEmployees.value = true;
final response = await ApiService.getAttendanceForDashboard(projectId); final response = await ApiService.getAttendanceForDashboard(projectId);
if (response != null) { if (response != null) {
employees.value = response; employees.value = response;
for (var emp in employees) { for (var emp in employees) {
uploadingStates[emp.id] = false.obs; uploadingStates[emp.id] = false.obs;
} }
logSafe(
"Dashboard Attendance fetched: ${employees.length} for project $projectId");
} else {
logSafe("Failed to fetch Dashboard Attendance for project $projectId",
level: LogLevel.error);
} }
isLoadingEmployees.value = false; isLoadingEmployees.value = false;
@ -272,102 +300,70 @@ class DashboardController extends GetxController {
Future<void> fetchMasterData() async { Future<void> fetchMasterData() async {
try { try {
final expenseTypesData = await ApiService.getMasterExpenseTypes(); final data = await ApiService.getMasterExpenseTypes();
if (expenseTypesData is List) { if (data is List) {
expenseTypes.value = expenseTypes.value =
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList(); data.map((e) => ExpenseTypeModel.fromJson(e)).toList();
}
} catch (e) {
logSafe('Error fetching master data', level: LogLevel.error, error: e);
} }
} catch (_) {}
} }
Future<void> fetchMonthlyExpenses({String? categoryId}) async { Future<void> fetchMonthlyExpenses({String? categoryId}) async {
try { try {
isMonthlyExpenseLoading.value = true; isMonthlyExpenseLoading.value = true;
int months = selectedMonthsCount.value;
logSafe(
'Fetching Monthly Expense Report for last $months months'
'${categoryId != null ? ' (categoryId: $categoryId)' : ''}',
level: LogLevel.info,
);
final response = await ApiService.getDashboardMonthlyExpensesApi( final response = await ApiService.getDashboardMonthlyExpensesApi(
categoryId: categoryId, categoryId: categoryId,
months: months, months: selectedMonthsCount.value,
); );
if (response != null && response.success) { if (response != null && response.success) {
monthlyExpenseList.value = response.data; monthlyExpenseList.value = response.data;
logSafe('Monthly Expense Report fetched successfully.',
level: LogLevel.info);
} else { } else {
monthlyExpenseList.clear(); monthlyExpenseList.clear();
logSafe('Failed to fetch Monthly Expense Report.',
level: LogLevel.error);
} }
} catch (e, st) {
monthlyExpenseList.clear();
logSafe('Error fetching Monthly Expense Report',
level: LogLevel.error, error: e, stackTrace: st);
} finally { } finally {
isMonthlyExpenseLoading.value = false; isMonthlyExpenseLoading.value = false;
} }
} }
Future<void> fetchPendingExpenses() async { Future<void> fetchPendingExpenses() async {
final String projectId = projectController.selectedProjectId.value; final id = projectController.selectedProjectId.value;
if (projectId.isEmpty) return; if (id.isEmpty) return;
try { try {
isPendingExpensesLoading.value = true; isPendingExpensesLoading.value = true;
final response =
await ApiService.getPendingExpensesApi(projectId: projectId); final response = await ApiService.getPendingExpensesApi(projectId: id);
if (response != null && response.success) { if (response != null && response.success) {
pendingExpensesData.value = response.data; pendingExpensesData.value = response.data;
logSafe('Pending expenses fetched successfully.', level: LogLevel.info);
} else { } else {
pendingExpensesData.value = null; pendingExpensesData.value = null;
logSafe('Failed to fetch pending expenses.', level: LogLevel.error);
} }
} catch (e, st) {
pendingExpensesData.value = null;
logSafe('Error fetching pending expenses',
level: LogLevel.error, error: e, stackTrace: st);
} finally { } finally {
isPendingExpensesLoading.value = false; isPendingExpensesLoading.value = false;
} }
} }
// =========================
// API Calls
// =========================
Future<void> fetchRoleWiseAttendance() async { Future<void> fetchRoleWiseAttendance() async {
final String projectId = projectController.selectedProjectId.value; final id = projectController.selectedProjectId.value;
if (projectId.isEmpty) return; if (id.isEmpty) return;
try { try {
isAttendanceLoading.value = true; isAttendanceLoading.value = true;
final List<dynamic>? response =
await ApiService.getDashboardAttendanceOverview( final response = await ApiService.getDashboardAttendanceOverview(
projectId, getAttendanceDays()); id,
getAttendanceDays(),
);
if (response != null) { if (response != null) {
roleWiseData.value = roleWiseData.value =
response.map((e) => Map<String, dynamic>.from(e)).toList(); response.map((e) => Map<String, dynamic>.from(e)).toList();
logSafe('Attendance overview fetched successfully.',
level: LogLevel.info);
} else { } else {
roleWiseData.clear(); roleWiseData.clear();
logSafe('Failed to fetch attendance overview: response is null.',
level: LogLevel.error);
} }
} catch (e, st) {
roleWiseData.clear();
logSafe('Error fetching attendance overview',
level: LogLevel.error, error: e, stackTrace: st);
} finally { } finally {
isAttendanceLoading.value = false; isAttendanceLoading.value = false;
} }
@ -377,109 +373,82 @@ class DashboardController extends GetxController {
required DateTime startDate, required DateTime startDate,
required DateTime endDate, required DateTime endDate,
}) async { }) async {
final String projectId = projectController.selectedProjectId.value; final id = projectController.selectedProjectId.value;
if (projectId.isEmpty) return; if (id.isEmpty) return;
try { try {
isExpenseTypeReportLoading.value = true; isExpenseTypeReportLoading.value = true;
final response = await ApiService.getExpenseTypeReportApi( final response = await ApiService.getExpenseTypeReportApi(
projectId: projectId, projectId: id,
startDate: startDate, startDate: startDate,
endDate: endDate, endDate: endDate,
); );
if (response != null && response.success) { if (response != null && response.success) {
expenseTypeReportData.value = response.data; expenseTypeReportData.value = response.data;
logSafe('Expense Category Report fetched successfully.',
level: LogLevel.info);
} else { } else {
expenseTypeReportData.value = null; expenseTypeReportData.value = null;
logSafe('Failed to fetch Expense Category Report.',
level: LogLevel.error);
} }
} catch (e, st) {
expenseTypeReportData.value = null;
logSafe('Error fetching Expense Category Report',
level: LogLevel.error, error: e, stackTrace: st);
} finally { } finally {
isExpenseTypeReportLoading.value = false; isExpenseTypeReportLoading.value = false;
} }
} }
Future<void> fetchProjectProgress() async { Future<void> fetchProjectProgress() async {
final String projectId = projectController.selectedProjectId.value; final id = projectController.selectedProjectId.value;
if (projectId.isEmpty) return; if (id.isEmpty) return;
try { try {
isProjectLoading.value = true; isProjectLoading.value = true;
final response = await ApiService.getProjectProgress( final response = await ApiService.getProjectProgress(
projectId: projectId, days: getProjectDays()); projectId: id,
days: getProjectDays(),
);
if (response != null && response.success) { if (response != null && response.success) {
projectChartData.value = projectChartData.value =
response.data.map((d) => ChartTaskData.fromProjectData(d)).toList(); response.data.map((d) => ChartTaskData.fromProjectData(d)).toList();
logSafe('Project progress data mapped for chart', level: LogLevel.info);
} else { } else {
projectChartData.clear(); projectChartData.clear();
logSafe('Failed to fetch project progress', level: LogLevel.error);
} }
} catch (e, st) {
projectChartData.clear();
logSafe('Error fetching project progress',
level: LogLevel.error, error: e, stackTrace: st);
} finally { } finally {
isProjectLoading.value = false; isProjectLoading.value = false;
} }
} }
Future<void> fetchDashboardTasks({required String projectId}) async { Future<void> fetchDashboardTasks({required String projectId}) async {
if (projectId.isEmpty) return;
try { try {
isTasksLoading.value = true; isTasksLoading.value = true;
final response = await ApiService.getDashboardTasks(projectId: projectId); final response = await ApiService.getDashboardTasks(projectId: projectId);
if (response != null && response.success) { if (response != null && response.success) {
totalTasks.value = response.data?.totalTasks ?? 0; totalTasks.value = response.data?.totalTasks ?? 0;
completedTasks.value = response.data?.completedTasks ?? 0; completedTasks.value = response.data?.completedTasks ?? 0;
logSafe('Dashboard tasks fetched', level: LogLevel.info);
} else { } else {
totalTasks.value = 0; totalTasks.value = 0;
completedTasks.value = 0; completedTasks.value = 0;
logSafe('Failed to fetch tasks', level: LogLevel.error);
} }
} catch (e, st) {
totalTasks.value = 0;
completedTasks.value = 0;
logSafe('Error fetching tasks',
level: LogLevel.error, error: e, stackTrace: st);
} finally { } finally {
isTasksLoading.value = false; isTasksLoading.value = false;
} }
} }
Future<void> fetchDashboardTeams({required String projectId}) async { Future<void> fetchDashboardTeams({required String projectId}) async {
if (projectId.isEmpty) return;
try { try {
isTeamsLoading.value = true; isTeamsLoading.value = true;
final response = await ApiService.getDashboardTeams(projectId: projectId); final response = await ApiService.getDashboardTeams(projectId: projectId);
if (response != null && response.success) { if (response != null && response.success) {
totalEmployees.value = response.data?.totalEmployees ?? 0; totalEmployees.value = response.data?.totalEmployees ?? 0;
inToday.value = response.data?.inToday ?? 0; inToday.value = response.data?.inToday ?? 0;
logSafe('Dashboard teams fetched', level: LogLevel.info);
} else { } else {
totalEmployees.value = 0; totalEmployees.value = 0;
inToday.value = 0; inToday.value = 0;
logSafe('Failed to fetch teams', level: LogLevel.error);
} }
} catch (e, st) {
totalEmployees.value = 0;
inToday.value = 0;
logSafe('Error fetching teams',
level: LogLevel.error, error: e, stackTrace: st);
} finally { } finally {
isTeamsLoading.value = false; isTeamsLoading.value = false;
} }

View File

@ -36,6 +36,7 @@ class ApiEndpoints {
"/Dashboard/expense/monthly"; "/Dashboard/expense/monthly";
static const String getExpenseTypeReport = "/Dashboard/expense/type"; static const String getExpenseTypeReport = "/Dashboard/expense/type";
static const String getPendingExpenses = "/Dashboard/expense/pendings"; static const String getPendingExpenses = "/Dashboard/expense/pendings";
static const String getCollectionOverview = "/dashboard/collection-overview";
///// Projects Module API Endpoints ///// Projects Module API Endpoints
static const String createProject = "/project"; static const String createProject = "/project";

View File

@ -45,6 +45,8 @@ import 'package:on_field_work/model/service_project/job_comments.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/infra_project/infra_project_list.dart'; import 'package:on_field_work/model/infra_project/infra_project_list.dart';
import 'package:on_field_work/model/infra_project/infra_project_details.dart'; import 'package:on_field_work/model/infra_project/infra_project_details.dart';
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
class ApiService { class ApiService {
static const bool enableLogs = true; static const bool enableLogs = true;
@ -315,6 +317,43 @@ class ApiService {
return null; return null;
} }
} }
/// ============================================
/// GET COLLECTION OVERVIEW (Dashboard)
/// ============================================
static Future<CollectionOverviewResponse?> getCollectionOverview({
String? projectId,
}) async {
try {
// Build query params (only add projectId if not null)
final queryParams = <String, String>{};
if (projectId != null && projectId.isNotEmpty) {
queryParams['projectId'] = projectId;
}
final response = await _getRequest(
ApiEndpoints.getCollectionOverview,
queryParams: queryParams,
);
if (response == null) {
_log("getCollectionOverview: No response from server",
level: LogLevel.error);
return null;
}
// Parse full JSON (success, message, data, etc.)
final parsedJson =
_parseResponseForAllData(response, label: "CollectionOverview");
if (parsedJson == null) return null;
return CollectionOverviewResponse.fromJson(parsedJson);
} catch (e, stack) {
_log("Exception in getCollectionOverview: $e\n$stack",
level: LogLevel.error);
return null;
}
}
// Infra Project Module APIs // Infra Project Module APIs

View File

@ -1,118 +1,128 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
// --- MAIN WIDGET FILE --- // ===============================================================
// MAIN WIDGET
// ===============================================================
class CollectionsHealthWidget extends StatelessWidget { class CollectionsHealthWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Derived Metrics from the JSON Analysis: return GetBuilder<DashboardController>(
const double totalDue = 34190.0; builder: (controller) {
const double totalCollected = 5000.0; final data = controller.collectionOverviewData.value;
const double totalValue = totalDue + totalCollected; final isLoading = controller.isCollectionOverviewLoading.value;
// Calculate Pending Percentage for Gauge if (isLoading) {
final double pendingPercentage = return const Center(
totalValue > 0 ? totalDue / totalValue : 0.0; child: Padding(
padding: EdgeInsets.all(32.0),
child: CircularProgressIndicator(),
),
);
}
// 1. MAIN CARD CONTAINER (White Theme) if (data == null) {
return Container( return Container(
decoration: BoxDecoration( decoration: _boxDecoration(),
color: Colors.white, padding: const EdgeInsets.all(16.0),
borderRadius: BorderRadius.circular(5), child: Center(
boxShadow: [ child: MyText.bodyMedium(
BoxShadow( 'No collection overview data available.',
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
), ),
],
), ),
);
}
final double totalDue = data.totalDueAmount;
final double totalCollected = data.totalCollectedAmount;
final double pendingPercentage = data.pendingPercentage / 100.0;
final double dsoDays = controller.calculatedDSO;
return Container(
decoration: _boxDecoration(),
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
// 1. HEADER
_buildHeader(), _buildHeader(),
const SizedBox(height: 20), const SizedBox(height: 20),
// 2. MAIN CONTENT ROW (Layout)
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
// Left Section: Gauge Chart, Due Amount, & Timelines
Expanded( Expanded(
flex: 5, flex: 5,
child: _buildLeftChartSection( child: _buildLeftChartSection(
totalDue: totalDue, totalDue: totalDue,
pendingPercentage: pendingPercentage, pendingPercentage: pendingPercentage,
totalCollected: totalCollected,
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
// Right Section: Metric Cards
Expanded( Expanded(
flex: 4, flex: 4,
child: _buildRightMetricsSection( child: _buildRightMetricsSection(
totalCollected: totalCollected, data: data,
dsoDays: dsoDays,
), ),
), ),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// 3. AGING ANALYSIS SECTION _buildAgingAnalysis(data: data),
_buildAgingAnalysis(),
], ],
), ),
); );
},
);
} }
// --- HELPER METHOD 1: HEADER --- // ===============================================================
// HEADER
// ===============================================================
Widget _buildHeader() { Widget _buildHeader() {
return Row(mainAxisAlignment: MainAxisAlignment.start, children: [ return Row(
children: [
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.bodyMedium( MyText.bodyMedium('Collections Health Overview', fontWeight: 700),
'Collections Health Overview',
fontWeight: 700,
),
const SizedBox(height: 2), const SizedBox(height: 2),
MyText.bodySmall( MyText.bodySmall('View your collection health data.',
'View your collection health data.', color: Colors.grey),
color: Colors.grey,
),
], ],
), ),
), ),
]); ],
);
} }
// --- HELPER METHOD 2: LEFT SECTION (CHARTS) --- // ===============================================================
// LEFT SECTION (GAUGE + SUMMARY + TREND PLACEHOLDERS)
// ===============================================================
Widget _buildLeftChartSection({ Widget _buildLeftChartSection({
required double totalDue, required double totalDue,
required double pendingPercentage, required double pendingPercentage,
required double totalCollected,
}) { }) {
// Format the percentage for display
String pendingPercentStr = (pendingPercentage * 100).toStringAsFixed(0); String pendingPercentStr = (pendingPercentage * 100).toStringAsFixed(0);
// Use the derived totalCollected for a better context String collectedPercentStr =
const double totalCollected = 5000.0; ((1 - pendingPercentage) * 100).toStringAsFixed(0);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
// Top: Gauge Chart Row(children: [
Row(
children: <Widget>[
_GaugeChartPlaceholder( _GaugeChartPlaceholder(
backgroundColor: Colors.white, backgroundColor: Colors.white,
pendingPercentage: pendingPercentage, pendingPercentage: pendingPercentage,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
], ]),
),
const SizedBox(height: 20), const SizedBox(height: 20),
// Total Due + Summary
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -125,56 +135,11 @@ class CollectionsHealthWidget extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
MyText.bodySmall( MyText.bodySmall(
'• Pending ($pendingPercentStr%) • ${totalCollected.toStringAsFixed(0)} Collected', '• Pending ($pendingPercentStr%) • Collected ($collectedPercentStr%)',
color: Colors.black54, color: Colors.black54,
), ),
],
),
),
],
),
const SizedBox(height: 20),
// Bottom: Timeline Charts (Trend Analysis)
Row(
children: <Widget>[
// Expected Collections Timeline (Bar Chart Placeholder)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
MyText.bodySmall( MyText.bodySmall(
'Expected Collections Trend', '${totalCollected.toStringAsFixed(0)} Collected',
),
const SizedBox(height: 8),
const _TimelineChartPlaceholder(
isBar: true,
barColor: Color(0xFF2196F3),
),
MyText.bodySmall(
'Week 16 Nov 2025',
color: Colors.black54,
),
],
),
),
const SizedBox(width: 10),
// Collection Rate Trend (Area Chart Placeholder)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
MyText.bodySmall(
'Collection Rate Trend',
),
const SizedBox(height: 8),
const _TimelineChartPlaceholder(
isBar: false,
areaColor: Color(0xFF4CAF50),
),
MyText.bodySmall(
'Week 14 Nov 2025',
color: Colors.black54, color: Colors.black54,
), ),
], ],
@ -186,56 +151,42 @@ class CollectionsHealthWidget extends StatelessWidget {
); );
} }
// --- HELPER METHOD 3: RIGHT SECTION (METRICS) --- // ===============================================================
// RIGHT SIDE METRICS
// ===============================================================
Widget _buildRightMetricsSection({ Widget _buildRightMetricsSection({
required double totalCollected, required CollectionOverviewData data,
required double dsoDays,
}) { }) {
final double totalCollected = data.totalCollectedAmount;
final String topClientName = data.topClient?.name ?? 'N/A';
final double topClientBalance = data.topClientBalance;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
// Metric Card 1: Top Client
_buildMetricCard( _buildMetricCard(
title: 'Top Client Balance', title: 'Top Client Balance',
value: 'Peninsula Land Limited', value: topClientName,
subValue: '34,190', subValue: '${topClientBalance.toStringAsFixed(0)}',
valueColor: const Color(0xFFF44336), // Red (Pending/Due) valueColor: Colors.red,
isDetailed: true, isDetailed: true,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Metric Card 2: Total Collected (YTD)
_buildMetricCard( _buildMetricCard(
title: 'Total Collected (YTD)', title: 'Total Collected (YTD)',
value: '${totalCollected.toStringAsFixed(0)}', value: '${totalCollected.toStringAsFixed(0)}',
subValue: 'Collected', subValue: 'Collected',
valueColor: const Color(0xFF4CAF50), // Green (Positive Value) valueColor: Colors.green,
isDetailed: false,
),
const SizedBox(height: 10),
// Metric Card 3: DSO
_buildMetricCard(
title: 'Days Sales Outstanding (DSO)',
value: '45 Days',
subValue: '↑ 5 Days',
valueColor: const Color(0xFFFF9800), // Orange (Needs improvement)
isDetailed: false,
),
const SizedBox(height: 10),
// Metric Card 4: Bad Debt Ratio
_buildMetricCard(
title: 'Bad Debt Ratio',
value: '0.8%',
subValue: '↓ 0.2%',
valueColor: const Color(0xFF4CAF50), // Green (Positive Change)
isDetailed: false, isDetailed: false,
), ),
], ],
); );
} }
// --- HELPER METHOD 4: METRIC CARD WIDGET --- // ===============================================================
// METRIC CARD UI
// ===============================================================
Widget _buildMetricCard({ Widget _buildMetricCard({
required String title, required String title,
required String value, required String value,
@ -252,34 +203,17 @@ class CollectionsHealthWidget extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
MyText.bodySmall( MyText.bodySmall(title, color: Colors.black54),
title,
color: Colors.black54,
),
const SizedBox(height: 2), const SizedBox(height: 2),
if (isDetailed) ...[ if (isDetailed) ...[
MyText.bodySmall( MyText.bodySmall(value, fontWeight: 600),
value, MyText.bodyMedium(subValue, color: valueColor, fontWeight: 700),
fontWeight: 600,
),
MyText.bodyMedium(
subValue,
color: valueColor,
fontWeight: 700,
),
] else ] else
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
MyText.bodySmall( MyText.bodySmall(value, fontWeight: 600),
value, MyText.bodySmall(subValue, color: valueColor, fontWeight: 600),
fontWeight: 600,
),
MyText.bodySmall(
subValue,
color: valueColor,
fontWeight: 600,
),
], ],
), ),
], ],
@ -287,101 +221,109 @@ class CollectionsHealthWidget extends StatelessWidget {
); );
} }
// --- NEW HELPER METHOD: AGING ANALYSIS --- // ===============================================================
Widget _buildAgingAnalysis() { // AGING ANALYSIS (DYNAMIC)
// Hardcoded data // ===============================================================
const double due0to20Days = 0.0; Widget _buildAgingAnalysis({required CollectionOverviewData data}) {
const double due20to45Days = 34190.0; final buckets = [
const double due45to90Days = 0.0;
const double dueOver90Days = 0.0;
final double totalOutstanding =
due0to20Days + due20to45Days + due45to90Days + dueOver90Days;
// Define buckets with their risk color
final List<AgingBucketData> buckets = [
AgingBucketData( AgingBucketData(
'0-20 Days', '0-30 Days',
due0to20Days, data.bucket0To30Amount,
const Color(0xFF4CAF50), // Green (Low Risk) Colors.green,
data.bucket0To30Invoices,
), ),
AgingBucketData( AgingBucketData(
'20-45 Days', '30-60 Days',
due20to45Days, data.bucket30To60Amount,
const Color(0xFFFF9800), // Orange (Medium Risk) Colors.orange,
data.bucket30To60Invoices,
), ),
AgingBucketData( AgingBucketData(
'45-90 Days', '60-90 Days',
due45to90Days, data.bucket60To90Amount,
const Color(0xFFF44336).withOpacity(0.7), // Light Red Colors.red.shade300,
data.bucket60To90Invoices,
), ),
AgingBucketData( AgingBucketData(
'> 90 Days', '> 90 Days',
dueOver90Days, data.bucket90PlusAmount,
const Color(0xFFF44336), // Dark Red Colors.red,
data.bucket90PlusInvoices,
), ),
]; ];
final double totalOutstanding = buckets.fold(0, (sum, b) => sum + b.amount);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: [
MyText.bodyMedium( MyText.bodyMedium(
'Outstanding Collections Aging Analysis', 'Outstanding Collections Aging Analysis',
fontWeight: 700, fontWeight: 700,
), ),
MyText.bodySmall(
'Total Outstanding: ₹${totalOutstanding.toStringAsFixed(0)}',
color: Colors.black54,
),
const SizedBox(height: 10), const SizedBox(height: 10),
// Stacked bar visualization
_AgingStackedBar( _AgingStackedBar(
buckets: buckets, buckets: buckets,
totalOutstanding: totalOutstanding, totalOutstanding: totalOutstanding,
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
// Legend / Bucket details
Wrap( Wrap(
spacing: 12, spacing: 12,
runSpacing: 8, runSpacing: 8,
children: buckets children: buckets
.map( .map((bucket) => _buildAgingLegendItem(bucket.title,
(bucket) => _buildAgingLegendItem( bucket.amount, bucket.color, bucket.invoiceCount))
bucket.title,
bucket.amount,
bucket.color,
),
)
.toList(), .toList(),
), ),
], ],
); );
} }
// Legend item for aging buckets Widget _buildAgingLegendItem(
Widget _buildAgingLegendItem(String title, double amount, Color color) { String title, double amount, Color color, int count // Updated parameter
) {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 10, width: 10,
height: 10, height: 10,
decoration: BoxDecoration( decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6), const SizedBox(width: 6),
MyText.bodySmall( MyText.bodySmall(
'$title: ₹${amount.toStringAsFixed(0)}', '$title: ₹${amount.toStringAsFixed(0)} (${count} Invoices)' // Updated text
),
],
);
}
// ===============================================================
// COMMON BOX DECORATION
// ===============================================================
BoxDecoration _boxDecoration() {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
), ),
], ],
); );
} }
} }
// --- CUSTOM PAINTERS / PLACEHOLDERS --- // =====================================================================
// CUSTOM PLACEHOLDER WIDGETS (Gauge + Bar/Area + Aging Bars)
// =====================================================================
// Placeholder for the Semi-Circle Gauge Chart // Gauge Chart
class _GaugeChartPlaceholder extends StatelessWidget { class _GaugeChartPlaceholder extends StatelessWidget {
final Color backgroundColor; final Color backgroundColor;
final double pendingPercentage; final double pendingPercentage;
@ -398,7 +340,6 @@ class _GaugeChartPlaceholder extends StatelessWidget {
height: 80, height: 80,
child: Stack( child: Stack(
children: [ children: [
// BACKGROUND GAUGE
CustomPaint( CustomPaint(
size: const Size(120, 70), size: const Size(120, 70),
painter: _SemiCirclePainter( painter: _SemiCirclePainter(
@ -406,17 +347,12 @@ class _GaugeChartPlaceholder extends StatelessWidget {
pendingPercentage: pendingPercentage, pendingPercentage: pendingPercentage,
), ),
), ),
// CENTER TEXT
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: FittedBox( child: FittedBox(
child: MyText.bodySmall( child: MyText.bodySmall('RISK LEVEL', fontWeight: 600),
'RISK LEVEL',
fontWeight: 600,
),
), ),
), ),
), ),
@ -426,15 +362,12 @@ class _GaugeChartPlaceholder extends StatelessWidget {
} }
} }
// Painter for the semi-circular gauge chart visualization
class _SemiCirclePainter extends CustomPainter { class _SemiCirclePainter extends CustomPainter {
final Color canvasColor; final Color canvasColor;
final double pendingPercentage; final double pendingPercentage;
_SemiCirclePainter({ _SemiCirclePainter(
required this.canvasColor, {required this.canvasColor, required this.pendingPercentage});
required this.pendingPercentage,
});
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
@ -443,184 +376,47 @@ class _SemiCirclePainter extends CustomPainter {
radius: size.width / 2, radius: size.width / 2,
); );
const double totalArc = 3.14159; const double arc = 3.14159;
final double pendingSweepAngle = totalArc * pendingPercentage; final double pendingSweep = arc * pendingPercentage;
final double collectedSweepAngle = totalArc * (1.0 - pendingPercentage); final double collectedSweep = arc * (1 - pendingPercentage);
// Background Arc
final backgroundPaint = Paint() final backgroundPaint = Paint()
..color = Colors.black.withOpacity(0.1) ..color = Colors.black.withOpacity(0.1)
..style = PaintingStyle.stroke
..strokeWidth = 10;
canvas.drawArc(rect, totalArc, totalArc, false, backgroundPaint);
// Pending Arc
final pendingPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 10 ..strokeWidth = 10
..shader = const LinearGradient(
colors: [
Color(0xFFFF9800),
Color(0xFFF44336),
],
).createShader(rect);
canvas.drawArc(rect, totalArc, pendingSweepAngle, false, pendingPaint);
// Collected Arc
final collectedPaint = Paint()
..color = const Color(0xFF4CAF50)
..style = PaintingStyle.stroke
..strokeWidth = 10;
canvas.drawArc(
rect,
totalArc + pendingSweepAngle,
collectedSweepAngle,
false,
collectedPaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// Placeholder for the Bar/Area Charts
class _TimelineChartPlaceholder extends StatelessWidget {
final bool isBar;
final Color? barColor;
final Color? areaColor;
const _TimelineChartPlaceholder({
required this.isBar,
this.barColor,
this.areaColor,
});
@override
Widget build(BuildContext context) {
return Container(
height: 50,
width: double.infinity,
color: Colors.transparent,
child: isBar
? _BarChartVisual(barColor: barColor!)
: _AreaChartVisual(areaColor: areaColor!),
);
}
}
class _BarChartVisual extends StatelessWidget {
final Color barColor;
const _BarChartVisual({required this.barColor});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
_Bar(0.4),
_Bar(0.7),
_Bar(1.0),
_Bar(0.6),
_Bar(0.8),
],
);
}
}
class _Bar extends StatelessWidget {
final double heightFactor;
const _Bar(this.heightFactor);
@override
Widget build(BuildContext context) {
// Bar color is taken from parent via DefaultTextStyle/Theme if needed;
// you can wrap with Theme if you want dynamic colors.
return Container(
width: 8,
height: 50 * heightFactor,
decoration: BoxDecoration(
color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(5),
),
);
}
}
class _AreaChartVisual extends StatelessWidget {
final Color areaColor;
const _AreaChartVisual({required this.areaColor});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _AreaChartPainter(areaColor: areaColor),
size: const Size(double.infinity, 50),
);
}
}
class _AreaChartPainter extends CustomPainter {
final Color areaColor;
_AreaChartPainter({required this.areaColor});
@override
void paint(Canvas canvas, Size size) {
final points = [
Offset(0, size.height * 0.5),
Offset(size.width * 0.25, size.height * 0.8),
Offset(size.width * 0.5, size.height * 0.3),
Offset(size.width * 0.75, size.height * 0.9),
Offset(size.width, size.height * 0.4),
];
final path = Path()
..moveTo(points.first.dx, size.height)
..lineTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
path.lineTo(points.last.dx, size.height);
path.close();
final areaPaint = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
areaColor.withOpacity(0.5),
areaColor.withOpacity(0.0),
],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height))
..style = PaintingStyle.fill;
canvas.drawPath(path, areaPaint);
final linePaint = Paint()
..color = areaColor
..strokeWidth = 2
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
canvas.drawPath(Path()..addPolygon(points, false), linePaint); canvas.drawArc(rect, arc, arc, false, backgroundPaint);
final pendingPaint = Paint()
..strokeWidth = 10
..style = PaintingStyle.stroke
..shader = const LinearGradient(
colors: [Colors.orange, Colors.red],
).createShader(rect);
canvas.drawArc(rect, arc, pendingSweep, false, pendingPaint);
final collectedPaint = Paint()
..color = Colors.green
..strokeWidth = 10
..style = PaintingStyle.stroke;
canvas.drawArc(
rect, arc + pendingSweep, collectedSweep, false, collectedPaint);
} }
@override @override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
} }
// --- DATA MODEL --- // AGING BUCKET
class AgingBucketData { class AgingBucketData {
final String title; final String title;
final double amount; final double amount;
final Color color; final Color color;
final int invoiceCount; // ADDED
AgingBucketData(this.title, this.amount, this.color); // UPDATED CONSTRUCTOR
AgingBucketData(this.title, this.amount, this.color, this.invoiceCount);
} }
// --- STACKED BAR VISUAL ---
class _AgingStackedBar extends StatelessWidget { class _AgingStackedBar extends StatelessWidget {
final List<AgingBucketData> buckets; final List<AgingBucketData> buckets;
final double totalOutstanding; final double totalOutstanding;
@ -640,31 +436,22 @@ class _AgingStackedBar extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Center( child: Center(
child: MyText.bodySmall( child: MyText.bodySmall('No Outstanding Collections',
'No Outstanding Collections', color: Colors.black54),
color: Colors.black54,
),
), ),
); );
} }
final List<Widget> segments =
buckets.where((b) => b.amount > 0).map((bucket) {
final double flexValue = bucket.amount / totalOutstanding;
return Expanded(
flex: (flexValue * 100).toInt(),
child: Container(
height: 16,
color: bucket.color,
),
);
}).toList();
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, children: buckets.where((b) => b.amount > 0).map((bucket) {
children: segments, final flexValue = bucket.amount / totalOutstanding;
return Expanded(
flex: (flexValue * 1000).toInt(),
child: Container(height: 16, color: bucket.color),
);
}).toList(),
), ),
); );
} }

View File

@ -0,0 +1,192 @@
import 'dart:convert';
/// ===============================
/// MAIN MODEL: CollectionOverview
/// ===============================
class CollectionOverviewResponse {
final bool success;
final String message;
final CollectionOverviewData data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
CollectionOverviewResponse({
required this.success,
required this.message,
required this.data,
required this.errors,
required this.statusCode,
required this.timestamp,
});
factory CollectionOverviewResponse.fromJson(Map<String, dynamic> json) {
return CollectionOverviewResponse(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: CollectionOverviewData.fromJson(json['data'] ?? {}),
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.tryParse(json['timestamp'] ?? '') ?? DateTime.now(),
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data.toJson(),
'errors': errors,
'statusCode': statusCode,
'timestamp': timestamp.toIso8601String(),
};
}
}
/// ===============================
/// DATA BLOCK
/// ===============================
class CollectionOverviewData {
final double totalDueAmount;
final double totalCollectedAmount;
final double totalValue;
final double pendingPercentage;
final double collectedPercentage;
final int bucket0To30Invoices;
final int bucket30To60Invoices;
final int bucket60To90Invoices;
final int bucket90PlusInvoices;
final double bucket0To30Amount;
final double bucket30To60Amount;
final double bucket60To90Amount;
final double bucket90PlusAmount;
final double topClientBalance;
final TopClient? topClient;
CollectionOverviewData({
required this.totalDueAmount,
required this.totalCollectedAmount,
required this.totalValue,
required this.pendingPercentage,
required this.collectedPercentage,
required this.bucket0To30Invoices,
required this.bucket30To60Invoices,
required this.bucket60To90Invoices,
required this.bucket90PlusInvoices,
required this.bucket0To30Amount,
required this.bucket30To60Amount,
required this.bucket60To90Amount,
required this.bucket90PlusAmount,
required this.topClientBalance,
required this.topClient,
});
factory CollectionOverviewData.fromJson(Map<String, dynamic> json) {
return CollectionOverviewData(
totalDueAmount: (json['totalDueAmount'] ?? 0).toDouble(),
totalCollectedAmount: (json['totalCollectedAmount'] ?? 0).toDouble(),
totalValue: (json['totalValue'] ?? 0).toDouble(),
pendingPercentage: (json['pendingPercentage'] ?? 0).toDouble(),
collectedPercentage: (json['collectedPercentage'] ?? 0).toDouble(),
bucket0To30Invoices: json['bucket0To30Invoices'] ?? 0,
bucket30To60Invoices: json['bucket30To60Invoices'] ?? 0,
bucket60To90Invoices: json['bucket60To90Invoices'] ?? 0,
bucket90PlusInvoices: json['bucket90PlusInvoices'] ?? 0,
bucket0To30Amount: (json['bucket0To30Amount'] ?? 0).toDouble(),
bucket30To60Amount: (json['bucket30To60Amount'] ?? 0).toDouble(),
bucket60To90Amount: (json['bucket60To90Amount'] ?? 0).toDouble(),
bucket90PlusAmount: (json['bucket90PlusAmount'] ?? 0).toDouble(),
topClientBalance: (json['topClientBalance'] ?? 0).toDouble(),
topClient: json['topClient'] != null
? TopClient.fromJson(json['topClient'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'totalDueAmount': totalDueAmount,
'totalCollectedAmount': totalCollectedAmount,
'totalValue': totalValue,
'pendingPercentage': pendingPercentage,
'collectedPercentage': collectedPercentage,
'bucket0To30Invoices': bucket0To30Invoices,
'bucket30To60Invoices': bucket30To60Invoices,
'bucket60To90Invoices': bucket60To90Invoices,
'bucket90PlusInvoices': bucket90PlusInvoices,
'bucket0To30Amount': bucket0To30Amount,
'bucket30To60Amount': bucket30To60Amount,
'bucket60To90Amount': bucket60To90Amount,
'bucket90PlusAmount': bucket90PlusAmount,
'topClientBalance': topClientBalance,
'topClient': topClient?.toJson(),
};
}
}
/// ===============================
/// NESTED MODEL: Top Client
/// ===============================
class TopClient {
final String id;
final String name;
final String? email;
final String? contactPerson;
final String? address;
final String? gstNumber;
final String? contactNumber;
final int? sprid;
TopClient({
required this.id,
required this.name,
this.email,
this.contactPerson,
this.address,
this.gstNumber,
this.contactNumber,
this.sprid,
});
factory TopClient.fromJson(Map<String, dynamic> json) {
return TopClient(
id: json['id'] ?? '',
name: json['name'] ?? '',
email: json['email'],
contactPerson: json['contactPerson'],
address: json['address'],
gstNumber: json['gstNumber'],
contactNumber: json['contactNumber'],
sprid: json['sprid'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'contactPerson': contactPerson,
'address': address,
'gstNumber': gstNumber,
'contactNumber': contactNumber,
'sprid': sprid,
};
}
}
/// ===============================
/// Optional: Quick decode method
/// ===============================
CollectionOverviewResponse parseCollectionOverview(String jsonString) {
return CollectionOverviewResponse.fromJson(jsonDecode(jsonString));
}