import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/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'; class CollectionsHealthWidget extends StatelessWidget { @override Widget build(BuildContext context) { final DashboardController controller = Get.find(); return Obx(() { final data = controller.collectionOverviewData.value; final isLoading = controller.isCollectionOverviewLoading.value; // 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: 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: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium('Collections Health Overview', fontWeight: 700), const SizedBox(height: 2), MyText.bodySmall('View your collection health data.', color: Colors.grey), ], ), ), ], ); } // ============================== // LEFT SECTION (GAUGE + SUMMARY) // ============================== Widget _buildLeftChartSection({ required double totalDue, required double pendingPercentage, required double totalCollected, }) { String pendingPercentStr = (pendingPercentage * 100).toStringAsFixed(0); String collectedPercentStr = ((1 - pendingPercentage) * 100).toStringAsFixed(0); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _GaugeChartPlaceholder( backgroundColor: Colors.white, pendingPercentage: pendingPercentage, ), const SizedBox(width: 12), ], ), const SizedBox(height: 20), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyLarge( '₹${totalDue.toStringAsFixed(0)} DUE', fontWeight: 700, ), const SizedBox(height: 4), MyText.bodySmall( '• Pending ($pendingPercentStr%) • Collected ($collectedPercentStr%)', color: Colors.black54, ), MyText.bodySmall( '₹${totalCollected.toStringAsFixed(0)} Collected', color: Colors.black54, ), ], ), ), ], ), ], ); } // ============================== // RIGHT METRICS SECTION // ============================== Widget _buildRightMetricsSection({ required CollectionOverviewData data, required double dsoDays, }) { final String topClientName = data.topClient?.name ?? 'N/A'; final double topClientBalance = data.topClientBalance; return Column( children: [ _buildMetricCard( title: 'Top Client Balance', value: topClientName, subValue: '₹${topClientBalance.toStringAsFixed(0)}', valueColor: Colors.red, isDetailed: true, ), const SizedBox(height: 10), _buildMetricCard( title: 'Total Collected (YTD)', value: '₹${data.totalCollectedAmount.toStringAsFixed(0)}', subValue: 'Collected', valueColor: Colors.green, isDetailed: false, ), ], ); } Widget _buildMetricCard({ required String title, required String value, required String subValue, required Color valueColor, required bool isDetailed, }) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodySmall(title, color: Colors.black54), const SizedBox(height: 2), if (isDetailed) ...[ MyText.bodySmall(value, fontWeight: 600), MyText.bodyMedium(subValue, color: valueColor, fontWeight: 700), ] else Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MyText.bodySmall(value, fontWeight: 600), MyText.bodySmall(subValue, color: valueColor, fontWeight: 600), ], ), ], ), ); } // ============================== // 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), ]; final double totalOutstanding = buckets.fold(0, (sum, b) => sum + b.amount); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium('Outstanding Collections Aging Analysis', fontWeight: 700), MyText.bodySmall( 'Total Outstanding: ₹${totalOutstanding.toStringAsFixed(0)}', color: Colors.black54), const SizedBox(height: 10), _AgingStackedBar(buckets: buckets, totalOutstanding: totalOutstanding), const SizedBox(height: 15), Wrap( spacing: 12, runSpacing: 8, children: buckets .map((bucket) => _buildAgingLegendItem(bucket.title, bucket.amount, bucket.color, bucket.invoiceCount)) .toList(), ), ], ); } Widget _buildAgingLegendItem( String title, double amount, Color color, int count) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), const SizedBox(width: 6), MyText.bodySmall( '$title: ₹${amount.toStringAsFixed(0)} ($count Invoices)'), ], ); } BoxDecoration _boxDecoration() { return BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], ); } } // ===================================================================== // CUSTOM PLACEHOLDER WIDGETS (Gauge + Bar/Area + Aging Bars) // ===================================================================== // Gauge Chart class _GaugeChartPlaceholder extends StatelessWidget { final Color backgroundColor; final double pendingPercentage; const _GaugeChartPlaceholder({ required this.backgroundColor, required this.pendingPercentage, }); @override Widget build(BuildContext context) { return SizedBox( width: 120, height: 80, child: Stack( children: [ CustomPaint( size: const Size(120, 70), painter: _SemiCirclePainter( canvasColor: backgroundColor, pendingPercentage: pendingPercentage, ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 8), child: FittedBox( child: MyText.bodySmall('RISK LEVEL', fontWeight: 600), ), ), ), ], ), ); } } class _SemiCirclePainter extends CustomPainter { final Color canvasColor; final double pendingPercentage; _SemiCirclePainter( {required this.canvasColor, required this.pendingPercentage}); @override void paint(Canvas canvas, Size size) { final rect = Rect.fromCircle( center: Offset(size.width / 2, size.height), radius: size.width / 2, ); const double arc = 3.14159; final double pendingSweep = arc * pendingPercentage; final double collectedSweep = arc * (1 - pendingPercentage); final backgroundPaint = Paint() ..color = Colors.black.withOpacity(0.1) ..strokeWidth = 10 ..style = PaintingStyle.stroke; canvas.drawArc(rect, arc, arc, false, backgroundPaint); final pendingPaint = Paint() ..strokeWidth = 10 ..style = PaintingStyle.stroke ..shader = const LinearGradient( colors: [Colors.orange, Colors.red], ).createShader(rect); canvas.drawArc(rect, arc, pendingSweep, false, pendingPaint); final collectedPaint = Paint() ..color = Colors.green ..strokeWidth = 10 ..style = PaintingStyle.stroke; canvas.drawArc( rect, arc + pendingSweep, collectedSweep, false, collectedPaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } // AGING BUCKET class AgingBucketData { final String title; final double amount; final Color color; final int invoiceCount; // ADDED // UPDATED CONSTRUCTOR AgingBucketData(this.title, this.amount, this.color, this.invoiceCount); } class _AgingStackedBar extends StatelessWidget { final List buckets; final double totalOutstanding; const _AgingStackedBar({ required this.buckets, required this.totalOutstanding, }); @override Widget build(BuildContext context) { if (totalOutstanding == 0) { return Container( height: 16, decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(8), ), child: Center( child: MyText.bodySmall('No Outstanding Collections', color: Colors.black54), ), ); } return ClipRRect( borderRadius: BorderRadius.circular(8), child: Row( children: buckets.where((b) => b.amount > 0).map((bucket) { final flexValue = bucket.amount / totalOutstanding; return Expanded( flex: (flexValue * 1000).toInt(), child: Container(height: 16, color: bucket.color), ); }).toList(), ), ); } }