import 'package:flutter/material.dart'; import 'package:on_field_work/helpers/widgets/my_text.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( 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: 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. AGING ANALYSIS SECTION _buildAgingAnalysis(), ], ), ); } // --- HELPER METHOD 1: HEADER --- Widget _buildHeader() { return Row(mainAxisAlignment: MainAxisAlignment.start, 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, ), ], ), ), ]); } // --- 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 Row( children: [ _GaugeChartPlaceholder( backgroundColor: Colors.white, pendingPercentage: pendingPercentage, ), const SizedBox(width: 12), ], ), const SizedBox(height: 20), // Total Due + Summary 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%) • ₹${totalCollected.toStringAsFixed(0)} Collected', color: Colors.black54, ), ], ), ), ], ), const SizedBox(height: 20), // Bottom: Timeline Charts (Trend Analysis) Row( children: [ // Expected Collections Timeline (Bar Chart Placeholder) Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodySmall( 'Expected Collections Trend', ), const SizedBox(height: 8), const _TimelineChartPlaceholder( isBar: true, barColor: Color(0xFF2196F3), ), MyText.bodySmall( 'Week 16 Nov 2025', color: Colors.black54, ), ], ), ), const SizedBox(width: 10), // Collection Rate Trend (Area Chart Placeholder) Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodySmall( 'Collection Rate Trend', ), const SizedBox(height: 8), const _TimelineChartPlaceholder( isBar: false, areaColor: Color(0xFF4CAF50), ), MyText.bodySmall( 'Week 14 Nov 2025', color: Colors.black54, ), ], ), ), ], ), ], ); } // --- HELPER METHOD 3: RIGHT SECTION (METRICS) --- Widget _buildRightMetricsSection({ required double totalCollected, }) { return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Metric Card 1: Top Client _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: Total Collected (YTD) _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: DSO _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: Bad Debt Ratio _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 --- 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, ), ], ), ], ), ); } // --- NEW HELPER METHOD: AGING ANALYSIS --- Widget _buildAgingAnalysis() { // Hardcoded data 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 ), AgingBucketData( '> 90 Days', dueOver90Days, const Color(0xFFF44336), // Dark Red ), ]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium( 'Outstanding Collections Aging Analysis', fontWeight: 700, ), const SizedBox(height: 10), // Stacked bar visualization _AgingStackedBar( buckets: buckets, totalOutstanding: totalOutstanding, ), const SizedBox(height: 15), // Legend / Bucket details Wrap( spacing: 12, runSpacing: 8, children: buckets .map( (bucket) => _buildAgingLegendItem( bucket.title, bucket.amount, bucket.color, ), ) .toList(), ), ], ); } // Legend item for aging buckets 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), MyText.bodySmall( '$title: ₹${amount.toStringAsFixed(0)}', ), ], ); } } // --- CUSTOM PAINTERS / PLACEHOLDERS --- // 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), painter: _SemiCirclePainter( canvasColor: backgroundColor, pendingPercentage: pendingPercentage, ), ), // CENTER TEXT Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 8), child: FittedBox( child: MyText.bodySmall( 'RISK LEVEL', fontWeight: 600, ), ), ), ), ], ), ); } } // Painter for the semi-circular gauge chart visualization 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 totalArc = 3.14159; final double pendingSweepAngle = totalArc * pendingPercentage; final double collectedSweepAngle = totalArc * (1.0 - pendingPercentage); // Background Arc final backgroundPaint = Paint() ..color = Colors.black.withOpacity(0.1) ..style = PaintingStyle.stroke ..strokeWidth = 10; canvas.drawArc(rect, totalArc, totalArc, false, backgroundPaint); // Pending Arc final pendingPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 10 ..shader = const LinearGradient( colors: [ Color(0xFFFF9800), Color(0xFFF44336), ], ).createShader(rect); canvas.drawArc(rect, totalArc, pendingSweepAngle, false, pendingPaint); // Collected Arc final collectedPaint = Paint() ..color = const Color(0xFF4CAF50) ..style = PaintingStyle.stroke ..strokeWidth = 10; canvas.drawArc( rect, totalArc + pendingSweepAngle, collectedSweepAngle, false, collectedPaint, ); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } // Placeholder for the Bar/Area Charts 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, 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: const [ _Bar(0.4), _Bar(0.7), _Bar(1.0), _Bar(0.6), _Bar(0.8), ], ); } } class _Bar extends StatelessWidget { final double heightFactor; const _Bar(this.heightFactor); @override Widget build(BuildContext context) { // Bar color is taken from parent via DefaultTextStyle/Theme if needed; // you can wrap with Theme if you want dynamic colors. return Container( width: 8, height: 50 * heightFactor, decoration: BoxDecoration( color: Colors.grey.shade400, 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), ]; 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(); final areaPaint = Paint() ..shader = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ areaColor.withOpacity(0.5), areaColor.withOpacity(0.0), ], ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)) ..style = PaintingStyle.fill; canvas.drawPath(path, areaPaint); final linePaint = Paint() ..color = areaColor ..strokeWidth = 2 ..style = PaintingStyle.stroke; canvas.drawPath(Path()..addPolygon(points, false), linePaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } // --- DATA MODEL --- class AgingBucketData { final String title; final double amount; final Color color; AgingBucketData(this.title, this.amount, this.color); } // --- 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: Center( child: MyText.bodySmall( 'No Outstanding Collections', color: Colors.black54, ), ), ); } final List segments = buckets.where((b) => b.amount > 0).map((bucket) { final double flexValue = bucket.amount / totalOutstanding; return Expanded( flex: (flexValue * 100).toInt(), child: Container( height: 16, color: bucket.color, ), ); }).toList(); return ClipRRect( borderRadius: BorderRadius.circular(8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: segments, ), ); } }