addedapi for purchase invoice
This commit is contained in:
parent
8686d696f0
commit
48a96a703b
@ -9,6 +9,7 @@ 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/employees/employee_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
||||
|
||||
class DashboardController extends GetxController {
|
||||
// Dependencies
|
||||
@ -68,9 +69,13 @@ class DashboardController extends GetxController {
|
||||
final uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
// Collection
|
||||
final isCollectionOverviewLoading = false.obs;
|
||||
final isCollectionOverviewLoading = true.obs;
|
||||
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
||||
|
||||
// =========================
|
||||
// Purchase Invoice Overview
|
||||
// =========================
|
||||
final isPurchaseInvoiceLoading = true.obs;
|
||||
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
||||
// Constants
|
||||
final List<String> ranges = ['7D', '15D', '30D'];
|
||||
static const _rangeDaysMap = {
|
||||
@ -211,6 +216,7 @@ class DashboardController extends GetxController {
|
||||
fetchMonthlyExpenses(),
|
||||
fetchMasterData(),
|
||||
fetchCollectionOverview(),
|
||||
fetchPurchaseInvoiceOverview(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -259,6 +265,19 @@ class DashboardController extends GetxController {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchPurchaseInvoiceOverview() async {
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
await _executeApiCall(isPurchaseInvoiceLoading, () async {
|
||||
final response = await ApiService.getPurchaseInvoiceOverview(
|
||||
projectId: projectId,
|
||||
);
|
||||
purchaseInvoiceOverviewData.value =
|
||||
(response?.success == true) ? response!.data : null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchPendingExpenses() async {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
if (id.isEmpty) return;
|
||||
|
||||
@ -38,6 +38,9 @@ class ApiEndpoints {
|
||||
static const String getPendingExpenses = "/Dashboard/expense/pendings";
|
||||
static const String getCollectionOverview = "/dashboard/collection-overview";
|
||||
|
||||
static const String getPurchaseInvoiceOverview =
|
||||
"/dashboard/purchase-invoice-overview";
|
||||
|
||||
///// Projects Module API Endpoints
|
||||
static const String createProject = "/project";
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ 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_details.dart';
|
||||
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
||||
|
||||
|
||||
class ApiService {
|
||||
@ -317,6 +318,45 @@ class ApiService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// ============================================
|
||||
/// GET PURCHASE INVOICE OVERVIEW (Dashboard)
|
||||
/// ============================================
|
||||
static Future<PurchaseInvoiceOverviewResponse?> getPurchaseInvoiceOverview({
|
||||
String? projectId,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, String>{};
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
queryParams['projectId'] = projectId;
|
||||
}
|
||||
|
||||
final response = await _getRequest(
|
||||
ApiEndpoints.getPurchaseInvoiceOverview,
|
||||
queryParams: queryParams,
|
||||
);
|
||||
|
||||
if (response == null) {
|
||||
_log("getPurchaseInvoiceOverview: No response from server",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
final parsedJson = _parseResponseForAllData(
|
||||
response,
|
||||
label: "PurchaseInvoiceOverview",
|
||||
);
|
||||
|
||||
if (parsedJson == null) return null;
|
||||
|
||||
return PurchaseInvoiceOverviewResponse.fromJson(parsedJson);
|
||||
} catch (e, stack) {
|
||||
_log("Exception in getPurchaseInvoiceOverview: $e\n$stack",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/// ============================================
|
||||
/// GET COLLECTION OVERVIEW (Dashboard)
|
||||
/// ============================================
|
||||
|
||||
@ -3,85 +3,83 @@ import 'package:get/get.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';
|
||||
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
|
||||
|
||||
// ===============================================================
|
||||
// MAIN WIDGET
|
||||
// ===============================================================
|
||||
class CollectionsHealthWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<DashboardController>(
|
||||
builder: (controller) {
|
||||
final data = controller.collectionOverviewData.value;
|
||||
final isLoading = controller.isCollectionOverviewLoading.value;
|
||||
final DashboardController controller = Get.find<DashboardController>();
|
||||
|
||||
if (isLoading) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Obx(() {
|
||||
final data = controller.collectionOverviewData.value;
|
||||
final isLoading = controller.isCollectionOverviewLoading.value;
|
||||
|
||||
if (data == null) {
|
||||
return Container(
|
||||
decoration: _boxDecoration(),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: MyText.bodyMedium(
|
||||
'No collection overview data available.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final double totalDue = data.totalDueAmount;
|
||||
final double totalCollected = data.totalCollectedAmount;
|
||||
final double pendingPercentage = data.pendingPercentage / 100.0;
|
||||
final double dsoDays = controller.calculatedDSO;
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return Container(
|
||||
decoration: _boxDecoration(), // Maintain the outer box decoration
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SkeletonLoaders.collectionHealthSkeleton(),
|
||||
);
|
||||
}
|
||||
|
||||
// No data
|
||||
if (data == null) {
|
||||
return Container(
|
||||
decoration: _boxDecoration(),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: _buildLeftChartSection(
|
||||
totalDue: totalDue,
|
||||
pendingPercentage: pendingPercentage,
|
||||
totalCollected: totalCollected,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: _buildRightMetricsSection(
|
||||
data: data,
|
||||
dsoDays: dsoDays,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildAgingAnalysis(data: data),
|
||||
],
|
||||
child: Center(
|
||||
child: MyText.bodyMedium('No collection overview data available.'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Data available
|
||||
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),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: _buildLeftChartSection(
|
||||
totalDue: totalDue,
|
||||
pendingPercentage: pendingPercentage,
|
||||
totalCollected: totalCollected,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: _buildRightMetricsSection(
|
||||
data: data,
|
||||
dsoDays: dsoDays,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildAgingAnalysis(data: data),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// ==============================
|
||||
// HEADER
|
||||
// ===============================================================
|
||||
// ==============================
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
@ -100,9 +98,9 @@ class CollectionsHealthWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// LEFT SECTION (GAUGE + SUMMARY + TREND PLACEHOLDERS)
|
||||
// ===============================================================
|
||||
// ==============================
|
||||
// LEFT SECTION (GAUGE + SUMMARY)
|
||||
// ==============================
|
||||
Widget _buildLeftChartSection({
|
||||
required double totalDue,
|
||||
required double pendingPercentage,
|
||||
@ -115,13 +113,15 @@ class CollectionsHealthWidget extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(children: [
|
||||
_GaugeChartPlaceholder(
|
||||
backgroundColor: Colors.white,
|
||||
pendingPercentage: pendingPercentage,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
]),
|
||||
Row(
|
||||
children: [
|
||||
_GaugeChartPlaceholder(
|
||||
backgroundColor: Colors.white,
|
||||
pendingPercentage: pendingPercentage,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
@ -151,15 +151,13 @@ class CollectionsHealthWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// RIGHT SIDE METRICS
|
||||
// ===============================================================
|
||||
// ==============================
|
||||
// RIGHT METRICS SECTION
|
||||
// ==============================
|
||||
Widget _buildRightMetricsSection({
|
||||
required CollectionOverviewData data,
|
||||
required double dsoDays,
|
||||
}) {
|
||||
final double totalCollected = data.totalCollectedAmount;
|
||||
|
||||
final String topClientName = data.topClient?.name ?? 'N/A';
|
||||
final double topClientBalance = data.topClientBalance;
|
||||
|
||||
@ -175,7 +173,7 @@ class CollectionsHealthWidget extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
_buildMetricCard(
|
||||
title: 'Total Collected (YTD)',
|
||||
value: '₹${totalCollected.toStringAsFixed(0)}',
|
||||
value: '₹${data.totalCollectedAmount.toStringAsFixed(0)}',
|
||||
subValue: 'Collected',
|
||||
valueColor: Colors.green,
|
||||
isDetailed: false,
|
||||
@ -184,9 +182,6 @@ class CollectionsHealthWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// METRIC CARD UI
|
||||
// ===============================================================
|
||||
Widget _buildMetricCard({
|
||||
required String title,
|
||||
required String value,
|
||||
@ -221,35 +216,19 @@ class CollectionsHealthWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// AGING ANALYSIS (DYNAMIC)
|
||||
// ===============================================================
|
||||
// ==============================
|
||||
// AGING ANALYSIS
|
||||
// ==============================
|
||||
Widget _buildAgingAnalysis({required CollectionOverviewData data}) {
|
||||
final buckets = [
|
||||
AgingBucketData(
|
||||
'0-30 Days',
|
||||
data.bucket0To30Amount,
|
||||
Colors.green,
|
||||
data.bucket0To30Invoices,
|
||||
),
|
||||
AgingBucketData(
|
||||
'30-60 Days',
|
||||
data.bucket30To60Amount,
|
||||
Colors.orange,
|
||||
data.bucket30To60Invoices,
|
||||
),
|
||||
AgingBucketData(
|
||||
'60-90 Days',
|
||||
data.bucket60To90Amount,
|
||||
Colors.red.shade300,
|
||||
data.bucket60To90Invoices,
|
||||
),
|
||||
AgingBucketData(
|
||||
'> 90 Days',
|
||||
data.bucket90PlusAmount,
|
||||
Colors.red,
|
||||
data.bucket90PlusInvoices,
|
||||
),
|
||||
AgingBucketData('0-30 Days', data.bucket0To30Amount, Colors.green,
|
||||
data.bucket0To30Invoices),
|
||||
AgingBucketData('30-60 Days', data.bucket30To60Amount, Colors.orange,
|
||||
data.bucket30To60Invoices),
|
||||
AgingBucketData('60-90 Days', data.bucket60To90Amount,
|
||||
Colors.red.shade300, data.bucket60To90Invoices),
|
||||
AgingBucketData('> 90 Days', data.bucket90PlusAmount, Colors.red,
|
||||
data.bucket90PlusInvoices),
|
||||
];
|
||||
|
||||
final double totalOutstanding = buckets.fold(0, (sum, b) => sum + b.amount);
|
||||
@ -257,19 +236,13 @@ class CollectionsHealthWidget extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
'Outstanding Collections Aging Analysis',
|
||||
fontWeight: 700,
|
||||
),
|
||||
MyText.bodyMedium('Outstanding Collections Aging Analysis',
|
||||
fontWeight: 700),
|
||||
MyText.bodySmall(
|
||||
'Total Outstanding: ₹${totalOutstanding.toStringAsFixed(0)}',
|
||||
color: Colors.black54,
|
||||
),
|
||||
'Total Outstanding: ₹${totalOutstanding.toStringAsFixed(0)}',
|
||||
color: Colors.black54),
|
||||
const SizedBox(height: 10),
|
||||
_AgingStackedBar(
|
||||
buckets: buckets,
|
||||
totalOutstanding: totalOutstanding,
|
||||
),
|
||||
_AgingStackedBar(buckets: buckets, totalOutstanding: totalOutstanding),
|
||||
const SizedBox(height: 15),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
@ -284,8 +257,7 @@ class CollectionsHealthWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildAgingLegendItem(
|
||||
String title, double amount, Color color, int count // Updated parameter
|
||||
) {
|
||||
String title, double amount, Color color, int count) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -295,15 +267,11 @@ class CollectionsHealthWidget extends StatelessWidget {
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 6),
|
||||
MyText.bodySmall(
|
||||
'$title: ₹${amount.toStringAsFixed(0)} (${count} Invoices)' // Updated text
|
||||
),
|
||||
'$title: ₹${amount.toStringAsFixed(0)} ($count Invoices)'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// COMMON BOX DECORATION
|
||||
// ===============================================================
|
||||
BoxDecoration _boxDecoration() {
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
|
||||
@ -1,122 +1,63 @@
|
||||
// lib/widgets/purchase_invoice_dashboard.dart
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
|
||||
/// =======================
|
||||
/// INTERNAL DUMMY DATA
|
||||
/// =======================
|
||||
|
||||
const String _purchaseInvoiceDummyData = '''
|
||||
{
|
||||
"success": true,
|
||||
"message": "Purchase invoice list fetched successfully.",
|
||||
"data": {
|
||||
"currentPage": 1,
|
||||
"pageSize": 20,
|
||||
"totalPages": 1,
|
||||
"totalCount": 6,
|
||||
"hasPrevious": false,
|
||||
"hasNext": false,
|
||||
"data": [
|
||||
{
|
||||
"id": "0950d08d-a98e-43e5-a414-3de1071d0615",
|
||||
"title": "Purchase of Booster Pump",
|
||||
"proformaInvoiceAmount": 100000.0,
|
||||
"project": { "name": "Kohinoor Sportsville" },
|
||||
"supplier": { "name": "Fire Safety Solutions" },
|
||||
"status": { "displayName": "Draft" },
|
||||
"totalAmount": 0.0
|
||||
},
|
||||
{
|
||||
"id": "c8828c81-d943-4f94-8c4f-f5e985ef5eb0",
|
||||
"title": "Fire Panel Purchase",
|
||||
"proformaInvoiceAmount": 50000.0,
|
||||
"project": { "name": "Kohinoor Sportsville" },
|
||||
"supplier": { "name": "Fire Safety Solutions" },
|
||||
"status": { "displayName": "Draft" },
|
||||
"totalAmount": 0.0
|
||||
},
|
||||
{
|
||||
"id": "8bbe811f-b0fd-4b36-8c0c-3df4dd0f8aee",
|
||||
"title": "Fire Pump Purchase",
|
||||
"proformaInvoiceAmount": 30000.0,
|
||||
"project": { "name": "Kohinoor Sportsville" },
|
||||
"supplier": { "name": "Fire Safety Solutions" },
|
||||
"status": { "displayName": "Draft" },
|
||||
"totalAmount": 0.0
|
||||
},
|
||||
{
|
||||
"id": "b485a0e8-1955-44a4-8062-e2004b4d4ada",
|
||||
"title": "Pipe purchase at Lodha",
|
||||
"proformaInvoiceAmount": 20000.0,
|
||||
"project": { "name": "LODHA GIARDINO KHARADI FAPA" },
|
||||
"supplier": { "name": "Elite Tech Services" },
|
||||
"status": { "displayName": "Draft" },
|
||||
"totalAmount": 20000.0
|
||||
},
|
||||
{
|
||||
"id": "67406cdc-2b16-4290-a333-adc4fe7c0f70",
|
||||
"title": "Pipe purchase For ",
|
||||
"proformaInvoiceAmount": 10000.0,
|
||||
"project": { "name": "Raja Bahadur International Ltd." },
|
||||
"supplier": { "name": "Elite Tech Services" },
|
||||
"status": { "displayName": "Draft" },
|
||||
"totalAmount": 9800.0
|
||||
},
|
||||
{
|
||||
"id": "4b8b60cb-65ac-4eff-8d35-dc3fa2ac37aa",
|
||||
"title": "Cement material supply for Basement work",
|
||||
"proformaInvoiceAmount": 10000.0,
|
||||
"project": { "name": "ANP Ultimas Wakad" },
|
||||
"supplier": { "name": "Skyline Fire Safety Pvt Ltd" },
|
||||
"status": { "displayName": "Draft" },
|
||||
"totalAmount": 24000.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": null,
|
||||
"statusCode": 200,
|
||||
"timestamp": "2025-12-04T09:50:43.6663252Z"
|
||||
}
|
||||
''';
|
||||
|
||||
/// =======================
|
||||
/// REDESIGNED PUBLIC DASHBOARD WIDGET
|
||||
/// =======================
|
||||
/// Parent does NOT need to pass any data.
|
||||
/// Optionally, parent can pass a JSON string if desired later.
|
||||
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
|
||||
|
||||
class CompactPurchaseInvoiceDashboard extends StatelessWidget {
|
||||
final String? jsonString;
|
||||
|
||||
const CompactPurchaseInvoiceDashboard({
|
||||
super.key,
|
||||
this.jsonString, // if null, internal dummy JSON is used
|
||||
});
|
||||
const CompactPurchaseInvoiceDashboard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PurchaseInvoiceDashboardData data =
|
||||
_parsePurchaseInvoiceDashboardData(
|
||||
jsonString ?? _purchaseInvoiceDummyData);
|
||||
final DashboardController controller = Get.find();
|
||||
|
||||
final metrics = data.metrics;
|
||||
// Use Obx to reactively listen to data changes
|
||||
return Obx(() {
|
||||
final data = controller.purchaseInvoiceOverviewData.value;
|
||||
|
||||
const double mainPadding = 16.0;
|
||||
// Show loading state while API call is in progress
|
||||
if (controller.isPurchaseInvoiceLoading.value) {
|
||||
return SkeletonLoaders.purchaseInvoiceDashboardSkeleton();
|
||||
}
|
||||
|
||||
// Show empty state if no data
|
||||
if (data == null || data.totalInvoices == 0) {
|
||||
return Center(
|
||||
child: MyText.bodySmall('No purchase invoices found.'),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert API response to internal PurchaseInvoiceData list
|
||||
final invoices = (data.projectBreakdown ?? [])
|
||||
.map((project) => PurchaseInvoiceData(
|
||||
id: project.id ?? '',
|
||||
title: project.name ?? 'Unknown',
|
||||
proformaInvoiceAmount: project.totalValue ?? 0.0,
|
||||
supplierName: data.topSupplier?.name ?? 'N/A',
|
||||
projectName: project.name ?? 'Unknown',
|
||||
statusName: 'Unknown', // API might have status if needed
|
||||
))
|
||||
.toList();
|
||||
|
||||
final metrics = PurchaseInvoiceMetricsCalculator().calculate(invoices);
|
||||
|
||||
return _buildDashboard(metrics);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildDashboard(PurchaseInvoiceMetrics metrics) {
|
||||
const double spacing = 16.0;
|
||||
const double smallSpacing = 8.0;
|
||||
|
||||
// Outer Container for a polished card effect
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(mainPadding),
|
||||
padding: const EdgeInsets.all(spacing),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(5), // Slightly rounder corners
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08), // Softer, more subtle shadow
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
@ -128,40 +69,29 @@ class CompactPurchaseInvoiceDashboard extends StatelessWidget {
|
||||
children: [
|
||||
const _DashboardHeader(),
|
||||
const SizedBox(height: spacing),
|
||||
|
||||
// 1. Total Value Card (Dominant Metric)
|
||||
_TotalValueCard(
|
||||
totalProformaAmount: metrics.totalProformaAmount,
|
||||
totalCount: metrics.totalCount,
|
||||
),
|
||||
|
||||
const SizedBox(height: spacing),
|
||||
|
||||
// 2. Key Metrics Row (Condensed, Broker-style display)
|
||||
_CondensedMetricsRow(
|
||||
draftCount: metrics.draftCount,
|
||||
avgInvoiceValue: metrics.avgInvoiceValue,
|
||||
topSupplierName: metrics.topSupplierName,
|
||||
spacing: smallSpacing,
|
||||
),
|
||||
|
||||
const SizedBox(height: spacing),
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
const SizedBox(height: spacing),
|
||||
|
||||
// 3. Status Breakdown (Donut Chart Style)
|
||||
const _SectionTitle('Status Breakdown by Value'),
|
||||
const SizedBox(height: smallSpacing),
|
||||
_StatusDonutChart(
|
||||
statusBuckets: metrics.statusBuckets,
|
||||
totalAmount: metrics.totalProformaAmount,
|
||||
),
|
||||
|
||||
const SizedBox(height: spacing),
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
const SizedBox(height: spacing),
|
||||
|
||||
// 4. Top Projects Breakdown (Top 3)
|
||||
const _SectionTitle('Top Projects by Proforma Value'),
|
||||
const SizedBox(height: smallSpacing),
|
||||
_ProjectBreakdown(
|
||||
@ -175,32 +105,6 @@ class CompactPurchaseInvoiceDashboard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// =======================
|
||||
/// INTERNAL PARSING
|
||||
/// =======================
|
||||
|
||||
PurchaseInvoiceDashboardData _parsePurchaseInvoiceDashboardData(
|
||||
String jsonStr,
|
||||
) {
|
||||
final Map<String, dynamic> root =
|
||||
json.decode(jsonStr) as Map<String, dynamic>;
|
||||
final List<dynamic> rawInvoices =
|
||||
(root['data']?['data'] as List<dynamic>?) ?? const [];
|
||||
|
||||
final List<PurchaseInvoiceData> invoices = rawInvoices
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(PurchaseInvoiceData.fromJson)
|
||||
.toList();
|
||||
|
||||
final PurchaseInvoiceMetrics metrics =
|
||||
PurchaseInvoiceMetricsCalculator().calculate(invoices);
|
||||
|
||||
return PurchaseInvoiceDashboardData(
|
||||
invoices: invoices,
|
||||
metrics: metrics,
|
||||
);
|
||||
}
|
||||
|
||||
/// Container object used internally
|
||||
class PurchaseInvoiceDashboardData {
|
||||
final List<PurchaseInvoiceData> invoices;
|
||||
|
||||
@ -1531,6 +1531,424 @@ class SkeletonLoaders {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// NEW SKELETON LOADER METHODS
|
||||
// ====================================================================
|
||||
|
||||
/// Skeleton for the CollectionsHealthWidget
|
||||
static Widget collectionHealthSkeleton() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ShimmerEffect(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Header Skeleton
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 14, width: 180, color: Colors.grey.shade300),
|
||||
MySpacing.height(4),
|
||||
Container(
|
||||
height: 10, width: 140, color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Main Content Row (Left Chart + Right Metrics)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Left Chart Section
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Gauge Chart Placeholder
|
||||
Container(
|
||||
width: 120,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(60), bottom: Radius.zero),
|
||||
),
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: 12, width: 60, color: Colors.grey.shade400),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Summary Text Placeholders
|
||||
Container(
|
||||
height: 16, width: 150, color: Colors.grey.shade300),
|
||||
MySpacing.height(6),
|
||||
Container(
|
||||
height: 12, width: 200, color: Colors.grey.shade300),
|
||||
MySpacing.height(4),
|
||||
Container(
|
||||
height: 12, width: 100, color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Right Metrics Section
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Column(
|
||||
children: [
|
||||
// Metric Card 1
|
||||
_buildMetricCardSkeleton(),
|
||||
const SizedBox(height: 10),
|
||||
// Metric Card 2
|
||||
_buildMetricCardSkeleton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Aging Analysis Section
|
||||
Container(height: 14, width: 220, color: Colors.grey.shade300),
|
||||
MySpacing.height(4),
|
||||
Container(height: 10, width: 180, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Aging Stacked Bar Placeholder
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
4,
|
||||
(index) => Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
height: 16, color: Colors.grey.shade400),
|
||||
)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Aging Legend Placeholders
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: List.generate(
|
||||
4,
|
||||
(index) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
shape: BoxShape.circle)),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 120,
|
||||
color: Colors.grey.shade300),
|
||||
],
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper for Metric Card Skeleton
|
||||
static Widget _buildMetricCardSkeleton() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200, // Background color for the card
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(height: 10, width: 90, color: Colors.grey.shade300),
|
||||
MySpacing.height(4),
|
||||
Container(height: 12, width: 100, color: Colors.grey.shade300),
|
||||
MySpacing.height(4),
|
||||
Container(height: 14, width: 80, color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Skeleton for the CompactPurchaseInvoiceDashboard
|
||||
static Widget purchaseInvoiceDashboardSkeleton() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ShimmerEffect(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header Skeleton
|
||||
Row(mainAxisAlignment: MainAxisAlignment.start, children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 14, width: 200, color: Colors.grey.shade300),
|
||||
MySpacing.height(4),
|
||||
Container(
|
||||
height: 10, width: 150, color: Colors.grey.shade300),
|
||||
]))
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Total Value Card Skeleton
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200, // Simulated light blue background
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: 12, width: 160, color: Colors.grey.shade300),
|
||||
Icon(Icons.account_balance_wallet_outlined,
|
||||
color: Colors.grey.shade300, size: 20),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
Container(
|
||||
height: 16, width: 120, color: Colors.grey.shade300),
|
||||
MySpacing.height(4),
|
||||
Container(
|
||||
height: 12, width: 180, color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Condensed Metrics Row Skeleton
|
||||
Row(
|
||||
children: List.generate(
|
||||
3,
|
||||
(index) => Expanded(
|
||||
child: Padding(
|
||||
padding: index == 1
|
||||
? MySpacing.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200, // Card background
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.circle,
|
||||
color: Colors.grey.shade300, size: 16),
|
||||
MySpacing.width(4),
|
||||
Container(
|
||||
height: 10,
|
||||
width: 50,
|
||||
color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
MySpacing.height(6),
|
||||
Container(
|
||||
height: 14,
|
||||
width: 80,
|
||||
color: Colors.grey.shade300),
|
||||
Container(
|
||||
height: 10,
|
||||
width: 60,
|
||||
color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
color: Colors.transparent), // Hidden divider for spacing
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Status Breakdown Section Skeleton (Chart + Legend)
|
||||
Container(height: 12, width: 180, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Donut Chart Placeholder
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.grey.shade300, width: 6),
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 10, width: 60, color: Colors.grey.shade300),
|
||||
MySpacing.height(4),
|
||||
Container(
|
||||
height: 14, width: 40, color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Legend/Details Placeholder
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(
|
||||
3,
|
||||
(index) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin:
|
||||
const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
shape: BoxShape.circle)),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 80,
|
||||
color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: 14,
|
||||
width: 50,
|
||||
color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
color: Colors.transparent), // Hidden divider for spacing
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Top Projects Section Skeleton
|
||||
Container(height: 12, width: 200, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 8),
|
||||
Column(
|
||||
children: List.generate(
|
||||
3,
|
||||
(index) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
shape: BoxShape.circle)),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 12,
|
||||
width: double.infinity,
|
||||
color: Colors.grey.shade300),
|
||||
MySpacing.height(4),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Container(
|
||||
height: 4, color: Colors.grey.shade300),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
height: 14,
|
||||
width: 70,
|
||||
color: Colors.grey.shade300),
|
||||
MySpacing.height(2),
|
||||
Container(
|
||||
height: 10,
|
||||
width: 40,
|
||||
color: Colors.grey.shade300),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom reusable Shimmer Effect widget.
|
||||
|
||||
221
lib/model/dashboard/purchase_invoice_model.dart
Normal file
221
lib/model/dashboard/purchase_invoice_model.dart
Normal file
@ -0,0 +1,221 @@
|
||||
// ============================
|
||||
// PurchaseInvoiceOverviewModel.dart
|
||||
// ============================
|
||||
|
||||
class PurchaseInvoiceOverviewResponse {
|
||||
final bool? success;
|
||||
final String? message;
|
||||
final PurchaseInvoiceOverviewData? data;
|
||||
final dynamic errors;
|
||||
final int? statusCode;
|
||||
final DateTime? timestamp;
|
||||
|
||||
PurchaseInvoiceOverviewResponse({
|
||||
this.success,
|
||||
this.message,
|
||||
this.data,
|
||||
this.errors,
|
||||
this.statusCode,
|
||||
this.timestamp,
|
||||
});
|
||||
|
||||
factory PurchaseInvoiceOverviewResponse.fromJson(Map<String, dynamic> json) {
|
||||
return PurchaseInvoiceOverviewResponse(
|
||||
success: json['success'] as bool?,
|
||||
message: json['message'] as String?,
|
||||
data: json['data'] != null
|
||||
? PurchaseInvoiceOverviewData.fromJson(json['data'])
|
||||
: null,
|
||||
errors: json['errors'],
|
||||
statusCode: json['statusCode'] as int?,
|
||||
timestamp: json['timestamp'] != null
|
||||
? DateTime.tryParse(json['timestamp'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'success': success,
|
||||
'message': message,
|
||||
'data': data?.toJson(),
|
||||
'errors': errors,
|
||||
'statusCode': statusCode,
|
||||
'timestamp': timestamp?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PurchaseInvoiceOverviewData {
|
||||
final int? totalInvoices;
|
||||
final double? totalValue;
|
||||
final double? averageValue;
|
||||
final List<StatusBreakdown>? statusBreakdown;
|
||||
final List<ProjectBreakdown>? projectBreakdown;
|
||||
final TopSupplier? topSupplier;
|
||||
|
||||
PurchaseInvoiceOverviewData({
|
||||
this.totalInvoices,
|
||||
this.totalValue,
|
||||
this.averageValue,
|
||||
this.statusBreakdown,
|
||||
this.projectBreakdown,
|
||||
this.topSupplier,
|
||||
});
|
||||
|
||||
factory PurchaseInvoiceOverviewData.fromJson(Map<String, dynamic> json) {
|
||||
return PurchaseInvoiceOverviewData(
|
||||
totalInvoices: json['totalInvoices'] as int?,
|
||||
totalValue: (json['totalValue'] != null)
|
||||
? (json['totalValue'] as num).toDouble()
|
||||
: null,
|
||||
averageValue: (json['averageValue'] != null)
|
||||
? (json['averageValue'] as num).toDouble()
|
||||
: null,
|
||||
statusBreakdown: json['statusBreakdown'] != null
|
||||
? (json['statusBreakdown'] as List)
|
||||
.map((e) => StatusBreakdown.fromJson(e))
|
||||
.toList()
|
||||
: null,
|
||||
projectBreakdown: json['projectBreakdown'] != null
|
||||
? (json['projectBreakdown'] as List)
|
||||
.map((e) => ProjectBreakdown.fromJson(e))
|
||||
.toList()
|
||||
: null,
|
||||
topSupplier: json['topSupplier'] != null
|
||||
? TopSupplier.fromJson(json['topSupplier'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'totalInvoices': totalInvoices,
|
||||
'totalValue': totalValue,
|
||||
'averageValue': averageValue,
|
||||
'statusBreakdown': statusBreakdown?.map((e) => e.toJson()).toList(),
|
||||
'projectBreakdown': projectBreakdown?.map((e) => e.toJson()).toList(),
|
||||
'topSupplier': topSupplier?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class StatusBreakdown {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final int? count;
|
||||
final double? totalValue;
|
||||
final double? percentage;
|
||||
|
||||
StatusBreakdown({
|
||||
this.id,
|
||||
this.name,
|
||||
this.count,
|
||||
this.totalValue,
|
||||
this.percentage,
|
||||
});
|
||||
|
||||
factory StatusBreakdown.fromJson(Map<String, dynamic> json) {
|
||||
return StatusBreakdown(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
count: json['count'] as int?,
|
||||
totalValue: (json['totalValue'] != null)
|
||||
? (json['totalValue'] as num).toDouble()
|
||||
: null,
|
||||
percentage: (json['percentage'] != null)
|
||||
? (json['percentage'] as num).toDouble()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'count': count,
|
||||
'totalValue': totalValue,
|
||||
'percentage': percentage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectBreakdown {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final int? count;
|
||||
final double? totalValue;
|
||||
final double? percentage;
|
||||
|
||||
ProjectBreakdown({
|
||||
this.id,
|
||||
this.name,
|
||||
this.count,
|
||||
this.totalValue,
|
||||
this.percentage,
|
||||
});
|
||||
|
||||
factory ProjectBreakdown.fromJson(Map<String, dynamic> json) {
|
||||
return ProjectBreakdown(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
count: json['count'] as int?,
|
||||
totalValue: (json['totalValue'] != null)
|
||||
? (json['totalValue'] as num).toDouble()
|
||||
: null,
|
||||
percentage: (json['percentage'] != null)
|
||||
? (json['percentage'] as num).toDouble()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'count': count,
|
||||
'totalValue': totalValue,
|
||||
'percentage': percentage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TopSupplier {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final int? count;
|
||||
final double? totalValue;
|
||||
final double? percentage;
|
||||
|
||||
TopSupplier({
|
||||
this.id,
|
||||
this.name,
|
||||
this.count,
|
||||
this.totalValue,
|
||||
this.percentage,
|
||||
});
|
||||
|
||||
factory TopSupplier.fromJson(Map<String, dynamic> json) {
|
||||
return TopSupplier(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
count: json['count'] as int?,
|
||||
totalValue: (json['totalValue'] != null)
|
||||
? (json['totalValue'] as num).toDouble()
|
||||
: null,
|
||||
percentage: (json['percentage'] != null)
|
||||
? (json['percentage'] as num).toDouble()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'count': count,
|
||||
'totalValue': totalValue,
|
||||
'percentage': percentage,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user