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/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';
|
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 {
|
class DashboardController extends GetxController {
|
||||||
// Dependencies
|
// Dependencies
|
||||||
@ -68,9 +69,13 @@ class DashboardController extends GetxController {
|
|||||||
final uploadingStates = <String, RxBool>{}.obs;
|
final uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
// Collection
|
// Collection
|
||||||
final isCollectionOverviewLoading = false.obs;
|
final isCollectionOverviewLoading = true.obs;
|
||||||
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
||||||
|
// =========================
|
||||||
|
// Purchase Invoice Overview
|
||||||
|
// =========================
|
||||||
|
final isPurchaseInvoiceLoading = true.obs;
|
||||||
|
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
||||||
// Constants
|
// Constants
|
||||||
final List<String> ranges = ['7D', '15D', '30D'];
|
final List<String> ranges = ['7D', '15D', '30D'];
|
||||||
static const _rangeDaysMap = {
|
static const _rangeDaysMap = {
|
||||||
@ -211,6 +216,7 @@ class DashboardController extends GetxController {
|
|||||||
fetchMonthlyExpenses(),
|
fetchMonthlyExpenses(),
|
||||||
fetchMasterData(),
|
fetchMasterData(),
|
||||||
fetchCollectionOverview(),
|
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 {
|
Future<void> fetchPendingExpenses() async {
|
||||||
final id = projectController.selectedProjectId.value;
|
final id = projectController.selectedProjectId.value;
|
||||||
if (id.isEmpty) return;
|
if (id.isEmpty) return;
|
||||||
|
|||||||
@ -38,6 +38,9 @@ class ApiEndpoints {
|
|||||||
static const String getPendingExpenses = "/Dashboard/expense/pendings";
|
static const String getPendingExpenses = "/Dashboard/expense/pendings";
|
||||||
static const String getCollectionOverview = "/dashboard/collection-overview";
|
static const String getCollectionOverview = "/dashboard/collection-overview";
|
||||||
|
|
||||||
|
static const String getPurchaseInvoiceOverview =
|
||||||
|
"/dashboard/purchase-invoice-overview";
|
||||||
|
|
||||||
///// Projects Module API Endpoints
|
///// Projects Module API Endpoints
|
||||||
static const String createProject = "/project";
|
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_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';
|
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
|
||||||
|
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
||||||
|
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
@ -317,6 +318,45 @@ class ApiService {
|
|||||||
return null;
|
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)
|
/// GET COLLECTION OVERVIEW (Dashboard)
|
||||||
/// ============================================
|
/// ============================================
|
||||||
|
|||||||
@ -3,39 +3,38 @@ 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/controller/dashboard/dashboard_controller.dart';
|
||||||
import 'package:on_field_work/model/dashboard/collection_overview_model.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 {
|
class CollectionsHealthWidget extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<DashboardController>(
|
final DashboardController controller = Get.find<DashboardController>();
|
||||||
builder: (controller) {
|
|
||||||
|
return Obx(() {
|
||||||
final data = controller.collectionOverviewData.value;
|
final data = controller.collectionOverviewData.value;
|
||||||
final isLoading = controller.isCollectionOverviewLoading.value;
|
final isLoading = controller.isCollectionOverviewLoading.value;
|
||||||
|
|
||||||
|
// Loading state
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return const Center(
|
return Container(
|
||||||
child: Padding(
|
decoration: _boxDecoration(), // Maintain the outer box decoration
|
||||||
padding: EdgeInsets.all(32.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: CircularProgressIndicator(),
|
child: SkeletonLoaders.collectionHealthSkeleton(),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No data
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: _boxDecoration(),
|
decoration: _boxDecoration(),
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: MyText.bodyMedium(
|
child: MyText.bodyMedium('No collection overview data available.'),
|
||||||
'No collection overview data available.',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data available
|
||||||
final double totalDue = data.totalDueAmount;
|
final double totalDue = data.totalDueAmount;
|
||||||
final double totalCollected = data.totalCollectedAmount;
|
final double totalCollected = data.totalCollectedAmount;
|
||||||
final double pendingPercentage = data.pendingPercentage / 100.0;
|
final double pendingPercentage = data.pendingPercentage / 100.0;
|
||||||
@ -75,13 +74,12 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
// ==============================
|
||||||
// HEADER
|
// HEADER
|
||||||
// ===============================================================
|
// ==============================
|
||||||
Widget _buildHeader() {
|
Widget _buildHeader() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@ -100,9 +98,9 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
// ==============================
|
||||||
// LEFT SECTION (GAUGE + SUMMARY + TREND PLACEHOLDERS)
|
// LEFT SECTION (GAUGE + SUMMARY)
|
||||||
// ===============================================================
|
// ==============================
|
||||||
Widget _buildLeftChartSection({
|
Widget _buildLeftChartSection({
|
||||||
required double totalDue,
|
required double totalDue,
|
||||||
required double pendingPercentage,
|
required double pendingPercentage,
|
||||||
@ -115,13 +113,15 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(children: [
|
Row(
|
||||||
|
children: [
|
||||||
_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),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@ -151,15 +151,13 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
// ==============================
|
||||||
// RIGHT SIDE METRICS
|
// RIGHT METRICS SECTION
|
||||||
// ===============================================================
|
// ==============================
|
||||||
Widget _buildRightMetricsSection({
|
Widget _buildRightMetricsSection({
|
||||||
required CollectionOverviewData data,
|
required CollectionOverviewData data,
|
||||||
required double dsoDays,
|
required double dsoDays,
|
||||||
}) {
|
}) {
|
||||||
final double totalCollected = data.totalCollectedAmount;
|
|
||||||
|
|
||||||
final String topClientName = data.topClient?.name ?? 'N/A';
|
final String topClientName = data.topClient?.name ?? 'N/A';
|
||||||
final double topClientBalance = data.topClientBalance;
|
final double topClientBalance = data.topClientBalance;
|
||||||
|
|
||||||
@ -175,7 +173,7 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildMetricCard(
|
_buildMetricCard(
|
||||||
title: 'Total Collected (YTD)',
|
title: 'Total Collected (YTD)',
|
||||||
value: '₹${totalCollected.toStringAsFixed(0)}',
|
value: '₹${data.totalCollectedAmount.toStringAsFixed(0)}',
|
||||||
subValue: 'Collected',
|
subValue: 'Collected',
|
||||||
valueColor: Colors.green,
|
valueColor: Colors.green,
|
||||||
isDetailed: false,
|
isDetailed: false,
|
||||||
@ -184,9 +182,6 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
|
||||||
// METRIC CARD UI
|
|
||||||
// ===============================================================
|
|
||||||
Widget _buildMetricCard({
|
Widget _buildMetricCard({
|
||||||
required String title,
|
required String title,
|
||||||
required String value,
|
required String value,
|
||||||
@ -221,35 +216,19 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
// ==============================
|
||||||
// AGING ANALYSIS (DYNAMIC)
|
// AGING ANALYSIS
|
||||||
// ===============================================================
|
// ==============================
|
||||||
Widget _buildAgingAnalysis({required CollectionOverviewData data}) {
|
Widget _buildAgingAnalysis({required CollectionOverviewData data}) {
|
||||||
final buckets = [
|
final buckets = [
|
||||||
AgingBucketData(
|
AgingBucketData('0-30 Days', data.bucket0To30Amount, Colors.green,
|
||||||
'0-30 Days',
|
data.bucket0To30Invoices),
|
||||||
data.bucket0To30Amount,
|
AgingBucketData('30-60 Days', data.bucket30To60Amount, Colors.orange,
|
||||||
Colors.green,
|
data.bucket30To60Invoices),
|
||||||
data.bucket0To30Invoices,
|
AgingBucketData('60-90 Days', data.bucket60To90Amount,
|
||||||
),
|
Colors.red.shade300, data.bucket60To90Invoices),
|
||||||
AgingBucketData(
|
AgingBucketData('> 90 Days', data.bucket90PlusAmount, Colors.red,
|
||||||
'30-60 Days',
|
data.bucket90PlusInvoices),
|
||||||
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);
|
final double totalOutstanding = buckets.fold(0, (sum, b) => sum + b.amount);
|
||||||
@ -257,19 +236,13 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.bodyMedium(
|
MyText.bodyMedium('Outstanding Collections Aging Analysis',
|
||||||
'Outstanding Collections Aging Analysis',
|
fontWeight: 700),
|
||||||
fontWeight: 700,
|
|
||||||
),
|
|
||||||
MyText.bodySmall(
|
MyText.bodySmall(
|
||||||
'Total Outstanding: ₹${totalOutstanding.toStringAsFixed(0)}',
|
'Total Outstanding: ₹${totalOutstanding.toStringAsFixed(0)}',
|
||||||
color: Colors.black54,
|
color: Colors.black54),
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_AgingStackedBar(
|
_AgingStackedBar(buckets: buckets, totalOutstanding: totalOutstanding),
|
||||||
buckets: buckets,
|
|
||||||
totalOutstanding: totalOutstanding,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
@ -284,8 +257,7 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAgingLegendItem(
|
Widget _buildAgingLegendItem(
|
||||||
String title, double amount, Color color, int count // Updated parameter
|
String title, double amount, Color color, int count) {
|
||||||
) {
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -295,15 +267,11 @@ class CollectionsHealthWidget extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
MyText.bodySmall(
|
MyText.bodySmall(
|
||||||
'$title: ₹${amount.toStringAsFixed(0)} (${count} Invoices)' // Updated text
|
'$title: ₹${amount.toStringAsFixed(0)} ($count Invoices)'),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
|
||||||
// COMMON BOX DECORATION
|
|
||||||
// ===============================================================
|
|
||||||
BoxDecoration _boxDecoration() {
|
BoxDecoration _boxDecoration() {
|
||||||
return BoxDecoration(
|
return BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|||||||
@ -1,122 +1,63 @@
|
|||||||
// lib/widgets/purchase_invoice_dashboard.dart
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/material.dart';
|
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_text.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||||
|
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.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.
|
|
||||||
|
|
||||||
class CompactPurchaseInvoiceDashboard extends StatelessWidget {
|
class CompactPurchaseInvoiceDashboard extends StatelessWidget {
|
||||||
final String? jsonString;
|
const CompactPurchaseInvoiceDashboard({super.key});
|
||||||
|
|
||||||
const CompactPurchaseInvoiceDashboard({
|
|
||||||
super.key,
|
|
||||||
this.jsonString, // if null, internal dummy JSON is used
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final PurchaseInvoiceDashboardData data =
|
final DashboardController controller = Get.find();
|
||||||
_parsePurchaseInvoiceDashboardData(
|
|
||||||
jsonString ?? _purchaseInvoiceDummyData);
|
|
||||||
|
|
||||||
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 spacing = 16.0;
|
||||||
const double smallSpacing = 8.0;
|
const double smallSpacing = 8.0;
|
||||||
|
|
||||||
// Outer Container for a polished card effect
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(mainPadding),
|
padding: const EdgeInsets.all(spacing),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(5), // Slightly rounder corners
|
borderRadius: BorderRadius.circular(5),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.08), // Softer, more subtle shadow
|
color: Colors.black.withOpacity(0.08),
|
||||||
blurRadius: 15,
|
blurRadius: 15,
|
||||||
offset: const Offset(0, 5),
|
offset: const Offset(0, 5),
|
||||||
),
|
),
|
||||||
@ -128,40 +69,29 @@ class CompactPurchaseInvoiceDashboard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const _DashboardHeader(),
|
const _DashboardHeader(),
|
||||||
const SizedBox(height: spacing),
|
const SizedBox(height: spacing),
|
||||||
|
|
||||||
// 1. Total Value Card (Dominant Metric)
|
|
||||||
_TotalValueCard(
|
_TotalValueCard(
|
||||||
totalProformaAmount: metrics.totalProformaAmount,
|
totalProformaAmount: metrics.totalProformaAmount,
|
||||||
totalCount: metrics.totalCount,
|
totalCount: metrics.totalCount,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: spacing),
|
const SizedBox(height: spacing),
|
||||||
|
|
||||||
// 2. Key Metrics Row (Condensed, Broker-style display)
|
|
||||||
_CondensedMetricsRow(
|
_CondensedMetricsRow(
|
||||||
draftCount: metrics.draftCount,
|
draftCount: metrics.draftCount,
|
||||||
avgInvoiceValue: metrics.avgInvoiceValue,
|
avgInvoiceValue: metrics.avgInvoiceValue,
|
||||||
topSupplierName: metrics.topSupplierName,
|
topSupplierName: metrics.topSupplierName,
|
||||||
spacing: smallSpacing,
|
spacing: smallSpacing,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: spacing),
|
const SizedBox(height: spacing),
|
||||||
const Divider(height: 1, thickness: 0.5),
|
const Divider(height: 1, thickness: 0.5),
|
||||||
const SizedBox(height: spacing),
|
const SizedBox(height: spacing),
|
||||||
|
|
||||||
// 3. Status Breakdown (Donut Chart Style)
|
|
||||||
const _SectionTitle('Status Breakdown by Value'),
|
const _SectionTitle('Status Breakdown by Value'),
|
||||||
const SizedBox(height: smallSpacing),
|
const SizedBox(height: smallSpacing),
|
||||||
_StatusDonutChart(
|
_StatusDonutChart(
|
||||||
statusBuckets: metrics.statusBuckets,
|
statusBuckets: metrics.statusBuckets,
|
||||||
totalAmount: metrics.totalProformaAmount,
|
totalAmount: metrics.totalProformaAmount,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: spacing),
|
const SizedBox(height: spacing),
|
||||||
const Divider(height: 1, thickness: 0.5),
|
const Divider(height: 1, thickness: 0.5),
|
||||||
const SizedBox(height: spacing),
|
const SizedBox(height: spacing),
|
||||||
|
|
||||||
// 4. Top Projects Breakdown (Top 3)
|
|
||||||
const _SectionTitle('Top Projects by Proforma Value'),
|
const _SectionTitle('Top Projects by Proforma Value'),
|
||||||
const SizedBox(height: smallSpacing),
|
const SizedBox(height: smallSpacing),
|
||||||
_ProjectBreakdown(
|
_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
|
/// Container object used internally
|
||||||
class PurchaseInvoiceDashboardData {
|
class PurchaseInvoiceDashboardData {
|
||||||
final List<PurchaseInvoiceData> invoices;
|
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.
|
/// 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