import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:line_icons/line_icons.dart'; import 'package:intl/intl.dart'; import '../../../../common/theme/theme.dart'; import '../../../../domain/analytic/analytic.dart'; class FinanceCashFlow extends StatelessWidget { final List dailyData; const FinanceCashFlow({super.key, required this.dailyData}); @override Widget build(BuildContext context) { // Calculate totals from daily data final totalCashIn = _calculateTotalCashIn(); final totalCashOut = _calculateTotalCashOut(); final netFlow = totalCashIn - totalCashOut; return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppColor.textLight.withOpacity(0.1), spreadRadius: 1, blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( gradient: LinearGradient( colors: AppColor.primaryGradient, ), borderRadius: BorderRadius.circular(8), ), child: const Icon( LineIcons.areaChart, color: AppColor.white, size: 20, ), ), const SizedBox(width: 12), Text( 'Analisis Cash Flow', style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold), ), ], ), IconButton( onPressed: () {}, icon: const Icon( LineIcons.alternateExternalLink, color: AppColor.primary, ), ), ], ), const SizedBox(height: 20), // Cash Flow Indicators Row( children: [ Expanded( child: _buildCashFlowIndicator( 'Cash In', _formatCurrency(totalCashIn), LineIcons.arrowUp, AppColor.success, ), ), const SizedBox(width: 16), Expanded( child: _buildCashFlowIndicator( 'Cash Out', _formatCurrency(totalCashOut), LineIcons.arrowDown, AppColor.error, ), ), const SizedBox(width: 16), Expanded( child: _buildCashFlowIndicator( 'Net Flow', _formatCurrency(netFlow), LineIcons.equals, AppColor.info, ), ), ], ), const SizedBox(height: 20), // FL Chart Implementation Container( height: 200, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColor.background, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColor.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Grafik Cash Flow ${dailyData.length} Hari Terakhir', style: AppStyle.sm.copyWith( color: AppColor.textSecondary, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), Expanded( child: dailyData.isEmpty ? _buildEmptyChart() : LineChart(_buildLineChartData()), ), const SizedBox(height: 12), // Legend Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildChartLegend('Cash In', AppColor.success), const SizedBox(width: 20), _buildChartLegend('Cash Out', AppColor.error), const SizedBox(width: 20), _buildChartLegend('Net Flow', AppColor.info), ], ), ], ), ), ], ), ); } LineChartData _buildLineChartData() { final maxValue = _getMaxChartValue(); final minValue = _getMinChartValue(); return LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: (maxValue / 5).roundToDouble(), getDrawingHorizontalLine: (value) { return FlLine(color: AppColor.borderLight, strokeWidth: 1); }, ), titlesData: FlTitlesData( show: true, rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, interval: 1, getTitlesWidget: (double value, TitleMeta meta) { final index = value.toInt(); if (index >= 0 && index < dailyData.length) { final date = DateTime.parse(dailyData[index].date); final dayName = _getDayName(date.weekday); return SideTitleWidget( meta: meta, child: Text( dayName, style: const TextStyle( color: AppColor.textSecondary, fontWeight: FontWeight.w500, fontSize: 10, ), ), ); } return SideTitleWidget(meta: meta, child: Text('')); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: (maxValue / 3).roundToDouble(), reservedSize: 42, getTitlesWidget: (double value, TitleMeta meta) { return Text( _formatChartValue(value), style: const TextStyle( color: AppColor.textSecondary, fontWeight: FontWeight.w500, fontSize: 10, ), textAlign: TextAlign.left, ); }, ), ), ), borderData: FlBorderData( show: true, border: Border.all(color: AppColor.borderLight), ), minX: 0, maxX: (dailyData.length - 1).toDouble(), minY: minValue, maxY: maxValue, lineBarsData: [ // Cash In Line (Revenue) LineChartBarData( spots: _buildCashInSpots(), isCurved: true, gradient: LinearGradient( colors: [AppColor.success.withOpacity(0.8), AppColor.success], ), barWidth: 3, isStrokeCapRound: true, dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: 4, color: AppColor.success, strokeWidth: 2, strokeColor: AppColor.white, ); }, ), belowBarData: BarAreaData( show: true, gradient: LinearGradient( colors: [ AppColor.success.withOpacity(0.1), AppColor.success.withOpacity(0.0), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), ), // Cash Out Line (Total Cost) LineChartBarData( spots: _buildCashOutSpots(), isCurved: true, gradient: LinearGradient( colors: [AppColor.error.withOpacity(0.8), AppColor.error], ), barWidth: 3, isStrokeCapRound: true, dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: 4, color: AppColor.error, strokeWidth: 2, strokeColor: AppColor.white, ); }, ), ), // Net Flow Line (Net Profit) LineChartBarData( spots: _buildNetFlowSpots(), isCurved: true, gradient: LinearGradient( colors: [AppColor.info.withOpacity(0.8), AppColor.info], ), barWidth: 3, isStrokeCapRound: true, dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: 4, color: AppColor.info, strokeWidth: 2, strokeColor: AppColor.white, ); }, ), ), ], ); } Widget _buildEmptyChart() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( LineIcons.lineChart, size: 48, color: AppColor.textSecondary.withOpacity(0.3), ), const SizedBox(height: 12), Text( 'Tidak ada data untuk ditampilkan', style: AppStyle.sm.copyWith(color: AppColor.textSecondary), ), ], ), ); } // Helper methods for calculating data int _calculateTotalCashIn() { return dailyData.fold(0, (sum, data) => sum + data.revenue); } int _calculateTotalCashOut() { return dailyData.fold( 0, (sum, data) => sum + data.cost + data.tax + data.discount, ); } double _getMaxChartValue() { if (dailyData.isEmpty) return 30000000; final maxRevenue = dailyData .map((e) => e.revenue) .reduce((a, b) => a > b ? a : b); final maxCost = dailyData .map((e) => e.cost + e.tax + e.discount) .reduce((a, b) => a > b ? a : b); final maxValue = maxRevenue > maxCost ? maxRevenue : maxCost; return (maxValue * 1.2).toDouble(); // Add 20% padding } double _getMinChartValue() { if (dailyData.isEmpty) return -5000000; final minNetProfit = dailyData .map((e) => e.netProfit) .reduce((a, b) => a < b ? a : b); return minNetProfit < 0 ? (minNetProfit * 1.2).toDouble() : 0; } List _buildCashInSpots() { return dailyData.asMap().entries.map((entry) { return FlSpot(entry.key.toDouble(), entry.value.revenue.toDouble()); }).toList(); } List _buildCashOutSpots() { return dailyData.asMap().entries.map((entry) { final totalCost = entry.value.cost + entry.value.tax + entry.value.discount; return FlSpot(entry.key.toDouble(), totalCost.toDouble()); }).toList(); } List _buildNetFlowSpots() { return dailyData.asMap().entries.map((entry) { return FlSpot(entry.key.toDouble(), entry.value.netProfit.toDouble()); }).toList(); } String _getDayName(int weekday) { switch (weekday) { case 1: return 'Sen'; case 2: return 'Sel'; case 3: return 'Rab'; case 4: return 'Kam'; case 5: return 'Jum'; case 6: return 'Sab'; case 7: return 'Min'; default: return ''; } } String _formatChartValue(double value) { if (value.abs() >= 1000000) { return '${(value / 1000000).toStringAsFixed(0)}M'; } else if (value.abs() >= 1000) { return '${(value / 1000).toStringAsFixed(0)}K'; } else { return value.toStringAsFixed(0); } } String _formatCurrency(int amount) { if (amount.abs() >= 1000000000) { return 'Rp ${(amount / 1000000000).toStringAsFixed(1)}B'; } else if (amount.abs() >= 1000000) { return 'Rp ${(amount / 1000000).toStringAsFixed(1)}M'; } else if (amount.abs() >= 1000) { return 'Rp ${(amount / 1000).toStringAsFixed(1)}K'; } else { return 'Rp ${NumberFormat('#,###', 'id_ID').format(amount)}'; } } Widget _buildChartLegend(String label, Color color) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 12, height: 12, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), Text( label, style: AppStyle.xs.copyWith( color: AppColor.textSecondary, fontWeight: FontWeight.w500, ), ), ], ); } Widget _buildCashFlowIndicator( String label, String amount, IconData icon, Color color, ) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.2)), ), child: Column( children: [ Icon(icon, color: color, size: 20), const SizedBox(height: 8), Text( label, style: AppStyle.xs.copyWith(color: AppColor.textSecondary), ), const SizedBox(height: 4), Text( amount, style: AppStyle.md.copyWith( fontWeight: FontWeight.bold, color: color, ), ), ], ), ); } }