diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index 3681b4c..850914c 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -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 = {}.obs; // Collection - final isCollectionOverviewLoading = false.obs; + final isCollectionOverviewLoading = true.obs; final collectionOverviewData = Rx(null); - +// ========================= +// Purchase Invoice Overview +// ========================= + final isPurchaseInvoiceLoading = true.obs; + final purchaseInvoiceOverviewData = Rx(null); // Constants final List 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 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 fetchPendingExpenses() async { final id = projectController.selectedProjectId.value; if (id.isEmpty) return; diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index ce82b18..4d36d63 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -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"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index ad7c3c3..6f60a02 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -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 getPurchaseInvoiceOverview({ + String? projectId, + }) async { + try { + final queryParams = {}; + 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) /// ============================================ diff --git a/lib/helpers/widgets/dashbaord/collection_dashboard_card.dart b/lib/helpers/widgets/dashbaord/collection_dashboard_card.dart index 3ec43bd..d52bb45 100644 --- a/lib/helpers/widgets/dashbaord/collection_dashboard_card.dart +++ b/lib/helpers/widgets/dashbaord/collection_dashboard_card.dart @@ -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( - builder: (controller) { - final data = controller.collectionOverviewData.value; - final isLoading = controller.isCollectionOverviewLoading.value; + final DashboardController controller = Get.find(); - 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: [ - _buildHeader(), - const SizedBox(height: 20), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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: [ + _buildHeader(), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: [ - 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, diff --git a/lib/helpers/widgets/dashbaord/purchase_invoice_dashboard.dart b/lib/helpers/widgets/dashbaord/purchase_invoice_dashboard.dart index 218a5d0..b5f3fe8 100644 --- a/lib/helpers/widgets/dashbaord/purchase_invoice_dashboard.dart +++ b/lib/helpers/widgets/dashbaord/purchase_invoice_dashboard.dart @@ -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 root = - json.decode(jsonStr) as Map; - final List rawInvoices = - (root['data']?['data'] as List?) ?? const []; - - final List invoices = rawInvoices - .whereType>() - .map(PurchaseInvoiceData.fromJson) - .toList(); - - final PurchaseInvoiceMetrics metrics = - PurchaseInvoiceMetricsCalculator().calculate(invoices); - - return PurchaseInvoiceDashboardData( - invoices: invoices, - metrics: metrics, - ); -} - /// Container object used internally class PurchaseInvoiceDashboardData { final List invoices; diff --git a/lib/helpers/widgets/my_custom_skeleton.dart b/lib/helpers/widgets/my_custom_skeleton.dart index 13c858c..3da325e 100644 --- a/lib/helpers/widgets/my_custom_skeleton.dart +++ b/lib/helpers/widgets/my_custom_skeleton.dart @@ -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: [ + // 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: [ + // 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: [ + 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. diff --git a/lib/model/dashboard/purchase_invoice_model.dart b/lib/model/dashboard/purchase_invoice_model.dart new file mode 100644 index 0000000..220ec4c --- /dev/null +++ b/lib/model/dashboard/purchase_invoice_model.dart @@ -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 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 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; + final List? projectBreakdown; + final TopSupplier? topSupplier; + + PurchaseInvoiceOverviewData({ + this.totalInvoices, + this.totalValue, + this.averageValue, + this.statusBreakdown, + this.projectBreakdown, + this.topSupplier, + }); + + factory PurchaseInvoiceOverviewData.fromJson(Map 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 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 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 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 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 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 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 toJson() { + return { + 'id': id, + 'name': name, + 'count': count, + 'totalValue': totalValue, + 'percentage': percentage, + }; + } +}