import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import '../../../../common/extension/extension.dart'; import '../../../../common/theme/theme.dart'; import '../../../../domain/analytic/analytic.dart'; import '../../../components/spacer/spacer.dart'; import '../../../components/widgets/empty_widget.dart'; class ReportSales extends StatelessWidget { final List salesData; const ReportSales({super.key, required this.salesData}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: AppColor.surface, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: AppColor.textSecondary.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Section Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.lang.sales_chart, style: AppStyle.xxl.copyWith( color: AppColor.textPrimary, fontWeight: FontWeight.bold, ), ), const SpaceHeight(4), Text( salesData.isEmpty ? context.lang.no_data_available : context.lang.total_days_overview(salesData.length), style: AppStyle.md.copyWith(color: AppColor.textSecondary), ), ], ), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColor.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.show_chart, color: AppColor.primary, size: 24, ), ), ], ), const SpaceHeight(20), // Sales Summary Cards if (salesData.isNotEmpty) ...[ _buildSalesSummary(context), const SpaceHeight(20), ], // Chart Container salesData.isEmpty ? _buildEmptyChart(context) : Container( height: 300, padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColor.primary.withOpacity(0.05), AppColor.backgroundLight, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColor.primary.withOpacity(0.1), width: 2, ), ), child: _buildSalesChart(), ), const SpaceHeight(16), // Legend if (salesData.isNotEmpty) Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildLegendItem(context.lang.sales_data, AppColor.primary), ], ), ], ), ); } Widget _buildSalesSummary(BuildContext context) { final totalSales = salesData.fold(0, (sum, item) => sum + item.sales); final totalOrders = salesData.fold( 0, (sum, item) => sum + item.orders, ); final totalItems = salesData.fold(0, (sum, item) => sum + item.items); final totalNetSales = salesData.fold( 0, (sum, item) => sum + item.netSales, ); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColor.backgroundLight, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColor.border.withOpacity(0.3), width: 1), ), child: Column( children: [ Row( children: [ Expanded( child: _buildSummaryItem( context.lang.total_sales, totalSales.currencyFormatRp, Icons.attach_money, AppColor.success, ), ), Container( width: 1, height: 40, color: AppColor.border.withOpacity(0.3), margin: const EdgeInsets.symmetric(horizontal: 16), ), Expanded( child: _buildSummaryItem( context.lang.net_sales, totalNetSales.currencyFormatRp, Icons.trending_up, AppColor.primary, ), ), ], ), const SpaceHeight(16), Row( children: [ Expanded( child: _buildSummaryItem( context.lang.total_orders, totalOrders.toString(), Icons.shopping_cart, AppColor.info, ), ), Container( width: 1, height: 40, color: AppColor.border.withOpacity(0.3), margin: const EdgeInsets.symmetric(horizontal: 16), ), Expanded( child: _buildSummaryItem( context.lang.total_items, totalItems.toString(), Icons.inventory, AppColor.warning, ), ), ], ), ], ), ); } Widget _buildSummaryItem( String label, String value, IconData icon, Color color, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 16, color: color), const SpaceWidth(6), Expanded( child: Text( label, style: AppStyle.xs.copyWith( color: AppColor.textSecondary, fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SpaceHeight(6), Text( value, style: AppStyle.md.copyWith( color: color, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ); } Widget _buildSalesChart() { final maxValue = _getMaxValue(); final spots = _generateSpots(salesData); return LineChart( LineChartData( gridData: FlGridData( show: true, drawHorizontalLine: true, drawVerticalLine: false, horizontalInterval: maxValue / 5, getDrawingHorizontalLine: (value) { return FlLine( color: AppColor.border.withOpacity(0.3), strokeWidth: 1, dashArray: [5, 5], ); }, ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 70, getTitlesWidget: (value, meta) { return Text( _formatCurrency(value), style: AppStyle.xs.copyWith( color: AppColor.textSecondary, fontWeight: FontWeight.w500, ), ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 32, getTitlesWidget: (value, meta) { final index = value.toInt(); if (index >= 0 && index < salesData.length) { final date = DateTime.parse(salesData[index].date); final dayName = _getDayName(date.weekday); return Padding( padding: const EdgeInsets.only(top: 8), child: Text( dayName, style: AppStyle.xs.copyWith( color: AppColor.textSecondary, fontWeight: FontWeight.w500, ), ), ); } return const Text(''); }, ), ), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData(show: false), minX: 0, maxX: (salesData.length - 1).toDouble(), minY: 0, maxY: maxValue, lineBarsData: [ // Main sales line LineChartBarData( spots: spots, isCurved: true, curveSmoothness: 0.35, gradient: LinearGradient( colors: [AppColor.primary, AppColor.primaryLight], begin: Alignment.centerLeft, end: Alignment.centerRight, ), barWidth: 4, isStrokeCapRound: true, belowBarData: BarAreaData( show: true, gradient: LinearGradient( colors: [ AppColor.primary.withOpacity(0.3), AppColor.primary.withOpacity(0.1), Colors.transparent, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: 6, color: AppColor.surface, strokeWidth: 3, strokeColor: AppColor.primary, ); }, ), ), ], lineTouchData: LineTouchData( enabled: true, touchTooltipData: LineTouchTooltipData( tooltipPadding: const EdgeInsets.all(12), getTooltipItems: (List touchedBarSpots) { return touchedBarSpots .map((barSpot) { final index = barSpot.x.toInt(); if (index >= 0 && index < salesData.length) { final sale = salesData[index]; final date = DateTime.parse(sale.date); final dayName = _getDayName(date.weekday); return LineTooltipItem( '$dayName\n', const TextStyle( color: AppColor.textWhite, fontWeight: FontWeight.bold, fontSize: 14, ), children: [ TextSpan( text: 'Sales: ${sale.sales.currencyFormatRp}\n', style: AppStyle.sm.copyWith( color: AppColor.textWhite, fontWeight: FontWeight.w500, ), ), TextSpan( text: 'Orders: ${sale.orders}\n', style: AppStyle.sm.copyWith( color: AppColor.textWhite, fontWeight: FontWeight.w400, ), ), TextSpan( text: 'Net: ${sale.netSales.currencyFormatRp}', style: AppStyle.sm.copyWith( color: AppColor.textWhite, fontWeight: FontWeight.w400, ), ), ], ); } return null; }) .where((item) => item != null) .cast() .toList(); }, ), touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) { // Handle touch events here if needed }, handleBuiltInTouches: true, ), ), ); } Widget _buildEmptyChart(BuildContext context) { return EmptyWidget( title: context.lang.no_sales_data, message: context.lang.no_sales_data_desc, emptyIcon: Icons.show_chart, ); } List _generateSpots(List data) { return data.asMap().entries.map((entry) { final index = entry.key; final sale = entry.value; return FlSpot(index.toDouble(), sale.sales.toDouble()); }).toList(); } double _getMaxValue() { if (salesData.isEmpty) return 1000000; double maxValue = salesData .map((e) => e.sales.toDouble()) .reduce((a, b) => a > b ? a : b); // Add 20% padding to max value return maxValue * 1.2; } String _formatCurrency(double value) { if (value >= 1000000) { return '${(value / 1000000).toStringAsFixed(1)}M'; } else if (value >= 1000) { return '${(value / 1000).toStringAsFixed(0)}K'; } return value.toStringAsFixed(0); } String _getDayName(int weekday) { const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; return days[weekday - 1]; } Widget _buildLegendItem(String label, Color color) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 16, height: 3, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), ), ), const SpaceWidth(8), Text( label, style: AppStyle.sm.copyWith( color: AppColor.textSecondary, fontWeight: FontWeight.w500, ), ), ], ); } }