// collections_health_widget.dart import 'package:flutter/material.dart'; // --- MAIN WIDGET FILE --- class CollectionsHealthWidget extends StatelessWidget { @override Widget build(BuildContext context) { // Derived Metrics from the JSON Analysis: const double totalDue = 34190.0; const double totalCollected = 5000.0; const double totalValue = totalDue + totalCollected; // Calculate Pending Percentage for Gauge final double pendingPercentage = totalValue > 0 ? totalDue / totalValue : 0.0; // 1. MAIN CARD CONTAINER (White Theme) return Container( decoration: BoxDecoration( // Main card background color: WHITE color: Colors.white, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( // Lighter shadow for white background color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], ), padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // 1. HEADER _buildHeader(), const SizedBox(height: 20), // 2. MAIN CONTENT ROW (Layout) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Left Section: Gauge Chart, Due Amount, & Timelines Expanded( flex: 5, child: _buildLeftChartSection( totalDue: totalDue, pendingPercentage: pendingPercentage, ), ), const SizedBox(width: 16), // Right Section: Metric Cards Expanded( flex: 4, child: _buildRightMetricsSection( totalCollected: totalCollected, ), ), ], ), const SizedBox(height: 20), // 3. NEW: AGING ANALYSIS SECTION _buildAgingAnalysis(), ], ), ); } // --- HELPER METHOD 1: HEADER --- Widget _buildHeader() { return Row( children: const [ Text( 'Collections Health Overview', style: TextStyle( // Text color: Dark for contrast on white background color: Colors.black87, fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ); } // --- HELPER METHOD 2: LEFT SECTION (CHARTS) --- Widget _buildLeftChartSection({ required double totalDue, required double pendingPercentage, }) { // Format the percentage for display String pendingPercentStr = (pendingPercentage * 100).toStringAsFixed(0); // Use the derived totalCollected for a better context const double totalCollected = 5000.0; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Top: Gauge Chart and Due Amount Row( children: [ // Use White-Theme Gauge Placeholder with calculated percentage _GaugeChartPlaceholder( backgroundColor: Colors.white, pendingPercentage: pendingPercentage), const SizedBox(width: 12), ], ), const SizedBox(height: 20), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '₹${totalDue.toStringAsFixed(0)} DUE', style: const TextStyle( // Text color: Dark for contrast color: Colors.black87, fontSize: 22, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '• Pending ($pendingPercentStr%) • ₹${totalCollected.toStringAsFixed(0)} Collected', // Sub-text color: Lighter dark color style: const TextStyle(color: Colors.black54, fontSize: 12), ), ], ), ), ], ), const SizedBox(height: 20), // Bottom: Timeline Charts (Trend Analysis) Row( children: [ // Expected Collections Timeline (Bar Chart Placeholder) Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Expected Collections Trend', // Text color: Dark for contrast style: TextStyle(color: Colors.black87, fontSize: 12)), const SizedBox(height: 8), _TimelineChartPlaceholder( isBar: true, barColor: const Color(0xFF2196F3)), // Blue Bar Chart const Text('Week 16 Nov 2025', style: TextStyle(color: Colors.black54, fontSize: 10)), ], ), ), const SizedBox(width: 10), // Collection Rate Trend (Area Chart Placeholder for Analysis) Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Collection Rate Trend', // Text color: Dark for contrast style: TextStyle(color: Colors.black87, fontSize: 12)), const SizedBox(height: 8), _TimelineChartPlaceholder( isBar: false, areaColor: const Color(0xFF4CAF50)), // Green Area Chart (Rate) const Text('Week 14 Nov 2025', style: TextStyle(color: Colors.black54, fontSize: 10)), ], ), ), ], ), ], ); } // --- HELPER METHOD 3: RIGHT SECTION (METRICS) --- Widget _buildRightMetricsSection({ required double totalCollected, }) { return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Metric Card 1: Top Client (From Invoice 1) _buildMetricCard( title: 'Top Client Balance', value: 'Peninsula Land Limited', subValue: '₹34,190', valueColor: const Color(0xFFF44336), // Red (Pending/Due) isDetailed: true, ), const SizedBox(height: 10), // Metric Card 2 (NEW): Total Collected (From Invoice 2) _buildMetricCard( title: 'Total Collected (YTD)', value: '₹${totalCollected.toStringAsFixed(0)}', subValue: 'Collected', valueColor: const Color(0xFF4CAF50), // Green (Positive Value) isDetailed: false, ), const SizedBox(height: 10), // Metric Card 3: Days Sales Outstanding (DSO) (Analytical Placeholder) _buildMetricCard( title: 'Days Sales Outstanding (DSO)', value: '45 Days', subValue: '↑ 5 Days', valueColor: const Color(0xFFFF9800), // Orange (Needs improvement) isDetailed: false, ), const SizedBox(height: 10), // Metric Card 4: Last Metric Card in the image (Repeating rate for layout) _buildMetricCard( title: 'Bad Debt Ratio', value: '0.8%', subValue: '↓ 0.2%', valueColor: const Color(0xFF4CAF50), // Green (Positive Change) isDetailed: false, ), ], ); } // --- HELPER METHOD 4: METRIC CARD WIDGET (Unchanged) --- 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( // Light grey background for card separation on white theme color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, // Title text color: Soft dark grey style: const TextStyle(color: Colors.black54, fontSize: 10), ), const SizedBox(height: 2), // Conditional rendering for the detailed vs simple card structure if (isDetailed) ...[ Text( value, // Client Name style: const TextStyle( // Value text color: Dark color: Colors.black87, fontSize: 12, fontWeight: FontWeight.w600, ), ), Text( subValue, // Due Amount style: TextStyle( // Color remains dynamic (Red/Green/etc.) color: valueColor, fontSize: 16, fontWeight: FontWeight.bold, ), ), ] else Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( value, style: const TextStyle( // Value text color: Dark color: Colors.black87, fontSize: 14, fontWeight: FontWeight.w600, ), ), Text( subValue, style: TextStyle( // Color remains dynamic (Red/Green/etc.) color: valueColor, fontSize: 14, fontWeight: FontWeight.w600, ), ), ], ), ], ), ); } // --- NEW HELPER METHOD: AGING ANALYSIS --- // --- NEW HELPER METHOD: AGING ANALYSIS (MODIFIED) --- Widget _buildAgingAnalysis() { // Hardcoded data based on the JSON analysis const double due0to20Days = 0.0; const double due20to45Days = 34190.0; const double due45to90Days = 0.0; const double dueOver90Days = 0.0; final double totalOutstanding = due0to20Days + due20to45Days + due45to90Days + dueOver90Days; // Define buckets with their risk color final List buckets = [ AgingBucketData('0-20 Days', due0to20Days, const Color(0xFF4CAF50)), // Green (Low Risk) AgingBucketData('20-45 Days', due20to45Days, const Color(0xFFFF9800)), // Orange (Medium Risk) AgingBucketData('45-90 Days', due45to90Days, const Color(0xFFF44336).withOpacity(0.7)), // Light Red (High Risk) AgingBucketData('> 90 Days', dueOver90Days, const Color(0xFFF44336)), // Dark Red (Very High Risk) ]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Outstanding Collections Aging Analysis', style: TextStyle( color: Colors.black87, fontSize: 14, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 10), // NEW: STACKED BAR VISUALIZATION _AgingStackedBar( buckets: buckets, totalOutstanding: totalOutstanding, ), const SizedBox(height: 15), // NEW: LEGEND/BUCKET DETAILS (Replaces the old _buildAgingBucket layout) Wrap( spacing: 12, runSpacing: 8, children: buckets .map((bucket) => _buildAgingLegendItem( bucket.title, bucket.amount, bucket.color)) .toList(), ), ], ); } // Keep the old _buildAgingBucket helper and rename it to a legend item // This is more efficient than recreating the logic Widget _buildAgingLegendItem(String title, double amount, Color color) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 6), Text( '${title}: ₹${amount.toStringAsFixed(0)}', style: const TextStyle(color: Colors.black87, fontSize: 12), ), ], ); } } // --- CUSTOM PAINTERS / PLACEHOLDERS (Modified to accept percentage) --- // Placeholder for the Semi-Circle 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: [ // BACKGROUND GAUGE CustomPaint( size: const Size(120, 70), // Pass the pending percentage to the painter painter: _SemiCirclePainter( canvasColor: backgroundColor, pendingPercentage: pendingPercentage), ), // CENTER CIRCLE Positioned( bottom: 0, left: 30, child: Container( height: 60, width: 60, decoration: BoxDecoration( // Center circle background is the same as the main card background (white) color: backgroundColor, shape: BoxShape.circle, border: Border.all(color: Colors.black.withOpacity(0.1)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ // Icon color: Dark Icon(Icons.work, color: Colors.black87, size: 20), SizedBox(height: 2), Text( 'RISK LEVEL', // Text color: Soft dark grey style: TextStyle(color: Colors.black54, fontSize: 8), ), ], ), ), ), ], ), ); } } // Painter for the semi-circular gauge chart visualization (Simplified) 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); // Radians for a semi-circle const double totalArc = 3.14159; // Calculate the sweep angles final double pendingSweepAngle = totalArc * pendingPercentage; final double collectedSweepAngle = totalArc * (1.0 - pendingPercentage); // Background Arc (Full semi-circle) final backgroundPaint = Paint() // Background arc is a soft grey on a white theme ..color = Colors.black.withOpacity(0.1) ..style = PaintingStyle.stroke ..strokeWidth = 10; canvas.drawArc(rect, totalArc, totalArc, false, backgroundPaint); // Start at 180 deg (Pi) // Pending Arc (Red/Orange segment) final pendingPaint = Paint() ..color = const Color(0xFFF44336) // Red ..style = PaintingStyle.stroke ..strokeWidth = 10 ..shader = const LinearGradient(colors: [ Color(0xFFFF9800), // Orange Color(0xFFF44336), // Red ]).createShader(rect); // Start angle: 3.14 (180 deg) canvas.drawArc(rect, totalArc, pendingSweepAngle, false, pendingPaint); // Collected Arc (Green segment) final collectedPaint = Paint() ..color = const Color(0xFF4CAF50) // Green ..style = PaintingStyle.stroke ..strokeWidth = 10; // Start angle: 3.14 + pendingSweepAngle canvas.drawArc(rect, totalArc + pendingSweepAngle, collectedSweepAngle, false, collectedPaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } // Placeholder for the Bar/Area Charts (Unchanged) class _TimelineChartPlaceholder extends StatelessWidget { final bool isBar; final Color? barColor; final Color? areaColor; const _TimelineChartPlaceholder( {required this.isBar, this.barColor, this.areaColor}); @override Widget build(BuildContext context) { return Container( height: 50, width: double.infinity, color: Colors.transparent, // Transparent container for visual space child: isBar ? _BarChartVisual(barColor: barColor!) : _AreaChartVisual(areaColor: areaColor!), ); } } class _BarChartVisual extends StatelessWidget { final Color barColor; const _BarChartVisual({required this.barColor}); @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _Bar(0.4, barColor), _Bar(0.7, barColor), _Bar(1.0, barColor), _Bar(0.6, barColor), _Bar(0.8, barColor), ], ); } } class _Bar extends StatelessWidget { final double heightFactor; final Color color; const _Bar(this.heightFactor, this.color); @override Widget build(BuildContext context) { return Container( width: 8, height: 50 * heightFactor, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(5), ), ); } } class _AreaChartVisual extends StatelessWidget { final Color areaColor; const _AreaChartVisual({required this.areaColor}); @override Widget build(BuildContext context) { return CustomPaint( painter: _AreaChartPainter(areaColor: areaColor), size: const Size(double.infinity, 50), ); } } class _AreaChartPainter extends CustomPainter { final Color areaColor; _AreaChartPainter({required this.areaColor}); @override void paint(Canvas canvas, Size size) { final points = [ Offset(0, size.height * 0.5), Offset(size.width * 0.25, size.height * 0.8), Offset(size.width * 0.5, size.height * 0.3), Offset(size.width * 0.75, size.height * 0.9), Offset(size.width, size.height * 0.4), ]; // Path for the area final path = Path() ..moveTo(points.first.dx, size.height) ..lineTo(points.first.dx, points.first.dy); for (int i = 1; i < points.length; i++) { path.lineTo(points[i].dx, points[i].dy); } path.lineTo(points.last.dx, size.height); path.close(); // Paint for the gradient fill final areaPaint = Paint() ..shader = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ areaColor.withOpacity(0.5), // Lighter opacity on white theme areaColor.withOpacity(0.0), // Transparent ], ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)) ..style = PaintingStyle.fill; canvas.drawPath(path, areaPaint); // Paint for the line stroke final linePaint = Paint() ..color = areaColor ..strokeWidth = 2 ..style = PaintingStyle.stroke; canvas.drawPath(Path()..addPolygon(points, false), linePaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } // --- NEW DATA MODEL --- class AgingBucketData { final String title; final double amount; final Color color; AgingBucketData(this.title, this.amount, this.color); } // --- NEW HELPER WIDGET: STACKED BAR VISUAL --- 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: const Center( child: Text( 'No Outstanding Collections', style: TextStyle(color: Colors.black54, fontSize: 10), ), ), ); } // Build the segments for the Row final List segments = buckets.where((b) => b.amount > 0).map((bucket) { final double flexValue = bucket.amount / totalOutstanding; return Expanded( flex: (flexValue * 100).toInt(), // Use a scaled integer for flex child: Container( height: 16, color: bucket.color, ), ); }).toList(); return ClipRRect( borderRadius: BorderRadius.circular(8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: segments, ), ); } }